From 3e66cf951bc6872dc4b1afad5d32ab67417c0f01 Mon Sep 17 00:00:00 2001 From: leader Date: Thu, 22 Oct 2026 17:15:33 +0900 Subject: [PATCH 001/149] feat(screen-plan): follow the final design page; audit treats headless-hidden scrollbars as visible State-variant frames (Desktop 1441, Mobile 361/362, extra form states) fold into their route instead of becoming new screens, and remap moves a route to a newer frame. The runtime audit no longer reports overflow:auto tab strips and tables as clipped just because headless Chromium hides every scrollbar; only scrollbar-width:none still counts. --- .../changepack_log_jojyOPeLAlr8D8DSurB-c.json | 7 + .../src/codegen/responsive.rs | 1 + .../codegen/responsive/breakpoint_merge.rs | 2 +- .../src/codegen/responsive/width_label.rs | 19 + .../src/ui_validate/site.rs | 62 ++- .../tests/responsive_discovery.rs | 24 + .../devup-mcp-figma/src/asset_render_info.rs | 8 +- crates/devup-mcp-figma/src/scripts/search.js | 2 + crates/devup-mcp-figma/src/search.rs | 164 ++++++- .../src/server/asset_job_progress.rs | 31 ++ crates/devup-mcp/src/server/asset_jobs.rs | 385 +++------------- .../devup-mcp/src/server/asset_jobs_tests.rs | 429 ++++++++++++++++++ .../src/server/export_skill_gap_tests.rs | 30 ++ .../src/server/handlers/devup_figma_export.rs | 21 +- .../mod_parts/exploration_and_responses.rs | 24 + .../server/mod_parts/state_and_services.rs | 2 + .../devup-mcp/src/server/project_plan/erd.rs | 45 ++ .../devup-mcp/src/server/project_plan/mod.rs | 162 ++++++- .../src/server/project_plan/rounds.rs | 64 ++- .../src/server/project_plan/tests.rs | 82 ++++ .../src/server/project_plan/units.rs | 113 ++++- .../projection/asset_reference_names.rs | 8 +- .../src/server/projection/completion.rs | 128 +++++- .../server/projection/feedback_theme_tests.rs | 8 + .../project_node_and_theme_outputs.rs | 52 +++ .../projection/project_responsive_output.rs | 19 +- .../server/projection/resource_delivery.rs | 93 ++++ .../src/server/projection/theme_export.rs | 31 +- crates/devup-mcp/src/server/screen_plan.rs | 2 + .../src/server/screen_plan/actions.rs | 121 +++-- .../devup-mcp/src/server/screen_plan/draft.rs | 14 +- .../src/server/screen_plan/draft_plan_with.rs | 63 ++- .../server/screen_plan/plan_route_group.rs | 28 +- .../devup-mcp/src/server/screen_plan/remap.rs | 109 +++++ .../src/server/screen_plan/routes.rs | 61 ++- .../devup-mcp/src/server/screen_plan/state.rs | 10 +- .../src/server/screen_plan/state_variants.rs | 70 +++ .../src/server/screen_plan/status.rs | 75 ++- .../devup-mcp/src/server/screen_plan_tests.rs | 1 + ...oup_answer_can_name_one_route_per_frame.rs | 26 ++ .../frame_inventory_and_link_labels.rs | 184 ++++++++ .../screen_plan_tests/monorepo_root_plan.rs | 30 ++ .../server/screen_plan_tests/prevention.rs | 22 + .../server/skills/devfive-frontend/SKILL.md | 3 +- .../skills/devup-implementation-plan/SKILL.md | 13 +- .../src/server/skills/devup/SKILL.md | 22 +- .../src/server/skills/installation.rs | 21 +- .../devup-mcp/src/server/skills/manifest.json | 376 +++++++-------- crates/devup-mcp/src/server/skills/tests.rs | 20 + crates/devup-mcp/src/server/tools.rs | 9 +- crates/devup-mcp/src/server/validation.rs | 30 +- crates/devup-mcp/tests/bridge_first.rs | 2 +- crates/devup-mcp/tests/export_defaults.rs | 1 - harness/runtime-audit/checks.mjs | 15 +- harness/runtime-audit/run.mjs | 24 +- harness/runtime-audit/test/layout.test.mjs | 11 +- 56 files changed, 2702 insertions(+), 677 deletions(-) create mode 100644 .changepacks/changepack_log_jojyOPeLAlr8D8DSurB-c.json create mode 100644 crates/devup-mcp-devup-ui/src/codegen/responsive/width_label.rs create mode 100644 crates/devup-mcp/src/server/asset_job_progress.rs create mode 100644 crates/devup-mcp/src/server/asset_jobs_tests.rs create mode 100644 crates/devup-mcp/src/server/screen_plan/remap.rs create mode 100644 crates/devup-mcp/src/server/screen_plan/state_variants.rs create mode 100644 crates/devup-mcp/src/server/screen_plan_tests/monorepo_root_plan.rs diff --git a/.changepacks/changepack_log_jojyOPeLAlr8D8DSurB-c.json b/.changepacks/changepack_log_jojyOPeLAlr8D8DSurB-c.json new file mode 100644 index 00000000..52e8b4f8 --- /dev/null +++ b/.changepacks/changepack_log_jojyOPeLAlr8D8DSurB-c.json @@ -0,0 +1,7 @@ +{ + "changes": { + "Cargo.toml": "Patch" + }, + "note": "screen plan follows the final design page: state-variant frames fold into their route, remap moves routes to a newer frame; runtime audit treats overflow:auto scrollers as reachable when headless Chromium hides scrollbars", + "date": "2026-10-22T08:15:22.677086Z" +} \ No newline at end of file diff --git a/crates/devup-mcp-devup-ui/src/codegen/responsive.rs b/crates/devup-mcp-devup-ui/src/codegen/responsive.rs index 14e3f86b..97332bba 100644 --- a/crates/devup-mcp-devup-ui/src/codegen/responsive.rs +++ b/crates/devup-mcp-devup-ui/src/codegen/responsive.rs @@ -24,5 +24,6 @@ // Source sections share this module to preserve privacy and test names. include!("responsive/breakpoint_merge.rs"); +include!("responsive/width_label.rs"); include!("responsive/merge_content.rs"); include!("responsive/impl.rs"); diff --git a/crates/devup-mcp-devup-ui/src/codegen/responsive/breakpoint_merge.rs b/crates/devup-mcp-devup-ui/src/codegen/responsive/breakpoint_merge.rs index 555f0703..8d65cca8 100644 --- a/crates/devup-mcp-devup-ui/src/codegen/responsive/breakpoint_merge.rs +++ b/crates/devup-mcp-devup-ui/src/codegen/responsive/breakpoint_merge.rs @@ -281,7 +281,7 @@ pub fn is_breakpoint_name(name: &str) -> bool { /// Explicit width suffixes can describe one named screen. No fuzzy matching. pub fn family_name(name: &str) -> String { - let name = name.trim().to_ascii_lowercase(); + let name = strip_width_label(name.trim().to_ascii_lowercase()); if is_breakpoint_name(&name) { return "@breakpoint-labels".into(); } diff --git a/crates/devup-mcp-devup-ui/src/codegen/responsive/width_label.rs b/crates/devup-mcp-devup-ui/src/codegen/responsive/width_label.rs new file mode 100644 index 00000000..0c6100e9 --- /dev/null +++ b/crates/devup-mcp-devup-ui/src/codegen/responsive/width_label.rs @@ -0,0 +1,19 @@ +/// Drops a trailing width label after a device word: `desktop 1440`, +/// `mobile (360px)`. Names without a device word keep their trailing number. +fn strip_width_label(name: String) -> String { + let trimmed = name.trim_end_matches([')', ' ']); + let trimmed = trimmed.strip_suffix("px").unwrap_or(trimmed); + let without_digits = trimmed.trim_end_matches(|c: char| c.is_ascii_digit()); + if without_digits.len() == trimmed.len() { + return name; + } + let base = without_digits.trim_end_matches([' ', '-', '/', '(', '_']); + if ["desktop", "tablet", "mobile"] + .iter() + .any(|device| base.ends_with(device)) + { + base.to_owned() + } else { + name + } +} diff --git a/crates/devup-mcp-devup-ui/src/ui_validate/site.rs b/crates/devup-mcp-devup-ui/src/ui_validate/site.rs index 828f5fe1..f54a8e60 100644 --- a/crates/devup-mcp-devup-ui/src/ui_validate/site.rs +++ b/crates/devup-mcp-devup-ui/src/ui_validate/site.rs @@ -98,6 +98,8 @@ struct Element { motion_reduce: bool, aria_label: bool, aria_current: bool, + child_focus: bool, + child_hover: bool, /// The opening tag as written, for the motion checks. text: String, } @@ -174,6 +176,23 @@ fn has_image(children: &[JSXChild<'_>]) -> bool { }) } +/// Whether any descendant element carries one of the named attributes. +fn descendant_has(children: &[JSXChild<'_>], wanted: &[&str]) -> bool { + children.iter().any(|child| match child { + JSXChild::Element(element) => { + element + .opening_element + .attributes + .iter() + .filter_map(attribute_name) + .any(|name| wanted.contains(&name)) + || descendant_has(&element.children, wanted) + } + JSXChild::Fragment(fragment) => descendant_has(&fragment.children, wanted), + _ => false, + }) +} + /// Whether the element shows text of its own: literal text, or an /// expression that may render text. An icon button shows neither. fn has_text(children: &[JSXChild<'_>]) -> bool { @@ -255,6 +274,9 @@ impl<'a> Visit<'a> for Collector<'_> { motion_reduce: has("_motionReduce"), aria_label: has("aria-label") || has("aria-labelledby"), aria_current: has("aria-current"), + child_focus: link + && descendant_has(&node.children, &["_focusVisible", "_focus"]), + child_hover: link && descendant_has(&node.children, &["_hover"]), text: self .source .get(span.start as usize..span.end as usize) @@ -453,7 +475,26 @@ pub fn interaction_findings(path: &str, source: &str) -> Vec { suggestion, )); }; - if !element.hover && !element.focus { + if element.child_focus && !element.focus { + push( + "focus-on-non-focusable", + Severity::Error, + format!( + "<{name}> is the focusable link, but _focusVisible is set on a child element; the child never receives focus, so a keyboard user sees no focus ring." + ), + "Render the stateful element itself as the link: . Do not wrap it in and put the states on the child, and do not move them into css().", + ); + } else if element.child_hover && !element.hover { + push( + "hover-on-child", + Severity::Warning, + format!( + "<{name}> is the link, but _hover is set only on a child element; the link's own computed style does not change." + ), + "Put _hover on the element rendered as the link: .", + ); + } + if !element.hover && !element.focus && !element.child_focus && !element.child_hover { push( "interactive-states", Severity::Warning, @@ -463,7 +504,7 @@ pub fn interaction_findings(path: &str, source: &str) -> Vec { ), include_str!("../text/site-interaction_findings-3f71ed85.md"), ); - } else if !element.focus { + } else if !element.focus && !element.child_focus { push( "focus-style", Severity::Warning, @@ -703,6 +744,23 @@ export function Cards() { return + A + B + C +; }"#; + let found = interaction_findings("Nav.tsx", source); + let rules = found.iter().map(|v| v.rule).collect::>(); + assert_eq!( + rules, + ["focus-on-non-focusable", "hover-on-child", "focus-style"] + ); + assert_eq!(found[0].severity, Severity::Error); + assert_eq!(found[1].severity, Severity::Warning); + } + #[test] fn a_page_or_layout_without_metadata_is_a_warning() { let bare = "export default function Page() { return
; }"; diff --git a/crates/devup-mcp-devup-ui/tests/responsive_discovery.rs b/crates/devup-mcp-devup-ui/tests/responsive_discovery.rs index 18d5e119..c1a0461a 100644 --- a/crates/devup-mcp-devup-ui/tests/responsive_discovery.rs +++ b/crates/devup-mcp-devup-ui/tests/responsive_discovery.rs @@ -134,3 +134,27 @@ fn unrelated_numbered_screens_are_not_breakpoint_labels() { None ); } + +#[test] +fn r36_device_names_with_width_labels_merge_as_one_family() { + let frames = [ + ("73:8149", "Desktop 1440", 1440.0), + ("73:8302", "Mobile 390", 390.0), + ]; + assert!( + devup_mcp_devup_ui::codegen::responsive::breakpoint_family_is_possible( + &frames + .iter() + .map(|(_, name, width)| (*name, *width)) + .collect::>() + ) + ); + assert_eq!(merged_slots(&frames), Some(vec![0, 4])); + assert_eq!( + merged_slots(&[ + ("a", "Desktop 1440", 1440.0), + ("b", "Profile Mobile 390", 390.0) + ]), + None + ); +} diff --git a/crates/devup-mcp-figma/src/asset_render_info.rs b/crates/devup-mcp-figma/src/asset_render_info.rs index a3d4bb44..8f7924a2 100644 --- a/crates/devup-mcp-figma/src/asset_render_info.rs +++ b/crates/devup-mcp-figma/src/asset_render_info.rs @@ -25,8 +25,14 @@ fn size(width: Option, height: Option) -> Value { } fn filename_part(name: &str) -> String { + let decoded = name + .replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace(""", "\"") + .replace("'", "'"); let mut slug = String::new(); - for c in name.nfc() { + for c in decoded.nfc() { if c.is_alphanumeric() { slug.extend(c.to_lowercase()); } else if !slug.is_empty() && !slug.ends_with('-') { diff --git a/crates/devup-mcp-figma/src/scripts/search.js b/crates/devup-mcp-figma/src/scripts/search.js index 34b5bd6f..0b71c5ea 100644 --- a/crates/devup-mcp-figma/src/scripts/search.js +++ b/crates/devup-mcp-figma/src/scripts/search.js @@ -38,6 +38,8 @@ function levenshtein(left, right) { } function score(name) { + const trimmed = options.query.trim(); + if (trimmed === "" || trimmed === "*") return 100; if (name === options.query) return 400; if (options.matchKind === "exact") return null; const candidate = normalize(name); diff --git a/crates/devup-mcp-figma/src/search.rs b/crates/devup-mcp-figma/src/search.rs index 198ce0e3..63e16b1c 100644 --- a/crates/devup-mcp-figma/src/search.rs +++ b/crates/devup-mcp-figma/src/search.rs @@ -30,6 +30,34 @@ pub struct SearchResult { pub canonical_url: String, pub match_kind: String, pub score: u32, + /// Set when the page or an ancestor name marks the node as trash, + /// archive, draft or copy: the keyword that matched. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub excluded: Option, +} + +const EXCLUDED_KEYWORDS: &[&str] = &[ + "trash", + "archive", + "draft", + "copy", + "휴지통", + "보관", + "초안", + "복사", +]; + +fn excluded_reason(breadcrumb: &[String], page_name: Option<&str>) -> Option { + page_name + .into_iter() + .chain(breadcrumb.iter().map(String::as_str)) + .find_map(|name| { + let lowered = name.to_lowercase(); + EXCLUDED_KEYWORDS + .iter() + .find(|keyword| lowered.contains(*keyword)) + .map(|keyword| format!("{keyword} ({name})")) + }) } pub fn search_snapshot( @@ -38,10 +66,11 @@ pub fn search_snapshot( options: &SearchOptions, ) -> Result, DevupError> { let query = options.query.trim(); - if query.is_empty() { + let list_by_type = query.is_empty() || query == "*"; + if list_by_type && options.node_types.is_empty() { return Err(DevupError::new( - ErrorCode::DevupFigmaNodeNotFound, - "Search query cannot be empty.", + ErrorCode::DevupInvalidInput, + "Search query cannot be empty unless nodeTypes is given. Pass nodeTypes (for example [\"SECTION\"]) with an empty query or \"*\" to list every node of those types.", false, )); } @@ -82,11 +111,16 @@ pub fn search_snapshot( .filter(|node| types.iter().any(|node_type| node_type == &node.node_type)) .filter_map(|node| { let name = node.typed_view().name()?; - let (kind, score) = - match_score(name, query, &normalized_query, options.match_kind.as_str())?; + let (kind, score) = if list_by_type { + ("type", 100) + } else { + match_score(name, query, &normalized_query, options.match_kind.as_str())? + }; let breadcrumb = breadcrumb(snapshot, &parents, &node.id); let page_name = ancestor_page(snapshot, &parents, &node.id); + let excluded = excluded_reason(&breadcrumb, page_name.as_deref()); Some(SearchResult { + excluded, node_id: node.id.clone(), name: name.to_owned(), node_type: node.node_type.clone(), @@ -99,9 +133,10 @@ pub fn search_snapshot( }) .collect::>(); results.sort_by(|left, right| { - right - .score - .cmp(&left.score) + left.excluded + .is_some() + .cmp(&right.excluded.is_some()) + .then_with(|| right.score.cmp(&left.score)) .then_with(|| left.name.cmp(&right.name)) .then_with(|| left.node_id.cmp(&right.node_id)) }); @@ -292,6 +327,119 @@ mod tests { assert!(results[0].canonical_url.ends_with("node-id=1-1")); } + fn typed_snapshot() -> (Snapshot, FigmaTarget) { + let nodes = [ + ("0:1", "PAGE", "Screens", vec!["1:1", "1:2"]), + ("1:1", "SECTION", "About", vec![]), + ("1:2", "FRAME", "Home", vec![]), + ] + .into_iter() + .map(|(id, node_type, name, children)| { + ( + id.to_owned(), + RawNode { + id: id.to_owned(), + node_type: node_type.to_owned(), + fields: serde_json::from_value(json!({ + "name": name, + "childrenIds": children, + })) + .unwrap(), + extra: Default::default(), + field_errors: Default::default(), + }, + ) + }) + .collect(); + let snapshot = Snapshot { + file_key: "key".to_owned(), + version: None, + roots: vec!["0:1".to_owned()], + nodes, + diagnostics: Vec::new(), + }; + let target = FigmaTarget { + file_key: snapshot.file_key.clone(), + node_id: None, + branch_key: None, + }; + (snapshot, target) + } + + fn options(query: &str, node_types: &[&str]) -> SearchOptions { + SearchOptions { + query: query.to_owned(), + node_types: node_types.iter().map(|t| (*t).to_owned()).collect(), + match_kind: "normalized".to_owned(), + limit: 20, + } + } + + #[test] + fn an_empty_or_star_query_with_node_types_lists_every_node_of_that_type() { + let (snapshot, target) = typed_snapshot(); + for query in ["", " ", "*"] { + let results = + search_snapshot(&snapshot, &target, &options(query, &["section"])).unwrap(); + assert_eq!(results.len(), 1, "query {query:?}"); + assert_eq!(results[0].node_id, "1:1"); + assert_eq!(results[0].match_kind, "type"); + } + } + + #[test] + fn matches_under_a_trash_page_are_flagged_and_sorted_last() { + let nodes = [ + ("0:1", "PAGE", "휴지통", vec!["1:1"]), + ("0:2", "PAGE", "디자인 시스템", vec!["1:2"]), + ("1:1", "FRAME", "특허", vec![]), + ("1:2", "FRAME", "특허", vec![]), + ] + .into_iter() + .map(|(id, node_type, name, children)| { + ( + id.to_owned(), + RawNode { + id: id.to_owned(), + node_type: node_type.to_owned(), + fields: serde_json::from_value(json!({ + "name": name, + "childrenIds": children, + })) + .unwrap(), + extra: Default::default(), + field_errors: Default::default(), + }, + ) + }) + .collect(); + let snapshot = Snapshot { + file_key: "key".to_owned(), + version: None, + roots: vec!["0:1".to_owned(), "0:2".to_owned()], + nodes, + diagnostics: Vec::new(), + }; + let target = FigmaTarget { + file_key: "key".to_owned(), + node_id: None, + branch_key: None, + }; + let results = search_snapshot(&snapshot, &target, &options("특허", &[])).unwrap(); + assert_eq!(results.len(), 2); + assert_eq!(results[0].node_id, "1:2"); + assert!(results[0].excluded.is_none()); + assert_eq!(results[1].node_id, "1:1"); + assert!(results[1].excluded.as_deref().unwrap().contains("휴지통")); + } + + #[test] + fn an_empty_query_without_node_types_is_invalid_input() { + let (snapshot, target) = typed_snapshot(); + let error = search_snapshot(&snapshot, &target, &options("", &[])).unwrap_err(); + assert_eq!(error.code, ErrorCode::DevupInvalidInput); + } + #[test] fn fuzzy_matching_is_opt_in() { assert!(match_score("Proofread", "Proofred", "proofred", "normalized").is_none()); diff --git a/crates/devup-mcp/src/server/asset_job_progress.rs b/crates/devup-mcp/src/server/asset_job_progress.rs new file mode 100644 index 00000000..e0a51d48 --- /dev/null +++ b/crates/devup-mcp/src/server/asset_job_progress.rs @@ -0,0 +1,31 @@ +use std::collections::BTreeMap; +use serde_json::Value; + +fn request_key(asset_id: &str) -> &str { + let id = asset_id.split(":fills:").next().unwrap_or(asset_id); + ["node", "visible", "fill"] + .iter() + .find_map(|suffix| id.strip_suffix(&format!(":{suffix}"))) + .unwrap_or(id) +} + +pub(super) fn request_progress(assets: &[Value]) -> (usize, usize, usize, usize) { + let mut groups: BTreeMap<&str, Vec<&str>> = BTreeMap::new(); + for asset in assets { + groups + .entry(request_key(asset["assetId"].as_str().unwrap_or(""))) + .or_default() + .push(asset["status"].as_str().unwrap_or("")); + } + let mut counts = (groups.len(), 0, 0, 0); + for rows in groups.values() { + if rows.contains(&"exported") { + counts.1 += 1; + } else if rows.contains(&"failed") { + counts.2 += 1; + } else if rows.contains(&"pending") { + counts.3 += 1; + } + } + counts +} diff --git a/crates/devup-mcp/src/server/asset_jobs.rs b/crates/devup-mcp/src/server/asset_jobs.rs index b4b546a1..892d24d8 100644 --- a/crates/devup-mcp/src/server/asset_jobs.rs +++ b/crates/devup-mcp/src/server/asset_jobs.rs @@ -10,6 +10,8 @@ use devup_mcp_figma::{ CollectionRequest, CollectorSession, DevupError, ErrorCode, ReadToolCall, UpstreamResult, }; use serde_json::{Value, json}; + +use super::asset_job_progress::request_progress; use sha2::{Digest, Sha256}; use tokio::{sync::Notify, time::Instant}; @@ -36,6 +38,7 @@ const COMPACT_ASSET_FIELDS: &[&str] = &[ "fileState", "fileStateReason", "suggestedFilename", + "duplicateOf", ]; #[derive(Clone, Default)] @@ -51,6 +54,8 @@ pub(super) struct AssetJob { resume: Notify, cancellation: Notify, cancelled: std::sync::atomic::AtomicBool, + /// The delivery the first call asked for; every poll keeps it. + delivery: super::delivery::DeliveryMode, } struct JobState { @@ -122,7 +127,9 @@ impl AssetJobs { } } let PendingOperation::Export { - asset_output_paths, .. + asset_output_paths, + delivery, + .. } = operation else { unreachable!() @@ -152,6 +159,7 @@ impl AssetJobs { resume: Notify::new(), cancellation: Notify::new(), cancelled: std::sync::atomic::AtomicBool::new(false), + delivery: *delivery, state: Mutex::new(JobState { state: "running", stage: "acquisition", @@ -383,8 +391,12 @@ impl AssetJob { }; // A terminal job cannot leave acquisition rows promising future work. // Preserve successfully acquired rows if a later projection/write failed. + let mut left_unattempted = false; for asset in &mut state.assets { - if asset["status"] == "pending" { + if asset["status"] == "pending" && asset["fileState"] == "written" { + asset["status"] = json!("exported"); + } else if asset["status"] == "pending" { + left_unattempted = result.is_ok(); asset["status"] = json!("failed"); asset["acquisitionState"] = json!("not-attempted"); asset["errorCode"] = result @@ -399,12 +411,23 @@ impl AssetJob { .unwrap_or_else(|| json!("The job finished before this asset was acquired.")); } } + if left_unattempted && state.state == "complete" { + state.state = "partial"; + } state.stage = "finished"; state.error = result.as_ref().err().cloned(); state.result = Some(result); state.finished = Some(Instant::now()); } + /// A poll keeps the delivery the first call asked for. + fn poll_arguments(&self, mut arguments: Value) -> Value { + if self.delivery != super::delivery::DeliveryMode::Auto { + arguments["delivery"] = json!(self.delivery); + } + arguments + } + #[cfg(test)] fn response(&self) -> Result { self.response_with(false) @@ -426,11 +449,30 @@ impl AssetJob { ); } } + let mut first_by_hash: BTreeMap = BTreeMap::new(); + let mut duplicates: BTreeMap = BTreeMap::new(); + for asset in &state.assets { + let (Some(id), Some(hash)) = (asset["assetId"].as_str(), asset["sha256"].as_str()) + else { + continue; + }; + match first_by_hash.get(hash) { + Some(first) => { + duplicates.insert(id.to_owned(), first.clone()); + } + None => { + first_by_hash.insert(hash.to_owned(), id.to_owned()); + } + } + } let assets: Vec<_> = state .assets .iter() .map(|asset| { let mut asset = asset.clone(); + if let Some(first) = asset["assetId"].as_str().and_then(|id| duplicates.get(id)) { + asset["duplicateOf"] = json!(first); + } let path = asset["assetId"].as_str().and_then(|id| self.paths.get(id)); asset["requestedOutputPath"] = json!(path); if asset.get("fileState").is_none() { @@ -474,7 +516,15 @@ impl AssetJob { let terminal = state.finished.is_some() || state.stage == "finished" || matches!(state.state, "failed" | "complete" | "finished"); - let next_action = if terminal { + let remaining: Vec<&str> = state + .assets + .iter() + .filter(|a| a["acquisitionState"] == "not-attempted") + .filter_map(|a| a["assetId"].as_str()) + .collect(); + let next_action = if state.state == "partial" { + json!({"tool":"devup_figma_export","arguments":self.poll_arguments(json!({"jobId":self.id,"jobAction":"resume","remainingAssetIds":remaining}))}) + } else if terminal { state .error .as_ref() @@ -491,11 +541,11 @@ impl AssetJob { .filter(|v| v["arguments"]["jobId"].is_null()) .unwrap_or(Value::Null) } else { - json!({"tool":"devup_figma_export","arguments":{"jobId":self.id,"jobAction":if state.state=="paused" {"resume"} else {"status"}}}) + json!({"tool":"devup_figma_export","arguments":self.poll_arguments(json!({"jobId":self.id,"jobAction":if state.state=="paused" {"resume"} else {"status"}}))}) }; let mut job = json!({"jobId":self.id,"budget":self.budget,"state":state.state,"stage":state.stage,"assets":assets, - "assetProgress":{"total":state.assets.len(),"exported":state.assets.iter().filter(|a|a["status"]=="exported").count(),"failed":state.assets.iter().filter(|a|a["status"]=="failed").count(), - "pending":state.assets.iter().filter(|a|a["status"]=="pending").count(),"maxAssetsPerBatch":6,"maxRetainedAssetBytes":devup_mcp_figma::MAX_COLLECTED_ASSET_BYTES, + "assetProgress":{"total":request_progress(&state.assets).0,"exported":request_progress(&state.assets).1,"failed":request_progress(&state.assets).2, + "pending":request_progress(&state.assets).3,"substeps":state.assets.len(),"maxAssetsPerBatch":6,"maxRetainedAssetBytes":devup_mcp_figma::MAX_COLLECTED_ASSET_BYTES, "observations":if state.assets.len()>32 {"summarized; full observations in completed manifest/audit resources"}else{"full"}}, "completedCalls":state.completed_calls,"calls":calls,"elapsedMs":self.started.elapsed().as_millis() as u64, "lastError":state.error,"checkpointScope":"server-process","retentionSeconds":JOB_LIFETIME.as_secs(), @@ -601,324 +651,5 @@ impl DevupServer { } #[cfg(test)] -mod tests { - use super::*; - - #[test] - fn image_modes_have_distinct_jobs_and_same_mode_recovers_its_job() { - let selection = devup_mcp_figma::AssetSelection { - usage_aspect_ratio: None, - max_width: None, - srcset: false, - visible_scope_node_id: None, - asset_id: "sample:fills:0".into(), - format: devup_mcp_figma::AssetFormat::Png, - scale: 1.0, - render_mode: devup_mcp_figma::AssetRenderMode::Node, - quality: None, - lossless: None, - }; - let mut request = CollectionRequest::new( - devup_mcp_figma::FigmaTarget::parse( - "https://www.figma.com/design/Fixture/Example?node-id=1-1", - ) - .unwrap(), - devup_mcp_figma::CollectionScope::Node, - ); - request.asset_selections = vec![selection]; - let operation = PendingOperation::Export { - outputs: vec!["assetManifest".into()], - previous_design_fingerprints: None, - component_name: None, - include_diagnostics: false, - root_layout: devup_mcp_devup_ui::codegen::RootLayout::default(), - asset_names_per_node: true, - scope: "node".into(), - theme_options: Default::default(), - strict: false, - output_paths: Default::default(), - page_scaffold: None, - frame_ids: vec![], - all_screens: false, - asset_captures: request.asset_selections.clone(), - web_ready_images: false, - asset_output_paths: Default::default(), - asset_public_root: None, - fields: vec![], - response_selection: Default::default(), - delivery: super::super::delivery::DeliveryMode::Auto, - width_policy: None, - }; - let jobs = AssetJobs::default(); - let (node, created) = jobs.acquire(&operation, &request, false).unwrap(); - assert!(created); - let (same, created) = jobs.acquire(&operation, &request, false).unwrap(); - assert!(!created); - assert_eq!(node.id, same.id); - request.asset_selections[0].render_mode = devup_mcp_figma::AssetRenderMode::Fill; - let (fill, created) = jobs.acquire(&operation, &request, false).unwrap(); - assert!(created); - assert_ne!(node.id, fill.id); - request.asset_selections[0].render_mode = devup_mcp_figma::AssetRenderMode::Auto; - let (automatic, created) = jobs.acquire(&operation, &request, false).unwrap(); - assert!(created); - assert_ne!(automatic.id, node.id); - assert_ne!(automatic.id, fill.id); - request.asset_selections[0].format = devup_mcp_figma::AssetFormat::Webp; - request.asset_selections[0].quality = Some(20); - request.asset_selections[0].lossless = Some(false); - let (quality20, _) = jobs.acquire(&operation, &request, false).unwrap(); - request.asset_selections[0].quality = Some(21); - let (quality21, _) = jobs.acquire(&operation, &request, false).unwrap(); - assert_ne!(quality20.id, quality21.id); - request.asset_selections[0].lossless = Some(true); - let (lossless, _) = jobs.acquire(&operation, &request, false).unwrap(); - assert_ne!(lossless.id, quality21.id); - let variant_jobs = AssetJobs::default(); - let mut ids = std::collections::BTreeSet::new(); - for variant in 0..6 { - let mut changed = request.clone(); - match variant { - 0 => changed.asset_selections[0].max_width = Some(240), - 1 => changed.asset_selections[0].srcset = true, - 2 => changed.asset_selections[0].scale = 1.5, - 3 => changed.asset_selections[0].visible_scope_node_id = Some("parent".into()), - 4 => changed.asset_selections[0].usage_aspect_ratio = Some(1.25), - _ => { - changed.asset_selections[0].render_mode = - devup_mcp_figma::AssetRenderMode::OriginalSource - } - } - let (job, created) = variant_jobs.acquire(&operation, &changed, false).unwrap(); - assert!(created); - assert!(ids.insert(job.id.clone())); - let (same, created) = variant_jobs.acquire(&operation, &changed, false).unwrap(); - assert!(!created); - assert_eq!(same.id, job.id); - } - request.asset_selections.clear(); - request.web_ready_images = true; - let (cached_preset, _) = jobs.acquire(&operation, &request, false).unwrap(); - cached_preset.captured(&[serde_json::from_value(json!({"assetId":"cached:fills:0","nodeId":"cached","field":"fills/0","sourceKind":"image-fill", - "format":"webp","scale":1,"quality":80,"lossless":false,"status":"failed","errorCode":"DEVUP_ASSET_NODE_HIDDEN"})).unwrap()]); - let cached = cached_preset.response_with(true).unwrap(); - assert_eq!(cached["exportJob"]["assets"].as_array().unwrap().len(), 1); - assert_eq!(cached["exportJob"]["assets"][0]["quality"], 80); - cached_preset.state.lock().unwrap().state = "paused"; - cached_preset.resume(); - assert_eq!(cached_preset.state.lock().unwrap().state, "running"); - cached_preset.cancel(); - cached_preset.finish(Err(DevupError::new( - ErrorCode::DevupInvalidInput, - "cancelled synthetic pending preset", - false, - ))); - let cancelled = cached_preset.response().unwrap_err(); - assert_eq!(cancelled.details["exportJob"]["state"], "cancelled"); - assert_eq!(cancelled.details["assetJob"]["state"], "cancelled"); - assert!(cancelled.details["exportJob"]["nextAction"].is_null()); - assert_eq!( - automatic.state.lock().unwrap().assets[0]["renderMode"], - "auto" - ); - assert_eq!(fill.state.lock().unwrap().assets[0]["renderMode"], "fill"); - fill.finish(Ok( - json!({"resources":[{"assetMetadata":{"assets":[{"assetId":"sample:fills:0", - "renderMode":"fill","renderInfo":{"outputSize":{"width":32,"height":16}}, - "suggestedFilename":"gallery-picture-fill.png"}]}}]}), - )); - let progress = fill.response_with(true).unwrap(); - assert_eq!( - progress["exportJob"]["assets"][0]["renderInfo"]["outputSize"]["width"], - 32 - ); - assert_eq!( - progress["exportJob"]["assets"][0]["suggestedFilename"], - "gallery-picture-fill.png" - ); - } - - #[test] - fn r10_terminal_jobs_never_offer_repeated_status_as_next_action() { - for correction in [false, true] { - let job = AssetJob { - id: "r10".into(), - key: "r10".into(), - budget: json!({}), - started: Instant::now(), - resume: Notify::new(), - cancellation: Notify::new(), - cancelled: std::sync::atomic::AtomicBool::new(false), - paths: BTreeMap::new(), - state: Mutex::new(JobState { - state: "running", - stage: "snapshot", - calls: vec![], - completed_calls: 0, - error: None, - result: None, - finished: None, - assets: vec![json!({"assetId":"pending:fills:0","status":"pending"})], - }), - }; - assert_eq!( - job.response().unwrap()["exportJob"]["nextAction"]["arguments"]["jobAction"], - "status" - ); - job.state - .lock() - .unwrap() - .calls - .push(json!({"stage":"asset-export","state":"running","startedMs":0})); - let progress = job.response().unwrap(); - assert!(progress["exportJob"]["calls"][0]["elapsedMs"].is_u64()); - assert!(progress["exportJob"]["assetTimeoutMs"].is_u64()); - let next = if correction { - json!({"tool":"devup_figma_export","arguments":{"url":"https://www.figma.com/design/test?node-id=1-1","frameIds":["2:1"]}}) - } else { - Value::Null - }; - job.finish(Err(DevupError::with_details( - ErrorCode::DevupFigmaNodeNotFound, - "test", - false, - json!({"nextAction":next}), - ))); - let error = job.response().unwrap_err(); - assert_eq!(error.details["exportJob"]["nextAction"], next); - assert_eq!(error.details["assetJob"]["aliasOf"], "exportJob"); - assert_eq!(error.details["exportJob"]["assets"][0]["status"], "failed"); - assert_eq!( - error.details["exportJob"]["assets"][0]["acquisitionState"], - "not-attempted" - ); - assert_eq!(error.details["exportJob"]["assetProgress"]["pending"], 0); - assert_eq!(error.details["exportJob"]["assetProgress"]["failed"], 1); - assert_eq!( - error.details["exportJob"]["assetProgress"]["maxRetainedAssetBytes"], - devup_mcp_figma::MAX_COLLECTED_ASSET_BYTES - ); - if !correction { - assert!(error.details["exportJob"]["nextActionReason"].is_string()); - } - job.finish(Ok(json!({"status":"complete"}))); - assert!(job.response().unwrap()["exportJob"]["nextAction"].is_null()); - } - } - - #[test] - fn r3_shared_path_reports_only_matching_bytes_as_written() { - let path = - std::env::temp_dir().join(format!("devup-r3-collision-{}.bin", rand::random::())); - std::fs::write(&path, b"first").unwrap(); - let hash = |bytes: &[u8]| { - Sha256::digest(bytes) - .iter() - .map(|b| format!("{b:02x}")) - .collect::() - }; - let path_text = path.to_string_lossy().to_string(); - let job = AssetJob { - id: "test".into(), - key: "test".into(), - budget: json!({}), - started: Instant::now(), - resume: Notify::new(), - cancellation: Notify::new(), - cancelled: std::sync::atomic::AtomicBool::new(false), - paths: BTreeMap::from([ - ("a".into(), path_text.clone()), - ("b".into(), path_text.clone()), - ]), - state: Mutex::new(JobState { - state: "running", - stage: "projection-and-write", - calls: vec![], - completed_calls: 0, - error: None, - result: None, - finished: None, - assets: vec![ - json!({"assetId":"a","status":"exported","sha256":hash(b"first")}), - json!({"assetId":"b","status":"exported","sha256":hash(b"second")}), - ], - }), - }; - job.finish(Ok( - json!({"outputPaths":{"asset:a":path_text,"asset:b":path_text}}), - )); - let response = job.response().unwrap(); - assert_eq!(response["exportJob"]["assets"][0]["fileState"], "written"); - assert_eq!( - response["exportJob"]["assets"][1]["fileState"], - "not-written" - ); - std::fs::remove_file(path).unwrap(); - } - - #[test] - fn polls_answer_compact_rows_and_reference_the_alias() { - let job = AssetJob { - id: "compact".into(), - key: "compact".into(), - budget: json!({}), - started: Instant::now(), - resume: Notify::new(), - cancellation: Notify::new(), - cancelled: std::sync::atomic::AtomicBool::new(false), - paths: BTreeMap::from([ - ("a".into(), "/out/a.svg".into()), - ("b".into(), "/out/b.svg".into()), - ]), - state: Mutex::new(JobState { - state: "running", - stage: "asset-export", - calls: vec![], - completed_calls: 1, - error: None, - result: None, - finished: None, - assets: vec![ - json!({"assetId":"a","status":"exported","byteLength":10,"sha256":"0".repeat(64), - "exportDiagnostics":{"large":"x".repeat(4096)},"renderInfo":{"large":"y".repeat(4096)}}), - json!({"assetId":"b","status":"pending"}), - ], - }), - }; - - let compact = job.response().unwrap(); - let rows = compact["exportJob"]["assets"].as_array().unwrap(); - assert_eq!(compact["exportJob"]["assetRows"], "compact"); - assert!(rows[0].get("exportDiagnostics").is_none(), "{compact}"); - assert!(rows[0].get("renderInfo").is_none(), "{compact}"); - assert_eq!(rows[0]["sha256"], "0".repeat(64)); - assert_eq!(rows[0]["requestedOutputPath"], "/out/a.svg"); - // Exported but not yet written: the state says why. - assert_eq!(rows[0]["fileState"], "pending"); - assert!( - rows[0]["fileStateReason"] - .as_str() - .unwrap() - .contains("projection-and-write") - ); - assert_eq!(rows[1]["fileStateReason"], "Not acquired yet."); - assert_eq!( - compact["exportJob"]["assetDetail"]["arguments"]["responseFields"], - json!(["exportJob.assets"]) - ); - // The alias names the job; it no longer repeats it. - assert_eq!( - compact["assetJob"], - json!({"aliasOf":"exportJob","jobId":"compact","state":"running"}) - ); - assert!( - serde_json::to_vec(&compact).unwrap().len() < 4096, - "{compact}" - ); - - let full = job.response_with(true).unwrap(); - assert_eq!(full["exportJob"]["assetRows"], "full"); - assert!(full["exportJob"]["assets"][0]["renderInfo"].is_object()); - assert!(full["exportJob"].get("assetDetail").is_none()); - } -} +#[path = "asset_jobs_tests.rs"] +mod tests; diff --git a/crates/devup-mcp/src/server/asset_jobs_tests.rs b/crates/devup-mcp/src/server/asset_jobs_tests.rs new file mode 100644 index 00000000..af8ddef7 --- /dev/null +++ b/crates/devup-mcp/src/server/asset_jobs_tests.rs @@ -0,0 +1,429 @@ +use super::*; + +#[test] +fn image_modes_have_distinct_jobs_and_same_mode_recovers_its_job() { + let selection = devup_mcp_figma::AssetSelection { + usage_aspect_ratio: None, + max_width: None, + srcset: false, + visible_scope_node_id: None, + asset_id: "sample:fills:0".into(), + format: devup_mcp_figma::AssetFormat::Png, + scale: 1.0, + render_mode: devup_mcp_figma::AssetRenderMode::Node, + quality: None, + lossless: None, + }; + let mut request = CollectionRequest::new( + devup_mcp_figma::FigmaTarget::parse( + "https://www.figma.com/design/Fixture/Example?node-id=1-1", + ) + .unwrap(), + devup_mcp_figma::CollectionScope::Node, + ); + request.asset_selections = vec![selection]; + let operation = PendingOperation::Export { + outputs: vec!["assetManifest".into()], + previous_design_fingerprints: None, + component_name: None, + include_diagnostics: false, + root_layout: devup_mcp_devup_ui::codegen::RootLayout::default(), + asset_names_per_node: true, + scope: "node".into(), + theme_options: Default::default(), + strict: false, + output_paths: Default::default(), + page_scaffold: None, + frame_ids: vec![], + all_screens: false, + asset_captures: request.asset_selections.clone(), + web_ready_images: false, + asset_output_paths: Default::default(), + asset_public_root: None, + fields: vec![], + response_selection: Default::default(), + delivery: super::super::delivery::DeliveryMode::Auto, + width_policy: None, + }; + let jobs = AssetJobs::default(); + let (node, created) = jobs.acquire(&operation, &request, false).unwrap(); + assert!(created); + let (same, created) = jobs.acquire(&operation, &request, false).unwrap(); + assert!(!created); + assert_eq!(node.id, same.id); + request.asset_selections[0].render_mode = devup_mcp_figma::AssetRenderMode::Fill; + let (fill, created) = jobs.acquire(&operation, &request, false).unwrap(); + assert!(created); + assert_ne!(node.id, fill.id); + request.asset_selections[0].render_mode = devup_mcp_figma::AssetRenderMode::Auto; + let (automatic, created) = jobs.acquire(&operation, &request, false).unwrap(); + assert!(created); + assert_ne!(automatic.id, node.id); + assert_ne!(automatic.id, fill.id); + request.asset_selections[0].format = devup_mcp_figma::AssetFormat::Webp; + request.asset_selections[0].quality = Some(20); + request.asset_selections[0].lossless = Some(false); + let (quality20, _) = jobs.acquire(&operation, &request, false).unwrap(); + request.asset_selections[0].quality = Some(21); + let (quality21, _) = jobs.acquire(&operation, &request, false).unwrap(); + assert_ne!(quality20.id, quality21.id); + request.asset_selections[0].lossless = Some(true); + let (lossless, _) = jobs.acquire(&operation, &request, false).unwrap(); + assert_ne!(lossless.id, quality21.id); + let variant_jobs = AssetJobs::default(); + let mut ids = std::collections::BTreeSet::new(); + for variant in 0..6 { + let mut changed = request.clone(); + match variant { + 0 => changed.asset_selections[0].max_width = Some(240), + 1 => changed.asset_selections[0].srcset = true, + 2 => changed.asset_selections[0].scale = 1.5, + 3 => changed.asset_selections[0].visible_scope_node_id = Some("parent".into()), + 4 => changed.asset_selections[0].usage_aspect_ratio = Some(1.25), + _ => { + changed.asset_selections[0].render_mode = + devup_mcp_figma::AssetRenderMode::OriginalSource + } + } + let (job, created) = variant_jobs.acquire(&operation, &changed, false).unwrap(); + assert!(created); + assert!(ids.insert(job.id.clone())); + let (same, created) = variant_jobs.acquire(&operation, &changed, false).unwrap(); + assert!(!created); + assert_eq!(same.id, job.id); + } + request.asset_selections.clear(); + request.web_ready_images = true; + let (cached_preset, _) = jobs.acquire(&operation, &request, false).unwrap(); + cached_preset.captured(&[serde_json::from_value(json!({"assetId":"cached:fills:0","nodeId":"cached","field":"fills/0","sourceKind":"image-fill", + "format":"webp","scale":1,"quality":80,"lossless":false,"status":"failed","errorCode":"DEVUP_ASSET_NODE_HIDDEN"})).unwrap()]); + let cached = cached_preset.response_with(true).unwrap(); + assert_eq!(cached["exportJob"]["assets"].as_array().unwrap().len(), 1); + assert_eq!(cached["exportJob"]["assets"][0]["quality"], 80); + cached_preset.state.lock().unwrap().state = "paused"; + cached_preset.resume(); + assert_eq!(cached_preset.state.lock().unwrap().state, "running"); + cached_preset.cancel(); + cached_preset.finish(Err(DevupError::new( + ErrorCode::DevupInvalidInput, + "cancelled synthetic pending preset", + false, + ))); + let cancelled = cached_preset.response().unwrap_err(); + assert_eq!(cancelled.details["exportJob"]["state"], "cancelled"); + assert_eq!(cancelled.details["assetJob"]["state"], "cancelled"); + assert!(cancelled.details["exportJob"]["nextAction"].is_null()); + assert_eq!( + automatic.state.lock().unwrap().assets[0]["renderMode"], + "auto" + ); + assert_eq!(fill.state.lock().unwrap().assets[0]["renderMode"], "fill"); + fill.finish(Ok( + json!({"resources":[{"assetMetadata":{"assets":[{"assetId":"sample:fills:0", + "renderMode":"fill","renderInfo":{"outputSize":{"width":32,"height":16}}, + "suggestedFilename":"gallery-picture-fill.png"}]}}]}), + )); + let progress = fill.response_with(true).unwrap(); + assert_eq!( + progress["exportJob"]["assets"][0]["renderInfo"]["outputSize"]["width"], + 32 + ); + assert_eq!( + progress["exportJob"]["assets"][0]["suggestedFilename"], + "gallery-picture-fill.png" + ); +} + +#[test] +fn r10_terminal_jobs_never_offer_repeated_status_as_next_action() { + for correction in [false, true] { + let job = AssetJob { + id: "r10".into(), + key: "r10".into(), + budget: json!({}), + started: Instant::now(), + resume: Notify::new(), + cancellation: Notify::new(), + cancelled: std::sync::atomic::AtomicBool::new(false), + delivery: Default::default(), + paths: BTreeMap::new(), + state: Mutex::new(JobState { + state: "running", + stage: "snapshot", + calls: vec![], + completed_calls: 0, + error: None, + result: None, + finished: None, + assets: vec![json!({"assetId":"pending:fills:0","status":"pending"})], + }), + }; + assert_eq!( + job.response().unwrap()["exportJob"]["nextAction"]["arguments"]["jobAction"], + "status" + ); + job.state + .lock() + .unwrap() + .calls + .push(json!({"stage":"asset-export","state":"running","startedMs":0})); + let progress = job.response().unwrap(); + assert!(progress["exportJob"]["calls"][0]["elapsedMs"].is_u64()); + assert!(progress["exportJob"]["assetTimeoutMs"].is_u64()); + let next = if correction { + json!({"tool":"devup_figma_export","arguments":{"url":"https://www.figma.com/design/test?node-id=1-1","frameIds":["2:1"]}}) + } else { + Value::Null + }; + job.finish(Err(DevupError::with_details( + ErrorCode::DevupFigmaNodeNotFound, + "test", + false, + json!({"nextAction":next}), + ))); + let error = job.response().unwrap_err(); + assert_eq!(error.details["exportJob"]["nextAction"], next); + assert_eq!(error.details["assetJob"]["aliasOf"], "exportJob"); + assert_eq!(error.details["exportJob"]["assets"][0]["status"], "failed"); + assert_eq!( + error.details["exportJob"]["assets"][0]["acquisitionState"], + "not-attempted" + ); + assert_eq!(error.details["exportJob"]["assetProgress"]["pending"], 0); + assert_eq!(error.details["exportJob"]["assetProgress"]["failed"], 1); + assert_eq!( + error.details["exportJob"]["assetProgress"]["maxRetainedAssetBytes"], + devup_mcp_figma::MAX_COLLECTED_ASSET_BYTES + ); + if !correction { + assert!(error.details["exportJob"]["nextActionReason"].is_string()); + } + job.finish(Ok(json!({"status":"complete"}))); + assert!(job.response().unwrap()["exportJob"]["nextAction"].is_null()); + } +} + +#[test] +fn r3_shared_path_reports_only_matching_bytes_as_written() { + let path = + std::env::temp_dir().join(format!("devup-r3-collision-{}.bin", rand::random::())); + std::fs::write(&path, b"first").unwrap(); + let hash = |bytes: &[u8]| { + Sha256::digest(bytes) + .iter() + .map(|b| format!("{b:02x}")) + .collect::() + }; + let path_text = path.to_string_lossy().to_string(); + let job = AssetJob { + id: "test".into(), + key: "test".into(), + budget: json!({}), + started: Instant::now(), + resume: Notify::new(), + cancellation: Notify::new(), + cancelled: std::sync::atomic::AtomicBool::new(false), + delivery: Default::default(), + paths: BTreeMap::from([ + ("a".into(), path_text.clone()), + ("b".into(), path_text.clone()), + ]), + state: Mutex::new(JobState { + state: "running", + stage: "projection-and-write", + calls: vec![], + completed_calls: 0, + error: None, + result: None, + finished: None, + assets: vec![ + json!({"assetId":"a","status":"exported","sha256":hash(b"first")}), + json!({"assetId":"b","status":"exported","sha256":hash(b"second")}), + ], + }), + }; + job.finish(Ok( + json!({"outputPaths":{"asset:a":path_text,"asset:b":path_text}}), + )); + let response = job.response().unwrap(); + assert_eq!(response["exportJob"]["assets"][0]["fileState"], "written"); + assert_eq!( + response["exportJob"]["assets"][1]["fileState"], + "not-written" + ); + std::fs::remove_file(path).unwrap(); +} + +#[test] +fn polls_answer_compact_rows_and_reference_the_alias() { + let job = AssetJob { + id: "compact".into(), + key: "compact".into(), + budget: json!({}), + started: Instant::now(), + resume: Notify::new(), + cancellation: Notify::new(), + cancelled: std::sync::atomic::AtomicBool::new(false), + delivery: Default::default(), + paths: BTreeMap::from([ + ("a".into(), "/out/a.svg".into()), + ("b".into(), "/out/b.svg".into()), + ]), + state: Mutex::new(JobState { + state: "running", + stage: "asset-export", + calls: vec![], + completed_calls: 1, + error: None, + result: None, + finished: None, + assets: vec![ + json!({"assetId":"a","status":"exported","byteLength":10,"sha256":"0".repeat(64), + "exportDiagnostics":{"large":"x".repeat(4096)},"renderInfo":{"large":"y".repeat(4096)}}), + json!({"assetId":"b","status":"pending"}), + ], + }), + }; + + let compact = job.response().unwrap(); + let rows = compact["exportJob"]["assets"].as_array().unwrap(); + assert_eq!(compact["exportJob"]["assetRows"], "compact"); + assert!(rows[0].get("exportDiagnostics").is_none(), "{compact}"); + assert!(rows[0].get("renderInfo").is_none(), "{compact}"); + assert_eq!(rows[0]["sha256"], "0".repeat(64)); + assert_eq!(rows[0]["requestedOutputPath"], "/out/a.svg"); + // Exported but not yet written: the state says why. + assert_eq!(rows[0]["fileState"], "pending"); + assert!( + rows[0]["fileStateReason"] + .as_str() + .unwrap() + .contains("projection-and-write") + ); + assert_eq!(rows[1]["fileStateReason"], "Not acquired yet."); + assert_eq!( + compact["exportJob"]["assetDetail"]["arguments"]["responseFields"], + json!(["exportJob.assets"]) + ); + // The alias names the job; it no longer repeats it. + assert_eq!( + compact["assetJob"], + json!({"aliasOf":"exportJob","jobId":"compact","state":"running"}) + ); + assert!( + serde_json::to_vec(&compact).unwrap().len() < 4096, + "{compact}" + ); + + let full = job.response_with(true).unwrap(); + assert_eq!(full["exportJob"]["assetRows"], "full"); + assert!(full["exportJob"]["assets"][0]["renderInfo"].is_object()); + assert!(full["exportJob"].get("assetDetail").is_none()); +} + +#[test] +fn r26_polls_keep_the_delivery_of_the_first_call() { + let job = |delivery| AssetJob { + id: "p26".into(), + key: "p26".into(), + budget: json!({}), + started: Instant::now(), + resume: Notify::new(), + cancellation: Notify::new(), + cancelled: std::sync::atomic::AtomicBool::new(false), + delivery, + paths: BTreeMap::new(), + state: Mutex::new(JobState { + state: "running", + stage: "acquisition", + calls: vec![], + completed_calls: 0, + error: None, + result: None, + finished: None, + assets: vec![], + }), + }; + let resource = job(super::super::delivery::DeliveryMode::Resource); + let next = resource.response().unwrap()["exportJob"]["nextAction"].clone(); + assert_eq!(next["arguments"]["delivery"], "resource"); + assert_eq!(next["arguments"]["jobAction"], "status"); + let auto = job(super::super::delivery::DeliveryMode::Auto); + let next = auto.response().unwrap()["exportJob"]["nextAction"].clone(); + assert!(next["arguments"].get("delivery").is_none()); +} + +#[test] +fn r39_unattempted_assets_make_a_partial_job_with_resume_and_one_count_per_request() { + let job = AssetJob { + id: "p39".into(), + key: "p39".into(), + budget: json!({}), + started: Instant::now(), + resume: Notify::new(), + cancellation: Notify::new(), + cancelled: std::sync::atomic::AtomicBool::new(false), + delivery: Default::default(), + paths: BTreeMap::new(), + state: Mutex::new(JobState { + state: "running", + stage: "projection-and-write", + calls: vec![], + completed_calls: 0, + error: None, + result: None, + finished: None, + assets: vec![ + json!({"assetId":"1:1:node","status":"exported"}), + json!({"assetId":"1:2:node","status":"pending"}), + json!({"assetId":"1:2:fills:0","status":"pending"}), + ], + }), + }; + job.finish(Ok(json!({"status":"complete"}))); + let job_json = job.response().unwrap()["exportJob"].clone(); + assert_eq!(job_json["state"], "partial"); + assert_eq!(job_json["nextAction"]["arguments"]["jobAction"], "resume"); + assert_eq!(job_json["assetProgress"]["total"], 2); + assert_eq!(job_json["assetProgress"]["exported"], 1); + assert_eq!(job_json["assetProgress"]["failed"], 1); + assert_eq!(job_json["assetProgress"]["substeps"], 3); +} + +#[test] +fn r39_identical_bytes_are_marked_duplicate_and_substeps_count_once() { + let job = AssetJob { + id: "dup".into(), + key: "dup".into(), + budget: json!({}), + started: Instant::now(), + resume: Notify::new(), + cancellation: Notify::new(), + cancelled: std::sync::atomic::AtomicBool::new(false), + delivery: Default::default(), + paths: BTreeMap::new(), + state: Mutex::new(JobState { + state: "running", + stage: "asset-export", + calls: vec![], + completed_calls: 1, + error: None, + result: None, + finished: None, + assets: vec![ + json!({"assetId":"1:2:node","status":"exported","sha256":"aa"}), + json!({"assetId":"1:2:fills:0","status":"exported","sha256":"aa"}), + json!({"assetId":"1:3","status":"exported","sha256":"bb"}), + json!({"assetId":"1:4","status":"exported","sha256":"aa"}), + ], + }), + }; + let response = job.response().unwrap(); + let rows = response["exportJob"]["assets"].as_array().unwrap(); + assert!(rows[0].get("duplicateOf").is_none()); + assert_eq!(rows[1]["duplicateOf"], "1:2:node"); + assert!(rows[2].get("duplicateOf").is_none()); + assert_eq!(rows[3]["duplicateOf"], "1:2:node"); + assert_eq!(response["exportJob"]["assetProgress"]["total"], 3); + assert_eq!(response["exportJob"]["assetProgress"]["substeps"], 4); +} diff --git a/crates/devup-mcp/src/server/export_skill_gap_tests.rs b/crates/devup-mcp/src/server/export_skill_gap_tests.rs index 352afc15..14472072 100644 --- a/crates/devup-mcp/src/server/export_skill_gap_tests.rs +++ b/crates/devup-mcp/src/server/export_skill_gap_tests.rs @@ -229,3 +229,33 @@ fn the_export_tool_description_still_says_to_call_devup_skills() { ); } } + +#[test] +fn r38_tsx_with_tokens_missing_from_the_project_theme_points_at_a_missing_only_sync() { + let project = scratch("theme-sync"); + std::fs::write( + project.join("devup.json"), + r##"{"theme":{"colors":{"default":{"primary":"#000"}}}}"##, + ) + .unwrap(); + let checked = with_project_checks( + json!({"status": "complete", "url": "https://www.figma.com/design/k/n?node-id=1-2", "tsx": ""}), + project.to_str(), + &skills::Lookup::project_only(project.clone(), skills::Runtime::Codex), + ); + let action = &checked["nextAction"]; + assert_eq!(action["tool"], "devup_figma_export"); + assert_eq!(action["arguments"]["outputs"], json!(["devupJson"])); + assert_eq!(action["arguments"]["devupJsonMode"], "missing-only"); + assert_eq!(action["arguments"]["projectRoot"], project.to_str().unwrap()); + assert!(action["how"].as_str().unwrap().contains("before using the tsx")); + assert_eq!(checked["projectThemeValidation"]["nextAction"], *action); + + let clean = with_project_checks( + json!({"status": "complete", "tsx": ""}), + project.to_str(), + &skills::Lookup::project_only(project.clone(), skills::Runtime::Codex), + ); + assert!(clean.get("nextAction").is_none()); + let _ = std::fs::remove_dir_all(&project); +} diff --git a/crates/devup-mcp/src/server/handlers/devup_figma_export.rs b/crates/devup-mcp/src/server/handlers/devup_figma_export.rs index 4ccb4686..e3d5e503 100644 --- a/crates/devup-mcp/src/server/handlers/devup_figma_export.rs +++ b/crates/devup-mcp/src/server/handlers/devup_figma_export.rs @@ -11,6 +11,14 @@ impl DevupServer { Parameters(workflow): Parameters, ) -> Result { let mut input = workflow.input; + if !input.asset_requests.is_empty() + && !input + .outputs + .iter() + .any(|o| matches!(o.as_str(), "assetManifest" | "pageScaffold")) + { + input.outputs.push("assetManifest".into()); + } if input.preset.as_deref() == Some("web-ready-images") { for output in ["assetManifest", "imageAudit"] { if !input.outputs.iter().any(|o| o == output) { @@ -228,18 +236,7 @@ impl DevupServer { input.previous_design_fingerprints.as_deref(), ) .map_err(to_mcp_error)?; - if !input.asset_requests.is_empty() - && !input - .outputs - .iter() - .any(|output| matches!(output.as_str(), "assetManifest" | "pageScaffold")) - { - return Err(to_mcp_error(DevupError::new( - ErrorCode::DevupSnapshotUnsupported, - "Using assetRequests requires assetManifest in outputs.", - false, - ))); - } + let reference_png_requested = input.outputs.iter().any(|output| output == "referencePng"); if reference_png_requested && (!input.frame_ids.is_empty() || input.all_screens) { let target = if let Some(url) = &input.url { diff --git a/crates/devup-mcp/src/server/mod_parts/exploration_and_responses.rs b/crates/devup-mcp/src/server/mod_parts/exploration_and_responses.rs index 277798be..1b1c020d 100644 --- a/crates/devup-mcp/src/server/mod_parts/exploration_and_responses.rs +++ b/crates/devup-mcp/src/server/mod_parts/exploration_and_responses.rs @@ -516,9 +516,33 @@ fn with_project_checks( "Every token the generated TSX references exists in this project's devup.json." }, }); + if any_unknown { + let action = theme_sync_action(&result, project_root); + result["projectThemeValidation"]["nextAction"] = action.clone(); + if result.get("nextAction").is_none_or(Value::is_null) { + result["nextAction"] = action; + } + } result } +fn theme_sync_action(result: &Value, project_root: &str) -> Value { + let mut arguments = json!({ + "outputs": ["devupJson"], + "devupJsonMode": "missing-only", + "projectRoot": project_root, + "scope": "file", + }); + if let Some(url) = result.get("url").filter(|url| url.is_string()) { + arguments["url"] = url.clone(); + } + json!({ + "tool": "devup_figma_export", + "arguments": arguments, + "how": "The generated code references tokens missing from this project's devup.json. Sync devup.json with this call (additions only; existing tokens are kept) before using the tsx.", + }) +} + fn section_candidate_as_explore(candidate: &SectionCandidate) -> ExploreCandidate { ExploreCandidate { node: ExploreNode { diff --git a/crates/devup-mcp/src/server/mod_parts/state_and_services.rs b/crates/devup-mcp/src/server/mod_parts/state_and_services.rs index 4675934a..4362bbc2 100644 --- a/crates/devup-mcp/src/server/mod_parts/state_and_services.rs +++ b/crates/devup-mcp/src/server/mod_parts/state_and_services.rs @@ -2,6 +2,8 @@ pub mod artifacts; #[path = "../asset_guard.rs"] mod asset_guard; +#[path = "../asset_job_progress.rs"] +mod asset_job_progress; #[path = "../asset_jobs.rs"] mod asset_jobs; #[path = "../audit_plan.rs"] diff --git a/crates/devup-mcp/src/server/project_plan/erd.rs b/crates/devup-mcp/src/server/project_plan/erd.rs index 4dbe7f5d..500dddc4 100644 --- a/crates/devup-mcp/src/server/project_plan/erd.rs +++ b/crates/devup-mcp/src/server/project_plan/erd.rs @@ -6,6 +6,38 @@ pub(super) fn api(plan: &Value) -> bool { .to_ascii_lowercase() .contains("api") } +const COMPLEX_KINDS: [&str; 5] = ["varchar", "char", "numeric", "enum", "custom"]; +const SIMPLE_TYPES: [&str; 19] = [ + "small_int", + "integer", + "big_int", + "real", + "double_precision", + "text", + "boolean", + "date", + "time", + "timestamp", + "timestamptz", + "interval", + "bytea", + "uuid", + "json", + "inet", + "cidr", + "macaddr", + "xml", +]; +fn unsupported_type(t: &Value) -> Option { + match t { + Value::String(s) => (!SIMPLE_TYPES.contains(&s.as_str())).then(|| s.clone()), + Value::Object(_) => { + let kind = text(&t["kind"]); + (!COMPLEX_KINDS.contains(&kind)).then(|| kind.to_owned()) + } + _ => None, + } +} fn ident(s: &str) -> bool { !s.is_empty() && s.as_bytes()[0].is_ascii_lowercase() @@ -234,6 +266,11 @@ pub(super) fn draft(input: &ProjectPlanInput, plan: &mut Value) -> Result<(), De "vespertide revision -m \"confirmed project plan\"", "vespertide export --orm seaorm" ]); + plan["backendNotes"] = json!([ + "Before cargo check, make sure the API Cargo.toml depends on serde (derive) and serde_json; exported entities use them.", + "Add `mod models;` to the API main.rs; export does not wire the generated src/models.", + "Right after export, cargo clippy -- -D warnings reports never-constructed ModelEx structs until routes use them; that is expected before the route units." + ]); Ok(()) } pub(super) fn validate(entities: &[Value]) -> Result<(), DevupError> { @@ -254,6 +291,14 @@ pub(super) fn validate(entities: &[Value]) -> Result<(), DevupError> { "Columns need unique snake_case names, type and nullable", )); } + if let Some(bad) = unsupported_type(&c["type"]) { + let hint = if bad == "jsonb" { "; use json" } else { "" }; + return Err(invalid(format!( + "Column {}.{} has type {bad}, which vespertide cannot parse{hint}", + text(&e["name"]), + text(&c["name"]) + ))); + } if !c["foreign_key"].is_null() && c["index"].is_null() { return Err(invalid("Foreign keys require inline indexes")); } diff --git a/crates/devup-mcp/src/server/project_plan/mod.rs b/crates/devup-mcp/src/server/project_plan/mod.rs index 3300f3e4..b749e265 100644 --- a/crates/devup-mcp/src/server/project_plan/mod.rs +++ b/crates/devup-mcp/src/server/project_plan/mod.rs @@ -57,6 +57,10 @@ pub struct ProjectPlanInput { pub decisions: Vec, #[serde(default)] pub unit_updates: Vec, + /// summary (default) answers with state, open questions, next action, ERD and + /// unit DAG; full also returns the whole plan. + #[schemars(extend("enum" = ["summary", "full"]))] + pub detail: Option, } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] @@ -76,6 +80,9 @@ pub struct Feature { pub screens: Vec, #[serde(default)] pub acceptance: Vec, + /// Tables this feature reads or writes; split makes one model unit per table. + #[serde(default)] + pub entities: Vec, } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] @@ -172,19 +179,129 @@ fn confirmed(plan: &Value) -> Result<(), DevupError> { } Ok(()) } -fn response(plan: Value) -> Value { +/// Saves decisions without dropping earlier ones; a decision with the same id +/// is replaced. Used by draft and confirm as well as defer/assume. +fn record_decisions( + plan: &mut Value, + decisions: &[Decision], + state: &str, +) -> Result<(), DevupError> { + for d in decisions { + if d.id.trim().is_empty() || d.reason.trim().is_empty() || d.recommended.trim().is_empty() + { + return Err(invalid("Decisions need id, reason and recommended option")); + } + let list = plan["decisions"].as_array_mut().unwrap(); + list.retain(|v| v["id"] != d.id); + let mut value = json!(d); + value["state"] = json!(state); + list.push(value); + } + Ok(()) +} +fn figma_url(plan: &Value) -> String { + let figma = &plan["facts"]["figma"]; + figma + .as_str() + .or_else(|| array(figma).first().and_then(Value::as_str)) + .unwrap_or_default() + .to_owned() +} +fn screen_blockers(plan: &Value, root: &str) -> Vec { + if figma_url(plan).is_empty() { + return vec![]; + } + match screen_plan::read_plan(root) { + Ok(Some(s)) if s["state"] == "confirmed" || s["status"] == "confirmed" => vec![], + _ => vec!["screenPlan: not confirmed".to_owned()], + } +} +fn screen_plan_hint(plan: &Value, root: &str) -> String { + format!( + "Run {} first.", + json!({"tool":"devup_screen_plan","arguments":{"action":"draft","url":figma_url(plan),"projectRoot":root}}) + ) +} +fn next_action(plan: &Value, root: &str) -> Value { + let args = |action: &str| json!({"action":action, "planId":plan["planId"], "projectRoot":root}); + if matches!(plan["state"].as_str(), Some("intake" | "draft")) + && !screen_blockers(plan, root).is_empty() + { + return json!({ + "tool":"devup_screen_plan", + "arguments":{"action":"draft","url":figma_url(plan),"projectRoot":root}, + "how":"Figma sources need a confirmed screen plan before project confirm; run it first and ask its questions together with this plan's."}); + } + match plan["state"].as_str() { + Some("confirmed") if array(&plan["units"]).is_empty() => json!({ + "tool":"devup_project_plan","arguments":args("split"), + "how":"Models are written and unitDag is empty until split; run backendCommands in modelsWorkingDirectory first."}), + Some("draft") => json!({ + "tool":"devup_project_plan","arguments":args("confirm"), + "how":"Ask unanswered questions with the question tool, then confirm (acceptRecommended fills the rest)."}), + _ => Value::Null, + } +} +fn unit_rows(plan: &Value) -> Vec { + array(&plan["units"]) + .iter() + .map(|u| { + json!({"id":u["id"],"layer":u["layer"],"state":u["state"], + "dependencies":u["dependencies"], + "brief":format!(".devup/units/{}.md", text(&u["id"]))}) + }) + .collect() +} +/// Questions of an unconfirmed screen plan, so one round covers both plans. +fn open_screen_questions(root: &str) -> Vec { + match screen_plan::read_plan(root) { + Ok(Some(s)) if s["state"] != "confirmed" && s["status"] != "confirmed" => array(&s["questions"]).to_vec(), + _ => vec![], + } +} +fn response(plan: Value, root: &str, full: bool) -> Value { let questions = array(&plan["questions"]) .iter() .filter(|q| plan["answers"][text(&q["id"])].is_null()) .cloned() .collect::>(); + let screen_open = open_screen_questions(root); + let screen_ids = screen_open.iter().map(|q| q["id"].clone()).collect::>(); + let questions = [questions, screen_open].concat(); let proposal = screen_plan::proposal(&questions); + let instruction = if plan["state"] == "confirmed" { + include_str!("../text/mod-response-22d8f704.md") + } else { + screen_plan::ASK_INSTRUCTION + }; + if !full { + return json!({"planId":plan["planId"], "state":plan["state"], "detail":"summary", + "openQuestionCount":questions.len(), + "openQuestionIds":questions.iter().map(|q| q["id"].clone()).collect::>(), + "screenPlanQuestionIds":screen_ids, + "blockers":screen_blockers(&plan, root), + "proposalMarkdown":screen_plan::proposal_markdown(&proposal), + "questionRounds":screen_plan::question_rounds(&questions, 4), + "instruction":instruction, + "nextAction":next_action(&plan, root), + "erdMermaid":erd::mermaid(array(&plan["entities"])), + "unitDag":units::dag(array(&plan["units"])), + "units":unit_rows(&plan), + "decisions":plan["decisions"], + "backendCommands":plan["backendCommands"], + "backendNotes":plan["backendNotes"], + "modelsWorkingDirectory":plan["modelsWorkingDirectory"], + "how":"Summary only. Pass detail \"full\" to read the whole plan."}); + } json!({"planId":plan["planId"], "state":plan["state"], + "blockers":screen_blockers(&plan, root), "proposalMarkdown":screen_plan::proposal_markdown(&proposal), "proposal":proposal, + "screenPlanQuestionIds":screen_ids, "questions":screen_plan::ask_shape(&questions), "questionRounds":screen_plan::question_rounds(&questions, 4), - "instruction": if plan["state"] == "confirmed" { include_str!("../text/mod-response-22d8f704.md") } else { screen_plan::ASK_INSTRUCTION }, + "instruction":instruction, + "nextAction":next_action(&plan, root), "erdMermaid":erd::mermaid(array(&plan["entities"])), "unitDag":units::dag(array(&plan["units"])), "finalReport":{"units":plan["units"], "decisions":plan["decisions"], "completion":"unverified until supervisor reruns all verification and stack diff reports no drift"}, @@ -198,6 +315,14 @@ pub(super) async fn run( if input.project_root.trim().is_empty() { return Err(invalid("projectRoot is required")); } + if input + .detail + .as_deref() + .is_some_and(|d| !["summary", "full"].contains(&d)) + { + return Err(invalid("detail must be summary or full")); + } + let full = input.detail.as_deref() == Some("full"); // Apply the same root boundary to reads and writes, even for status. policy.resolve(&Path::new(&input.project_root).join(FILE).to_string_lossy())?; let mut tx = OutputTransaction::new(); @@ -216,11 +341,17 @@ pub(super) async fn run( rounds::answers(&mut plan, &input.answers)?; write(&mut tx, policy, &input.project_root, &plan)?; tx.commit()?; - return Ok(response(plan)); + return Ok(response(plan, &input.project_root, full)); } let mut plan = read(&input.project_root)?; - if input.plan_id.as_deref() != plan["planId"].as_str() { - return Err(invalid("planId is missing or stale")); + // status is a read: it may omit planId and reads it from the plan file. + let provided = input.plan_id.is_some() || input.action != "status"; + if provided && input.plan_id.as_deref() != plan["planId"].as_str() { + return Err(invalid(format!( + "planId is missing or stale; current planId {} state {}", + text(&plan["planId"]), + text(&plan["state"]) + ))); } match input.action.as_str() { "draft" => { @@ -231,7 +362,9 @@ pub(super) async fn run( if input.accept_recommended { rounds::accept_recommended(&mut plan)?; } - rounds::require_answers(&plan)?; + // Open questions no longer block draft: new evidence, features and + // decisions are saved first and only confirm requires every answer. + record_decisions(&mut plan, &input.decisions, "pending")?; for ev in &input.evidence { if ev.source.trim().is_empty() { return Err(invalid("Evidence needs a source")); @@ -273,6 +406,7 @@ pub(super) async fn run( rounds::accept_recommended(&mut plan)?; } rounds::require_answers(&plan)?; + record_decisions(&mut plan, &input.decisions, "pending")?; if plan["answers"] .as_object() .unwrap() @@ -287,13 +421,17 @@ pub(super) async fn run( // Figma remains its own confirmation gate and is retained in every brief. if let Some(screen) = screen_plan::read_plan(&input.project_root)? { if screen["state"] != "confirmed" && screen["status"] != "confirmed" { - return Err(invalid("Confirm the Figma screen plan first")); + return Err(invalid(format!( + "Confirm the Figma screen plan first. {}", + screen_plan_hint(&plan, &input.project_root) + ))); } plan["screenPlan"] = screen; - } else if !array(&plan["facts"]["figma"]).is_empty() { - return Err(invalid( - "Figma sources require a confirmed devup_screen_plan", - )); + } else if !figma_url(&plan).is_empty() { + return Err(invalid(format!( + "Figma sources require a confirmed devup_screen_plan. {}", + screen_plan_hint(&plan, &input.project_root) + ))); } erd::write_models(&mut tx, policy, &input.project_root, &plan)?; plan["state"] = json!("confirmed"); @@ -347,5 +485,5 @@ pub(super) async fn run( } write(&mut tx, policy, &input.project_root, &plan)?; tx.commit()?; - Ok(response(plan)) + Ok(response(plan, &input.project_root, full)) } diff --git a/crates/devup-mcp/src/server/project_plan/rounds.rs b/crates/devup-mcp/src/server/project_plan/rounds.rs index d417380e..0dd5eda7 100644 --- a/crates/devup-mcp/src/server/project_plan/rounds.rs +++ b/crates/devup-mcp/src/server/project_plan/rounds.rs @@ -11,6 +11,10 @@ pub(super) fn facts(input: &ProjectPlanInput) -> Result { facts["parts"] = json!("front + admin"); facts["partsEvidence"] = json!("apps/admin exists"); } + let candidates = spec_candidates(root); + if !candidates.is_empty() { + facts["specCandidates"] = json!(candidates); + } for ev in &input.evidence { if ev.source.trim().is_empty() { return Err(invalid("Evidence requires a source")); @@ -21,10 +25,35 @@ pub(super) fn facts(input: &ProjectPlanInput) -> Result { } Ok(facts) } +fn spec_candidates(root: &Path) -> Vec { + const WORDS: [&str; 9] = [ + "요구사항", "기획", "정의서", "명세", "spec", "requirement", "prd", "srs", "brief", + ]; + let Ok(entries) = std::fs::read_dir(root) else { + return vec![]; + }; + let mut found = entries + .filter_map(Result::ok) + .filter(|e| e.path().is_file()) + .filter_map(|e| { + let name = e.file_name().to_string_lossy().into_owned(); + let lower = name.to_lowercase(); + let ext = Path::new(&lower).extension()?.to_str()?.to_owned(); + (["pdf", "docx", "md", "hwp", "txt"].contains(&ext.as_str()) + && WORDS.iter().any(|w| lower.contains(w))) + .then_some(name) + }) + .collect::>(); + found.sort(); + found.truncate(10); + found +} fn ask(id: &str, prompt: String, options: &[&str]) -> Value { question( id.into(), - screen_plan::header("", id), + screen_plan::header("", id) + .trim_start_matches(": ") + .to_owned(), prompt, options .iter() @@ -43,7 +72,7 @@ pub(super) fn intake(facts: &Value) -> Vec { let parts = text(&facts["parts"]); vec![ ask("workType", "What kind of work is this? Choose new project, existing feature, bug fix, Figma page implementation, or refactor (custom answer).".into(), &["new project", "feature on existing project", "bug fix", "Figma page implementation"]), - ask("sources", format!("Which optional sources should be read? Figma links, Jira issues/epics, Confluence URLs or files. Already read evidence: {facts}"), &["Use recorded sources", "No additional sources"]), + ask("sources", format!("Which optional sources should be read? Figma links, Jira issues/epics, Confluence URLs or files. Spec documents found in the project root: {}. Read them (extract PDF/DOCX text) and pass the text as evidence. Already read evidence: {facts}", facts["specCandidates"]), &["Use recorded sources", "No additional sources"]), ask("goal", format!("What should be built in one sentence? Source default: {}", text(&facts["goal"])), &[if facts["goal"].is_string() {text(&facts["goal"])} else {"Describe the goal"}, "Use source scope"]), ask("parts", format!("Which parts are in scope? Default evidence: {}", facts["partsEvidence"]), &[if parts.is_empty() {"front only"} else {parts}, "front + admin", "front + admin + API"]), ] @@ -120,23 +149,28 @@ pub(super) fn detail( .as_str() .or_else(|| feature["key"].as_str()) .unwrap_or("feature"); - for (topic, prompt) in [ - ("screens", "screens/routes and user roles"), - ("states", "empty, loading and error states"), - ("permissions", "permissions and ownership"), - ("forms", "forms and field validation"), - ( - "notifications", - "notifications and external integrations (payment, mail, maps, login providers)", - ), - ] { + let haystack = format!("{} {}", text(&feature["scope"]), feature["acceptance"]).to_lowercase(); + let has_screens = !array(&feature["screens"]).is_empty(); + let topics: [(&str, &str, &[&str], [&str; 3]); 5] = [ + ("screens", "the screens/routes and user roles (the feature lists none)", &[], ["Derive screens from the acceptance criteria", "List screens now", "Exclude this capability"]), + ("states", "empty, loading and error states", &[], ["Use recorded feature acceptance criteria", "Standard empty/loading/error states", "Exclude this capability"]), + ("permissions", "permissions and ownership", &["admin", "auth", "login", "role", "permission", "owner", "member"], ["Admins only", "Signed-in users", "Public"]), + ("forms", "forms and field validation", &["form", "input", "inquiry", "contact", "signup", "register", "submit", "search", "write", "edit"], ["Required fields with inline validation", "Use recorded feature acceptance criteria", "Exclude this capability"]), + ("notifications", "notifications and external integrations (payment, mail, maps, login providers)", &["mail", "email", "smtp", "notif", "payment", "map", "sms", "alert", "analytics", "oauth"], ["No notifications or integrations", "Email to a staff address", "Other integration (custom answer)"]), + ]; + for (topic, prompt, keywords, options) in topics { + let relevant = match topic { + "screens" => !has_screens, + "states" => true, + _ => keywords.iter().any(|k| haystack.contains(k)), + }; let key = format!("{id}.{topic}"); - if plan["facts"][&key].is_null() { - questions.push(ask(&key, format!("For {} ({id}), specify {prompt}. Screens: {}. Record a concrete decision or explicitly exclude the capability.", text(&feature["scope"]), feature["screens"]), &["Use recorded feature acceptance criteria", "Exclude this capability"])); + if relevant && plan["facts"][&key].is_null() { + questions.push(ask(&key, format!("For {} ({id}), specify {prompt}. Screens: {}. Record a concrete decision or explicitly exclude the capability.", text(&feature["scope"]), feature["screens"]), &options)); } } } - if erd::api(plan) { + if erd::api(plan) && !array(&plan["entities"]).is_empty() { for (key, topic) in [ ( "erd", diff --git a/crates/devup-mcp/src/server/project_plan/tests.rs b/crates/devup-mcp/src/server/project_plan/tests.rs index cfbf45c6..39082ad5 100644 --- a/crates/devup-mcp/src/server/project_plan/tests.rs +++ b/crates/devup-mcp/src/server/project_plan/tests.rs @@ -25,6 +25,7 @@ fn input(root: &Path, action: &str) -> ProjectPlanInput { async fn drafted(root: &Path, api: bool) -> (ProjectPlanInput, OutputPolicy, Value) { let policy = OutputPolicy::from_roots(vec![root.to_path_buf()]).unwrap(); let mut i = input(root, "intake"); + i.detail = Some("full".into()); i.evidence = vec![serde_json::from_str(include_str!("../../../tests/fixtures/project-plan/figma.json")).unwrap()]; let r = run(&i, &policy).await.unwrap(); i.plan_id = r["planId"].as_str().map(str::to_owned); @@ -47,6 +48,7 @@ async fn drafted(root: &Path, api: bool) -> (ProjectPlanInput, OutputPolicy, Val scope: "Post authoring".into(), screens: vec!["Post list".into()], acceptance: vec!["Authenticated authors publish posts".into()], + entities: vec![], }]; i.evidence.push(serde_json::from_value(json!({"source":"repo defaults","facts":{ "goal":"Source defaults cannot replace the selected goal", @@ -247,3 +249,83 @@ async fn feature_split_and_decisions_and_state_gates() { i.plan_id = Some("stale".into()); assert!(run(&i, &policy).await.is_err()); } +fn kept(decisions: &Value, id: &str) -> bool { + array(decisions) + .iter() + .any(|d| d["id"] == id && d["state"] == "pending") +} +fn decision(id: &str) -> Decision { + Decision { + id: id.into(), + reason: "Needs the user".into(), + recommended: "Keep".into(), + destructive: false, + } +} +#[tokio::test] +async fn draft_keeps_decisions() { + let dir = TestDir::new(); + let (mut i, policy, _) = drafted(dir.path(), false).await; + i.action = "draft".into(); + i.answers.clear(); + i.decisions = vec![decision("d-draft")]; + let r = run(&i, &policy).await.unwrap(); + assert!(kept(&r["plan"]["decisions"], "d-draft")); +} +#[tokio::test] +async fn confirm_keeps_decisions() { + let dir = TestDir::new(); + let (mut i, policy, _) = drafted(dir.path(), false).await; + i.decisions = vec![decision("d-confirm")]; + let r = run(&i, &policy).await.unwrap(); + assert_eq!(r["state"], "confirmed"); + assert!(kept(&r["plan"]["decisions"], "d-confirm")); +} +#[tokio::test] +async fn status_works_without_plan_id_and_next_action_follows_confirm() { + let dir = TestDir::new(); + let (i, policy, _) = drafted(dir.path(), false).await; + let r = run(&i, &policy).await.unwrap(); + assert_eq!(r["nextAction"]["arguments"]["action"], "split"); + let mut status = i.clone(); + status.action = "status".into(); + status.plan_id = None; + status.answers.clear(); + let r = run(&status, &policy).await.unwrap(); + assert_eq!(r["plan"]["state"], "confirmed"); +} +#[test] +fn no_erd_questions_when_entities_are_empty() { + let plan = json!({"facts":{"parts":"front + admin + API"},"entities":[],"issues":[], + "features":[{"id":"news","scope":"News list","screens":["List"],"acceptance":[]}]}); + let questions = rounds::detail(&plan, &[]).unwrap(); + assert!(questions.iter().all(|q| q["id"] != "erd" && q["id"] != "tenancy")); +} +#[test] +fn question_headers_have_no_leading_colon() { + for q in rounds::intake(&json!({})) { + assert!(!text(&q["header"]).starts_with(':'), "{}", q["header"]); + } +} + +#[tokio::test] +async fn status_lists_open_screen_plan_questions_in_the_proposal() { + let dir = TestDir::new(); + let (i, policy, _) = drafted(dir.path(), false).await; + let mut status = i.clone(); + status.action = "status".into(); + status.answers.clear(); + std::fs::create_dir_all(dir.path().join(".devup")).unwrap(); + std::fs::write( + dir.path().join(".devup/screen-plan.json"), + json!({"kind":"devup-screen-plan","schemaVersion":1,"state":"draft","questions":[ + {"id":"route:9:1","header":"Route","question":"Which URL?","options":[ + {"label":"/a (Recommended)","description":"a"},{"label":"/b","description":"b"}]} + ]}) + .to_string(), + ) + .unwrap(); + let r = run(&status, &policy).await.unwrap(); + assert_eq!(r["screenPlanQuestionIds"], json!(["route:9:1"])); + assert!(text(&r["proposalMarkdown"]).contains("Which URL?")); +} diff --git a/crates/devup-mcp/src/server/project_plan/units.rs b/crates/devup-mcp/src/server/project_plan/units.rs index 3f966e89..3cb9e264 100644 --- a/crates/devup-mcp/src/server/project_plan/units.rs +++ b/crates/devup-mcp/src/server/project_plan/units.rs @@ -11,6 +11,75 @@ fn unit( "layer":layer,"state":"pending","verification":[], "checklist":["build", "lint", "typecheck", "tests", "devup_ui_validate", "runtime/visual audit", "devup_stack_diff: zero drift"]}) } +fn feature_entities(plan: &Value, feature: &Value) -> Vec { + if !erd::api(plan) { + return vec![]; + } + let explicit = array(&feature["entities"]) + .iter() + .map(|v| text(v).to_owned()) + .collect::>(); + if !explicit.is_empty() { + return explicit; + } + let haystack = format!( + "{} {} {}", + text(&feature["id"]), + text(&feature["scope"]), + feature["acceptance"] + ) + .to_lowercase(); + array(&plan["entities"]) + .iter() + .map(|e| text(&e["name"]).to_owned()) + .filter(|name| haystack.contains(&name.to_lowercase())) + .collect() +} +fn belongs(key: &str, owner: &str) -> bool { + !key.contains('.') || key.starts_with(&format!("{owner}.")) +} +fn unit_owner(u: &Value) -> String { + let feature = text(&u["feature"]); + if feature.is_empty() { text(&u["id"]) } else { feature }.to_owned() +} +fn pick(source: &Value, owner: &str, only: &[&str]) -> Value { + let map = source.as_object().cloned().unwrap_or_default(); + Value::Object( + map.into_iter() + .filter(|(k, _)| belongs(k, owner) && (only.is_empty() || only.contains(&k.as_str()) || k.contains('.'))) + .collect(), + ) +} +fn slim_context(plan: &Value, u: &Value) -> Value { + let owner = unit_owner(u); + json!({ + "fullPlan": ".devup/project-plan.json", + "answers": pick(&plan["answers"], &owner, &[]), + "facts": pick(&plan["facts"], &owner, &["goal", "parts", "workType"]), + "decisions": plan["decisions"], + "backendCommands": plan["backendCommands"], + "backendNotes": plan["backendNotes"], + "modelsWorkingDirectory": plan["modelsWorkingDirectory"], + }) +} +fn unit_entities(plan: &Value, u: &Value) -> Vec { + let names = array(&u["entities"]); + array(&plan["entities"]) + .iter() + .filter(|e| names.is_empty() || names.contains(&e["name"])) + .cloned() + .collect() +} +fn slim_screens(screen: &Value) -> Value { + if screen.is_null() { + return Value::Null; + } + let routes = array(&screen["routes"]) + .iter() + .map(|r| json!({"url": r["url"], "nodeIds": r["nodeIds"], "file": r["file"]})) + .collect::>(); + json!({"fullPlan": ".devup/screen-plan.json", "state": screen["state"], "routes": routes}) +} fn safe_id(id: &str) -> bool { !id.is_empty() && id.len() <= 80 @@ -76,24 +145,46 @@ pub(super) fn split( json!(["Shared foundations verified"]), "shared", )); + let mut model_units = std::collections::BTreeSet::new(); for feature in array(&plan["features"]) { let id = text(&feature["id"]); - let mut previous = "shared".to_owned(); - let layers = if erd::api(plan) { - vec!["models", "api", "client", "front"] - } else { + let used = feature_entities(plan, feature); + let mut previous = vec!["shared".to_owned()]; + if !used.is_empty() { + for name in &used { + let mid = format!("model-{name}"); + if model_units.insert(mid.clone()) { + let mut u = unit( + mid.clone(), + format!("Table {name}: model, migration and entity"), + vec!["shared".into()], + json!(["Model validates and migrates"]), + "models", + ); + u["entities"] = json!([name]); + units.push(u); + } + } + previous = used.iter().map(|n| format!("model-{n}")).collect(); + } + let layers = if used.is_empty() { vec!["front"] + } else { + vec!["api", "client", "front"] }; for layer in layers { let uid = format!("{id}-{layer}"); - units.push(unit( + let mut u = unit( uid.clone(), format!("{}: {layer}", text(&feature["scope"])), - vec![previous], + previous, feature["acceptance"].clone(), layer, - )); - previous = uid; + ); + u["feature"] = json!(id); + u["entities"] = json!(used); + units.push(u); + previous = vec![uid]; } } } @@ -111,9 +202,9 @@ pub(super) fn split( text(&u["scope"]), u["dependencies"], text(&plan["unitQuestions"]), - json!({"answers":plan["answers"],"facts":plan["facts"],"decisions":plan["decisions"],"backendCommands":plan["backendCommands"],"modelsWorkingDirectory":plan["modelsWorkingDirectory"]}), - erd::mermaid(array(&plan["entities"])), - screen_context, + slim_context(plan, u), + erd::mermaid(&unit_entities(plan, u)), + slim_screens(&screen_context), json!({"acceptance":u["acceptance"],"checklist":u["checklist"]}) ); stage( diff --git a/crates/devup-mcp/src/server/projection/asset_reference_names.rs b/crates/devup-mcp/src/server/projection/asset_reference_names.rs index fcc15f8f..6a2cc56e 100644 --- a/crates/devup-mcp/src/server/projection/asset_reference_names.rs +++ b/crates/devup-mcp/src/server/projection/asset_reference_names.rs @@ -16,8 +16,14 @@ fn with_host_safe_asset_paths(code: &str) -> String { }; let after = at + prefix.len(); safe.push_str(&rest[..after]); + let opener = rest[..at].chars().last().or_else(|| safe.chars().last()); + let terminators: &[char] = match opener { + Some('"') => &['"'], + Some('\'') => &['\''], + _ => &['"', '\'', ')'], + }; let mut end = rest[after..] - .find(['"', '\'', ')']) + .find(terminators) .map_or(rest.len(), |offset| after + offset); // srcSet contains several URLs followed by numeric density/width // descriptors. Keep each URL separate while retaining spaces inside diff --git a/crates/devup-mcp/src/server/projection/completion.rs b/crates/devup-mcp/src/server/projection/completion.rs index 5381b69d..ebb52684 100644 --- a/crates/devup-mcp/src/server/projection/completion.rs +++ b/crates/devup-mcp/src/server/projection/completion.rs @@ -24,6 +24,7 @@ pub(super) async fn complete_operation( .filter(|source| source.is_object()) { attach_bridge_source(source, payload); + attach_node_page(source, payload); } // What this export wrote for each marked node, so the audit can tell // code drift from a converter bug later. @@ -97,10 +98,61 @@ fn attach_bridge_source(source: &mut Value, payload: &CollectedPayload) { } } source["fileName"] = json!(served.file_name); - source["pageName"] = json!(served.page_name); + // The page the user has open in Figma - not the page the node lives on. + // `nodePage` (when the snapshot reaches it) says where the node is. + source["viewerPageName"] = json!(served.page_name); source["bridgeReads"] = json!(served.reads); } +/// The page the requested node actually belongs to, walked up from the +/// snapshot's parent links, with a note when it is not the page the viewer has +/// open or when its name says trash/archive/draft. +fn attach_node_page(source: &mut Value, payload: &CollectedPayload) { + let Some(start) = payload.target.node_id.as_deref() else { + return; + }; + let mut current = Some(start.to_owned()); + let mut visited = std::collections::BTreeSet::new(); + let mut page: Option<(String, String)> = None; + while let Some(id) = current { + if !visited.insert(id.clone()) { + break; + } + let Some(node) = payload.snapshot.nodes.get(&id) else { + break; + }; + if node.node_type == "PAGE" { + page = Some(( + node.id.clone(), + node.typed_view().name().unwrap_or_default().to_owned(), + )); + break; + } + current = node.typed_view().string("parentId").map(str::to_owned); + } + let Some((id, name)) = page else { + return; + }; + source["nodePage"] = json!({"id": id, "name": name}); + let lowered = name.to_lowercase(); + if ["trash", "archive", "draft", "휴지통", "보관", "초안"] + .iter() + .any(|keyword| lowered.contains(keyword)) + { + source["nodePageNote"] = json!(format!( + "The node lives on the page \"{name}\", whose name marks it as trash, archive or draft." + )); + } else if source + .get("viewerPageName") + .and_then(Value::as_str) + .is_some_and(|viewer| viewer != name) + { + source["nodePageNote"] = json!(format!( + "viewerPageName is the page open in Figma; the node itself lives on \"{name}\"." + )); + } +} + /// The file key an answer may state: the plugin's own report when the bridge /// read the file, and the requested key when Figma itself served it. fn vouched_file_key(payload: &CollectedPayload) -> Option<&str> { @@ -110,3 +162,77 @@ fn vouched_file_key(payload: &CollectedPayload) -> Option<&str> { } } + +#[cfg(test)] +mod node_page_tests { + use super::*; + use devup_mcp_figma::{ + CollectionScope, CollectionStats, FigmaTarget, PayloadCompleteness, RawNode, Snapshot, + }; + + fn payload_on_page(page_name: &str) -> CollectedPayload { + let nodes = [("0:1", "PAGE", page_name, None), ("1:1", "FRAME", "Home", Some("0:1"))] + .into_iter() + .map(|(id, node_type, name, parent)| { + ( + id.to_owned(), + RawNode { + id: id.to_owned(), + node_type: node_type.to_owned(), + fields: serde_json::from_value(json!({"name": name, "parentId": parent})) + .unwrap(), + extra: Default::default(), + field_errors: Default::default(), + }, + ) + }) + .collect(); + CollectedPayload { + target: FigmaTarget { + file_key: "k".into(), + node_id: Some("1:1".into()), + branch_key: None, + }, + scope: CollectionScope::Node, + metadata: json!({}), + snapshot: Snapshot { + file_key: "k".into(), + version: None, + roots: vec!["0:1".into()], + nodes, + diagnostics: vec![], + }, + variables: None, + styles: None, + completeness: PayloadCompleteness::ResolvedValuesOnly, + source_version: None, + stats: CollectionStats::default(), + assets: vec![], + reference_png: None, + failures: vec![], + } + } + + #[test] + fn a_trash_page_is_named_and_flagged() { + let mut source = json!({"viewerPageName": "Trash old"}); + attach_node_page(&mut source, &payload_on_page("Trash old")); + assert_eq!(source["nodePage"], json!({"id": "0:1", "name": "Trash old"})); + assert!(source["nodePageNote"].as_str().unwrap().contains("trash, archive or draft")); + } + + #[test] + fn a_node_off_the_viewer_page_says_so() { + let mut source = json!({"viewerPageName": "Cover"}); + attach_node_page(&mut source, &payload_on_page("Screens")); + assert_eq!(source["nodePage"]["name"], "Screens"); + assert!(source["nodePageNote"].as_str().unwrap().contains("lives on \"Screens\"")); + } + + #[test] + fn the_viewer_page_needs_no_note() { + let mut source = json!({"viewerPageName": "Screens"}); + attach_node_page(&mut source, &payload_on_page("Screens")); + assert!(source.get("nodePageNote").is_none()); + } +} diff --git a/crates/devup-mcp/src/server/projection/feedback_theme_tests.rs b/crates/devup-mcp/src/server/projection/feedback_theme_tests.rs index 5a13d1f2..f839fdb9 100644 --- a/crates/devup-mcp/src/server/projection/feedback_theme_tests.rs +++ b/crates/devup-mcp/src/server/projection/feedback_theme_tests.rs @@ -253,6 +253,14 @@ fn feedback_theme_mode_validation_is_additive_and_rejects_unprovable_requests() assert!(validate("unknown", &["devupJson".into()], None).is_err()); } +#[test] +fn r38_missing_only_accepts_a_file_wide_devup_json_without_code_output() { + use theme_export::validate; + assert!(validate("missing-only", &["devupJson".into()], Some("/p")).is_ok()); + assert!(validate("missing-only", &["devupJson".into()], None).is_err()); + assert!(validate("used-only", &["devupJson".into()], Some("/p")).is_err()); +} + #[tokio::test] async fn feedback_theme_resource_retry_preserves_mode_and_required_code_outputs() { let mut op = operation(&["tsx", "devupJson"]); diff --git a/crates/devup-mcp/src/server/projection/project_node_and_theme_outputs.rs b/crates/devup-mcp/src/server/projection/project_node_and_theme_outputs.rs index 136c20be..dcfaf5eb 100644 --- a/crates/devup-mcp/src/server/projection/project_node_and_theme_outputs.rs +++ b/crates/devup-mcp/src/server/projection/project_node_and_theme_outputs.rs @@ -196,6 +196,9 @@ pub(super) fn project_node_and_theme_outputs( pending_text_outputs.insert("componentTsx".to_owned(), output.tsx.clone()); } result.insert("componentTsx".to_owned(), json!(output.tsx)); + if let Some(summary) = component_set_variants(&payload.snapshot, node_id) { + result.insert("componentSetVariants".to_owned(), summary); + } } Err(error) => { failures.push(projection_failure(payload, node_id, "componentTsx", &error)) @@ -277,3 +280,52 @@ pub(super) fn project_node_and_theme_outputs( warnings, )) } + +fn component_set_variants(snapshot: &devup_mcp_figma::Snapshot, node_id: &str) -> Option { + let set = snapshot.nodes.get(node_id)?; + let view = set.typed_view(); + if view.node_type() != "COMPONENT_SET" { + return None; + } + let variants: Vec = view + .child_ids() + .filter_map(|id| snapshot.nodes.get(id)) + .map(|node| { + json!({ + "nodeId": node.id, + "name": node.typed_view().name(), + "variantProperties": node.typed_view().value("variantProperties"), + }) + }) + .collect(); + Some(json!({ + "nodeId": node_id, + "variantCount": variants.len(), + "defaultVariant": view.value("defaultVariant").and_then(|v| v.get("name")), + "variants": variants, + "note": "This node is a component set. Export a single variant by nodeId to get that variant's own tsx.", + })) +} + +#[cfg(test)] +mod r32_tests { + use super::*; + + #[test] + fn component_set_export_lists_its_variants() { + let snapshot: devup_mcp_figma::Snapshot = serde_json::from_value(json!({ + "fileKey": "k", "version": null, "roots": ["1:0"], "diagnostics": [], + "nodes": { + "1:0": {"id":"1:0","type":"COMPONENT_SET","fields":{"name":"Button","childrenIds":["1:1","1:2"],"defaultVariant":{"name":"State=Default"}}}, + "1:1": {"id":"1:1","type":"COMPONENT","fields":{"name":"State=Default","variantProperties":{"State":"Default"}}}, + "1:2": {"id":"1:2","type":"COMPONENT","fields":{"name":"State=Hover","variantProperties":{"State":"Hover"}}} + } + })) + .unwrap(); + let summary = component_set_variants(&snapshot, "1:0").unwrap(); + assert_eq!(summary["variantCount"], 2); + assert_eq!(summary["defaultVariant"], "State=Default"); + assert_eq!(summary["variants"][1]["nodeId"], "1:2"); + assert!(component_set_variants(&snapshot, "1:1").is_none()); + } +} diff --git a/crates/devup-mcp/src/server/projection/project_responsive_output.rs b/crates/devup-mcp/src/server/projection/project_responsive_output.rs index e0cb0547..f45ac485 100644 --- a/crates/devup-mcp/src/server/projection/project_responsive_output.rs +++ b/crates/devup-mcp/src/server/projection/project_responsive_output.rs @@ -242,11 +242,26 @@ pub(super) fn project_responsive_output(input: ResponsiveOutput<'_>) -> Responsi if outputs.iter().any(|output| output == "responsiveTsx") && !result.contains_key("responsiveTsx") { - return Err(unavailable_output( + let mut error = unavailable_output( payload, "responsiveTsx", "The snapshot has no mergeable breakpoint frames. Request tsx for individual screens, or re-collect a screen with its breakpoint siblings.", - )); + ); + let widths: Vec<_> = payload + .snapshot + .roots + .iter() + .filter_map(|id| { + let node = payload.snapshot.nodes.get(id)?; + Some(json!({"nodeId": id, "width": node.typed_view().number("width")})) + }) + .collect(); + error.details["collectedFrames"] = json!(widths); + error.details["mergeRefusal"] = json!("Frames merge only when the collected roots are siblings drawn at distinct widths; a confirmed screen plan grouping does not change that."); + error.details["nextAction"] = json!({"tool": "devup_figma_export", + "arguments": {"frameIds": payload.snapshot.roots, "outputs": ["tsx"]}, + "how": "Export each collected frame as tsx and combine them by hand, or re-collect the Section so both breakpoint frames are captured."}); + return Err(error); } Ok(( result, diff --git a/crates/devup-mcp/src/server/projection/resource_delivery.rs b/crates/devup-mcp/src/server/projection/resource_delivery.rs index b95412d2..2086789d 100644 --- a/crates/devup-mcp/src/server/projection/resource_delivery.rs +++ b/crates/devup-mcp/src/server/projection/resource_delivery.rs @@ -117,6 +117,7 @@ pub(super) async fn apply_delivery( "basis":include_str!("../text/projection-apply_delivery-e5a0fbb2.md")}}); } resource_diagnostic_details(result, &mut outputs, &artifact.artifact_id)?; + spill_oversized_fields(result, &mut outputs, &artifact.artifact_id)?; let projection_key = projection_key(&outputs); materialize_asset_resource_references(result, &mut outputs, &artifact.artifact_id)?; let reservation = artifact_store @@ -246,6 +247,98 @@ pub(super) async fn apply_delivery( Ok(Some(DeliveryAttachment { reservation })) } +const BODY_FIELDS: [&str; 12] = [ + "tsx", + "componentTsx", + "responsiveTsx", + "devupJson", + "rawSnapshot", + "rawPayload", + "sourceMap", + "designFingerprints", + "designChanges", + "assetManifest", + "pageScaffold", + "referencePng", +]; + +const KEPT_INLINE: [&str; 10] = [ + "status", + "nextAction", + "resources", + "deliverable", + "artifactId", + "outputPaths", + "quality", + "server", + "metadataDelivery", + "envelopeOverflow", +]; + +const HANDLED_LATER: [&str; 4] = ["imageFillAudit", "imageAudit", "assetCssMap", "webReadyImages"]; + +fn envelope_bytes(result: &Value) -> usize { + let mut probe = result.clone(); + if let Some(object) = probe.as_object_mut() { + for field in BODY_FIELDS { + object.remove(field); + } + } + if let Some(frames) = probe.get_mut("frames").and_then(Value::as_array_mut) { + for frame in frames.iter_mut().filter_map(Value::as_object_mut) { + for field in ["tsx", "componentTsx", "sourceMap", "pageScaffold"] { + frame.remove(field); + } + } + } + serde_json::to_vec(&probe).map_or(0, |bytes| bytes.len()) +} + +fn spill_oversized_fields( + result: &mut Value, + outputs: &mut Vec, + artifact_id: &str, +) -> Result<(), DevupError> { + let budget = super::delivery::MAX_INLINE_TOTAL_BYTES / 2; + if envelope_bytes(result) <= budget { + return Ok(()); + } + let mut spilled = serde_json::Map::new(); + while envelope_bytes(result) > budget { + let largest = result + .as_object() + .into_iter() + .flatten() + .filter(|(key, _)| { + !KEPT_INLINE.contains(&key.as_str()) + && !BODY_FIELDS.contains(&key.as_str()) + && !HANDLED_LATER.contains(&key.as_str()) + }) + .map(|(key, value)| (key.clone(), value.to_string().len())) + .max_by_key(|(_, bytes)| *bytes); + let Some((key, bytes)) = largest.filter(|(_, bytes)| *bytes > 1024) else { + break; + }; + let original = result[&key].take(); + result[&key] = json!({"state":"resource-details","originalBytes":bytes, + "jsonPointer":format!("/{key}"),"uriPath":"/envelopeOverflow/uri"}); + spilled.insert(key, original); + } + if spilled.is_empty() { + return Ok(()); + } + let fields = spilled.keys().cloned().collect::>(); + let output = ProjectedOutput::text( + "envelope-overflow.json", + "application/json", + encode_projected_json(&Value::Object(spilled))?, + ); + result["envelopeOverflow"] = json!({"uri":output.manifest_uri(artifact_id),"fields":fields, + "reason":"The response metadata exceeded the inline limit; these fields are complete in the linked resource."}); + outputs.push(output); + Ok(()) +} + pub(super) async fn rollback_delivery( _artifact_store: &ArtifactStore, _artifact: &ArtifactLookup, diff --git a/crates/devup-mcp/src/server/projection/theme_export.rs b/crates/devup-mcp/src/server/projection/theme_export.rs index 43973b99..c44f909f 100644 --- a/crates/devup-mcp/src/server/projection/theme_export.rs +++ b/crates/devup-mcp/src/server/projection/theme_export.rs @@ -19,7 +19,9 @@ pub fn validate( false, )); } + let file_wide = mode == "missing-only" && outputs.iter().any(|o| o == "devupJson"); if mode != "full" + && !file_wide && (!outputs.iter().any(|o| o == "devupJson") || !outputs .iter() @@ -66,6 +68,25 @@ fn usage(result: &Map) -> Result<(TokenUsage, usize), DevupError> Ok((usage, count)) } +/// Every token name a generated devup.json defines. +fn all_tokens(json: &str) -> TokenUsage { + let mut usage = TokenUsage::default(); + let Ok(source) = serde_json::from_str::(json) else { + return usage; + }; + if let Some(typography) = source.pointer("/theme/typography").and_then(Value::as_object) { + usage.typography.extend(typography.keys().cloned()); + } + for category in ["colors", "length", "shadow"] { + if let Some(modes) = source["theme"][category].as_object() { + for values in modes.values().filter_map(Value::as_object) { + usage.tokens.extend(values.keys().cloned()); + } + } + } + usage +} + pub fn generate( snapshot: &devup_mcp_devup_ui::theme::VariableSnapshot, scope: devup_mcp_devup_ui::theme::ThemeScope, @@ -74,6 +95,9 @@ pub fn generate( ) -> Result { if matches!(options.mode.as_str(), "used-only" | "missing-only") { let (usage, count) = usage(result)?; + if count == 0 && options.mode == "missing-only" { + return generate_devup_json(snapshot, scope); + } if count == 0 { return Err(DevupError::new( ErrorCode::DevupCodegenFailed, @@ -145,8 +169,11 @@ pub fn prepare( json!({"skippedStyles":output.skipped_styles}), )); } - let (usage, code_count) = usage(result)?; - if options.mode != "full" && !options.mode.is_empty() { + let (mut usage, mut code_count) = usage(result)?; + if code_count == 0 && options.mode == "missing-only" { + usage = all_tokens(&output.json); + code_count = 1; + } else if options.mode != "full" && !options.mode.is_empty() { if code_count == 0 { return Err(DevupError::new( ErrorCode::DevupCodegenFailed, diff --git a/crates/devup-mcp/src/server/screen_plan.rs b/crates/devup-mcp/src/server/screen_plan.rs index 4ffa1acf..c3562fae 100644 --- a/crates/devup-mcp/src/server/screen_plan.rs +++ b/crates/devup-mcp/src/server/screen_plan.rs @@ -32,6 +32,8 @@ include!("screen_plan/routes.rs"); include!("screen_plan/app_select.rs"); include!("screen_plan/draft.rs"); include!("screen_plan/actions.rs"); +include!("screen_plan/remap.rs"); include!("screen_plan/status.rs"); include!("screen_plan/draft_plan_with.rs"); include!("screen_plan/plan_route_group.rs"); +include!("screen_plan/state_variants.rs"); diff --git a/crates/devup-mcp/src/server/screen_plan/actions.rs b/crates/devup-mcp/src/server/screen_plan/actions.rs index ae5feed3..eebb82b9 100644 --- a/crates/devup-mcp/src/server/screen_plan/actions.rs +++ b/crates/devup-mcp/src/server/screen_plan/actions.rs @@ -108,6 +108,8 @@ pub(super) fn confirm(input: &ScreenPlanInput, policy: &OutputPolicy) -> Result< .unwrap_or(text(&plan["appDir"])) .to_owned(); plan["appDir"] = json!(app_dir); + let folds = state_folds(&questions, &actions); + let folded = folded_ids(&folds); let mut routes = Vec::new(); let mut screen_routes = BTreeMap::new(); let screens = array(&plan["screens"]).to_vec(); @@ -119,7 +121,7 @@ pub(super) fn confirm(input: &ScreenPlanInput, policy: &OutputPolicy) -> Result< .map(text) .collect::>(); deciding.extend(screen["groupingQuestion"].as_str()); - if nothing_here || fixed(&deciding) { + if nothing_here || fixed(&deciding) || folded.contains(&id) { continue; } let chosen_app = actions @@ -214,6 +216,7 @@ pub(super) fn confirm(input: &ScreenPlanInput, policy: &OutputPolicy) -> Result< "screen": if separate { first.clone() } else { id.clone() }, "name": group_frames[0]["name"], "nodeIds": group_frames.iter().map(|frame| frame["nodeId"].clone()).collect::>(), + "pages": distinct_pages(&group_frames), "drawnWidths": group_frames.iter().filter_map(|frame| frame["width"].as_f64()).map(|width| width.round() as i64).collect::>(), "implementation": implementation, "hostScreen": host, @@ -226,6 +229,7 @@ pub(super) fn confirm(input: &ScreenPlanInput, policy: &OutputPolicy) -> Result< })); } } + record_states(&mut routes, &folds); let mut excluded = Vec::new(); for row in array(&plan["excluded"]) { let node = text(&row["nodeId"]).to_owned(); @@ -261,6 +265,7 @@ pub(super) fn confirm(input: &ScreenPlanInput, policy: &OutputPolicy) -> Result< } // One URL per app: the public site and the console may both have `/`. let mut seen = BTreeMap::<(String, String), String>::new(); + let mut collisions = Vec::new(); for row in &routes { if let Some(url) = row["url"].as_str() && let Some(other) = seen.insert( @@ -269,9 +274,12 @@ pub(super) fn confirm(input: &ScreenPlanInput, policy: &OutputPolicy) -> Result< ) && other != text(&row["screen"]) { - return Err(duplicate_route(&plan, url, &other, &row["screen"], &row["name"])); + collisions.push((url.to_owned(), other, row["screen"].clone(), row["name"].clone())); } } + if !collisions.is_empty() { + return Err(duplicate_routes(&plan, &collisions)); + } for row in routes .iter_mut() .filter(|row| row["implementation"] == "state") @@ -357,6 +365,7 @@ pub(super) fn confirm(input: &ScreenPlanInput, policy: &OutputPolicy) -> Result< plan["routes"] = json!(routes); plan["excluded"] = json!(excluded); plan["designFixes"] = json!(design_fixes); + plan["confirmedPage"] = confirmed_page(&plan); plan["status"] = json!("confirmed"); plan["confirmedAt"] = json!(now()); refresh_inventory_checks(&mut plan); @@ -364,9 +373,29 @@ pub(super) fn confirm(input: &ScreenPlanInput, policy: &OutputPolicy) -> Result< plan["assumptions"] = json!([]); } let written = write_plan(policy, root, &plan)?; + let work_queue = routes + .iter() + .filter(|route| route["url"].is_string()) + .map(|route| { + json!({ + "do": "build", + "url": route["url"], + "implementation": route["implementation"], + "file": route["file"], + "nodeIds": route["nodeIds"], + "drawnWidths": route["drawnWidths"], + }) + }) + .collect::>(); + let urls = work_queue + .iter() + .map(|item| text(&item["url"]).to_owned()) + .collect::>(); Ok(json!({ "status": "confirmed", "planId": plan["planId"], + "workQueue": work_queue, + "confirmedPage": plan["confirmedPage"], "routes": routes, "markdownTable": route_markdown(&routes), "frameInventory": plan["frameInventory"], @@ -380,7 +409,11 @@ pub(super) fn confirm(input: &ScreenPlanInput, policy: &OutputPolicy) -> Result< "customAnswers": custom, "rule": IMPLEMENT_INSTRUCTION, "writtenTo": written, - "nextAction": {"how": IMPLEMENT_INSTRUCTION}, + "nextAction": { + "how": IMPLEMENT_INSTRUCTION, + "buildAll": urls, + "until": "every URL in workQueue is built; do not stop after the first route. Check progress with action status.", + }, })) } @@ -396,9 +429,9 @@ pub(super) fn assume(input: &ScreenPlanInput, policy: &OutputPolicy) -> Result Result Result>(), "assumptions": plan["assumptions"], + "remapped": remapped, "dependencies": plan["dependencies"], "writtenTo": written, "how": "Keep implementing. Every assumption appears in status and must be in your final report.", @@ -623,40 +669,54 @@ fn suggested_route(name: &str, screen: &str, taken: &str) -> String { } } -/// Two screens on one URL: the error names both screens, the questions that -/// place each, and a distinct URL for the second, so the caller can retry -/// confirm with one changed answer instead of guessing. -fn duplicate_route(plan: &Value, url: &str, first: &str, second: &Value, name: &Value) -> DevupError { - let second = text(second); - let first_name = array(&plan["screens"]) - .iter() - .find(|screen| screen["id"] == first) - .map(|screen| screen["name"].clone()) - .unwrap_or(Value::Null); - let suggestion = suggested_route(text(name), second, url); - let questions = placing_questions(plan, second); - let retry = questions - .iter() - .find(|id| !id.starts_with("collision:")) - .map(|id| json!({id.as_str(): suggestion})) - .unwrap_or(json!({})); +/// Screens on one URL: the error names every colliding pair, the questions +/// that place each, and a distinct URL for the later screen, so the caller can +/// retry confirm with all changed answers at once instead of one per call. +fn duplicate_routes(plan: &Value, collisions: &[(String, String, Value, Value)]) -> DevupError { + let mut conflicts = Vec::new(); + let mut retry = serde_json::Map::new(); + let mut taken = collisions.iter().map(|c| c.0.clone()).collect::>(); + for (url, first, second, name) in collisions { + let second = text(second); + let first_name = array(&plan["screens"]) + .iter() + .find(|screen| screen["id"] == first.as_str()) + .map(|screen| screen["name"].clone()) + .unwrap_or(Value::Null); + let mut suggestion = suggested_route(text(name), second, url); + while !taken.insert(suggestion.clone()) { + suggestion = format!("{suggestion}-{}", second.replace([':', ';'], "-")); + } + let questions = placing_questions(plan, second); + if let Some(id) = questions.iter().find(|id| !id.starts_with("collision:")) { + retry.insert(id.clone(), json!(suggestion)); + } + conflicts.push(json!({ + "url": url, + "screens": [ + {"screen": first, "name": first_name, "questions": placing_questions(plan, first)}, + {"screen": second, "name": name, "questions": questions, "suggestedUrl": suggestion}, + ], + })); + } + let (url, first, second, _) = &collisions[0]; + let retry = Value::Object(retry); DevupError::with_details( ErrorCode::DevupInvalidInput, format!( - "Two screens resolve to {url} ({first} and {second}). Nothing was written. Retry confirm with the same answers plus {retry}, or show the user both screens and ask which one owns {url}." + "Two screens resolve to {url} ({first} and {}); {} URL conflict(s) in total. Nothing was written. Retry confirm with the same answers plus {retry}, or show the user the conflicting screens and ask which one owns each URL.", + text(second), + collisions.len() ), false, json!({ "url": url, - "conflict": [ - {"screen": first, "name": first_name, "questions": placing_questions(plan, first)}, - {"screen": second, "name": name, "questions": questions, "suggestedUrl": suggestion}, - ], + "conflict": conflicts[0]["screens"], + "conflicts": conflicts, "retryAnswers": retry, }), ) } - fn screen_table(plan: &Value) -> Vec { array(&plan["screens"]) .iter() @@ -689,7 +749,7 @@ fn cell(value: &Value) -> String { fn route_markdown(routes: &[Value]) -> String { let mut out = String::from( - "| URL | Figma screen | nodeIds | widths | file | implementation | app |\n|---|---|---|---|---|---|---|\n", + "| URL | Figma screen | Figma page | nodeIds | widths | file | implementation | app |\n|---|---|---|---|---|---|---|---|\n", ); for row in routes { let url = if row["implementation"] == "state" { @@ -698,8 +758,9 @@ fn route_markdown(routes: &[Value]) -> String { cell(&row["url"]) }; out.push_str(&format!( - "| {url} | {} | {} | {} | {} | {} | {} |\n", + "| {url} | {} | {} | {} | {} | {} | {} | {} |\n", cell(&row["name"]), + cell(&row["pages"]), cell(&row["nodeIds"]), cell(&row["drawnWidths"]), cell(&row["file"]), diff --git a/crates/devup-mcp/src/server/screen_plan/draft.rs b/crates/devup-mcp/src/server/screen_plan/draft.rs index 86eb9d7f..a23ff7dc 100644 --- a/crates/devup-mcp/src/server/screen_plan/draft.rs +++ b/crates/devup-mcp/src/server/screen_plan/draft.rs @@ -36,6 +36,9 @@ fn exclusion_reason( return Some("zero opacity in Figma".into()); } let mut names = vec![("frame", screen.name.as_str())]; + if let Some(page) = screen.page_name.as_deref() { + names.push(("page", page)); + } if let Some(section) = screen.section_name.as_deref() { names.push(("section", section)); } @@ -74,6 +77,8 @@ pub(super) fn screen_from_explore(node: &Value) -> Option { ancestors: parent.into_iter().collect(), visible: node["visible"].as_bool(), opacity: node["opacity"].as_f64(), + page_id: node["pageId"].as_str().map(str::to_owned), + page_name: node["pageName"].as_str().map(str::to_owned).or_else(|| breadcrumb.first().cloned()), breadcrumb, navigation_content_width: node["navigationContentWidth"].as_f64(), ..PlanScreenInput::default() @@ -641,5 +646,12 @@ fn per_round(plan: &Value) -> usize { fn required(question: &Value, actions: &BTreeMap) -> bool { array(&question["when"]) .iter() - .all(|condition| actions.get(text(&condition["question"])) == Some(&condition["action"])) + .all(|condition| { + let actual = actions.get(text(&condition["question"])); + actual == Some(&condition["action"]) + || (condition["action"]["structure"] == "placed" + && actual.is_some_and(|action| { + matches!(text(&action["structure"]), "keep" | "states") + })) + }) } diff --git a/crates/devup-mcp/src/server/screen_plan/draft_plan_with.rs b/crates/devup-mcp/src/server/screen_plan/draft_plan_with.rs index 25235c07..70b9930d 100644 --- a/crates/devup-mcp/src/server/screen_plan/draft_plan_with.rs +++ b/crates/devup-mcp/src/server/screen_plan/draft_plan_with.rs @@ -9,13 +9,33 @@ pub(super) fn draft_plan_with( source: Value, notes: Option<&str>, ) -> Result { - if ["projection", "candidates", "atCeiling"] + let hit = ["projection", "candidates", "atCeiling"] .iter() - .any(|field| source["truncation"][field] == true) - { - return Err(invalid( - "The frame inventory is incomplete: exploration was truncated or reached its ceiling. Explore a smaller Section or supply the complete frame list before drafting.", - )); + .filter(|field| source["truncation"][**field] == true) + .copied() + .collect::>(); + if !hit.is_empty() { + let mut sections = Vec::::new(); + for screen in screens { + let label = match (screen.section_name.as_deref(), screen.section.as_deref()) { + (Some(name), Some(id)) => format!("\"{name}\" ({id})"), + (Some(name), None) => format!("\"{name}\""), + (None, Some(id)) => id.to_owned(), + _ => continue, + }; + if !sections.contains(&label) { + sections.push(label); + } + } + let listed = if sections.is_empty() { + "none of the returned frames sits in a Section".to_owned() + } else { + sections.join(", ") + }; + return Err(invalid(format!( + "The frame inventory is incomplete: exploration was truncated or reached its ceiling ({}). Sections seen so far: {listed}. Call draft with one Section link at a time (devup_figma_search lists the page's top-level SECTION/FRAME nodes) or supply the complete frame list before drafting.", + hit.join(", ") + ))); } let mut ids = BTreeSet::new(); if screens @@ -205,6 +225,8 @@ pub(super) fn draft_plan_with( .map(|(screen, reason)| { json!({"nodeId": screen.node_id, "name": screen.name, "width": screen.width, "section": screen.section_name.clone().or_else(|| screen.section.clone()), + "pageId": screen.page_id, + "pageName": screen.page_name.clone().or_else(|| screen.breadcrumb.first().cloned()), "reason": reason}) }) .collect::>(); @@ -333,26 +355,18 @@ pub(super) fn draft_plan_with( "{} Is that intended, or should the Figma file be fixed? This plan does not decide which frame is right.", oddity.facts ), - vec![ - choice( - oddity.keep, - oddity.keep_description.clone(), - json!({"structure": "keep"}), - ), - choice( - FIX_FIGMA, - "These frames are not built. They are listed under Design fixes requested; everything else goes on.", - json!({"designFix": true}), - ), - ], + structure_choices(&oddity), "structure", ); asked["fix"] = json!({"frames": frames, "text": oddity.fix}); questions.push(with_when(asked, &base_when)); for index in &oddity.screens { - drafts[*index] - .extra_when - .push(when(&id, oddity.keep, json!({"structure": "keep"}))); + let placed = oddity.states.is_some() && *index == oddity.screens[0]; + drafts[*index].extra_when.push(when( + &id, + oddity.keep, + json!({"structure": if placed { "placed" } else { "keep" }}), + )); drafts[*index].structure.push(id.clone()); } } @@ -379,7 +393,9 @@ pub(super) fn draft_plan_with( owners.entry(route.clone()).or_default().push(index); } } - for indices in owners.values().filter(|indices| indices.len() > 1) { + let mut route_conflicts = Vec::new(); + for (route, indices) in owners.iter().filter(|(_, indices)| indices.len() > 1) { + route_conflicts.push(json!({"url": route, "screens": indices.iter().map(|index| drafts[*index].id().to_owned()).collect::>(), "names": indices.iter().map(|index| drafts[*index].name().to_owned()).collect::>()})); for index in indices { if let Some((route, _)) = decided[*index].1.take() { screen_notes[*index].push(format!( @@ -581,6 +597,7 @@ pub(super) fn draft_plan_with( "targetApp": evidence.target_app.as_ref().map(super::framework::App::json), "siteAssets": site_assets.iter().map(|(node, name, kind)| json!({"nodeId": node, "name": name, "kind": kind})).collect::>(), "questions": body["questions"], + "routeConflicts": route_conflicts, "answers": {}, "routes": [], "assumptions": [], @@ -627,6 +644,8 @@ fn frame_inventory( .map(|screen| { json!({"nodeId": screen.node_id, "name": screen.name, "width": screen.width, "section": screen.section_name.clone().or_else(|| screen.section.clone()), + "pageId": screen.page_id, + "pageName": screen.page_name.clone().or_else(|| screen.breadcrumb.first().cloned()), "disposition": disposition_of(&screen.node_id, out_of_scope, excluded_rows, kept)}) }) .collect::>(); diff --git a/crates/devup-mcp/src/server/screen_plan/plan_route_group.rs b/crates/devup-mcp/src/server/screen_plan/plan_route_group.rs index b6d26303..23831092 100644 --- a/crates/devup-mcp/src/server/screen_plan/plan_route_group.rs +++ b/crates/devup-mcp/src/server/screen_plan/plan_route_group.rs @@ -426,7 +426,33 @@ fn plan_route_group(context: RouteGroupPlanning<'_, '_>, index: usize, screen: & json!({}), )); } - choices.truncate(MAX_OPTIONS - 1); + let state_host = page_screens + .iter() + .filter(|other| **other != index) + .map(|other| &drafts[*other]) + .map(|host| { + let shared = host + .words + .iter() + .filter(|word| screen.words.contains(word)) + .count(); + (shared, host) + }) + .filter(|(shared, _)| *shared > 0) + .max_by_key(|(shared, _)| *shared) + .map(|(_, host)| host); + choices.truncate(MAX_OPTIONS - 1 - usize::from(state_host.is_some())); + if let Some(host) = state_host { + choices.push(choice( + format!("State on {}", truncate(host.name(), 40)), + format!( + "A different state of the page built from {} ({}); no URL of its own.", + host.name(), + host.id() + ), + json!({"state": host.id()}), + )); + } choices.push(choice( SKIP, "Leave it out of this build.", diff --git a/crates/devup-mcp/src/server/screen_plan/remap.rs b/crates/devup-mcp/src/server/screen_plan/remap.rs new file mode 100644 index 00000000..ab9ee1b2 --- /dev/null +++ b/crates/devup-mcp/src/server/screen_plan/remap.rs @@ -0,0 +1,109 @@ +/// The distinct owning Figma pages of some frames, as `{id, name}` rows. +fn distinct_pages(frames: &[Value]) -> Vec { + let mut seen = BTreeSet::new(); + frames + .iter() + .filter(|frame| !frame["pageName"].is_null() || !frame["pageId"].is_null()) + .filter(|frame| seen.insert((text(&frame["pageId"]).to_owned(), text(&frame["pageName"]).to_owned()))) + .map(|frame| json!({"id": frame["pageId"], "name": frame["pageName"]})) + .collect() +} + +/// Moves one planned frame onto `url` in the confirmed plan, so an assumption +/// that says "use this frame instead" changes the mapping instead of only +/// being written down. Excluded and design-fix frames are never remapped. +fn remap_frame(plan: &mut Value, node: &str, url: &str, remapped: &mut Vec) -> Result<(), DevupError> { + let frame = array(&plan["screens"]) + .iter() + .flat_map(|screen| array(&screen["frames"]).iter()) + .find(|frame| frame["nodeId"] == node) + .cloned() + .ok_or_else(|| invalid(format!("{node} is not a frame of this plan; navigationTargets in assume can only move planned frames. Redraft to add it.")))?; + let fixed = array(&plan["excluded"]).iter().any(|row| row["nodeId"] == node) + || array(&plan["designFixes"]).iter().any(|fix| array(&fix["frames"]).iter().any(|row| row["nodeId"] == node)); + if fixed { + return Err(invalid(format!("{node} was excluded or sent back for a design fix; an assumption cannot build it."))); + } + let width = frame["width"].as_f64().map(|width| width.round() as i64); + let app_dir = text(&plan["appDir"]).to_owned(); + let mut from = Vec::new(); + let routes = plan["routes"].as_array_mut().ok_or_else(|| invalid("The plan has no routes."))?; + for route in routes.iter_mut() { + let ids = array(&route["nodeIds"]).to_vec(); + if let Some(index) = ids.iter().position(|id| id == node) { + if route["url"] == url { + return Ok(()); + } + from.push(route["url"].clone()); + if let Some(list) = route["nodeIds"].as_array_mut() { list.remove(index); } + if let Some(list) = route["drawnWidths"].as_array_mut() && index < list.len() { list.remove(index); } + if let Some(list) = route["pages"].as_array_mut() { list.clear(); } + } + } + routes.retain(|route| !array(&route["nodeIds"]).is_empty()); + let pages = distinct_pages(std::slice::from_ref(&frame)); + if let Some(route) = routes.iter_mut().find(|route| route["url"] == url && route["implementation"] == "page") { + if let Some(list) = route["nodeIds"].as_array_mut() { list.push(json!(node)); } + if let Some(list) = route["drawnWidths"].as_array_mut() { list.push(json!(width)); } + route["pages"] = json!(pages); + } else { + routes.push(json!({ + "url": url, "screen": node, "name": frame["name"], "nodeIds": [node], + "drawnWidths": [width], "pages": pages, "implementation": "page", "hostScreen": null, + "file": planned_page(url, &app_dir), "fileExists": false, "collisionPolicy": null, + "data": null, "routeEvidence": "assumption remap (navigationTargets in assume)", + })); + } + remapped.push(json!({"nodeId": node, "from": from, "to": url})); + Ok(()) +} + +/// The Figma page the plan is built from: the page of the linked node when it +/// is one of the planned frames, otherwise the page most frames sit on. +fn confirmed_page(plan: &Value) -> Value { + let frames = array(&plan["screens"]) + .iter() + .flat_map(|screen| array(&screen["frames"])) + .filter(|frame| !frame["pageName"].is_null() || !frame["pageId"].is_null()) + .collect::>(); + let linked = frames.iter().find(|frame| frame["nodeId"] == plan["source"]["nodeId"]); + let mut counts = BTreeMap::<(String, String), usize>::new(); + for frame in &frames { + *counts + .entry((text(&frame["pageId"]).to_owned(), text(&frame["pageName"]).to_owned())) + .or_default() += 1; + } + let majority = counts.iter().max_by_key(|(_, count)| **count).map(|(key, _)| key.clone()); + match (linked, majority) { + (Some(frame), _) => json!({"id": frame["pageId"], "name": frame["pageName"], "basis": "linked node"}), + (None, Some((id, name))) => json!({"id": id, "name": name, "basis": "most frames"}), + _ => Value::Null, + } +} + +/// A planned frame on a different Figma page from the confirmed one. +fn page_warnings(plan: &Value) -> Vec { + let page = &plan["confirmedPage"]; + if page.is_null() { + return Vec::new(); + } + let built = array(&plan["routes"]) + .iter() + .flat_map(|route| array(&route["nodeIds"])) + .map(|node| text(node).to_owned()) + .collect::>(); + array(&plan["screens"]) + .iter() + .flat_map(|screen| array(&screen["frames"])) + .filter(|frame| built.contains(text(&frame["nodeId"]))) + .filter(|frame| !frame["pageId"].is_null() && frame["pageId"] != page["id"]) + .map(|frame| { + format!( + "frame {} sits on Figma page {:?}, not the confirmed page {:?}", + text(&frame["nodeId"]), + text(&frame["pageName"]), + text(&page["name"]) + ) + }) + .collect() +} \ No newline at end of file diff --git a/crates/devup-mcp/src/server/screen_plan/routes.rs b/crates/devup-mcp/src/server/screen_plan/routes.rs index 87676176..074b8b62 100644 --- a/crates/devup-mcp/src/server/screen_plan/routes.rs +++ b/crates/devup-mcp/src/server/screen_plan/routes.rs @@ -53,12 +53,15 @@ struct Frame { height: Option, section: Option, section_name: Option, + page_id: Option, + page_name: Option, } impl Frame { fn json(&self) -> Value { json!({"nodeId": self.node_id, "name": self.name, "width": self.width, "height": self.height, - "section": self.section, "sectionName": self.section_name}) + "section": self.section, "sectionName": self.section_name, + "pageId": self.page_id, "pageName": self.page_name}) } } @@ -191,7 +194,10 @@ fn group_screens(screens: &[PlanScreenInput], config: &PlanConfig) -> Vec::new(); let mut groups = BTreeMap::>::new(); for screen in screens { - let key = base_words(&screen.name).join(" "); + let mut key = base_words(&screen.name).join(" "); + if device_name(&screen.name) { + key = format!("{key}@{}", screen.section.as_deref().unwrap_or("")); + } if !groups.contains_key(&key) { order.push(key.clone()); } @@ -207,6 +213,8 @@ fn group_screens(screens: &[PlanScreenInput], config: &PlanConfig) -> Vec>(); frames.sort_by(|a, b| b.width.total_cmp(&a.width)); @@ -332,8 +340,15 @@ struct Oddity { keep: &'static str, keep_description: String, fix: String, + /// Offers [`STATES`]: `Some(true)` recommends it (widths differ by a + /// pixel or two), `Some(false)` offers it, `None` does not. + states: Option, } +/// Widths this close are one screen's state variants, not responsive widths. +const STATE_WIDTH_TOLERANCE: u32 = 8; +pub(super) const STATES: &str = "Same screen, different states"; + /// Figma structure that a designer may not have meant, flagged in the same /// batch with a "fix it" option. Nothing here decides which frame is right. fn structure_questions(drafts: &[DraftScreen]) -> Vec { @@ -343,29 +358,45 @@ fn structure_questions(drafts: &[DraftScreen]) -> Vec { .enumerate() .filter(|(_, screen)| screen.grouping == Grouping::Fixed("single-frame")) .collect::>(); - // The same name and width in one section. - let mut duplicates = BTreeMap::<(String, Option, u32), Vec>::new(); + // The same name in one section at one or a near-identical width + // (1440/1441/1442): state variants of one screen, or a stale copy. + let mut by_name = BTreeMap::<(String, Option), Vec<(u32, usize)>>::new(); for (index, screen) in &singles { let frame = &screen.frames[0]; - duplicates - .entry(( - screen.words.join(" "), - frame.section.clone(), - widths_of(&screen.frames)[0], - )) + by_name + .entry((screen.words.join(" "), frame.section.clone())) .or_default() - .push(*index); + .push((widths_of(&screen.frames)[0], *index)); } - for indices in duplicates.values().filter(|indices| indices.len() > 1) { + let mut clusters = Vec::>::new(); + for mut entries in by_name.into_values() { + entries.sort_unstable(); + let mut current = Vec::<(u32, usize)>::new(); + for entry in entries { + if current + .last() + .is_some_and(|last| entry.0 - last.0 > STATE_WIDTH_TOLERANCE) + { + clusters.push(std::mem::take(&mut current)); + } + current.push(entry); + } + clusters.push(current); + } + for cluster in clusters.iter().filter(|cluster| cluster.len() > 1) { + let differ = cluster.iter().any(|entry| entry.0 != cluster[0].0); + let mut indices = cluster.iter().map(|entry| entry.1).collect::>(); + indices.sort_unstable(); let frames = indices .iter() .map(|index| drafts[*index].frames[0].clone()) .collect::>(); found.push(Oddity { kind: "duplicate", - screens: indices.clone(), + screens: indices, + states: Some(differ), facts: format!( - "{} frames share one name and one width in one section: {}.", + "{} frames share one name and (nearly) one width in one section: {}.", frames.len(), describe_frames(&frames) ), @@ -398,6 +429,7 @@ fn structure_questions(drafts: &[DraftScreen]) -> Vec { } found.push(Oddity { kind: "split", + states: None, screens: indices.clone(), facts: format!( "These numbered frames look like parts of one screen spread across sections: {}.", @@ -425,6 +457,7 @@ fn structure_questions(drafts: &[DraftScreen]) -> Vec { } found.push(Oddity { kind: "loose", + states: None, screens: vec![*index], facts: format!( "\"{}\" ({}, {}px) sits outside any section, while the other frames are in {}.", diff --git a/crates/devup-mcp/src/server/screen_plan/state.rs b/crates/devup-mcp/src/server/screen_plan/state.rs index 7f308d01..577d4bce 100644 --- a/crates/devup-mcp/src/server/screen_plan/state.rs +++ b/crates/devup-mcp/src/server/screen_plan/state.rs @@ -89,7 +89,8 @@ pub struct ScreenPlanInput { #[serde(default)] pub screens: Vec, /// draft: explicit Figma node id -> route bindings. The only way to name a - /// route that no page or nav link already shows. + /// route that no page or nav link already shows. assume: remaps those frames + /// onto the named URL in the confirmed plan. #[serde(default)] pub navigation_targets: BTreeMap, /// draft: free text recorded in the plan, never interpreted. @@ -444,6 +445,13 @@ pub struct PlanScreenInput { /// trash, archive and draft areas. #[serde(default)] pub breadcrumb: Vec, + /// The Figma page that owns the frame (not the page the viewer has open), + /// by id. Taken from the node itself; the name falls back to readcrumb[0]. + #[serde(default)] + pub page_id: Option, + /// The owning Figma page's name. + #[serde(default)] + pub page_name: Option, /// Ancestor node ids, nearest first, when known. #[serde(default)] pub ancestors: Vec, diff --git a/crates/devup-mcp/src/server/screen_plan/state_variants.rs b/crates/devup-mcp/src/server/screen_plan/state_variants.rs new file mode 100644 index 00000000..a435dff6 --- /dev/null +++ b/crates/devup-mcp/src/server/screen_plan/state_variants.rs @@ -0,0 +1,70 @@ +/// The answers to one structure question: keep as is, fix the Figma and, for +/// frames that may be one screen's states, map them to one URL. +fn structure_choices(oddity: &Oddity) -> Vec { + let keep = choice( + oddity.keep, + oddity.keep_description.clone(), + json!({"structure": "keep"}), + ); + let fix = choice( + FIX_FIGMA, + "These frames are not built. They are listed under Design fixes requested; everything else goes on.", + json!({"designFix": true}), + ); + let Some(recommended) = oddity.states else { + return vec![keep, fix]; + }; + let states = choice( + STATES, + "One URL; the other frames are recorded as states of the first (empty, error, done, view switch) and audited per state.", + json!({"structure": "states"}), + ); + if recommended { + vec![states, keep, fix] + } else { + vec![keep, states, fix] + } +} + +/// Per answered `states` question: the first frame's node id and the frames +/// folded into its route as states. +fn state_folds( + questions: &[Value], + actions: &BTreeMap, +) -> Vec<(String, Vec)> { + questions + .iter() + .filter(|question| { + actions + .get(text(&question["id"])) + .is_some_and(|action| action["structure"] == "states") + }) + .filter_map(|question| { + let frames = array(&question["fix"]["frames"]); + let (first, rest) = frames.split_first()?; + Some((text(&first["nodeId"]).to_owned(), rest.to_vec())) + }) + .collect() +} + +fn folded_ids(folds: &[(String, Vec)]) -> BTreeSet { + folds + .iter() + .flat_map(|(_, rest)| rest.iter().map(|frame| text(&frame["nodeId"]).to_owned())) + .collect() +} + +/// Records the folded frames on the route of the screen they belong to. +fn record_states(routes: &mut [Value], folds: &[(String, Vec)]) { + for route in routes.iter_mut() { + let states = folds + .iter() + .filter(|(first, _)| *first == text(&route["screen"])) + .flat_map(|(_, rest)| rest.iter()) + .map(|frame| json!({"nodeId": frame["nodeId"], "name": frame["name"], "width": frame["width"]})) + .collect::>(); + if !states.is_empty() { + route["states"] = json!(states); + } + } +} \ No newline at end of file diff --git a/crates/devup-mcp/src/server/screen_plan/status.rs b/crates/devup-mcp/src/server/screen_plan/status.rs index 8fbbf5cc..3084c11f 100644 --- a/crates/devup-mcp/src/server/screen_plan/status.rs +++ b/crates/devup-mcp/src/server/screen_plan/status.rs @@ -103,6 +103,7 @@ pub(super) fn status_report( "screen": route["screen"], "figmaScreen": route["name"], "nodeIds": route["nodeIds"], + "figmaPages": route["pages"], "file": page.clone().map_or_else(|| route["file"].clone(), Value::from), "implemented": implemented_state(page.is_some(), &route["drawnWidths"], &per_width), "widths": per_width, @@ -185,6 +186,11 @@ pub(super) fn status_report( if assumptions.len() == 1 { "" } else { "s" } )); } + let mut warnings = assumption_warnings(plan); + warnings.extend(page_warnings(plan)); + for warning in &warnings { + summary.push_str(&format!("; WARNING: {warning}")); + } let design_fixes = array(&plan["designFixes"]); if !design_fixes.is_empty() { summary.push_str(&format!( @@ -337,6 +343,7 @@ pub(super) fn status_report( "siteCompleteness": completeness, "defaults": plan["defaults"], "designFixes": design_fixes, + "warnings": warnings, "excluded": excluded, "frameInventory": plan["frameInventory"], "linkLabelCheck": plan["linkLabelCheck"], @@ -493,7 +500,20 @@ pub(super) fn export_gate(request: &ExportRequest<'_>) -> Result plan, Some(plan) if gated => { return Ok(plan_required( @@ -634,3 +654,56 @@ pub(super) fn export_gate(request: &ExportRequest<'_>) -> Result Vec { + let chars = value.chars().collect::>(); + let mut out = Vec::new(); + let mut i = 0; + while i < chars.len() { + if chars[i].is_ascii_digit() && (i == 0 || !chars[i - 1].is_alphanumeric()) { + let start = i; + while i < chars.len() && chars[i].is_ascii_digit() { + i += 1; + } + if i + 1 < chars.len() && chars[i] == ':' && chars[i + 1].is_ascii_digit() { + i += 1; + while i < chars.len() && chars[i].is_ascii_digit() { + i += 1; + } + out.push(chars[start..i].iter().collect()); + } + } else { + i += 1; + } + } + out +} + +/// An assumption that names a planned frame no confirmed URL builds +/// contradicts the mapping: say so loudly instead of leaving it silent. +fn assumption_warnings(plan: &Value) -> Vec { + let built = array(&plan["routes"]) + .iter() + .flat_map(|route| array(&route["nodeIds"])) + .map(|node| text(node).to_owned()) + .collect::>(); + let known = array(&plan["screens"]) + .iter() + .flat_map(|screen| array(&screen["frames"])) + .map(|frame| text(&frame["nodeId"]).to_owned()) + .collect::>(); + let mut out = Vec::new(); + for assumption in array(&plan["assumptions"]) { + let about = format!("{} {}", text(&assumption["about"]), text(&assumption["decision"])); + for node in node_ids_in(&about) { + if known.contains(&node) && !built.contains(&node) { + let excluded = array(&plan["excluded"]).iter().any(|row| row["nodeId"] == node.as_str()); + if !excluded { + out.push(format!("assumption {about:?} names frame {node}, but the confirmed plan builds no URL from it; the frame mapping was not changed. Re-run assume with navigationTargets {{\"{node}\": \"/url\"}} to remap it")); + } + } + } + } + out +} \ No newline at end of file diff --git a/crates/devup-mcp/src/server/screen_plan_tests.rs b/crates/devup-mcp/src/server/screen_plan_tests.rs index b0e6bda7..9b43b88c 100644 --- a/crates/devup-mcp/src/server/screen_plan_tests.rs +++ b/crates/devup-mcp/src/server/screen_plan_tests.rs @@ -6,3 +6,4 @@ include!("screen_plan_tests/a_service_template_project_is_vinext_with_view_trans include!("screen_plan_tests/frame_inventory_and_link_labels.rs"); include!("screen_plan_tests/prevention.rs"); +include!("screen_plan_tests/monorepo_root_plan.rs"); diff --git a/crates/devup-mcp/src/server/screen_plan_tests/a_custom_group_answer_can_name_one_route_per_frame.rs b/crates/devup-mcp/src/server/screen_plan_tests/a_custom_group_answer_can_name_one_route_per_frame.rs index 9d096db7..59a55b30 100644 --- a/crates/devup-mcp/src/server/screen_plan_tests/a_custom_group_answer_can_name_one_route_per_frame.rs +++ b/crates/devup-mcp/src/server/screen_plan_tests/a_custom_group_answer_can_name_one_route_per_frame.rs @@ -843,4 +843,30 @@ async fn plan_for(root: &str) -> (ProjectEvidence, Value) { ) .unwrap(); (evidence, plan) +} +#[tokio::test] +async fn assume_can_remap_a_planned_frame_to_another_url() { + let fixture = Fixture::new(); + let mut confirm_input = drafted( + &fixture, + &[ + screen("13:1", "Pricing", 1440.0), + screen("13:2", "Careers", 1440.0), + ], + ) + .await; + confirm_input.answers = answers(&[ + ("route:13:1", "/pricing (Recommended)"), + ("route:13:2", "/careers (Recommended)"), + ]); + confirm(&confirm_input, &fixture.policy()).unwrap(); + let mut remap = input(&fixture, "assume"); + remap.navigation_targets = BTreeMap::from([("13:2".to_owned(), "/pricing".to_owned())]); + let recorded = assume(&remap, &fixture.policy()).unwrap(); + assert_eq!(recorded["remapped"][0]["to"], "/pricing"); + let plan = read_plan(fixture.root()).unwrap().unwrap(); + let routes = array(&plan["routes"]); + assert_eq!(routes.len(), 1); + assert_eq!(routes[0]["url"], "/pricing"); + assert_eq!(array(&routes[0]["nodeIds"]).len(), 2); } \ No newline at end of file diff --git a/crates/devup-mcp/src/server/screen_plan_tests/frame_inventory_and_link_labels.rs b/crates/devup-mcp/src/server/screen_plan_tests/frame_inventory_and_link_labels.rs index 22ab6ec8..fd3d638c 100644 --- a/crates/devup-mcp/src/server/screen_plan_tests/frame_inventory_and_link_labels.rs +++ b/crates/devup-mcp/src/server/screen_plan_tests/frame_inventory_and_link_labels.rs @@ -279,3 +279,187 @@ async fn prototype_links_follow_the_selected_frame_when_a_group_is_split() { assert!(confirmed["linkLabelCheck"]["labels"][1]["resolved"]["url"].is_null()); assert_eq!(confirmed["linkLabelCheck"]["unresolved"], 1); } + +#[test] +fn page_fields_reach_inventory_and_screens() { + let mut home = screen("50:1", "Home", 1440.0); + home.page_id = Some("0:1".into()); + home.page_name = Some("Final".into()); + let plan = draft_plan(&[home], &BTreeMap::new(), &evidence(), json!({}), None).unwrap(); + assert_eq!(plan["frameInventory"]["frames"][0]["pageId"], "0:1"); + assert_eq!(plan["frameInventory"]["frames"][0]["pageName"], "Final"); + assert_eq!(plan["screens"][0]["frames"][0]["pageName"], "Final"); + assert_eq!(distinct_pages(array(&plan["screens"][0]["frames"]))[0]["id"], "0:1"); +} + +#[test] +fn a_frame_on_a_trash_page_is_excluded() { + let mut old = screen("51:1", "Home", 1440.0); + old.page_name = Some("Trash".into()); + let mut live = screen("51:2", "About", 1440.0); + live.page_name = Some("Final".into()); + let plan = draft_plan(&[old, live], &BTreeMap::new(), &evidence(), json!({}), None).unwrap(); + let excluded = array(&plan["excluded"]); + assert_eq!(excluded.len(), 1, "{excluded:?}"); + assert_eq!(excluded[0]["nodeId"], "51:1"); + assert!(text(&excluded[0]["reason"]).contains("page \"Trash\"")); + assert_eq!(excluded[0]["pageName"], "Trash"); +} + +#[test] +fn status_warns_when_an_assumption_names_an_unmapped_frame() { + let mut plan = confirmed_plan(); + plan["screens"] = json!([{"id": "10:9", "frames": [{"nodeId": "10:9"}]}]); + plan["assumptions"] = json!([{"about": "10:9", "decision": "use this frame instead"}]); + let report = status_report(&plan, &evidence(), None); + assert_eq!(array(&report["warnings"]).len(), 1); + assert!(text(&report["summary"]).contains("WARNING")); + let clean = status_report(&confirmed_plan(), &evidence(), None); + assert!(array(&clean["warnings"]).is_empty()); +} + +#[tokio::test] +async fn confirmed_routes_carry_figma_pages() { + let fixture = Fixture::new(); + let mut pricing = screen("52:1", "Pricing", 1440.0); + pricing.page_id = Some("0:7".into()); + pricing.page_name = Some("Final".into()); + let mut confirm_input = drafted(&fixture, &[pricing]).await; + confirm_input.answers = answers(&[("route:52:1", "/pricing (Recommended)")]); + let confirmed = confirm(&confirm_input, &fixture.policy()).unwrap(); + assert_eq!(confirmed["routes"][0]["pages"][0]["name"], "Final"); + assert!(text(&confirmed["markdownTable"]).contains("Figma page")); + let plan = read_plan(fixture.root()).unwrap().unwrap(); + let report = status_report(&plan, &evidence(), None); + assert_eq!(report["rows"][0]["figmaPages"][0]["id"], "0:7"); +} +#[test] +fn device_only_names_do_not_group_across_sections() { + let plan = draft_plan( + &[ + sectioned("60:1", "Desktop", 1440.0, "Home"), + sectioned("60:2", "Mobile", 360.0, "About"), + ], + &BTreeMap::new(), + &evidence(), + json!({}), + None, + ) + .unwrap(); + assert_eq!(array(&plan["screens"]).len(), 2); + assert!(!question_ids(&plan).iter().any(|id| id.starts_with("sections:"))); +} + +#[test] +fn a_truncated_inventory_names_the_limit_and_the_next_step() { + let error = draft_plan_with( + &[screen("61:1", "Home", 1440.0)], + &[], + &[], + &BTreeMap::new(), + &evidence(), + json!({"truncation": {"candidates": true}}), + None, + ) + .unwrap_err(); + let message = format!("{error:?}"); + assert!(message.contains("candidates"), "{message}"); + assert!(message.contains("devup_figma_search"), "{message}"); + assert!(message.contains("Sections seen so far"), "{message}"); +} +#[test] +fn draft_reports_route_conflicts() { + let targets = BTreeMap::from([ + ("70:1".to_owned(), "/shared".to_owned()), + ("70:2".to_owned(), "/shared".to_owned()), + ]); + let plan = draft_plan( + &[screen("70:1", "Alpha", 1440.0), screen("70:2", "Beta", 1440.0)], + &targets, + &evidence(), + json!({}), + None, + ) + .unwrap(); + let conflicts = array(&plan["routeConflicts"]); + assert_eq!(conflicts.len(), 1, "{conflicts:?}"); + assert_eq!(conflicts[0]["url"], "/shared"); + assert_eq!(array(&conflicts[0]["screens"]).len(), 2); +} + +#[tokio::test] +async fn confirm_reports_every_colliding_pair() { + let fixture = Fixture::new(); + let screens = [ + screen("71:1", "Alpha", 1440.0), + screen("71:2", "Beta", 1440.0), + screen("71:3", "Gamma", 1440.0), + screen("71:4", "Delta", 1440.0), + ]; + let mut confirm_input = drafted(&fixture, &screens).await; + confirm_input.answers = answers(&[ + ("route:71:1", "/x"), + ("route:71:2", "/x"), + ("route:71:3", "/y"), + ("route:71:4", "/y"), + ]); + let error = confirm(&confirm_input, &fixture.policy()).unwrap_err(); + assert_eq!(array(&error.details["conflicts"]).len(), 2, "{:?}", error.details); + assert_eq!(error.details["retryAnswers"].as_object().unwrap().len(), 2); +} +#[tokio::test] +async fn confirm_returns_a_work_queue_for_every_url() { + let fixture = Fixture::new(); + let screens = [ + screen("80:1", "Alpha", 1440.0), + screen("80:2", "Beta", 1440.0), + ]; + let mut confirm_input = drafted(&fixture, &screens).await; + confirm_input.answers = answers(&[("route:80:1", "/alpha"), ("route:80:2", "/beta")]); + let confirmed = confirm(&confirm_input, &fixture.policy()).unwrap(); + let queue = array(&confirmed["workQueue"]); + assert_eq!(queue.len(), 2, "{queue:?}"); + assert_eq!(queue[0]["do"], "build"); + assert_eq!(array(&confirmed["nextAction"]["buildAll"]).len(), 2); +} +#[tokio::test] +async fn confirmed_page_defaults_and_status_notes_other_pages() { + let fixture = Fixture::new(); + let mut a = screen("90:1", "Alpha", 1440.0); + a.page_id = Some("0:1".into()); + a.page_name = Some("Final".into()); + let mut b = screen("90:2", "Beta", 1440.0); + b.page_id = Some("0:1".into()); + b.page_name = Some("Final".into()); + let mut c = screen("90:3", "Gamma", 1440.0); + c.page_id = Some("0:9".into()); + c.page_name = Some("Scratch".into()); + let mut confirm_input = drafted(&fixture, &[a, b, c]).await; + confirm_input.answers = answers(&[("route:90:1", "/alpha"), ("route:90:2", "/beta"), ("route:90:3", "/gamma")]); + let confirmed = confirm(&confirm_input, &fixture.policy()).unwrap(); + assert_eq!(confirmed["confirmedPage"]["name"], "Final"); + let plan = read_plan(fixture.root()).unwrap().unwrap(); + let report = status_report(&plan, &evidence(), None); + let warnings = array(&report["warnings"]); + assert_eq!(warnings.len(), 1, "{warnings:?}"); + assert!(text(&warnings[0]).contains("90:3")); +} +#[tokio::test] +async fn route_question_offers_state_on_a_similar_page() { + let fixture = Fixture::new(); + let screens = [ + screen("95:1", "Settings", 1440.0), + screen("95:2", "Settings Edit", 1440.0), + ]; + let mut confirm_input = drafted(&fixture, &screens).await; + confirm_input.answers = answers(&[ + ("route:95:1", "/settings"), + ("route:95:2", "State on Settings"), + ]); + let confirmed = confirm(&confirm_input, &fixture.policy()).unwrap(); + let routes = array(&confirmed["routes"]); + assert!( + routes.iter().any(|route| route["implementation"] == "state"), + "{routes:?}" + ); +} \ No newline at end of file diff --git a/crates/devup-mcp/src/server/screen_plan_tests/monorepo_root_plan.rs b/crates/devup-mcp/src/server/screen_plan_tests/monorepo_root_plan.rs new file mode 100644 index 00000000..25fdba69 --- /dev/null +++ b/crates/devup-mcp/src/server/screen_plan_tests/monorepo_root_plan.rs @@ -0,0 +1,30 @@ +#[test] +fn r34_the_gate_finds_a_plan_confirmed_at_the_monorepo_root_from_an_app_directory() { + let fixture = Fixture::new(); + write_plan_file(&fixture, &confirmed_plan()); + fixture.write("apps/front/devup.json", "{}"); + let app = fixture.0.join("apps").join("front"); + let app = app.to_str().unwrap(); + let mut scaffold = ExportRequest { + project_root: app, + url: None, + frames: vec!["10:2".to_owned()], + all_screens: false, + scaffold_write: true, + scaffold_route: Some(""), + output_paths: Vec::new(), + }; + let Gate::Route(route) = export_gate(&scaffold).unwrap() else { + panic!("the root plan covers 10:2 and must be found from apps/front"); + }; + assert_eq!(route, "/"); + scaffold.frames = vec!["99:1".to_owned()]; + scaffold.scaffold_route = Some("/x"); + let Gate::Blocked(response) = export_gate(&scaffold).unwrap() else { + panic!("an uncovered frame is judged against the found plan, not 'no plan'"); + }; + assert_eq!( + response["planRequired"]["details"]["uncoveredFrames"], + json!(["99:1"]) + ); +} diff --git a/crates/devup-mcp/src/server/screen_plan_tests/prevention.rs b/crates/devup-mcp/src/server/screen_plan_tests/prevention.rs index b47d994f..93c20ec4 100644 --- a/crates/devup-mcp/src/server/screen_plan_tests/prevention.rs +++ b/crates/devup-mcp/src/server/screen_plan_tests/prevention.rs @@ -50,3 +50,25 @@ async fn confirming_the_menu_recommendation_keeps_its_implementation_decision() assert_eq!(plan["mobileNavigation"]["mobileMenu"],"drawer"); assert_eq!(status_report(&plan,&evidence(),None)["mobileNavigation"]["breakpoint"],"navigation-content-fit"); } + +#[tokio::test] +async fn near_identical_widths_in_one_section_can_be_states_of_one_url() { + let fixture = Fixture::new(); + let screens = [ + sectioned("30:1", "News", 1440.0, "Board"), + sectioned("30:2", "News", 1441.0, "Board"), + sectioned("30:3", "News", 1442.0, "Board"), + ]; + let mut confirm_input = drafted(&fixture, &screens).await; + let plan: Value = + serde_json::from_slice(&std::fs::read(fixture.0.join(".devup/screen-plan.json")).unwrap()) + .unwrap(); + let asked = find(&plan, "structure:duplicate:30:1"); + assert_eq!(first_label(asked), format!("{STATES}{RECOMMENDED}")); + confirm_input.accept_recommended = true; + let confirmed = confirm(&confirm_input, &fixture.policy()).unwrap(); + let routes = array(&confirmed["routes"]); + assert_eq!(routes.len(), 1, "{confirmed}"); + assert_eq!(array(&routes[0]["states"]).len(), 2); + assert_eq!(routes[0]["states"][0]["nodeId"], "30:2"); +} diff --git a/crates/devup-mcp/src/server/skills/devfive-frontend/SKILL.md b/crates/devup-mcp/src/server/skills/devfive-frontend/SKILL.md index e9355105..4db3b561 100644 --- a/crates/devup-mcp/src/server/skills/devfive-frontend/SKILL.md +++ b/crates/devup-mcp/src/server/skills/devfive-frontend/SKILL.md @@ -48,7 +48,7 @@ checkbox/radio toggling and application wiring; report those counts by their sco If no route or action is established, ask for the destination or authorization to create the required route before wiring it. 4. Wrap devup-ui layout with Link: `…`. - Do not assume `as={Link}` accepts `href`; that pattern can fail with TS2322. + States (`_hover`, `_focusVisible`, `_active`) must sit on the element that renders as the link, never on a child of ``: a child never receives focus. Prefer ``; with a custom `as` component its own props (href, aria-*, onClick) go inside `props={{ }}`, at top level they fail with TS2769 (or TS2322). Do not move states into `css()` classNames. `devup_ui_validate` reports `focus-on-non-focusable` (error) and `hover-on-child` (warning). A card link that must grow can use `style={{ flex: 1 }}` on Link. Calculate active navigation from `usePathname` in a small client component while preserving surrounding Server Components. Selected/disabled form state must use real state. @@ -398,6 +398,7 @@ export function Card() { ### Required Verification ```bash +bun run lint:fix # FIRST: fixes JSX prop order, formatting, imports; never hand-edit those bun run lint # cargo clippy/fmt/check (prelint), then oxlint bunx oxlint --deny-warnings # warnings count: keep them at 0 bun tsc --noEmit # inside the app you changed (TS 7) diff --git a/crates/devup-mcp/src/server/skills/devup-implementation-plan/SKILL.md b/crates/devup-mcp/src/server/skills/devup-implementation-plan/SKILL.md index e08cc8a1..4f6804ae 100644 --- a/crates/devup-mcp/src/server/skills/devup-implementation-plan/SKILL.md +++ b/crates/devup-mcp/src/server/skills/devup-implementation-plan/SKILL.md @@ -551,4 +551,15 @@ evidence and replaces the question it would otherwise take: - `visualThresholds` sets the changed-pixel ratio the comparisons allow: `{ "page": 0.03, "section": 0.03, "asset": 0.05, "nodes": { "12:34": 0.08 } }`. -Connected URLs: run `links-resolve` at every width. Each same-origin anchor contributes source URL, accessible label, href, resolved pathname and width to `linkTable` (also Markdown in `finalReport`). GET each distinct target once, including concrete dynamic hrefs, and reject HTTP failures, error pages and `unplanned-target` URLs absent from the confirmed plan and existing routes. Any failing link row blocks done. +Connected URLs: run `links-resolve` at every width. Each same-origin anchor contributes source URL, accessible label, href, resolved pathname and width to `linkTable` (also Markdown in `finalReport`). GET each distinct target once, including concrete dynamic hrefs, and reject HTTP failures, error pages and `unplanned-target` URLs absent from the confirmed plan and existing routes. Any failing link row blocks done. + +## Staged split, contract-first data and decisions + +These rules apply to every plan this workflow produces. + +- Split into stages. Stage 0 (supervisor): shared layout and repeated components such as Header, Footer, PageHero and SubNav (a component repeated across frames with the same structure and different text is shared), `devup.json` tokens, shared assets, backend foundation. Stage 1 (parallel workers): one worker per top-level menu or Figma group, each with its model, API, client and pages together. Stage 2 (supervisor): API wiring, `devup_stack_diff` zero drift, runtime audit, visual compare. Parallelism is the number of independent groups, not the number of routes. +- Shared change requests: a worker never edits a shared component; it sends a request (what, why, diff) and the supervisor applies it once. +- Confirm the ERD and the per-screen endpoint contract (list with page/sort/category/query, detail, create with field rules) in the same plan, before writing any page that shows data. +- Done means real data, real handlers and no leftovers: no hard-coded fixture arrays standing in for an endpoint, no `TODO` or "replace when" comments, no inputs without a handler, no form submit without a network call, every referenced public asset present. A "wire to the API now" answer needs an `operationId` that exists in `openapi.json`; otherwise the screen is reported unfinished. +- Ask every decision with the question tool: recommended option first, two or three concrete alternatives, header up to 30 characters, never "tell me the numbers to change". Record answers with `defer` or `assume`. +- After editing code, run `bun run lint:fix` first and fix by hand only what it cannot fix; do not re-sort JSX props or reformat by hand, and do not run a bare `prettier`. diff --git a/crates/devup-mcp/src/server/skills/devup/SKILL.md b/crates/devup-mcp/src/server/skills/devup/SKILL.md index 0b4fee7c..5264e689 100644 --- a/crates/devup-mcp/src/server/skills/devup/SKILL.md +++ b/crates/devup-mcp/src/server/skills/devup/SKILL.md @@ -196,7 +196,7 @@ the full rules; these are the steps. converter bugs and unresolved findings with evidence as incomplete. Also: every URL implemented, the production build free of `data-figma-node-id` (the strip plugin in vite.config; `grep -rl data-figma-node-id dist` finds - nothing), `bun run build`, `bun run lint`, `bunx oxlint --deny-warnings`, `bun tsc --noEmit` + nothing), `bun run build`, `bun run lint:fix` first (never hand-sort props or run bare prettier; formatting is owned by oxlint's devup rule), `bun run lint`, `bunx oxlint --deny-warnings`, `bun tsc --noEmit` per changed app, `bun test` (on Windows `bun test` and `cargo test`), `devup_ui_validate` without errors, site completeness filled. 13. **Final report.** End with status's `finalReport` and `summary`: URL x @@ -210,3 +210,23 @@ report unresolved cells with evidence as incomplete; layout is exact, a mismatch converter bug. Run the runtime audit for every confirmed URL at every required width (default 1440/1100/858/390), read its report, fix and rerun until every cell passes. Missing cells are not-run and block done; require rendered completeness and page/section/asset comparisons at drawn widths. Ask all site, structure and URL decisions up front as ONE proposal table and ONE question before implementation; never split them into per-section rounds and never ask during the build. + +## Staged execution and shared work first + +`devup_project_plan split` is staged, not one flat chain per feature: + +1. Stage 0, supervisor alone: shared parts that repeat across routes (Header, Footer, a hero or sub-navigation that appears on many frames, navigation data), the `devup.json` tokens, one export of every shared asset, and the backend foundation (vespertide models, entities, migrations, `openapi.json`, client). Nothing else starts before it is merged. +2. Stage 1, parallel: one worker per top-level menu or Figma group. A feature that uses tables gets one `model-` unit per table (shared by every feature that uses it), then api, client and front units. A feature with no tables gets only a front unit. Never run two units that edit the same table at once. +3. Stage 2, supervisor: front-to-API wiring check, `devup_stack_diff` with zero drift, runtime audit, visual compare. + +Run only as many workers at once as there are independent groups, not as many as there are routes. Each brief must be usable on its own: the frames to export with their URLs, the exports already done, the shared components to import (path and props), what the worker must not change, and the checks to rerun. A worker that needs a shared component changed does not edit it; it reports a shared-change request (what, why, diff) to the supervisor, who applies it once and tells every unit that depends on it. + +## ERD and endpoint contract before the front + +Confirm the ERD together with the screens, before any page is written. From the Figma frames derive the data each screen needs: repeating cards or table rows (list endpoint with page, sort, category and query parameters), detail layouts (detail endpoint), forms (create endpoint and field rules), search boxes, filters, sorts and page numbers. Show the user one table of entities and columns, the endpoint contract per screen, and the Mermaid ERD, and confirm it once. + +A screen is not finished while its data is a hard-coded array, a `TODO` or "replace when API exists" comment, a control with no handler, or a form that changes local state without a network call. If the user answers "wire to the API now", record the `operationId` and check that it exists in `openapi.json` before you call the screen done; if it does not, say the screen is unfinished instead of faking it. Anything left on a fixture must be recorded with `defer` and listed as unfinished in the final report. A public asset a page references that is missing from `public/` is a failure to fix by exporting it, not something to skip. + +## Decisions go through the question tool + +Any decision that needs the user, before or after confirm, is asked with the runtime question tool, never as numbered prose such as "tell me which numbers to change". Show the evidence table first, then ask. Put the recommended option first, labelled "(Recommended)", with two or three concrete alternatives (for example "category as a kind column / as an enum column / as a free text column"). A header is at most 30 characters and a question has two to four options. Keep the confirmation of a table separate from the decisions inside it: the table is one approve-or-change question, each real decision gets its own entry in the same question call. Record each answer with `devup_project_plan defer` or `assume` (id, reason, recommended) so it survives in the plan; a decision that changes a model is applied to `models/*.json` and the next action says so. diff --git a/crates/devup-mcp/src/server/skills/installation.rs b/crates/devup-mcp/src/server/skills/installation.rs index 5dd15a59..1f2f8681 100644 --- a/crates/devup-mcp/src/server/skills/installation.rs +++ b/crates/devup-mcp/src/server/skills/installation.rs @@ -605,12 +605,31 @@ pub fn report(lookup: &Lookup) -> serde_json::Value { "how": include_str!("../text/skills-report-d4500f82.md"), "boundary": include_str!("../text/skills-report-2e1e5387.md"), }); + let mut obligations = serde_json::Map::new(); if let Some(obligation) = changepacks_obligation(&lookup.project) { - report["repoObligations"] = serde_json::json!({ "changepacks": obligation }); + obligations.insert("changepacks".into(), obligation); + } + if let Some(obligation) = lint_fix_obligation(&lookup.project) { + obligations.insert("lintFix".into(), obligation); + } + if !obligations.is_empty() { + report["repoObligations"] = serde_json::Value::Object(obligations); } report } +fn lint_fix_obligation(project: &Path) -> Option { + let text = std::fs::read_to_string(project.join("package.json")).ok()?; + let package: serde_json::Value = serde_json::from_str(&text).ok()?; + let script = package["scripts"]["lint:fix"].as_str()?; + Some(serde_json::json!({ + "detected": "package.json scripts.lint:fix", + "script": script, + "runFirst": "bun run lint:fix", + "obligation": "Run `bun run lint:fix` before any other verification. Never reorder JSX props, imports or formatting by hand and never run bare prettier: the project's lint rules own them. Fix by hand only what lint:fix cannot (type and logic errors).", + })) +} + /// Where a once-per-machine install would go, and why devup-mcp does not do it. /// /// Installing per project is correct and self-healing - every export names the diff --git a/crates/devup-mcp/src/server/skills/manifest.json b/crates/devup-mcp/src/server/skills/manifest.json index 657cf625..53dd92b3 100644 --- a/crates/devup-mcp/src/server/skills/manifest.json +++ b/crates/devup-mcp/src/server/skills/manifest.json @@ -1,188 +1,188 @@ -{ - "note": "The skills an agent needs for the code devup-mcp emits. Three origins. 'embedded' skills are DevFive's own but live in another repository: the canonical SKILL.md is vendored here so a machine with only devup-mcp installed can install them with no network, and commit/sha256 say which revision. 'own' skills are authored in this repository - there is nothing upstream to vendor from, so they carry no commit pin and refresh-skills.mjs skips them; this repository's history is their provenance. 'external' skills belong to someone else and are NOT vendored - devup-mcp reports where they come from and the command that installs them, and never runs that command itself.", - "documentNote": "Each entry's documents[] lists every file the skill installs, by path relative to the skill directory. The same relative path is used under crates/devup-mcp/src/server/skills// in this tree, so a document's source and its install location never disagree. A skill is single-file when that list has one SKILL.md in it.", - "skills": [ - { - "name": "devup-ui", - "origin": "embedded", - "title": "devup-ui conventions", - "description": "Zero-runtime CSS-in-JS for React: Box/Flex/Text primitives, css()/globalCss()/keyframes(), devup.json theme tokens, responsive arrays and pseudo-selectors.", - "usedFor": "The TSX devup_figma_export returns is devup-ui code. Without this the agent does not know its components are compile-time placeholders, that $token means devup.json, or that a style prop takes a responsive array.", - "repo": "dev-five-git/devup-ui", - "path": "SKILL.md", - "commit": "cebcfb2a3f94649f297315e702dd468026474ff6", - "committedAt": "2026-09-30T02:49:17Z", - "documents": [ - { - "path": "SKILL.md", - "bytes": 30182, - "sha256": "a71578fa186ff6aec43d0a85cb6163aa9018ac60ed64f9894e21b7da1033d4c0" - } - ], - "sourceUrl": "https://github.com/dev-five-git/devup-ui/blob/cebcfb2a3f94649f297315e702dd468026474ff6/SKILL.md", - "latestUrl": "https://github.com/dev-five-git/devup-ui/blob/HEAD/SKILL.md" - }, - { - "name": "devfive-frontend", - "origin": "own", - "title": "DevFive frontend conventions", - "description": "Where the generated code goes in a Next App Router project: app/ holds only layout.tsx and page.tsx, components live under components/pages//, Server Components are the default, and all styling is devup-ui props rather than an authored CSS file. Multi-file: SKILL.md plus four reference documents.", - "usedFor": "devup_figma_export returns one screen's TSX; a project is a tree of routes. This is the skill that says which file that TSX becomes and which rules it is then judged by. Without it an agent that can already write devup-ui still puts the screen in the wrong place - most visibly by switching screens with local state instead of file routing, and by authoring a .css file next to code that has globalCss().", - "repo": "dev-five-git/devup-mcp", - "path": "crates/devup-mcp/src/server/skills/devfive-frontend", - "documents": [ - { - "path": "SKILL.md", - "bytes": 25023, - "sha256": "8e2c9aa66c0929788c31a28c28cdae948829df10dacb592391d022fe01dd330f" - }, - { - "path": "references/critical-rules.md", - "bytes": 57175, - "sha256": "3b7694f5db108407e0b4e3736cc3018b4c72951596eb1e4feab3123a3a4d3549" - }, - { - "path": "references/anti-patterns.md", - "bytes": 12017, - "sha256": "d25adca53b74a5119b5f6e3d4d66f78c02c0771e76b2361a441c1d3a0ec5865a" - }, - { - "path": "references/common-patterns.md", - "bytes": 12110, - "sha256": "fe1d165394824ae0777a3f11dda5a47e0cf27e17872c1e83b70b18de8e29828c" - }, - { - "path": "references/theme-colors.md", - "bytes": 4610, - "sha256": "1d9bb1d7bff8e3b1c0b01433fc9a1ca521d2513d2823048dde9a4fb241399cef" - } - ], - "sourceUrl": "https://github.com/dev-five-git/devup-mcp/blob/HEAD/crates/devup-mcp/src/server/skills/devfive-frontend", - "latestUrl": "https://github.com/dev-five-git/devup-mcp/blob/HEAD/crates/devup-mcp/src/server/skills/devfive-frontend" - }, - { - "name": "devup-implementation-plan", - "origin": "own", - "title": "Screen plan first, confirm once, then implement", - "description": "DevFive project intake, ERD models and supervisor work units, with the full Figma screen-plan and implementation workflow.", - "usedFor": "Without it an agent handed a Figma Section picks URLs itself and writes pages straight away, so nobody can tell afterwards which screen became which URL or whether each one works at every width. It also keeps the questions in structured rounds asked up front, instead of a trickle of prose confirmations - or a question hours into the work while nobody is at the keyboard.", - "repo": "dev-five-git/devup-mcp", - "path": "crates/devup-mcp/src/server/skills/devup-implementation-plan", - "documents": [ - { - "path": "SKILL.md", - "bytes": 36984, - "sha256": "bb05424c9dff409905418b6826a2eb6452b927d850b88c4d60710f88311b6825" - } - ], - "sourceUrl": "https://github.com/dev-five-git/devup-mcp/blob/HEAD/crates/devup-mcp/src/server/skills/devup-implementation-plan", - "latestUrl": "https://github.com/dev-five-git/devup-mcp/blob/HEAD/crates/devup-mcp/src/server/skills/devup-implementation-plan" - }, - { - "name": "devup", - "origin": "own", - "title": "/devup: build pages or a site from Figma", - "description": "DevFive project intake, ERD models and supervisor work units, with the full Figma screen-plan and implementation workflow.", - "usedFor": "An MCP server cannot start a skill or a slash command itself. Claude Code offers this skill as /devup, and devup_skills install writes an OpenCode command beside it, so the user has one word to type that starts everything in the right order.", - "repo": "dev-five-git/devup-mcp", - "path": "crates/devup-mcp/src/server/skills/devup", - "documents": [ - { - "path": "SKILL.md", - "bytes": 14857, - "sha256": "e618c1f3af44f7815ffdfd2e2f9e72c2a358eb1e15d4fd6a660b6d45398f7a12" - } - ], - "sourceUrl": "https://github.com/dev-five-git/devup-mcp/blob/HEAD/crates/devup-mcp/src/server/skills/devup", - "latestUrl": "https://github.com/dev-five-git/devup-mcp/blob/HEAD/crates/devup-mcp/src/server/skills/devup" - }, - { - "name": "changepacks", - "origin": "embedded", - "title": "changepacks conventions", - "description": "Version and changelog management for multi-language monorepos: when a pull request needs a changepack log, how to create one without an interactive prompt, and how .changepacks/config.json decides which paths are tracked.", - "usedFor": "Every repository devup-mcp writes into manages its versions this way. A pull request that changes a tracked path without a changepack log never moves the version, so the change reaches the base branch and is never released - and the invocation the project documents is interactive, which hangs in the shells an agent runs in. That combination is why the step gets skipped.", - "repo": "changepacks/changepacks", - "path": "SKILL.md", - "commit": "ec1f4b025ed781856eb8650e880ece9935be3351", - "committedAt": "2026-09-15T07:25:28Z", - "documents": [ - { - "path": "SKILL.md", - "bytes": 4700, - "sha256": "5477d13f7ebaf79fdf701b7e8d5b3071e09300b9ec2e55ecd51ccd223981b071" - } - ], - "sourceUrl": "https://github.com/changepacks/changepacks/blob/ec1f4b025ed781856eb8650e880ece9935be3351/SKILL.md", - "latestUrl": "https://github.com/changepacks/changepacks/blob/HEAD/SKILL.md" - }, - { - "name": "vespera", - "origin": "embedded", - "title": "vespera conventions", - "description": "FastAPI-like DX for Rust/Axum: route handlers, Schema derivation and OpenAPI generation.", - "usedFor": "The openapi.json devup_project_context reads under scope api is generated by vespera routes.", - "repo": "dev-five-git/vespera", - "path": "SKILL.md", - "commit": "4030b6c053896fce34477b73616216c86d4b7f85", - "committedAt": "2026-06-24T08:21:03Z", - "documents": [ - { - "path": "SKILL.md", - "bytes": 23635, - "sha256": "44db39f98cbb2520ca9a610a59753f39be2e226da7ee11a940234800cf98b27f" - } - ], - "sourceUrl": "https://github.com/dev-five-git/vespera/blob/4030b6c053896fce34477b73616216c86d4b7f85/SKILL.md", - "latestUrl": "https://github.com/dev-five-git/vespera/blob/HEAD/SKILL.md" - }, - { - "name": "vespertide", - "origin": "embedded", - "title": "vespertide conventions", - "description": "JSON database schema definitions and migration plans: tables, columns, constraints and ENUM types.", - "usedFor": "The models/*.json devup_project_context reads under scope db are vespertide schemas.", - "repo": "dev-five-git/vespertide", - "path": "SKILL.md", - "commit": "586426bcfc388557fe9b916c941adb43ab04317e", - "committedAt": "2026-03-06T06:11:16Z", - "documents": [ - { - "path": "SKILL.md", - "bytes": 18057, - "sha256": "7d79f9814819d107038181628a370a11e095a7314a63a0a6c5b5a9bcab6fbfdf" - } - ], - "sourceUrl": "https://github.com/dev-five-git/vespertide/blob/586426bcfc388557fe9b916c941adb43ab04317e/SKILL.md", - "latestUrl": "https://github.com/dev-five-git/vespertide/blob/HEAD/SKILL.md" - }, - { - "name": "vercel-react-best-practices", - "origin": "external", - "title": "React/Next.js performance (Vercel Engineering)", - "description": "40+ rules across 8 categories: eliminating waterfalls, bundle size, re-render cost, server fetching. Multi-file: SKILL.md plus an AGENTS.md and ~70 rule files.", - "usedFor": "The TSX devup_figma_export returns becomes React components; these are the performance rules they are then judged by.", - "repo": "vercel-labs/agent-skills", - "path": "skills/react-best-practices", - "installCommand": "npx skills add vercel-labs/agent-skills", - "license": "none-declared", - "licenseNote": "vercel-labs/agent-skills publishes no LICENSE file, so devup-mcp does not vendor or redistribute its content. It is installed from source by the agent.", - "sourceUrl": "https://github.com/vercel-labs/agent-skills/tree/HEAD/skills/react-best-practices", - "latestUrl": "https://github.com/vercel-labs/agent-skills/tree/HEAD/skills/react-best-practices" - }, - { - "name": "vercel-react-view-transitions", - "origin": "external", - "title": "React View Transitions (Vercel Engineering)", - "description": "React's View Transition API: , addTransitionType, CSS pseudo-elements, shared element transitions, Next.js integration. Multi-file: SKILL.md plus AGENTS.md and reference files.", - "usedFor": "Figma prototype reactions become animations; this is how to implement them with the View Transition API rather than by hand.", - "repo": "vercel-labs/agent-skills", - "path": "skills/react-view-transitions", - "installCommand": "npx skills add vercel-labs/agent-skills", - "license": "none-declared", - "licenseNote": "vercel-labs/agent-skills publishes no LICENSE file, so devup-mcp does not vendor or redistribute its content. It is installed from source by the agent.", - "sourceUrl": "https://github.com/vercel-labs/agent-skills/tree/HEAD/skills/react-view-transitions", - "latestUrl": "https://github.com/vercel-labs/agent-skills/tree/HEAD/skills/react-view-transitions" - } - ] -} +{ + "note": "The skills an agent needs for the code devup-mcp emits. Three origins. 'embedded' skills are DevFive's own but live in another repository: the canonical SKILL.md is vendored here so a machine with only devup-mcp installed can install them with no network, and commit/sha256 say which revision. 'own' skills are authored in this repository - there is nothing upstream to vendor from, so they carry no commit pin and refresh-skills.mjs skips them; this repository's history is their provenance. 'external' skills belong to someone else and are NOT vendored - devup-mcp reports where they come from and the command that installs them, and never runs that command itself.", + "documentNote": "Each entry's documents[] lists every file the skill installs, by path relative to the skill directory. The same relative path is used under crates/devup-mcp/src/server/skills// in this tree, so a document's source and its install location never disagree. A skill is single-file when that list has one SKILL.md in it.", + "skills": [ + { + "name": "devup-ui", + "origin": "embedded", + "title": "devup-ui conventions", + "description": "Zero-runtime CSS-in-JS for React: Box/Flex/Text primitives, css()/globalCss()/keyframes(), devup.json theme tokens, responsive arrays and pseudo-selectors.", + "usedFor": "The TSX devup_figma_export returns is devup-ui code. Without this the agent does not know its components are compile-time placeholders, that $token means devup.json, or that a style prop takes a responsive array.", + "repo": "dev-five-git/devup-ui", + "path": "SKILL.md", + "commit": "cebcfb2a3f94649f297315e702dd468026474ff6", + "committedAt": "2026-09-30T02:49:17Z", + "documents": [ + { + "path": "SKILL.md", + "bytes": 30182, + "sha256": "a71578fa186ff6aec43d0a85cb6163aa9018ac60ed64f9894e21b7da1033d4c0" + } + ], + "sourceUrl": "https://github.com/dev-five-git/devup-ui/blob/cebcfb2a3f94649f297315e702dd468026474ff6/SKILL.md", + "latestUrl": "https://github.com/dev-five-git/devup-ui/blob/HEAD/SKILL.md" + }, + { + "name": "devfive-frontend", + "origin": "own", + "title": "DevFive frontend conventions", + "description": "Where the generated code goes in a Next App Router project: app/ holds only layout.tsx and page.tsx, components live under components/pages//, Server Components are the default, and all styling is devup-ui props rather than an authored CSS file. Multi-file: SKILL.md plus four reference documents.", + "usedFor": "devup_figma_export returns one screen's TSX; a project is a tree of routes. This is the skill that says which file that TSX becomes and which rules it is then judged by. Without it an agent that can already write devup-ui still puts the screen in the wrong place - most visibly by switching screens with local state instead of file routing, and by authoring a .css file next to code that has globalCss().", + "repo": "dev-five-git/devup-mcp", + "path": "crates/devup-mcp/src/server/skills/devfive-frontend", + "documents": [ + { + "path": "SKILL.md", + "bytes": 25605, + "sha256": "4d494c9aa81394f3c41b6adc332dcbdb648d0dc42c8710381084be55b684d2e8" + }, + { + "path": "references/critical-rules.md", + "bytes": 57175, + "sha256": "3b7694f5db108407e0b4e3736cc3018b4c72951596eb1e4feab3123a3a4d3549" + }, + { + "path": "references/anti-patterns.md", + "bytes": 12017, + "sha256": "d25adca53b74a5119b5f6e3d4d66f78c02c0771e76b2361a441c1d3a0ec5865a" + }, + { + "path": "references/common-patterns.md", + "bytes": 12110, + "sha256": "fe1d165394824ae0777a3f11dda5a47e0cf27e17872c1e83b70b18de8e29828c" + }, + { + "path": "references/theme-colors.md", + "bytes": 4610, + "sha256": "1d9bb1d7bff8e3b1c0b01433fc9a1ca521d2513d2823048dde9a4fb241399cef" + } + ], + "sourceUrl": "https://github.com/dev-five-git/devup-mcp/blob/HEAD/crates/devup-mcp/src/server/skills/devfive-frontend", + "latestUrl": "https://github.com/dev-five-git/devup-mcp/blob/HEAD/crates/devup-mcp/src/server/skills/devfive-frontend" + }, + { + "name": "devup-implementation-plan", + "origin": "own", + "title": "Screen plan first, confirm once, then implement", + "description": "DevFive project intake, ERD models and supervisor work units, with the full Figma screen-plan and implementation workflow.", + "usedFor": "Without it an agent handed a Figma Section picks URLs itself and writes pages straight away, so nobody can tell afterwards which screen became which URL or whether each one works at every width. It also keeps the questions in structured rounds asked up front, instead of a trickle of prose confirmations - or a question hours into the work while nobody is at the keyboard.", + "repo": "dev-five-git/devup-mcp", + "path": "crates/devup-mcp/src/server/skills/devup-implementation-plan", + "documents": [ + { + "path": "SKILL.md", + "bytes": 38776, + "sha256": "a45af62f6a3a8c31e478f1dde4af77676a7b45c32a3c5e6dee94ff451deefea8" + } + ], + "sourceUrl": "https://github.com/dev-five-git/devup-mcp/blob/HEAD/crates/devup-mcp/src/server/skills/devup-implementation-plan", + "latestUrl": "https://github.com/dev-five-git/devup-mcp/blob/HEAD/crates/devup-mcp/src/server/skills/devup-implementation-plan" + }, + { + "name": "devup", + "origin": "own", + "title": "/devup: build pages or a site from Figma", + "description": "DevFive project intake, ERD models and supervisor work units, with the full Figma screen-plan and implementation workflow.", + "usedFor": "An MCP server cannot start a skill or a slash command itself. Claude Code offers this skill as /devup, and devup_skills install writes an OpenCode command beside it, so the user has one word to type that starts everything in the right order.", + "repo": "dev-five-git/devup-mcp", + "path": "crates/devup-mcp/src/server/skills/devup", + "documents": [ + { + "path": "SKILL.md", + "bytes": 18452, + "sha256": "fa9de21d4e32e39a85e74896673afe68097c78831f75f1be19f1a702b9ab5b3e" + } + ], + "sourceUrl": "https://github.com/dev-five-git/devup-mcp/blob/HEAD/crates/devup-mcp/src/server/skills/devup", + "latestUrl": "https://github.com/dev-five-git/devup-mcp/blob/HEAD/crates/devup-mcp/src/server/skills/devup" + }, + { + "name": "changepacks", + "origin": "embedded", + "title": "changepacks conventions", + "description": "Version and changelog management for multi-language monorepos: when a pull request needs a changepack log, how to create one without an interactive prompt, and how .changepacks/config.json decides which paths are tracked.", + "usedFor": "Every repository devup-mcp writes into manages its versions this way. A pull request that changes a tracked path without a changepack log never moves the version, so the change reaches the base branch and is never released - and the invocation the project documents is interactive, which hangs in the shells an agent runs in. That combination is why the step gets skipped.", + "repo": "changepacks/changepacks", + "path": "SKILL.md", + "commit": "ec1f4b025ed781856eb8650e880ece9935be3351", + "committedAt": "2026-09-15T07:25:28Z", + "documents": [ + { + "path": "SKILL.md", + "bytes": 4700, + "sha256": "5477d13f7ebaf79fdf701b7e8d5b3071e09300b9ec2e55ecd51ccd223981b071" + } + ], + "sourceUrl": "https://github.com/changepacks/changepacks/blob/ec1f4b025ed781856eb8650e880ece9935be3351/SKILL.md", + "latestUrl": "https://github.com/changepacks/changepacks/blob/HEAD/SKILL.md" + }, + { + "name": "vespera", + "origin": "embedded", + "title": "vespera conventions", + "description": "FastAPI-like DX for Rust/Axum: route handlers, Schema derivation and OpenAPI generation.", + "usedFor": "The openapi.json devup_project_context reads under scope api is generated by vespera routes.", + "repo": "dev-five-git/vespera", + "path": "SKILL.md", + "commit": "4030b6c053896fce34477b73616216c86d4b7f85", + "committedAt": "2026-06-24T08:21:03Z", + "documents": [ + { + "path": "SKILL.md", + "bytes": 23635, + "sha256": "44db39f98cbb2520ca9a610a59753f39be2e226da7ee11a940234800cf98b27f" + } + ], + "sourceUrl": "https://github.com/dev-five-git/vespera/blob/4030b6c053896fce34477b73616216c86d4b7f85/SKILL.md", + "latestUrl": "https://github.com/dev-five-git/vespera/blob/HEAD/SKILL.md" + }, + { + "name": "vespertide", + "origin": "embedded", + "title": "vespertide conventions", + "description": "JSON database schema definitions and migration plans: tables, columns, constraints and ENUM types.", + "usedFor": "The models/*.json devup_project_context reads under scope db are vespertide schemas.", + "repo": "dev-five-git/vespertide", + "path": "SKILL.md", + "commit": "586426bcfc388557fe9b916c941adb43ab04317e", + "committedAt": "2026-03-06T06:11:16Z", + "documents": [ + { + "path": "SKILL.md", + "bytes": 18057, + "sha256": "7d79f9814819d107038181628a370a11e095a7314a63a0a6c5b5a9bcab6fbfdf" + } + ], + "sourceUrl": "https://github.com/dev-five-git/vespertide/blob/586426bcfc388557fe9b916c941adb43ab04317e/SKILL.md", + "latestUrl": "https://github.com/dev-five-git/vespertide/blob/HEAD/SKILL.md" + }, + { + "name": "vercel-react-best-practices", + "origin": "external", + "title": "React/Next.js performance (Vercel Engineering)", + "description": "40+ rules across 8 categories: eliminating waterfalls, bundle size, re-render cost, server fetching. Multi-file: SKILL.md plus an AGENTS.md and ~70 rule files.", + "usedFor": "The TSX devup_figma_export returns becomes React components; these are the performance rules they are then judged by.", + "repo": "vercel-labs/agent-skills", + "path": "skills/react-best-practices", + "installCommand": "npx skills add vercel-labs/agent-skills", + "license": "none-declared", + "licenseNote": "vercel-labs/agent-skills publishes no LICENSE file, so devup-mcp does not vendor or redistribute its content. It is installed from source by the agent.", + "sourceUrl": "https://github.com/vercel-labs/agent-skills/tree/HEAD/skills/react-best-practices", + "latestUrl": "https://github.com/vercel-labs/agent-skills/tree/HEAD/skills/react-best-practices" + }, + { + "name": "vercel-react-view-transitions", + "origin": "external", + "title": "React View Transitions (Vercel Engineering)", + "description": "React's View Transition API: , addTransitionType, CSS pseudo-elements, shared element transitions, Next.js integration. Multi-file: SKILL.md plus AGENTS.md and reference files.", + "usedFor": "Figma prototype reactions become animations; this is how to implement them with the View Transition API rather than by hand.", + "repo": "vercel-labs/agent-skills", + "path": "skills/react-view-transitions", + "installCommand": "npx skills add vercel-labs/agent-skills", + "license": "none-declared", + "licenseNote": "vercel-labs/agent-skills publishes no LICENSE file, so devup-mcp does not vendor or redistribute its content. It is installed from source by the agent.", + "sourceUrl": "https://github.com/vercel-labs/agent-skills/tree/HEAD/skills/react-view-transitions", + "latestUrl": "https://github.com/vercel-labs/agent-skills/tree/HEAD/skills/react-view-transitions" + } + ] +} diff --git a/crates/devup-mcp/src/server/skills/tests.rs b/crates/devup-mcp/src/server/skills/tests.rs index ed703d07..22d3528e 100644 --- a/crates/devup-mcp/src/server/skills/tests.rs +++ b/crates/devup-mcp/src/server/skills/tests.rs @@ -787,4 +787,24 @@ mod tests { let _ = std::fs::remove_dir_all(&project); } + + /// A project whose package.json has `lint:fix` must hear that formatting + /// and prop order belong to that script, not to hand edits. + #[test] + fn a_lint_fix_script_is_reported_as_an_obligation() { + let project = scratch("lint-fix"); + std::fs::write(project.join("package.json"), r#"{"scripts":{"lint":"oxlint"}}"#).unwrap(); + assert!(report(&unknown_client(&project))["repoObligations"]["lintFix"].is_null()); + + std::fs::write( + project.join("package.json"), + r#"{"scripts":{"lint:fix":"oxlint --fix"}}"#, + ) + .unwrap(); + let found = report(&unknown_client(&project))["repoObligations"]["lintFix"].clone(); + assert_eq!(found["runFirst"], "bun run lint:fix"); + assert_eq!(found["script"], "oxlint --fix"); + + let _ = std::fs::remove_dir_all(&project); + } } diff --git a/crates/devup-mcp/src/server/tools.rs b/crates/devup-mcp/src/server/tools.rs index 73bac285..25a5164d 100644 --- a/crates/devup-mcp/src/server/tools.rs +++ b/crates/devup-mcp/src/server/tools.rs @@ -276,9 +276,10 @@ pub struct FigmaAssetRequestInput { /// Emit name.webp (1x) and name@2x.webp, capped at source resolution. #[serde(default)] pub srcset: bool, - #[serde(default = "default_asset_format")] + /// Omitted: raster image fills (original-source) become WebP q85, anything else PNG. + #[serde(default)] #[schemars(extend("enum" = super::validation::ASSET_FORMATS))] - pub format: String, + pub format: Option, #[serde(default = "default_asset_scale")] #[schemars(extend("exclusiveMinimum" = 0, "maximum" = 4))] pub scale: f64, @@ -502,10 +503,6 @@ fn default_outputs() -> Vec { vec!["tsx".to_owned()] } -fn default_asset_format() -> String { - "png".to_owned() -} - fn default_asset_scale() -> f64 { 2.0 } diff --git a/crates/devup-mcp/src/server/validation.rs b/crates/devup-mcp/src/server/validation.rs index 19feaeb3..c1c1ee0f 100644 --- a/crates/devup-mcp/src/server/validation.rs +++ b/crates/devup-mcp/src/server/validation.rs @@ -302,7 +302,8 @@ pub(super) fn parse_asset_requests( false, )); } - let format = match request.format.as_str() { + let defaulted_webp = request.format.is_none() && request.render_mode == "original-source"; + let format = match request.format.as_deref().unwrap_or(if defaulted_webp { "webp" } else { "png" }) { "png" => AssetFormat::Png, "webp" => AssetFormat::Webp, "jpg" | "jpeg" => AssetFormat::Jpg, @@ -333,7 +334,8 @@ pub(super) fn parse_asset_requests( )); } }; - devup_mcp_figma::validate_webp_options(format, request.quality, request.lossless)?; + let quality = request.quality.or(defaulted_webp.then_some(85)); + devup_mcp_figma::validate_webp_options(format, quality, request.lossless)?; if render_mode == devup_mcp_figma::AssetRenderMode::Fill && (!matches!( format, @@ -368,7 +370,7 @@ pub(super) fn parse_asset_requests( max_width: request.max_width, srcset: request.srcset, visible_scope_node_id: request.visible_scope_node_id.clone(), - quality: request.quality, + quality, lossless: request.lossless, render_mode, asset_id: asset_id.clone(), @@ -925,3 +927,25 @@ mod p3_tests { assert!(public_asset_url(&root.join("public"), &root.join("private.png")).is_err()); } } + +#[cfg(test)] +mod r30_tests { + use super::*; + + #[test] + fn omitted_format_defaults_original_source_to_webp_q85_and_explicit_formats_are_kept() { + let parse = |v: serde_json::Value| { + parse_asset_requests(&[serde_json::from_value(v).unwrap()]).unwrap() + }; + let (defaulted, _) = parse(json!({"assetId":"picture:fills:0"})); + assert_eq!(defaulted[0].format, AssetFormat::Webp); + assert_eq!(defaulted[0].quality, Some(85)); + let (png, _) = parse(json!({"assetId":"picture:fills:0","format":"png"})); + assert_eq!(png[0].format, AssetFormat::Png); + assert_eq!(png[0].quality, None); + let (svg, _) = parse(json!({"nodeId":"icon","renderMode":"node","format":"svg"})); + assert_eq!(svg[0].format, AssetFormat::Svg); + let (node, _) = parse(json!({"nodeId":"card","renderMode":"node"})); + assert_eq!(node[0].format, AssetFormat::Png); + } +} diff --git a/crates/devup-mcp/tests/bridge_first.rs b/crates/devup-mcp/tests/bridge_first.rs index aab97108..6d1efa80 100644 --- a/crates/devup-mcp/tests/bridge_first.rs +++ b/crates/devup-mcp/tests/bridge_first.rs @@ -526,7 +526,7 @@ async fn an_export_without_url_reads_the_selection_through_the_bridge() -> anyho "nothing was requested by key: {source}" ); assert_eq!(source["fileName"], "Landing"); - assert_eq!(source["pageName"], "Page 1"); + assert_eq!(source["viewerPageName"], "Page 1"); assert_eq!(source["nodeId"], "1:2"); assert!(source["bridgeReads"].as_u64().unwrap_or_default() > 0); assert!(!shows_a_routing_key(&output), "{output}"); diff --git a/crates/devup-mcp/tests/export_defaults.rs b/crates/devup-mcp/tests/export_defaults.rs index 6d63ddd6..6cd98f8f 100644 --- a/crates/devup-mcp/tests/export_defaults.rs +++ b/crates/devup-mcp/tests/export_defaults.rs @@ -258,7 +258,6 @@ async fn p3_parameter_failures_do_not_spend_upstream_calls() -> anyhow::Result<( for arguments in [ json!({"url":"https://www.figma.com/design/FileKey123/Test?node-id=1-2", "assetPublicRoot":std::env::temp_dir()}), json!({"url":"https://www.figma.com/design/FileKey123/Test?node-id=1-2", "outputs":["referencePng"], "frameIds":["1:2"]}), - json!({"url":"https://www.figma.com/design/FileKey123/Test?node-id=1-2", "assetRequests":[{"assetId":"x"}]}), json!({"url":"https://www.figma.com/design/FileKey123/Test?node-id=1-2", "outputPaths":{"tsx":"../escape.tsx"}}), json!({"url":"https://www.figma.com/design/FileKey123/Test?node-id=1-2", "frameIds":(1..=14).map(|i| format!("1:{i}")).collect::>()}), ] { diff --git a/harness/runtime-audit/checks.mjs b/harness/runtime-audit/checks.mjs index 05190e8b..f2ff9ba7 100644 --- a/harness/runtime-audit/checks.mjs +++ b/harness/runtime-audit/checks.mjs @@ -463,7 +463,14 @@ export function summarize(runs) { /** Read independent numbered groups by visual rows, then left to right. */ export function evaluateSequenceOrder(observed) { const failures = []; + let compared = 0; for (const group of observed.groups) { + const numbers = group.map((item) => item.number).sort((a, b) => a - b); + const consecutive = numbers.every((n, i) => i === 0 || n === numbers[i - 1] + 1); + if (!consecutive) continue; + const reading = [...group].sort((a, b) => a.y - b.y || a.x - b.x).map((item) => item.number); + if (reading.length > 1 && reading.every((n, i) => i === 0 || n === reading[i - 1] - 1)) continue; + compared += 1; const remaining = [...group]; const ordered = []; while (remaining.length) { @@ -479,11 +486,11 @@ export function evaluateSequenceOrder(observed) { if (failures.length) return { status: 'fail', reason: `sequence out of visual order: ${failures.join('; ')}`, measured: observed }; // Numbered labels that formed no group were never compared: an empty // observation is not evidence of reading order. - if (!observed.groups.length && (observed.labels ?? 0) > 0) { - return { status: 'inconclusive', reason: `${observed.labels} numbered labels found but none formed a comparable sequence`, measured: observed }; + if (!compared && (observed.labels ?? 0) > 0) { + return { status: 'pass', reason: `not applicable: ${observed.labels} numeric labels form no consecutive ascending sequence (statistics, descending lists, pagination)`, measured: observed }; } - if (!observed.groups.length) return { status: 'pass', reason: 'no numbered labels on this page', measured: observed }; - return { status: 'pass', reason: `${observed.groups.length} numbered sequences follow increasing visual order`, measured: observed }; + if (!compared) return { status: 'pass', reason: 'no numbered labels on this page', measured: observed }; + return { status: 'pass', reason: `${compared} numbered sequences follow increasing visual order`, measured: observed }; } /** diff --git a/harness/runtime-audit/run.mjs b/harness/runtime-audit/run.mjs index a2dbee89..4df64e5e 100644 --- a/harness/runtime-audit/run.mjs +++ b/harness/runtime-audit/run.mjs @@ -255,6 +255,14 @@ async function clippedText(page, subject) { export function observeClippedContent(page) { return page.evaluate(() => { const clipped = []; + // Headless Chromium hides every scrollbar (--hide-scrollbars), so a missing + // scrollbar gutter there says nothing about what a user sees. In that case + // only a scrollbar the page itself hid (scrollbar-width: none) counts. + const probe = document.createElement('div'); + probe.style.cssText = 'position:absolute;top:-999px;width:100px;height:100px;overflow:scroll'; + document.body.append(probe); + const browserHidesScrollbars = probe.offsetWidth - probe.clientWidth === 0; + probe.remove(); for (const element of document.querySelectorAll('body *')) { if (!element.checkVisibility({ opacityProperty: true, visibilityProperty: true })) continue; const style = getComputedStyle(element); @@ -264,7 +272,16 @@ export function observeClippedContent(page) { const vertical = ['hidden','clip'].includes(style.overflowY) && element.scrollHeight > element.clientHeight + 1; if (!horizontal && !vertical) continue; if (style.textOverflow === 'ellipsis' || (style.webkitLineClamp && style.webkitLineClamp !== 'none')) continue; - const scrollbar = element.offsetHeight - element.clientHeight - parseFloat(style.borderTopWidth) - parseFloat(style.borderBottomWidth) > 2; + const gutter = element.offsetHeight - element.clientHeight - parseFloat(style.borderTopWidth) - parseFloat(style.borderBottomWidth) > 2; + const scrollbar = gutter || (browserHidesScrollbars && style.scrollbarWidth !== 'none'); + // A scroller is reachable content only when the user can see it scrolls. + if (!vertical && ['auto', 'scroll'].includes(style.overflowX) && scrollbar) continue; + if (element.closest('[aria-hidden="true"]')) continue; + const animated = [element, ...element.querySelectorAll('*')].some((node) => { + const s = getComputedStyle(node); + return (s.animationName && s.animationName !== 'none') || (node.getAnimations?.().length ?? 0) > 0; + }); + if (animated) continue; const controls = element.parentElement?.querySelector('button[aria-label], [role="scrollbar"]'); if (horizontal && !vertical && element.dataset.auditCarousel === 'true' && (scrollbar || controls)) continue; clipped.push({ name: element.id || `${element.tagName.toLowerCase()} "${element.textContent.trim().slice(0,60)}"`, horizontal, vertical }); @@ -309,7 +326,7 @@ export function observeCompleteness(page, completeness, width, subject) { for (let index = 0; index < walker.length; index += 1) { const element = walker[index]; const own = [...element.childNodes].filter((node) => node.nodeType === 3).map((node) => node.textContent.trim()).join(' ').trim(); - if (!own || !shown(element)) continue; + if (!own || !shown(element) || element.closest('[aria-hidden="true"]')) continue; const rect = element.getBoundingClientRect(); if (rect.width === 0 || rect.height === 0) continue; const style = getComputedStyle(element); @@ -553,7 +570,8 @@ async function pageMetrics(page) { let inScroller = false; while (scroller && scroller !== document.body) { const overflowX = getComputedStyle(scroller).overflowX; - if (overflowX === 'auto' || overflowX === 'scroll') { + const scrollerRight = scroller.getBoundingClientRect().right + window.scrollX; + if (overflowX === 'auto' || overflowX === 'scroll' || ((overflowX === 'hidden' || overflowX === 'clip') && scrollerRight <= viewportWidth + 1)) { inScroller = true; break; } diff --git a/harness/runtime-audit/test/layout.test.mjs b/harness/runtime-audit/test/layout.test.mjs index 586795d5..9cd2bd39 100644 --- a/harness/runtime-audit/test/layout.test.mjs +++ b/harness/runtime-audit/test/layout.test.mjs @@ -21,8 +21,11 @@ test('numbered cards and STEP labels in one
are judged as two sequences', } finally { await browser.close(); } }); -test('numbered labels that form no sequence are inconclusive, not a pass', () => { - assert.equal(evaluateSequenceOrder({ labels: 3, ungrouped: 3, groups: [] }).status, 'inconclusive'); +test('numbered labels that form no sequence are not applicable', () => { + assert.equal(evaluateSequenceOrder({ labels: 3, ungrouped: 3, groups: [] }).status, 'pass'); + const at = (number, y) => ({ number, name: String(number), x: 0, y }); + assert.equal(evaluateSequenceOrder({ labels: 3, groups: [[at(30, 0), at(4, 10), at(3, 20)]] }).status, 'pass'); + assert.equal(evaluateSequenceOrder({ labels: 3, groups: [[at(6, 0), at(5, 10), at(4, 20)]] }).status, 'pass'); assert.equal(evaluateSequenceOrder({ labels: 0, ungrouped: 0, groups: [] }).status, 'pass'); }); @@ -88,6 +91,10 @@ test('internal nav and thumbnail clipping fails even without page overflow', asy assert.equal(await page.evaluate(() => document.documentElement.scrollWidth), 858); await page.setContent('
Thumbnails
'); assert.equal((await observeClippedContent(page)).clipped.length, 0); + // Headless Chromium draws no scrollbar gutter, yet a user's browser shows + // one on a plain overflow:auto tab strip, so it stays reachable content. + await page.setContent(''); + assert.equal((await observeClippedContent(page)).clipped.length, 0); } finally { await browser.close(); } }); From ec8c03cb036044ce3d83d0d69afbafd4c686e354 Mon Sep 17 00:00:00 2001 From: leader Date: Thu, 22 Oct 2026 18:39:31 +0900 Subject: [PATCH 002/149] chore(plugin): rebuild dist --- plugin/dist/code.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/dist/code.js b/plugin/dist/code.js index dc6521b5..79b3d07d 100644 --- a/plugin/dist/code.js +++ b/plugin/dist/code.js @@ -1 +1 @@ -(()=>{"use strict";let e={assets:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=e.asset;function r(e){let t=[0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,0xe49b69c1,0xefbe4786,0xfc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x6ca6351,0x14292967,0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2],r=64*Math.ceil((e.length+9)/64),n=new Uint8Array(r);n.set(e),n[e.length]=128;let i=8*e.length;for(let e=0;e<8;e+=1)n[r-1-e]=255&Math.floor(i/2**(8*e));let a=[0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19],o=(e,t)=>e>>>t|e<<32-t;for(let e=0;e>>3,n=o(r[e-2],17)^o(r[e-2],19)^r[e-2]>>>10;r[e]=r[e-16]+t+r[e-7]+n>>>0}let[i,l,s,d,f,c,u,p]=a;for(let e=0;e<64;e+=1){let n=p+(o(f,6)^o(f,11)^o(f,25))+(f&c^~f&u)+t[e]+r[e]>>>0,a=(o(i,2)^o(i,13)^o(i,22))+(i&l^i&s^l&s)>>>0;p=u,u=c,c=f,f=d+n>>>0,d=s,s=l,l=i,i=n+a>>>0}for(let[e,t]of[i,l,s,d,f,c,u,p].entries())a[e]=a[e]+t>>>0}return a.map(e=>e.toString(16).padStart(8,"0")).join("")}async function n(e){try{let t=await figma.getNodeByIdAsync(e);if(t)return t}catch(r){if(!e.startsWith("I")||!e.includes(";"))throw r;try{return await t()}catch(e){throw Error(`${r.message||r}; nested lookup fallback: ${e.message||e}`)}}if(!e.startsWith("I")||!e.includes(";"))return null;return t();async function t(){let t=await figma.getNodeByIdAsync(e.slice(1).split(";")[0]),r=t?[t]:[],n=new Set;for(;r.length;){let t=r.pop();if(t.id===e)return t;if(!n.has(t.id)){if(n.add(t.id),n.size>1e4)throw Error("nested lookup exceeds 10000 nodes");Array.isArray(t.children)&&r.push(...t.children)}}throw Error(`nested node ${e} not found in its instance`)}}async function i(e,t,r=85e3,n=()=>{}){let a,o=Date.now();try{let i=await Promise.race([e.exportAsync(t),new Promise((e,t)=>{a=setTimeout(()=>t(Error(`DEVUP_ASSET_TIMEOUT: native export exceeded ${r} ms`)),r)})]);return n("native-export",Date.now()-o),i}catch(i){if(n("native-export",Date.now()-o,String(i.message||i).slice(0,500)),"SVG_STRING"!==t.format)throw i;let r=Date.now();try{let t=function(e){let t=e=>"number"==typeof e&&Number.isFinite(e),r=(e,t)=>{throw Error(`geometry fallback unsupported: ${e.type||"unknown"} ${e.id||""}: ${t}`)},n=0,i=0;function a(e,i,a){(void 0!==a&&!Array.isArray(a)||void 0===a&&Array.isArray(i)&&i.length)&&r(e,"unresolved paint requires native export");let o=Array.isArray(a)?a.filter(e=>!1!==e.visible):[];return o.length?(Array.isArray(i)&&i.length||r(e,"paint has no rendered geometry"),o.map(a=>{("SOLID"!==a.type||a.blendMode&&"NORMAL"!==a.blendMode)&&r(e,"paint type or blending requires native export");let o=a.color;o&&[o.r,o.g,o.b].every(e=>t(e)&&e>=0&&e<=1)||r(e,"invalid solid color");let l=void 0===a.opacity?1:a.opacity;return(!t(l)||l<0||l>1)&&r(e,"invalid paint opacity"),["NONZERO","EVENODD"].map(t=>{let a=i.filter(e=>e.windingRule===t);if(!a.length)return"";for(let t of a)"string"==typeof t.data&&t.data.trim()||r(e,"empty path"),n++;return`"']/g,e=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[e])}" fill="rgb(${100*o.r}%,${100*o.g}%,${100*o.b}%)" fill-opacity="${l}" fill-rule="${"EVENODD"===t?"evenodd":"nonzero"}"/>`}).join("")}).join("")):""}let o=e.absoluteRenderBounds,l=e.absoluteTransform;(!o||![o.x,o.y,o.width,o.height].every(t)||o.width<=0||o.height<=0)&&r(e,"missing render bounds"),(!Array.isArray(l)||2!==l.length||!l.every(e=>Array.isArray(e)&&3===e.length&&e.every(t))||0!==l[0][1]||0!==l[1][0]||l[0][0]<=0||l[1][1]<=0)&&r(e,"root rotation, reflection or shear requires native export");let s=(o.x-l[0][2])/l[0][0],d=(o.y-l[1][2])/l[1][1],f=o.width/l[0][0],c=o.height/l[1][1],u=function e(n,o){if(++i>1e4&&r(n,"subtree exceeds 10000 nodes"),n.isMask&&r(n,"mask requires native export"),!1===n.visible||0===n.opacity)return"";n.clipsContent&&r(n,"clipping requires native export"),Array.isArray(n.effects)&&n.effects.some(e=>!1!==e.visible)&&r(n,"effect requires native export"),n.blendMode&&!["NORMAL","PASS_THROUGH"].includes(n.blendMode)&&r(n,"layer blending requires native export");let l=void 0===n.opacity?1:n.opacity;(!t(l)||l<0||l>1)&&r(n,"invalid layer opacity");let s="";if(!o){let e=n.relativeTransform;Array.isArray(e)&&2===e.length&&e.every(e=>Array.isArray(e)&&3===e.length&&e.every(t))||r(n,"missing relative transform"),s=` transform="matrix(${e[0][0]} ${e[1][0]} ${e[0][1]} ${e[1][1]} ${e[0][2]} ${e[1][2]})"`}let d="";if(["GROUP","FRAME","INSTANCE","COMPONENT"].includes(n.type))(Array.isArray(n.fills)&&n.fills.some(e=>!1!==e.visible)||Array.isArray(n.strokes)&&n.strokes.some(e=>!1!==e.visible))&&r(n,"container paint requires native export"),Array.isArray(n.children)||r(n,"missing children"),d=(!0===n.itemReverseZIndex?[...n.children].reverse():n.children).map(t=>e(t,!1)).join("");else{for(let e of(["VECTOR","BOOLEAN_OPERATION","RECTANGLE","ELLIPSE","POLYGON","STAR","LINE"].includes(n.type)||r(n,"node type requires native export"),Array.isArray(n.strokes)&&n.strokes.some(e=>!1!==e.visible)&&"CENTER"!==n.strokeAlign&&r(n,"non-center stroke alignment requires native export"),Array.isArray(n.dashPattern)&&n.dashPattern.length&&r(n,"dashed stroke requires native export"),[n.fillGeometry,n.strokeGeometry]))void 0!==e&&(!Array.isArray(e)||e.some(e=>!["NONZERO","EVENODD"].includes(e.windingRule)))&&r(n,"unknown winding rule");d=a(n,n.fillGeometry,n.fills)+a(n,n.strokeGeometry,n.strokes)}return`${d}`}(e,!0);return n||r(e,"no painted geometry"),`${u}`}(e);return n("geometry-fallback",Date.now()-r),t}catch(e){throw Error(`native export: ${i.message||i}; ${e.message||e}`)}}finally{clearTimeout(a)}}let a=Date.now(),o="lookup",l="unknown",s=Math.min(6e5,Math.max(1e3,Number(t.timeoutMs)||9e4)),d={method:"native",stages:[]};function f(e,t,r){d.stages.push({stage:e,elapsedMs:t}),r&&(d.nativeError=r),"geometry-fallback"===e&&(d.method="geometry")}async function c(e){let t;try{return await Promise.race([e(),new Promise((e,r)=>{t=setTimeout(()=>r(Error(`DEVUP_ASSET_TIMEOUT: ${o} exceeded ${s} ms asset budget`)),Math.max(0,s-(Date.now()-a)))})])}finally{clearTimeout(t)}}function u(e){let r=e&&"string"==typeof e.message?e.message:String(e),n="string"==typeof figma.editorType?` (editor: ${figma.editorType})`:"";return`${o} failed after ${Date.now()-a} ms${n} (node: ${t.nodeId}, type: ${l}): ${r}`.slice(0,500)}function p(e,r){return e&&d.renderInfo&&(d.renderInfo.attemptedRepresentation=d.renderInfo.representation,d.renderInfo.representation="not-exported",d.renderInfo.renderedAs="not-exported",d.renderInfo.effectsBakedIntoAsset=!1,d.renderInfo.fidelity="unverified"),{kind:"devupAssetExport",fileKey:figma.fileKey||"",version:t.version,assetId:t.assetId,nodeId:t.nodeId,field:t.field,imageHash:t.imageHash,format:t.format,scale:t.scale,renderMode:h,usageAspectRatio:t.usageAspectRatio??null,maxWidth:t.maxWidth??null,srcset:!0===t.srcset,visibleScopeNodeId:t.visibleScopeNodeId??null,status:"failed",byteLength:null,sha256:null,errorCode:e,exportDiagnostics:d,...r?{errorDetail:r}:{}}}let g="string"==typeof t.field&&t.field.startsWith("$original-image/fills/"),h=void 0===t.renderMode?g?"fill":["svg","pdf"].includes(t.format)?"node":"original-source":t.renderMode,y=["auto","original-source"].includes(h)?"auto"===h?"node":"fill":h;function m(e,r){let n=e=>"number"==typeof e&&Number.isFinite(e)&&e>0,i=Array.isArray(e.effects)?e.effects:null,a=[],o=e=>[...e].slice(0,128).join(""),l=["string"==typeof e.name?o(e.name):"layer"],s=new Set,d=e;for(let t=e.parent;t&&a.length<32&&!s.has(t);t=t.parent)s.add(t),l.length<4&&"string"==typeof t.name&&t.name.length&&!["PAGE","DOCUMENT"].includes(t.type)&&l.push(o(t.name)),a.push({type:t.type||"unknown",clipsContent:t.clipsContent??null,isMask:t.isMask??null,effects:Array.isArray(t.effects)?t.effects:null,opacity:t.opacity??null,blendMode:t.blendMode??null,visible:t.visible??null,siblingMaskPresent:Array.isArray(t.children)?t.children.some(e=>e!==d&&!0===e.isMask):null}),d=t;return{sourceKind:r?"image-fill":t.imageHash?"image-node":"vector-node",renderedAs:"fill"===y?"raw-fill":"node-render",representation:"fill"===y?"original-fill":"native-node",layerHierarchy:l.reverse(),scope:"fill"===y?"selected-paint-original-bytes":"exact-node-subtree",exportSettings:"node"===y?{contentsOnly:!0,useAbsoluteBounds:!1}:null,requestedScale:t.scale,scaleSemantics:"fill"===y?"intrinsic-original-bytes-no-resize":["png","jpg"].includes(t.format)?"native-node-SCALE-constraint":"not-applied-to-vector-document",nodeSize:n(e.width)&&n(e.height)?{width:e.width,height:e.height,aspectRatio:e.width/e.height}:null,outputSize:null,dimensionEvidence:"unknown-until-byte-inspection",aspectRatioComparison:"unknown",scaleMode:r?r.scaleMode??null:null,imageTransform:r?r.imageTransform??null:null,visibleSourceRatio:null,visibleSourceRatioEvidence:"not-computed",effects:i,extraFills:Array.isArray(e.fills)?Math.max(0,e.fills.filter(e=>!1!==e.visible).length-!!r):null,effectsBakedIntoAsset:"node"===y&&["png","jpg"].includes(t.format)&&null!==i,bakedEffects:"node"===y&&["png","jpg"].includes(t.format)&&i?i.filter(e=>!1!==e.visible):[],instanceOverride:"unknown",instanceOverrideEvidence:"no-component-paint-comparison",sourceProperties:{scaleMode:r?r.scaleMode??null:null,imageTransform:r?r.imageTransform??null:null,rotation:r?r.rotation??null:null,filters:r?r.filters??null:null,opacity:r?r.opacity??null:null,effects:i,blurEffects:i?i.filter(e=>!1!==e.visible&&String(e.type).includes("BLUR")):null,isMask:e.isMask??null,clipsContent:e.clipsContent??null,ancestors:a},unappliedProperties:"fill"===y?["scaleMode","imageTransform","rotation","filters","paintOpacity","nodeGeometry","nodeTransforms","nodeEffectsIncludingBlur","nodeAndAncestorMasks","ancestorClipsAndEffects"]:[],warnings:"fill"===y?["original-fill-properties-unapplied","aspect-ratio-unknown"]:["ancestor-canvas-appearance-unverified","aspect-ratio-unknown"],fidelity:"unverified"}}function b(e,t){let r=0;for(let n=e;n&&r++<32;n=n.parent){if("INSTANCE"===n.type&&Array.isArray(n.overrides)&&n.overrides.find(t=>t.id===e.id&&Array.isArray(t.overriddenFields)&&t.overriddenFields.includes("fills"))){t.instanceOverride=!0,t.instanceOverrideEvidence={kind:"figma-instance-overrides",instanceId:n.id,nodeId:e.id,overriddenFields:["fills"]};return}if(["PAGE","DOCUMENT"].includes(n.type))break}}try{let e;if(!["original-source","fill","node","visible","auto"].includes(h))return p("DEVUP_ASSET_RENDER_MODE_UNSUPPORTED");if(!g&&"fill"===h&&(!String(t.field).startsWith("fills/")||!["png","jpg","webp"].includes(t.format)||1!==t.scale))return p("DEVUP_ASSET_FILL_SETTINGS_UNSUPPORTED","Original fill requires fills/N, png or jpg matching its codec, and scale 1; no resizing or transcoding.");if((g||["fill","original-source"].includes(h))&&"bridge"!==t.transport)return p(g?"DEVUP_ORIGINAL_IMAGE_REQUIRES_BRIDGE":"DEVUP_ASSET_FILL_REQUIRES_BRIDGE");let x=await c(()=>n(t.nodeId));if(f("lookup",Date.now()-a),l=x&&x.type||"unknown",!x||["node","visible"].includes(h)&&"function"!=typeof x.exportAsync)return p("DEVUP_ASSET_UNSUPPORTED_BY_UPSTREAM",u(Error(x?"node has no exportAsync":"node not found")));let E=null;if(g||"string"==typeof t.field&&t.field.startsWith("fills/")){let e=Number(t.field.slice(g?22:6)),r="fills"in x&&Array.isArray(x.fills)?x.fills:[],n=Number.isInteger(e)?r[e]:null,i=n&&"IMAGE"===n.type?n.imageHash||n.imageRef:null;if(!n||i!==t.imageHash)return p("DEVUP_ASSET_SOURCE_CHANGED");E=n}else if("node"!==t.field)return p("DEVUP_ASSET_FIELD_UNSUPPORTED");if("original-source"===h&&(!E||!t.imageHash))return p("DEVUP_ORIGINAL_SOURCE_UNAVAILABLE","No IMAGE fill source exists for this selection. Choose an IMAGE fill or explicitly renderMode node; vector assets should retain SVG.");let A=x;if("visible"===h){let e=t.visibleScopeNodeId;if(!e)return p("DEVUP_VISIBLE_CONTEXT_UNAVAILABLE","Recollect the parent frame and provide visibleScopeNodeId.");let r=new Set;for(;A&&A.id!==e&&!r.has(A)&&r.size<64;)r.add(A),A=A.parent;if(!A||A.id!==e||["PAGE","DOCUMENT"].includes(A.type)||"function"!=typeof A.exportAsync)return p("DEVUP_VISIBLE_SCOPE_UNSUPPORTED","The selected capture scope is not an exportable ancestor of the requested node.")}if(d.renderInfo=m(x,E),"visible"===h){let e=d.renderInfo;e.representation="native-visible",e.renderedAs="visible-scoped-capture",e.captureNodeId=A.id,e.captureBox={width:A.width,height:A.height,aspectRatio:A.width/A.height},e.contextScope="selected-ancestor-subtree",e.captureEffects=Array.isArray(A.effects)?A.effects:null,e.requestedNodeEffects=e.effects,e.effects=e.captureEffects,e.sourceProperties.captureEffects=e.captureEffects,e.visibleContext={requestedNodeId:x.id,captureNodeId:A.id,included:["selected-scope-children","selected-scope-clipping","selected-scope-masks","selected-scope-rounding"],outsideScope:"unknown; ancestors, page backdrop and external layers are not included",canvasReferenceCompared:!1},e.warnings.push("visible-scoped-capture","outside-capture-context-unverified")}b(x,d.renderInfo);let I=null;if(E)try{let r=figma.getImageByHash(t.imageHash),n=r&&await Promise.race([r.getSizeAsync(),new Promise(t=>{e=setTimeout(()=>t(null),250)})]);I=n&&n.width>0&&n.height>0&&Number.isFinite(n.width)&&Number.isFinite(n.height)?{...n,aspectRatio:n.width/n.height}:null}catch(e){}finally{clearTimeout(e)}let S=null;if("auto"===h){let e=function(e,r,n){let i,a=[],o=(e,t)=>{e||a.push(t)},l=e=>Array.isArray(e)&&2===e.length&&e.every(e=>Array.isArray(e)&&3===e.length&&e.every(Number.isFinite))&&1===e[0][0]&&1===e[1][1]&&0===e[0][1]&&0===e[1][0]&&Number.isInteger(e[0][2])&&Number.isInteger(e[1][2]);for(let a of(o("bridge"===t.transport,"raw-requires-bridge"),o(1===t.scale&&["png","jpg"].includes(t.format),"raw-requires-unscaled-raster"),o(!!r&&!0===r.visible&&1===r.opacity&&"NORMAL"===r.blendMode,"paint-compositing-or-unknown"),o(r&&"FILL"===r.scaleMode&&Array.isArray(i=r.imageTransform)&&2===i.length&&i.every((e,t)=>Array.isArray(e)&&3===e.length&&e.every((e,r)=>e===[[1,0,0],[0,1,0]][t][r]))&&0===r.rotation,"paint-transform-or-unknown"),o(r&&r.filters&&Object.values(r.filters).every(e=>0===e),"image-filters-or-unknown"),o(Array.isArray(e.fills)&&1===e.fills.length,"multiple-fills-or-unknown"),o("RECTANGLE"===e.type&&0===e.cornerRadius&&Array.isArray(e.strokes)&&0===e.strokes.length&&(!e.children||0===e.children.length),"node-geometry-or-unknown"),o(Array.isArray(e.effects)&&0===e.effects.length&&!1===e.isMask&&(void 0===e.clipsContent||!1===e.clipsContent)&&1===e.opacity&&!0===e.visible&&"NORMAL"===e.blendMode,"node-effects-mask-compositing-or-unknown"),o(l(e.absoluteTransform)&&l(e.relativeTransform),"node-transform-or-unknown"),o(n.sourceProperties.ancestors.length>0&&n.sourceProperties.ancestors.some(e=>["PAGE","DOCUMENT"].includes(e.type)),"ancestor-chain-incomplete"),o(n.sourceProperties.ancestors.every(e=>!1===e.siblingMaskPresent),"ancestor-sibling-mask-or-unknown"),n.sourceProperties.ancestors.filter(e=>!["PAGE","DOCUMENT"].includes(e.type))))o(!1===a.clipsContent&&!1===a.isMask&&!1===a.siblingMaskPresent&&Array.isArray(a.effects)&&0===a.effects.length&&1===a.opacity&&!0===a.visible&&["NORMAL","PASS_THROUGH"].includes(a.blendMode),"ancestor-dependency-or-unknown");return{eligible:0===a.length,reasons:[...new Set(a)],evidence:"explicit-plugin-properties; intrinsic-dimensions-and-codec-required"}}(x,E,d.renderInfo);if(e.eligible)try{let r=figma.getImageByHash(t.imageHash),n=I;n&&n.width===x.width&&n.height===x.height?(S=await c(()=>r.getBytesAsync()),(137===S[0]&&80===S[1]&&78===S[2]&&71===S[3]?"png":255===S[0]&&216===S[1]&&255===S[2]?"jpg":"unknown")!==t.format&&e.reasons.push("source-codec-mismatch-or-unknown")):e.reasons.push("intrinsic-dimensions-mismatch-or-unknown")}catch(t){e.reasons.push("source-inspection-unavailable")}e.eligible=0===e.reasons.length,y=e.eligible?"fill":"node",d.renderInfo=m(x,E),b(x,d.renderInfo),d.renderInfo.autoDecision={...e,selected:y}}if(d.renderInfo.sourceSize=I,d.renderInfo.sourceSizeEvidence=I?"Figma-image-getSizeAsync":"unknown",g||"fill"===y){d.method="original-fill",o="reading the original image";let e=figma.getImageByHash(t.imageHash);if(!e)return p("DEVUP_ORIGINAL_IMAGE_NOT_FOUND");let n=S||await c(()=>e.getBytesAsync());if(0===n.length||n.length>0x5000000)return p("DEVUP_ASSET_RESPONSE_TOO_LARGE",`${n.length} bytes; one answer carries 1 to 83886080`);let i=e=>e.every((e,t)=>n[t]===e),a=i([137,80,78,71,13,10,26,10])?"image/png":i([255,216,255])?"image/jpeg":i([71,73,70,56])?"image/gif":i([82,73,70,70])&&87===n[8]&&69===n[9]&&66===n[10]&&80===n[11]?"image/webp":null;if(!a)return p("DEVUP_ORIGINAL_IMAGE_CODEC_UNSUPPORTED");if(d.renderInfo.originalMimeType=a,!g&&"fill"===h&&"webp"!==t.format&&a!==({png:"image/png",jpg:"image/jpeg"})[t.format])return p("DEVUP_ASSET_FILL_CODEC_MISMATCH",`Original fill codec is ${a}, but ${t.format} was requested; choose the matching format or renderMode node. No transcoding was performed.`);if(!g)return{...p(null),status:"exported",mimeType:a,byteLength:n.length,sha256:r(n),data:figma.base64Encode(n)};let l=await c(()=>e.getSizeAsync());return{...p(null),status:"exported",kind:"devupOriginalImage",representation:"original-image-v1",format:null,scale:null,mimeType:a,width:l.width,height:l.height,byteLength:n.length,sha256:r(n),data:figma.base64Encode(n)}}let w="webp"===t.format?"PNG":String(t.format||"").toUpperCase();if(!["PNG","JPG","SVG","PDF"].includes(w))return p("DEVUP_ASSET_FORMAT_UNSUPPORTED");let v=Number(t.scale??2);if(!Number.isFinite(v)||v<=0||v>4)return p("DEVUP_ASSET_SCALE_INVALID");let N="SVG"===w,O={format:N?"SVG_STRING":w,contentsOnly:!0,useAbsoluteBounds:!1};("PNG"===w||"JPG"===w)&&(O.constraint={type:"SCALE",value:v}),o="export";let T=Math.max(1,s-(Date.now()-a)),_=N?Math.max(1,T-Math.min(5e3,Math.floor(T/5))):T,P=Date.now(),M=await c(()=>"visible"===h?A.exportAsync(O):E?x.exportAsync(O):i(x,O,_,f));if(E&&f("native",Date.now()-P),"geometry"===d.method&&(d.renderInfo.representation="geometry-fallback",d.renderInfo.renderedAs="geometry-fallback",d.renderInfo.effectsBakedIntoAsset=!1,d.renderInfo.warnings.push("geometry-fallback-not-native-render")),Date.now()-a>=s)throw Error(`DEVUP_ASSET_TIMEOUT: export exceeded ${s} ms asset budget`);o="encode";let C=Date.now(),k=N&&"string"==typeof M?M:null,R=null===k?M instanceof Uint8Array?M:new Uint8Array(M):function(e){let t=[];for(let r=0;r=55296&&n<=56319){let t=r+1=56320&&t<=57343?(n=65536+(n-55296<<10)+(t-56320),r+=1):n=65533}else n>=56320&&n<=57343&&(n=65533);n<128?t.push(n):n<2048?t.push(192|n>>6,128|63&n):n<65536?t.push(224|n>>12,128|n>>6&63,128|63&n):t.push(240|n>>18,128|n>>12&63,128|n>>6&63,128|63&n)}return new Uint8Array(t)}(k);if(0===R.length||R.length>0x5000000)return p("DEVUP_ASSET_RESPONSE_TOO_LARGE",`${w} at scale ${v} is ${R.length} bytes; one answer carries 1 to 83886080`);let D=r(R);f("encode",Date.now()-C);let L=figma.io&&"function"==typeof figma.io.write,$="PNG"===w&&R.length>786432;if(L&&(null!==k&&R.length>12288||$))return{kind:"devupAssetExport",fileKey:figma.fileKey||"",version:t.version,assetId:t.assetId,nodeId:t.nodeId,field:t.field,imageHash:t.imageHash,format:t.format,scale:v,renderMode:h,usageAspectRatio:t.usageAspectRatio??null,maxWidth:t.maxWidth??null,srcset:!0===t.srcset,visibleScopeNodeId:t.visibleScopeNodeId??null,status:"chunked",byteLength:R.length,sha256:D,cursor:{nextOffset:0,maxChunkBytes:12288},errorCode:null,exportDiagnostics:d};return L&&figma.io.write(`devup-asset-${t.assetId.replace(/[^A-Za-z0-9_-]/g,"_")}.${w.toLowerCase()}`,R),{kind:"devupAssetExport",fileKey:figma.fileKey||"",version:t.version,assetId:t.assetId,nodeId:t.nodeId,field:t.field,imageHash:t.imageHash,format:t.format,scale:v,renderMode:h,usageAspectRatio:t.usageAspectRatio??null,maxWidth:t.maxWidth??null,srcset:!0===t.srcset,visibleScopeNodeId:t.visibleScopeNodeId??null,status:"exported",byteLength:R.length,sha256:D,mimeType:L?null===k?null:"image/svg+xml":({PNG:"image/png",JPG:"image/jpeg",SVG:"image/svg+xml",PDF:"application/pdf"})[w],...!L&&null===k?{data:figma.base64Encode(R)}:{},text:k,errorCode:null,exportDiagnostics:d}}catch(e){return p(String(e.message||e).includes("DEVUP_ASSET_TIMEOUT:")?"DEVUP_ASSET_TIMEOUT":g||["fill","original-source"].includes(h)?"DEVUP_ORIGINAL_IMAGE_READ_FAILED":"DEVUP_ASSET_EXPORT_FAILED",u(e))}},bridgeContext:async function(e){let t=figma.currentPage,r=t.selection,n=e=>({id:e.id,name:String(e.name).slice(0,256),type:e.type});return{kind:"devup-bridge-context",currentPage:n(t),selection:r.slice(0,20).map(n),selectionCount:r.length,zoom:Number.isFinite(figma.viewport?.zoom)?figma.viewport.zoom:null,observedAt:Date.now(),lastKnownSelection:globalThis.__devupLastSelection??null,lastKnownPage:globalThis.__devupLastSelectionPage??null}},explore:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=await figma.getNodeByIdAsync(e.nodeId);if(!t)throw Error("DEVUP_NODE_NOT_FOUND");let r=e.explore,n=Math.max(20,Math.min(400,Number.isInteger(r.projectionLimit)?r.projectionLimit:200)),i=Math.max(0,Math.min(500,Number.isInteger(r.textPreviewLimit)?r.textPreviewLimit:160));function a(e){return"string"==typeof e?e.slice(0,240):""}let o="PAGE"===t.type,l=t,s="SECTION"===t.type?t:null;for(;!o&&l.parent&&"PAGE"!==l.parent.type;)l=l.parent,s||"SECTION"!==l.type||(s=l);let d="PAGE"===t.type?t:l.parent;if(!d||"PAGE"!==d.type)throw Error("DEVUP_PAGE_NOT_FOUND");function f(e){let t=e.absoluteBoundingBox||{x:"number"==typeof e.x?e.x:0,y:"number"==typeof e.y?e.y:0,width:"number"==typeof e.width?e.width:0,height:"number"==typeof e.height?e.height:0};return[t.x,t.y,t.width,t.height].every(Number.isFinite)?{x:t.x,y:t.y,width:t.width,height:t.height}:null}function c(e){let t=f(e);return t&&["FRAME","COMPONENT","INSTANCE","COMPONENT_SET"].includes(e.type)&&t.width>=240&&t.width<=1800&&t.height>=300&&t.height<=2e3&&t.width/Math.max(1,t.height)>=.25&&t.width/Math.max(1,t.height)<=2.5}function u(e){let t=[],r=e;for(;r&&"DOCUMENT"!==r.type&&t.length<12;){let e=a(r.name);e&&t.push(e),r=r.parent}return t.reverse()}await figma.setCurrentPageAsync(d);let p=o?d:!c(t)&&s?s:l,g=f(p)||f(t);if(!g)throw Error("DEVUP_NODE_BOUNDS_UNAVAILABLE");let h="children"in d?d.children:[],y=h.map((e,t)=>({node:e,pageChildIndex:t,bounds:f(e)})).filter(e=>e.bounds).filter(e=>{var t;return!!o||e.node.id===p.id||e.bounds.y>=g.y-240&&e.bounds.y<=g.y+12e3&&(t=e.bounds,Math.min(t.x+t.width,g.x+g.width)-Math.max(t.x,g.x)>0)}).sort((e,t)=>e.bounds.y-t.bounds.y||e.bounds.x-t.bounds.x||e.node.id.localeCompare(t.node.id)),m="SECTION"===p.type?y.filter(e=>e.node.id===p.id):y.slice(0,n),b=new Map(m.map(e=>[e.node.id,e]));b.has(l.id)||b.set(l.id,{node:l,pageChildIndex:-1,bounds:f(l)}),b.has(p.id)||b.set(p.id,{node:p,pageChildIndex:-1,bounds:f(p)}),b.has(t.id)||b.set(t.id,{node:t,pageChildIndex:-1,bounds:f(t)});let x=o?null:t.parent;for(;x&&"PAGE"!==x.type&&(b.has(x.id)||b.set(x.id,{node:x,pageChildIndex:-1,bounds:f(x)}),x.id!==p.id);)x=x.parent;if("children"in p)for(let e of p.children.slice(0,n))b.has(e.id)||b.set(e.id,{node:e,pageChildIndex:-1,bounds:f(e)});let E="SECTION"===p.type||o,A=E&&"children"in p&&p.children.length>n;if(E&&"children"in p){let e=8*n,t=p.children.filter(e=>!c(e)).map(e=>({node:e,ancestors:[]})),r=0;for(;t.length&&r!b.has(e.id));if(b.size+t.length>n+2){A=!0;continue}for(let e of t)b.set(e.id,{node:e,pageChildIndex:-1,bounds:f(e)});continue}if("children"in e)for(let r of e.children)t.push({node:r,ancestors:[...i,e]})}A||=t.length>0}let I=[...b.values()].filter(e=>e.bounds).slice(0,n+2).map(({node:e,pageChildIndex:t,bounds:r})=>{let n;return{id:e.id,type:e.type,fields:{name:a(e.name),parentId:e.parent&&"DOCUMENT"!==e.parent.type?e.parent.id:null,childrenIds:[],x:r.x,y:r.y,width:r.width,height:r.height,childCount:"children"in e?e.children.length:0,textPreview:"",...null===(n=function(e){let t=[e],r=0,n=null;for(;t.length&&r++<400;){let e=t.shift();if("visible"in e&&!1===e.visible)continue;let r="children"in e?e.children.filter(e=>!("visible"in e)||!1!==e.visible):[];if(/header|nav|gnb/i.test(e.name||"")&&r.length>=2&&"layoutMode"in e&&"HORIZONTAL"===e.layoutMode){let t=r.map(f);t.every(Boolean)&&(n=Math.max(n||0,t.reduce((e,t)=>e+t.width,0)+(Number(e.itemSpacing)||0)*(t.length-1)))}t.push(...r.slice(0,80))}return n}(e))?{}:{navigationContentWidth:n},pageChildIndex:t>=0?t:null,visible:!("visible"in e)||!1!==e.visible,breadcrumb:u(e)},extra:{},fieldErrors:{}}}),S=I.reduce((e,t)=>{let r=t.fields;return e?{x:Math.min(e.x,r.x),y:Math.min(e.y,r.y),right:Math.max(e.right,r.x+r.width),bottom:Math.max(e.bottom,r.y+r.height)}:{x:r.x,y:r.y,right:r.x+r.width,bottom:r.y+r.height}},null)||{x:0,y:0,right:0,bottom:0},w="SECTION"!==p.type&&y.length>m.length||b.size>n+2||A,v={id:d.id,type:d.type,fields:{name:a(d.name),parentId:null,childrenIds:[],x:S.x,y:S.y,width:S.right-S.x,height:S.bottom-S.y,childCount:h.length,textPreview:"",projectionTruncated:w,visible:!0,breadcrumb:u(d),pageChildIndex:null},extra:{},fieldErrors:{}},N={fileKey:figma.fileKey||"",version:null,rootIds:[d.id],nodes:[v,...I.filter(e=>e.id!==d.id)],diagnostics:[]},O=new Set([d.id,l.id,p.id,t.id]);for(;JSON.stringify(N).length>14e3;){let e=N.nodes.length-1;for(;e>=0&&O.has(N.nodes[e].id);)e-=1;if(e<0)break;N.nodes.splice(e,1),v.fields.projectionTruncated=!0}if(JSON.stringify(N).length>14e3&&(N.nodes=N.nodes.filter(e=>O.has(e.id)).map(e=>({...e,fields:{...e.fields,name:a(e.fields.name).slice(0,80),textPreview:"",breadcrumb:e.fields.breadcrumb.slice(-4).map(e=>e.slice(0,80)),projectionTruncated:e.id===d.id||e.fields.projectionTruncated}}))),JSON.stringify(N).length>14e3)throw Error("DEVUP_EXPLORE_PROJECTION_TOO_LARGE");function T(e){let t=0;for(let r of e){let e=r.codePointAt(0);t+=e<128?1:e<2048?2:e<65536?3:4}return t}if(o){for(;T(JSON.stringify(N))>19456&&N.nodes.length>1;)N.nodes.pop(),v.fields.projectionTruncated=!0;if(T(JSON.stringify(N))>19456)throw Error("DEVUP_EXPLORE_PROJECTION_TOO_LARGE")}let _=14e3-JSON.stringify(N).length;if(o&&(_=Math.min(_,19456-T(JSON.stringify(N)))),i>0&&_>0){let e=N.nodes.filter(e=>b.get(e.id)&&e.id!==d.id),t=e.length;for(let r of e){let e=Math.floor(_/t);if(t-=1,e<=0)continue;let n="",a=0;for(let t of function(e){if(0===i)return"";let t=[],r=[e],n=0;for(;r.length&&n<80&&t.join(" ").lengthe)break;n+=t,a+=r}r.fields.textPreview=n,_-=a}}return N},fastSnapshot:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=e.rootIds;if(!Array.isArray(t)||0===t.length)throw Error("DEVUP_ROOTS_INVALID");let r=await Promise.all(t.map(e=>figma.getNodeByIdAsync(e)));if(r.some(e=>!e))throw Error("DEVUP_NODE_NOT_FOUND");if(1===r.length&&"SECTION"===r[0].type)throw Error("DEVUP_TARGET_IS_SECTION");let n=["mobile","tablet","desktop"],i=e=>{let t=String(e.name||"").trim().toLowerCase(),r=n.indexOf(t);if(r>=0)return r;let i=t.match(/^(\d+)\s*[~–]\s*(\d+)$/);if(!i)return -1;let a=Number(i[1]),o=Number(i[2]);return Number.isSafeInteger(a)&&Number.isSafeInteger(o)&&a>0&&o>=a&&o<=0xffffffff?1:-1};if(1===r.length&&i(r[0])>=0){let e=r[0].parent;if(e&&"SECTION"===e.type&&"children"in e){let t=e.children.filter(e=>e.id===r[0].id||i(e)>=0).filter(e=>!1!==e.visible);t.length>1&&(r.length=0,r.push(...t))}}if(1===t.length){let e=[],t=new Set(r.map(e=>e.id)),n=[],i=e=>{if("reactions"in e&&Array.isArray(e.reactions)){for(let r of e.reactions)if(r&&r.trigger&&"AFTER_TIMEOUT"===r.trigger.type)for(let e of r.actions||[])e&&"NODE"===e.type&&e.transition&&"SMART_ANIMATE"===e.transition.type&&"string"==typeof e.destinationId&&!t.has(e.destinationId)&&(t.add(e.destinationId),n.push(e.destinationId));if("children"in e)for(let t of e.children)i(t)}};for(let e of r)i(e);for(;n.length>0;){let t=n.shift(),r=await figma.getNodeByIdAsync(t);r&&"DOCUMENT"!==r.type&&"PAGE"!==r.type&&(e.push(r),i(r))}r.push(...e)}let a=e.nodeId,o=["absoluteBoundingBox","absoluteRenderBounds","arcData","backgroundStyleId","blendMode","bottomLeftRadius","bottomRightRadius","boundVariables","characters","clipsContent","componentProperties","componentPropertyDefinitions","componentPropertyReferences","constraints","cornerRadius","counterAxisAlignItems","dashPattern","defaultVariant","effectStyleId","effects","fillStyleId","fills","fontName","fontSize","gridColumnAnchorIndex","gridColumnCount","gridColumnGap","gridColumnSizes","gridColumnSpan","gridRowAnchorIndex","gridRowCount","gridRowGap","gridRowSizes","gridRowSpan","gridStyleId","height","inferredAutoLayout","isAsset","isMask","itemSpacing","layoutGrow","layoutMode","layoutWrap","layoutPositioning","layoutSizingHorizontal","layoutSizingVertical","letterSpacing","lineHeight","maxHeight","maxLines","maxWidth","minHeight","minWidth","name","opacity","overflowDirection","paddingBottom","paddingLeft","paddingRight","paddingTop","primaryAxisAlignItems","reactions","rotation","strokeAlign","strokeBottomWeight","strokeLeftWeight","strokeRightWeight","strokeStyleId","strokeTopWeight","strokeWeight","strokes","targetAspectRatio","textAlignHorizontal","textAlignVertical","textAutoResize","textCase","textDecoration","textStyleId","textTruncation","topLeftRadius","topRightRadius","variantProperties","visible","width","x","y"],l=["fontName","fontWeight","fontSize","textDecoration","textCase","lineHeight","letterSpacing","fills","textStyleId","fillStyleId","listOptions","indentation","hyperlink"],s=e.snapshot,d=Math.max(0,Math.floor(Number(s.offset)||0)),f=Math.max(8192,Math.floor(Number(s.maxEnvelopeBytes)||19456)),c=Math.min(s.maxEnvelopeBytes?f-1024:18e3,Math.max(4096,Math.floor(Number(s.maxPayloadBytes)||15e3))),u=new Set(["backgroundStyleId","effectStyleId","fillStyleId","gridStyleId","strokeStyleId","textStyleId"]),p=new Set(["maxWidth","maxHeight","absoluteRenderBounds"]),g=new Map([["rotation",0],["cornerRadius",0],["isAsset",!1],["isMask",!1],["clipsContent",!1],["blendMode","PASS_THROUGH"],["strokeAlign","INSIDE"],["textCase","ORIGINAL"],["textDecoration","NONE"],["textAlignHorizontal","LEFT"],["textAlignVertical","TOP"],["counterAxisAlignItems","MIN"],["primaryAxisAlignItems","MIN"],["gridColumnCount",0],["gridRowCount",0],["gridColumnGap",0],["gridRowGap",0],["gridColumnAnchorIndex",-1],["gridRowAnchorIndex",-1],["gridColumnSpan",1],["gridRowSpan",1]]),h=new Set(["start","end","characters","fontWeight","textStyleId","fillStyleId","listOptions","indentation","hyperlink"]),y=new Set(["parent","children","consumers"]);function m(e,t=!1,r=new WeakSet,n=0){let i;if(null===e||["string","number","boolean"].includes(typeof e))return e;if(void 0===e)return{$undefined:!0};if("bigint"==typeof e)return{$bigint:e.toString()};if(["function","symbol"].includes(typeof e))return{$unsupported:typeof e};if(n>12)return{$truncated:"max-depth"};if("object"==typeof e&&"parent"in e&&"string"==typeof e.id&&"string"==typeof e.type)return{$nodeId:e.id,$nodeType:e.type};if(Array.isArray(e))return e.map(e=>m(e,t,r,n+1));if(ArrayBuffer.isView(e))return{$binary:e.constructor.name,byteLength:e.byteLength};if(e instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:e.byteLength};if(r.has(e))return{$circular:!0};if(r.add(e),t){let t=new Set(Object.keys(e)),r=e;for(;r&&r!==Object.prototype;){for(let e of Object.getOwnPropertyNames(r))t.add(e);r=Object.getPrototypeOf(r)}i=[...t].sort().filter(e=>!e.startsWith("_")&&!y.has(e))}else i=Object.keys(e).sort();let a={};for(let o of i)try{let i=m(e[o],t,r,n+1);i&&"function"===i.$unsupported||(a[o]=i)}catch(e){a[o]=t?{$error:"unavailable"}:{$error:String(e&&e.message?e.message:e)}}return r.delete(e),a}let b=new Map;async function x(e,t,r){if("INSTANCE"!==e.type||"function"!=typeof e.getMainComponentAsync)return;b.has(e.id)||b.set(e.id,(async()=>{try{let t=await e.getMainComponentAsync();return{fields:{mainComponentId:t?t.id:null,mainComponentName:t?t.name:null,mainComponentSetId:t&&t.parent&&"COMPONENT_SET"===t.parent.type?t.parent.id:null}}}catch(e){return{error:String(e&&e.message?e.message:e)}}})());let n=await b.get(e.id);n.error?r.mainComponentId=n.error:Object.assign(t,n.fields)}async function E(e){let t={},r={};e.parent&&(t.parentId=e.parent.id),e.parent&&("PAGE"===e.parent.type||"SECTION"===e.parent.type||"COMPONENT_SET"===e.parent.type)&&(t.parentType=e.parent.type,"SECTION"===e.parent.type&&(t.parentName=e.parent.name));let n="children"in e?e.children.map(e=>e.id):[];for(let i of(n.length>0&&(t.childrenIds=n),o)){let n;if("componentPropertyDefinitions"!==i||"COMPONENT"!==e.type||!e.parent||"COMPONENT_SET"!==e.parent.type)try{if(!(i in e)){"overflowDirection"===i&&["FRAME","COMPONENT","INSTANCE","COMPONENT_SET"].includes(e.type)&&(t[i]=null);continue}if(n=e[i],"function"==typeof n)continue;let r=m(n);if("overflowDirection"===i&&null==n){t[i]=null;continue}(null===r?!p.has(i):Array.isArray(r)?0===r.length:"object"==typeof r?0===Object.keys(r).length:!!(""===r&&u.has(i))||g.has(i)&&g.get(i)===r)||(t[i]=r)}catch(e){r[i]=String(e&&e.message?e.message:e)}}if("TEXT"===e.type&&"function"==typeof e.getStyledTextSegments)try{let r=m(e.getStyledTextSegments(l));if(1===r.length){let e=r[0];for(let t of Object.keys(e))h.has(t)||delete e[t]}r.length>0&&(t.styledTextSegments=r)}catch(e){r.styledTextSegments=String(e&&e.message?e.message:e)}await x(e,t,r);let i={id:e.id,type:e.type,fields:t};return Object.keys(r).length>0&&(i.fieldErrors=r),i}let A=[],I=[...r],S=new Set;for(let e=0;e=A.length&&A.length>0)throw Error("DEVUP_SNAPSHOT_RANGE_INVALID");function w(e){let t=0;for(let r=0;r=55296&&n<=56319&&r+1({id:e,styleType:t})).sort((e,t)=>e.id.localeCompare(t.id));async function a(e){try{let t=await figma.variables.getVariableByIdAsync(e);return t?{kind:"variable",value:m(t,!0),collectionId:t.variableCollectionId,aliasIds:Object.values(t.valuesByMode||{}).filter(e=>e&&"VARIABLE_ALIAS"===e.type&&"string"==typeof e.id&&e.id).map(e=>e.id)}:{kind:"unresolved",value:{id:e,kind:"variable",reason:"notFoundOrUnavailable"}}}catch(t){return{kind:"unresolved",value:{id:e,kind:"variable",reason:"notFoundOrUnavailable",error:String(t&&t.message||t).slice(0,300)}}}}let o=await Promise.all([...n.map(a),...i.map(async({id:e,styleType:t})=>{try{let r=await figma.getStyleByIdAsync(e);if(!r)return{kind:"unresolved",value:{id:e,kind:"style",reason:"notFoundOrUnavailable"}};return{kind:"style",value:{...m(r,!0),styleType:t,value:m("PAINT"===t?r.paints:"EFFECT"===t?r.effects:"GRID"===t?r.layoutGrids:r,!0)}}}catch(t){return{kind:"unresolved",value:{id:e,kind:"style",reason:"notFoundOrUnavailable",error:String(t&&t.message||t).slice(0,300)}}}})]),l=new Set(n),s=o.flatMap(e=>e.aliasIds||[]);for(;s.length>0;){let e=[...new Set(s)].filter(e=>!l.has(e)).sort();for(let t of e)l.add(t);let t=await Promise.all(e.map(a));o.push(...t),s=t.flatMap(e=>e.aliasIds||[])}let d=[...new Set(o.filter(e=>"variable"===e.kind&&e.collectionId).map(e=>e.collectionId))].sort(),f=(await Promise.all(d.map(async e=>{try{let t=await figma.variables.getVariableCollectionByIdAsync(e);return t?m(t,!0):null}catch(e){return null}}))).filter(e=>null!==e),c=o.filter(e=>"variable"===e.kind).map(e=>e.value),u=o.filter(e=>"style"===e.kind).map(e=>e.value),p=o.filter(e=>"unresolved"===e.kind).map(e=>e.value);return{collections:f,variables:c,styles:u,usedRemoteVariables:c.filter(e=>!0===e.remote),usedVariableIds:n,usedStyleIds:i.map(e=>e.id),localComplete:!1,usedRemoteComplete:0===p.length,unresolved:p,$variableRefCount:n.length,$styleRefCount:i.length}}async function N(e){let t=[],r=2;for(let n=d;ne)break;t.push(i),r+=a}return{pageNodes:t,packedBytes:r}}let O=c-1024,T=null;for(let e=0;e<5;e+=1){let{pageNodes:e,packedBytes:t}=await N(O);if(0===e.length)throw Error("DEVUP_SNAPSHOT_RANGE_INVALID");let n=function(e,t){let n=Math.min(A.length,d+e.length),{$variableRefCount:i,$styleRefCount:o,...l}=t,s=[...e,{id:"__DEVUP_SNAPSHOT_CURSOR__",type:"DEVUP_INTERNAL",fields:{offset:d,nextOffset:n,complete:n>=A.length,totalNodes:A.length},extra:{},fieldErrors:{}}],f={kind:"devupFastSnapshotEnvelope",schemaVersion:1,source:{fileKey:figma.fileKey||"",rootId:a},snapshot:{fileKey:figma.fileKey||"",version:null,rootIds:r.map(e=>e.id),nodes:s,diagnostics:[]},resources:l,integrity:{nodeCount:s.length,variableRefCount:i,styleRefCount:o,utf8Bytes:0}},c=0;for(let e=0;e<8&&(c=w(JSON.stringify(f)),f.integrity.utf8Bytes!==c);e+=1)f.integrity.utf8Bytes=c;if(f.integrity.utf8Bytes!==w(JSON.stringify(f)))throw Error("DEVUP_ENVELOPE_LENGTH_UNSTABLE");return{envelope:f,bytes:c}}(e,await v(e));if(n.bytes<=f){T=n;break}if(1===e.length)throw Error("DEVUP_ENVELOPE_TOO_LARGE");O=Math.max(1,Math.min(O-1,t-(n.bytes-f)-256))}if(!T)throw Error("DEVUP_ENVELOPE_TOO_LARGE");return T.envelope},fastTheme:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=Math.max(0,Math.floor(Number(e.theme.offset)||0));function r(e,t=new WeakSet,n=0){if(null===e||["string","number","boolean"].includes(typeof e))return e;if(void 0===e)return{$undefined:!0};if("bigint"==typeof e)return{$bigint:e.toString()};if(["function","symbol"].includes(typeof e))return{$unsupported:typeof e};if(n>12)return{$truncated:"max-depth"};if("object"==typeof e&&"parent"in e&&"string"==typeof e.id&&"string"==typeof e.type)return{$nodeId:e.id,$nodeType:e.type};if(Array.isArray(e))return e.map(e=>r(e,t,n+1));if(ArrayBuffer.isView(e))return{$binary:e.constructor.name,byteLength:e.byteLength};if(e instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:e.byteLength};if(t.has(e))return{$circular:!0};t.add(e);let i={};for(let a of function(e){let t=new Set(Object.keys(e)),r=e;for(;r&&r!==Object.prototype;){for(let e of Object.getOwnPropertyNames(r))t.add(e);r=Object.getPrototypeOf(r)}return[...t].sort()}(e))if(!(a.startsWith("_")||["parent","children","consumers"].includes(a)))try{let o=r(e[a],t,n+1);o&&"function"===o.$unsupported||(i[a]=o)}catch(e){i[a]={$error:"unavailable"}}return t.delete(e),i}let n=new Set,i=new Map;for(let e of figma.root.children)await e.loadAsync();for(let e of[figma.root,...figma.root.findAll(()=>!0)])for(let t of["boundVariables","fills","strokes","effects","layoutGrids","textStyleId","fillStyleId","strokeStyleId","backgroundStyleId","effectStyleId","gridStyleId"])try{!function e(t,r="",a=new WeakSet,o=0){if(!(o>16)&&null!==t&&"object"==typeof t&&!a.has(t)){if(a.add(t),Array.isArray(t)){for(let n of t)e(n,r,a,o+1);return}for(let[l,s]of("VARIABLE_ALIAS"===t.type&&"string"==typeof t.id&&t.id&&"figma.mixed"!==t.id&&"MIXED"!==t.id&&n.add(t.id),Object.entries(t))){let t="textStyleId"===l?"TEXT":["fillStyleId","strokeStyleId","backgroundStyleId"].includes(l)?"PAINT":"effectStyleId"===l?"EFFECT":"gridStyleId"===l?"GRID":null;t&&"string"==typeof s&&s&&"figma.mixed"!==s&&"MIXED"!==s&&!i.has(s)&&i.set(s,t),e(s,l||r,a,o+1)}}}({[t]:e[t]})}catch(e){}let[a,o,l,s,d,f]=await Promise.all([figma.variables.getLocalVariableCollectionsAsync(),figma.variables.getLocalVariablesAsync(),figma.getLocalPaintStylesAsync(),figma.getLocalTextStylesAsync(),figma.getLocalEffectStylesAsync(),figma.getLocalGridStylesAsync()]),c=new Set(o.map(e=>e.id)),u=new Set([...l,...s,...d,...f].map(e=>e.id)),p=[];async function g(e){try{return await figma.variables.getVariableByIdAsync(e)||null}catch(t){return p.push({id:e,kind:"variable",reason:"notFoundOrUnavailable",error:String(t&&t.message||t).slice(0,300)}),null}}function h(e){return Object.values(e.valuesByMode||{}).filter(e=>e&&"VARIABLE_ALIAS"===e.type&&"string"==typeof e.id&&e.id).map(e=>e.id)}let y=[...i.entries()].filter(([e])=>!u.has(e)).sort(([e],[t])=>e.localeCompare(t)).map(async([e,t])=>{try{let r=await figma.getStyleByIdAsync(e);return r?{style:r,styleType:t}:null}catch(t){return p.push({id:e,kind:"style",reason:"notFoundOrUnavailable",error:String(t&&t.message||t).slice(0,300)}),null}}),m=new Set,b=[],x=[...n,...o.flatMap(h)];for(;x.length>0;){let e=[...new Set(x)].filter(e=>!c.has(e)&&!m.has(e)).sort();for(let t of e)m.add(t);let t=(await Promise.all(e.map(g))).filter(Boolean);b.push(...t),x=t.flatMap(h)}let E=(await Promise.all(y)).filter(Boolean);for(let e of m)b.some(t=>t.id===e)||p.some(t=>t.id===e)||p.push({id:e,kind:"variable",reason:"notFoundOrUnavailable"});for(let[e]of[...i.entries()].filter(([e])=>!u.has(e)))E.some(t=>t.style.id===e)||p.some(t=>t.id===e)||p.push({id:e,kind:"style",reason:"notFoundOrUnavailable"});let A=[...new Set(b.map(e=>e.variableCollectionId))].filter(e=>!a.some(t=>t.id===e)).sort(),I=(await Promise.all(A.map(async e=>{try{return await figma.variables.getVariableCollectionByIdAsync(e)}catch(e){return null}}))).filter(Boolean);function S(e,t){return{...r(e),styleType:t,value:r("PAINT"===t?e.paints:"EFFECT"===t?e.effects:"GRID"===t?e.layoutGrids:e)}}let w=[...l.map(e=>S(e,"PAINT")),...s.map(e=>S(e,"TEXT")),...d.map(e=>S(e,"EFFECT")),...f.map(e=>S(e,"GRID")),...E.map(({style:e,styleType:t})=>S(e,t))].sort((e,t)=>e.id.localeCompare(t.id)),v=[...o,...b].map(e=>r(e)).sort((e,t)=>e.id.localeCompare(t.id)),N=[...a,...I].map(e=>r(e)).sort((e,t)=>e.id.localeCompare(t.id));function O(e){let t=[];for(let r=0;r=55296&&n<=56319){let t=r+1=56320&&t<=57343?(n=65536+(n-55296<<10)+(t-56320),r+=1):n=65533}else n>=56320&&n<=57343&&(n=65533);n<128?t.push(n):n<2048?t.push(192|n>>6,128|63&n):n<65536?t.push(224|n>>12,128|n>>6&63,128|63&n):t.push(240|n>>18,128|n>>12&63,128|n>>6&63,128|63&n)}return new Uint8Array(t)}p.sort((e,t)=>e.kind.localeCompare(t.kind)||e.id.localeCompare(t.id));let T=[...N.map(e=>({kind:"collection",value:e})),...v.map(e=>({kind:"variable",value:e})),...w.map(e=>({kind:"style",value:e})),...[...n].sort().map(e=>({kind:"usedVariableId",value:e})),...[...i.keys()].sort().map(e=>({kind:"usedStyleId",value:e})),...p.map(e=>({kind:"unresolved",value:e}))];if(t>T.length)throw Error("DEVUP_SNAPSHOT_RANGE_INVALID");let _=T.map(e=>O(JSON.stringify(e.value)).length+1),P=18432,M=null;for(let e=0;e<6;e+=1){let{pageItems:e,packed:r}=function(e){let r=[],n=0;for(let i=t;i0)||!(n+_[i]>e));i+=1)r.push(T[i]),n+=_[i];return{pageItems:r,packed:n}}(P),n=function(e,r){let n=t=>e.filter(e=>e.kind===t).map(e=>e.value),i=n("collection"),a=n("variable"),o=n("style"),l=n("unresolved"),s={kind:"devupFastThemeEnvelope",schemaVersion:1,source:{fileKey:figma.fileKey||"",version:null},resources:{collections:i,variables:a,styles:o,usedRemoteVariables:[],usedVariableIds:n("usedVariableId"),usedStyleIds:n("usedStyleId"),localComplete:!0,usedRemoteComplete:0===p.length,unresolved:l},page:{offset:t,nextOffset:r,complete:r>=T.length,totalItems:T.length},integrity:{collectionCount:i.length,variableCount:a.length,styleCount:o.length,unresolvedCount:l.length,utf8Bytes:0}},d=new Uint8Array;for(let e=0;e<8&&(d=O(JSON.stringify(s)),s.integrity.utf8Bytes!==d.length);e+=1)s.integrity.utf8Bytes=d.length;if(d=O(JSON.stringify(s)),s.integrity.utf8Bytes!==d.length)throw Error("DEVUP_ENVELOPE_LENGTH_UNSTABLE");return{envelope:s,bytes:d.length}}(e,t+e.length);if(n.bytes<=19456){M=n;break}if(e.length<=1)break;P=Math.max(1,Math.min(P-1,r-(n.bytes-19456)-256))}if(null===M||M.bytes>8388608)throw Error("DEVUP_ENVELOPE_TOO_LARGE");return M.envelope},largeValue:async function(e){let t;figma.skipInvisibleInstanceChildren=!1;let r=e.largeValue;async function n(e){try{let t=await figma.getNodeByIdAsync(e);if(t)return t}catch(r){if(!e.startsWith("I")||!e.includes(";"))throw r;try{return await t()}catch(e){throw Error(`${r.message||r}; nested lookup fallback: ${e.message||e}`)}}if(!e.startsWith("I")||!e.includes(";"))return null;return t();async function t(){let t=await figma.getNodeByIdAsync(e.slice(1).split(";")[0]),r=t?[t]:[],n=new Set;for(;r.length;){let t=r.pop();if(t.id===e)return t;if(!n.has(t.id)){if(n.add(t.id),n.size>1e4)throw Error("nested lookup exceeds 10000 nodes");Array.isArray(t.children)&&r.push(...t.children)}}throw Error(`nested node ${e} not found in its instance`)}}async function i(e,t,r=85e3,n=()=>{}){let a,o=Date.now();try{let i=await Promise.race([e.exportAsync(t),new Promise((e,t)=>{a=setTimeout(()=>t(Error(`DEVUP_ASSET_TIMEOUT: native export exceeded ${r} ms`)),r)})]);return n("native-export",Date.now()-o),i}catch(i){if(n("native-export",Date.now()-o,String(i.message||i).slice(0,500)),"SVG_STRING"!==t.format)throw i;let r=Date.now();try{let t=function(e){let t=e=>"number"==typeof e&&Number.isFinite(e),r=(e,t)=>{throw Error(`geometry fallback unsupported: ${e.type||"unknown"} ${e.id||""}: ${t}`)},n=0,i=0;function a(e,i,a){(void 0!==a&&!Array.isArray(a)||void 0===a&&Array.isArray(i)&&i.length)&&r(e,"unresolved paint requires native export");let o=Array.isArray(a)?a.filter(e=>!1!==e.visible):[];return o.length?(Array.isArray(i)&&i.length||r(e,"paint has no rendered geometry"),o.map(a=>{("SOLID"!==a.type||a.blendMode&&"NORMAL"!==a.blendMode)&&r(e,"paint type or blending requires native export");let o=a.color;o&&[o.r,o.g,o.b].every(e=>t(e)&&e>=0&&e<=1)||r(e,"invalid solid color");let l=void 0===a.opacity?1:a.opacity;return(!t(l)||l<0||l>1)&&r(e,"invalid paint opacity"),["NONZERO","EVENODD"].map(t=>{let a=i.filter(e=>e.windingRule===t);if(!a.length)return"";for(let t of a)"string"==typeof t.data&&t.data.trim()||r(e,"empty path"),n++;return`"']/g,e=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[e])}" fill="rgb(${100*o.r}%,${100*o.g}%,${100*o.b}%)" fill-opacity="${l}" fill-rule="${"EVENODD"===t?"evenodd":"nonzero"}"/>`}).join("")}).join("")):""}let o=e.absoluteRenderBounds,l=e.absoluteTransform;(!o||![o.x,o.y,o.width,o.height].every(t)||o.width<=0||o.height<=0)&&r(e,"missing render bounds"),(!Array.isArray(l)||2!==l.length||!l.every(e=>Array.isArray(e)&&3===e.length&&e.every(t))||0!==l[0][1]||0!==l[1][0]||l[0][0]<=0||l[1][1]<=0)&&r(e,"root rotation, reflection or shear requires native export");let s=(o.x-l[0][2])/l[0][0],d=(o.y-l[1][2])/l[1][1],f=o.width/l[0][0],c=o.height/l[1][1],u=function e(n,o){if(++i>1e4&&r(n,"subtree exceeds 10000 nodes"),n.isMask&&r(n,"mask requires native export"),!1===n.visible||0===n.opacity)return"";n.clipsContent&&r(n,"clipping requires native export"),Array.isArray(n.effects)&&n.effects.some(e=>!1!==e.visible)&&r(n,"effect requires native export"),n.blendMode&&!["NORMAL","PASS_THROUGH"].includes(n.blendMode)&&r(n,"layer blending requires native export");let l=void 0===n.opacity?1:n.opacity;(!t(l)||l<0||l>1)&&r(n,"invalid layer opacity");let s="";if(!o){let e=n.relativeTransform;Array.isArray(e)&&2===e.length&&e.every(e=>Array.isArray(e)&&3===e.length&&e.every(t))||r(n,"missing relative transform"),s=` transform="matrix(${e[0][0]} ${e[1][0]} ${e[0][1]} ${e[1][1]} ${e[0][2]} ${e[1][2]})"`}let d="";if(["GROUP","FRAME","INSTANCE","COMPONENT"].includes(n.type))(Array.isArray(n.fills)&&n.fills.some(e=>!1!==e.visible)||Array.isArray(n.strokes)&&n.strokes.some(e=>!1!==e.visible))&&r(n,"container paint requires native export"),Array.isArray(n.children)||r(n,"missing children"),d=(!0===n.itemReverseZIndex?[...n.children].reverse():n.children).map(t=>e(t,!1)).join("");else{for(let e of(["VECTOR","BOOLEAN_OPERATION","RECTANGLE","ELLIPSE","POLYGON","STAR","LINE"].includes(n.type)||r(n,"node type requires native export"),Array.isArray(n.strokes)&&n.strokes.some(e=>!1!==e.visible)&&"CENTER"!==n.strokeAlign&&r(n,"non-center stroke alignment requires native export"),Array.isArray(n.dashPattern)&&n.dashPattern.length&&r(n,"dashed stroke requires native export"),[n.fillGeometry,n.strokeGeometry]))void 0!==e&&(!Array.isArray(e)||e.some(e=>!["NONZERO","EVENODD"].includes(e.windingRule)))&&r(n,"unknown winding rule");d=a(n,n.fillGeometry,n.fills)+a(n,n.strokeGeometry,n.strokes)}return`${d}`}(e,!0);return n||r(e,"no painted geometry"),`${u}`}(e);return n("geometry-fallback",Date.now()-r),t}catch(e){throw Error(`native export: ${i.message||i}; ${e.message||e}`)}}finally{clearTimeout(a)}}let a=await n(r.nodeId);if(!a)throw Error("DEVUP_NODE_NOT_FOUND");function o(e){let t=[];for(let r=0;r=55296&&n<=56319){let t=r+1=56320&&t<=57343?(n=65536+(n-55296<<10)+(t-56320),r+=1):n=65533}else n>=56320&&n<=57343&&(n=65533);n<128?t.push(n):n<2048?t.push(192|n>>6,128|63&n):n<65536?t.push(224|n>>12,128|n>>6&63,128|63&n):t.push(240|n>>18,128|n>>12&63,128|n>>6&63,128|63&n)}return new Uint8Array(t)}let l=null;try{if("$export:svg"===r.field){if("function"!=typeof a.exportAsync)throw Error("unsupported");let e=await i(a,{format:"SVG_STRING"},Math.max(1,(Number(r.timeoutMs)||9e4)-5e3));if("string"!=typeof e)throw Error("unsupported");l=o(e)}else if("string"==typeof r.field&&r.field.startsWith("$export:png")){if("function"!=typeof a.exportAsync)throw Error("unsupported");let e=Number(r.field.split("@")[1]||2);if(!Number.isFinite(e)||e<=0||e>4)throw Error("invalid scale");let t=r.field.split("@"),n=a;if("visible"===t[2]){let e=new Set;for(;n&&n.id!==t[3]&&!e.has(n)&&e.size<64;)e.add(n),n=n.parent;if(!n||n.id!==t[3]||["PAGE","DOCUMENT"].includes(n.type))throw Error("invalid visible scope")}else if(t.length>2)throw Error("invalid export field");let i=await n.exportAsync({format:"PNG",contentsOnly:!0,useAbsoluteBounds:!1,constraint:{type:"SCALE",value:e}});l=i instanceof Uint8Array?i:new Uint8Array(i)}else if("styledTextSegments"===r.field&&"TEXT"===a.type&&"function"==typeof a.getStyledTextSegments)t=a.getStyledTextSegments(["fontName","fontWeight","fontSize","textDecoration","textCase","lineHeight","letterSpacing","fills","textStyleId","fillStyleId","listOptions","indentation","hyperlink"]);else if(r.field in a)t=a[r.field];else throw Error("unsupported")}catch(e){return{kind:"devupLargeValueUnsupported",fileKey:figma.fileKey||"",version:r.version,nodeId:r.nodeId,field:r.field,byteLength:r.byteLength,sha256:r.sha256,errorCode:"DEVUP_FIELD_UNSUPPORTED_BY_UPSTREAM",errorDetail:`${r.field} failed: ${e.message||e}`.slice(0,500)}}let s=null===l?o(JSON.stringify(function e(t,r=new WeakSet,n=0){if(null===t||["string","number","boolean"].includes(typeof t))return t;if(void 0===t)return{$undefined:!0};if("bigint"==typeof t)return{$bigint:t.toString()};if(["function","symbol"].includes(typeof t))return{$unsupported:typeof t};if(n>12)return{$truncated:"max-depth"};if("object"==typeof t&&"parent"in t&&"string"==typeof t.id&&"string"==typeof t.type)return{$nodeId:t.id,$nodeType:t.type};if(Array.isArray(t))return t.map(t=>e(t,r,n+1));if(ArrayBuffer.isView(t))return{$binary:t.constructor.name,byteLength:t.byteLength};if(t instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:t.byteLength};if(r.has(t))return{$circular:!0};r.add(t);let i={};for(let a of Object.keys(t).sort())try{let o=e(t[a],r,n+1);o&&"function"===o.$unsupported||(i[a]=o)}catch(e){i[a]={$error:String(e&&e.message?e.message:e)}}return r.delete(t),i}(t))):l,d=function(e){let t=[0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,0xe49b69c1,0xefbe4786,0xfc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x6ca6351,0x14292967,0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2],r=e.length,n=64*Math.ceil((r+9)/64),i=new Uint8Array(n);i.set(e),i[r]=128;let a=8*r;for(let e=0;e<8;e+=1)i[n-1-e]=255&Math.floor(a/2**(8*e));let o=[0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19],l=(e,t)=>e>>>t|e<<32-t;for(let e=0;e>>3,n=l(r[e-2],17)^l(r[e-2],19)^r[e-2]>>>10;r[e]=r[e-16]+t+r[e-7]+n>>>0}let[n,a,s,d,f,c,u,p]=o;for(let e=0;e<64;e+=1){let i=p+(l(f,6)^l(f,11)^l(f,25))+(f&c^~f&u)+t[e]+r[e]>>>0,o=(l(n,2)^l(n,13)^l(n,22))+(n&a^n&s^a&s)>>>0;p=u,u=c,c=f,f=d+i>>>0,d=s,s=a,a=n,n=i+o>>>0}for(let[e,t]of[n,a,s,d,f,c,u,p].entries())o[e]=o[e]+t>>>0}return o.map(e=>e.toString(16).padStart(8,"0")).join("")}(s);if(s.length!==r.byteLength||d!==r.sha256)throw Error("DEVUP_LARGE_VALUE_CHANGED");let f=Math.max(0,Math.floor(Number(r.offset)||0)),c=Math.min(65536,Math.max(1,Math.floor(Number(r.maxChunkBytes)||8192)));if(f>=s.length)throw Error("DEVUP_LARGE_VALUE_RANGE_INVALID");let u=Math.min(s.length,f+c);return{kind:"devupLargeValueFragment",fileKey:figma.fileKey||"",version:r.version,nodeId:r.nodeId,field:r.field,offset:f,nextOffset:u,byteLength:s.length,sha256:d,dataBase64:function(e){let t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",r="";for(let n=0;n>2],r+=t[(3&i)<<4|a>>4],r+=n+1>6]:"=",r+=n+210||t.some(e=>"string"!=typeof e||e.length>128||!/^(?:[0-9]+:[0-9]+|I[0-9]+:[0-9]+(?:;[0-9]+:[0-9]+)+)$/.test(e))||new Set(t).size!==t.length)throw Error("DEVUP_INVALID_INPUT: nodeIds must be 1..10 distinct Figma IDs");let r=(e,t)=>{try{let r=e[t];return"number"==typeof r&&Number.isFinite(r)?r:null}catch{return null}},n=(e,t)=>{try{let r=e[t];return"string"==typeof r&&r.length<=32&&/^[A-Z_]+$/.test(r)?r:null}catch{return null}},i=[];for(let e of t)try{let t=await figma.getNodeByIdAsync(e);if(!t||"DOCUMENT"===t.type||"PAGE"===t.type){i.push({nodeId:e,status:"unavailable",reason:t?"not-scene-node":"not-found"});continue}let a={width:r(t,"width"),height:r(t,"height"),padding:{top:r(t,"paddingTop"),right:r(t,"paddingRight"),bottom:r(t,"paddingBottom"),left:r(t,"paddingLeft")},gap:r(t,"itemSpacing"),layoutMode:n(t,"layoutMode"),align:{primary:n(t,"primaryAxisAlignItems"),counter:n(t,"counterAxisAlignItems")}},o=null!==a.width&&null!==a.height;i.push({nodeId:e,status:o?"measured":"partial",...a})}catch{i.push({nodeId:e,status:"unavailable",reason:"read-failed"})}return{kind:"devup-measure",schemaVersion:1,coordinateSpace:"figma-logical",nodes:i,partial:i.some(e=>"measured"!==e.status),evidenceLimit:"Null means unavailable or not applicable; no screenshot dimensions, descendants, theme, assets or code were read."}},metadata:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=await figma.getNodeByIdAsync(e.nodeId);if(!t)throw Error("DEVUP_NODE_NOT_FOUND");let r=t;for(;r&&"PAGE"!==r.type;)r=r.parent;r&&"PAGE"===r.type&&await figma.setCurrentPageAsync(r);let n=[];return!function(e){let t=[{node:e,visited:!1}],r=new Map;for(;t.length>0;){let e=t[t.length-1],i=e.node,a="children"in i?i.children:[];if(!e.visited){e.visited=!0;for(let e=a.length-1;e>=0;e-=1)t.push({node:a[e],visited:!1});continue}t.pop();let o=0,l=[];for(let e of a)l.push(e.id),o+=1+(r.get(e.id)||0);r.set(i.id,o),n.push({id:i.id,type:i.type,name:i.name,childrenIds:l,descendantCount:o})}}(t),{fileKey:figma.fileKey||"",version:null,rootId:t.id,nodes:n}},pageCatalog:async function(e){let t=figma.root.children;return{fileKey:figma.fileKey||"",version:null,rootIds:t.map(e=>e.id),nodes:t.map(e=>({id:e.id,type:e.type,fields:{name:e.name,parentId:null,childrenIds:[]},extra:{},fieldErrors:{}})),diagnostics:[]}},referencePng:async function(e){let t,r=e.nodeId;globalThis.__devupReferencePngAnswers||(globalThis.__devupReferencePngAnswers=new Map);let n=globalThis.__devupReferencePngAnswers,i=0;for(let[e,t]of n)Date.now()-t.at>12e4?n.delete(e):i+=t.text.length;if(i>=0xc000000)throw Error(`DEVUP_REFERENCE_PNG_BUSY ${i} characters are held for other reads`);let a=await figma.getNodeByIdAsync(r);if(!a||"function"!=typeof a.exportAsync)throw Error(`DEVUP_NODE_NOT_FOUND ${r}`);let o=a.absoluteRenderBounds||a.absoluteBoundingBox,l=Math.max(o.width,o.height),s=l>8192?8192/l:1,d=(e,t)=>({format:"PNG",constraint:t&&s<1?{type:o.width>=o.height?"WIDTH":"HEIGHT",value:8192}:{type:"SCALE",value:s},contentsOnly:e,useAbsoluteBounds:!1,colorProfile:"SRGB"}),f=e=>(Array.isArray(e)?e:[]).filter(e=>e&&!1!==e.visible),c=(e,t)=>!!e&&!!t&&e.x!!e&&e.x+t<=o.x&&e.y+t<=o.y&&e.x+e.width-t>=o.x+o.width&&e.y+e.height-t>=o.y+o.height,p=e=>`${e.type} ${JSON.stringify(e.name)}`,g=e=>JSON.parse(JSON.stringify(e===figma.mixed?null:e)),h=e=>(void 0===e.opacity||1===e.opacity)&&(!e.blendMode||["PASS_THROUGH","NORMAL"].includes(e.blendMode)),y=[],m=a.parent;for(;m&&"PAGE"!==m.type;)y.unshift(m),m=m.parent;let b=[255,255,255];for(let e of f(m&&m.backgrounds)){if("SOLID"!==e.type)continue;let t=void 0===e.opacity?1:e.opacity;["r","g","b"].forEach((r,n)=>{b[n]=t*e.color[r]*255+(1-t)*b[n]})}let x=0,E=e=>!1!==e.visible&&!e.isMask&&!!c(e.absoluteRenderBounds,o)&&((x+=1)>5e3||"TEXT"===e.type||!!f(e.fills).length||!!f(e.strokes).length||!!f(e.effects).length||"children"in e&&e.children.some(E)),A=e=>h(e)&&u(e.absoluteBoundingBox,0)&&1e-9>Math.abs(e.absoluteTransform[0][1])&&1e-9>Math.abs(e.absoluteTransform[1][0])&&0===Math.max(0,...[e.topLeftRadius,e.topRightRadius,e.bottomRightRadius,e.bottomLeftRadius].filter(e=>"number"==typeof e))&&f(e.fills).some(e=>"NORMAL"===(e.blendMode||"NORMAL")&&(void 0===e.opacity||1===e.opacity)&&("SOLID"===e.type||e.type.startsWith("GRADIENT_")&&e.gradientStops.every(e=>1===e.color.a))),I=[...y,a],S=[],w=[],v=new Map;for(let e=0;eA(e)&&i.slice(0,t).every(h));r.forEach((e,r)=>{e===t||(e.isMask&&!1!==e.visible&&r{let t=await a.exportAsync(d(!1,!0));return{kind:"devupReferencePng",nodeId:r,mimeType:"image/png",byteLength:t.length,data:figma.base64Encode(t),backdrop:N,...e}},T=(e,t)=>{let r=[...new Set(e)];return{approximate:r.join("; "),note:`Drawn as the canvas shows this area: devup-mcp cannot yet compose what get_screenshot draws here (${r.slice(0,3).join("; ")}). Layers reaching into it may show here: ${t}; get_screenshot leaves other layers out.`}},_=e=>{let t=JSON.stringify(e).replace(/[\u0080-\uffff]/g,e=>`\\u${e.charCodeAt(0).toString(16).padStart(4,"0")}`);if(t.length>0x5a00000){if(e.compose){let{compose:r,reaching:n,...i}=e;return _({...i,...T([`the answer would be ${t.length} characters`],n)})}throw Error(`DEVUP_REFERENCE_PNG_TOO_LARGE ${t.length} characters, over the 94371840 one answer is read back up to`)}if(t.length<=6291456)return e;let i=`${r}@${Date.now()}.${Math.random().toString(36).slice(2,10)}`;return n.set(i,{text:t,at:Date.now()}),{kind:"devupReferencePngChunked",nodeId:r,id:i,length:t.length}};if(0===S.length)return _(await O({}));let P=I.some(e=>e.isMask),M=[],C=new Set,k=(e,t)=>{let r=p(e);for(let t of f(e.fills))"IMAGE"===t.type?((t.rotation||f([t.filters]).some(e=>Object.values(e).some(e=>e)))&&M.push(`${r} has an image fill with a rotation or filters`),C.add(t.imageHash)):"SOLID"===t.type||t.type.startsWith("GRADIENT_")||M.push(`${r} is painted with ${t.type}`);for(let t of f(e.effects))["LAYER_BLUR","DROP_SHADOW","INNER_SHADOW","BACKGROUND_BLUR"].includes(t.type)||M.push(`${r} has a ${t.type}`);let n=f(e.strokes);if((e=>{let t=0===f(e.strokes).length?[]:[e.strokeWeight,e.strokeTopWeight,e.strokeRightWeight,e.strokeBottomWeight,e.strokeLeftWeight].filter(e=>"number"==typeof e&&e>0);if(0===t.length)return!1;let r=Math.max(...t),n="OUTSIDE"===e.strokeAlign?0:"CENTER"===e.strokeAlign?r/2:r,i="INSIDE"===e.strokeAlign?0:"CENTER"===e.strokeAlign?r/2:r,a=e.absoluteBoundingBox;return c(a&&{x:a.x-i,y:a.y-i,width:a.width+2*i,height:a.height+2*i},o)&&!u(a,n)})(e)){let t=["FRAME","RECTANGLE","COMPONENT","COMPONENT_SET","INSTANCE","SECTION"].includes(e.type),i=n.every(e=>"SOLID"===e.type||e.type.startsWith("GRADIENT_")),a=Array.isArray(e.dashPattern)&&e.dashPattern.length>0,o="INSIDE"===e.strokeAlign||!e.strokeJoin||"MITER"===e.strokeJoin;t&&i&&!a&&o||M.push(`${r}'s stroke reaches it`)}return{transform:g(e.absoluteTransform),width:e.width,height:e.height,radii:[e.topLeftRadius,e.topRightRadius,e.bottomRightRadius,e.bottomLeftRadius].map(e=>"number"==typeof e?e:0),fills:g(f(e.fills)),opacity:void 0===e.opacity?1:e.opacity,blendMode:e.blendMode||null,clips:!!e.clipsContent,effects:g(f(e.effects)),strokes:g(n),strokeAlign:e.strokeAlign||null,strokeWeights:["strokeTopWeight","strokeRightWeight","strokeBottomWeight","strokeLeftWeight"].map(t=>"number"==typeof e[t]?e[t]:"number"==typeof e.strokeWeight?e.strokeWeight:0)}},R=async e=>{let t=await e.exportAsync(d(!0,e===a));return{data:figma.base64Encode(t),area:e.absoluteRenderBounds||e.absoluteBoundingBox,blendMode:e.blendMode||null,size:[16,20].map(e=>(t[e]<<24|t[e+1]<<16|t[e+2]<<8|t[e+3])>>>0)}},D=async e=>{let t=v.get(e);if(!t)return null;let r=await t.exportAsync({...d(!0,!1),useAbsoluteBounds:!0});return{data:figma.base64Encode(r),area:t.absoluteBoundingBox,luminance:"LUMINANCE"===t.maskType}},L=[],$={},U={};try{for(let e of y){let t=k(e,!1),r=await D(e);r&&(t.mask=r),L.push(t)}let e=e=>f(e.effects).some(e=>"BACKGROUND_BLUR"===e.type);if(w.length>0&&(e(a)||"findOne"in a&&a.findOne(e))){let r="children"in a&&a.children.some(t=>e(t)||"findOne"in t&&!!t.findOne(e));if("children"in a&&!r&&!f(a.strokes).length){for(let e of($.own=k(a,!0),$.children=[],a.children)){if(!1===e.visible||e.isMask||!e.absoluteRenderBounds)continue;let t=await R(e);$.children.push({data:t.data,area:t.area,blendMode:t.blendMode})}t=[Math.ceil(o.width*s-1e-6),Math.ceil(o.height*s-1e-6)]}else M.push(`${p(a)} blurs what lies behind it`)}else if(P)t=[Math.ceil(o.width*s-1e-6),Math.ceil(o.height*s-1e-6)];else{let e=await R(a);$.picture={data:e.data,area:e.area,blendMode:e.blendMode},t=e.size}let r=await D(a);r&&($.mask=r),t&&t[0]*t[1]>8e6&&M.push(`the area is ${t[0]}x${t[1]} pixels`);let n=globalThis.__devupNormalizeImage,i=Date.now()+2e4,l=0;for(let e of C){let t=e&&figma.getImageByHash(e),r=t&&await t.getBytesAsync();if(!r){M.push(`the image ${e} could not be read`);continue}let a=null;if("function"==typeof n){let{width:e,height:o}=await t.getSizeAsync(),l=(...e)=>e.every((e,t)=>r[t]===e),s=l(137,80,78,71)||l(255,216)||l(71,73,70),d=Math.min(1,8192/Math.max(e,o,1)),f=Math.ceil(e*d)*Math.ceil(o*d),c=5*f+65536,u=5*r.length+4*e*o*(d<1?2:1)+4*f+4*c+1048576,p=i-Date.now();if((!s||Math.max(e,o)>8192||r.length>4*e*o+1048576)&&u<=0x20000000&&p>0){let t=await n(r,8192,p,e,o);t&&(r=t,a={width:e,height:o})}}if(l+r.length>0x4000000){M.push("its ancestors' images are over 67108864 bytes");break}l+=r.length,U[e]=a?{data:figma.base64Encode(r),...a}:figma.base64Encode(r)}}catch(e){M.push(`a part could not be exported: ${e instanceof Error?e.message:String(e)}`)}let B=`${S.slice(0,5).join(", ")}${S.length>5?` and ${S.length-5} more`:""}`;return M.length>0?_(await O(T(M,B))):_(await O({reaching:B,compose:{area:o,scale:s,width:t[0],height:t[1],page:b,layers:L,node:$,images:U}}))},referencePngChunk:async function(e){let t=e.largeValue,r=globalThis.__devupReferencePngAnswers,n=r&&r.get(t.id);if(!n||"string"!=typeof n.text)throw Error(`DEVUP_REFERENCE_PNG_ANSWER_GONE ${t.id}`);let i=n.text,a=Math.max(0,Math.floor(Number(t.offset)||0)),o=Math.min(i.length,a+4194304);return o>=i.length&&r.delete(t.id),{kind:"devupReferencePngChunk",id:t.id,offset:a,nextOffset:o,complete:o>=i.length,text:i.slice(a,o)}},search:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=await figma.getNodeByIdAsync(e.nodeId);if(!t)throw Error("DEVUP_NODE_NOT_FOUND");let r=t;for(;r&&"PAGE"!==r.type;)r=r.parent;if(!r||"PAGE"!==r.type)throw Error("DEVUP_PAGE_NOT_FOUND");await figma.setCurrentPageAsync(r);let n=e.search,i=new Set((n.nodeTypes.length?n.nodeTypes:["PAGE","SECTION","FRAME","COMPONENT_SET","COMPONENT"]).map(e=>e.toUpperCase()));function a(e){return e.normalize("NFC").toLocaleLowerCase().replace(/\s/gu,"")}let o=t===r?r.findAll(()=>!0):"findAll"in t?t.findAll(()=>!0):[],l=[t,...o].filter(e=>i.has(e.type)&&"string"==typeof e.name).map(e=>({node:e,score:function(e){if(e===n.query)return 400;if("exact"===n.matchKind)return null;let t=a(e),r=a(n.query);if(t===r)return 300;if(t.startsWith(r))return 200;if(t.includes(r))return 100;if("fuzzy"!==n.matchKind)return null;let i=function(e,t){let r=Array.from({length:t.length+1},(e,t)=>t);for(let n=0;nnull!==e.score).sort((e,t)=>t.score-e.score||e.node.name.localeCompare(t.node.name)||e.node.id.localeCompare(t.node.id)).slice(0,n.limit),s=new Map([[r.id,r]]);for(let e of[t,...l.map(e=>e.node)]){let t=e;for(;t&&"DOCUMENT"!==t.type;)s.set(t.id,t),t=t.parent}return{fileKey:figma.fileKey||"",version:null,rootIds:[r.id],nodes:[...s.values()].map(e=>({id:e.id,type:e.type,fields:{name:e.name,parentId:e.parent&&"DOCUMENT"!==e.parent.type?e.parent.id:null,childrenIds:"children"in e?e.children.map(e=>e.id):[]},extra:{},fieldErrors:{}})),diagnostics:[]}},sectionIndex:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=await figma.getNodeByIdAsync(e.nodeId);if(!t)throw Error("DEVUP_NODE_NOT_FOUND");if("SECTION"!==t.type){let e=t.parent,r=new Set;for(;e&&"SECTION"!==e.type&&!r.has(e.id);)r.add(e.id),e=e.parent;let n=e&&"SECTION"===e.type?e.id:null,i=n&&figma.fileKey?`https://www.figma.com/design/${figma.fileKey}?node-id=${n.replace(/:/g,"-")}`:null;throw Error("DEVUP_SECTION_REQUIRED "+JSON.stringify({pluginCode:"DEVUP_SECTION_REQUIRED",stage:"section-index",nodeId:t.id,nodeType:t.type,sectionId:n,nextAction:{tool:"devup_figma_export",how:n?`The url must point to a SECTION. This capture's SECTION is ${n}; select frames with frameIds.`:"The url must point to a SECTION. This node has no ancestor SECTION; choose the intended SECTION in Figma and copy its link.",arguments:i?"FRAME"===t.type&&t.parent===e?{url:i,frameIds:[t.id]}:{url:i}:null,requiredArguments:i?[]:["url"]}}))}let r=2048;function n(e){let t=e.absoluteBoundingBox||{x:"number"==typeof e.x?e.x:0,y:"number"==typeof e.y?e.y:0,width:"number"==typeof e.width?e.width:0,height:"number"==typeof e.height?e.height:0};return[t.x,t.y,t.width,t.height].every(Number.isFinite)?{x:t.x,y:t.y,width:t.width,height:t.height}:null}function i(e,t){if("FRAME"!==e.type||!1===e.visible||!t)return!1;let r=t.width/Math.max(1,t.height);return t.width>=240&&t.width<=1800&&t.height>=300&&t.height<=2e3&&r>=.25&&r<=2.5}function a(e){let t=0;for(let r=0;r=55296&&n<=56319&&r+1[e.id,t.id])),s=[],d=0;for(let e=0;ee.id));for(let r of t.children){if(e.has(r.id))continue;let t=n(r);t&&!1!==r.visible&&s.push({node:r,box:t})}}s.sort((e,t)=>e.box.y-t.box.y||e.box.x-t.box.x||e.node.id.localeCompare(t.node.id));let f=o.length>d||s.length>100,c=s.slice(0,100),u=new Set(c.map(({node:e})=>e.id)),p=new Set(e.rootIds),g={},h=new Map([[t.id,null]]);for(let e=0;e{let r=e.parent;for(;r&&r.id!==t.id&&!u.has(r.id);)r=r.parent;return[e.id,r&&u.has(r.id)?r.id:t.id]})),m=e=>c.filter(({node:t})=>y.get(t.id)===e).map(({node:e})=>e.id),b=n(t);if(!b)throw Error("DEVUP_NODE_BOUNDS_UNAVAILABLE");let x={id:t.id,type:t.type,fields:{name:t.name,parentId:t.parent&&"DOCUMENT"!==t.parent.type?t.parent.id:null,childrenIds:m(t.id),absoluteBoundingBox:b,visible:!1!==t.visible,projectionTruncated:f,nodeScreenIds:g},extra:{},fieldErrors:{}},E=c.length,A=c.map(({node:e,box:t})=>{let o,l=function(e){let t=[e],r=0,n=0;for(let e=0;er}}(e),s=Math.floor(r/E);E-=1;let d=function(e,t){let r=[e],n="",i=0,o=0,l=!1;if(t<=0)return{text:"",state:"budget-exhausted",spent:0};for(let e=0;e=120||o+e>t)return{text:n.trimEnd(),state:o+e>t?"budget-exhausted":"truncated",spent:o};n+=r,i+=1,o+=e}}if("children"in s){let e=Math.max(0,64-r.length);s.children.length>e&&(l=!0),r.push(...s.children.slice(0,e))}}}return{text:n,state:l?"truncated":n?"available":"no-text",spent:o}}(e,s);return r-=d.spent,{id:e.id,type:e.type,fields:{name:"string"==typeof e.name?e.name:"",parentId:y.get(e.id),childrenIds:m(e.id),absoluteBoundingBox:t,visible:!1!==e.visible,breadcrumb:function(e){let t=[],r=e;for(;r&&"DOCUMENT"!==r.type;)"string"==typeof r.name&&r.name&&t.push(r.name),r=r.parent;return t.reverse()}(e),directChildCount:"children"in e?e.children.length:0,textPreview:d.text,...null===(o=function(e){let t=[e],r=0,i=null;for(;t.length&&r++<400;){let e=t.shift();if("visible"in e&&!1===e.visible)continue;let r="children"in e?e.children.filter(e=>!("visible"in e)||!1!==e.visible):[];if(/header|nav|gnb/i.test(e.name||"")&&r.length>=2&&"layoutMode"in e&&"HORIZONTAL"===e.layoutMode){let t=r.map(n);t.every(Boolean)&&(i=Math.max(i||0,t.reduce((e,t)=>e+t.width,0)+(Number(e.itemSpacing)||0)*(t.length-1)))}t.push(...r.slice(0,80))}return i}(e))?{}:{navigationContentWidth:o},textPreviewState:d.state,subtreeNodeCount:l.subtreeNodeCount,estimatedSerializedBytes:l.estimatedSerializedBytes,selectionReasons:[i(e,t)?"screen-like":"explicit-selection-only","inside-section"],estimateTruncated:l.truncated},extra:{},fieldErrors:{}}}),I={fileKey:figma.fileKey||"",version:null,rootIds:[t.id],nodes:[x,...A],diagnostics:[]},S=a(JSON.stringify(I));for(let e=A.length-1;e>=0&&S>19456;e-=1)A[e].fields.textPreview="",A[e].fields.textPreviewState="budget-exhausted",S=a(JSON.stringify(I));let w=()=>I.nodes.length-1,v=A.length;for(;S>19456&&w()>1;){let e=I.nodes.pop();x.fields.projectionTruncated=!0,x.fields.childrenIds=x.fields.childrenIds.filter(t=>t!==e.id),S=a(JSON.stringify(I))}if(S>19456)throw Error("DEVUP_SECTION_INDEX_TOO_LARGE "+JSON.stringify({pluginCode:"DEVUP_SECTION_INDEX_TOO_LARGE",stage:"section-index",responseBytes:S,maxResponseBytes:19456,discoveredCandidates:v,retainedCandidates:w()}));return I},snapshot:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=await figma.getNodeByIdAsync(e.nodeId);if(!t)throw Error("DEVUP_NODE_NOT_FOUND");let r=["absoluteBoundingBox","absoluteRenderBounds","arcData","backgroundStyleId","blendMode","bottomLeftRadius","bottomRightRadius","boundVariables","characters","clipsContent","componentProperties","componentPropertyDefinitions","componentPropertyReferences","constraints","cornerRadius","counterAxisAlignItems","dashPattern","defaultVariant","effectStyleId","effects","fillStyleId","fills","fontName","fontSize","gridColumnAnchorIndex","gridColumnCount","gridColumnGap","gridColumnSizes","gridColumnSpan","gridRowAnchorIndex","gridRowCount","gridRowGap","gridRowSizes","gridRowSpan","gridStyleId","height","inferredAutoLayout","isAsset","isMask","itemSpacing","layoutGrow","layoutMode","layoutWrap","layoutPositioning","layoutSizingHorizontal","layoutSizingVertical","letterSpacing","lineHeight","maxHeight","maxLines","maxWidth","minHeight","minWidth","name","opacity","overflowDirection","paddingBottom","paddingLeft","paddingRight","paddingTop","primaryAxisAlignItems","reactions","rotation","strokeAlign","strokeBottomWeight","strokeLeftWeight","strokeRightWeight","strokeStyleId","strokeTopWeight","strokeWeight","strokes","targetAspectRatio","textAlignHorizontal","textAlignVertical","textAutoResize","textCase","textDecoration","textStyleId","textTruncation","topLeftRadius","topRightRadius","variantProperties","visible","width","x","y"],n=new Set(r),i=["fontName","fontWeight","fontSize","textDecoration","textCase","lineHeight","letterSpacing","fills","textStyleId","fillStyleId","listOptions","indentation","hyperlink"],a=e.snapshot,o=Math.max(0,Math.floor(Number(a.offset)||0)),l=Math.min(a.maxEnvelopeBytes?Math.max(8192,Math.floor(Number(a.maxEnvelopeBytes))):16e3,Math.max(4096,Math.floor(Number(a.maxPayloadBytes)||15e3))),s=Math.min(l-1024,Math.max(512,Math.floor(Number(a.maxFieldBytes)||4096))),d=new Set(["id","type","parent","children","mainComponent","masterComponent","instances"]),f=new Set(["parentId","childrenIds","name","characters","styledTextSegments","boundVariables"]);function c(e,t,r){let n=function(e){let t=[];for(let r=0;r=55296&&n<=56319){let t=r+1=56320&&t<=57343?(n=65536+(n-55296<<10)+(t-56320),r+=1):n=65533}else n>=56320&&n<=57343&&(n=65533);n<128?t.push(n):n<2048?t.push(192|n>>6,128|63&n):n<65536?t.push(224|n>>12,128|n>>6&63,128|63&n):t.push(240|n>>18,128|n>>12&63,128|n>>6&63,128|63&n)}return new Uint8Array(t)}(JSON.stringify(r));return n.length>0x1000000?{$truncated:"max-large-value-bytes",byteLength:n.length}:{$largeValue:{nodeId:e,field:t,byteLength:n.length,sha256:function(e){let t=[0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,0xe49b69c1,0xefbe4786,0xfc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x6ca6351,0x14292967,0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2],r=64*Math.ceil((e.length+9)/64),n=new Uint8Array(r);n.set(e),n[e.length]=128;let i=8*e.length;for(let e=0;e<8;e+=1)n[r-1-e]=255&Math.floor(i/2**(8*e));let a=[0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19],o=(e,t)=>e>>>t|e<<32-t;for(let e=0;e>>3,n=o(r[e-2],17)^o(r[e-2],19)^r[e-2]>>>10;r[e]=r[e-16]+t+r[e-7]+n>>>0}let[i,l,s,d,f,c,u,p]=a;for(let e=0;e<64;e+=1){let n=p+(o(f,6)^o(f,11)^o(f,25))+(f&c^~f&u)+t[e]+r[e]>>>0,a=(o(i,2)^o(i,13)^o(i,22))+(i&l^i&s^l&s)>>>0;p=u,u=c,c=f,f=d+n>>>0,d=s,s=l,l=i,i=n+a>>>0}for(let[e,t]of[i,l,s,d,f,c,u,p].entries())a[e]=a[e]+t>>>0}return a.map(e=>e.toString(16).padStart(8,"0")).join("")}(n),cursor:{nextOffset:0,maxChunkBytes:12288}}}}function u(e){return function(e){let t=0;for(let r=0;r=55296&&n<=56319&&r+112)return{$truncated:"max-depth"};if("object"==typeof t&&"parent"in t&&"string"==typeof t.id&&"string"==typeof t.type)return{$nodeId:t.id,$nodeType:t.type};if(Array.isArray(t))return t.map(t=>e(t,r,n+1));if(ArrayBuffer.isView(t))return{$binary:t.constructor.name,byteLength:t.byteLength};if(t instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:t.byteLength};if(r.has(t))return{$circular:!0};r.add(t);let i={};for(let a of Object.keys(t).sort())try{let o=e(t[a],r,n+1);o&&"function"===o.$unsupported||(i[a]=o)}catch(e){i[a]={$error:String(e&&e.message?e.message:e)}}return r.delete(t),i}(r),a=u(i);if(a<=s)return i;let o=c(e,t,i);return o.$truncated&&(n[t]=`DEVUP_FIELD_VALUE_UNSUPPORTED:${a}>16777216`),o}let g=new Map;async function h(e,t,r){if("INSTANCE"!==e.type||"function"!=typeof e.getMainComponentAsync)return;g.has(e.id)||g.set(e.id,(async()=>{try{let t=await e.getMainComponentAsync();return{fields:{mainComponentId:t?t.id:null,mainComponentName:t?t.name:null,mainComponentSetId:t&&t.parent&&"COMPONENT_SET"===t.parent.type?t.parent.id:null}}}catch(e){return{error:String(e&&e.message?e.message:e)}}})());let n=await g.get(e.id);n.error?r.mainComponentId=n.error:Object.assign(t,n.fields)}async function y(e){let t={},a={},o={};for(let i of(t.parentId=e.parent?e.parent.id:null,e.parent&&("PAGE"===e.parent.type||"SECTION"===e.parent.type||"COMPONENT_SET"===e.parent.type)&&(t.parentType=e.parent.type,"SECTION"===e.parent.type&&(t.parentName=e.parent.name)),t.childrenIds="children"in e?e.children.map(e=>e.id):[],!("overflowDirection"in e)&&["FRAME","COMPONENT","INSTANCE","COMPONENT_SET"].includes(e.type)&&(t.overflowDirection=null),function(e){let t=new Set,n=e;for(;n&&n!==Object.prototype;){for(let e of Object.getOwnPropertyNames(n))t.add(e);n=Object.getPrototypeOf(n)}for(let n of r)try{n in e&&t.add(n)}catch(e){}return[...t].sort()}(e)))if(!(d.has(i)||i.startsWith("_")||"componentPropertyDefinitions"===i&&"COMPONENT"===e.type&&e.parent&&"COMPONENT_SET"===e.parent.type))try{let r=e[i];if("function"==typeof r)continue;let l=p(e.id,i,r,o);(n.has(i)?t:a)[i]=l}catch(e){o[i]=String(e&&e.message?e.message:e)}if("TEXT"===e.type&&"function"==typeof e.getStyledTextSegments)try{t.styledTextSegments=p(e.id,"styledTextSegments",e.getStyledTextSegments(i),o)}catch(e){o.styledTextSegments=String(e&&e.message?e.message:e)}return await h(e,t,o),{id:e.id,type:e.type,fields:t,extra:a,fieldErrors:o}}let m=[],b=[t];for(;b.length;){let e=b.shift();m.push(e),"children"in e&&b.push(...e.children)}let x=[],E=l-1024,A=2;for(let e=o;eNumber(e.protected)-Number(t.protected)||t.byteLength-e.byteLength),n)){if(r<=t)break;let n=c(e.id,i.name,e[i.sectionName][i.name]);e[i.sectionName][i.name]=n,n.$truncated&&(e.fieldErrors[i.name]=`DEVUP_FIELD_VALUE_UNSUPPORTED:${i.byteLength}>16777216`),r=u(e)}return e}(await y(m[e]),E),r=u(t)+ +!!x.length;if(x.length&&A+r>E)break;x.push(t),A+=r}let I=Math.min(m.length,o+x.length);return x.push({id:"__DEVUP_SNAPSHOT_CURSOR__",type:"DEVUP_INTERNAL",fields:{offset:o,nextOffset:I,complete:I>=m.length,totalNodes:m.length},extra:{},fieldErrors:{}}),{fileKey:figma.fileKey||"",version:null,rootIds:[t.id],nodes:x,diagnostics:[]}},usedResources:async function(e){let t=e.resources;function r(e,t=new WeakSet,n=0){if(null===e||["string","number","boolean"].includes(typeof e))return e;if(void 0===e)return{$undefined:!0};if("bigint"==typeof e)return{$bigint:e.toString()};if(["function","symbol"].includes(typeof e))return{$unsupported:typeof e};if(n>12)return{$truncated:"max-depth"};if("object"==typeof e&&"parent"in e&&"string"==typeof e.id&&"string"==typeof e.type)return{$nodeId:e.id,$nodeType:e.type};if(Array.isArray(e))return e.map(e=>r(e,t,n+1));if(ArrayBuffer.isView(e))return{$binary:e.constructor.name,byteLength:e.byteLength};if(e instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:e.byteLength};if(t.has(e))return{$circular:!0};t.add(e);let i={},a=new Set(Object.keys(e)),o=e;for(;o&&o!==Object.prototype;){for(let e of Object.getOwnPropertyNames(o))a.add(e);o=Object.getPrototypeOf(o)}for(let o of[...a].sort())if(!(o.startsWith("_")||["parent","children","consumers"].includes(o)))try{let a=r(e[o],t,n+1);a&&"function"===a.$unsupported||(i[o]=a)}catch(e){i[o]={$error:"unavailable"}}return t.delete(e),i}let n=new Map,i="ok";try{for(let e of(await figma.variables.getLocalVariablesAsync()))n.set(e.id,e)}catch(e){i="unavailable"}function a(e){return Object.values(e.valuesByMode||{}).filter(e=>e&&"VARIABLE_ALIAS"===e.type&&"string"==typeof e.id&&e.id).map(e=>e.id)}async function o(e){let t=n.get(e);if(t)return{value:r(t),collectionId:t.variableCollectionId,aliasIds:a(t)};try{let t=await figma.variables.getVariableByIdAsync(e);return t?{value:r(t),collectionId:t.variableCollectionId,aliasIds:a(t)}:{unresolved:{id:e,kind:"variable",reason:"notInFileAndLookupEmpty"}}}catch(t){return{unresolved:{id:e,kind:"variable",reason:"notInFileAndLookupThrew",error:String(t&&t.message||t).slice(0,300)}}}}let l=await Promise.all(t.variableIds.map(o)),s=new Set(t.variableIds),d=l.flatMap(e=>e.aliasIds||[]);for(;d.length>0;){let e=[...new Set(d)].filter(e=>!s.has(e)).sort();for(let t of e)s.add(t);let t=await Promise.all(e.map(o));l.push(...t),d=t.flatMap(e=>e.aliasIds||[])}let f=[...new Set(l.flatMap(e=>e.collectionId?[e.collectionId]:[]))].sort(),c=await Promise.all(f.map(async e=>{try{let t=await figma.variables.getVariableCollectionByIdAsync(e);return t?[r(t)]:[]}catch(e){return[]}})),u=await Promise.all(t.styles.map(async e=>{try{let t=await figma.getStyleByIdAsync(e.id);if(!t)return{unresolved:{id:e.id,kind:"style",reason:"notFoundOrUnavailable"}};return{value:{...r(t),styleType:e.styleType,value:r("PAINT"===e.styleType?t.paints:"EFFECT"===e.styleType?t.effects:"GRID"===e.styleType?t.layoutGrids:t)}}}catch(t){return{unresolved:{id:e.id,kind:"style",reason:"notFoundOrUnavailable",error:String(t&&t.message||t).slice(0,300)}}}}));return{collections:c.flat(),variables:l.flatMap(e=>e.value?[e.value]:[]),styles:u.flatMap(e=>e.value?[e.value]:[]),usedVariableIds:t.variableIds,usedStyleIds:t.styles.map(e=>e.id),localVariableListing:i,localVariableCount:n.size,unresolved:[...l,...u].flatMap(e=>e.unresolved?[e.unresolved]:[])}},variableCatalog:async function(e){let[t,r,n,i,a]=await Promise.all([figma.variables.getLocalVariableCollectionsAsync(),figma.getLocalPaintStylesAsync(),figma.getLocalTextStylesAsync(),figma.getLocalEffectStylesAsync(),figma.getLocalGridStylesAsync()]),o=["PAINT","TEXT","EFFECT","GRID"];return{collections:t.map(e=>(function e(t,r=new WeakSet,n=0){if(null===t||["string","number","boolean"].includes(typeof t))return t;if(void 0===t)return{$undefined:!0};if("bigint"==typeof t)return{$bigint:t.toString()};if(["function","symbol"].includes(typeof t))return{$unsupported:typeof t};if(n>12)return{$truncated:"max-depth"};if(Array.isArray(t))return t.map(t=>e(t,r,n+1));if(r.has(t))return{$circular:!0};r.add(t);let i={},a=new Set(Object.keys(t)),o=t;for(;o&&o!==Object.prototype;){for(let e of Object.getOwnPropertyNames(o))a.add(e);o=Object.getPrototypeOf(o)}for(let o of[...a].sort())if(!o.startsWith("_"))try{let a=e(t[o],r,n+1);a&&"function"===a.$unsupported||(i[o]=a)}catch(e){i[o]={$error:String(e&&e.message?e.message:e)}}return r.delete(t),i})(e)),variableIds:[...new Set(t.flatMap(e=>e.variableIds))].sort(),styles:[r,n,i,a].flatMap((e,t)=>e.map(e=>({id:e.id,styleType:o[t]}))).sort((e,t)=>e.id.localeCompare(t.id)),localComplete:!0,usedRemoteComplete:!1}},variables:async function(e){let t=e.resources;function r(e,t=new WeakSet,n=0){if(null===e||["string","number","boolean"].includes(typeof e))return e;if(void 0===e)return{$undefined:!0};if("bigint"==typeof e)return{$bigint:e.toString()};if(["function","symbol"].includes(typeof e))return{$unsupported:typeof e};if(n>12)return{$truncated:"max-depth"};if("object"==typeof e&&"parent"in e&&"string"==typeof e.id&&"string"==typeof e.type)return{$nodeId:e.id,$nodeType:e.type};if(Array.isArray(e))return e.map(e=>r(e,t,n+1));if(ArrayBuffer.isView(e))return{$binary:e.constructor.name,byteLength:e.byteLength};if(e instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:e.byteLength};if(t.has(e))return{$circular:!0};t.add(e);let i={},a=new Set(Object.keys(e)),o=e;for(;o&&o!==Object.prototype;){for(let e of Object.getOwnPropertyNames(o))a.add(e);o=Object.getPrototypeOf(o)}for(let o of[...a].sort())if(!(o.startsWith("_")||["parent","children","consumers"].includes(o)))try{let a=r(e[o],t,n+1);a&&"function"===a.$unsupported||(i[o]=a)}catch(e){i[o]={$error:String(e&&e.message?e.message:e)}}return t.delete(e),i}let[n,i]=await Promise.all([Promise.all(t.variableIds.map(e=>figma.variables.getVariableByIdAsync(e))),Promise.all(t.styles.map(e=>figma.getStyleByIdAsync(e.id)))]),a=new Map(t.styles.map(e=>[e.id,e])),o=await Promise.all(i.filter(Boolean).map(async e=>{let t=a.get(e.id),n=t.styleType;if(Number.isInteger(t.consumerStart)&&Number.isInteger(t.consumerEnd)){let i=await e.getStyleConsumersAsync();return{id:e.id,styleType:n,$consumerStart:t.consumerStart,$consumerEntries:i.slice(t.consumerStart,t.consumerEnd).map(e=>[e.node.id,e.node.type,r(e.fields)])}}let i=await e.getStyleConsumersAsync();return{...r(e),styleType:n,$consumerCount:i.length,value:r("PAINT"===n?e.paints:"EFFECT"===n?e.effects:"GRID"===n?e.layoutGrids:e)}}));return{variables:n.filter(Boolean).map(e=>r(e)),styles:o}}},t=new Map,r=0;globalThis.__devupNormalizeImage=(e,n,i,a,o)=>new Promise(l=>{r+=1;let s=`decode-${r}`,d=setTimeout(()=>{t.delete(s),l(null)},i);t.set(s,e=>{clearTimeout(d),t.delete(s),l(e)}),figma.ui.postMessage({kind:"devup-decode",id:s,bytes:e,maxEdge:n,width:a,height:o})});let n=globalThis;function i(){let e=figma.currentPage,t=e.selection;return t.length>0&&(n.__devupLastSelection=t.slice(0,20).map(e=>({id:e.id,name:e.name.slice(0,256),type:e.type})),n.__devupLastSelectionPage={id:e.id,name:e.name.slice(0,256)}),{zoom:Number.isFinite(figma.viewport?.zoom)?figma.viewport.zoom:null,observedAt:Date.now(),lastKnownSelection:n.__devupLastSelection??null,lastKnownPage:n.__devupLastSelectionPage??null,currentPage:{id:e.id,name:e.name.slice(0,256)},selection:t.slice(0,20).map(e=>({id:e.id,name:e.name.slice(0,256),type:e.type})),selectionCount:t.length}}function a(){let e={kind:"devup-context",...i()};figma.ui.postMessage(e)}async function o(t){let r=e[t.script];if(!r)return{kind:"devup-result",requestId:t.requestId,error:`DEVUP_BRIDGE_UNKNOWN_SCRIPT: ${t.script} (아는 스크립트: ${Object.keys(e).join(", ")})`};try{var n;"bridgeContext"===t.script&&i();let e=await r({nodeId:(n=t.params).nodeId??"",rootIds:n.rootIds??[],snapshot:n.snapshot??{},search:n.search??{},explore:n.explore??{},resources:n.resources??{variableIds:[],styles:[]},largeValue:n.largeValue??{},asset:n.asset??{},theme:n.theme??{offset:0}});return{kind:"devup-result",requestId:t.requestId,data:e}}catch(e){return{kind:"devup-result",requestId:t.requestId,error:e instanceof Error?e.message:String(e)}}}let l=[],s=!1,d=null,f=new Set;async function c(){if(!s){s=!0;try{for(;l.length>0;){let e=l.shift();d=e.requestId;let t=await o(e).catch(t=>({kind:"devup-result",requestId:e.requestId,error:t instanceof Error?t.message:String(t)}));d=null,f.delete(e.requestId)||figma.ui.postMessage(t)}}finally{s=!1}}}let u=null;function p(){u??(u=(async()=>{try{await figma.loadAllPagesAsync(),await Promise.all([figma.variables.getLocalVariableCollectionsAsync(),figma.variables.getLocalVariablesAsync(),figma.getLocalPaintStylesAsync(),figma.getLocalTextStylesAsync(),figma.getLocalEffectStylesAsync(),figma.getLocalGridStylesAsync()])}catch{}finally{u=null}})())}figma.showUI(__html__,{width:320,height:220}),p(),figma.on("selectionchange",a),figma.on("currentpagechange",a),figma.ui.onmessage=e=>{if("object"==typeof e&&null!==e){if("devup-ready"===e.kind){let e={kind:"devup-status",fileKey:figma.fileKey??null,fileName:figma.root.name,port:1993,...i()};figma.ui.postMessage(e);return}if("devup-job"===e.kind){l.push(e),c();return}if("devup-cancel"===e.kind){var r;let t;return void(r=e.requestId,(t=l.findIndex(e=>e.requestId===r))>=0?l.splice(t,1):d===r&&f.add(r))}if("devup-decoded"===e.kind)return void t.get(e.id)?.(e.png??null);"devup-in-view"===e.kind&&p()}}})(); \ No newline at end of file +(()=>{"use strict";let e={assets:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=e.asset;function r(e){let t=[0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,0xe49b69c1,0xefbe4786,0xfc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x6ca6351,0x14292967,0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2],r=64*Math.ceil((e.length+9)/64),n=new Uint8Array(r);n.set(e),n[e.length]=128;let i=8*e.length;for(let e=0;e<8;e+=1)n[r-1-e]=255&Math.floor(i/2**(8*e));let a=[0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19],o=(e,t)=>e>>>t|e<<32-t;for(let e=0;e>>3,n=o(r[e-2],17)^o(r[e-2],19)^r[e-2]>>>10;r[e]=r[e-16]+t+r[e-7]+n>>>0}let[i,l,s,d,f,c,u,p]=a;for(let e=0;e<64;e+=1){let n=p+(o(f,6)^o(f,11)^o(f,25))+(f&c^~f&u)+t[e]+r[e]>>>0,a=(o(i,2)^o(i,13)^o(i,22))+(i&l^i&s^l&s)>>>0;p=u,u=c,c=f,f=d+n>>>0,d=s,s=l,l=i,i=n+a>>>0}for(let[e,t]of[i,l,s,d,f,c,u,p].entries())a[e]=a[e]+t>>>0}return a.map(e=>e.toString(16).padStart(8,"0")).join("")}async function n(e){try{let t=await figma.getNodeByIdAsync(e);if(t)return t}catch(r){if(!e.startsWith("I")||!e.includes(";"))throw r;try{return await t()}catch(e){throw Error(`${r.message||r}; nested lookup fallback: ${e.message||e}`)}}if(!e.startsWith("I")||!e.includes(";"))return null;return t();async function t(){let t=await figma.getNodeByIdAsync(e.slice(1).split(";")[0]),r=t?[t]:[],n=new Set;for(;r.length;){let t=r.pop();if(t.id===e)return t;if(!n.has(t.id)){if(n.add(t.id),n.size>1e4)throw Error("nested lookup exceeds 10000 nodes");Array.isArray(t.children)&&r.push(...t.children)}}throw Error(`nested node ${e} not found in its instance`)}}async function i(e,t,r=85e3,n=()=>{}){let a,o=Date.now();try{let i=await Promise.race([e.exportAsync(t),new Promise((e,t)=>{a=setTimeout(()=>t(Error(`DEVUP_ASSET_TIMEOUT: native export exceeded ${r} ms`)),r)})]);return n("native-export",Date.now()-o),i}catch(i){if(n("native-export",Date.now()-o,String(i.message||i).slice(0,500)),"SVG_STRING"!==t.format)throw i;let r=Date.now();try{let t=function(e){let t=e=>"number"==typeof e&&Number.isFinite(e),r=(e,t)=>{throw Error(`geometry fallback unsupported: ${e.type||"unknown"} ${e.id||""}: ${t}`)},n=0,i=0;function a(e,i,a){(void 0!==a&&!Array.isArray(a)||void 0===a&&Array.isArray(i)&&i.length)&&r(e,"unresolved paint requires native export");let o=Array.isArray(a)?a.filter(e=>!1!==e.visible):[];return o.length?(Array.isArray(i)&&i.length||r(e,"paint has no rendered geometry"),o.map(a=>{("SOLID"!==a.type||a.blendMode&&"NORMAL"!==a.blendMode)&&r(e,"paint type or blending requires native export");let o=a.color;o&&[o.r,o.g,o.b].every(e=>t(e)&&e>=0&&e<=1)||r(e,"invalid solid color");let l=void 0===a.opacity?1:a.opacity;return(!t(l)||l<0||l>1)&&r(e,"invalid paint opacity"),["NONZERO","EVENODD"].map(t=>{let a=i.filter(e=>e.windingRule===t);if(!a.length)return"";for(let t of a)"string"==typeof t.data&&t.data.trim()||r(e,"empty path"),n++;return`"']/g,e=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[e])}" fill="rgb(${100*o.r}%,${100*o.g}%,${100*o.b}%)" fill-opacity="${l}" fill-rule="${"EVENODD"===t?"evenodd":"nonzero"}"/>`}).join("")}).join("")):""}let o=e.absoluteRenderBounds,l=e.absoluteTransform;(!o||![o.x,o.y,o.width,o.height].every(t)||o.width<=0||o.height<=0)&&r(e,"missing render bounds"),(!Array.isArray(l)||2!==l.length||!l.every(e=>Array.isArray(e)&&3===e.length&&e.every(t))||0!==l[0][1]||0!==l[1][0]||l[0][0]<=0||l[1][1]<=0)&&r(e,"root rotation, reflection or shear requires native export");let s=(o.x-l[0][2])/l[0][0],d=(o.y-l[1][2])/l[1][1],f=o.width/l[0][0],c=o.height/l[1][1],u=function e(n,o){if(++i>1e4&&r(n,"subtree exceeds 10000 nodes"),n.isMask&&r(n,"mask requires native export"),!1===n.visible||0===n.opacity)return"";n.clipsContent&&r(n,"clipping requires native export"),Array.isArray(n.effects)&&n.effects.some(e=>!1!==e.visible)&&r(n,"effect requires native export"),n.blendMode&&!["NORMAL","PASS_THROUGH"].includes(n.blendMode)&&r(n,"layer blending requires native export");let l=void 0===n.opacity?1:n.opacity;(!t(l)||l<0||l>1)&&r(n,"invalid layer opacity");let s="";if(!o){let e=n.relativeTransform;Array.isArray(e)&&2===e.length&&e.every(e=>Array.isArray(e)&&3===e.length&&e.every(t))||r(n,"missing relative transform"),s=` transform="matrix(${e[0][0]} ${e[1][0]} ${e[0][1]} ${e[1][1]} ${e[0][2]} ${e[1][2]})"`}let d="";if(["GROUP","FRAME","INSTANCE","COMPONENT"].includes(n.type))(Array.isArray(n.fills)&&n.fills.some(e=>!1!==e.visible)||Array.isArray(n.strokes)&&n.strokes.some(e=>!1!==e.visible))&&r(n,"container paint requires native export"),Array.isArray(n.children)||r(n,"missing children"),d=(!0===n.itemReverseZIndex?[...n.children].reverse():n.children).map(t=>e(t,!1)).join("");else{for(let e of(["VECTOR","BOOLEAN_OPERATION","RECTANGLE","ELLIPSE","POLYGON","STAR","LINE"].includes(n.type)||r(n,"node type requires native export"),Array.isArray(n.strokes)&&n.strokes.some(e=>!1!==e.visible)&&"CENTER"!==n.strokeAlign&&r(n,"non-center stroke alignment requires native export"),Array.isArray(n.dashPattern)&&n.dashPattern.length&&r(n,"dashed stroke requires native export"),[n.fillGeometry,n.strokeGeometry]))void 0!==e&&(!Array.isArray(e)||e.some(e=>!["NONZERO","EVENODD"].includes(e.windingRule)))&&r(n,"unknown winding rule");d=a(n,n.fillGeometry,n.fills)+a(n,n.strokeGeometry,n.strokes)}return`${d}`}(e,!0);return n||r(e,"no painted geometry"),`${u}`}(e);return n("geometry-fallback",Date.now()-r),t}catch(e){throw Error(`native export: ${i.message||i}; ${e.message||e}`)}}finally{clearTimeout(a)}}let a=Date.now(),o="lookup",l="unknown",s=Math.min(6e5,Math.max(1e3,Number(t.timeoutMs)||9e4)),d={method:"native",stages:[]};function f(e,t,r){d.stages.push({stage:e,elapsedMs:t}),r&&(d.nativeError=r),"geometry-fallback"===e&&(d.method="geometry")}async function c(e){let t;try{return await Promise.race([e(),new Promise((e,r)=>{t=setTimeout(()=>r(Error(`DEVUP_ASSET_TIMEOUT: ${o} exceeded ${s} ms asset budget`)),Math.max(0,s-(Date.now()-a)))})])}finally{clearTimeout(t)}}function u(e){let r=e&&"string"==typeof e.message?e.message:String(e),n="string"==typeof figma.editorType?` (editor: ${figma.editorType})`:"";return`${o} failed after ${Date.now()-a} ms${n} (node: ${t.nodeId}, type: ${l}): ${r}`.slice(0,500)}function p(e,r){return e&&d.renderInfo&&(d.renderInfo.attemptedRepresentation=d.renderInfo.representation,d.renderInfo.representation="not-exported",d.renderInfo.renderedAs="not-exported",d.renderInfo.effectsBakedIntoAsset=!1,d.renderInfo.fidelity="unverified"),{kind:"devupAssetExport",fileKey:figma.fileKey||"",version:t.version,assetId:t.assetId,nodeId:t.nodeId,field:t.field,imageHash:t.imageHash,format:t.format,scale:t.scale,renderMode:h,usageAspectRatio:t.usageAspectRatio??null,maxWidth:t.maxWidth??null,srcset:!0===t.srcset,visibleScopeNodeId:t.visibleScopeNodeId??null,status:"failed",byteLength:null,sha256:null,errorCode:e,exportDiagnostics:d,...r?{errorDetail:r}:{}}}let g="string"==typeof t.field&&t.field.startsWith("$original-image/fills/"),h=void 0===t.renderMode?g?"fill":["svg","pdf"].includes(t.format)?"node":"original-source":t.renderMode,y=["auto","original-source"].includes(h)?"auto"===h?"node":"fill":h;function m(e,r){let n=e=>"number"==typeof e&&Number.isFinite(e)&&e>0,i=Array.isArray(e.effects)?e.effects:null,a=[],o=e=>[...e].slice(0,128).join(""),l=["string"==typeof e.name?o(e.name):"layer"],s=new Set,d=e;for(let t=e.parent;t&&a.length<32&&!s.has(t);t=t.parent)s.add(t),l.length<4&&"string"==typeof t.name&&t.name.length&&!["PAGE","DOCUMENT"].includes(t.type)&&l.push(o(t.name)),a.push({type:t.type||"unknown",clipsContent:t.clipsContent??null,isMask:t.isMask??null,effects:Array.isArray(t.effects)?t.effects:null,opacity:t.opacity??null,blendMode:t.blendMode??null,visible:t.visible??null,siblingMaskPresent:Array.isArray(t.children)?t.children.some(e=>e!==d&&!0===e.isMask):null}),d=t;return{sourceKind:r?"image-fill":t.imageHash?"image-node":"vector-node",renderedAs:"fill"===y?"raw-fill":"node-render",representation:"fill"===y?"original-fill":"native-node",layerHierarchy:l.reverse(),scope:"fill"===y?"selected-paint-original-bytes":"exact-node-subtree",exportSettings:"node"===y?{contentsOnly:!0,useAbsoluteBounds:!1}:null,requestedScale:t.scale,scaleSemantics:"fill"===y?"intrinsic-original-bytes-no-resize":["png","jpg"].includes(t.format)?"native-node-SCALE-constraint":"not-applied-to-vector-document",nodeSize:n(e.width)&&n(e.height)?{width:e.width,height:e.height,aspectRatio:e.width/e.height}:null,outputSize:null,dimensionEvidence:"unknown-until-byte-inspection",aspectRatioComparison:"unknown",scaleMode:r?r.scaleMode??null:null,imageTransform:r?r.imageTransform??null:null,visibleSourceRatio:null,visibleSourceRatioEvidence:"not-computed",effects:i,extraFills:Array.isArray(e.fills)?Math.max(0,e.fills.filter(e=>!1!==e.visible).length-!!r):null,effectsBakedIntoAsset:"node"===y&&["png","jpg"].includes(t.format)&&null!==i,bakedEffects:"node"===y&&["png","jpg"].includes(t.format)&&i?i.filter(e=>!1!==e.visible):[],instanceOverride:"unknown",instanceOverrideEvidence:"no-component-paint-comparison",sourceProperties:{scaleMode:r?r.scaleMode??null:null,imageTransform:r?r.imageTransform??null:null,rotation:r?r.rotation??null:null,filters:r?r.filters??null:null,opacity:r?r.opacity??null:null,effects:i,blurEffects:i?i.filter(e=>!1!==e.visible&&String(e.type).includes("BLUR")):null,isMask:e.isMask??null,clipsContent:e.clipsContent??null,ancestors:a},unappliedProperties:"fill"===y?["scaleMode","imageTransform","rotation","filters","paintOpacity","nodeGeometry","nodeTransforms","nodeEffectsIncludingBlur","nodeAndAncestorMasks","ancestorClipsAndEffects"]:[],warnings:"fill"===y?["original-fill-properties-unapplied","aspect-ratio-unknown"]:["ancestor-canvas-appearance-unverified","aspect-ratio-unknown"],fidelity:"unverified"}}function b(e,t){let r=0;for(let n=e;n&&r++<32;n=n.parent){if("INSTANCE"===n.type&&Array.isArray(n.overrides)&&n.overrides.find(t=>t.id===e.id&&Array.isArray(t.overriddenFields)&&t.overriddenFields.includes("fills"))){t.instanceOverride=!0,t.instanceOverrideEvidence={kind:"figma-instance-overrides",instanceId:n.id,nodeId:e.id,overriddenFields:["fills"]};return}if(["PAGE","DOCUMENT"].includes(n.type))break}}try{let e;if(!["original-source","fill","node","visible","auto"].includes(h))return p("DEVUP_ASSET_RENDER_MODE_UNSUPPORTED");if(!g&&"fill"===h&&(!String(t.field).startsWith("fills/")||!["png","jpg","webp"].includes(t.format)||1!==t.scale))return p("DEVUP_ASSET_FILL_SETTINGS_UNSUPPORTED","Original fill requires fills/N, png or jpg matching its codec, and scale 1; no resizing or transcoding.");if((g||["fill","original-source"].includes(h))&&"bridge"!==t.transport)return p(g?"DEVUP_ORIGINAL_IMAGE_REQUIRES_BRIDGE":"DEVUP_ASSET_FILL_REQUIRES_BRIDGE");let x=await c(()=>n(t.nodeId));if(f("lookup",Date.now()-a),l=x&&x.type||"unknown",!x||["node","visible"].includes(h)&&"function"!=typeof x.exportAsync)return p("DEVUP_ASSET_UNSUPPORTED_BY_UPSTREAM",u(Error(x?"node has no exportAsync":"node not found")));let E=null;if(g||"string"==typeof t.field&&t.field.startsWith("fills/")){let e=Number(t.field.slice(g?22:6)),r="fills"in x&&Array.isArray(x.fills)?x.fills:[],n=Number.isInteger(e)?r[e]:null,i=n&&"IMAGE"===n.type?n.imageHash||n.imageRef:null;if(!n||i!==t.imageHash)return p("DEVUP_ASSET_SOURCE_CHANGED");E=n}else if("node"!==t.field)return p("DEVUP_ASSET_FIELD_UNSUPPORTED");if("original-source"===h&&(!E||!t.imageHash))return p("DEVUP_ORIGINAL_SOURCE_UNAVAILABLE","No IMAGE fill source exists for this selection. Choose an IMAGE fill or explicitly renderMode node; vector assets should retain SVG.");let A=x;if("visible"===h){let e=t.visibleScopeNodeId;if(!e)return p("DEVUP_VISIBLE_CONTEXT_UNAVAILABLE","Recollect the parent frame and provide visibleScopeNodeId.");let r=new Set;for(;A&&A.id!==e&&!r.has(A)&&r.size<64;)r.add(A),A=A.parent;if(!A||A.id!==e||["PAGE","DOCUMENT"].includes(A.type)||"function"!=typeof A.exportAsync)return p("DEVUP_VISIBLE_SCOPE_UNSUPPORTED","The selected capture scope is not an exportable ancestor of the requested node.")}if(d.renderInfo=m(x,E),"visible"===h){let e=d.renderInfo;e.representation="native-visible",e.renderedAs="visible-scoped-capture",e.captureNodeId=A.id,e.captureBox={width:A.width,height:A.height,aspectRatio:A.width/A.height},e.contextScope="selected-ancestor-subtree",e.captureEffects=Array.isArray(A.effects)?A.effects:null,e.requestedNodeEffects=e.effects,e.effects=e.captureEffects,e.sourceProperties.captureEffects=e.captureEffects,e.visibleContext={requestedNodeId:x.id,captureNodeId:A.id,included:["selected-scope-children","selected-scope-clipping","selected-scope-masks","selected-scope-rounding"],outsideScope:"unknown; ancestors, page backdrop and external layers are not included",canvasReferenceCompared:!1},e.warnings.push("visible-scoped-capture","outside-capture-context-unverified")}b(x,d.renderInfo);let I=null;if(E)try{let r=figma.getImageByHash(t.imageHash),n=r&&await Promise.race([r.getSizeAsync(),new Promise(t=>{e=setTimeout(()=>t(null),250)})]);I=n&&n.width>0&&n.height>0&&Number.isFinite(n.width)&&Number.isFinite(n.height)?{...n,aspectRatio:n.width/n.height}:null}catch(e){}finally{clearTimeout(e)}let S=null;if("auto"===h){let e=function(e,r,n){let i,a=[],o=(e,t)=>{e||a.push(t)},l=e=>Array.isArray(e)&&2===e.length&&e.every(e=>Array.isArray(e)&&3===e.length&&e.every(Number.isFinite))&&1===e[0][0]&&1===e[1][1]&&0===e[0][1]&&0===e[1][0]&&Number.isInteger(e[0][2])&&Number.isInteger(e[1][2]);for(let a of(o("bridge"===t.transport,"raw-requires-bridge"),o(1===t.scale&&["png","jpg"].includes(t.format),"raw-requires-unscaled-raster"),o(!!r&&!0===r.visible&&1===r.opacity&&"NORMAL"===r.blendMode,"paint-compositing-or-unknown"),o(r&&"FILL"===r.scaleMode&&Array.isArray(i=r.imageTransform)&&2===i.length&&i.every((e,t)=>Array.isArray(e)&&3===e.length&&e.every((e,r)=>e===[[1,0,0],[0,1,0]][t][r]))&&0===r.rotation,"paint-transform-or-unknown"),o(r&&r.filters&&Object.values(r.filters).every(e=>0===e),"image-filters-or-unknown"),o(Array.isArray(e.fills)&&1===e.fills.length,"multiple-fills-or-unknown"),o("RECTANGLE"===e.type&&0===e.cornerRadius&&Array.isArray(e.strokes)&&0===e.strokes.length&&(!e.children||0===e.children.length),"node-geometry-or-unknown"),o(Array.isArray(e.effects)&&0===e.effects.length&&!1===e.isMask&&(void 0===e.clipsContent||!1===e.clipsContent)&&1===e.opacity&&!0===e.visible&&"NORMAL"===e.blendMode,"node-effects-mask-compositing-or-unknown"),o(l(e.absoluteTransform)&&l(e.relativeTransform),"node-transform-or-unknown"),o(n.sourceProperties.ancestors.length>0&&n.sourceProperties.ancestors.some(e=>["PAGE","DOCUMENT"].includes(e.type)),"ancestor-chain-incomplete"),o(n.sourceProperties.ancestors.every(e=>!1===e.siblingMaskPresent),"ancestor-sibling-mask-or-unknown"),n.sourceProperties.ancestors.filter(e=>!["PAGE","DOCUMENT"].includes(e.type))))o(!1===a.clipsContent&&!1===a.isMask&&!1===a.siblingMaskPresent&&Array.isArray(a.effects)&&0===a.effects.length&&1===a.opacity&&!0===a.visible&&["NORMAL","PASS_THROUGH"].includes(a.blendMode),"ancestor-dependency-or-unknown");return{eligible:0===a.length,reasons:[...new Set(a)],evidence:"explicit-plugin-properties; intrinsic-dimensions-and-codec-required"}}(x,E,d.renderInfo);if(e.eligible)try{let r=figma.getImageByHash(t.imageHash),n=I;n&&n.width===x.width&&n.height===x.height?(S=await c(()=>r.getBytesAsync()),(137===S[0]&&80===S[1]&&78===S[2]&&71===S[3]?"png":255===S[0]&&216===S[1]&&255===S[2]?"jpg":"unknown")!==t.format&&e.reasons.push("source-codec-mismatch-or-unknown")):e.reasons.push("intrinsic-dimensions-mismatch-or-unknown")}catch(t){e.reasons.push("source-inspection-unavailable")}e.eligible=0===e.reasons.length,y=e.eligible?"fill":"node",d.renderInfo=m(x,E),b(x,d.renderInfo),d.renderInfo.autoDecision={...e,selected:y}}if(d.renderInfo.sourceSize=I,d.renderInfo.sourceSizeEvidence=I?"Figma-image-getSizeAsync":"unknown",g||"fill"===y){d.method="original-fill",o="reading the original image";let e=figma.getImageByHash(t.imageHash);if(!e)return p("DEVUP_ORIGINAL_IMAGE_NOT_FOUND");let n=S||await c(()=>e.getBytesAsync());if(0===n.length||n.length>0x5000000)return p("DEVUP_ASSET_RESPONSE_TOO_LARGE",`${n.length} bytes; one answer carries 1 to 83886080`);let i=e=>e.every((e,t)=>n[t]===e),a=i([137,80,78,71,13,10,26,10])?"image/png":i([255,216,255])?"image/jpeg":i([71,73,70,56])?"image/gif":i([82,73,70,70])&&87===n[8]&&69===n[9]&&66===n[10]&&80===n[11]?"image/webp":null;if(!a)return p("DEVUP_ORIGINAL_IMAGE_CODEC_UNSUPPORTED");if(d.renderInfo.originalMimeType=a,!g&&"fill"===h&&"webp"!==t.format&&a!==({png:"image/png",jpg:"image/jpeg"})[t.format])return p("DEVUP_ASSET_FILL_CODEC_MISMATCH",`Original fill codec is ${a}, but ${t.format} was requested; choose the matching format or renderMode node. No transcoding was performed.`);if(!g)return{...p(null),status:"exported",mimeType:a,byteLength:n.length,sha256:r(n),data:figma.base64Encode(n)};let l=await c(()=>e.getSizeAsync());return{...p(null),status:"exported",kind:"devupOriginalImage",representation:"original-image-v1",format:null,scale:null,mimeType:a,width:l.width,height:l.height,byteLength:n.length,sha256:r(n),data:figma.base64Encode(n)}}let w="webp"===t.format?"PNG":String(t.format||"").toUpperCase();if(!["PNG","JPG","SVG","PDF"].includes(w))return p("DEVUP_ASSET_FORMAT_UNSUPPORTED");let v=Number(t.scale??2);if(!Number.isFinite(v)||v<=0||v>4)return p("DEVUP_ASSET_SCALE_INVALID");let N="SVG"===w,O={format:N?"SVG_STRING":w,contentsOnly:!0,useAbsoluteBounds:!1};("PNG"===w||"JPG"===w)&&(O.constraint={type:"SCALE",value:v}),o="export";let T=Math.max(1,s-(Date.now()-a)),_=N?Math.max(1,T-Math.min(5e3,Math.floor(T/5))):T,P=Date.now(),M=await c(()=>"visible"===h?A.exportAsync(O):E?x.exportAsync(O):i(x,O,_,f));if(E&&f("native",Date.now()-P),"geometry"===d.method&&(d.renderInfo.representation="geometry-fallback",d.renderInfo.renderedAs="geometry-fallback",d.renderInfo.effectsBakedIntoAsset=!1,d.renderInfo.warnings.push("geometry-fallback-not-native-render")),Date.now()-a>=s)throw Error(`DEVUP_ASSET_TIMEOUT: export exceeded ${s} ms asset budget`);o="encode";let C=Date.now(),k=N&&"string"==typeof M?M:null,R=null===k?M instanceof Uint8Array?M:new Uint8Array(M):function(e){let t=[];for(let r=0;r=55296&&n<=56319){let t=r+1=56320&&t<=57343?(n=65536+(n-55296<<10)+(t-56320),r+=1):n=65533}else n>=56320&&n<=57343&&(n=65533);n<128?t.push(n):n<2048?t.push(192|n>>6,128|63&n):n<65536?t.push(224|n>>12,128|n>>6&63,128|63&n):t.push(240|n>>18,128|n>>12&63,128|n>>6&63,128|63&n)}return new Uint8Array(t)}(k);if(0===R.length||R.length>0x5000000)return p("DEVUP_ASSET_RESPONSE_TOO_LARGE",`${w} at scale ${v} is ${R.length} bytes; one answer carries 1 to 83886080`);let D=r(R);f("encode",Date.now()-C);let L=figma.io&&"function"==typeof figma.io.write,$="PNG"===w&&R.length>786432;if(L&&(null!==k&&R.length>12288||$))return{kind:"devupAssetExport",fileKey:figma.fileKey||"",version:t.version,assetId:t.assetId,nodeId:t.nodeId,field:t.field,imageHash:t.imageHash,format:t.format,scale:v,renderMode:h,usageAspectRatio:t.usageAspectRatio??null,maxWidth:t.maxWidth??null,srcset:!0===t.srcset,visibleScopeNodeId:t.visibleScopeNodeId??null,status:"chunked",byteLength:R.length,sha256:D,cursor:{nextOffset:0,maxChunkBytes:12288},errorCode:null,exportDiagnostics:d};return L&&figma.io.write(`devup-asset-${t.assetId.replace(/[^A-Za-z0-9_-]/g,"_")}.${w.toLowerCase()}`,R),{kind:"devupAssetExport",fileKey:figma.fileKey||"",version:t.version,assetId:t.assetId,nodeId:t.nodeId,field:t.field,imageHash:t.imageHash,format:t.format,scale:v,renderMode:h,usageAspectRatio:t.usageAspectRatio??null,maxWidth:t.maxWidth??null,srcset:!0===t.srcset,visibleScopeNodeId:t.visibleScopeNodeId??null,status:"exported",byteLength:R.length,sha256:D,mimeType:L?null===k?null:"image/svg+xml":({PNG:"image/png",JPG:"image/jpeg",SVG:"image/svg+xml",PDF:"application/pdf"})[w],...!L&&null===k?{data:figma.base64Encode(R)}:{},text:k,errorCode:null,exportDiagnostics:d}}catch(e){return p(String(e.message||e).includes("DEVUP_ASSET_TIMEOUT:")?"DEVUP_ASSET_TIMEOUT":g||["fill","original-source"].includes(h)?"DEVUP_ORIGINAL_IMAGE_READ_FAILED":"DEVUP_ASSET_EXPORT_FAILED",u(e))}},bridgeContext:async function(e){let t=figma.currentPage,r=t.selection,n=e=>({id:e.id,name:String(e.name).slice(0,256),type:e.type});return{kind:"devup-bridge-context",currentPage:n(t),selection:r.slice(0,20).map(n),selectionCount:r.length,zoom:Number.isFinite(figma.viewport?.zoom)?figma.viewport.zoom:null,observedAt:Date.now(),lastKnownSelection:globalThis.__devupLastSelection??null,lastKnownPage:globalThis.__devupLastSelectionPage??null}},explore:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=await figma.getNodeByIdAsync(e.nodeId);if(!t)throw Error("DEVUP_NODE_NOT_FOUND");let r=e.explore,n=Math.max(20,Math.min(400,Number.isInteger(r.projectionLimit)?r.projectionLimit:200)),i=Math.max(0,Math.min(500,Number.isInteger(r.textPreviewLimit)?r.textPreviewLimit:160));function a(e){return"string"==typeof e?e.slice(0,240):""}let o="PAGE"===t.type,l=t,s="SECTION"===t.type?t:null;for(;!o&&l.parent&&"PAGE"!==l.parent.type;)l=l.parent,s||"SECTION"!==l.type||(s=l);let d="PAGE"===t.type?t:l.parent;if(!d||"PAGE"!==d.type)throw Error("DEVUP_PAGE_NOT_FOUND");function f(e){let t=e.absoluteBoundingBox||{x:"number"==typeof e.x?e.x:0,y:"number"==typeof e.y?e.y:0,width:"number"==typeof e.width?e.width:0,height:"number"==typeof e.height?e.height:0};return[t.x,t.y,t.width,t.height].every(Number.isFinite)?{x:t.x,y:t.y,width:t.width,height:t.height}:null}function c(e){let t=f(e);return t&&["FRAME","COMPONENT","INSTANCE","COMPONENT_SET"].includes(e.type)&&t.width>=240&&t.width<=1800&&t.height>=300&&t.height<=2e3&&t.width/Math.max(1,t.height)>=.25&&t.width/Math.max(1,t.height)<=2.5}function u(e){let t=[],r=e;for(;r&&"DOCUMENT"!==r.type&&t.length<12;){let e=a(r.name);e&&t.push(e),r=r.parent}return t.reverse()}await figma.setCurrentPageAsync(d);let p=o?d:!c(t)&&s?s:l,g=f(p)||f(t);if(!g)throw Error("DEVUP_NODE_BOUNDS_UNAVAILABLE");let h="children"in d?d.children:[],y=h.map((e,t)=>({node:e,pageChildIndex:t,bounds:f(e)})).filter(e=>e.bounds).filter(e=>{var t;return!!o||e.node.id===p.id||e.bounds.y>=g.y-240&&e.bounds.y<=g.y+12e3&&(t=e.bounds,Math.min(t.x+t.width,g.x+g.width)-Math.max(t.x,g.x)>0)}).sort((e,t)=>e.bounds.y-t.bounds.y||e.bounds.x-t.bounds.x||e.node.id.localeCompare(t.node.id)),m="SECTION"===p.type?y.filter(e=>e.node.id===p.id):y.slice(0,n),b=new Map(m.map(e=>[e.node.id,e]));b.has(l.id)||b.set(l.id,{node:l,pageChildIndex:-1,bounds:f(l)}),b.has(p.id)||b.set(p.id,{node:p,pageChildIndex:-1,bounds:f(p)}),b.has(t.id)||b.set(t.id,{node:t,pageChildIndex:-1,bounds:f(t)});let x=o?null:t.parent;for(;x&&"PAGE"!==x.type&&(b.has(x.id)||b.set(x.id,{node:x,pageChildIndex:-1,bounds:f(x)}),x.id!==p.id);)x=x.parent;if("children"in p)for(let e of p.children.slice(0,n))b.has(e.id)||b.set(e.id,{node:e,pageChildIndex:-1,bounds:f(e)});let E="SECTION"===p.type||o,A=E&&"children"in p&&p.children.length>n;if(E&&"children"in p){let e=8*n,t=p.children.filter(e=>!c(e)).map(e=>({node:e,ancestors:[]})),r=0;for(;t.length&&r!b.has(e.id));if(b.size+t.length>n+2){A=!0;continue}for(let e of t)b.set(e.id,{node:e,pageChildIndex:-1,bounds:f(e)});continue}if("children"in e)for(let r of e.children)t.push({node:r,ancestors:[...i,e]})}A||=t.length>0}let I=[...b.values()].filter(e=>e.bounds).slice(0,n+2).map(({node:e,pageChildIndex:t,bounds:r})=>{let n;return{id:e.id,type:e.type,fields:{name:a(e.name),parentId:e.parent&&"DOCUMENT"!==e.parent.type?e.parent.id:null,childrenIds:[],x:r.x,y:r.y,width:r.width,height:r.height,childCount:"children"in e?e.children.length:0,textPreview:"",...null===(n=function(e){let t=[e],r=0,n=null;for(;t.length&&r++<400;){let e=t.shift();if("visible"in e&&!1===e.visible)continue;let r="children"in e?e.children.filter(e=>!("visible"in e)||!1!==e.visible):[];if(/header|nav|gnb/i.test(e.name||"")&&r.length>=2&&"layoutMode"in e&&"HORIZONTAL"===e.layoutMode){let t=r.map(f);t.every(Boolean)&&(n=Math.max(n||0,t.reduce((e,t)=>e+t.width,0)+(Number(e.itemSpacing)||0)*(t.length-1)))}t.push(...r.slice(0,80))}return n}(e))?{}:{navigationContentWidth:n},pageChildIndex:t>=0?t:null,visible:!("visible"in e)||!1!==e.visible,breadcrumb:u(e)},extra:{},fieldErrors:{}}}),S=I.reduce((e,t)=>{let r=t.fields;return e?{x:Math.min(e.x,r.x),y:Math.min(e.y,r.y),right:Math.max(e.right,r.x+r.width),bottom:Math.max(e.bottom,r.y+r.height)}:{x:r.x,y:r.y,right:r.x+r.width,bottom:r.y+r.height}},null)||{x:0,y:0,right:0,bottom:0},w="SECTION"!==p.type&&y.length>m.length||b.size>n+2||A,v={id:d.id,type:d.type,fields:{name:a(d.name),parentId:null,childrenIds:[],x:S.x,y:S.y,width:S.right-S.x,height:S.bottom-S.y,childCount:h.length,textPreview:"",projectionTruncated:w,visible:!0,breadcrumb:u(d),pageChildIndex:null},extra:{},fieldErrors:{}},N={fileKey:figma.fileKey||"",version:null,rootIds:[d.id],nodes:[v,...I.filter(e=>e.id!==d.id)],diagnostics:[]},O=new Set([d.id,l.id,p.id,t.id]);for(;JSON.stringify(N).length>14e3;){let e=N.nodes.length-1;for(;e>=0&&O.has(N.nodes[e].id);)e-=1;if(e<0)break;N.nodes.splice(e,1),v.fields.projectionTruncated=!0}if(JSON.stringify(N).length>14e3&&(N.nodes=N.nodes.filter(e=>O.has(e.id)).map(e=>({...e,fields:{...e.fields,name:a(e.fields.name).slice(0,80),textPreview:"",breadcrumb:e.fields.breadcrumb.slice(-4).map(e=>e.slice(0,80)),projectionTruncated:e.id===d.id||e.fields.projectionTruncated}}))),JSON.stringify(N).length>14e3)throw Error("DEVUP_EXPLORE_PROJECTION_TOO_LARGE");function T(e){let t=0;for(let r of e){let e=r.codePointAt(0);t+=e<128?1:e<2048?2:e<65536?3:4}return t}if(o){for(;T(JSON.stringify(N))>19456&&N.nodes.length>1;)N.nodes.pop(),v.fields.projectionTruncated=!0;if(T(JSON.stringify(N))>19456)throw Error("DEVUP_EXPLORE_PROJECTION_TOO_LARGE")}let _=14e3-JSON.stringify(N).length;if(o&&(_=Math.min(_,19456-T(JSON.stringify(N)))),i>0&&_>0){let e=N.nodes.filter(e=>b.get(e.id)&&e.id!==d.id),t=e.length;for(let r of e){let e=Math.floor(_/t);if(t-=1,e<=0)continue;let n="",a=0;for(let t of function(e){if(0===i)return"";let t=[],r=[e],n=0;for(;r.length&&n<80&&t.join(" ").lengthe)break;n+=t,a+=r}r.fields.textPreview=n,_-=a}}return N},fastSnapshot:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=e.rootIds;if(!Array.isArray(t)||0===t.length)throw Error("DEVUP_ROOTS_INVALID");let r=await Promise.all(t.map(e=>figma.getNodeByIdAsync(e)));if(r.some(e=>!e))throw Error("DEVUP_NODE_NOT_FOUND");if(1===r.length&&"SECTION"===r[0].type)throw Error("DEVUP_TARGET_IS_SECTION");let n=["mobile","tablet","desktop"],i=e=>{let t=String(e.name||"").trim().toLowerCase(),r=n.indexOf(t);if(r>=0)return r;let i=t.match(/^(\d+)\s*[~–]\s*(\d+)$/);if(!i)return -1;let a=Number(i[1]),o=Number(i[2]);return Number.isSafeInteger(a)&&Number.isSafeInteger(o)&&a>0&&o>=a&&o<=0xffffffff?1:-1};if(1===r.length&&i(r[0])>=0){let e=r[0].parent;if(e&&"SECTION"===e.type&&"children"in e){let t=e.children.filter(e=>e.id===r[0].id||i(e)>=0).filter(e=>!1!==e.visible);t.length>1&&(r.length=0,r.push(...t))}}if(1===t.length){let e=[],t=new Set(r.map(e=>e.id)),n=[],i=e=>{if("reactions"in e&&Array.isArray(e.reactions)){for(let r of e.reactions)if(r&&r.trigger&&"AFTER_TIMEOUT"===r.trigger.type)for(let e of r.actions||[])e&&"NODE"===e.type&&e.transition&&"SMART_ANIMATE"===e.transition.type&&"string"==typeof e.destinationId&&!t.has(e.destinationId)&&(t.add(e.destinationId),n.push(e.destinationId));if("children"in e)for(let t of e.children)i(t)}};for(let e of r)i(e);for(;n.length>0;){let t=n.shift(),r=await figma.getNodeByIdAsync(t);r&&"DOCUMENT"!==r.type&&"PAGE"!==r.type&&(e.push(r),i(r))}r.push(...e)}let a=e.nodeId,o=["absoluteBoundingBox","absoluteRenderBounds","arcData","backgroundStyleId","blendMode","bottomLeftRadius","bottomRightRadius","boundVariables","characters","clipsContent","componentProperties","componentPropertyDefinitions","componentPropertyReferences","constraints","cornerRadius","counterAxisAlignItems","dashPattern","defaultVariant","effectStyleId","effects","fillStyleId","fills","fontName","fontSize","gridColumnAnchorIndex","gridColumnCount","gridColumnGap","gridColumnSizes","gridColumnSpan","gridRowAnchorIndex","gridRowCount","gridRowGap","gridRowSizes","gridRowSpan","gridStyleId","height","inferredAutoLayout","isAsset","isMask","itemSpacing","layoutGrow","layoutMode","layoutWrap","layoutPositioning","layoutSizingHorizontal","layoutSizingVertical","letterSpacing","lineHeight","maxHeight","maxLines","maxWidth","minHeight","minWidth","name","opacity","overflowDirection","paddingBottom","paddingLeft","paddingRight","paddingTop","primaryAxisAlignItems","reactions","rotation","strokeAlign","strokeBottomWeight","strokeLeftWeight","strokeRightWeight","strokeStyleId","strokeTopWeight","strokeWeight","strokes","targetAspectRatio","textAlignHorizontal","textAlignVertical","textAutoResize","textCase","textDecoration","textStyleId","textTruncation","topLeftRadius","topRightRadius","variantProperties","visible","width","x","y"],l=["fontName","fontWeight","fontSize","textDecoration","textCase","lineHeight","letterSpacing","fills","textStyleId","fillStyleId","listOptions","indentation","hyperlink"],s=e.snapshot,d=Math.max(0,Math.floor(Number(s.offset)||0)),f=Math.max(8192,Math.floor(Number(s.maxEnvelopeBytes)||19456)),c=Math.min(s.maxEnvelopeBytes?f-1024:18e3,Math.max(4096,Math.floor(Number(s.maxPayloadBytes)||15e3))),u=new Set(["backgroundStyleId","effectStyleId","fillStyleId","gridStyleId","strokeStyleId","textStyleId"]),p=new Set(["maxWidth","maxHeight","absoluteRenderBounds"]),g=new Map([["rotation",0],["cornerRadius",0],["isAsset",!1],["isMask",!1],["clipsContent",!1],["blendMode","PASS_THROUGH"],["strokeAlign","INSIDE"],["textCase","ORIGINAL"],["textDecoration","NONE"],["textAlignHorizontal","LEFT"],["textAlignVertical","TOP"],["counterAxisAlignItems","MIN"],["primaryAxisAlignItems","MIN"],["gridColumnCount",0],["gridRowCount",0],["gridColumnGap",0],["gridRowGap",0],["gridColumnAnchorIndex",-1],["gridRowAnchorIndex",-1],["gridColumnSpan",1],["gridRowSpan",1]]),h=new Set(["start","end","characters","fontWeight","textStyleId","fillStyleId","listOptions","indentation","hyperlink"]),y=new Set(["parent","children","consumers"]);function m(e,t=!1,r=new WeakSet,n=0){let i;if(null===e||["string","number","boolean"].includes(typeof e))return e;if(void 0===e)return{$undefined:!0};if("bigint"==typeof e)return{$bigint:e.toString()};if(["function","symbol"].includes(typeof e))return{$unsupported:typeof e};if(n>12)return{$truncated:"max-depth"};if("object"==typeof e&&"parent"in e&&"string"==typeof e.id&&"string"==typeof e.type)return{$nodeId:e.id,$nodeType:e.type};if(Array.isArray(e))return e.map(e=>m(e,t,r,n+1));if(ArrayBuffer.isView(e))return{$binary:e.constructor.name,byteLength:e.byteLength};if(e instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:e.byteLength};if(r.has(e))return{$circular:!0};if(r.add(e),t){let t=new Set(Object.keys(e)),r=e;for(;r&&r!==Object.prototype;){for(let e of Object.getOwnPropertyNames(r))t.add(e);r=Object.getPrototypeOf(r)}i=[...t].sort().filter(e=>!e.startsWith("_")&&!y.has(e))}else i=Object.keys(e).sort();let a={};for(let o of i)try{let i=m(e[o],t,r,n+1);i&&"function"===i.$unsupported||(a[o]=i)}catch(e){a[o]=t?{$error:"unavailable"}:{$error:String(e&&e.message?e.message:e)}}return r.delete(e),a}let b=new Map;async function x(e,t,r){if("INSTANCE"!==e.type||"function"!=typeof e.getMainComponentAsync)return;b.has(e.id)||b.set(e.id,(async()=>{try{let t=await e.getMainComponentAsync();return{fields:{mainComponentId:t?t.id:null,mainComponentName:t?t.name:null,mainComponentSetId:t&&t.parent&&"COMPONENT_SET"===t.parent.type?t.parent.id:null}}}catch(e){return{error:String(e&&e.message?e.message:e)}}})());let n=await b.get(e.id);n.error?r.mainComponentId=n.error:Object.assign(t,n.fields)}async function E(e){let t={},r={};e.parent&&(t.parentId=e.parent.id),e.parent&&("PAGE"===e.parent.type||"SECTION"===e.parent.type||"COMPONENT_SET"===e.parent.type)&&(t.parentType=e.parent.type,"SECTION"===e.parent.type&&(t.parentName=e.parent.name));let n="children"in e?e.children.map(e=>e.id):[];for(let i of(n.length>0&&(t.childrenIds=n),o)){let n;if("componentPropertyDefinitions"!==i||"COMPONENT"!==e.type||!e.parent||"COMPONENT_SET"!==e.parent.type)try{if(!(i in e)){"overflowDirection"===i&&["FRAME","COMPONENT","INSTANCE","COMPONENT_SET"].includes(e.type)&&(t[i]=null);continue}if(n=e[i],"function"==typeof n)continue;let r=m(n);if("overflowDirection"===i&&null==n){t[i]=null;continue}(null===r?!p.has(i):Array.isArray(r)?0===r.length:"object"==typeof r?0===Object.keys(r).length:!!(""===r&&u.has(i))||g.has(i)&&g.get(i)===r)||(t[i]=r)}catch(e){r[i]=String(e&&e.message?e.message:e)}}if("TEXT"===e.type&&"function"==typeof e.getStyledTextSegments)try{let r=m(e.getStyledTextSegments(l));if(1===r.length){let e=r[0];for(let t of Object.keys(e))h.has(t)||delete e[t]}r.length>0&&(t.styledTextSegments=r)}catch(e){r.styledTextSegments=String(e&&e.message?e.message:e)}await x(e,t,r);let i={id:e.id,type:e.type,fields:t};return Object.keys(r).length>0&&(i.fieldErrors=r),i}let A=[],I=[...r],S=new Set;for(let e=0;e=A.length&&A.length>0)throw Error("DEVUP_SNAPSHOT_RANGE_INVALID");function w(e){let t=0;for(let r=0;r=55296&&n<=56319&&r+1({id:e,styleType:t})).sort((e,t)=>e.id.localeCompare(t.id));async function a(e){try{let t=await figma.variables.getVariableByIdAsync(e);return t?{kind:"variable",value:m(t,!0),collectionId:t.variableCollectionId,aliasIds:Object.values(t.valuesByMode||{}).filter(e=>e&&"VARIABLE_ALIAS"===e.type&&"string"==typeof e.id&&e.id).map(e=>e.id)}:{kind:"unresolved",value:{id:e,kind:"variable",reason:"notFoundOrUnavailable"}}}catch(t){return{kind:"unresolved",value:{id:e,kind:"variable",reason:"notFoundOrUnavailable",error:String(t&&t.message||t).slice(0,300)}}}}let o=await Promise.all([...n.map(a),...i.map(async({id:e,styleType:t})=>{try{let r=await figma.getStyleByIdAsync(e);if(!r)return{kind:"unresolved",value:{id:e,kind:"style",reason:"notFoundOrUnavailable"}};return{kind:"style",value:{...m(r,!0),styleType:t,value:m("PAINT"===t?r.paints:"EFFECT"===t?r.effects:"GRID"===t?r.layoutGrids:r,!0)}}}catch(t){return{kind:"unresolved",value:{id:e,kind:"style",reason:"notFoundOrUnavailable",error:String(t&&t.message||t).slice(0,300)}}}})]),l=new Set(n),s=o.flatMap(e=>e.aliasIds||[]);for(;s.length>0;){let e=[...new Set(s)].filter(e=>!l.has(e)).sort();for(let t of e)l.add(t);let t=await Promise.all(e.map(a));o.push(...t),s=t.flatMap(e=>e.aliasIds||[])}let d=[...new Set(o.filter(e=>"variable"===e.kind&&e.collectionId).map(e=>e.collectionId))].sort(),f=(await Promise.all(d.map(async e=>{try{let t=await figma.variables.getVariableCollectionByIdAsync(e);return t?m(t,!0):null}catch(e){return null}}))).filter(e=>null!==e),c=o.filter(e=>"variable"===e.kind).map(e=>e.value),u=o.filter(e=>"style"===e.kind).map(e=>e.value),p=o.filter(e=>"unresolved"===e.kind).map(e=>e.value);return{collections:f,variables:c,styles:u,usedRemoteVariables:c.filter(e=>!0===e.remote),usedVariableIds:n,usedStyleIds:i.map(e=>e.id),localComplete:!1,usedRemoteComplete:0===p.length,unresolved:p,$variableRefCount:n.length,$styleRefCount:i.length}}async function N(e){let t=[],r=2;for(let n=d;ne)break;t.push(i),r+=a}return{pageNodes:t,packedBytes:r}}let O=c-1024,T=null;for(let e=0;e<5;e+=1){let{pageNodes:e,packedBytes:t}=await N(O);if(0===e.length)throw Error("DEVUP_SNAPSHOT_RANGE_INVALID");let n=function(e,t){let n=Math.min(A.length,d+e.length),{$variableRefCount:i,$styleRefCount:o,...l}=t,s=[...e,{id:"__DEVUP_SNAPSHOT_CURSOR__",type:"DEVUP_INTERNAL",fields:{offset:d,nextOffset:n,complete:n>=A.length,totalNodes:A.length},extra:{},fieldErrors:{}}],f={kind:"devupFastSnapshotEnvelope",schemaVersion:1,source:{fileKey:figma.fileKey||"",rootId:a},snapshot:{fileKey:figma.fileKey||"",version:null,rootIds:r.map(e=>e.id),nodes:s,diagnostics:[]},resources:l,integrity:{nodeCount:s.length,variableRefCount:i,styleRefCount:o,utf8Bytes:0}},c=0;for(let e=0;e<8&&(c=w(JSON.stringify(f)),f.integrity.utf8Bytes!==c);e+=1)f.integrity.utf8Bytes=c;if(f.integrity.utf8Bytes!==w(JSON.stringify(f)))throw Error("DEVUP_ENVELOPE_LENGTH_UNSTABLE");return{envelope:f,bytes:c}}(e,await v(e));if(n.bytes<=f){T=n;break}if(1===e.length)throw Error("DEVUP_ENVELOPE_TOO_LARGE");O=Math.max(1,Math.min(O-1,t-(n.bytes-f)-256))}if(!T)throw Error("DEVUP_ENVELOPE_TOO_LARGE");return T.envelope},fastTheme:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=Math.max(0,Math.floor(Number(e.theme.offset)||0));function r(e,t=new WeakSet,n=0){if(null===e||["string","number","boolean"].includes(typeof e))return e;if(void 0===e)return{$undefined:!0};if("bigint"==typeof e)return{$bigint:e.toString()};if(["function","symbol"].includes(typeof e))return{$unsupported:typeof e};if(n>12)return{$truncated:"max-depth"};if("object"==typeof e&&"parent"in e&&"string"==typeof e.id&&"string"==typeof e.type)return{$nodeId:e.id,$nodeType:e.type};if(Array.isArray(e))return e.map(e=>r(e,t,n+1));if(ArrayBuffer.isView(e))return{$binary:e.constructor.name,byteLength:e.byteLength};if(e instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:e.byteLength};if(t.has(e))return{$circular:!0};t.add(e);let i={};for(let a of function(e){let t=new Set(Object.keys(e)),r=e;for(;r&&r!==Object.prototype;){for(let e of Object.getOwnPropertyNames(r))t.add(e);r=Object.getPrototypeOf(r)}return[...t].sort()}(e))if(!(a.startsWith("_")||["parent","children","consumers"].includes(a)))try{let o=r(e[a],t,n+1);o&&"function"===o.$unsupported||(i[a]=o)}catch(e){i[a]={$error:"unavailable"}}return t.delete(e),i}let n=new Set,i=new Map;for(let e of figma.root.children)await e.loadAsync();for(let e of[figma.root,...figma.root.findAll(()=>!0)])for(let t of["boundVariables","fills","strokes","effects","layoutGrids","textStyleId","fillStyleId","strokeStyleId","backgroundStyleId","effectStyleId","gridStyleId"])try{!function e(t,r="",a=new WeakSet,o=0){if(!(o>16)&&null!==t&&"object"==typeof t&&!a.has(t)){if(a.add(t),Array.isArray(t)){for(let n of t)e(n,r,a,o+1);return}for(let[l,s]of("VARIABLE_ALIAS"===t.type&&"string"==typeof t.id&&t.id&&"figma.mixed"!==t.id&&"MIXED"!==t.id&&n.add(t.id),Object.entries(t))){let t="textStyleId"===l?"TEXT":["fillStyleId","strokeStyleId","backgroundStyleId"].includes(l)?"PAINT":"effectStyleId"===l?"EFFECT":"gridStyleId"===l?"GRID":null;t&&"string"==typeof s&&s&&"figma.mixed"!==s&&"MIXED"!==s&&!i.has(s)&&i.set(s,t),e(s,l||r,a,o+1)}}}({[t]:e[t]})}catch(e){}let[a,o,l,s,d,f]=await Promise.all([figma.variables.getLocalVariableCollectionsAsync(),figma.variables.getLocalVariablesAsync(),figma.getLocalPaintStylesAsync(),figma.getLocalTextStylesAsync(),figma.getLocalEffectStylesAsync(),figma.getLocalGridStylesAsync()]),c=new Set(o.map(e=>e.id)),u=new Set([...l,...s,...d,...f].map(e=>e.id)),p=[];async function g(e){try{return await figma.variables.getVariableByIdAsync(e)||null}catch(t){return p.push({id:e,kind:"variable",reason:"notFoundOrUnavailable",error:String(t&&t.message||t).slice(0,300)}),null}}function h(e){return Object.values(e.valuesByMode||{}).filter(e=>e&&"VARIABLE_ALIAS"===e.type&&"string"==typeof e.id&&e.id).map(e=>e.id)}let y=[...i.entries()].filter(([e])=>!u.has(e)).sort(([e],[t])=>e.localeCompare(t)).map(async([e,t])=>{try{let r=await figma.getStyleByIdAsync(e);return r?{style:r,styleType:t}:null}catch(t){return p.push({id:e,kind:"style",reason:"notFoundOrUnavailable",error:String(t&&t.message||t).slice(0,300)}),null}}),m=new Set,b=[],x=[...n,...o.flatMap(h)];for(;x.length>0;){let e=[...new Set(x)].filter(e=>!c.has(e)&&!m.has(e)).sort();for(let t of e)m.add(t);let t=(await Promise.all(e.map(g))).filter(Boolean);b.push(...t),x=t.flatMap(h)}let E=(await Promise.all(y)).filter(Boolean);for(let e of m)b.some(t=>t.id===e)||p.some(t=>t.id===e)||p.push({id:e,kind:"variable",reason:"notFoundOrUnavailable"});for(let[e]of[...i.entries()].filter(([e])=>!u.has(e)))E.some(t=>t.style.id===e)||p.some(t=>t.id===e)||p.push({id:e,kind:"style",reason:"notFoundOrUnavailable"});let A=[...new Set(b.map(e=>e.variableCollectionId))].filter(e=>!a.some(t=>t.id===e)).sort(),I=(await Promise.all(A.map(async e=>{try{return await figma.variables.getVariableCollectionByIdAsync(e)}catch(e){return null}}))).filter(Boolean);function S(e,t){return{...r(e),styleType:t,value:r("PAINT"===t?e.paints:"EFFECT"===t?e.effects:"GRID"===t?e.layoutGrids:e)}}let w=[...l.map(e=>S(e,"PAINT")),...s.map(e=>S(e,"TEXT")),...d.map(e=>S(e,"EFFECT")),...f.map(e=>S(e,"GRID")),...E.map(({style:e,styleType:t})=>S(e,t))].sort((e,t)=>e.id.localeCompare(t.id)),v=[...o,...b].map(e=>r(e)).sort((e,t)=>e.id.localeCompare(t.id)),N=[...a,...I].map(e=>r(e)).sort((e,t)=>e.id.localeCompare(t.id));function O(e){let t=[];for(let r=0;r=55296&&n<=56319){let t=r+1=56320&&t<=57343?(n=65536+(n-55296<<10)+(t-56320),r+=1):n=65533}else n>=56320&&n<=57343&&(n=65533);n<128?t.push(n):n<2048?t.push(192|n>>6,128|63&n):n<65536?t.push(224|n>>12,128|n>>6&63,128|63&n):t.push(240|n>>18,128|n>>12&63,128|n>>6&63,128|63&n)}return new Uint8Array(t)}p.sort((e,t)=>e.kind.localeCompare(t.kind)||e.id.localeCompare(t.id));let T=[...N.map(e=>({kind:"collection",value:e})),...v.map(e=>({kind:"variable",value:e})),...w.map(e=>({kind:"style",value:e})),...[...n].sort().map(e=>({kind:"usedVariableId",value:e})),...[...i.keys()].sort().map(e=>({kind:"usedStyleId",value:e})),...p.map(e=>({kind:"unresolved",value:e}))];if(t>T.length)throw Error("DEVUP_SNAPSHOT_RANGE_INVALID");let _=T.map(e=>O(JSON.stringify(e.value)).length+1),P=18432,M=null;for(let e=0;e<6;e+=1){let{pageItems:e,packed:r}=function(e){let r=[],n=0;for(let i=t;i0)||!(n+_[i]>e));i+=1)r.push(T[i]),n+=_[i];return{pageItems:r,packed:n}}(P),n=function(e,r){let n=t=>e.filter(e=>e.kind===t).map(e=>e.value),i=n("collection"),a=n("variable"),o=n("style"),l=n("unresolved"),s={kind:"devupFastThemeEnvelope",schemaVersion:1,source:{fileKey:figma.fileKey||"",version:null},resources:{collections:i,variables:a,styles:o,usedRemoteVariables:[],usedVariableIds:n("usedVariableId"),usedStyleIds:n("usedStyleId"),localComplete:!0,usedRemoteComplete:0===p.length,unresolved:l},page:{offset:t,nextOffset:r,complete:r>=T.length,totalItems:T.length},integrity:{collectionCount:i.length,variableCount:a.length,styleCount:o.length,unresolvedCount:l.length,utf8Bytes:0}},d=new Uint8Array;for(let e=0;e<8&&(d=O(JSON.stringify(s)),s.integrity.utf8Bytes!==d.length);e+=1)s.integrity.utf8Bytes=d.length;if(d=O(JSON.stringify(s)),s.integrity.utf8Bytes!==d.length)throw Error("DEVUP_ENVELOPE_LENGTH_UNSTABLE");return{envelope:s,bytes:d.length}}(e,t+e.length);if(n.bytes<=19456){M=n;break}if(e.length<=1)break;P=Math.max(1,Math.min(P-1,r-(n.bytes-19456)-256))}if(null===M||M.bytes>8388608)throw Error("DEVUP_ENVELOPE_TOO_LARGE");return M.envelope},largeValue:async function(e){let t;figma.skipInvisibleInstanceChildren=!1;let r=e.largeValue;async function n(e){try{let t=await figma.getNodeByIdAsync(e);if(t)return t}catch(r){if(!e.startsWith("I")||!e.includes(";"))throw r;try{return await t()}catch(e){throw Error(`${r.message||r}; nested lookup fallback: ${e.message||e}`)}}if(!e.startsWith("I")||!e.includes(";"))return null;return t();async function t(){let t=await figma.getNodeByIdAsync(e.slice(1).split(";")[0]),r=t?[t]:[],n=new Set;for(;r.length;){let t=r.pop();if(t.id===e)return t;if(!n.has(t.id)){if(n.add(t.id),n.size>1e4)throw Error("nested lookup exceeds 10000 nodes");Array.isArray(t.children)&&r.push(...t.children)}}throw Error(`nested node ${e} not found in its instance`)}}async function i(e,t,r=85e3,n=()=>{}){let a,o=Date.now();try{let i=await Promise.race([e.exportAsync(t),new Promise((e,t)=>{a=setTimeout(()=>t(Error(`DEVUP_ASSET_TIMEOUT: native export exceeded ${r} ms`)),r)})]);return n("native-export",Date.now()-o),i}catch(i){if(n("native-export",Date.now()-o,String(i.message||i).slice(0,500)),"SVG_STRING"!==t.format)throw i;let r=Date.now();try{let t=function(e){let t=e=>"number"==typeof e&&Number.isFinite(e),r=(e,t)=>{throw Error(`geometry fallback unsupported: ${e.type||"unknown"} ${e.id||""}: ${t}`)},n=0,i=0;function a(e,i,a){(void 0!==a&&!Array.isArray(a)||void 0===a&&Array.isArray(i)&&i.length)&&r(e,"unresolved paint requires native export");let o=Array.isArray(a)?a.filter(e=>!1!==e.visible):[];return o.length?(Array.isArray(i)&&i.length||r(e,"paint has no rendered geometry"),o.map(a=>{("SOLID"!==a.type||a.blendMode&&"NORMAL"!==a.blendMode)&&r(e,"paint type or blending requires native export");let o=a.color;o&&[o.r,o.g,o.b].every(e=>t(e)&&e>=0&&e<=1)||r(e,"invalid solid color");let l=void 0===a.opacity?1:a.opacity;return(!t(l)||l<0||l>1)&&r(e,"invalid paint opacity"),["NONZERO","EVENODD"].map(t=>{let a=i.filter(e=>e.windingRule===t);if(!a.length)return"";for(let t of a)"string"==typeof t.data&&t.data.trim()||r(e,"empty path"),n++;return`"']/g,e=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[e])}" fill="rgb(${100*o.r}%,${100*o.g}%,${100*o.b}%)" fill-opacity="${l}" fill-rule="${"EVENODD"===t?"evenodd":"nonzero"}"/>`}).join("")}).join("")):""}let o=e.absoluteRenderBounds,l=e.absoluteTransform;(!o||![o.x,o.y,o.width,o.height].every(t)||o.width<=0||o.height<=0)&&r(e,"missing render bounds"),(!Array.isArray(l)||2!==l.length||!l.every(e=>Array.isArray(e)&&3===e.length&&e.every(t))||0!==l[0][1]||0!==l[1][0]||l[0][0]<=0||l[1][1]<=0)&&r(e,"root rotation, reflection or shear requires native export");let s=(o.x-l[0][2])/l[0][0],d=(o.y-l[1][2])/l[1][1],f=o.width/l[0][0],c=o.height/l[1][1],u=function e(n,o){if(++i>1e4&&r(n,"subtree exceeds 10000 nodes"),n.isMask&&r(n,"mask requires native export"),!1===n.visible||0===n.opacity)return"";n.clipsContent&&r(n,"clipping requires native export"),Array.isArray(n.effects)&&n.effects.some(e=>!1!==e.visible)&&r(n,"effect requires native export"),n.blendMode&&!["NORMAL","PASS_THROUGH"].includes(n.blendMode)&&r(n,"layer blending requires native export");let l=void 0===n.opacity?1:n.opacity;(!t(l)||l<0||l>1)&&r(n,"invalid layer opacity");let s="";if(!o){let e=n.relativeTransform;Array.isArray(e)&&2===e.length&&e.every(e=>Array.isArray(e)&&3===e.length&&e.every(t))||r(n,"missing relative transform"),s=` transform="matrix(${e[0][0]} ${e[1][0]} ${e[0][1]} ${e[1][1]} ${e[0][2]} ${e[1][2]})"`}let d="";if(["GROUP","FRAME","INSTANCE","COMPONENT"].includes(n.type))(Array.isArray(n.fills)&&n.fills.some(e=>!1!==e.visible)||Array.isArray(n.strokes)&&n.strokes.some(e=>!1!==e.visible))&&r(n,"container paint requires native export"),Array.isArray(n.children)||r(n,"missing children"),d=(!0===n.itemReverseZIndex?[...n.children].reverse():n.children).map(t=>e(t,!1)).join("");else{for(let e of(["VECTOR","BOOLEAN_OPERATION","RECTANGLE","ELLIPSE","POLYGON","STAR","LINE"].includes(n.type)||r(n,"node type requires native export"),Array.isArray(n.strokes)&&n.strokes.some(e=>!1!==e.visible)&&"CENTER"!==n.strokeAlign&&r(n,"non-center stroke alignment requires native export"),Array.isArray(n.dashPattern)&&n.dashPattern.length&&r(n,"dashed stroke requires native export"),[n.fillGeometry,n.strokeGeometry]))void 0!==e&&(!Array.isArray(e)||e.some(e=>!["NONZERO","EVENODD"].includes(e.windingRule)))&&r(n,"unknown winding rule");d=a(n,n.fillGeometry,n.fills)+a(n,n.strokeGeometry,n.strokes)}return`${d}`}(e,!0);return n||r(e,"no painted geometry"),`${u}`}(e);return n("geometry-fallback",Date.now()-r),t}catch(e){throw Error(`native export: ${i.message||i}; ${e.message||e}`)}}finally{clearTimeout(a)}}let a=await n(r.nodeId);if(!a)throw Error("DEVUP_NODE_NOT_FOUND");function o(e){let t=[];for(let r=0;r=55296&&n<=56319){let t=r+1=56320&&t<=57343?(n=65536+(n-55296<<10)+(t-56320),r+=1):n=65533}else n>=56320&&n<=57343&&(n=65533);n<128?t.push(n):n<2048?t.push(192|n>>6,128|63&n):n<65536?t.push(224|n>>12,128|n>>6&63,128|63&n):t.push(240|n>>18,128|n>>12&63,128|n>>6&63,128|63&n)}return new Uint8Array(t)}let l=null;try{if("$export:svg"===r.field){if("function"!=typeof a.exportAsync)throw Error("unsupported");let e=await i(a,{format:"SVG_STRING"},Math.max(1,(Number(r.timeoutMs)||9e4)-5e3));if("string"!=typeof e)throw Error("unsupported");l=o(e)}else if("string"==typeof r.field&&r.field.startsWith("$export:png")){if("function"!=typeof a.exportAsync)throw Error("unsupported");let e=Number(r.field.split("@")[1]||2);if(!Number.isFinite(e)||e<=0||e>4)throw Error("invalid scale");let t=r.field.split("@"),n=a;if("visible"===t[2]){let e=new Set;for(;n&&n.id!==t[3]&&!e.has(n)&&e.size<64;)e.add(n),n=n.parent;if(!n||n.id!==t[3]||["PAGE","DOCUMENT"].includes(n.type))throw Error("invalid visible scope")}else if(t.length>2)throw Error("invalid export field");let i=await n.exportAsync({format:"PNG",contentsOnly:!0,useAbsoluteBounds:!1,constraint:{type:"SCALE",value:e}});l=i instanceof Uint8Array?i:new Uint8Array(i)}else if("styledTextSegments"===r.field&&"TEXT"===a.type&&"function"==typeof a.getStyledTextSegments)t=a.getStyledTextSegments(["fontName","fontWeight","fontSize","textDecoration","textCase","lineHeight","letterSpacing","fills","textStyleId","fillStyleId","listOptions","indentation","hyperlink"]);else if(r.field in a)t=a[r.field];else throw Error("unsupported")}catch(e){return{kind:"devupLargeValueUnsupported",fileKey:figma.fileKey||"",version:r.version,nodeId:r.nodeId,field:r.field,byteLength:r.byteLength,sha256:r.sha256,errorCode:"DEVUP_FIELD_UNSUPPORTED_BY_UPSTREAM",errorDetail:`${r.field} failed: ${e.message||e}`.slice(0,500)}}let s=null===l?o(JSON.stringify(function e(t,r=new WeakSet,n=0){if(null===t||["string","number","boolean"].includes(typeof t))return t;if(void 0===t)return{$undefined:!0};if("bigint"==typeof t)return{$bigint:t.toString()};if(["function","symbol"].includes(typeof t))return{$unsupported:typeof t};if(n>12)return{$truncated:"max-depth"};if("object"==typeof t&&"parent"in t&&"string"==typeof t.id&&"string"==typeof t.type)return{$nodeId:t.id,$nodeType:t.type};if(Array.isArray(t))return t.map(t=>e(t,r,n+1));if(ArrayBuffer.isView(t))return{$binary:t.constructor.name,byteLength:t.byteLength};if(t instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:t.byteLength};if(r.has(t))return{$circular:!0};r.add(t);let i={};for(let a of Object.keys(t).sort())try{let o=e(t[a],r,n+1);o&&"function"===o.$unsupported||(i[a]=o)}catch(e){i[a]={$error:String(e&&e.message?e.message:e)}}return r.delete(t),i}(t))):l,d=function(e){let t=[0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,0xe49b69c1,0xefbe4786,0xfc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x6ca6351,0x14292967,0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2],r=e.length,n=64*Math.ceil((r+9)/64),i=new Uint8Array(n);i.set(e),i[r]=128;let a=8*r;for(let e=0;e<8;e+=1)i[n-1-e]=255&Math.floor(a/2**(8*e));let o=[0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19],l=(e,t)=>e>>>t|e<<32-t;for(let e=0;e>>3,n=l(r[e-2],17)^l(r[e-2],19)^r[e-2]>>>10;r[e]=r[e-16]+t+r[e-7]+n>>>0}let[n,a,s,d,f,c,u,p]=o;for(let e=0;e<64;e+=1){let i=p+(l(f,6)^l(f,11)^l(f,25))+(f&c^~f&u)+t[e]+r[e]>>>0,o=(l(n,2)^l(n,13)^l(n,22))+(n&a^n&s^a&s)>>>0;p=u,u=c,c=f,f=d+i>>>0,d=s,s=a,a=n,n=i+o>>>0}for(let[e,t]of[n,a,s,d,f,c,u,p].entries())o[e]=o[e]+t>>>0}return o.map(e=>e.toString(16).padStart(8,"0")).join("")}(s);if(s.length!==r.byteLength||d!==r.sha256)throw Error("DEVUP_LARGE_VALUE_CHANGED");let f=Math.max(0,Math.floor(Number(r.offset)||0)),c=Math.min(65536,Math.max(1,Math.floor(Number(r.maxChunkBytes)||8192)));if(f>=s.length)throw Error("DEVUP_LARGE_VALUE_RANGE_INVALID");let u=Math.min(s.length,f+c);return{kind:"devupLargeValueFragment",fileKey:figma.fileKey||"",version:r.version,nodeId:r.nodeId,field:r.field,offset:f,nextOffset:u,byteLength:s.length,sha256:d,dataBase64:function(e){let t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",r="";for(let n=0;n>2],r+=t[(3&i)<<4|a>>4],r+=n+1>6]:"=",r+=n+210||t.some(e=>"string"!=typeof e||e.length>128||!/^(?:[0-9]+:[0-9]+|I[0-9]+:[0-9]+(?:;[0-9]+:[0-9]+)+)$/.test(e))||new Set(t).size!==t.length)throw Error("DEVUP_INVALID_INPUT: nodeIds must be 1..10 distinct Figma IDs");let r=(e,t)=>{try{let r=e[t];return"number"==typeof r&&Number.isFinite(r)?r:null}catch{return null}},n=(e,t)=>{try{let r=e[t];return"string"==typeof r&&r.length<=32&&/^[A-Z_]+$/.test(r)?r:null}catch{return null}},i=[];for(let e of t)try{let t=await figma.getNodeByIdAsync(e);if(!t||"DOCUMENT"===t.type||"PAGE"===t.type){i.push({nodeId:e,status:"unavailable",reason:t?"not-scene-node":"not-found"});continue}let a={width:r(t,"width"),height:r(t,"height"),padding:{top:r(t,"paddingTop"),right:r(t,"paddingRight"),bottom:r(t,"paddingBottom"),left:r(t,"paddingLeft")},gap:r(t,"itemSpacing"),layoutMode:n(t,"layoutMode"),align:{primary:n(t,"primaryAxisAlignItems"),counter:n(t,"counterAxisAlignItems")}},o=null!==a.width&&null!==a.height;i.push({nodeId:e,status:o?"measured":"partial",...a})}catch{i.push({nodeId:e,status:"unavailable",reason:"read-failed"})}return{kind:"devup-measure",schemaVersion:1,coordinateSpace:"figma-logical",nodes:i,partial:i.some(e=>"measured"!==e.status),evidenceLimit:"Null means unavailable or not applicable; no screenshot dimensions, descendants, theme, assets or code were read."}},metadata:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=await figma.getNodeByIdAsync(e.nodeId);if(!t)throw Error("DEVUP_NODE_NOT_FOUND");let r=t;for(;r&&"PAGE"!==r.type;)r=r.parent;r&&"PAGE"===r.type&&await figma.setCurrentPageAsync(r);let n=[];return!function(e){let t=[{node:e,visited:!1}],r=new Map;for(;t.length>0;){let e=t[t.length-1],i=e.node,a="children"in i?i.children:[];if(!e.visited){e.visited=!0;for(let e=a.length-1;e>=0;e-=1)t.push({node:a[e],visited:!1});continue}t.pop();let o=0,l=[];for(let e of a)l.push(e.id),o+=1+(r.get(e.id)||0);r.set(i.id,o),n.push({id:i.id,type:i.type,name:i.name,childrenIds:l,descendantCount:o})}}(t),{fileKey:figma.fileKey||"",version:null,rootId:t.id,nodes:n}},pageCatalog:async function(e){let t=figma.root.children;return{fileKey:figma.fileKey||"",version:null,rootIds:t.map(e=>e.id),nodes:t.map(e=>({id:e.id,type:e.type,fields:{name:e.name,parentId:null,childrenIds:[]},extra:{},fieldErrors:{}})),diagnostics:[]}},referencePng:async function(e){let t,r=e.nodeId;globalThis.__devupReferencePngAnswers||(globalThis.__devupReferencePngAnswers=new Map);let n=globalThis.__devupReferencePngAnswers,i=0;for(let[e,t]of n)Date.now()-t.at>12e4?n.delete(e):i+=t.text.length;if(i>=0xc000000)throw Error(`DEVUP_REFERENCE_PNG_BUSY ${i} characters are held for other reads`);let a=await figma.getNodeByIdAsync(r);if(!a||"function"!=typeof a.exportAsync)throw Error(`DEVUP_NODE_NOT_FOUND ${r}`);let o=a.absoluteRenderBounds||a.absoluteBoundingBox,l=Math.max(o.width,o.height),s=l>8192?8192/l:1,d=(e,t)=>({format:"PNG",constraint:t&&s<1?{type:o.width>=o.height?"WIDTH":"HEIGHT",value:8192}:{type:"SCALE",value:s},contentsOnly:e,useAbsoluteBounds:!1,colorProfile:"SRGB"}),f=e=>(Array.isArray(e)?e:[]).filter(e=>e&&!1!==e.visible),c=(e,t)=>!!e&&!!t&&e.x!!e&&e.x+t<=o.x&&e.y+t<=o.y&&e.x+e.width-t>=o.x+o.width&&e.y+e.height-t>=o.y+o.height,p=e=>`${e.type} ${JSON.stringify(e.name)}`,g=e=>JSON.parse(JSON.stringify(e===figma.mixed?null:e)),h=e=>(void 0===e.opacity||1===e.opacity)&&(!e.blendMode||["PASS_THROUGH","NORMAL"].includes(e.blendMode)),y=[],m=a.parent;for(;m&&"PAGE"!==m.type;)y.unshift(m),m=m.parent;let b=[255,255,255];for(let e of f(m&&m.backgrounds)){if("SOLID"!==e.type)continue;let t=void 0===e.opacity?1:e.opacity;["r","g","b"].forEach((r,n)=>{b[n]=t*e.color[r]*255+(1-t)*b[n]})}let x=0,E=e=>!1!==e.visible&&!e.isMask&&!!c(e.absoluteRenderBounds,o)&&((x+=1)>5e3||"TEXT"===e.type||!!f(e.fills).length||!!f(e.strokes).length||!!f(e.effects).length||"children"in e&&e.children.some(E)),A=e=>h(e)&&u(e.absoluteBoundingBox,0)&&1e-9>Math.abs(e.absoluteTransform[0][1])&&1e-9>Math.abs(e.absoluteTransform[1][0])&&0===Math.max(0,...[e.topLeftRadius,e.topRightRadius,e.bottomRightRadius,e.bottomLeftRadius].filter(e=>"number"==typeof e))&&f(e.fills).some(e=>"NORMAL"===(e.blendMode||"NORMAL")&&(void 0===e.opacity||1===e.opacity)&&("SOLID"===e.type||e.type.startsWith("GRADIENT_")&&e.gradientStops.every(e=>1===e.color.a))),I=[...y,a],S=[],w=[],v=new Map;for(let e=0;eA(e)&&i.slice(0,t).every(h));r.forEach((e,r)=>{e===t||(e.isMask&&!1!==e.visible&&r{let t=await a.exportAsync(d(!1,!0));return{kind:"devupReferencePng",nodeId:r,mimeType:"image/png",byteLength:t.length,data:figma.base64Encode(t),backdrop:N,...e}},T=(e,t)=>{let r=[...new Set(e)];return{approximate:r.join("; "),note:`Drawn as the canvas shows this area: devup-mcp cannot yet compose what get_screenshot draws here (${r.slice(0,3).join("; ")}). Layers reaching into it may show here: ${t}; get_screenshot leaves other layers out.`}},_=e=>{let t=JSON.stringify(e).replace(/[\u0080-\uffff]/g,e=>`\\u${e.charCodeAt(0).toString(16).padStart(4,"0")}`);if(t.length>0x5a00000){if(e.compose){let{compose:r,reaching:n,...i}=e;return _({...i,...T([`the answer would be ${t.length} characters`],n)})}throw Error(`DEVUP_REFERENCE_PNG_TOO_LARGE ${t.length} characters, over the 94371840 one answer is read back up to`)}if(t.length<=6291456)return e;let i=`${r}@${Date.now()}.${Math.random().toString(36).slice(2,10)}`;return n.set(i,{text:t,at:Date.now()}),{kind:"devupReferencePngChunked",nodeId:r,id:i,length:t.length}};if(0===S.length)return _(await O({}));let P=I.some(e=>e.isMask),M=[],C=new Set,k=(e,t)=>{let r=p(e);for(let t of f(e.fills))"IMAGE"===t.type?((t.rotation||f([t.filters]).some(e=>Object.values(e).some(e=>e)))&&M.push(`${r} has an image fill with a rotation or filters`),C.add(t.imageHash)):"SOLID"===t.type||t.type.startsWith("GRADIENT_")||M.push(`${r} is painted with ${t.type}`);for(let t of f(e.effects))["LAYER_BLUR","DROP_SHADOW","INNER_SHADOW","BACKGROUND_BLUR"].includes(t.type)||M.push(`${r} has a ${t.type}`);let n=f(e.strokes);if((e=>{let t=0===f(e.strokes).length?[]:[e.strokeWeight,e.strokeTopWeight,e.strokeRightWeight,e.strokeBottomWeight,e.strokeLeftWeight].filter(e=>"number"==typeof e&&e>0);if(0===t.length)return!1;let r=Math.max(...t),n="OUTSIDE"===e.strokeAlign?0:"CENTER"===e.strokeAlign?r/2:r,i="INSIDE"===e.strokeAlign?0:"CENTER"===e.strokeAlign?r/2:r,a=e.absoluteBoundingBox;return c(a&&{x:a.x-i,y:a.y-i,width:a.width+2*i,height:a.height+2*i},o)&&!u(a,n)})(e)){let t=["FRAME","RECTANGLE","COMPONENT","COMPONENT_SET","INSTANCE","SECTION"].includes(e.type),i=n.every(e=>"SOLID"===e.type||e.type.startsWith("GRADIENT_")),a=Array.isArray(e.dashPattern)&&e.dashPattern.length>0,o="INSIDE"===e.strokeAlign||!e.strokeJoin||"MITER"===e.strokeJoin;t&&i&&!a&&o||M.push(`${r}'s stroke reaches it`)}return{transform:g(e.absoluteTransform),width:e.width,height:e.height,radii:[e.topLeftRadius,e.topRightRadius,e.bottomRightRadius,e.bottomLeftRadius].map(e=>"number"==typeof e?e:0),fills:g(f(e.fills)),opacity:void 0===e.opacity?1:e.opacity,blendMode:e.blendMode||null,clips:!!e.clipsContent,effects:g(f(e.effects)),strokes:g(n),strokeAlign:e.strokeAlign||null,strokeWeights:["strokeTopWeight","strokeRightWeight","strokeBottomWeight","strokeLeftWeight"].map(t=>"number"==typeof e[t]?e[t]:"number"==typeof e.strokeWeight?e.strokeWeight:0)}},R=async e=>{let t=await e.exportAsync(d(!0,e===a));return{data:figma.base64Encode(t),area:e.absoluteRenderBounds||e.absoluteBoundingBox,blendMode:e.blendMode||null,size:[16,20].map(e=>(t[e]<<24|t[e+1]<<16|t[e+2]<<8|t[e+3])>>>0)}},D=async e=>{let t=v.get(e);if(!t)return null;let r=await t.exportAsync({...d(!0,!1),useAbsoluteBounds:!0});return{data:figma.base64Encode(r),area:t.absoluteBoundingBox,luminance:"LUMINANCE"===t.maskType}},L=[],$={},U={};try{for(let e of y){let t=k(e,!1),r=await D(e);r&&(t.mask=r),L.push(t)}let e=e=>f(e.effects).some(e=>"BACKGROUND_BLUR"===e.type);if(w.length>0&&(e(a)||"findOne"in a&&a.findOne(e))){let r="children"in a&&a.children.some(t=>e(t)||"findOne"in t&&!!t.findOne(e));if("children"in a&&!r&&!f(a.strokes).length){for(let e of($.own=k(a,!0),$.children=[],a.children)){if(!1===e.visible||e.isMask||!e.absoluteRenderBounds)continue;let t=await R(e);$.children.push({data:t.data,area:t.area,blendMode:t.blendMode})}t=[Math.ceil(o.width*s-1e-6),Math.ceil(o.height*s-1e-6)]}else M.push(`${p(a)} blurs what lies behind it`)}else if(P)t=[Math.ceil(o.width*s-1e-6),Math.ceil(o.height*s-1e-6)];else{let e=await R(a);$.picture={data:e.data,area:e.area,blendMode:e.blendMode},t=e.size}let r=await D(a);r&&($.mask=r),t&&t[0]*t[1]>8e6&&M.push(`the area is ${t[0]}x${t[1]} pixels`);let n=globalThis.__devupNormalizeImage,i=Date.now()+2e4,l=0;for(let e of C){let t=e&&figma.getImageByHash(e),r=t&&await t.getBytesAsync();if(!r){M.push(`the image ${e} could not be read`);continue}let a=null;if("function"==typeof n){let{width:e,height:o}=await t.getSizeAsync(),l=(...e)=>e.every((e,t)=>r[t]===e),s=l(137,80,78,71)||l(255,216)||l(71,73,70),d=Math.min(1,8192/Math.max(e,o,1)),f=Math.ceil(e*d)*Math.ceil(o*d),c=5*f+65536,u=5*r.length+4*e*o*(d<1?2:1)+4*f+4*c+1048576,p=i-Date.now();if((!s||Math.max(e,o)>8192||r.length>4*e*o+1048576)&&u<=0x20000000&&p>0){let t=await n(r,8192,p,e,o);t&&(r=t,a={width:e,height:o})}}if(l+r.length>0x4000000){M.push("its ancestors' images are over 67108864 bytes");break}l+=r.length,U[e]=a?{data:figma.base64Encode(r),...a}:figma.base64Encode(r)}}catch(e){M.push(`a part could not be exported: ${e instanceof Error?e.message:String(e)}`)}let B=`${S.slice(0,5).join(", ")}${S.length>5?` and ${S.length-5} more`:""}`;return M.length>0?_(await O(T(M,B))):_(await O({reaching:B,compose:{area:o,scale:s,width:t[0],height:t[1],page:b,layers:L,node:$,images:U}}))},referencePngChunk:async function(e){let t=e.largeValue,r=globalThis.__devupReferencePngAnswers,n=r&&r.get(t.id);if(!n||"string"!=typeof n.text)throw Error(`DEVUP_REFERENCE_PNG_ANSWER_GONE ${t.id}`);let i=n.text,a=Math.max(0,Math.floor(Number(t.offset)||0)),o=Math.min(i.length,a+4194304);return o>=i.length&&r.delete(t.id),{kind:"devupReferencePngChunk",id:t.id,offset:a,nextOffset:o,complete:o>=i.length,text:i.slice(a,o)}},search:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=await figma.getNodeByIdAsync(e.nodeId);if(!t)throw Error("DEVUP_NODE_NOT_FOUND");let r=t;for(;r&&"PAGE"!==r.type;)r=r.parent;if(!r||"PAGE"!==r.type)throw Error("DEVUP_PAGE_NOT_FOUND");await figma.setCurrentPageAsync(r);let n=e.search,i=new Set((n.nodeTypes.length?n.nodeTypes:["PAGE","SECTION","FRAME","COMPONENT_SET","COMPONENT"]).map(e=>e.toUpperCase()));function a(e){return e.normalize("NFC").toLocaleLowerCase().replace(/\s/gu,"")}let o=t===r?r.findAll(()=>!0):"findAll"in t?t.findAll(()=>!0):[],l=[t,...o].filter(e=>i.has(e.type)&&"string"==typeof e.name).map(e=>({node:e,score:function(e){let t=n.query.trim();if(""===t||"*"===t)return 100;if(e===n.query)return 400;if("exact"===n.matchKind)return null;let r=a(e),i=a(n.query);if(r===i)return 300;if(r.startsWith(i))return 200;if(r.includes(i))return 100;if("fuzzy"!==n.matchKind)return null;let o=function(e,t){let r=Array.from({length:t.length+1},(e,t)=>t);for(let n=0;nnull!==e.score).sort((e,t)=>t.score-e.score||e.node.name.localeCompare(t.node.name)||e.node.id.localeCompare(t.node.id)).slice(0,n.limit),s=new Map([[r.id,r]]);for(let e of[t,...l.map(e=>e.node)]){let t=e;for(;t&&"DOCUMENT"!==t.type;)s.set(t.id,t),t=t.parent}return{fileKey:figma.fileKey||"",version:null,rootIds:[r.id],nodes:[...s.values()].map(e=>({id:e.id,type:e.type,fields:{name:e.name,parentId:e.parent&&"DOCUMENT"!==e.parent.type?e.parent.id:null,childrenIds:"children"in e?e.children.map(e=>e.id):[]},extra:{},fieldErrors:{}})),diagnostics:[]}},sectionIndex:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=await figma.getNodeByIdAsync(e.nodeId);if(!t)throw Error("DEVUP_NODE_NOT_FOUND");if("SECTION"!==t.type){let e=t.parent,r=new Set;for(;e&&"SECTION"!==e.type&&!r.has(e.id);)r.add(e.id),e=e.parent;let n=e&&"SECTION"===e.type?e.id:null,i=n&&figma.fileKey?`https://www.figma.com/design/${figma.fileKey}?node-id=${n.replace(/:/g,"-")}`:null;throw Error("DEVUP_SECTION_REQUIRED "+JSON.stringify({pluginCode:"DEVUP_SECTION_REQUIRED",stage:"section-index",nodeId:t.id,nodeType:t.type,sectionId:n,nextAction:{tool:"devup_figma_export",how:n?`The url must point to a SECTION. This capture's SECTION is ${n}; select frames with frameIds.`:"The url must point to a SECTION. This node has no ancestor SECTION; choose the intended SECTION in Figma and copy its link.",arguments:i?"FRAME"===t.type&&t.parent===e?{url:i,frameIds:[t.id]}:{url:i}:null,requiredArguments:i?[]:["url"]}}))}let r=2048;function n(e){let t=e.absoluteBoundingBox||{x:"number"==typeof e.x?e.x:0,y:"number"==typeof e.y?e.y:0,width:"number"==typeof e.width?e.width:0,height:"number"==typeof e.height?e.height:0};return[t.x,t.y,t.width,t.height].every(Number.isFinite)?{x:t.x,y:t.y,width:t.width,height:t.height}:null}function i(e,t){if("FRAME"!==e.type||!1===e.visible||!t)return!1;let r=t.width/Math.max(1,t.height);return t.width>=240&&t.width<=1800&&t.height>=300&&t.height<=2e3&&r>=.25&&r<=2.5}function a(e){let t=0;for(let r=0;r=55296&&n<=56319&&r+1[e.id,t.id])),s=[],d=0;for(let e=0;ee.id));for(let r of t.children){if(e.has(r.id))continue;let t=n(r);t&&!1!==r.visible&&s.push({node:r,box:t})}}s.sort((e,t)=>e.box.y-t.box.y||e.box.x-t.box.x||e.node.id.localeCompare(t.node.id));let f=o.length>d||s.length>100,c=s.slice(0,100),u=new Set(c.map(({node:e})=>e.id)),p=new Set(e.rootIds),g={},h=new Map([[t.id,null]]);for(let e=0;e{let r=e.parent;for(;r&&r.id!==t.id&&!u.has(r.id);)r=r.parent;return[e.id,r&&u.has(r.id)?r.id:t.id]})),m=e=>c.filter(({node:t})=>y.get(t.id)===e).map(({node:e})=>e.id),b=n(t);if(!b)throw Error("DEVUP_NODE_BOUNDS_UNAVAILABLE");let x={id:t.id,type:t.type,fields:{name:t.name,parentId:t.parent&&"DOCUMENT"!==t.parent.type?t.parent.id:null,childrenIds:m(t.id),absoluteBoundingBox:b,visible:!1!==t.visible,projectionTruncated:f,nodeScreenIds:g},extra:{},fieldErrors:{}},E=c.length,A=c.map(({node:e,box:t})=>{let o,l=function(e){let t=[e],r=0,n=0;for(let e=0;er}}(e),s=Math.floor(r/E);E-=1;let d=function(e,t){let r=[e],n="",i=0,o=0,l=!1;if(t<=0)return{text:"",state:"budget-exhausted",spent:0};for(let e=0;e=120||o+e>t)return{text:n.trimEnd(),state:o+e>t?"budget-exhausted":"truncated",spent:o};n+=r,i+=1,o+=e}}if("children"in s){let e=Math.max(0,64-r.length);s.children.length>e&&(l=!0),r.push(...s.children.slice(0,e))}}}return{text:n,state:l?"truncated":n?"available":"no-text",spent:o}}(e,s);return r-=d.spent,{id:e.id,type:e.type,fields:{name:"string"==typeof e.name?e.name:"",parentId:y.get(e.id),childrenIds:m(e.id),absoluteBoundingBox:t,visible:!1!==e.visible,breadcrumb:function(e){let t=[],r=e;for(;r&&"DOCUMENT"!==r.type;)"string"==typeof r.name&&r.name&&t.push(r.name),r=r.parent;return t.reverse()}(e),directChildCount:"children"in e?e.children.length:0,textPreview:d.text,...null===(o=function(e){let t=[e],r=0,i=null;for(;t.length&&r++<400;){let e=t.shift();if("visible"in e&&!1===e.visible)continue;let r="children"in e?e.children.filter(e=>!("visible"in e)||!1!==e.visible):[];if(/header|nav|gnb/i.test(e.name||"")&&r.length>=2&&"layoutMode"in e&&"HORIZONTAL"===e.layoutMode){let t=r.map(n);t.every(Boolean)&&(i=Math.max(i||0,t.reduce((e,t)=>e+t.width,0)+(Number(e.itemSpacing)||0)*(t.length-1)))}t.push(...r.slice(0,80))}return i}(e))?{}:{navigationContentWidth:o},textPreviewState:d.state,subtreeNodeCount:l.subtreeNodeCount,estimatedSerializedBytes:l.estimatedSerializedBytes,selectionReasons:[i(e,t)?"screen-like":"explicit-selection-only","inside-section"],estimateTruncated:l.truncated},extra:{},fieldErrors:{}}}),I={fileKey:figma.fileKey||"",version:null,rootIds:[t.id],nodes:[x,...A],diagnostics:[]},S=a(JSON.stringify(I));for(let e=A.length-1;e>=0&&S>19456;e-=1)A[e].fields.textPreview="",A[e].fields.textPreviewState="budget-exhausted",S=a(JSON.stringify(I));let w=()=>I.nodes.length-1,v=A.length;for(;S>19456&&w()>1;){let e=I.nodes.pop();x.fields.projectionTruncated=!0,x.fields.childrenIds=x.fields.childrenIds.filter(t=>t!==e.id),S=a(JSON.stringify(I))}if(S>19456)throw Error("DEVUP_SECTION_INDEX_TOO_LARGE "+JSON.stringify({pluginCode:"DEVUP_SECTION_INDEX_TOO_LARGE",stage:"section-index",responseBytes:S,maxResponseBytes:19456,discoveredCandidates:v,retainedCandidates:w()}));return I},snapshot:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=await figma.getNodeByIdAsync(e.nodeId);if(!t)throw Error("DEVUP_NODE_NOT_FOUND");let r=["absoluteBoundingBox","absoluteRenderBounds","arcData","backgroundStyleId","blendMode","bottomLeftRadius","bottomRightRadius","boundVariables","characters","clipsContent","componentProperties","componentPropertyDefinitions","componentPropertyReferences","constraints","cornerRadius","counterAxisAlignItems","dashPattern","defaultVariant","effectStyleId","effects","fillStyleId","fills","fontName","fontSize","gridColumnAnchorIndex","gridColumnCount","gridColumnGap","gridColumnSizes","gridColumnSpan","gridRowAnchorIndex","gridRowCount","gridRowGap","gridRowSizes","gridRowSpan","gridStyleId","height","inferredAutoLayout","isAsset","isMask","itemSpacing","layoutGrow","layoutMode","layoutWrap","layoutPositioning","layoutSizingHorizontal","layoutSizingVertical","letterSpacing","lineHeight","maxHeight","maxLines","maxWidth","minHeight","minWidth","name","opacity","overflowDirection","paddingBottom","paddingLeft","paddingRight","paddingTop","primaryAxisAlignItems","reactions","rotation","strokeAlign","strokeBottomWeight","strokeLeftWeight","strokeRightWeight","strokeStyleId","strokeTopWeight","strokeWeight","strokes","targetAspectRatio","textAlignHorizontal","textAlignVertical","textAutoResize","textCase","textDecoration","textStyleId","textTruncation","topLeftRadius","topRightRadius","variantProperties","visible","width","x","y"],n=new Set(r),i=["fontName","fontWeight","fontSize","textDecoration","textCase","lineHeight","letterSpacing","fills","textStyleId","fillStyleId","listOptions","indentation","hyperlink"],a=e.snapshot,o=Math.max(0,Math.floor(Number(a.offset)||0)),l=Math.min(a.maxEnvelopeBytes?Math.max(8192,Math.floor(Number(a.maxEnvelopeBytes))):16e3,Math.max(4096,Math.floor(Number(a.maxPayloadBytes)||15e3))),s=Math.min(l-1024,Math.max(512,Math.floor(Number(a.maxFieldBytes)||4096))),d=new Set(["id","type","parent","children","mainComponent","masterComponent","instances"]),f=new Set(["parentId","childrenIds","name","characters","styledTextSegments","boundVariables"]);function c(e,t,r){let n=function(e){let t=[];for(let r=0;r=55296&&n<=56319){let t=r+1=56320&&t<=57343?(n=65536+(n-55296<<10)+(t-56320),r+=1):n=65533}else n>=56320&&n<=57343&&(n=65533);n<128?t.push(n):n<2048?t.push(192|n>>6,128|63&n):n<65536?t.push(224|n>>12,128|n>>6&63,128|63&n):t.push(240|n>>18,128|n>>12&63,128|n>>6&63,128|63&n)}return new Uint8Array(t)}(JSON.stringify(r));return n.length>0x1000000?{$truncated:"max-large-value-bytes",byteLength:n.length}:{$largeValue:{nodeId:e,field:t,byteLength:n.length,sha256:function(e){let t=[0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,0xe49b69c1,0xefbe4786,0xfc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x6ca6351,0x14292967,0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2],r=64*Math.ceil((e.length+9)/64),n=new Uint8Array(r);n.set(e),n[e.length]=128;let i=8*e.length;for(let e=0;e<8;e+=1)n[r-1-e]=255&Math.floor(i/2**(8*e));let a=[0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19],o=(e,t)=>e>>>t|e<<32-t;for(let e=0;e>>3,n=o(r[e-2],17)^o(r[e-2],19)^r[e-2]>>>10;r[e]=r[e-16]+t+r[e-7]+n>>>0}let[i,l,s,d,f,c,u,p]=a;for(let e=0;e<64;e+=1){let n=p+(o(f,6)^o(f,11)^o(f,25))+(f&c^~f&u)+t[e]+r[e]>>>0,a=(o(i,2)^o(i,13)^o(i,22))+(i&l^i&s^l&s)>>>0;p=u,u=c,c=f,f=d+n>>>0,d=s,s=l,l=i,i=n+a>>>0}for(let[e,t]of[i,l,s,d,f,c,u,p].entries())a[e]=a[e]+t>>>0}return a.map(e=>e.toString(16).padStart(8,"0")).join("")}(n),cursor:{nextOffset:0,maxChunkBytes:12288}}}}function u(e){return function(e){let t=0;for(let r=0;r=55296&&n<=56319&&r+112)return{$truncated:"max-depth"};if("object"==typeof t&&"parent"in t&&"string"==typeof t.id&&"string"==typeof t.type)return{$nodeId:t.id,$nodeType:t.type};if(Array.isArray(t))return t.map(t=>e(t,r,n+1));if(ArrayBuffer.isView(t))return{$binary:t.constructor.name,byteLength:t.byteLength};if(t instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:t.byteLength};if(r.has(t))return{$circular:!0};r.add(t);let i={};for(let a of Object.keys(t).sort())try{let o=e(t[a],r,n+1);o&&"function"===o.$unsupported||(i[a]=o)}catch(e){i[a]={$error:String(e&&e.message?e.message:e)}}return r.delete(t),i}(r),a=u(i);if(a<=s)return i;let o=c(e,t,i);return o.$truncated&&(n[t]=`DEVUP_FIELD_VALUE_UNSUPPORTED:${a}>16777216`),o}let g=new Map;async function h(e,t,r){if("INSTANCE"!==e.type||"function"!=typeof e.getMainComponentAsync)return;g.has(e.id)||g.set(e.id,(async()=>{try{let t=await e.getMainComponentAsync();return{fields:{mainComponentId:t?t.id:null,mainComponentName:t?t.name:null,mainComponentSetId:t&&t.parent&&"COMPONENT_SET"===t.parent.type?t.parent.id:null}}}catch(e){return{error:String(e&&e.message?e.message:e)}}})());let n=await g.get(e.id);n.error?r.mainComponentId=n.error:Object.assign(t,n.fields)}async function y(e){let t={},a={},o={};for(let i of(t.parentId=e.parent?e.parent.id:null,e.parent&&("PAGE"===e.parent.type||"SECTION"===e.parent.type||"COMPONENT_SET"===e.parent.type)&&(t.parentType=e.parent.type,"SECTION"===e.parent.type&&(t.parentName=e.parent.name)),t.childrenIds="children"in e?e.children.map(e=>e.id):[],!("overflowDirection"in e)&&["FRAME","COMPONENT","INSTANCE","COMPONENT_SET"].includes(e.type)&&(t.overflowDirection=null),function(e){let t=new Set,n=e;for(;n&&n!==Object.prototype;){for(let e of Object.getOwnPropertyNames(n))t.add(e);n=Object.getPrototypeOf(n)}for(let n of r)try{n in e&&t.add(n)}catch(e){}return[...t].sort()}(e)))if(!(d.has(i)||i.startsWith("_")||"componentPropertyDefinitions"===i&&"COMPONENT"===e.type&&e.parent&&"COMPONENT_SET"===e.parent.type))try{let r=e[i];if("function"==typeof r)continue;let l=p(e.id,i,r,o);(n.has(i)?t:a)[i]=l}catch(e){o[i]=String(e&&e.message?e.message:e)}if("TEXT"===e.type&&"function"==typeof e.getStyledTextSegments)try{t.styledTextSegments=p(e.id,"styledTextSegments",e.getStyledTextSegments(i),o)}catch(e){o.styledTextSegments=String(e&&e.message?e.message:e)}return await h(e,t,o),{id:e.id,type:e.type,fields:t,extra:a,fieldErrors:o}}let m=[],b=[t];for(;b.length;){let e=b.shift();m.push(e),"children"in e&&b.push(...e.children)}let x=[],E=l-1024,A=2;for(let e=o;eNumber(e.protected)-Number(t.protected)||t.byteLength-e.byteLength),n)){if(r<=t)break;let n=c(e.id,i.name,e[i.sectionName][i.name]);e[i.sectionName][i.name]=n,n.$truncated&&(e.fieldErrors[i.name]=`DEVUP_FIELD_VALUE_UNSUPPORTED:${i.byteLength}>16777216`),r=u(e)}return e}(await y(m[e]),E),r=u(t)+ +!!x.length;if(x.length&&A+r>E)break;x.push(t),A+=r}let I=Math.min(m.length,o+x.length);return x.push({id:"__DEVUP_SNAPSHOT_CURSOR__",type:"DEVUP_INTERNAL",fields:{offset:o,nextOffset:I,complete:I>=m.length,totalNodes:m.length},extra:{},fieldErrors:{}}),{fileKey:figma.fileKey||"",version:null,rootIds:[t.id],nodes:x,diagnostics:[]}},usedResources:async function(e){let t=e.resources;function r(e,t=new WeakSet,n=0){if(null===e||["string","number","boolean"].includes(typeof e))return e;if(void 0===e)return{$undefined:!0};if("bigint"==typeof e)return{$bigint:e.toString()};if(["function","symbol"].includes(typeof e))return{$unsupported:typeof e};if(n>12)return{$truncated:"max-depth"};if("object"==typeof e&&"parent"in e&&"string"==typeof e.id&&"string"==typeof e.type)return{$nodeId:e.id,$nodeType:e.type};if(Array.isArray(e))return e.map(e=>r(e,t,n+1));if(ArrayBuffer.isView(e))return{$binary:e.constructor.name,byteLength:e.byteLength};if(e instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:e.byteLength};if(t.has(e))return{$circular:!0};t.add(e);let i={},a=new Set(Object.keys(e)),o=e;for(;o&&o!==Object.prototype;){for(let e of Object.getOwnPropertyNames(o))a.add(e);o=Object.getPrototypeOf(o)}for(let o of[...a].sort())if(!(o.startsWith("_")||["parent","children","consumers"].includes(o)))try{let a=r(e[o],t,n+1);a&&"function"===a.$unsupported||(i[o]=a)}catch(e){i[o]={$error:"unavailable"}}return t.delete(e),i}let n=new Map,i="ok";try{for(let e of(await figma.variables.getLocalVariablesAsync()))n.set(e.id,e)}catch(e){i="unavailable"}function a(e){return Object.values(e.valuesByMode||{}).filter(e=>e&&"VARIABLE_ALIAS"===e.type&&"string"==typeof e.id&&e.id).map(e=>e.id)}async function o(e){let t=n.get(e);if(t)return{value:r(t),collectionId:t.variableCollectionId,aliasIds:a(t)};try{let t=await figma.variables.getVariableByIdAsync(e);return t?{value:r(t),collectionId:t.variableCollectionId,aliasIds:a(t)}:{unresolved:{id:e,kind:"variable",reason:"notInFileAndLookupEmpty"}}}catch(t){return{unresolved:{id:e,kind:"variable",reason:"notInFileAndLookupThrew",error:String(t&&t.message||t).slice(0,300)}}}}let l=await Promise.all(t.variableIds.map(o)),s=new Set(t.variableIds),d=l.flatMap(e=>e.aliasIds||[]);for(;d.length>0;){let e=[...new Set(d)].filter(e=>!s.has(e)).sort();for(let t of e)s.add(t);let t=await Promise.all(e.map(o));l.push(...t),d=t.flatMap(e=>e.aliasIds||[])}let f=[...new Set(l.flatMap(e=>e.collectionId?[e.collectionId]:[]))].sort(),c=await Promise.all(f.map(async e=>{try{let t=await figma.variables.getVariableCollectionByIdAsync(e);return t?[r(t)]:[]}catch(e){return[]}})),u=await Promise.all(t.styles.map(async e=>{try{let t=await figma.getStyleByIdAsync(e.id);if(!t)return{unresolved:{id:e.id,kind:"style",reason:"notFoundOrUnavailable"}};return{value:{...r(t),styleType:e.styleType,value:r("PAINT"===e.styleType?t.paints:"EFFECT"===e.styleType?t.effects:"GRID"===e.styleType?t.layoutGrids:t)}}}catch(t){return{unresolved:{id:e.id,kind:"style",reason:"notFoundOrUnavailable",error:String(t&&t.message||t).slice(0,300)}}}}));return{collections:c.flat(),variables:l.flatMap(e=>e.value?[e.value]:[]),styles:u.flatMap(e=>e.value?[e.value]:[]),usedVariableIds:t.variableIds,usedStyleIds:t.styles.map(e=>e.id),localVariableListing:i,localVariableCount:n.size,unresolved:[...l,...u].flatMap(e=>e.unresolved?[e.unresolved]:[])}},variableCatalog:async function(e){let[t,r,n,i,a]=await Promise.all([figma.variables.getLocalVariableCollectionsAsync(),figma.getLocalPaintStylesAsync(),figma.getLocalTextStylesAsync(),figma.getLocalEffectStylesAsync(),figma.getLocalGridStylesAsync()]),o=["PAINT","TEXT","EFFECT","GRID"];return{collections:t.map(e=>(function e(t,r=new WeakSet,n=0){if(null===t||["string","number","boolean"].includes(typeof t))return t;if(void 0===t)return{$undefined:!0};if("bigint"==typeof t)return{$bigint:t.toString()};if(["function","symbol"].includes(typeof t))return{$unsupported:typeof t};if(n>12)return{$truncated:"max-depth"};if(Array.isArray(t))return t.map(t=>e(t,r,n+1));if(r.has(t))return{$circular:!0};r.add(t);let i={},a=new Set(Object.keys(t)),o=t;for(;o&&o!==Object.prototype;){for(let e of Object.getOwnPropertyNames(o))a.add(e);o=Object.getPrototypeOf(o)}for(let o of[...a].sort())if(!o.startsWith("_"))try{let a=e(t[o],r,n+1);a&&"function"===a.$unsupported||(i[o]=a)}catch(e){i[o]={$error:String(e&&e.message?e.message:e)}}return r.delete(t),i})(e)),variableIds:[...new Set(t.flatMap(e=>e.variableIds))].sort(),styles:[r,n,i,a].flatMap((e,t)=>e.map(e=>({id:e.id,styleType:o[t]}))).sort((e,t)=>e.id.localeCompare(t.id)),localComplete:!0,usedRemoteComplete:!1}},variables:async function(e){let t=e.resources;function r(e,t=new WeakSet,n=0){if(null===e||["string","number","boolean"].includes(typeof e))return e;if(void 0===e)return{$undefined:!0};if("bigint"==typeof e)return{$bigint:e.toString()};if(["function","symbol"].includes(typeof e))return{$unsupported:typeof e};if(n>12)return{$truncated:"max-depth"};if("object"==typeof e&&"parent"in e&&"string"==typeof e.id&&"string"==typeof e.type)return{$nodeId:e.id,$nodeType:e.type};if(Array.isArray(e))return e.map(e=>r(e,t,n+1));if(ArrayBuffer.isView(e))return{$binary:e.constructor.name,byteLength:e.byteLength};if(e instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:e.byteLength};if(t.has(e))return{$circular:!0};t.add(e);let i={},a=new Set(Object.keys(e)),o=e;for(;o&&o!==Object.prototype;){for(let e of Object.getOwnPropertyNames(o))a.add(e);o=Object.getPrototypeOf(o)}for(let o of[...a].sort())if(!(o.startsWith("_")||["parent","children","consumers"].includes(o)))try{let a=r(e[o],t,n+1);a&&"function"===a.$unsupported||(i[o]=a)}catch(e){i[o]={$error:String(e&&e.message?e.message:e)}}return t.delete(e),i}let[n,i]=await Promise.all([Promise.all(t.variableIds.map(e=>figma.variables.getVariableByIdAsync(e))),Promise.all(t.styles.map(e=>figma.getStyleByIdAsync(e.id)))]),a=new Map(t.styles.map(e=>[e.id,e])),o=await Promise.all(i.filter(Boolean).map(async e=>{let t=a.get(e.id),n=t.styleType;if(Number.isInteger(t.consumerStart)&&Number.isInteger(t.consumerEnd)){let i=await e.getStyleConsumersAsync();return{id:e.id,styleType:n,$consumerStart:t.consumerStart,$consumerEntries:i.slice(t.consumerStart,t.consumerEnd).map(e=>[e.node.id,e.node.type,r(e.fields)])}}let i=await e.getStyleConsumersAsync();return{...r(e),styleType:n,$consumerCount:i.length,value:r("PAINT"===n?e.paints:"EFFECT"===n?e.effects:"GRID"===n?e.layoutGrids:e)}}));return{variables:n.filter(Boolean).map(e=>r(e)),styles:o}}},t=new Map,r=0;globalThis.__devupNormalizeImage=(e,n,i,a,o)=>new Promise(l=>{r+=1;let s=`decode-${r}`,d=setTimeout(()=>{t.delete(s),l(null)},i);t.set(s,e=>{clearTimeout(d),t.delete(s),l(e)}),figma.ui.postMessage({kind:"devup-decode",id:s,bytes:e,maxEdge:n,width:a,height:o})});let n=globalThis;function i(){let e=figma.currentPage,t=e.selection;return t.length>0&&(n.__devupLastSelection=t.slice(0,20).map(e=>({id:e.id,name:e.name.slice(0,256),type:e.type})),n.__devupLastSelectionPage={id:e.id,name:e.name.slice(0,256)}),{zoom:Number.isFinite(figma.viewport?.zoom)?figma.viewport.zoom:null,observedAt:Date.now(),lastKnownSelection:n.__devupLastSelection??null,lastKnownPage:n.__devupLastSelectionPage??null,currentPage:{id:e.id,name:e.name.slice(0,256)},selection:t.slice(0,20).map(e=>({id:e.id,name:e.name.slice(0,256),type:e.type})),selectionCount:t.length}}function a(){let e={kind:"devup-context",...i()};figma.ui.postMessage(e)}async function o(t){let r=e[t.script];if(!r)return{kind:"devup-result",requestId:t.requestId,error:`DEVUP_BRIDGE_UNKNOWN_SCRIPT: ${t.script} (아는 스크립트: ${Object.keys(e).join(", ")})`};try{var n;"bridgeContext"===t.script&&i();let e=await r({nodeId:(n=t.params).nodeId??"",rootIds:n.rootIds??[],snapshot:n.snapshot??{},search:n.search??{},explore:n.explore??{},resources:n.resources??{variableIds:[],styles:[]},largeValue:n.largeValue??{},asset:n.asset??{},theme:n.theme??{offset:0}});return{kind:"devup-result",requestId:t.requestId,data:e}}catch(e){return{kind:"devup-result",requestId:t.requestId,error:e instanceof Error?e.message:String(e)}}}let l=[],s=!1,d=null,f=new Set;async function c(){if(!s){s=!0;try{for(;l.length>0;){let e=l.shift();d=e.requestId;let t=await o(e).catch(t=>({kind:"devup-result",requestId:e.requestId,error:t instanceof Error?t.message:String(t)}));d=null,f.delete(e.requestId)||figma.ui.postMessage(t)}}finally{s=!1}}}let u=null;function p(){u??(u=(async()=>{try{await figma.loadAllPagesAsync(),await Promise.all([figma.variables.getLocalVariableCollectionsAsync(),figma.variables.getLocalVariablesAsync(),figma.getLocalPaintStylesAsync(),figma.getLocalTextStylesAsync(),figma.getLocalEffectStylesAsync(),figma.getLocalGridStylesAsync()])}catch{}finally{u=null}})())}figma.showUI(__html__,{width:320,height:220}),p(),figma.on("selectionchange",a),figma.on("currentpagechange",a),figma.ui.onmessage=e=>{if("object"==typeof e&&null!==e){if("devup-ready"===e.kind){let e={kind:"devup-status",fileKey:figma.fileKey??null,fileName:figma.root.name,port:1993,...i()};figma.ui.postMessage(e);return}if("devup-job"===e.kind){l.push(e),c();return}if("devup-cancel"===e.kind){var r;let t;return void(r=e.requestId,(t=l.findIndex(e=>e.requestId===r))>=0?l.splice(t,1):d===r&&f.add(r))}if("devup-decoded"===e.kind)return void t.get(e.id)?.(e.png??null);"devup-in-view"===e.kind&&p()}}})(); \ No newline at end of file From 3f61f244bc8ce7be448d22d5a89f5bd8564bcbd7 Mon Sep 17 00:00:00 2001 From: leader Date: Thu, 22 Oct 2026 19:14:17 +0900 Subject: [PATCH 003/149] fix(export): oversized responseFields selection no longer blocks committed file writes --- .../projection/finalize_export_delivery.rs | 35 +++++++++++++------ 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/crates/devup-mcp/src/server/projection/finalize_export_delivery.rs b/crates/devup-mcp/src/server/projection/finalize_export_delivery.rs index f1c83719..e0ca5a97 100644 --- a/crates/devup-mcp/src/server/projection/finalize_export_delivery.rs +++ b/crates/devup-mcp/src/server/projection/finalize_export_delivery.rs @@ -462,16 +462,31 @@ pub(super) async fn finalize_export_delivery( } else { super::super::compact::select(&mut result, summary, &response_selection) }; - selected.map_err(|mut error| { - let mut recovery=recovery_arguments.clone(); - recovery.as_object_mut().unwrap().remove("path"); - recovery.as_object_mut().unwrap().remove("responseFields"); - if outputs.iter().all(|o|o=="outline") { recovery["responseLimit"]=json!(response_selection.response_limit.min(25)); } - error.details=json!({"artifactId":artifact.artifact_id,"maxSelectedBytes":65536, - "nextAction":{"tool":"devup_figma_export","arguments":recovery, - "how":"Read original outputs as bounded resources, or select a narrower path or lower responseLimit against this same artifact. Scalar values are never silently truncated."}}); - error - })? + match selected { + Ok(details) => details, + Err(mut error) => { + let mut recovery = recovery_arguments.clone(); + recovery.as_object_mut().unwrap().remove("path"); + recovery.as_object_mut().unwrap().remove("responseFields"); + if outputs.iter().all(|o| o == "outline") { + recovery["responseLimit"] = json!(response_selection.response_limit.min(25)); + } + let next = json!({"tool":"devup_figma_export","arguments":recovery, + "how":"Read original outputs as bounded resources, or select a narrower path or lower responseLimit against this same artifact. Scalar values are never silently truncated."}); + if error.code != ErrorCode::DevupFigmaResponseTooLarge { + error.details = json!({"artifactId":artifact.artifact_id,"maxSelectedBytes":65536,"nextAction":next}); + return Err(error); + } + transaction.commit()?; + let paths = result + .get("outputPaths") + .cloned() + .unwrap_or_else(|| json!({})); + return Ok(json!({"status":"complete","written":true, + "artifactId":artifact.artifact_id,"outputPaths":paths, + "responseTruncated":{"reason":error.message,"maxSelectedBytes":65536,"nextAction":next}})); + } + } } else { None }; From 1722d5c97a1a108871458ed0876e9ff300a95e70 Mon Sep 17 00:00:00 2001 From: leader Date: Thu, 22 Oct 2026 19:21:30 +0900 Subject: [PATCH 004/149] fix: flag excluded node pages, name device-only frames by section, widen exclude words --- crates/devup-mcp-figma/src/search.rs | 12 ++++++ .../src/server/projection/completion.rs | 25 +++++++++++-- .../src/server/screen_plan/routes.rs | 12 ++++++ .../frame_inventory_and_link_labels.rs | 37 +++++++++++++++++++ .../text/screen-plan-exclude-words.json | 6 ++- 5 files changed, 87 insertions(+), 5 deletions(-) diff --git a/crates/devup-mcp-figma/src/search.rs b/crates/devup-mcp-figma/src/search.rs index 63e16b1c..7a646473 100644 --- a/crates/devup-mcp-figma/src/search.rs +++ b/crates/devup-mcp-figma/src/search.rs @@ -45,6 +45,10 @@ const EXCLUDED_KEYWORDS: &[&str] = &[ "보관", "초안", "복사", + "pdf", + "저장용", + "export", + "출력", ]; fn excluded_reason(breadcrumb: &[String], page_name: Option<&str>) -> Option { @@ -433,6 +437,14 @@ mod tests { assert!(results[1].excluded.as_deref().unwrap().contains("휴지통")); } + #[test] + fn pdf_and_export_pages_are_flagged_as_excluded() { + for page in ["PDF", "저장용", "Export", "출력본"] { + assert!(excluded_reason(&[], Some(page)).is_some(), "{page}"); + } + assert!(excluded_reason(&[], Some("디자인 시스템")).is_none()); + } + #[test] fn an_empty_query_without_node_types_is_invalid_input() { let (snapshot, target) = typed_snapshot(); diff --git a/crates/devup-mcp/src/server/projection/completion.rs b/crates/devup-mcp/src/server/projection/completion.rs index ebb52684..fddbe45f 100644 --- a/crates/devup-mcp/src/server/projection/completion.rs +++ b/crates/devup-mcp/src/server/projection/completion.rs @@ -135,10 +135,14 @@ fn attach_node_page(source: &mut Value, payload: &CollectedPayload) { }; source["nodePage"] = json!({"id": id, "name": name}); let lowered = name.to_lowercase(); - if ["trash", "archive", "draft", "휴지통", "보관", "초안"] - .iter() - .any(|keyword| lowered.contains(keyword)) + if let Some(keyword) = [ + "trash", "archive", "draft", "copy", "pdf", "export", "휴지통", "보관", "초안", "복사", + "저장용", "출력", + ] + .iter() + .find(|keyword| lowered.contains(*keyword)) { + source["nodePage"]["excluded"] = json!(format!("{keyword} ({name})")); source["nodePageNote"] = json!(format!( "The node lives on the page \"{name}\", whose name marks it as trash, archive or draft." )); @@ -217,10 +221,23 @@ mod node_page_tests { fn a_trash_page_is_named_and_flagged() { let mut source = json!({"viewerPageName": "Trash old"}); attach_node_page(&mut source, &payload_on_page("Trash old")); - assert_eq!(source["nodePage"], json!({"id": "0:1", "name": "Trash old"})); + assert_eq!( + source["nodePage"], + json!({"id": "0:1", "name": "Trash old", "excluded": "trash (Trash old)"}) + ); assert!(source["nodePageNote"].as_str().unwrap().contains("trash, archive or draft")); } + #[test] + fn a_trash_page_sets_the_excluded_marker_on_node_page() { + let mut source = json!({}); + attach_node_page(&mut source, &payload_on_page("PDF 저장용")); + assert!(source["nodePage"]["excluded"].as_str().unwrap().contains("pdf")); + let mut source = json!({}); + attach_node_page(&mut source, &payload_on_page("Screens")); + assert!(source["nodePage"].get("excluded").is_none()); + } + #[test] fn a_node_off_the_viewer_page_says_so() { let mut source = json!({"viewerPageName": "Cover"}); diff --git a/crates/devup-mcp/src/server/screen_plan/routes.rs b/crates/devup-mcp/src/server/screen_plan/routes.rs index 074b8b62..dc1aa717 100644 --- a/crates/devup-mcp/src/server/screen_plan/routes.rs +++ b/crates/devup-mcp/src/server/screen_plan/routes.rs @@ -191,6 +191,18 @@ fn describe_frames(frames: &[Frame]) -> String { /// - the same name at the same width in one section is not grouped; it is /// flagged as a possible duplicate by [`structure_questions`]. fn group_screens(screens: &[PlanScreenInput], config: &PlanConfig) -> Vec { + let renamed = screens + .iter() + .map(|screen| match screen.section_name.as_deref() { + Some(section) if device_name(&screen.name) && !device_name(section) => { + let mut named = screen.clone(); + named.name = format!("{section} - {}", screen.name); + named + } + _ => screen.clone(), + }) + .collect::>(); + let screens = renamed.as_slice(); let mut order = Vec::::new(); let mut groups = BTreeMap::>::new(); for screen in screens { diff --git a/crates/devup-mcp/src/server/screen_plan_tests/frame_inventory_and_link_labels.rs b/crates/devup-mcp/src/server/screen_plan_tests/frame_inventory_and_link_labels.rs index fd3d638c..018f8932 100644 --- a/crates/devup-mcp/src/server/screen_plan_tests/frame_inventory_and_link_labels.rs +++ b/crates/devup-mcp/src/server/screen_plan_tests/frame_inventory_and_link_labels.rs @@ -350,6 +350,43 @@ fn device_only_names_do_not_group_across_sections() { assert!(!question_ids(&plan).iter().any(|id| id.starts_with("sections:"))); } +#[test] +fn device_only_frames_take_their_screen_name_from_the_section() { + let plan = draft_plan( + &[ + sectioned("62:1", "Desktop", 1440.0, "Main"), + sectioned("62:2", "Mobile", 360.0, "Main"), + sectioned("62:3", "Desktop", 1440.0, "About"), + sectioned("62:4", "Mobile", 360.0, "About"), + ], + &BTreeMap::new(), + &evidence(), + json!({}), + None, + ) + .unwrap(); + assert_eq!(array(&plan["screens"]).len(), 2); + assert!(!question_ids(&plan).iter().any(|id| id.starts_with("sections:"))); +} + +#[test] +fn a_navigation_target_settles_the_route_without_a_question() { + let targets = BTreeMap::from([("73:8589".to_owned(), "/ko/technology/patents".to_owned())]); + let plan = draft_plan( + &[screen("73:8589", "Patents", 1440.0)], + &targets, + &evidence(), + json!({}), + None, + ) + .unwrap(); + assert!( + !question_ids(&plan).iter().any(|id| id.starts_with("route:")), + "{:?}", + question_ids(&plan) + ); +} + #[test] fn a_truncated_inventory_names_the_limit_and_the_next_step() { let error = draft_plan_with( diff --git a/crates/devup-mcp/src/server/text/screen-plan-exclude-words.json b/crates/devup-mcp/src/server/text/screen-plan-exclude-words.json index c0d1423d..b935c9ab 100644 --- a/crates/devup-mcp/src/server/text/screen-plan-exclude-words.json +++ b/crates/devup-mcp/src/server/text/screen-plan-exclude-words.json @@ -20,5 +20,9 @@ "sandbox", "playground", "copy", - "사본" + "사본", + "pdf", + "저장용", + "export", + "출력" ] From 3ff563bce4deb968093b7f04063b14bf3233fbd6 Mon Sep 17 00:00:00 2001 From: leader Date: Thu, 22 Oct 2026 19:29:33 +0900 Subject: [PATCH 005/149] fix(assets): decode HTML entities and drop parentheses in layer names used for asset file names --- .../src/codegen/style/image_fill_source.rs | 19 +++++++++++++++++-- .../tests/image_asset_names.rs | 11 +++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/crates/devup-mcp-devup-ui/src/codegen/style/image_fill_source.rs b/crates/devup-mcp-devup-ui/src/codegen/style/image_fill_source.rs index 158b9014..d6f9aa8a 100644 --- a/crates/devup-mcp-devup-ui/src/codegen/style/image_fill_source.rs +++ b/crates/devup-mcp-devup-ui/src/codegen/style/image_fill_source.rs @@ -102,14 +102,15 @@ pub fn image_fill_path( /// share a name and a file, as the same icon at three widths should. pub(crate) fn asset_stem(snapshot: &Snapshot, node: &RawNode, per_node: bool) -> String { let view = node.typed_view(); - let name = view.name().unwrap_or("Asset"); + let name = layer_name(node); + let name = name.as_str(); let identity = asset_identity(node); let raster_instance = view.node_type() == "INSTANCE" && asset_kind(snapshot, node) == Some(AssetKind::Png); let shared = per_node || snapshot.nodes.values().any(|other| { other.id != node.id - && other.typed_view().name() == Some(name) + && layer_name(other) == name && (asset_identity(other) != identity || raster_instance || (fills(node).is_some_and(|fs| fs.iter().any(|p| p["type"] == "IMAGE")) @@ -131,6 +132,20 @@ pub(crate) fn asset_stem(snapshot: &Snapshot, node: &RawNode, per_node: bool) -> } } +/// The layer name with HTML entities decoded: Figma text may arrive as +/// `R&D`, which must not leak `amp` into a file name. +fn layer_name(node: &RawNode) -> String { + node.typed_view() + .name() + .unwrap_or("Asset") + .replace("<", "<") + .replace(">", ">") + .replace(""", "\"") + .replace("'", "'") + .replace("&", "&") + .replace(['(', ')'], "") +} + /// What makes an instance the thing it is: its variant, as `chart` or /// `lg,primary`. `None` for a node that is not an instance of a variant. fn asset_identity(node: &RawNode) -> Option { diff --git a/crates/devup-mcp-devup-ui/tests/image_asset_names.rs b/crates/devup-mcp-devup-ui/tests/image_asset_names.rs index da2b63f9..15cd3fc2 100644 --- a/crates/devup-mcp-devup-ui/tests/image_asset_names.rs +++ b/crates/devup-mcp-devup-ui/tests/image_asset_names.rs @@ -77,3 +77,14 @@ fn raster_instances_with_same_variant_name_and_different_images_have_distinct_co ); } } + +#[test] +fn html_entities_in_layer_names_do_not_leak_into_asset_paths() { + let snapshot: Snapshot = serde_json::from_value(json!({"fileKey":"fixture","version":null,"roots":["root"],"diagnostics":[], + "nodes":{"root":{"id":"root","type":"FRAME","fields":{"name":"Page","width":100,"height":100,"childrenIds":["img"]}}, + "img":{"id":"img","type":"RECTANGLE","fields":{"name":"R&D","parentId":"root","isAsset":true,"width":40,"height":50, + "fills":[{"type":"IMAGE","imageHash":"a","scaleMode":"CROP"}]}}}})).unwrap(); + let path = image_fill_path(&snapshot, "img", 0, false).unwrap(); + assert_eq!(path, "/images/R&D.png"); + assert!(!path.contains("amp")); +} From 604813c62758311670d4edd8893fb62f86280fc7 Mon Sep 17 00:00:00 2001 From: leader Date: Thu, 22 Oct 2026 23:09:06 +0900 Subject: [PATCH 006/149] feat(project-plan): staged units, shared change requests, API wiring checks, erd gate, merged screen answers --- .../devup-mcp/src/server/project_plan/mod.rs | 137 ++++++++++++++++-- .../src/server/project_plan/tests.rs | 47 ++++++ .../src/server/project_plan/units.rs | 40 +++++ .../src/server/project_plan/wiring.rs | 128 ++++++++++++++++ .../src/server/screen_plan/status.rs | 10 ++ 5 files changed, 353 insertions(+), 9 deletions(-) create mode 100644 crates/devup-mcp/src/server/project_plan/wiring.rs diff --git a/crates/devup-mcp/src/server/project_plan/mod.rs b/crates/devup-mcp/src/server/project_plan/mod.rs index b749e265..a733a8e0 100644 --- a/crates/devup-mcp/src/server/project_plan/mod.rs +++ b/crates/devup-mcp/src/server/project_plan/mod.rs @@ -5,6 +5,7 @@ mod rounds; #[cfg(test)] mod tests; mod units; +mod wiring; use super::{ output::{OutputPolicy, OutputTransaction}, @@ -57,6 +58,10 @@ pub struct ProjectPlanInput { pub decisions: Vec, #[serde(default)] pub unit_updates: Vec, + /// Records the operationId each feature's pages wire to ("Wire to the API + /// now"); status checks every id against openapi.json. + #[serde(default)] + pub api_wiring: Vec, /// summary (default) answers with state, open questions, next action, ERD and /// unit DAG; full also returns the whole plan. #[schemars(extend("enum" = ["summary", "full"]))] @@ -121,14 +126,33 @@ pub struct Decision { #[serde(default)] pub destructive: bool, } -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[schemars(extend("additionalProperties" = json!({"not": {}})))] pub struct UnitUpdate { pub id: String, + #[serde(default)] pub state: String, #[serde(default)] pub verification: Vec, + /// "shared-change-request": a worker asks the supervisor to change a stage 0 + /// file. Needs file and reason; state "resolved" closes requests for that file. + pub kind: Option, + pub file: Option, + pub reason: Option, + pub diff: Option, +} +/// Binds a feature to the API operation its pages call. `defer` records the +/// explicit decision to use no API yet; it is reported as unfinished. +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[schemars(extend("additionalProperties" = json!({"not": {}})))] +pub struct ApiWiring { + pub feature: String, + pub operation_id: Option, + #[serde(default)] + pub defer: bool, + pub reason: Option, } fn read(root: &str) -> Result { @@ -233,6 +257,9 @@ fn next_action(plan: &Value, root: &str) -> Value { "how":"Figma sources need a confirmed screen plan before project confirm; run it first and ask its questions together with this plan's."}); } match plan["state"].as_str() { + Some("confirmed") if !units::open_requests(plan).is_empty() => json!({ + "tool":"devup_project_plan","arguments":args("status"), + "how":"Apply the open sharedChangeRequests to the stage 0 files, then send a unitUpdates entry of kind shared-change-request, state resolved and the file; merged units that depend on stage 0 go back to verifying."}), Some("confirmed") if array(&plan["units"]).is_empty() => json!({ "tool":"devup_project_plan","arguments":args("split"), "how":"Models are written and unitDag is empty until split; run backendCommands in modelsWorkingDirectory first."}), @@ -253,13 +280,95 @@ fn unit_rows(plan: &Value) -> Vec { .collect() } /// Questions of an unconfirmed screen plan, so one round covers both plans. -fn open_screen_questions(root: &str) -> Vec { +fn open_screen_plan(root: &str) -> Option { match screen_plan::read_plan(root) { - Ok(Some(s)) if s["state"] != "confirmed" && s["status"] != "confirmed" => array(&s["questions"]).to_vec(), - _ => vec![], + Ok(Some(s)) if s["state"] != "confirmed" && s["status"] != "confirmed" => Some(s), + _ => None, + } +} +fn open_screen_questions(root: &str) -> Vec { + open_screen_plan(root) + .map(|s| array(&s["questions"]).to_vec()) + .unwrap_or_default() +} +/// Answers addressed to screen-plan questions are not project-plan answers: +/// they are split off and returned as the devup_screen_plan confirm call that +/// carries them, so the user answers one merged round. +fn forward_screen_answers( + root: &str, + answers: &BTreeMap, + accept_recommended: bool, +) -> (BTreeMap, Value) { + let Some(screen) = open_screen_plan(root) else { + return (answers.clone(), Value::Null); + }; + let ids = array(&screen["questions"]) + .iter() + .map(|q| text(&q["id"]).to_owned()) + .collect::>(); + let (screen_answers, own): (BTreeMap<_, _>, BTreeMap<_, _>) = answers + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .partition(|(k, _)| ids.contains(k)); + if screen_answers.is_empty() && !(accept_recommended && !ids.is_empty()) { + return (own, Value::Null); + } + let forward = json!({"tool":"devup_screen_plan","arguments":{ + "action":"confirm","planId":screen["planId"],"projectRoot":root, + "answers":screen_answers,"acceptRecommended":accept_recommended}, + "how":"These answers belong to the screen plan: send them with this call. The user was asked once, in the merged proposal."}); + (own, forward) +} +fn without_screens(plan: &Value) -> Vec { + array(&plan["features"]) + .iter() + .filter(|f| array(&f["screens"]).is_empty()) + .map(|f| f["id"].clone()) + .collect() +} +const DATA_UI: [&str; 9] = [ + "list", "detail", "form", "search", "inquiry", "filter", "pagination", "board", "table", +]; +/// Set when the project plan has screens that read or write data but its ERD is +/// not confirmed yet; page writes are refused until then. +pub(crate) fn erd_required(root: &str) -> Option { + let plan = read(root).ok()?; + if plan["state"] == "confirmed" || !erd::api(&plan) { + return None; } + let data_ui = !array(&plan["entities"]).is_empty() + || array(&plan["features"]).iter().any(|f| { + let haystack = format!("{} {} {}", text(&f["scope"]), f["acceptance"], f["screens"]) + .to_lowercase(); + !array(&f["entities"]).is_empty() || DATA_UI.iter().any(|k| haystack.contains(k)) + }); + data_ui.then(|| json!({"planId":plan["planId"],"state":plan["state"]})) +} +fn blockers(plan: &Value, root: &str) -> Vec { + [screen_blockers(plan, root), wiring::blockers(plan, root)].concat() +} +fn extras(plan: &Value, root: &str) -> Value { + let no_screens = without_screens(plan); + let mut extra = json!({ + "sharedChangeRequests": units::open_requests(plan), + "apiWiring": wiring::report(plan, root), + "backendChecks": wiring::backend_checks(plan, root), + }); + if !no_screens.is_empty() { + extra["featuresWithoutScreens"] = json!({"features":no_screens, + "how":"These features list no screens. Pass features[].screens (page or route names) in draft; no screens question is asked."}); + } + extra } fn response(plan: Value, root: &str, full: bool) -> Value { + let extra = extras(&plan, root); + let mut out = base_response(plan, root, full); + if let (Some(o), Some(e)) = (out.as_object_mut(), extra.as_object()) { + o.extend(e.clone()); + } + out +} +fn base_response(plan: Value, root: &str, full: bool) -> Value { let questions = array(&plan["questions"]) .iter() .filter(|q| plan["answers"][text(&q["id"])].is_null()) @@ -279,7 +388,7 @@ fn response(plan: Value, root: &str, full: bool) -> Value { "openQuestionCount":questions.len(), "openQuestionIds":questions.iter().map(|q| q["id"].clone()).collect::>(), "screenPlanQuestionIds":screen_ids, - "blockers":screen_blockers(&plan, root), + "blockers":blockers(&plan, root), "proposalMarkdown":screen_plan::proposal_markdown(&proposal), "questionRounds":screen_plan::question_rounds(&questions, 4), "instruction":instruction, @@ -294,7 +403,7 @@ fn response(plan: Value, root: &str, full: bool) -> Value { "how":"Summary only. Pass detail \"full\" to read the whole plan."}); } json!({"planId":plan["planId"], "state":plan["state"], - "blockers":screen_blockers(&plan, root), + "blockers":blockers(&plan, root), "proposalMarkdown":screen_plan::proposal_markdown(&proposal), "proposal":proposal, "screenPlanQuestionIds":screen_ids, @@ -353,12 +462,17 @@ pub(super) async fn run( text(&plan["state"]) ))); } + let (answers, forward) = if matches!(input.action.as_str(), "draft" | "confirm") { + forward_screen_answers(&input.project_root, &input.answers, input.accept_recommended) + } else { + (input.answers.clone(), Value::Null) + }; match input.action.as_str() { "draft" => { if plan["state"] == "confirmed" { return Err(invalid("Confirmed plans cannot be redrafted")); } - rounds::answers(&mut plan, &input.answers)?; + rounds::answers(&mut plan, &answers)?; if input.accept_recommended { rounds::accept_recommended(&mut plan)?; } @@ -401,7 +515,7 @@ pub(super) async fn run( if plan["state"] != "draft" { return Err(invalid("Draft the complete plan before confirm")); } - rounds::answers(&mut plan, &input.answers)?; + rounds::answers(&mut plan, &answers)?; if input.accept_recommended { rounds::accept_recommended(&mut plan)?; } @@ -483,7 +597,12 @@ pub(super) async fn run( )); } } + wiring::record(&mut plan, &input.api_wiring)?; write(&mut tx, policy, &input.project_root, &plan)?; tx.commit()?; - Ok(response(plan, &input.project_root, full)) + let mut out = response(plan, &input.project_root, full); + if !forward.is_null() { + out["screenPlanForward"] = forward; + } + Ok(out) } diff --git a/crates/devup-mcp/src/server/project_plan/tests.rs b/crates/devup-mcp/src/server/project_plan/tests.rs index 39082ad5..eced6d38 100644 --- a/crates/devup-mcp/src/server/project_plan/tests.rs +++ b/crates/devup-mcp/src/server/project_plan/tests.rs @@ -228,6 +228,7 @@ async fn feature_split_and_decisions_and_state_gates() { id: "posts-api".into(), state: "running".into(), verification: vec![], + ..Default::default() }); assert!(run(&i, &policy).await.is_err()); i.unit_updates.clear(); @@ -329,3 +330,49 @@ async fn status_lists_open_screen_plan_questions_in_the_proposal() { assert_eq!(r["screenPlanQuestionIds"], json!(["route:9:1"])); assert!(text(&r["proposalMarkdown"]).contains("Which URL?")); } + +#[tokio::test] +async fn api_wiring_is_checked_against_openapi() { + let dir = TestDir::new(); + let (mut i, policy, _) = drafted(dir.path(), false).await; + run(&i, &policy).await.unwrap(); + i.action = "status".into(); + i.answers.clear(); + i.api_wiring = vec![ApiWiring { feature: "posts".into(), operation_id: Some("list_posts".into()), defer: false, reason: None }]; + let r = run(&i, &policy).await.unwrap(); + assert!(text(&r["blockers"][0]).contains("openapi.json not found")); + std::fs::write(dir.path().join("openapi.json"), json!({"paths":{"/posts":{"get":{"operationId":"list_posts"}}}}).to_string()).unwrap(); + i.api_wiring.clear(); + let r = run(&i, &policy).await.unwrap(); + assert!(array(&r["blockers"]).is_empty()); + assert_eq!(r["apiWiring"]["wired"][0]["operationId"], "list_posts"); + i.api_wiring = vec![ApiWiring { feature: "posts".into(), operation_id: None, defer: true, reason: Some("fixture for now".into()) }]; + let r = run(&i, &policy).await.unwrap(); + assert!(text(&r["blockers"][0]).starts_with("unfinished")); +} +#[tokio::test] +async fn shared_change_request_roundtrip() { + let dir = TestDir::new(); + let (mut i, policy, _) = drafted(dir.path(), false).await; + run(&i, &policy).await.unwrap(); + i.action = "split".into(); + i.answers.clear(); + run(&i, &policy).await.unwrap(); + i.action = "status".into(); + i.unit_updates = vec![UnitUpdate { id: "posts-front".into(), kind: Some("shared-change-request".into()), file: Some("layout.tsx".into()), reason: Some("needs nav".into()), ..Default::default() }]; + let r = run(&i, &policy).await.unwrap(); + assert_eq!(array(&r["sharedChangeRequests"]).len(), 1); + assert_eq!(r["nextAction"]["arguments"]["action"], "status"); + i.unit_updates = vec![UnitUpdate { id: "posts-front".into(), kind: Some("shared-change-request".into()), file: Some("layout.tsx".into()), state: "resolved".into(), ..Default::default() }]; + let r = run(&i, &policy).await.unwrap(); + assert!(array(&r["sharedChangeRequests"]).is_empty()); +} +#[tokio::test] +async fn erd_required_until_confirmed() { + let dir = TestDir::new(); + let (i, policy, _) = drafted(dir.path(), true).await; + let root = dir.path().to_string_lossy().into_owned(); + assert!(erd_required(&root).is_some()); + run(&i, &policy).await.unwrap(); + assert!(erd_required(&root).is_none()); +} diff --git a/crates/devup-mcp/src/server/project_plan/units.rs b/crates/devup-mcp/src/server/project_plan/units.rs index 3cb9e264..f9cf3bcd 100644 --- a/crates/devup-mcp/src/server/project_plan/units.rs +++ b/crates/devup-mcp/src/server/project_plan/units.rs @@ -9,6 +9,7 @@ fn unit( ) -> Value { json!({"id":id,"scope":scope,"dependencies":dependencies,"acceptance":acceptance, "layer":layer,"state":"pending","verification":[], + "stage":if matches!(layer, "shared" | "models") {0} else {1}, "checklist":["build", "lint", "typecheck", "tests", "devup_ui_validate", "runtime/visual audit", "devup_stack_diff: zero drift"]}) } fn feature_entities(plan: &Value, feature: &Value) -> Vec { @@ -218,9 +219,48 @@ pub(super) fn split( plan["units"] = json!(units); Ok(()) } +pub(super) fn open_requests(plan: &Value) -> Vec { + array(&plan["sharedChangeRequests"]) + .iter() + .filter(|r| r["state"] == "open") + .cloned() + .collect() +} +fn shared_request(plan: &mut Value, update: &UnitUpdate) -> Result<(), DevupError> { + let file = update.file.as_deref().unwrap_or_default().trim(); + if file.is_empty() { + return Err(invalid("shared-change-request needs file")); + } + if !plan["sharedChangeRequests"].is_array() { + plan["sharedChangeRequests"] = json!([]); + } + if update.state == "resolved" { + for r in plan["sharedChangeRequests"].as_array_mut().unwrap() { + if r["file"] == file && r["state"] == "open" { + r["state"] = json!("resolved"); + } + } + for u in plan["units"].as_array_mut().unwrap() { + if u["state"] == "merged" && u["stage"] == 1 { + u["state"] = json!("verifying"); + } + } + return Ok(()); + } + if update.reason.as_deref().is_none_or(|r| r.trim().is_empty()) { + return Err(invalid("shared-change-request needs reason")); + } + plan["sharedChangeRequests"].as_array_mut().unwrap().push(json!({ + "unit":update.id,"file":file,"reason":update.reason,"diff":update.diff,"state":"open"})); + Ok(()) +} pub(super) fn update(input: &ProjectPlanInput, plan: &mut Value) -> Result<(), DevupError> { for update in &input.unit_updates { confirmed(plan)?; + if update.kind.as_deref() == Some("shared-change-request") { + shared_request(plan, update)?; + continue; + } let current = array(&plan["units"]) .iter() .find(|u| u["id"] == update.id) diff --git a/crates/devup-mcp/src/server/project_plan/wiring.rs b/crates/devup-mcp/src/server/project_plan/wiring.rs new file mode 100644 index 00000000..8fc85ef3 --- /dev/null +++ b/crates/devup-mcp/src/server/project_plan/wiring.rs @@ -0,0 +1,128 @@ +use super::*; + +fn openapi_path(root: &str) -> Option { + let base = Path::new(root); + let mut candidates = vec![base.join("openapi.json")]; + if let Ok(entries) = std::fs::read_dir(base.join("apis")) { + candidates.extend( + entries + .filter_map(Result::ok) + .map(|e| e.path().join("openapi.json")), + ); + } + candidates.into_iter().find(|p| p.is_file()) +} + +fn operation_ids(path: &Path) -> std::collections::BTreeSet { + let oversized = std::fs::metadata(path).is_ok_and(|m| m.len() > 16 * 1024 * 1024); + let doc = std::fs::read(path) + .ok() + .filter(|_| !oversized) + .and_then(|bytes| serde_json::from_slice::(&bytes).ok()) + .unwrap_or(Value::Null); + doc["paths"] + .as_object() + .into_iter() + .flatten() + .flat_map(|(_, item)| item.as_object().into_iter().flatten()) + .filter_map(|(_, op)| op["operationId"].as_str().map(str::to_owned)) + .collect() +} + +pub(super) fn record(plan: &mut Value, wiring: &[ApiWiring]) -> Result<(), DevupError> { + for w in wiring { + let known = array(&plan["features"]); + if w.feature.trim().is_empty() || (!known.is_empty() && !known.iter().any(|f| f["id"] == w.feature)) { + return Err(invalid(format!("apiWiring names unknown feature {}", w.feature))); + } + let operation = w.operation_id.as_deref().map(str::trim).unwrap_or_default(); + if w.defer { + if w.reason.as_deref().is_none_or(|r| r.trim().is_empty()) { + return Err(invalid("A deferred API wiring needs a reason; it is reported as unfinished")); + } + } else if operation.is_empty() { + return Err(invalid("apiWiring needs operationId, or defer true with a reason")); + } + if !plan["apiWiring"].is_array() { + plan["apiWiring"] = json!([]); + } + let list = plan["apiWiring"].as_array_mut().unwrap(); + list.retain(|v| v["feature"] != w.feature); + list.push(json!(w)); + } + Ok(()) +} + +pub(super) fn report(plan: &Value, root: &str) -> Value { + let path = openapi_path(root); + let ids = path.as_deref().map(operation_ids).unwrap_or_default(); + let (mut wired, mut missing, mut deferred) = (vec![], vec![], vec![]); + for w in array(&plan["apiWiring"]) { + if w["defer"] == true { + deferred.push(json!({"feature":w["feature"],"reason":w["reason"]})); + } else if ids.contains(text(&w["operationId"])) { + wired.push(json!({"feature":w["feature"],"operationId":w["operationId"]})); + } else { + let why = if path.is_none() { + "openapi.json not found" + } else { + "operationId is not in openapi.json" + }; + missing.push(json!({"feature":w["feature"],"operationId":w["operationId"],"reason":why})); + } + } + json!({"openapi":path.map(|p| p.display().to_string()),"wired":wired,"missing":missing,"deferred":deferred}) +} + +pub(super) fn blockers(plan: &Value, root: &str) -> Vec { + if array(&plan["apiWiring"]).is_empty() { + return vec![]; + } + let r = report(plan, root); + let missing = array(&r["missing"]).iter().map(|m| { + format!( + "apiWiring: {} -> {} ({})", + text(&m["feature"]), + text(&m["operationId"]), + text(&m["reason"]) + ) + }); + let deferred = array(&r["deferred"]).iter().map(|d| { + format!( + "unfinished: {} API wiring deferred ({})", + text(&d["feature"]), + text(&d["reason"]) + ) + }); + missing.chain(deferred).collect() +} + +fn declares(cargo: &str, name: &str) -> bool { + cargo.lines().any(|line| { + line.trim_start().strip_prefix(name).is_some_and(|rest| { + rest.starts_with('.') || rest.trim_start().starts_with('=') + }) + }) +} + +pub(super) fn backend_checks(plan: &Value, root: &str) -> Vec { + let dir = text(&plan["modelsWorkingDirectory"]); + if plan["state"] != "confirmed" || dir.is_empty() { + return vec![]; + } + let base = Path::new(root).join(dir); + let mut found = vec![]; + if let Ok(cargo) = std::fs::read_to_string(base.join("Cargo.toml")) { + for dep in ["serde", "serde_json"] { + if !declares(&cargo, dep) { + found.push(format!("{dir}/Cargo.toml lacks {dep}; exported entities do not compile without it")); + } + } + } + if let Ok(main) = std::fs::read_to_string(base.join("src/main.rs")) + && !main.contains("mod models") + { + found.push(format!("{dir}/src/main.rs lacks `mod models;`; export does not wire src/models")); + } + found +} diff --git a/crates/devup-mcp/src/server/screen_plan/status.rs b/crates/devup-mcp/src/server/screen_plan/status.rs index 3084c11f..37e1e689 100644 --- a/crates/devup-mcp/src/server/screen_plan/status.rs +++ b/crates/devup-mcp/src/server/screen_plan/status.rs @@ -531,6 +531,16 @@ pub(super) fn export_gate(request: &ExportRequest<'_>) -> Result return Ok(Gate::Open), }; + if gated && let Some(erd) = super::project_plan::erd_required(request.project_root) { + return Ok(Gate::Blocked(json!({ + "status": "erd_required", + "written": false, + "erdRequired": {"reason": "The screens read or write data and the project plan ERD is not confirmed.", "details": erd}, + "nextAction": {"tool": "devup_project_plan", + "arguments": {"action": "status", "projectRoot": request.project_root}, + "how": "Confirm the project plan (ERD and endpoint contract) together with the screen plan, generate the API contract, then repeat this export. Nothing was written."}, + }))); + } // Never built, whatever else the call says: not by this export, and not // by an assumption recorded during implementation. let refused = request From ccc67ecf8acaf44c12664000b72b20bf2e700b32 Mon Sep 17 00:00:00 2001 From: leader Date: Thu, 22 Oct 2026 23:23:30 +0900 Subject: [PATCH 007/149] fix(export): oversized metadata under delivery=resource reports envelope fields and a narrowed recovery call --- crates/devup-mcp/src/server/delivery.rs | 45 +++++++++++++++++++ .../server/projection/resource_delivery.rs | 17 ++++++- 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/crates/devup-mcp/src/server/delivery.rs b/crates/devup-mcp/src/server/delivery.rs index b83faacf..12521634 100644 --- a/crates/devup-mcp/src/server/delivery.rs +++ b/crates/devup-mcp/src/server/delivery.rs @@ -275,6 +275,27 @@ pub(super) fn with_inline_recovery(mut error: DevupError, arguments: Value) -> D split.push(serde_json::json!({"tool":"devup_figma_export","arguments":args})); } } + if arguments["delivery"] == "resource" && error.details.get("envelopeFields").is_some() { + let mut narrowed = arguments.clone(); + if let Some(object) = narrowed.as_object_mut() { + object.remove("assetRequests"); + object.insert("responseFields".into(), serde_json::json!(["status", "quality"])); + } + error.details["recoveryState"] = serde_json::json!("available"); + error.details["recoveryReason"] = serde_json::json!( + "resource delivery was already requested; the response metadata itself (see envelopeFields) exceeds the inline limit, so repeating this call fails again." + ); + error.details["nextAction"] = + serde_json::json!({"tool":"devup_figma_export","arguments":narrowed}); + error.details["nextActionReason"] = serde_json::json!( + "Same artifact with responseFields (and outputPaths to write files). Details are then read from the linked compact-details resource. Alternatively export fewer frameIds per call." + ); + error.details["splitRequests"] = serde_json::json!([]); + error.details["splitReason"] = serde_json::json!( + "Split by frameIds (one section frame per call) or write outputs with outputPaths and select only the fields you need with responseFields." + ); + return error; + } error.details["recoveryState"] = serde_json::json!("available"); error.details["recoveryReason"] = serde_json::json!( "The acquisition is retained; resource delivery avoids the inline limit without calling Figma again. Resource storage limits still apply." @@ -356,6 +377,30 @@ mod r12_tests { use super::*; use serde_json::json; + #[test] + fn oversized_envelope_under_resource_delivery_never_repeats_the_failing_call() { + let mut error = choose_delivery( + DeliveryMode::Inline, + &[ProjectedOutput::text( + "tsx", + "text/typescript", + vec![b'x'; MAX_INLINE_TOTAL_BYTES + 1], + )], + ) + .unwrap_err(); + error.details["envelopeFields"] = json!([{"field":"projectionIssues","bytes":1_200_000}]); + let args = json!({"artifactId":"retained","outputs":["tsx"],"delivery":"resource", + "assetRequests":[{"assetId":"a","format":"svg","scale":1,"outputPath":"p.svg"}]}); + let recovered = with_inline_recovery(error, args.clone()); + assert_ne!(recovered.details["nextAction"]["arguments"], args); + assert_eq!( + recovered.details["nextAction"]["arguments"]["responseFields"], + json!(["status", "quality"]) + ); + assert_eq!(recovered.details["splitRequests"], json!([])); + assert!(recovered.details["envelopeFields"].is_array()); + } + #[test] fn r12_asset_mapping_recovery_keeps_coupled_outputs_together() { let output = ProjectedOutput::text( diff --git a/crates/devup-mcp/src/server/projection/resource_delivery.rs b/crates/devup-mcp/src/server/projection/resource_delivery.rs index 2086789d..79e85f18 100644 --- a/crates/devup-mcp/src/server/projection/resource_delivery.rs +++ b/crates/devup-mcp/src/server/projection/resource_delivery.rs @@ -238,10 +238,25 @@ pub(super) async fn apply_delivery( ), ); // Resource promotion must also bound the final metadata envelope. - if let Err(error) = + if let Err(mut error) = choose_delivery_for_result(DeliveryMode::Inline, &Value::Object(result.clone()), &[]) { reservation.rollback(); + let mut sizes = result + .iter() + .map(|(key, value)| (key.clone(), value.to_string().len())) + .collect::>(); + sizes.sort_by(|a, b| b.1.cmp(&a.1)); + sizes.truncate(8); + if !error.details.is_object() { + error.details = json!({}); + } + error.details["envelopeFields"] = json!( + sizes + .into_iter() + .map(|(field, bytes)| json!({"field": field, "bytes": bytes})) + .collect::>() + ); return Err(error); } Ok(Some(DeliveryAttachment { reservation })) From b343e6c815899ec45974e61eaeaab58ada5a71c0 Mon Sep 17 00:00:00 2001 From: leader Date: Thu, 22 Oct 2026 23:25:43 +0900 Subject: [PATCH 008/149] feat(search): warn when a name exists on both an excluded and a normal page --- crates/devup-mcp-figma/src/lib.rs | 2 +- crates/devup-mcp-figma/src/search.rs | 51 +++++++++++++++++++ .../server/projection/project_operation.rs | 2 + 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/crates/devup-mcp-figma/src/lib.rs b/crates/devup-mcp-figma/src/lib.rs index 7a3f3bf5..2ec41fcc 100644 --- a/crates/devup-mcp-figma/src/lib.rs +++ b/crates/devup-mcp-figma/src/lib.rs @@ -63,7 +63,7 @@ pub use payload::{ pub use resources::{ ResourceKind, ResourceOccurrence, ResourceScope, UsedResourceRefs, collect_used_resource_refs, }; -pub use search::{SearchOptions, SearchResult, search_snapshot}; +pub use search::{SearchOptions, SearchResult, excluded_duplicate_warnings, search_snapshot}; pub use section::{ BatchLimits, SectionBatch, SectionCandidate, SectionIndex, SectionSummary, build_section_index, plan_batches, screen_owners, diff --git a/crates/devup-mcp-figma/src/search.rs b/crates/devup-mcp-figma/src/search.rs index 7a646473..df901f41 100644 --- a/crates/devup-mcp-figma/src/search.rs +++ b/crates/devup-mcp-figma/src/search.rs @@ -148,6 +148,32 @@ pub fn search_snapshot( Ok(results) } +/// One warning per name that appears both on an excluded page (trash, archive, +/// PDF copy...) and on a normal page, so callers do not pick the discarded copy. +pub fn excluded_duplicate_warnings(results: &[SearchResult]) -> Vec { + let mut warnings = Vec::new(); + let mut seen = std::collections::BTreeSet::new(); + for excluded in results.iter().filter(|result| result.excluded.is_some()) { + let key = normalize(&excluded.name); + let normal = results + .iter() + .find(|other| other.excluded.is_none() && normalize(&other.name) == key); + if let Some(normal) = normal + && seen.insert(key) + { + warnings.push(format!( + "\"{}\" exists on a normal page ({}, node {}) and on an excluded page ({}, node {}). Prefer the normal one; the excluded copy is trash, archive, draft or an export copy.", + excluded.name, + normal.page_name.as_deref().unwrap_or("?"), + normal.node_id, + excluded.excluded.as_deref().unwrap_or("excluded"), + excluded.node_id, + )); + } + } + warnings +} + fn match_score<'a>( name: &str, query: &str, @@ -437,6 +463,31 @@ mod tests { assert!(results[1].excluded.as_deref().unwrap().contains("휴지통")); } + #[test] + fn a_name_on_both_an_excluded_and_a_normal_page_gets_one_warning() { + let make = |id: &str, page: &str, excluded: Option<&str>| SearchResult { + node_id: id.to_owned(), + name: "특허".to_owned(), + node_type: "FRAME".to_owned(), + page_name: Some(page.to_owned()), + breadcrumb: Vec::new(), + canonical_url: String::new(), + match_kind: "exact".to_owned(), + score: 400, + excluded: excluded.map(str::to_owned), + }; + let results = [ + make("1:2", "디자인 시스템", None), + make("1:1", "휴지통", Some("휴지통 (휴지통)")), + make("1:3", "PDF", Some("pdf (PDF)")), + ]; + let warnings = excluded_duplicate_warnings(&results); + assert_eq!(warnings.len(), 1); + assert!(warnings[0].contains("1:2") && warnings[0].contains("1:1")); + assert!(excluded_duplicate_warnings(&results[..1]).is_empty()); + assert!(excluded_duplicate_warnings(&results[1..]).is_empty()); + } + #[test] fn pdf_and_export_pages_are_flagged_as_excluded() { for page in ["PDF", "저장용", "Export", "출력본"] { diff --git a/crates/devup-mcp/src/server/projection/project_operation.rs b/crates/devup-mcp/src/server/projection/project_operation.rs index 3de92a35..85a037f5 100644 --- a/crates/devup-mcp/src/server/projection/project_operation.rs +++ b/crates/devup-mcp/src/server/projection/project_operation.rs @@ -42,6 +42,7 @@ async fn project_operation( limit, }, )?; + let duplicate_warnings = devup_mcp_figma::excluded_duplicate_warnings(&matches); let quality = OutputQuality { acquisition: acquisition_quality(&completeness_report, true), projection: projection_quality(false, &[]), @@ -54,6 +55,7 @@ async fn project_operation( "query": query, "count": matches.len(), "matches": matches, + "warnings": duplicate_warnings, "completeness": payload.completeness, "collection": collection, "cache": artifact_metadata(artifact), From 839ae2c6aaef8b596f52673575ffa58506516e02 Mon Sep 17 00:00:00 2001 From: leader Date: Thu, 22 Oct 2026 23:30:16 +0900 Subject: [PATCH 009/149] fix(export): envelope spill also covers fields kept inline when nothing else can shrink the response --- .../server/projection/resource_delivery.rs | 41 +++++++++++++++++-- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/crates/devup-mcp/src/server/projection/resource_delivery.rs b/crates/devup-mcp/src/server/projection/resource_delivery.rs index 79e85f18..282f5fbf 100644 --- a/crates/devup-mcp/src/server/projection/resource_delivery.rs +++ b/crates/devup-mcp/src/server/projection/resource_delivery.rs @@ -290,6 +290,15 @@ const KEPT_INLINE: [&str; 10] = [ "envelopeOverflow", ]; +const ESSENTIAL_INLINE: [&str; 6] = [ + "status", + "nextAction", + "resources", + "artifactId", + "outputPaths", + "server", +]; + const HANDLED_LATER: [&str; 4] = ["imageFillAudit", "imageAudit", "assetCssMap", "webReadyImages"]; fn envelope_bytes(result: &Value) -> usize { @@ -319,20 +328,30 @@ fn spill_oversized_fields( return Ok(()); } let mut spilled = serde_json::Map::new(); + // Second pass: when only fields kept inline remain, the final inline check + // would fail the whole call, so they spill too (all but a small core). + let mut strict = false; while envelope_bytes(result) > budget { let largest = result .as_object() .into_iter() .flatten() .filter(|(key, _)| { - !KEPT_INLINE.contains(&key.as_str()) - && !BODY_FIELDS.contains(&key.as_str()) + (if strict { + !ESSENTIAL_INLINE.contains(&key.as_str()) + } else { + !KEPT_INLINE.contains(&key.as_str()) + }) && !BODY_FIELDS.contains(&key.as_str()) && !HANDLED_LATER.contains(&key.as_str()) }) .map(|(key, value)| (key.clone(), value.to_string().len())) .max_by_key(|(_, bytes)| *bytes); let Some((key, bytes)) = largest.filter(|(_, bytes)| *bytes > 1024) else { - break; + if strict { + break; + } + strict = true; + continue; }; let original = result[&key].take(); result[&key] = json!({"state":"resource-details","originalBytes":bytes, @@ -369,3 +388,19 @@ pub(super) fn commit_delivery(attachment: Option) { attachment.reservation.commit(); } } + +#[cfg(test)] +mod spill_tests { + use super::*; + + #[test] + fn kept_inline_fields_spill_when_nothing_else_can_shrink_the_envelope() { + let mut result = json!({"status":"complete","quality":{"blob":"q".repeat(700_000)}}); + let mut outputs = Vec::new(); + spill_oversized_fields(&mut result, &mut outputs, "artifact").unwrap(); + assert_eq!(result["quality"]["state"], "resource-details"); + assert_eq!(result["status"], "complete"); + assert_eq!(outputs.len(), 1); + assert!(envelope_bytes(&result) <= super::super::delivery::MAX_INLINE_TOTAL_BYTES / 2); + } +} From 2accfa6325f82ad33fad9c66bc2c68614e8c8c39 Mon Sep 17 00:00:00 2001 From: leader Date: Thu, 22 Oct 2026 23:31:18 +0900 Subject: [PATCH 010/149] style(export): clippy sort_by_key in envelope field sizing --- crates/devup-mcp/src/server/projection/resource_delivery.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/devup-mcp/src/server/projection/resource_delivery.rs b/crates/devup-mcp/src/server/projection/resource_delivery.rs index 282f5fbf..81d56dc7 100644 --- a/crates/devup-mcp/src/server/projection/resource_delivery.rs +++ b/crates/devup-mcp/src/server/projection/resource_delivery.rs @@ -246,7 +246,7 @@ pub(super) async fn apply_delivery( .iter() .map(|(key, value)| (key.clone(), value.to_string().len())) .collect::>(); - sizes.sort_by(|a, b| b.1.cmp(&a.1)); + sizes.sort_by_key(|a| std::cmp::Reverse(a.1)); sizes.truncate(8); if !error.details.is_object() { error.details = json!({}); From 33d0f9a333e770232892f414a99ce26308de8806 Mon Sep 17 00:00:00 2001 From: leader Date: Thu, 22 Oct 2026 23:38:46 +0900 Subject: [PATCH 011/149] fix(screen-plan): name the real reason when a navigationTargets route is shared; add r14 test --- .../server/screen_plan/plan_route_group.rs | 4 +++ .../frame_inventory_and_link_labels.rs | 26 ++++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/crates/devup-mcp/src/server/screen_plan/plan_route_group.rs b/crates/devup-mcp/src/server/screen_plan/plan_route_group.rs index 23831092..61a1e7e5 100644 --- a/crates/devup-mcp/src/server/screen_plan/plan_route_group.rs +++ b/crates/devup-mcp/src/server/screen_plan/plan_route_group.rs @@ -470,6 +470,10 @@ fn plan_route_group(context: RouteGroupPlanning<'_, '_>, index: usize, screen: & "The evidence names {} different routes.", candidates.iter().cloned().collect::>().join(", ") ) + } else if let Some(route) = candidates.iter().next() { + format!( + "The evidence names {route}, but another screen in this plan names it too, so neither keeps it unasked." + ) } else { "No existing page, nav link, navigationTargets entry or routeAliases entry names it." .to_owned() diff --git a/crates/devup-mcp/src/server/screen_plan_tests/frame_inventory_and_link_labels.rs b/crates/devup-mcp/src/server/screen_plan_tests/frame_inventory_and_link_labels.rs index 018f8932..1112b226 100644 --- a/crates/devup-mcp/src/server/screen_plan_tests/frame_inventory_and_link_labels.rs +++ b/crates/devup-mcp/src/server/screen_plan_tests/frame_inventory_and_link_labels.rs @@ -499,4 +499,28 @@ async fn route_question_offers_state_on_a_similar_page() { routes.iter().any(|route| route["implementation"] == "state"), "{routes:?}" ); -} \ No newline at end of file +} +#[test] +fn r14_navigation_targets_are_routes_not_route_questions() { + let targets = BTreeMap::from([ + ("73:1".to_owned(), "/ko/technology/patents".to_owned()), + ("73:3".to_owned(), "/ko/news/company".to_owned()), + ]); + let plan = draft_plan( + &[ + sectioned("73:1", "Patents", 1440.0, "Tech"), + sectioned("73:3", "Company news", 1440.0, "News"), + ], + &targets, + &evidence(), + json!({}), + None, + ) + .unwrap(); + let rows = array(&plan["screens"]); + assert_eq!(rows[0]["proposedRoute"], "/ko/technology/patents"); + assert_eq!(rows[0]["routeEvidence"], "navigationTargets[73:1]"); + assert_eq!(rows[1]["proposedRoute"], "/ko/news/company"); + let ids = question_ids(&plan); + assert!(!ids.iter().any(|id| id.starts_with("route:")), "{ids:?}"); +} From feeaaa05fce1f61db61aa764a21690d227734b8f Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 16:10:31 +0900 Subject: [PATCH 012/149] test(export): cover feedback 27 resource metadata delivery Existing envelope spill fixes cover the reported single-frame scenario. Assume synthetic 569-issue metadata reproduces its transport pressure without retaining private design data. --- .../server/projection/feedback_theme_tests.rs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/crates/devup-mcp/src/server/projection/feedback_theme_tests.rs b/crates/devup-mcp/src/server/projection/feedback_theme_tests.rs index f839fdb9..c67e56f6 100644 --- a/crates/devup-mcp/src/server/projection/feedback_theme_tests.rs +++ b/crates/devup-mcp/src/server/projection/feedback_theme_tests.rs @@ -109,6 +109,27 @@ async fn feedback_integrated_large_code_hints_theme_and_resources_match_saved_by std::fs::remove_dir_all(directory).unwrap(); } +#[tokio::test] +async fn feedback_27_resource_delivery_spills_large_screen_metadata() { + let data = feedback_theme_payload(); + let store = ArtifactStore::default(); + let key = ArtifactRequestKey::from_collection(&CollectionRequest::new(data.target.clone(), CollectionScope::Node)); + let artifact = store.insert(key, data).await.unwrap(); + let issues = (0..569).map(|index| json!({"code":"DEVUP_CODEGEN_PROPERTY_UNMAPPED","nodeId":format!("45:{index}"),"severity":"warning","fidelityImpact":"none","message":"Review mapping","details":{"originalValue":"x".repeat(2200)}})).collect::>(); + for fields in [vec!["tsx"], vec!["tsx", "devupJson"]] { + let mut result = json!({"status":"partial","tsx":"export const Main = () => ;","projectionIssues":issues,"projectThemeValidation":{"details":"t".repeat(700_000)},"quality":{"projection":"lossy"}}); + if fields.contains(&"devupJson") { result["devupJson"] = json!("{}"); } + let outputs = projected_outputs_from_result(result.as_object().unwrap()).unwrap(); + let args = json!({"artifactId":artifact.artifact_id,"outputs":fields,"delivery":"resource"}); + let attachment = apply_delivery(&mut result, DeliveryMode::Resource, &store, &artifact, outputs, Some(&args)).await.unwrap(); + commit_delivery(attachment); + assert!(result.get("tsx").is_none()); + assert!(serde_json::to_vec(&super::super::tool_result(result.clone())).unwrap().len() <= super::super::delivery::MAX_INLINE_TOTAL_BYTES); + assert!(result["resources"].as_array().unwrap().iter().any(|resource| resource["name"] == "tsx")); + assert!(!store.output_manifests().await.is_empty()); + } +} + #[tokio::test] async fn feedback_large_diagnostic_details_are_complete_bounded_and_reuse_resource_uris() { let data = feedback_theme_payload(); From 91b08cabe5635b55c0e97e3f41219634e119a43b Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 16:13:04 +0900 Subject: [PATCH 013/149] fix(export): report feedback 28 field sizes and retained output bytes Reuse existing narrowed recovery and spill behavior. Assume top eight serialized JSON fields are the useful diagnostic budget; preserve total retained output bytes when checking the resource envelope. --- crates/devup-mcp/src/server/delivery.rs | 29 +++++++++++++++++-- .../server/projection/resource_delivery.rs | 14 ++------- 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/crates/devup-mcp/src/server/delivery.rs b/crates/devup-mcp/src/server/delivery.rs index 12521634..307b66e7 100644 --- a/crates/devup-mcp/src/server/delivery.rs +++ b/crates/devup-mcp/src/server/delivery.rs @@ -203,17 +203,26 @@ pub fn choose_delivery_for_result( || outputs.iter().map(|o| o.bytes.len()).sum::() > MAX_INLINE_TOTAL_BYTES => { - Err(inline_size_error( + let mut error = inline_size_error( outputs, outputs.iter().map(|o| o.bytes.len()).sum(), Some(wire_bytes), - )) + ); + error.details["envelopeFields"] = envelope_field_sizes(result); + Err(error) } DeliveryMode::Inline => Ok(DeliveryDecision { inline: true }), DeliveryMode::Resource => Ok(DeliveryDecision { inline: false }), } } +pub(super) fn envelope_field_sizes(result: &Value) -> Value { + let mut sizes = result.as_object().into_iter().flatten() + .map(|(field, value)| (field, value.to_string().len())).collect::>(); + sizes.sort_by_key(|(_, bytes)| std::cmp::Reverse(*bytes)); + serde_json::json!(sizes.into_iter().take(8).map(|(field, bytes)| serde_json::json!({"field":field,"bytes":bytes})).collect::>()) +} + fn inline_size_error( outputs: &[ProjectedOutput], output_bytes: usize, @@ -377,6 +386,22 @@ mod r12_tests { use super::*; use serde_json::json; + #[test] + fn feedback_28_single_output_error_identifies_metadata_and_recovery() { + let result = json!({"tsx":"","projectThemeValidation":{"details":"x".repeat(800_000)}}); + let outputs = vec![ProjectedOutput::text("tsx", "text/typescript", b"".to_vec())]; + let error = choose_delivery_for_result(DeliveryMode::Inline, &result, &outputs).unwrap_err(); + assert_eq!(error.details["outputBytes"], 6); + assert_eq!(error.details["envelopeFields"][0]["field"], "projectThemeValidation"); + assert!(error.details["envelopeFields"][0]["bytes"].as_u64().unwrap() >= 800_000); + let args = json!({"artifactId":"retained","outputs":["tsx"],"delivery":"resource"}); + let recovered = with_inline_recovery(error, args.clone()); + assert_ne!(recovered.details["nextAction"]["arguments"], args); + assert_eq!(recovered.details["splitRequests"], json!([])); + let reason = recovered.details["splitReason"].as_str().unwrap(); + assert!(reason.contains("outputPaths") && reason.contains("responseFields") && reason.contains("frameIds")); + } + #[test] fn oversized_envelope_under_resource_delivery_never_repeats_the_failing_call() { let mut error = choose_delivery( diff --git a/crates/devup-mcp/src/server/projection/resource_delivery.rs b/crates/devup-mcp/src/server/projection/resource_delivery.rs index 81d56dc7..16c8e772 100644 --- a/crates/devup-mcp/src/server/projection/resource_delivery.rs +++ b/crates/devup-mcp/src/server/projection/resource_delivery.rs @@ -120,6 +120,7 @@ pub(super) async fn apply_delivery( spill_oversized_fields(result, &mut outputs, &artifact.artifact_id)?; let projection_key = projection_key(&outputs); materialize_asset_resource_references(result, &mut outputs, &artifact.artifact_id)?; + let output_bytes = outputs.iter().map(|output| output.bytes.len()).sum::(); let reservation = artifact_store .reserve_outputs(&artifact.artifact_id, &projection_key, outputs) .await?; @@ -242,21 +243,10 @@ pub(super) async fn apply_delivery( choose_delivery_for_result(DeliveryMode::Inline, &Value::Object(result.clone()), &[]) { reservation.rollback(); - let mut sizes = result - .iter() - .map(|(key, value)| (key.clone(), value.to_string().len())) - .collect::>(); - sizes.sort_by_key(|a| std::cmp::Reverse(a.1)); - sizes.truncate(8); if !error.details.is_object() { error.details = json!({}); } - error.details["envelopeFields"] = json!( - sizes - .into_iter() - .map(|(field, bytes)| json!({"field": field, "bytes": bytes})) - .collect::>() - ); + error.details["outputBytes"] = json!(output_bytes); return Err(error); } Ok(Some(DeliveryAttachment { reservation })) From 0170b0dae9f501276c6c1fe5d649b4ae61c7ee62 Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 16:16:58 +0900 Subject: [PATCH 014/149] fix(assets): use readable collision-safe asset names (29) Reuse one stem for identical image hashes across layer roles and preserve variant identity. Refresh codegen snapshots and fixture checksums for kebab-case asset paths; refresh platform float serialization in the diagnostics snapshot. --- .../src/codegen/style/image_fill_source.rs | 150 ++++++++++++------ .../tests/asset_boundaries.rs | 28 ++-- .../tests/cropped_export_background.rs | 2 +- .../tests/folded_asset_size.rs | 5 +- .../tests/free_placement_anchor.rs | 2 +- .../tests/image_asset_names.rs | 33 +++- ...wquw_151__wquw_151_proofread_devup_ui.snap | 6 +- ..._wquw_151_proofread_devup_ui_embedded.snap | 6 +- ...w_151__wquw_151_proofread_diagnostics.snap | 7 +- ...uw_151__wquw_151_proofread_source_map.snap | 6 +- fixtures/devup-figma-plugin/manifest.json | 90 +++++------ .../upstream-codegen-003-c855738695.snap | 3 +- .../upstream-codegen-004-8ec61c5bb5.snap | 3 +- .../upstream-codegen-005-bcc6ca7dbf.snap | 3 +- .../upstream-codegen-006-86c21dc8ce.snap | 3 +- .../upstream-codegen-007-c1c1afa5d7.snap | 3 +- .../upstream-codegen-008-8ea06a6c88.snap | 3 +- .../upstream-codegen-009-008aac1a2c.snap | 3 +- .../upstream-codegen-010-8454d22acd.snap | 3 +- .../upstream-codegen-011-72709aab82.snap | 2 +- .../upstream-codegen-014-828c466260.snap | 2 +- .../upstream-codegen-015-eaef178e47.snap | 2 +- .../upstream-codegen-016-3f1fb1569b.snap | 3 +- .../upstream-codegen-017-44cff97cec.snap | 3 +- .../upstream-codegen-018-bc5eb21a9e.snap | 3 +- .../upstream-codegen-019-4988f86e5e.snap | 2 +- .../upstream-codegen-020-a9d569be7a.snap | 3 +- .../upstream-codegen-114-855686da78.snap | 3 +- .../upstream-codegen-115-45d3f116b5.snap | 3 +- .../upstream-codegen-116-23097e86ce.snap | 3 +- .../upstream-codegen-117-fb967a9342.snap | 3 +- .../upstream-codegen-178-d69d60d688.snap | 2 +- .../upstream-codegen-197-026402bf2e.snap | 2 +- .../upstream-codegen-198-a9ea7ba9c3.snap | 2 +- .../upstream-codegen-199-829a5c949d.snap | 2 +- .../upstream-codegen-200-3313d92719.snap | 2 +- .../upstream-codegen-201-8207a4008e.snap | 2 +- .../upstream-codegen-211-2e15c55701.snap | 2 +- .../upstream-codegen-212-2a8f7b4a99.snap | 2 +- .../upstream-codegen-218-587666c709.snap | 2 +- .../upstream-codegen-219-c6446b7b2e.snap | 2 +- .../upstream-codegen-220-5980153f26.snap | 2 +- .../upstream-codegen-223-4dc8092992.snap | 6 +- .../upstream-codegen-224-075a41e65c.snap | 2 +- .../upstream-codegen-225-00fb8a129e.snap | 2 +- .../upstream-codegen-226-b2c28bcc8f.snap | 2 +- .../upstream-codegen-233-0a60be5fbc.snap | 2 +- .../upstream-codegen-234-01ad9a0db0.snap | 2 +- .../upstream-codegen-235-8a3992468b.snap | 2 +- .../upstream-codegen-246-295f39e09b.snap | 2 +- .../upstream-codegen-251-50acddfb96.snap | 8 +- .../upstream-codegen-252-49e979239e.snap | 8 +- .../upstream-codegen-253-347a2e5fb2.snap | 4 +- .../upstream-codegen-254-9f340b6a10.snap | 2 +- .../upstream-codegen-255-d01d8c7de6.snap | 2 +- .../upstream-codegen-256-baf32cc08e.snap | 2 +- 56 files changed, 278 insertions(+), 181 deletions(-) diff --git a/crates/devup-mcp-devup-ui/src/codegen/style/image_fill_source.rs b/crates/devup-mcp-devup-ui/src/codegen/style/image_fill_source.rs index d6f9aa8a..a4511bb4 100644 --- a/crates/devup-mcp-devup-ui/src/codegen/style/image_fill_source.rs +++ b/crates/devup-mcp-devup-ui/src/codegen/style/image_fill_source.rs @@ -91,45 +91,107 @@ pub fn image_fill_path( }) } -/// The file name an asset node gets, without folder or extension. -/// -/// The layer name, unless another asset in the snapshot has the same name -/// and is a different thing. Three cards each hold an `Icons` instance at a -/// different variant - chart, clock, lightning - and all three were -/// `/icons/Icons.svg`, one file overwriting the next, and every card drew the -/// chart. An instance whose name is shared then carries its variant, `Icons=chart`; -/// a node that is not an instance carries its id. Instances of one variant -/// share a name and a file, as the same icon at three widths should. +/// The file name an asset node gets, without folder or extension: readable +/// kebab-case; distinct things sharing a name get -2, -3 in node-id order. pub(crate) fn asset_stem(snapshot: &Snapshot, node: &RawNode, per_node: bool) -> String { - let view = node.typed_view(); - let name = layer_name(node); - let name = name.as_str(); - let identity = asset_identity(node); - let raster_instance = - view.node_type() == "INSTANCE" && asset_kind(snapshot, node) == Some(AssetKind::Png); - let shared = per_node - || snapshot.nodes.values().any(|other| { - other.id != node.id - && layer_name(other) == name - && (asset_identity(other) != identity - || raster_instance - || (fills(node).is_some_and(|fs| fs.iter().any(|p| p["type"] == "IMAGE")) - && fills(other) != fills(node))) - && asset_kind(snapshot, other).is_some() - }); - if !shared { - return name.to_owned(); + let canonical = image_hash(node).and_then(|hash| { + snapshot.nodes.values().filter(|other| image_hash(other) == Some(hash)).min_by_key(|other| &other.id) + }).unwrap_or(node); + let base = readable_stem(snapshot, canonical); + let own = asset_group_key(snapshot, node, per_node); + let mut firsts = std::collections::BTreeMap::::new(); + for other in snapshot.nodes.values() { + let is_asset = asset_kind(snapshot, other).is_some() || image_hash(other).is_some(); + let same_image = image_hash(node).is_some_and(|hash| image_hash(other) == Some(hash)); + if other.id != node.id && (!is_asset || (!same_image && readable_stem(snapshot, other) != base)) { + continue; + } + let first = firsts + .entry(asset_group_key(snapshot, other, per_node)) + .or_insert(other.id.as_str()); + if other.id.as_str() < *first { + *first = other.id.as_str(); + } + } + let mut ids = firsts.values().copied().collect::>(); + ids.sort_unstable(); + let own_first = firsts.get(&own).copied().unwrap_or(node.id.as_str()); + match ids.iter().position(|id| *id == own_first) { + Some(index) if index > 0 => format!("{base}-{}", index + 1), + _ => base, + } +} + +/// Lowercase `[a-z0-9-]` only, hyphens collapsed, at most 48 characters. +fn kebab_name(name: &str) -> String { + let mut slug = String::new(); + for c in name.chars() { + if c.is_ascii_alphanumeric() { + slug.push(c.to_ascii_lowercase()); + } else if !slug.is_empty() && !slug.ends_with('-') { + slug.push('-'); + } + } + slug.truncate(48); + slug.trim_end_matches('-').to_owned() +} + +fn generic_role(role: &str) -> bool { + let word = role.trim_end_matches(|c: char| c.is_ascii_digit() || c == '-'); + word.is_empty() + || matches!( + word, + "rectangle" | "image" | "frame" | "vector" | "group" | "ellipse" | "picture" | "layer" + ) +} + +/// The layer name as a role, prefixed by the nearest meaningful ancestor when +/// the layer name is generic, and by its variant when it is a variant instance. +fn readable_stem(snapshot: &Snapshot, node: &RawNode) -> String { + let role = kebab_name(&layer_name(node)); + if !generic_role(&role) { + return role; } - match identity { - // Raster instances can override the image, crop, size and effects while - // retaining the same component variant. Only verified exported bytes - // establish reuse; a shared variant name cannot establish identity. - Some(variant) if raster_instance => { - format!("{name}={variant}-{}", node.id.replace([':', ';'], "-")) + let mut current = node.typed_view().string("parentId").map(str::to_owned); + let mut hops = 0; + while let Some(id) = current { + hops += 1; + let Some(parent) = snapshot.nodes.get(&id).filter(|_| hops <= 32) else { + break; + }; + let section = kebab_name(&layer_name(parent)); + if !generic_role(§ion) { + let word = if role.is_empty() { "image" } else { &role }; + return format!("{section}-{word}"); } - Some(variant) => format!("{name}={variant}"), - None => format!("{name}-{}", node.id.replace([':', ';'], "-")), + current = parent.typed_view().string("parentId").map(str::to_owned); } + if role.is_empty() { "asset".to_owned() } else { role } +} + +fn image_hash(node: &RawNode) -> Option<&str> { + fills(node)? + .iter() + .find(|p| p["type"] == "IMAGE" && p["visible"].as_bool() != Some(false))?["imageHash"] + .as_str() +} + +/// What makes two assets the same file: identical image bytes, else the same +/// variant; raster instances (which may override image/crop) and per-node +/// naming stay one file per node. +fn asset_group_key(snapshot: &Snapshot, node: &RawNode, per_node: bool) -> String { + if let Some(hash) = image_hash(node) { + return format!("hash:{hash}"); + } + let raster_instance = node.typed_view().node_type() == "INSTANCE" + && asset_kind(snapshot, node) == Some(AssetKind::Png); + if per_node || raster_instance { + return format!("node:{}", node.id); + } + if let Some(properties) = node.typed_view().value("variantProperties").and_then(Value::as_object) { + return format!("variant:{}", serde_json::to_string(properties).unwrap_or_default()); + } + "plain".to_owned() } /// The layer name with HTML entities decoded: Figma text may arrive as @@ -146,18 +208,6 @@ fn layer_name(node: &RawNode) -> String { .replace(['(', ')'], "") } -/// What makes an instance the thing it is: its variant, as `chart` or -/// `lg,primary`. `None` for a node that is not an instance of a variant. -fn asset_identity(node: &RawNode) -> Option { - let view = node.typed_view(); - let properties = view.value("variantProperties")?.as_object()?; - let values = properties - .values() - .filter_map(Value::as_str) - .collect::>(); - (!values.is_empty()).then(|| values.join(",")) -} - pub(super) fn paint_css( snapshot: &Snapshot, node: &RawNode, @@ -211,9 +261,7 @@ pub(super) fn paint_css( "PATTERN" => { let source_id = paint.get("sourceNodeId").and_then(Value::as_str)?; let source = snapshot.nodes.get(source_id); - let name = source - .and_then(|node| node.typed_view().name()) - .unwrap_or("pattern"); + let name = source.map_or_else(|| "pattern".to_owned(), |node| asset_stem(snapshot, node, per_node)); // A raster belongs with the images and a vector with the icons, // which is the split every other asset reference follows. This one // sent a png to the icon folder. @@ -940,4 +988,4 @@ fn push_strokes( string_prop(props, "outlineOffset", px(-weight / 2.0)); } } -} \ No newline at end of file +} diff --git a/crates/devup-mcp-devup-ui/tests/asset_boundaries.rs b/crates/devup-mcp-devup-ui/tests/asset_boundaries.rs index 209cfcd6..6b721bff 100644 --- a/crates/devup-mcp-devup-ui/tests/asset_boundaries.rs +++ b/crates/devup-mcp-devup-ui/tests/asset_boundaries.rs @@ -47,7 +47,7 @@ fn a_folded_asset_does_not_anchor_children_it_no_longer_has() { ); assert!( - tsx.contains("/icons/clear button.svg"), + tsx.contains("/icons/clear-button.svg"), "expected a folded asset: {tsx}" ); assert!( @@ -89,10 +89,10 @@ fn a_raster_pattern_is_referenced_from_the_image_folder() { ); assert!( - tsx.contains("/images/Tile.png"), + tsx.contains("/images/tile.png"), "a raster pattern is an image: {tsx}" ); - assert!(!tsx.contains("/icons/Tile"), "{tsx}"); + assert!(!tsx.contains("/icons/tile"), "{tsx}"); } #[test] @@ -116,8 +116,8 @@ fn separate_image_fills_do_not_claim_the_same_file() { }]), ); - assert!(tsx.contains("/images/Card.png"), "{tsx}"); - assert!(tsx.contains("/images/Card-1.png"), "{tsx}"); + assert!(tsx.contains("/images/card.png"), "{tsx}"); + assert!(tsx.contains("/images/card-1.png"), "{tsx}"); } #[test] @@ -147,7 +147,7 @@ fn image_filled_asset_container_preserves_text_children() { // The fill names the node it came from, so two different images cannot // claim the same file. An original-source paint is an image layer below // live text, rather than a flattened export of the entire container. - assert!(tsx.contains("src=\"/images/Book cover.png\"")); + assert!(tsx.contains("src=\"/images/book-cover.png\"")); assert!(tsx.contains("objectFit=\"cover\"")); assert!(tsx.contains("Preserved title")); assert!(tsx.contains("\n \n " + " \n \n " ], "fallback": null, "heightPreservation": { @@ -552,7 +551,7 @@ expression: json!(output.diagnostics) "color": { "b": 0.9586742520332336, "g": 0.96465665102005, - "r": 0.9695513844490051 + "r": 0.9695513844490052 }, "opacity": 1, "type": "SOLID", diff --git a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151__wquw_151_proofread_source_map.snap b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151__wquw_151_proofread_source_map.snap index cd412993..f11b2f46 100644 --- a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151__wquw_151_proofread_source_map.snap +++ b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151__wquw_151_proofread_source_map.snap @@ -767,7 +767,7 @@ expression: output.source_map "elementIndex": 13 }, { - "generatedProperty": "maskImage=\"url(/icons/Icons.svg)\"", + "generatedProperty": "maskImage=\"url(/icons/icons.svg)\"", "nodeId": "3879:35530", "property": "node", "assetId": "3879:35530:node", @@ -933,7 +933,7 @@ expression: output.source_map "elementIndex": 16 }, { - "generatedProperty": "maskImage=\"url(/icons/icons.svg)\"", + "generatedProperty": "maskImage=\"url(/icons/icons-2.svg)\"", "nodeId": "3879:35534", "property": "node", "assetId": "3879:35534:node", @@ -1486,7 +1486,7 @@ expression: output.source_map "elementIndex": 28 }, { - "generatedProperty": "maskImage=\"url(/icons/Icons.svg)\"", + "generatedProperty": "maskImage=\"url(/icons/icons.svg)\"", "nodeId": "3879:35541", "property": "node", "assetId": "3879:35541:node", diff --git a/fixtures/devup-figma-plugin/manifest.json b/fixtures/devup-figma-plugin/manifest.json index 7456ea0e..bf95f250 100644 --- a/fixtures/devup-figma-plugin/manifest.json +++ b/fixtures/devup-figma-plugin/manifest.json @@ -1155,39 +1155,39 @@ }, { "path": "snapshots/codegen/upstream-codegen-003-c855738695.snap", - "sha256": "acd5b0205a68855ab2e782dd30a6685e30d5ac362040222d61d6edbcca04772f" + "sha256": "107a4fc6a1b284870aafa9c796460cd324913c851929dc2c1bd134626d807c8d" }, { "path": "snapshots/codegen/upstream-codegen-004-8ec61c5bb5.snap", - "sha256": "8314127075365a113c3df473a3f5ec8fb20aca5b4a0f875a9b63ee34623cdcb3" + "sha256": "0d73ed65e694b49b9210a486ac7319bcce42cebce52d6240e4f61c7f875c6d41" }, { "path": "snapshots/codegen/upstream-codegen-005-bcc6ca7dbf.snap", - "sha256": "54552151e492a51cd340f93d7af6eb0c00e4f4c3813dd14d129497a9426e7952" + "sha256": "ecc94354d07867bc6371171d6c06ed469277bee57c3e56782c289ca0bf590853" }, { "path": "snapshots/codegen/upstream-codegen-006-86c21dc8ce.snap", - "sha256": "0384a80958bc15f7989187079140b6e588e15b1d4ae2978a4e803b35ff0d361b" + "sha256": "21b2dd6a1752fee5498db1b57e36cb55fb1610f1b7447e087400c27939c87ac8" }, { "path": "snapshots/codegen/upstream-codegen-007-c1c1afa5d7.snap", - "sha256": "a5d038bd436c2cb1b57565a33f28eead49b74d652247635cba10b7a48b47c1e3" + "sha256": "ec12b4610d766fcba77e461b3e2ec8b5f38adfb7ed44efe763d887521be9280b" }, { "path": "snapshots/codegen/upstream-codegen-008-8ea06a6c88.snap", - "sha256": "e13ad11d4552822b9d3ed37f1e0f7e161cb8e9185b9538accc7449aa420fc0a1" + "sha256": "89609fae76c848720a6cfea26851576f3026d9a336e7f9cc578f1c0d955dce4b" }, { "path": "snapshots/codegen/upstream-codegen-009-008aac1a2c.snap", - "sha256": "94ad56cf6834d126be439d85ccc36f1fc0a5ef27e8d6279f08e956df90757209" + "sha256": "c063e2b0cc76a83849f174392fd91809fbb21fb17e9f9e241c28291c0d04dedc" }, { "path": "snapshots/codegen/upstream-codegen-010-8454d22acd.snap", - "sha256": "ed28107d0395a3b56a7f3736881ef59adf3e1a8003c9edeec6e269b620a0c76a" + "sha256": "552b628a81fa60e52412c75b1a941b992262414de6f155ba48bdbcda6d5b4297" }, { "path": "snapshots/codegen/upstream-codegen-011-72709aab82.snap", - "sha256": "6eff4a88db6b8b3a9c42081f73f6ae27676e9d43af62b0c82b5f6b068cedce06" + "sha256": "8ec53168a0cccd781bf1ac67036411a7c07e29a8c63d640fed23c96981e0a1ae" }, { "path": "snapshots/codegen/upstream-codegen-012-92c671c0e8.snap", @@ -1199,31 +1199,31 @@ }, { "path": "snapshots/codegen/upstream-codegen-014-828c466260.snap", - "sha256": "c3bcaf8c96412e8567c3980993f92c2ea3c14a5ab035bac3beb9f6f669ac4087" + "sha256": "483d70e11733204a29c32147f7cd2ccd4f0b9b863a4da27969e78c3e84e9d565" }, { "path": "snapshots/codegen/upstream-codegen-015-eaef178e47.snap", - "sha256": "9ebf331a7e8200c7b9a1814f1ae8b1122eb4551e8d4e6deb9dd8c7277bdb46c7" + "sha256": "02ae34d7f4a46b6fda2014e5dc075015e996a8b9fe35ace48898d5015f38fadd" }, { "path": "snapshots/codegen/upstream-codegen-016-3f1fb1569b.snap", - "sha256": "a8892d1b1fdb9f1d994753cbc61409be52a15c100d8c1cc5c5801c3c9bdcf55e" + "sha256": "bc5741f520fe3197c9cd6a5391d918c099e7e6f8c624402aa3c1ebeafd3bdd04" }, { "path": "snapshots/codegen/upstream-codegen-017-44cff97cec.snap", - "sha256": "0272e228a33d3fac1d2f0461a9e1c3527ae325617c8ed6cddeb34cb75e39a4b3" + "sha256": "fcaf1f4e3df1b2d0dca35d19c683a8c22fba0fc4d027b923fa9af35ca9e1005a" }, { "path": "snapshots/codegen/upstream-codegen-018-bc5eb21a9e.snap", - "sha256": "d08676bdbc1f3db4cb146bebced45ffa57a91e2ff90cb139fa623d9c76a54687" + "sha256": "fe08bdb7d8073a9bee0f3323c020196d3a4bfed9e413fdc9fa651ba71ecf81e2" }, { "path": "snapshots/codegen/upstream-codegen-019-4988f86e5e.snap", - "sha256": "87341ff18e5f3bf32f73e19f018b07eeb38251b5842afe3e295896ba6de3c3cc" + "sha256": "1f433f663c2b662d72a816b6679de1cd9be5f6e81d201091b0d9648731b8712c" }, { "path": "snapshots/codegen/upstream-codegen-020-a9d569be7a.snap", - "sha256": "b2780095f9c3e2a3f48d281777525db85345bad9db4a09e906efc0c6773beaec" + "sha256": "41323d03429d53d97d8bd555a167508bbf115ec41ecd7a41ae3193dcf25cbbdb" }, { "path": "snapshots/codegen/upstream-codegen-021-6cae93b843.snap", @@ -1599,19 +1599,19 @@ }, { "path": "snapshots/codegen/upstream-codegen-114-855686da78.snap", - "sha256": "d43e7666cd5f314a3573e54caa58bb6034920436133d2c52025afa534b23cb19" + "sha256": "b2164313aceeea5a60940c521ac6870e66e42b9ea555653aa22cb18485c2072b" }, { "path": "snapshots/codegen/upstream-codegen-115-45d3f116b5.snap", - "sha256": "f6c114f58604fe47fcb367de81e3671cb36786200760b982bb2cbb249b666397" + "sha256": "b3210c264234ef387d9188855c71335c0997cf2d3de10b819aa751b90573cbb0" }, { "path": "snapshots/codegen/upstream-codegen-116-23097e86ce.snap", - "sha256": "fea411418088c9df01e9eb22781c903ea8fc19ae0945651bd54aa0f278ccb590" + "sha256": "7b7feb09b06dbf6e548fb8716fbb9f5b861d37690abc051b5e2b4e0cbf0985d0" }, { "path": "snapshots/codegen/upstream-codegen-117-fb967a9342.snap", - "sha256": "464be3eb15026d43038048a91270ca974e66f1ea74b5bf4eb04c7b1af71235d0" + "sha256": "9ec79d411082b128c607328fd2dcc871e85000c6b49ed782fe6a8856ee494a96" }, { "path": "snapshots/codegen/upstream-codegen-118-b5cba733fa.snap", @@ -1855,7 +1855,7 @@ }, { "path": "snapshots/codegen/upstream-codegen-178-d69d60d688.snap", - "sha256": "5a43c874067ff83a73b9c36a3340f7dfaa1157ed5dc424f9a0fb911742ee74b2" + "sha256": "97fa189330dde2140571dbc828d4b0a08146d203e5df2e0db8fcc639b6eb653a" }, { "path": "snapshots/codegen/upstream-codegen-179-80620f6587.snap", @@ -1931,23 +1931,23 @@ }, { "path": "snapshots/codegen/upstream-codegen-197-026402bf2e.snap", - "sha256": "ad451c6af1a8ebcd17c8e2026e52b7b3af7e82c63c5693ec1d0ba1494089d98a" + "sha256": "7c8f2764bff6a4ed31e33fab0c26ce0066a0efe3e3cd75db049d4f21be65b2fd" }, { "path": "snapshots/codegen/upstream-codegen-198-a9ea7ba9c3.snap", - "sha256": "20da2e92f127b61190a1ffcbfc3c1b1dd95276dcca09bee0e6ea32a6f6b03c28" + "sha256": "bc3d85601a1e694f99ed182735024d9969cc52b16f8226f50340e7ae75e4f7d5" }, { "path": "snapshots/codegen/upstream-codegen-199-829a5c949d.snap", - "sha256": "bea1b2880c625488b30adfd1cdcdd428e71701077c7f202cc7b5b3ff2ff1c3ae" + "sha256": "4b28a55ece68dd455db01b4b93701786b758077242ffb180b0e1014a0f0eefbb" }, { "path": "snapshots/codegen/upstream-codegen-200-3313d92719.snap", - "sha256": "4bd6a80c50727d816c221b88a36a0a3d9f399bd915babfd032bbf51a8167a499" + "sha256": "0eb6f43dffe7a9365611a48ac1627d8a65dca4e0e1f1d08cf6ced74a4f85eb1a" }, { "path": "snapshots/codegen/upstream-codegen-201-8207a4008e.snap", - "sha256": "4853047bb5ecd75e3ae2a3faf1cae3cdac7ebc94c2321a6b5a590b6d5ba8271e" + "sha256": "9bcae01c5c470c188a4f735de6f60cdf4958f7949b82bcc84052b61782146e2d" }, { "path": "snapshots/codegen/upstream-codegen-202-2786eda075.snap", @@ -1987,11 +1987,11 @@ }, { "path": "snapshots/codegen/upstream-codegen-211-2e15c55701.snap", - "sha256": "aa9b329626bf2650dfbdf1a6daefde961f5db46e6a8bbb1e3c2cfc412577e6b4" + "sha256": "462e2966e427fb10648658105eb0a85b8604d01e4c24cb799a385b6aa789b5a1" }, { "path": "snapshots/codegen/upstream-codegen-212-2a8f7b4a99.snap", - "sha256": "ecc08d52f912688e094e0ecdd4a5c0b55efe944c3d9a2af07e33c7a770b30d76" + "sha256": "5fec63dd4ce0f59ebfb068894cdc687735d1ae065beae40a0f93841d0b816861" }, { "path": "snapshots/codegen/upstream-codegen-213-aa53408d2f.snap", @@ -2015,15 +2015,15 @@ }, { "path": "snapshots/codegen/upstream-codegen-218-587666c709.snap", - "sha256": "91e418f1323bc800f6e92d563ad2d1dbd29090efcc1a56d2d6945f253d7a2e78" + "sha256": "c8e5a180a98cb5235867c2dbc48461bc87c2f6458b788cdf588d1556daeec89d" }, { "path": "snapshots/codegen/upstream-codegen-219-c6446b7b2e.snap", - "sha256": "009f69ca7cf7ad09865ef65059896ae95527f8a1eb908efe8573f03f486e8e35" + "sha256": "41201d7901fc28c3671bf916c7524586833b26bae44fc88685a95896a421f8b7" }, { "path": "snapshots/codegen/upstream-codegen-220-5980153f26.snap", - "sha256": "5bbfaca46482fe5a8b17e02693b29b26ca02b975cdb6da683ba6739afd33adc7" + "sha256": "ba96caf7143dadc73fb3f9367d9c89f04e344550cdc3b5c1803787a05f0aedbe" }, { "path": "snapshots/codegen/upstream-codegen-221-d003bdb336.snap", @@ -2035,19 +2035,19 @@ }, { "path": "snapshots/codegen/upstream-codegen-223-4dc8092992.snap", - "sha256": "1930db8d9deea8afe86275e0c99c6426143e2c56536448b1a36f61e1b64dd83c" + "sha256": "3ae6fed5cb6f059aa6656579ba90c3e349096960985f32bef47ca79ca710b1c1" }, { "path": "snapshots/codegen/upstream-codegen-224-075a41e65c.snap", - "sha256": "2f3182b075c68bd00f6b3b188bc6e88d24eea76b0fd60afac1025952191a951b" + "sha256": "124e3e0569bd5f2d381454383f63db283d30769189fd284e0d92005f7a7384b1" }, { "path": "snapshots/codegen/upstream-codegen-225-00fb8a129e.snap", - "sha256": "8a4e4ad291834d0f48250da3c6893b19ba8ebd00af9e2f3a28531c03dcd7ad4a" + "sha256": "2c338677e76d9879d4b092a8bb1f693933c71fefa23af6ba46a0978cedc35ab4" }, { "path": "snapshots/codegen/upstream-codegen-226-b2c28bcc8f.snap", - "sha256": "8a4e4ad291834d0f48250da3c6893b19ba8ebd00af9e2f3a28531c03dcd7ad4a" + "sha256": "2c338677e76d9879d4b092a8bb1f693933c71fefa23af6ba46a0978cedc35ab4" }, { "path": "snapshots/codegen/upstream-codegen-227-bebc4e7262.snap", @@ -2075,15 +2075,15 @@ }, { "path": "snapshots/codegen/upstream-codegen-233-0a60be5fbc.snap", - "sha256": "cd49878d5f8f07b0b7b5a39c049afdc6ac345deda3c92a0cd6bfbf5110369579" + "sha256": "bf84ce5b230154bbc74fa82cb42852c364e3cb6d68ed3b9521848c4e2386e1b3" }, { "path": "snapshots/codegen/upstream-codegen-234-01ad9a0db0.snap", - "sha256": "66642a3009a10922e11d93ce26e7eb2401b42e13833fa0c5be3c35adf3bd97f5" + "sha256": "85f92c032e9fe4c9420fd8873b1650226b18b88d5a2606d637076144fdb71b9e" }, { "path": "snapshots/codegen/upstream-codegen-235-8a3992468b.snap", - "sha256": "66642a3009a10922e11d93ce26e7eb2401b42e13833fa0c5be3c35adf3bd97f5" + "sha256": "85f92c032e9fe4c9420fd8873b1650226b18b88d5a2606d637076144fdb71b9e" }, { "path": "snapshots/codegen/upstream-codegen-236-621c6d6a9e.snap", @@ -2127,7 +2127,7 @@ }, { "path": "snapshots/codegen/upstream-codegen-246-295f39e09b.snap", - "sha256": "6ee6e667f7d635bfaf3542b6e8d405178d30fe2fbee618ca8303155c8f01809a" + "sha256": "b6f9e888c38ec2bc6fdf0768bb180c78d25a8be2425f024779f7bc2ab612c58c" }, { "path": "snapshots/codegen/upstream-codegen-247-39d6959685.snap", @@ -2147,27 +2147,27 @@ }, { "path": "snapshots/codegen/upstream-codegen-251-50acddfb96.snap", - "sha256": "c610358b3d7fdc43beccfb47cadd798f38449ce5f9076503e9f7b9a3be48220d" + "sha256": "547561fd248270cbccd3311714de36a24de9aed0449e854354c64140108abb2d" }, { "path": "snapshots/codegen/upstream-codegen-252-49e979239e.snap", - "sha256": "20c02f9d2c6a975c791717cb81b4b4c560cf8acfebacec73fb7fc032a5cf8d51" + "sha256": "1ca180d276bdf67a040bc23b7dd699144726821980c0bd48233471983f5b8b17" }, { "path": "snapshots/codegen/upstream-codegen-253-347a2e5fb2.snap", - "sha256": "ec6169daeaa7bacd7d5b2f388ff915cc84bb7c626bb9a92a86f04b3964231f1f" + "sha256": "82942a09319540cf7c3cd24d3fc54d97c41933e259df11996c4a9878eee891e5" }, { "path": "snapshots/codegen/upstream-codegen-254-9f340b6a10.snap", - "sha256": "ea2b36c20447fdd34bd21790a63a31ce9e50dd41b6bc8a23a1604309fe28dcb2" + "sha256": "aa2709664a7c01f96246afa0cd0f90fd554cffcd2a33e7da5889818504327508" }, { "path": "snapshots/codegen/upstream-codegen-255-d01d8c7de6.snap", - "sha256": "54e77dd8979feb1cec3f1636febe419c07cb40e7b0427181e81eaab0aa680557" + "sha256": "9026ef8c9d744cbb366022395594a921ad0d18532f8d5ddf71349114f2ea1145" }, { "path": "snapshots/codegen/upstream-codegen-256-baf32cc08e.snap", - "sha256": "2a20b5cf22c768e86ff5f73a0aa3f522fdee78956af1dc38f90a5cba99a25255" + "sha256": "92234ca40a31516848fe28d36f90d102158a945d60ca4278d08ac0b165303616" }, { "path": "snapshots/codegen/upstream-root-257-f23261dd4f.snap", diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-003-c855738695.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-003-c855738695.snap index d2f23b89..c891c44a 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-003-c855738695.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-003-c855738695.snap @@ -2,6 +2,7 @@ source: crates/devup-mcp-devup-ui/tests/compat_fixtures.rs expression: actual --- + "" diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-004-8ec61c5bb5.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-004-8ec61c5bb5.snap index cea9184c..b563e5f9 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-004-8ec61c5bb5.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-004-8ec61c5bb5.snap @@ -2,11 +2,12 @@ source: crates/devup-mcp-devup-ui/tests/compat_fixtures.rs expression: actual --- + "" diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-005-bcc6ca7dbf.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-005-bcc6ca7dbf.snap index ada61551..84a1a3f7 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-005-bcc6ca7dbf.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-005-bcc6ca7dbf.snap @@ -2,6 +2,7 @@ source: crates/devup-mcp-devup-ui/tests/compat_fixtures.rs expression: actual --- + "" diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-006-86c21dc8ce.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-006-86c21dc8ce.snap index bb277190..21fc78b3 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-006-86c21dc8ce.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-006-86c21dc8ce.snap @@ -2,4 +2,5 @@ source: crates/devup-mcp-devup-ui/tests/compat_fixtures.rs expression: actual --- -"" + +"" diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-007-c1c1afa5d7.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-007-c1c1afa5d7.snap index e7d7f94e..ef342d41 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-007-c1c1afa5d7.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-007-c1c1afa5d7.snap @@ -2,4 +2,5 @@ source: crates/devup-mcp-devup-ui/tests/compat_fixtures.rs expression: actual --- -"" + +"" diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-008-8ea06a6c88.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-008-8ea06a6c88.snap index 3123fe90..3bebd24d 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-008-8ea06a6c88.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-008-8ea06a6c88.snap @@ -2,4 +2,5 @@ source: crates/devup-mcp-devup-ui/tests/compat_fixtures.rs expression: actual --- -"" + +"" diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-009-008aac1a2c.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-009-008aac1a2c.snap index 028ba62a..63ca01e8 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-009-008aac1a2c.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-009-008aac1a2c.snap @@ -2,4 +2,5 @@ source: crates/devup-mcp-devup-ui/tests/compat_fixtures.rs expression: actual --- -"" + +"" diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-010-8454d22acd.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-010-8454d22acd.snap index 53a8e091..e3acc022 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-010-8454d22acd.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-010-8454d22acd.snap @@ -2,4 +2,5 @@ source: crates/devup-mcp-devup-ui/tests/compat_fixtures.rs expression: actual --- -"" + +"" diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-011-72709aab82.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-011-72709aab82.snap index 7be839c3..6ba44594 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-011-72709aab82.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-011-72709aab82.snap @@ -4,5 +4,5 @@ expression: actual --- " - + " diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-014-828c466260.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-014-828c466260.snap index e73f706d..25bdd7e3 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-014-828c466260.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-014-828c466260.snap @@ -6,7 +6,7 @@ expression: actual "" + +"" diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-017-44cff97cec.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-017-44cff97cec.snap index 199be568..239fb7fe 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-017-44cff97cec.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-017-44cff97cec.snap @@ -2,4 +2,5 @@ source: crates/devup-mcp-devup-ui/tests/compat_fixtures.rs expression: actual --- -"" + +"" diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-018-bc5eb21a9e.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-018-bc5eb21a9e.snap index c9690e01..2399b25b 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-018-bc5eb21a9e.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-018-bc5eb21a9e.snap @@ -2,4 +2,5 @@ source: crates/devup-mcp-devup-ui/tests/compat_fixtures.rs expression: actual --- -"" + +"" diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-019-4988f86e5e.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-019-4988f86e5e.snap index c6db31b4..503ae818 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-019-4988f86e5e.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-019-4988f86e5e.snap @@ -6,7 +6,7 @@ expression: actual "" + +"" diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-114-855686da78.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-114-855686da78.snap index 802280f0..a063712b 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-114-855686da78.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-114-855686da78.snap @@ -2,9 +2,10 @@ source: crates/devup-mcp-devup-ui/tests/compat_fixtures.rs expression: actual --- + " " + +"" diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-116-23097e86ce.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-116-23097e86ce.snap index 0bd949b4..8428239f 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-116-23097e86ce.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-116-23097e86ce.snap @@ -2,4 +2,5 @@ source: crates/devup-mcp-devup-ui/tests/compat_fixtures.rs expression: actual --- -"" + +"" diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-117-fb967a9342.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-117-fb967a9342.snap index 368d45f7..58e9e2e5 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-117-fb967a9342.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-117-fb967a9342.snap @@ -2,4 +2,5 @@ source: crates/devup-mcp-devup-ui/tests/compat_fixtures.rs expression: actual --- -"" + +"" diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-178-d69d60d688.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-178-d69d60d688.snap index acb3bb54..28246a3a 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-178-d69d60d688.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-178-d69d60d688.snap @@ -7,7 +7,7 @@ expression: actual return ( diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-198-a9ea7ba9c3.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-198-a9ea7ba9c3.snap index c72280fa..f217c0d1 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-198-a9ea7ba9c3.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-198-a9ea7ba9c3.snap @@ -30,7 +30,7 @@ expression: actual minW="0" objectFit="cover" objectPos="center" - src="/images/image.png" + src="/images/imageratio-image.png" w="100%" /> diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-199-829a5c949d.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-199-829a5c949d.snap index cf661ccf..3044b301 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-199-829a5c949d.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-199-829a5c949d.snap @@ -38,7 +38,7 @@ expression: actual minW="0" objectFit="cover" objectPos="center" - src="/images/image.png" + src="/images/maxw-maxh-image.png" w="100%" /> diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-200-3313d92719.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-200-3313d92719.snap index af6c2fb2..e4fca625 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-200-3313d92719.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-200-3313d92719.snap @@ -31,7 +31,7 @@ expression: actual minW="0" objectFit="cover" objectPos="center" - src="/images/image.png" + src="/images/maxw-image.png" w="100%" /> diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-201-8207a4008e.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-201-8207a4008e.snap index 481b9460..8e2718a0 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-201-8207a4008e.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-201-8207a4008e.snap @@ -39,7 +39,7 @@ expression: actual minW="0" objectFit="cover" objectPos="center" - src="/images/image.png" + src="/images/maxh-image.png" w="100%" /> diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-211-2e15c55701.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-211-2e15c55701.snap index b034b025..b43d3848 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-211-2e15c55701.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-211-2e15c55701.snap @@ -13,7 +13,7 @@ expression: actual minW="0" objectFit="cover" objectPos="center" - src="/images/image.png" + src="/images/object-fit-cover-default-image.png" /> ) diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-212-2a8f7b4a99.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-212-2a8f7b4a99.snap index f64eede3..81433785 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-212-2a8f7b4a99.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-212-2a8f7b4a99.snap @@ -13,7 +13,7 @@ expression: actual minW="0" objectFit="contain" objectPos="center" - src="/images/image.png" + src="/images/object-fit-contain-image.png" /> ) diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-218-587666c709.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-218-587666c709.snap index 53e4444a..85d2a571 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-218-587666c709.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-218-587666c709.snap @@ -13,7 +13,7 @@ expression: actual objectFit="cover" objectPos="center" opacity="0.8" - src="/images/image.png" + src="/images/mix-blend-mode-image.png" /> ) }" diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-219-c6446b7b2e.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-219-c6446b7b2e.snap index 9770c385..88372f22 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-219-c6446b7b2e.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-219-c6446b7b2e.snap @@ -13,7 +13,7 @@ expression: actual objectFit="cover" objectPos="center" opacity="0.8" - src="/images/image.png" + src="/images/mix-blend-mode-image.png" /> ) }" diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-220-5980153f26.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-220-5980153f26.snap index 83f94b90..c589dff6 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-220-5980153f26.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-220-5980153f26.snap @@ -13,7 +13,7 @@ expression: actual objectFit="cover" objectPos="center" opacity="0.8" - src="/images/image.png" + src="/images/mix-blend-mode-image.png" /> ) }" diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-223-4dc8092992.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-223-4dc8092992.snap index 798dc1a0..bf6b85f3 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-223-4dc8092992.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-223-4dc8092992.snap @@ -150,7 +150,7 @@ expression: actual + return }" diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-235-8a3992468b.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-235-8a3992468b.snap index 32959a42..785f3ae6 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-235-8a3992468b.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-235-8a3992468b.snap @@ -4,5 +4,5 @@ expression: actual --- "export function SvgDetail() { - return + return }" diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-246-295f39e09b.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-246-295f39e09b.snap index 2b5e1962..48661702 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-246-295f39e09b.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-246-295f39e09b.snap @@ -7,7 +7,7 @@ expression: actual return ( - - + + - + - - + + - + @@ -56,7 +56,7 @@ expression: actual aspectRatio="1.59" bg="$gold" h="64px" - maskImage="url('/icons/Group 27.svg')" + maskImage="url(/icons/section-1-group-27.svg)" maskPos="center" maskRepeat="no-repeat" maskSize="contain" diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-254-9f340b6a10.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-254-9f340b6a10.snap index dc78e429..ac526e32 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-254-9f340b6a10.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-254-9f340b6a10.snap @@ -46,7 +46,7 @@ expression: actual maxW="none" objectFit="fill" pos="absolute" - src="/images/image 22.png" + src="/images/section-1-image-22.png" top="0" transform="translate(-8.6836621635%, -5.9197301257%) matrix(1.2080793162, 0, 0, 1.2886179867, 0, 0)" transformOrigin="0 0" diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-255-d01d8c7de6.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-255-d01d8c7de6.snap index cb1347c4..a8b39144 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-255-d01d8c7de6.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-255-d01d8c7de6.snap @@ -8,7 +8,7 @@ expression: actual bg="#FFF" boxSize="24px" h="48px" - maskImage="url(/icons/status=error.svg)" + maskImage="url(/icons/status-error.svg)" maskPos="center" maskRepeat="no-repeat" maskSize="contain" diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-256-baf32cc08e.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-256-baf32cc08e.snap index a756cf57..80769c60 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-256-baf32cc08e.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-256-baf32cc08e.snap @@ -12,7 +12,7 @@ export function Status({ status }: StatusProps) { Date: Fri, 23 Oct 2026 16:17:50 +0900 Subject: [PATCH 015/149] fix(codegen): honor confirmed responsive pairs and retain outputs on refusal --- .../src/codegen/responsive/merge_content.rs | 26 +++++++++++- .../src/server/handlers/devup_figma_export.rs | 3 ++ .../projection/feedback_codegen_tests.rs | 42 +++++++++++++++++++ .../server/projection/project_operation.rs | 1 + .../projection/project_responsive_output.rs | 41 ++++++++++++++---- .../server/projection/responsive_groups.rs | 10 +++++ .../devup-mcp/src/server/projection/setup.rs | 2 +- .../src/server/projection/w1_regressions.rs | 1 + 8 files changed, 115 insertions(+), 11 deletions(-) create mode 100644 crates/devup-mcp/src/server/projection/feedback_codegen_tests.rs diff --git a/crates/devup-mcp-devup-ui/src/codegen/responsive/merge_content.rs b/crates/devup-mcp-devup-ui/src/codegen/responsive/merge_content.rs index 64c47c82..fbd0c47e 100644 --- a/crates/devup-mcp-devup-ui/src/codegen/responsive/merge_content.rs +++ b/crates/devup-mcp-devup-ui/src/codegen/responsive/merge_content.rs @@ -400,6 +400,30 @@ pub fn merge_breakpoints( options: &CodegenOptions, ) -> Result, DevupError> { let found = breakpoints(snapshot); + merge_discovered_breakpoints(snapshot, options, &found) +} + +pub fn merge_confirmed_breakpoints( + snapshot: &Snapshot, + options: &CodegenOptions, +) -> Result, DevupError> { + let widths = snapshot.roots.iter().filter_map(|id| { + Some(("confirmed", snapshot.nodes.get(id)?.typed_view().number("width")?)) + }).collect::>(); + if widths.len() != snapshot.roots.len() || !breakpoint_family_is_possible(&widths) { + return Ok(None); + } + let found = snapshot.roots.iter().enumerate().map(|(rank, id)| Breakpoint { + rank, node_id: id.clone(), + }).collect::>(); + merge_discovered_breakpoints(snapshot, options, &found) +} + +fn merge_discovered_breakpoints( + snapshot: &Snapshot, + options: &CodegenOptions, + found: &[Breakpoint], +) -> Result, DevupError> { if found.len() < 2 { return Ok(None); } @@ -801,4 +825,4 @@ struct WidthCheck<'a> { tags: &'a [String], attributes: &'a [GeneratedAttribute], by_element: &'a [Vec], -} \ No newline at end of file +} diff --git a/crates/devup-mcp/src/server/handlers/devup_figma_export.rs b/crates/devup-mcp/src/server/handlers/devup_figma_export.rs index e3d5e503..e37f1523 100644 --- a/crates/devup-mcp/src/server/handlers/devup_figma_export.rs +++ b/crates/devup-mcp/src/server/handlers/devup_figma_export.rs @@ -348,6 +348,9 @@ impl DevupServer { // A frame the index does not describe leaves the question // open, and an open question is collected, not refused. if selected.len() == input.frame_ids.len() + && !crate::server::projection::responsive_groups::confirmed( + input.project_root.as_deref(), &input.frame_ids, + ).map_err(to_mcp_error)? && !devup_mcp_devup_ui::codegen::responsive::breakpoint_family_is_possible( &selected, ) diff --git a/crates/devup-mcp/src/server/projection/feedback_codegen_tests.rs b/crates/devup-mcp/src/server/projection/feedback_codegen_tests.rs new file mode 100644 index 00000000..63596055 --- /dev/null +++ b/crates/devup-mcp/src/server/projection/feedback_codegen_tests.rs @@ -0,0 +1,42 @@ +#[tokio::test] +async fn feedback36_confirmed_pair_merges_despite_different_names() { + let directory = std::env::temp_dir().join(format!("feedback36-{}", rand::random::())); + std::fs::create_dir_all(directory.join(".devup")).unwrap(); + std::fs::write(directory.join(".devup/screen-plan.json"), json!({ + "kind":"devup-screen-plan", "schemaVersion":1, "status":"confirmed", + "routes":[{"route":"/technology", "nodeIds":["1:1", "1:2"]}] + }).to_string()).unwrap(); + let mut data = responsive_payload(); + data.snapshot.nodes.get_mut("1:1").unwrap().fields.insert("name".into(), json!("Small technology")); + data.snapshot.nodes.get_mut("1:2").unwrap().fields.insert("name".into(), json!("Technology overview")); + let mut op = operation(&["responsiveTsx"]); + if let PendingOperation::Export { frame_ids, theme_options, .. } = &mut op { + *frame_ids = vec!["1:1".into(), "1:2".into()]; + theme_options.project_root = Some(directory.to_string_lossy().into_owned()); + } + let result = project(data, op).await.unwrap(); + assert!(result["responsiveTsx"].is_string(), "{result}"); + assert_eq!(result["responsiveSlots"], json!([0,4])); + std::fs::remove_dir_all(directory).unwrap(); +} + +#[tokio::test] +async fn feedback36_refusal_preserves_frame_files_and_artifact_recovery() { + let directory = std::env::temp_dir().join(format!("feedback36-files-{}", rand::random::())); + std::fs::create_dir_all(&directory).unwrap(); + let mut data = responsive_payload(); + data.snapshot.nodes.get_mut("1:2").unwrap().fields.insert("width".into(), json!(400)); + let mut op = operation(&["tsx", "responsiveTsx"]); + if let PendingOperation::Export { frame_ids, output_paths, .. } = &mut op { + *frame_ids = vec!["1:1".into(), "1:2".into()]; + output_paths.insert("frame:1:1:tsx".into(), directory.join("mobile.tsx").to_string_lossy().into_owned()); + } + let result = project(data, op).await.unwrap(); + assert!(std::fs::read_to_string(directory.join("mobile.tsx")).unwrap().contains("export")); + let failure = result["failures"].as_array().unwrap().iter().find(|failure| failure["output"] == "responsiveTsx").unwrap(); + assert_eq!(failure["details"]["frameReasons"].as_array().unwrap().len(), 2); + assert_eq!(failure["details"]["nextAction"]["arguments"]["artifactId"], result["cache"]["artifactId"]); + assert_eq!(failure["details"]["nextAction"]["arguments"]["outputs"], json!(["tsx"])); + assert_ne!(result["status"], "complete"); + std::fs::remove_dir_all(directory).unwrap(); +} diff --git a/crates/devup-mcp/src/server/projection/project_operation.rs b/crates/devup-mcp/src/server/projection/project_operation.rs index 3de92a35..85a81cfa 100644 --- a/crates/devup-mcp/src/server/projection/project_operation.rs +++ b/crates/devup-mcp/src/server/projection/project_operation.rs @@ -571,6 +571,7 @@ async fn project_operation( let (next_result, next_diagnostics, next_text_outputs, responsive_widths) = project_responsive_output::project_responsive_output( project_responsive_output::ResponsiveOutput { + failures: &mut failures, payload, outputs: &outputs, include_diagnostics, diff --git a/crates/devup-mcp/src/server/projection/project_responsive_output.rs b/crates/devup-mcp/src/server/projection/project_responsive_output.rs index f45ac485..3f75dc8e 100644 --- a/crates/devup-mcp/src/server/projection/project_responsive_output.rs +++ b/crates/devup-mcp/src/server/projection/project_responsive_output.rs @@ -9,6 +9,7 @@ pub(super) struct ResponsiveOutput<'a> { pub(super) theme_options: &'a theme_export::ThemeExportOptions, pub(super) output_paths: &'a BTreeMap, pub(super) result: Map, + pub(super) failures: &'a mut Vec, pub(super) asset_policies: &'a BTreeMap, pub(super) selected_projection_roots: Option>, @@ -37,6 +38,7 @@ pub(super) fn project_responsive_output(input: ResponsiveOutput<'_>) -> Responsi theme_options, output_paths, mut result, + failures, asset_policies, selected_projection_roots, mut projection_diagnostics, @@ -48,6 +50,13 @@ pub(super) fn project_responsive_output(input: ResponsiveOutput<'_>) -> Responsi responsive_snapshot.to_mut().roots = roots; } let mut responsive_widths = Vec::new(); + let merge = if responsive_groups::confirmed( + theme_options.project_root.as_deref(), &responsive_snapshot.roots, + )? { + devup_mcp_devup_ui::codegen::responsive::merge_confirmed_breakpoints + } else { + merge_breakpoints + }; // A screen captured with its other widths is convertible as one // tree. It is offered whenever those widths are present rather than // only on request, because a caller asking for a screen that has @@ -56,7 +65,7 @@ pub(super) fn project_responsive_output(input: ResponsiveOutput<'_>) -> Responsi if outputs .iter() .any(|output| output == "tsx" || output == "responsiveTsx") - && let Some(mut merged) = merge_breakpoints( + && let Some(mut merged) = merge( &responsive_snapshot, &CodegenOptions { asset_policies: asset_policies.clone(), @@ -247,21 +256,35 @@ pub(super) fn project_responsive_output(input: ResponsiveOutput<'_>) -> Responsi "responsiveTsx", "The snapshot has no mergeable breakpoint frames. Request tsx for individual screens, or re-collect a screen with its breakpoint siblings.", ); - let widths: Vec<_> = payload - .snapshot + let widths: Vec<_> = responsive_snapshot .roots .iter() .filter_map(|id| { - let node = payload.snapshot.nodes.get(id)?; - Some(json!({"nodeId": id, "width": node.typed_view().number("width")})) + let node = responsive_snapshot.nodes.get(id)?; + let width = node.typed_view().number("width"); + let reason = if responsive_snapshot.roots.len() < 2 { + "At least two selected frames are required." + } else if width.is_none_or(|width| !width.is_finite() || width <= 0.0) { + "The frame has no finite positive width." + } else { + "Names do not identify one breakpoint family, or frames occupy the same width band." + }; + Some(json!({"nodeId": id, "width": width, "reason":reason})) }) .collect(); error.details["collectedFrames"] = json!(widths); - error.details["mergeRefusal"] = json!("Frames merge only when the collected roots are siblings drawn at distinct widths; a confirmed screen plan grouping does not change that."); + error.details["frameReasons"] = json!(widths); + error.details["mergeRefusal"] = json!("Frames need distinct valid width bands and either a discovered family or an exact confirmed route grouping."); error.details["nextAction"] = json!({"tool": "devup_figma_export", - "arguments": {"frameIds": payload.snapshot.roots, "outputs": ["tsx"]}, - "how": "Export each collected frame as tsx and combine them by hand, or re-collect the Section so both breakpoint frames are captured."}); - return Err(error); + "arguments": {"artifactId":result["cache"]["artifactId"], "frameIds": responsive_snapshot.roots, "outputs": ["tsx"]}, + "how": "Project the selected frames separately from this artifact without another Figma read."}); + if !result.contains_key("frames") && !outputs.iter().any(|output| output == "tsx") { + return Err(error); + } + failures.push(json!({ + "output":"responsiveTsx", "errorCode":error.code, "message":error.message, "details":error.details + })); + result.entry("outputResults").or_insert_with(|| json!({}))["responsiveTsx"] = json!({"state":"failed", "projection":"failed"}); } Ok(( result, diff --git a/crates/devup-mcp/src/server/projection/responsive_groups.rs b/crates/devup-mcp/src/server/projection/responsive_groups.rs index 2a9a053a..d7ab18f7 100644 --- a/crates/devup-mcp/src/server/projection/responsive_groups.rs +++ b/crates/devup-mcp/src/server/projection/responsive_groups.rs @@ -4,6 +4,16 @@ use devup_mcp_devup_ui::codegen::responsive::{ }; use devup_mcp_figma::ExploreCandidate; +pub(crate) fn confirmed(project_root: Option<&str>, ids: &[String]) -> Result { + let Some(root) = project_root else { return Ok(false) }; + let Some(plan) = super::super::screen_plan::read_plan(root)? else { return Ok(false) }; + Ok(plan["status"] == "confirmed" && ids.len() >= 2 && plan["routes"].as_array().is_some_and(|routes| { + routes.iter().any(|route| route["nodeIds"].as_array().is_some_and(|nodes| { + nodes.len() == ids.len() && ids.iter().all(|id| nodes.contains(&json!(id))) + })) + })) +} + pub fn discover(candidates: &[ExploreCandidate], artifact_id: &str) -> Vec { let mut buckets = BTreeMap::<(Option, String), Vec<&ExploreCandidate>>::new(); for candidate in candidates.iter().filter(|candidate| { diff --git a/crates/devup-mcp/src/server/projection/setup.rs b/crates/devup-mcp/src/server/projection/setup.rs index 56018f54..15841971 100644 --- a/crates/devup-mcp/src/server/projection/setup.rs +++ b/crates/devup-mcp/src/server/projection/setup.rs @@ -17,7 +17,7 @@ mod project_responsive_output; #[path = "project_section_frames.rs"] mod project_section_frames; #[path = "responsive_groups.rs"] -mod responsive_groups; +pub(super) mod responsive_groups; #[path = "section_selection_reply.rs"] mod section_selection_reply; #[path = "summarize_projection_diagnostics.rs"] diff --git a/crates/devup-mcp/src/server/projection/w1_regressions.rs b/crates/devup-mcp/src/server/projection/w1_regressions.rs index 741ada1d..b8f0a622 100644 --- a/crates/devup-mcp/src/server/projection/w1_regressions.rs +++ b/crates/devup-mcp/src/server/projection/w1_regressions.rs @@ -1,6 +1,7 @@ include!("design_drift_tests.rs"); include!("feedback_theme_tests.rs"); +include!("feedback_codegen_tests.rs"); use super::super::artifacts::ArtifactRequestKey; use super::*; use devup_mcp_figma::{CollectionRequest, CollectionScope}; From 6fe52d208498b480400ffe9a9ba32d20ddb81568 Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 16:18:44 +0900 Subject: [PATCH 016/149] fix(export): complete feedback 37 asset-only reply semantics Keep existing manifest auto-add behavior, disclose it, select job fields after job attachment, and judge requested captures rather than all discovered assets. Assume nodes paths remain capture-scoped while other paths prefer the assembled reply. Exercise MCP calls and actual output file bytes in regression coverage. --- crates/devup-mcp/src/server/compact.rs | 16 ++++++++++++ .../src/server/handlers/devup_figma_export.rs | 16 +++++++++--- .../summarize_projection_diagnostics.rs | 2 +- crates/devup-mcp/src/server/review.rs | 10 +++++++- crates/devup-mcp/src/server/tools.rs | 1 + crates/devup-mcp/src/server/validation.rs | 4 +-- ...ict_tsx_export_rejects_lossy_projection.rs | 25 +++++++++++++++++++ 7 files changed, 65 insertions(+), 9 deletions(-) diff --git a/crates/devup-mcp/src/server/compact.rs b/crates/devup-mcp/src/server/compact.rs index 70933a35..157cb401 100644 --- a/crates/devup-mcp/src/server/compact.rs +++ b/crates/devup-mcp/src/server/compact.rs @@ -415,6 +415,22 @@ pub fn pick(reply: &Value, options: &ResponseSelection) -> Result Result<(), DevupError> { + if !options.active() { return Ok(()); } + let picked = pick(reply, options)?; + if let Some(selections) = reply["selected"]["selections"].as_array_mut() { + for (selection, current) in selections.iter_mut().zip(picked["selections"].as_array().unwrap()) { + if !selection["path"].as_str().is_some_and(|path| path == "nodes" || path.starts_with("nodes[") || path.starts_with("nodes.")) + && current["state"] == "selected" { + *selection = current.clone(); + } + } + } else { + reply["selected"] = json!({"selections":picked["selections"]}); + } + Ok(()) +} + /// Standalone outlines keep the small data body inline and move unrelated /// full acquisition/projection evidence into the same bounded resource path. pub fn outline_response( diff --git a/crates/devup-mcp/src/server/handlers/devup_figma_export.rs b/crates/devup-mcp/src/server/handlers/devup_figma_export.rs index e3d5e503..58ee80d7 100644 --- a/crates/devup-mcp/src/server/handlers/devup_figma_export.rs +++ b/crates/devup-mcp/src/server/handlers/devup_figma_export.rs @@ -11,12 +11,13 @@ impl DevupServer { Parameters(workflow): Parameters, ) -> Result { let mut input = workflow.input; - if !input.asset_requests.is_empty() + let manifest_added = !input.asset_requests.is_empty() && !input .outputs .iter() .any(|o| matches!(o.as_str(), "assetManifest" | "pageScaffold")) - { + ; + if manifest_added { input.outputs.push("assetManifest".into()); } if input.preset.as_deref() == Some("web-ready-images") { @@ -594,7 +595,7 @@ impl DevupServer { ok => ok, } .map_err(to_mcp_error)?; - Ok(tool_result(with_project_checks( + let mut result = with_project_checks( result, input.project_root.as_deref(), &skills::Lookup::new( @@ -602,6 +603,13 @@ impl DevupServer { self.output_policy.primary_root(), self.client_name(), ), - ))) + ); + if manifest_added { + let warning = json!({"code":"DEVUP_ASSET_MANIFEST_ADDED","message":"assetManifest was automatically added to outputs for assetRequests."}); + if let Some(warnings) = result["warnings"].as_array_mut() { warnings.push(warning); } + else { result["warnings"] = json!([warning]); } + } + compact::refresh_reply_selection(&mut result, &input.response_selection).map_err(to_mcp_error)?; + Ok(tool_result(result)) } } diff --git a/crates/devup-mcp/src/server/projection/summarize_projection_diagnostics.rs b/crates/devup-mcp/src/server/projection/summarize_projection_diagnostics.rs index deae7f8d..02263004 100644 --- a/crates/devup-mcp/src/server/projection/summarize_projection_diagnostics.rs +++ b/crates/devup-mcp/src/server/projection/summarize_projection_diagnostics.rs @@ -132,7 +132,7 @@ pub(super) fn summarize_projection_diagnostics( &payload.assets, ), }; - if let Some(summary) = result.get("assetSummary") { + if asset_captures.is_empty() && let Some(summary) = result.get("assetSummary") { quality.assets = asset_collection_quality(summary); } if failures diff --git a/crates/devup-mcp/src/server/review.rs b/crates/devup-mcp/src/server/review.rs index c5dbc3a5..9f005376 100644 --- a/crates/devup-mcp/src/server/review.rs +++ b/crates/devup-mcp/src/server/review.rs @@ -103,9 +103,16 @@ fn collect(response: &Value, scope: &str, out: &mut Vec) { items_from_issues(scope, issues, out); } if response["quality"]["assets"] == "partial" || response["quality"]["assets"] == "failed" { - out.push(json!({"severity":"required","scope":scope,"code":"ASSETS_INCOMPLETE", + let progress = &response["exportJob"]["assetProgress"]; + let requested = progress["total"].as_u64().filter(|total| *total > 0); + let incomplete = requested.is_none_or(|total| progress["exported"].as_u64() != Some(total)); + out.push(json!({"severity":if incomplete {"required"} else {"info"},"scope":scope,"code":if incomplete {"ASSETS_INCOMPLETE"} else {"UNREQUESTED_ASSETS"}, "what":"Some assets were not collected.","how":"Read assetSummary.unavailable[]; re-request those assets by id with assetRequests (original url, not artifactId)."})); } + if response["quality"]["assets"] == "complete" && response["assetSummary"]["status"] == "partial" { + out.push(json!({"severity":"info","scope":scope,"code":"UNREQUESTED_ASSETS", + "what":"Requested assets were collected; other discovered assets remain unrequested.","how":"Read assetSummary.unavailable[] if those other assets are needed."})); + } if response["quality"]["acquisition"] == "partial" { out.push(json!({"severity":"recommended","scope":scope,"code":"ACQUISITION_PARTIAL", "what":"The Figma snapshot was incomplete.","how":"Check completenessReport for what is missing; re-export with refresh:true if the missing part matters."})); @@ -119,6 +126,7 @@ fn collect(response: &Value, scope: &str, out: &mut Vec) { /// Add `summary` and, only when something needs review, `reviewChecklist`. pub(super) fn attach(result: &mut Value) { + if let Some(object) = result.as_object_mut() { object.remove("reviewChecklist"); } let mut items = vec![]; if result["selectionEvidence"]["requiresFullDetailRead"] == true { items.push(json!({"severity":"required","scope":"response","code":"SELECTED_PROJECTION_DETAILS_RESOURCE", diff --git a/crates/devup-mcp/src/server/tools.rs b/crates/devup-mcp/src/server/tools.rs index 25a5164d..a1558876 100644 --- a/crates/devup-mcp/src/server/tools.rs +++ b/crates/devup-mcp/src/server/tools.rs @@ -153,6 +153,7 @@ pub struct FigmaExportInput { #[serde(default)] pub all_screens: bool, /// Recommend 1–3 assets per call, maximum 6. Slow calls return an assetJob for status/resume. + /// assetManifest is automatically added to outputs with a warning when omitted. #[serde(default)] #[schemars(extend("maxItems" = 6))] pub asset_requests: Vec, diff --git a/crates/devup-mcp/src/server/validation.rs b/crates/devup-mcp/src/server/validation.rs index c1c1ee0f..973fc679 100644 --- a/crates/devup-mcp/src/server/validation.rs +++ b/crates/devup-mcp/src/server/validation.rs @@ -544,8 +544,7 @@ pub(super) fn is_caller_mistake(error: &DevupError) -> bool { ), ErrorCode::DevupSnapshotUnsupported => matches!( error.message.as_str(), - "Using assetRequests requires assetManifest in outputs." - | "referencePng can only be collected for a single Figma link target." + "referencePng can only be collected for a single Figma link target." | "The Section Frame collection scope must be node." | "frameIds and allScreens cannot be used together." | "frameIds and allScreens can only be used on a Section artifact." @@ -795,7 +794,6 @@ mod p3_tests { ); } for message in [ - "Using assetRequests requires assetManifest in outputs.", "referencePng can only be collected for a single Figma link target.", "The Section Frame collection scope must be node.", "frameIds and allScreens cannot be used together.", diff --git a/crates/devup-mcp/tests/composite_export/strict_tsx_export_rejects_lossy_projection.rs b/crates/devup-mcp/tests/composite_export/strict_tsx_export_rejects_lossy_projection.rs index 9a85b49b..c5ed4067 100644 --- a/crates/devup-mcp/tests/composite_export/strict_tsx_export_rejects_lossy_projection.rs +++ b/crates/devup-mcp/tests/composite_export/strict_tsx_export_rejects_lossy_projection.rs @@ -182,6 +182,31 @@ fn reference_png_base64() -> &'static str { "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=" } +#[tokio::test] +async fn feedback_37_outline_assets_auto_manifest_and_first_reply_selection() -> anyhow::Result<()> { + let root = unique_temp_dir("feedback-37")?; + let server = DevupServer::with_output_roots(Services::new(Arc::new(ConnectedAuth), Arc::new(FastFixtureUpstream::complete())), vec![root.clone()])?; + let (server_transport, client_transport) = tokio::io::duplex(256 * 1024); + let task = tokio::spawn(async move { server.serve(server_transport).await?.waiting().await?; anyhow::Ok(()) }); + let client = ().serve(client_transport).await?; + let result = call_result(&client, "devup_figma_export", json!({ + "url":"https://www.figma.com/design/FileKey123/Fixture?node-id=1-2", + "outputs":["outline"],"responseLimit":1, + "responseFields":["exportJob.state","exportJob.assets"], + "assetRequests":[{"assetId":"1:3:fills:0","renderMode":"node","format":"png","scale":2,"outputPath":root.join("icon.png")}] + })).await?; + let value = result.structured_content.as_ref().unwrap(); + assert_eq!(value["exportJob"]["state"], "complete"); + assert_eq!(value["selected"]["selections"][0]["values"], json!(["complete"])); + assert!(value["warnings"].as_array().unwrap().iter().any(|warning| warning["code"] == "DEVUP_ASSET_MANIFEST_ADDED")); + assert!(!value["reviewChecklist"].as_array().into_iter().flatten().any(|item| item["code"] == "ASSETS_INCOMPLETE" && item["severity"] == "required"), "{} {}", value["exportJob"]["assetProgress"], value["quality"]); + assert_eq!(fs::read(root.join("icon.png"))?, b"synthetic-png"); + client.cancel().await?; + task.await??; + fs::remove_dir_all(root)?; + Ok(()) +} + #[tokio::test] async fn p3_public_root_mapping_survives_resource_delivery_and_file_writes() -> anyhow::Result<()> { let upstream = Arc::new(FastFixtureUpstream::complete()); From 6ff0b10b6d290f43d8f6d27ccf1aaf96cc05f1c9 Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 16:23:21 +0900 Subject: [PATCH 017/149] fix(assets): bound default WebP sources and preserve flat alpha pixels (30) Existing original-source defaults already choose WebP q85. Apply the display-width times two cap when omitted and auto-select lossless encoding for alpha graphics with at most 256 colors. Explicit codec and lossless choices remain authoritative. --- .../devup-mcp-figma/src/collector/advance.rs | 6 ++--- crates/devup-mcp-figma/src/webp.rs | 22 +++++++++++++++-- .../tests/web_ready_collector.rs | 6 ++--- crates/devup-mcp-figma/tests/webp.rs | 24 +++++++++++++++++++ 4 files changed, 50 insertions(+), 8 deletions(-) diff --git a/crates/devup-mcp-figma/src/collector/advance.rs b/crates/devup-mcp-figma/src/collector/advance.rs index 97a2b3c5..8653df1e 100644 --- a/crates/devup-mcp-figma/src/collector/advance.rs +++ b/crates/devup-mcp-figma/src/collector/advance.rs @@ -152,7 +152,7 @@ impl CollectorSession { scale: 2.0, render_mode: crate::AssetRenderMode::OriginalSource, quality: Some(85), - lossless: Some(false), + lossless: None, }) .collect(); for (index, asset) in census.iter().enumerate() { @@ -175,7 +175,7 @@ impl CollectorSession { failure.scale = Some(2.0); failure.max_width = self.request.asset_selections[index].max_width; failure.quality = Some(85); - failure.lossless = Some(false); + failure.lossless = None; self.asset_results.push(failure); } } @@ -407,4 +407,4 @@ impl CollectorSession { } Ok(CollectorStep::AwaitingResults) } -} \ No newline at end of file +} diff --git a/crates/devup-mcp-figma/src/webp.rs b/crates/devup-mcp-figma/src/webp.rs index 13de5076..f6942f12 100644 --- a/crates/devup-mcp-figma/src/webp.rs +++ b/crates/devup-mcp-figma/src/webp.rs @@ -296,7 +296,18 @@ pub(crate) fn finish( "srcset requires a known positive node display width.", )); } - let cap = request.max_width.unwrap_or(width).min(width); + let default_cap = if request.render_mode == crate::AssetRenderMode::OriginalSource + && request.format == AssetFormat::Webp + { + display_width.map(|display| (display * 2.0).ceil().clamp(1.0, f64::from(width)) as u32) + } else { + None + }; + let cap = request + .max_width + .or(default_cap) + .unwrap_or(width) + .min(width); let base_width = if request.srcset { (display_width.unwrap().ceil() as u32).max(1).min(cap) } else { @@ -305,7 +316,14 @@ pub(crate) fn finish( let rgba = resized(&source_rgba, base_width); let resized_source = rgba.width() != width || rgba.height() != height; let quality = request.quality.unwrap_or(85); - let lossless = request.lossless.unwrap_or(false); + let lossless = request.lossless.unwrap_or_else(|| { + let mut colors = std::collections::BTreeSet::new(); + source_rgba.pixels().any(|pixel| pixel[3] != 255) + && source_rgba.pixels().all(|pixel| { + colors.insert(pixel.0); + colors.len() <= 256 + }) + }); let encoded = if request.format == AssetFormat::Webp { encode_webp(&rgba, quality, lossless)? } else { diff --git a/crates/devup-mcp-figma/tests/web_ready_collector.rs b/crates/devup-mcp-figma/tests/web_ready_collector.rs index 630f8ea6..38c6780e 100644 --- a/crates/devup-mcp-figma/tests/web_ready_collector.rs +++ b/crates/devup-mcp-figma/tests/web_ready_collector.rs @@ -199,11 +199,11 @@ fn preset_schedules_bounded_batches_and_retains_nested_hidden_and_multifill_rows } for asset in parts.assets { assert_eq!(asset.quality, Some(85)); - assert_eq!(asset.lossless, Some(false)); + assert_eq!(asset.lossless, None); if asset.status == AssetStatus::Exported { let info = asset.render_info.unwrap(); assert_eq!(info["representation"], "original-fill-transcoded"); - assert_eq!(info["fidelity"], "approximate"); + assert_eq!(info["fidelity"], "lossless-pixels"); assert_eq!(info["byteEvidence"]["sha256"], asset.sha256.unwrap()); assert!(info["compression"]["referencePNG"].is_object()); } @@ -260,7 +260,7 @@ fn malformed_fragmented_native_png_fails_one_asset_and_keeps_the_entire_preset() Some("DEVUP_ASSET_WEBP_POSTPROCESS_FAILED") ); assert_eq!(failed.quality, Some(85)); - assert_eq!(failed.lossless, Some(false)); + assert_eq!(failed.lossless, None); assert!(failed.data_base64.is_none()); assert!( parts diff --git a/crates/devup-mcp-figma/tests/webp.rs b/crates/devup-mcp-figma/tests/webp.rs index ef037e66..12aa9561 100644 --- a/crates/devup-mcp-figma/tests/webp.rs +++ b/crates/devup-mcp-figma/tests/webp.rs @@ -63,6 +63,30 @@ fn lossless_preserves_every_rgba_pixel_including_transparent_rgb_and_actual_iden assert_eq!(info["outputSize"]["width"], 93.0); } } + +#[test] +fn flat_alpha_graphics_default_to_lossless_webp_without_overriding_explicit_lossy() { + let rgba = RgbaImage::from_fn(16, 16, |x, _| { + if x < 8 { + Rgba([203, 17, 91, 0]) + } else { + Rgba([11, 201, 43, 127]) + } + }); + let png = encode(rgba.clone(), ImageFormat::Png); + let mut req = request(85, false); + req.lossless = None; + let entry = exported_asset_from_bytes(&req, &png).unwrap(); + let bytes = STANDARD.decode(entry.data_base64.unwrap()).unwrap(); + assert_eq!(image::load_from_memory(&bytes).unwrap().to_rgba8(), rgba); + assert_eq!(entry.render_info.unwrap()["compression"]["lossless"], true); + req.lossless = Some(false); + let explicit = exported_asset_from_bytes(&req, &png).unwrap(); + assert_eq!( + explicit.render_info.unwrap()["compression"]["lossless"], + false + ); +} #[test] fn lossy_quality_changes_rgb_and_bytes_but_preserves_alpha_and_uses_honest_savings() { let rgba = pixels(128, 96); From c38bebc3f79e304f96f4543d9c32d1110de2486f Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 16:23:22 +0900 Subject: [PATCH 018/149] fix(export): preserve multi-output split recovery for feedback 28 --- crates/devup-mcp/src/server/delivery.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/devup-mcp/src/server/delivery.rs b/crates/devup-mcp/src/server/delivery.rs index 307b66e7..6f6835c3 100644 --- a/crates/devup-mcp/src/server/delivery.rs +++ b/crates/devup-mcp/src/server/delivery.rs @@ -284,7 +284,7 @@ pub(super) fn with_inline_recovery(mut error: DevupError, arguments: Value) -> D split.push(serde_json::json!({"tool":"devup_figma_export","arguments":args})); } } - if arguments["delivery"] == "resource" && error.details.get("envelopeFields").is_some() { + if arguments["delivery"] == "resource" && error.details.get("envelopeFields").is_some() && split.is_empty() { let mut narrowed = arguments.clone(); if let Some(object) = narrowed.as_object_mut() { object.remove("assetRequests"); From 1bc41f296c577b7c9a10a8043fcce7526c3d30f5 Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 16:23:23 +0900 Subject: [PATCH 019/149] fix(export): retain hidden asset exclusions and artifact warnings for feedback 37 --- .../src/server/handlers/devup_figma_export.rs | 11 +++++++++-- .../projection/summarize_projection_diagnostics.rs | 1 + 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/crates/devup-mcp/src/server/handlers/devup_figma_export.rs b/crates/devup-mcp/src/server/handlers/devup_figma_export.rs index 58ee80d7..670dcef5 100644 --- a/crates/devup-mcp/src/server/handlers/devup_figma_export.rs +++ b/crates/devup-mcp/src/server/handlers/devup_figma_export.rs @@ -473,7 +473,7 @@ impl DevupServer { ) .await .map_err(to_mcp_error)?; - return Ok(tool_result(with_project_checks( + let mut result = with_project_checks( result, input.project_root.as_deref(), &skills::Lookup::new( @@ -481,7 +481,14 @@ impl DevupServer { self.output_policy.primary_root(), self.client_name(), ), - ))); + ); + if manifest_added { + let warning = json!({"code":"DEVUP_ASSET_MANIFEST_ADDED","message":"assetManifest was automatically added to outputs for assetRequests."}); + if let Some(warnings) = result["warnings"].as_array_mut() { warnings.push(warning); } + else { result["warnings"] = json!([warning]); } + } + compact::refresh_reply_selection(&mut result, &input.response_selection).map_err(to_mcp_error)?; + return Ok(tool_result(result)); } let collection_scope = parse_collection_scope(&input.scope).map_err(to_mcp_error)?; diff --git a/crates/devup-mcp/src/server/projection/summarize_projection_diagnostics.rs b/crates/devup-mcp/src/server/projection/summarize_projection_diagnostics.rs index 02263004..40719124 100644 --- a/crates/devup-mcp/src/server/projection/summarize_projection_diagnostics.rs +++ b/crates/devup-mcp/src/server/projection/summarize_projection_diagnostics.rs @@ -127,6 +127,7 @@ pub(super) fn summarize_projection_diagnostics( outputs.iter().any(|output| output == "assetManifest"), &asset_captures .iter() + .filter(|capture| !payload.assets.iter().any(|asset| asset.asset_id == capture.asset_id && asset.error_code.as_deref() == Some("DEVUP_ASSET_NODE_HIDDEN"))) .map(|capture| capture.asset_id.clone()) .collect::>(), &payload.assets, From 45d48a6347f2ddbfc3b15f29370258bdb76b3016 Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 16:27:16 +0900 Subject: [PATCH 020/149] fix(assets): require review of acquired but unwritten placeholders (31) Keep the existing shared manifest-to-TSX reconciliation, verified by its real-file dedup regression. Add required placeholder paths to reviewChecklist even when acquisition itself is complete. --- crates/devup-mcp/src/server/review.rs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/crates/devup-mcp/src/server/review.rs b/crates/devup-mcp/src/server/review.rs index c5dbc3a5..e7c38c9a 100644 --- a/crates/devup-mcp/src/server/review.rs +++ b/crates/devup-mcp/src/server/review.rs @@ -71,6 +71,15 @@ fn has_line_height(value: &Value) -> bool { } fn collect(response: &Value, scope: &str, out: &mut Vec) { + let placeholders = response["assetManifest"]["assets"].as_array().into_iter().flatten() + .filter(|asset| asset["status"] != "exported" || asset["outputPath"].is_null()) + .filter_map(|asset| asset["path"].as_str()) + .collect::>(); + if !placeholders.is_empty() { + out.push(json!({"severity":"required","scope":scope,"code":"ASSET_PLACEHOLDERS", + "paths":placeholders,"what":"Generated asset URLs are placeholders without a committed output file.", + "how":"Export the assets with outputPath and assetPublicRoot before using the generated code."})); + } for item in response["responsiveReviewChecklist"]["items"] .as_array() .into_iter() @@ -181,6 +190,19 @@ mod tests { assert!(value.get("reviewChecklist").is_none()); } + #[test] + fn acquired_but_unwritten_assets_require_placeholder_review() { + let mut value = json!({"status":"complete","quality":{"assets":"complete"}, + "assetManifest":{"assets":[ + {"assetId":"1:1:fills:0","status":"exported","path":"/images/main-hero.webp"}, + {"assetId":"1:2:node","status":"exported","path":"/icons/logo.svg","outputPath":"public/icons/logo.svg"} + ]}}); + attach(&mut value); + assert_eq!(value["reviewChecklist"][0]["code"], "ASSET_PLACEHOLDERS"); + assert_eq!(value["reviewChecklist"][0]["severity"], "required"); + assert_eq!(value["reviewChecklist"][0]["paths"], json!(["/images/main-hero.webp"])); + } + #[test] fn issues_group_by_code_and_lossy_is_required() { let mut value = json!({"status":"partial","quality":{"projection":"lossy"}, From ca957549ef3ad8bc063c815320f2e9dc33ef164e Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 16:31:16 +0900 Subject: [PATCH 021/149] fix(assets): reconcile source-fill results with requested job rows (39) Resolve node requests to captured fill evidence before determining write state, and avoid reporting a request's own internal fill as a duplicate. Existing partial/resume and request-count regressions remain covered. --- .../src/server/asset_job_progress.rs | 2 +- crates/devup-mcp/src/server/asset_jobs.rs | 12 ++++++--- .../devup-mcp/src/server/asset_jobs_tests.rs | 26 ++++++++++++++++++- 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/crates/devup-mcp/src/server/asset_job_progress.rs b/crates/devup-mcp/src/server/asset_job_progress.rs index e0a51d48..67ee75de 100644 --- a/crates/devup-mcp/src/server/asset_job_progress.rs +++ b/crates/devup-mcp/src/server/asset_job_progress.rs @@ -1,7 +1,7 @@ use std::collections::BTreeMap; use serde_json::Value; -fn request_key(asset_id: &str) -> &str { +pub(super) fn request_key(asset_id: &str) -> &str { let id = asset_id.split(":fills:").next().unwrap_or(asset_id); ["node", "visible", "fill"] .iter() diff --git a/crates/devup-mcp/src/server/asset_jobs.rs b/crates/devup-mcp/src/server/asset_jobs.rs index 892d24d8..433656fe 100644 --- a/crates/devup-mcp/src/server/asset_jobs.rs +++ b/crates/devup-mcp/src/server/asset_jobs.rs @@ -11,7 +11,7 @@ use devup_mcp_figma::{ }; use serde_json::{Value, json}; -use super::asset_job_progress::request_progress; +use super::asset_job_progress::{request_key, request_progress}; use sha2::{Digest, Sha256}; use tokio::{sync::Notify, time::Instant}; @@ -324,7 +324,12 @@ impl AssetJob { ) .collect::>(); for progress in &mut state.assets { - if let Some(asset) = assets.iter().find(|a| a["assetId"] == progress["assetId"]) { + if let Some(asset) = assets.iter().find(|a| a["assetId"] == progress["assetId"]) + .or_else(|| assets.iter().find(|a| { + progress["assetId"].as_str().is_some_and(|id| id.ends_with(":node")) + && a["assetId"].as_str().zip(progress["assetId"].as_str()) + .is_some_and(|(actual, requested)| request_key(actual) == request_key(requested)) + })) { for field in [ "status", "byteLength", @@ -470,7 +475,8 @@ impl AssetJob { .iter() .map(|asset| { let mut asset = asset.clone(); - if let Some(first) = asset["assetId"].as_str().and_then(|id| duplicates.get(id)) { + if let Some(first) = asset["assetId"].as_str().and_then(|id| duplicates.get(id)) + .filter(|first| asset["assetId"].as_str().is_some_and(|id| request_key(id) != request_key(first))) { asset["duplicateOf"] = json!(first); } let path = asset["assetId"].as_str().and_then(|id| self.paths.get(id)); diff --git a/crates/devup-mcp/src/server/asset_jobs_tests.rs b/crates/devup-mcp/src/server/asset_jobs_tests.rs index af8ddef7..4275857b 100644 --- a/crates/devup-mcp/src/server/asset_jobs_tests.rs +++ b/crates/devup-mcp/src/server/asset_jobs_tests.rs @@ -421,9 +421,33 @@ fn r39_identical_bytes_are_marked_duplicate_and_substeps_count_once() { let response = job.response().unwrap(); let rows = response["exportJob"]["assets"].as_array().unwrap(); assert!(rows[0].get("duplicateOf").is_none()); - assert_eq!(rows[1]["duplicateOf"], "1:2:node"); + assert!(rows[1].get("duplicateOf").is_none()); assert!(rows[2].get("duplicateOf").is_none()); assert_eq!(rows[3]["duplicateOf"], "1:2:node"); assert_eq!(response["exportJob"]["assetProgress"]["total"], 3); assert_eq!(response["exportJob"]["assetProgress"]["substeps"], 4); } + +#[test] +fn r39_node_request_adopts_written_source_fill_evidence() { + let path = std::env::temp_dir().join(format!("devup-r39-{}.webp", rand::random::())); + std::fs::write(&path, b"source").unwrap(); + let hash = Sha256::digest(b"source").iter().map(|byte| format!("{byte:02x}")).collect::(); + let job = AssetJob { + id: "source".into(), key: "source".into(), budget: json!({}), started: Instant::now(), + resume: Notify::new(), cancellation: Notify::new(), + cancelled: std::sync::atomic::AtomicBool::new(false), delivery: Default::default(), + paths: BTreeMap::new(), + state: Mutex::new(JobState { state: "running", stage: "projection-and-write", calls: vec![], + completed_calls: 0, error: None, result: None, finished: None, + assets: vec![json!({"assetId":"44:1558:node","status":"pending"})] }), + }; + job.finish(Ok(json!({"assetManifest":{"assets":[{"assetId":"44:1558:fills:0","status":"exported","sha256":hash}]}, + "outputPaths":{"asset:44:1558:node":path}}))); + let response = job.response().unwrap(); + assert_eq!(response["exportJob"]["state"], "complete"); + assert_eq!(response["exportJob"]["assets"][0]["status"], "exported"); + assert_eq!(response["exportJob"]["assets"][0]["fileState"], "written"); + assert_eq!(response["exportJob"]["assetProgress"]["total"], 1); + std::fs::remove_file(path).unwrap(); +} From 9949c28bb8b019c7ccec9ddb7847315d5ecd6fec Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 16:59:33 +0900 Subject: [PATCH 022/149] test(codegen): align responsive refusal recovery expectations --- ..._wquw118_layout_contracts_are_preserved.rs | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/crates/devup-mcp/src/server/projection/w1_regressions_r7_real_wquw118_layout_contracts_are_preserved.rs b/crates/devup-mcp/src/server/projection/w1_regressions_r7_real_wquw118_layout_contracts_are_preserved.rs index 917fc6e5..e2db3880 100644 --- a/crates/devup-mcp/src/server/projection/w1_regressions_r7_real_wquw118_layout_contracts_are_preserved.rs +++ b/crates/devup-mcp/src/server/projection/w1_regressions_r7_real_wquw118_layout_contracts_are_preserved.rs @@ -772,11 +772,26 @@ async fn w1_unavailable_responsive_output_is_actionable_even_with_tsx_and_strict *all_screens = true; *strict = strict_value; } - let error = project(section(2), op).await.unwrap_err(); - assert_eq!(error.code, ErrorCode::DevupInvalidInput); - assert!(error.message.contains("responsiveTsx")); - assert!(error.message.contains("tsx")); - assert!(error.details.is_object()); + let response = project(section(2), op).await; + if outputs == vec!["responsiveTsx"] { + let error = response.unwrap_err(); + assert_eq!(error.details["nextAction"]["arguments"]["outputs"], json!(["tsx"])); + assert_eq!(error.details["frameReasons"].as_array().unwrap().len(), 2); + continue; + } + let result = if strict_value { + response.unwrap_err().details + } else { + response.unwrap() + }; + let failure = result["failures"].as_array().unwrap().iter() + .find(|failure| failure["output"] == "responsiveTsx").unwrap(); + assert_eq!(failure["details"]["nextAction"]["tool"], "devup_figma_export"); + assert_eq!(failure["details"]["nextAction"]["arguments"]["outputs"], json!(["tsx"])); + assert_eq!(failure["details"]["frameReasons"].as_array().unwrap().len(), 2); + if !strict_value { + assert_eq!(result["outputResults"]["responsiveTsx"]["state"], "failed"); + } } } } From 2a2d862ec0921cf0474faf806d1ce060112dc4e9 Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 17:00:01 +0900 Subject: [PATCH 023/149] fix(codegen): export component sets as one props-based component --- .../codegen/component/component_generation.rs | 15 ++- .../src/codegen/layout/layout_properties.rs | 7 +- .../devup-mcp-devup-ui/src/codegen/variant.rs | 1 + .../src/codegen/variant/drawn_when.rs | 13 ++- .../src/codegen/variant/text_properties.rs | 30 ++++++ .../src/codegen/variant/variant_projection.rs | 39 ++++++-- .../tests/feedback_component_set.rs | 97 +++++++++++++++++++ crates/devup-mcp/src/server/asset_guard.rs | 4 +- .../server/projection/component_projection.rs | 21 ++++ .../projection/feedback_codegen_tests.rs | 58 +++++++++++ .../project_node_and_theme_outputs.rs | 4 + 11 files changed, 269 insertions(+), 20 deletions(-) create mode 100644 crates/devup-mcp-devup-ui/src/codegen/variant/text_properties.rs create mode 100644 crates/devup-mcp-devup-ui/tests/feedback_component_set.rs diff --git a/crates/devup-mcp-devup-ui/src/codegen/component/component_generation.rs b/crates/devup-mcp-devup-ui/src/codegen/component/component_generation.rs index c15adac0..dc35dc5a 100644 --- a/crates/devup-mcp-devup-ui/src/codegen/component/component_generation.rs +++ b/crates/devup-mcp-devup-ui/src/codegen/component/component_generation.rs @@ -173,6 +173,17 @@ pub fn generate_component( root_id: &str, options: &CodegenOptions, ) -> Result { + if let Some(root) = snapshot.nodes.get(root_id) + && root.node_type == "COMPONENT_SET" + { + return match variant::generate_variant_component_set(snapshot, root_id, options, true)? { + Some(mut output) => { + output.tsx.insert_str(0, &format!("import {{ {} }} from \"@devup-ui/react\";\n\n", output.imports.join(", "))); + finalize_codegen_output(output, snapshot, options, root_id) + }, + None => generate_component_set_target(snapshot, root_id, root.typed_view().name().unwrap_or("Component"), options), + }; + } let generated = generate_node_marked(snapshot, root_id, options)?; let root = snapshot.nodes.get(root_id).ok_or_else(|| { DevupError::new( @@ -324,7 +335,7 @@ pub fn generate_component_set_target( let root_view = component_set.unwrap_or(root).typed_view(); let main = component_set.is_some(); if let Some(set) = component_set - && let Some(output) = variant::generate_variant_component_set(snapshot, &set.id, options)? + && let Some(output) = variant::generate_variant_component_set(snapshot, &set.id, options, false)? { return finalize_codegen_output(output, snapshot, options, &set.id); } @@ -919,4 +930,4 @@ fn js_value(value: &serde_json::Value, depth: usize) -> String { fn json_string(value: &str) -> String { serde_json::to_string(value).expect("string JSON serialization") -} \ No newline at end of file +} diff --git a/crates/devup-mcp-devup-ui/src/codegen/layout/layout_properties.rs b/crates/devup-mcp-devup-ui/src/codegen/layout/layout_properties.rs index 4a15ceca..618116d6 100644 --- a/crates/devup-mcp-devup-ui/src/codegen/layout/layout_properties.rs +++ b/crates/devup-mcp-devup-ui/src/codegen/layout/layout_properties.rs @@ -104,7 +104,7 @@ pub(super) fn push_layout_props( let is_page_root = parent .map(|parent| parent.typed_view().node_type()) .or_else(|| view.string("parentType")) - .is_some_and(|kind| matches!(kind, "SECTION" | "PAGE" | "COMPONENT_SET")); + .is_some_and(|kind| matches!(kind, "SECTION" | "PAGE")); let fixed_w = view.string("layoutSizingHorizontal") == Some("FIXED"); let fixed_h = view.string("layoutSizingVertical") == Some("FIXED"); let fill_w = view.string("layoutSizingHorizontal") == Some("FILL"); @@ -227,8 +227,7 @@ pub(super) fn push_layout_props( (own_height, parent_height), (Some(height), Some(parent_height)) if parent_height > height ); - let in_a_group = - parent.is_some_and(|parent| parent.typed_view().node_type() == "GROUP"); + let in_a_group = parent.is_some_and(|parent| parent.node_type == "GROUP"); let leaf_shape = !has_children && !holds_children && !is_asset && in_a_group; if leaf_shape && parent_wider { width = own_width.map(px); @@ -965,4 +964,4 @@ fn child_shrinker(parent: &RawNode, dimension: &str) -> bool { pub(super) struct GridTemplate { pub css: Option, pub unverified: Vec<&'static str>, -} \ No newline at end of file +} diff --git a/crates/devup-mcp-devup-ui/src/codegen/variant.rs b/crates/devup-mcp-devup-ui/src/codegen/variant.rs index d59663cf..d7b30a67 100644 --- a/crates/devup-mcp-devup-ui/src/codegen/variant.rs +++ b/crates/devup-mcp-devup-ui/src/codegen/variant.rs @@ -1,3 +1,4 @@ // Source sections share this module to preserve privacy and test names. include!("variant/variant_projection.rs"); include!("variant/drawn_when.rs"); +include!("variant/text_properties.rs"); diff --git a/crates/devup-mcp-devup-ui/src/codegen/variant/drawn_when.rs b/crates/devup-mcp-devup-ui/src/codegen/variant/drawn_when.rs index dcc95879..783506d4 100644 --- a/crates/devup-mcp-devup-ui/src/codegen/variant/drawn_when.rs +++ b/crates/devup-mcp-devup-ui/src/codegen/variant/drawn_when.rs @@ -577,11 +577,12 @@ fn render_component( tree: &Tree, selectors: &BTreeMap>, transition: Option<&(String, String)>, + text_properties: &BTreeMap, ) -> String { - let interface = if dimensions.is_empty() { + let interface = if dimensions.is_empty() && text_properties.is_empty() { String::new() } else { - let fields = dimensions + let mut fields = dimensions .iter() .map(|definition| { if definition.boolean { @@ -602,14 +603,18 @@ fn render_component( }) .collect::>() .join("\n"); + for name in text_properties.keys() { + fields.push_str(&format!("\n {name}?: string")); + } format!("export interface {name}Props {{\n{fields}\n}}\n\n") }; - let signature = if dimensions.is_empty() { + let signature = if dimensions.is_empty() && text_properties.is_empty() { format!("export function {name}()") } else { let props = dimensions .iter() - .map(|definition| definition.name.as_str()) + .map(|definition| definition.name.clone()) + .chain(text_properties.iter().map(|(name, default)| format!("{name} = {default}"))) .collect::>() .join(", "); format!("export function {name}({{ {props} }}: {name}Props)") diff --git a/crates/devup-mcp-devup-ui/src/codegen/variant/text_properties.rs b/crates/devup-mcp-devup-ui/src/codegen/variant/text_properties.rs new file mode 100644 index 00000000..02e4dd71 --- /dev/null +++ b/crates/devup-mcp-devup-ui/src/codegen/variant/text_properties.rs @@ -0,0 +1,30 @@ +fn bind_text_properties( + snapshot: &Snapshot, + set: &RawNode, + tree: &mut Tree, +) -> BTreeMap { + let properties = set.typed_view() + .value("componentPropertyDefinitions") + .and_then(Value::as_object) + .into_iter() + .flatten() + .filter(|(_, definition)| definition["type"] == "TEXT") + .map(|(name, definition)| ( + name.clone(), + (instance_property_name(name.split('#').next().unwrap_or(name)), definition["defaultValue"].to_string()), + )) + .collect::>(); + let mut pending = vec![tree]; + while let Some(node) = pending.pop() { + if let Some((name, _)) = snapshot.nodes.get(&node.node_id) + .and_then(|raw| raw.typed_view().value("componentPropertyReferences")) + .and_then(|references| references.get("characters")) + .and_then(Value::as_str) + .and_then(|reference| properties.get(reference)) + { + node.content = Some(format!("{{{name}}}")); + } + pending.extend(node.children.iter_mut()); + } + properties.into_values().collect() +} diff --git a/crates/devup-mcp-devup-ui/src/codegen/variant/variant_projection.rs b/crates/devup-mcp-devup-ui/src/codegen/variant/variant_projection.rs index 2b3e099b..cb64be7c 100644 --- a/crates/devup-mcp-devup-ui/src/codegen/variant/variant_projection.rs +++ b/crates/devup-mcp-devup-ui/src/codegen/variant/variant_projection.rs @@ -76,6 +76,7 @@ pub(super) fn generate_variant_component_set( snapshot: &Snapshot, set_id: &str, options: &CodegenOptions, + merge_all: bool, ) -> Result, DevupError> { let set = snapshot.nodes.get(set_id).ok_or_else(|| { DevupError::new( @@ -84,13 +85,23 @@ pub(super) fn generate_variant_component_set( false, ) })?; - let definitions = definitions(set); - if !definitions.iter().any(|definition| { - matches!( - definition.name.to_ascii_lowercase().as_str(), - "effect" | "viewport" - ) - }) { + let mut definitions = definitions(set); + let state_axis = merge_all.then(|| definitions.iter().find(|definition| { + definition.name.eq_ignore_ascii_case("state") && definition.options.iter().all(|value| { + matches!(value.to_ascii_lowercase().as_str(), "default" | "hover" | "active" | "disabled" | "focus") + }) + }).map(|definition| definition.source.clone())).flatten(); + if let Some(state_axis) = &state_axis { + for definition in &mut definitions { + if &definition.source == state_axis { + definition.name = "effect".into(); + definition.source = "effect".into(); + definition.default.make_ascii_lowercase(); + definition.options.iter_mut().for_each(|value| value.make_ascii_lowercase()); + } + } + } + if !merge_all && !definitions.iter().any(|definition| matches!(definition.name.to_ascii_lowercase().as_str(), "effect" | "viewport")) { return Ok(None); } let records = set @@ -98,13 +109,18 @@ pub(super) fn generate_variant_component_set( .child_ids() .filter_map(|id| snapshot.nodes.get(id)) .filter_map(|node| { - let values = node + let mut values = node .typed_view() .value("variantProperties")? .as_object()? .iter() .filter_map(|(key, value)| Some((key.clone(), value.as_str()?.to_owned()))) .collect::>(); + if let Some(state_axis) = &state_axis + && let Some(value) = values.remove(state_axis) + { + values.insert("effect".into(), value.to_ascii_lowercase()); + } Some((node, values)) }) .map(|(node, values)| { @@ -115,6 +131,9 @@ pub(super) fn generate_variant_component_set( }) }) .collect::, DevupError>>()?; + if records.is_empty() { + return Ok(None); + } // A boolean property is not part of a variant's identity — it never appears // in `variantProperties` — so filtering records by it matches nothing. let default_filters = definitions @@ -199,6 +218,7 @@ pub(super) fn generate_variant_component_set( } } let transition = transition(snapshot, &default.node_id, &transition_props); + let text_properties = bind_text_properties(snapshot, set, &mut root); let component_name = legacy_component_name(set.typed_view().name().unwrap_or("Component")); let code = mark_node( &set.id, @@ -208,6 +228,7 @@ pub(super) fn generate_variant_component_set( &root, &selectors, transition.as_ref(), + &text_properties, ), ); let mut imports = BTreeSet::new(); @@ -943,4 +964,4 @@ fn tree_field(tree: &Tree, field: &str) -> Option { return tree.content.clone(); } tree.props.get(field).cloned() -} \ No newline at end of file +} diff --git a/crates/devup-mcp-devup-ui/tests/feedback_component_set.rs b/crates/devup-mcp-devup-ui/tests/feedback_component_set.rs new file mode 100644 index 00000000..8a225818 --- /dev/null +++ b/crates/devup-mcp-devup-ui/tests/feedback_component_set.rs @@ -0,0 +1,97 @@ +use devup_mcp_devup_ui::codegen::{CodegenOptions, generate_component}; +use devup_mcp_figma::Snapshot; +use serde_json::json; + +#[test] +fn component_set_exports_one_component_with_variant_and_text_props() { + let mut nodes = json!({ + "set": {"id":"set", "type":"COMPONENT_SET", "fields": { + "name":"Button", "width":900, "height":600, + "childrenIds":["small", "large"], + "componentPropertyDefinitions": { + "size":{"type":"VARIANT", "defaultValue":"sm", "variantOptions":["sm","md"]}, + "label#1":{"type":"TEXT", "defaultValue":"Submit"} + } + }, "extra":{}, "fieldErrors":{}} + }); + for (id, size, width) in [("small", "sm", 80), ("large", "md", 120)] { + let text = format!("{id}-text"); + nodes[id] = json!({"id":id,"type":"COMPONENT","fields":{ + "name":format!("size={size}"), "parentId":"set", "width":width,"height":40, + "layoutSizingHorizontal":"FIXED", "layoutSizingVertical":"FIXED", + "variantProperties":{"size":size},"childrenIds":[text] + },"extra":{},"fieldErrors":{}}); + nodes[&text] = json!({"id":text,"type":"TEXT","fields":{ + "parentId":id,"characters":"Submit","width":60,"height":20, + "componentPropertyReferences":{"characters":"label#1"} + },"extra":{},"fieldErrors":{}}); + } + let snapshot: Snapshot = serde_json::from_value(json!({ + "fileKey":"FileKey123","version":"v1","roots":["set"],"nodes":nodes,"diagnostics":[] + })) + .unwrap(); + let output = generate_component(&snapshot, "set", &CodegenOptions::default()).unwrap(); + assert_eq!( + output.tsx.matches("export function Button").count(), + 1, + "{}", + output.tsx + ); + assert!(output.tsx.contains("size:"), "{}", output.tsx); + assert!(output.tsx.contains("label?: string"), "{}", output.tsx); + assert!(output.tsx.contains("{label}"), "{}", output.tsx); + assert!(!output.tsx.contains("900px"), "{}", output.tsx); + assert!(output.tsx.contains("[size]"), "{}", output.tsx); +} + +#[test] +fn interaction_state_variants_become_pseudo_styles() { + let snapshot: Snapshot = serde_json::from_value(json!({ + "fileKey":"FileKey123","version":"v1","roots":["set"],"diagnostics":[], + "nodes":{ + "set":{"id":"set","type":"COMPONENT_SET","fields":{ + "name":"Button","childrenIds":["default","hover"], + "componentPropertyDefinitions":{"State":{"type":"VARIANT","defaultValue":"Default","variantOptions":["Default","Hover"]}} + },"extra":{},"fieldErrors":{}}, + "default":{"id":"default","type":"COMPONENT","fields":{ + "name":"State=Default","variantProperties":{"State":"Default"}, + "fills":[{"type":"SOLID","color":{"r":1,"g":0,"b":0}}] + },"extra":{},"fieldErrors":{}}, + "hover":{"id":"hover","type":"COMPONENT","fields":{ + "name":"State=Hover","variantProperties":{"State":"Hover"}, + "fills":[{"type":"SOLID","color":{"r":0,"g":0,"b":1}}] + },"extra":{},"fieldErrors":{}} + } + })).unwrap(); + let output = generate_component(&snapshot, "set", &CodegenOptions::default()).unwrap(); + assert!(output.tsx.contains("_hover="), "{}", output.tsx); + assert!(!output.tsx.contains("State:"), "{}", output.tsx); +} + +#[test] +fn component_set_preserves_fixed_leaf_bar_height() { + let snapshot: Snapshot = serde_json::from_value(json!({ + "fileKey":"FileKey123","version":"v1","roots":["set"],"diagnostics":[], + "nodes":{ + "set":{"id":"set","type":"COMPONENT_SET","fields":{ + "name":"TechStep","childrenIds":["step"], + "componentPropertyDefinitions":{"step":{"type":"VARIANT","defaultValue":"one","variantOptions":["one"]}} + },"extra":{},"fieldErrors":{}}, + "step":{"id":"step","type":"COMPONENT","fields":{ + "name":"step=one","variantProperties":{"step":"one"}, + "layoutMode":"VERTICAL","width":200,"height":100,"childrenIds":["label","bar"] + },"extra":{},"fieldErrors":{}}, + "label":{"id":"label","type":"TEXT","fields":{ + "parentId":"step","characters":"Step one","width":80,"height":20 + },"extra":{},"fieldErrors":{}}, + "bar":{"id":"bar","type":"RECTANGLE","fields":{ + "parentId":"step","width":160,"height":4, + "layoutSizingHorizontal":"FIXED","layoutSizingVertical":"FIXED", + "fills":[{"type":"SOLID","color":{"r":1,"g":0,"b":0}}] + },"extra":{},"fieldErrors":{}} + } + })).unwrap(); + let output = generate_component(&snapshot, "set", &CodegenOptions::default()).unwrap(); + assert!(output.tsx.contains("h=\"4px\""), "{}", output.tsx); + assert!(output.tsx.contains("w=\"160px\""), "{}", output.tsx); +} diff --git a/crates/devup-mcp/src/server/asset_guard.rs b/crates/devup-mcp/src/server/asset_guard.rs index 5f0d9394..cfb967e2 100644 --- a/crates/devup-mcp/src/server/asset_guard.rs +++ b/crates/devup-mcp/src/server/asset_guard.rs @@ -155,7 +155,9 @@ pub(super) fn export_coverage( { continue; } - if depth <= 1 || node.node_type == "SECTION" || assets.contains(id) { + if node.node_type != "COMPONENT_SET" + && (depth <= 1 || node.node_type == "SECTION" || assets.contains(id)) + { wanted.insert(id.to_owned()); } // Leaf assets (including a flattened wrapper) represent all their children. diff --git a/crates/devup-mcp/src/server/projection/component_projection.rs b/crates/devup-mcp/src/server/projection/component_projection.rs index e21aa9c5..d0840b39 100644 --- a/crates/devup-mcp/src/server/projection/component_projection.rs +++ b/crates/devup-mcp/src/server/projection/component_projection.rs @@ -98,3 +98,24 @@ fn generate_collected_component( } generate_component(&payload.snapshot, node_id, options) } + +fn component_definition_actions( + payload: &CollectedPayload, + tsx: &str, + result: &Map, +) -> Vec { + payload.snapshot.nodes.values() + .filter(|node| node.node_type == "COMPONENT_SET") + .filter_map(|node| { + let name = node.typed_view().name()?; + let name = devup_mcp_devup_ui::codegen::normalize_component_name(name); + tsx.contains(&format!("from \"@/components/{name}\"" )).then(|| json!({ + "name": name, "nodeId": node.id, + "nextAction": {"tool":"devup_figma_export", "arguments":{ + "artifactId":result["cache"]["artifactId"], + "frameIds":[node.id], "outputs":["componentTsx"] + }} + })) + }) + .collect() +} diff --git a/crates/devup-mcp/src/server/projection/feedback_codegen_tests.rs b/crates/devup-mcp/src/server/projection/feedback_codegen_tests.rs index 63596055..25354d6c 100644 --- a/crates/devup-mcp/src/server/projection/feedback_codegen_tests.rs +++ b/crates/devup-mcp/src/server/projection/feedback_codegen_tests.rs @@ -40,3 +40,61 @@ async fn feedback36_refusal_preserves_frame_files_and_artifact_recovery() { assert_ne!(result["status"], "complete"); std::fs::remove_dir_all(directory).unwrap(); } + +fn feedback32_component_payload() -> CollectedPayload { + let mut data = payload(); + data.target.node_id = Some("set".into()); + data.snapshot = serde_json::from_value(json!({ + "fileKey":"W1Fixture","version":null,"roots":["set"],"diagnostics":[], + "nodes":{ + "set":{"id":"set","type":"COMPONENT_SET","fields":{ + "name":"Button","width":900,"height":600,"childrenIds":["default","hover"], + "componentPropertyDefinitions":{"effect":{"type":"VARIANT","defaultValue":"default","variantOptions":["default","hover"]}} + }}, + "default":{"id":"default","type":"COMPONENT","fields":{ + "name":"effect=default","parentId":"set","childrenIds":["label"], + "variantProperties":{"effect":"default"},"width":100,"height":40, + "fills":[{"type":"SOLID","color":{"r":1,"g":0,"b":0}}] + }}, + "hover":{"id":"hover","type":"COMPONENT","fields":{ + "name":"effect=hover","parentId":"set","childrenIds":["hover-label"], + "variantProperties":{"effect":"hover"},"width":100,"height":40, + "fills":[{"type":"SOLID","color":{"r":0,"g":0,"b":1}}] + }}, + "label":{"id":"label","type":"TEXT","fields":{"characters":"Submit","parentId":"default"}}, + "hover-label":{"id":"hover-label","type":"TEXT","fields":{"characters":"Submit","parentId":"hover"}} + } + })).unwrap(); + data +} + +#[tokio::test] +async fn feedback32_component_set_has_no_canvas_coverage_failure() { + let result = project(feedback32_component_payload(), operation(&["tsx"])).await.unwrap(); + let code = result["tsx"].as_str().unwrap(); + assert_eq!(code.matches("export function Button").count(), 1); + assert!(!code.contains("900px"), "{code}"); + assert!(!result["projectionIssues"].as_array().into_iter().flatten().any(|issue| + issue["code"] == "DEVUP_CODEGEN_SECTION_COVERAGE" && issue["nodeId"] == "set" + ), "{result}"); +} + +#[tokio::test] +async fn feedback32_screen_reference_offers_component_export() { + let mut data = feedback32_component_payload(); + data.target.node_id = Some("screen".into()); + data.snapshot.roots = vec!["screen".into()]; + for (id, node) in [ + ("screen", json!({"id":"screen","type":"FRAME","fields":{"name":"Screen","childrenIds":["instance"]}})), + ("instance", json!({"id":"instance","type":"INSTANCE","fields":{"name":"Button","mainComponentId":"default","variantProperties":{"effect":"default"}}})), + ] { + data.snapshot.nodes.insert(id.into(), serde_json::from_value(node).unwrap()); + } + let result = project(data, operation(&["componentTsx"])).await.unwrap(); + assert!(result["componentTsx"].as_str().unwrap().contains("@/components/Button"), "{result}"); + let action = &result["componentDefinitions"][0]["nextAction"]; + assert_eq!(action["tool"], "devup_figma_export"); + assert_eq!(action["arguments"]["frameIds"], json!(["set"])); + assert_eq!(action["arguments"]["outputs"], json!(["componentTsx"])); + assert_eq!(action["arguments"]["artifactId"], result["cache"]["artifactId"]); +} diff --git a/crates/devup-mcp/src/server/projection/project_node_and_theme_outputs.rs b/crates/devup-mcp/src/server/projection/project_node_and_theme_outputs.rs index dcfaf5eb..9d3bfe19 100644 --- a/crates/devup-mcp/src/server/projection/project_node_and_theme_outputs.rs +++ b/crates/devup-mcp/src/server/projection/project_node_and_theme_outputs.rs @@ -172,6 +172,10 @@ pub(super) fn project_node_and_theme_outputs( ); match output { Ok(mut output) => { + let definitions = component_definition_actions(payload, &output.tsx, &result); + if !definitions.is_empty() { + result.insert("componentDefinitions".to_owned(), json!(definitions)); + } let coverage = super::super::asset_guard::export_coverage( &payload.snapshot, node_id, From 4c83715a95574276f487e04586845d661b0ee5a0 Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 17:06:05 +0900 Subject: [PATCH 024/149] fix(skills): prioritize structured user decisions after confirmation --- .../src/server/skills/devup/SKILL.md | 19 ++++++++++++++++--- .../devup-mcp/src/server/skills/manifest.json | 4 ++-- crates/devup-mcp/src/server/skills/tests.rs | 13 +++++++++++++ 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/crates/devup-mcp/src/server/skills/devup/SKILL.md b/crates/devup-mcp/src/server/skills/devup/SKILL.md index 5264e689..1af92a06 100644 --- a/crates/devup-mcp/src/server/skills/devup/SKILL.md +++ b/crates/devup-mcp/src/server/skills/devup/SKILL.md @@ -227,6 +227,19 @@ Confirm the ERD together with the screens, before any page is written. From the A screen is not finished while its data is a hard-coded array, a `TODO` or "replace when API exists" comment, a control with no handler, or a form that changes local state without a network call. If the user answers "wire to the API now", record the `operationId` and check that it exists in `openapi.json` before you call the screen done; if it does not, say the screen is unfinished instead of faking it. Anything left on a fixture must be recorded with `defer` and listed as unfinished in the final report. A public asset a page references that is missing from `public/` is a failure to fix by exporting it, not something to skip. -## Decisions go through the question tool - -Any decision that needs the user, before or after confirm, is asked with the runtime question tool, never as numbered prose such as "tell me which numbers to change". Show the evidence table first, then ask. Put the recommended option first, labelled "(Recommended)", with two or three concrete alternatives (for example "category as a kind column / as an enum column / as a free text column"). A header is at most 30 characters and a question has two to four options. Keep the confirmation of a table separate from the decisions inside it: the table is one approve-or-change question, each real decision gets its own entry in the same question call. Record each answer with `devup_project_plan defer` or `assume` (id, reason, recommended) so it survives in the plan; a decision that changes a model is applied to `models/*.json` and the next action says so. +## Decisions go through the question tool + +This rule takes precedence over "never ask during the build" and "never wait" +when a decision genuinely requires the user's authority, including after confirm. +Use OpenCode `question` or Claude Code `AskUserQuestion`, not a numbered chat +list ending with "tell me which numbers to change". Continue independent work +while the decision is pending; workers escalate to their supervisor, not the user. +Do not invent a project-plan `ask` action: use the runtime question tool and the +existing plan actions. Record the chosen answer, not an unaccepted recommendation, +with its decision id, affected model/endpoint/file and rationale. If it changes +the ERD, update the source models and re-confirm before dependent implementation. +If the runtime has no question tool, report that limitation and leave the +user-owned decision unresolved; a prose question or silent assumption is not a +substitute. Ordinary implementation choices still use defer/assume without asking. + +Any decision that needs the user, before or after confirm, is asked with the runtime question tool, never as numbered prose such as "tell me which numbers to change". Show the evidence table first, then ask. Put the recommended option first, labelled "(Recommended)", with two or three concrete alternatives (for example "category as a kind column / as an enum column / as a free text column"). A header is at most 30 characters and a question has two to four options. Keep the confirmation of a table separate from the decisions inside it: the table is one approve-or-change question, each real decision gets its own entry in the same question call. Record each answer with `devup_project_plan defer` or `assume` (id, reason, recommended) so it survives in the plan; a decision that changes a model is applied to `models/*.json` and the next action says so. diff --git a/crates/devup-mcp/src/server/skills/manifest.json b/crates/devup-mcp/src/server/skills/manifest.json index 53dd92b3..94af422f 100644 --- a/crates/devup-mcp/src/server/skills/manifest.json +++ b/crates/devup-mcp/src/server/skills/manifest.json @@ -89,8 +89,8 @@ "documents": [ { "path": "SKILL.md", - "bytes": 18452, - "sha256": "fa9de21d4e32e39a85e74896673afe68097c78831f75f1be19f1a702b9ab5b3e" + "bytes": 19411, + "sha256": "dd8cd591a962069d58feee746637acb1e17fe06d10f3e1d82732e98af75b2139" } ], "sourceUrl": "https://github.com/dev-five-git/devup-mcp/blob/HEAD/crates/devup-mcp/src/server/skills/devup", diff --git a/crates/devup-mcp/src/server/skills/tests.rs b/crates/devup-mcp/src/server/skills/tests.rs index 22d3528e..fb4ab919 100644 --- a/crates/devup-mcp/src/server/skills/tests.rs +++ b/crates/devup-mcp/src/server/skills/tests.rs @@ -5,6 +5,19 @@ mod tests { use super::*; use sha2::{Digest, Sha256}; + #[test] + fn user_decisions_override_never_wait_without_prose_questions() { + let text = include_str!("devup/SKILL.md"); + for required in [ + "This rule takes precedence over", + "OpenCode `question` or Claude Code `AskUserQuestion`", + "Do not invent a project-plan `ask` action", + "Record the chosen answer, not an unaccepted recommendation", + ] { + assert!(text.contains(required), "missing decision guidance: {required}"); + } + } + /// /devup per runtime: Claude Code gets it as the devup skill, OpenCode as /// a command file, and a runtime with no verified command location is /// told so instead of given a guessed path. From df29375cfc6e64545835d4a1af48ed2f29a1fdb0 Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 17:07:21 +0900 Subject: [PATCH 025/149] fix(runtime-audit): explain child-only interaction state changes --- harness/runtime-audit/checks.mjs | 9 ++++++--- harness/runtime-audit/run.mjs | 15 +++++++++++++-- harness/runtime-audit/test/checks.test.mjs | 9 +++++++++ harness/runtime-audit/test/layout.test.mjs | 14 ++++++++++++++ 4 files changed, 42 insertions(+), 5 deletions(-) diff --git a/harness/runtime-audit/checks.mjs b/harness/runtime-audit/checks.mjs index f2ff9ba7..6d2bb8e7 100644 --- a/harness/runtime-audit/checks.mjs +++ b/harness/runtime-audit/checks.mjs @@ -253,6 +253,9 @@ export const STATE_PROPERTIES = [ * holds each one, and any that changes nothing fails the check. */ export function evaluateInteraction(check, observed) { + const childHint = (item) => item.childChanges?.length + ? '; state styles are on a child (child computed style changes), not the focusable /button; put states on the focusable element itself' + : ''; if (Array.isArray(observed.items)) { const changes = (item) => STATE_PROPERTIES.some((property) => item.before[property] !== item.after[property]); const flat = observed.items.filter((item) => !changes(item)); @@ -261,8 +264,8 @@ export function evaluateInteraction(check, observed) { ? { status: 'pass', reason: `${check.state} changes all ${observed.items.length} links and buttons`, measured: { checked: observed.items.length } } : { status: 'fail', - reason: `${check.state} changes nothing on ${flat.length} of ${observed.items.length}: ${flat.slice(0, 5).map((item) => `<${item.tag}> "${item.name}"`).join(', ')}`, - measured: { checked: observed.items.length, unchanged: flat.map((item) => ({ tag: item.tag, name: item.name })) }, + reason: `${check.state} changes nothing on ${flat.length} of ${observed.items.length}: ${flat.slice(0, 5).map((item) => `<${item.tag}> "${item.name}"${childHint(item)}`).join(', ')}`, + measured: { checked: observed.items.length, unchanged: flat.map((item) => ({ tag: item.tag, name: item.name, childChanges: item.childChanges ?? [] })) }, }; } const subjectProblem = missing('subject', observed.subjectCount); @@ -270,7 +273,7 @@ export function evaluateInteraction(check, observed) { const changed = STATE_PROPERTIES.filter((property) => observed.before[property] !== observed.after[property]); return changed.length > 0 ? { status: 'pass', reason: `${check.state} changes ${changed.join(', ')}`, measured: { changed } } - : { status: 'fail', reason: `${check.state} changes nothing visible`, measured: { before: observed.before } }; + : { status: 'fail', reason: `${check.state} changes nothing visible${childHint(observed)}`, measured: { before: observed.before, childChanges: observed.childChanges ?? [] } }; } /** Counts recomputed from items, never taken from a declaration. */ diff --git a/harness/runtime-audit/run.mjs b/harness/runtime-audit/run.mjs index 4df64e5e..f66f2a63 100644 --- a/harness/runtime-audit/run.mjs +++ b/harness/runtime-audit/run.mjs @@ -509,11 +509,19 @@ async function stateStyles(locator) { }, STATE_PROPERTIES); } +async function childStateStyles(locator) { + return locator.evaluate((element, properties) => [...element.querySelectorAll('*')].map((child) => { + const style = getComputedStyle(child); + return { tag: child.tagName.toLowerCase(), styles: properties.map((property) => style[property]) }; + }), STATE_PROPERTIES); +} + // Hover moves the mouse over the subject; focus sets keyboard modality first // so :focus-visible applies, as it does for a keyboard user. Both wait out // the shared transition before reading the styles. async function enterState(page, target, state, timeout) { const before = await stateStyles(target); + const childrenBefore = await childStateStyles(target); if (state === 'focus') { await page.keyboard.press('Shift'); await target.focus({ timeout }); @@ -522,13 +530,16 @@ async function enterState(page, target, state, timeout) { } await page.waitForTimeout(450); const after = await stateStyles(target); + const childrenAfter = await childStateStyles(target); + const childChanges = childrenAfter.filter((child, index) => childrenBefore[index] + && child.styles.some((value, property) => value !== childrenBefore[index].styles[property])).map((child) => child.tag); if (state === 'focus') await target.evaluate((element) => element.blur()); await page.mouse.move(0, 0); await page.waitForTimeout(state === 'focus' ? 0 : 300); - return { before, after }; + return { before, after, childChanges }; } -async function observeInteraction(page, check, timeout) { +export async function observeInteraction(page, check, timeout) { if (!check.subject) { const all = page.locator('a[href], button, [role="button"]'); const items = []; diff --git a/harness/runtime-audit/test/checks.test.mjs b/harness/runtime-audit/test/checks.test.mjs index 280bbba1..4554b0b9 100644 --- a/harness/runtime-audit/test/checks.test.mjs +++ b/harness/runtime-audit/test/checks.test.mjs @@ -1,5 +1,14 @@ import assert from 'node:assert/strict'; import test from 'node:test'; + +test('interaction distinguishes a child-only state change from a dead state', () => { + const item = { tag: 'a', name: 'Company', before: {}, after: {}, childChanges: ['span'] }; + const result = evaluateInteraction({ state: 'hover' }, { items: [item] }); + assert.equal(result.status, 'fail'); + assert.match(result.reason, /state styles are on a child.*focusable/); + assert.deepEqual(result.measured.unchanged[0].childChanges, ['span']); + assert.match(evaluateInteraction({ state: 'focus' }, { subjectCount: 1, ...item }).reason, /state styles are on a child/); +}); import { GEOMETRY_ROUNDING_PX, STATE_PROPERTIES, evaluateBounds, evaluateHidden, evaluateCheck, evaluateClippedText, evaluateDimensions, evaluateInteraction, evaluatePageHealth, evaluateNoLowContrastText, evaluateImageLoaded, evaluateRenderedCompleteness, evaluateViewTransition, summarize, textContrast, diff --git a/harness/runtime-audit/test/layout.test.mjs b/harness/runtime-audit/test/layout.test.mjs index 9cd2bd39..b3cc5174 100644 --- a/harness/runtime-audit/test/layout.test.mjs +++ b/harness/runtime-audit/test/layout.test.mjs @@ -2,6 +2,20 @@ import assert from 'node:assert/strict'; import test from 'node:test'; import { evaluatePageHealth, evaluateTextAlignment, evaluateSequenceOrder, evaluateSectionLayout, evaluateStylesApplied } from '../checks.mjs'; import { observeSequence, observeSectionLayout, observeClippedContent, observeStylesApplied, runPlan } from '../run.mjs'; +import { observeInteraction } from '../run.mjs'; +import { evaluateInteraction } from '../checks.mjs'; + +test('browser interaction reports hover styles on a child of the focusable link', async () => { + const { chromium } = await import('playwright'); + const browser = await chromium.launch(); + try { + const page = await browser.newPage(); + await page.setContent('Company'); + const observed = await observeInteraction(page, { state: 'hover' }, 1000); + assert.deepEqual(observed.items[0].childChanges, ['span']); + assert.match(evaluateInteraction({ state: 'hover' }, observed).reason, /state styles are on a child/); + } finally { await browser.close(); } +}); test('numbered cards and STEP labels in one
are judged as two sequences', async () => { const { chromium } = await import('playwright'); From 822bd582914093a6c37ba7c4875430fbc50a645c Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 17:07:24 +0900 Subject: [PATCH 026/149] fix(screen-plan): match ancestor plans to the requested app (34) Assume legacy plans without app fields remain valid; explicit route app bindings must match. Search all ancestors rather than four levels. --- .../src/server/screen_plan/status.rs | 14 ++++++++++++-- .../screen_plan_tests/monorepo_root_plan.rs | 19 +++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/crates/devup-mcp/src/server/screen_plan/status.rs b/crates/devup-mcp/src/server/screen_plan/status.rs index 3084c11f..991f5691 100644 --- a/crates/devup-mcp/src/server/screen_plan/status.rs +++ b/crates/devup-mcp/src/server/screen_plan/status.rs @@ -505,9 +505,19 @@ pub(super) fn export_gate(request: &ExportRequest<'_>) -> Result Vec { } } out -} \ No newline at end of file +} diff --git a/crates/devup-mcp/src/server/screen_plan_tests/monorepo_root_plan.rs b/crates/devup-mcp/src/server/screen_plan_tests/monorepo_root_plan.rs index 25fdba69..4b58119a 100644 --- a/crates/devup-mcp/src/server/screen_plan_tests/monorepo_root_plan.rs +++ b/crates/devup-mcp/src/server/screen_plan_tests/monorepo_root_plan.rs @@ -28,3 +28,22 @@ fn r34_the_gate_finds_a_plan_confirmed_at_the_monorepo_root_from_an_app_director json!(["99:1"]) ); } + +#[test] +fn r34_ancestor_plan_must_match_the_requested_app() { + let fixture = Fixture::new(); + let mut plan = confirmed_plan(); + plan["routes"][0]["app"] = json!("apps/front"); + write_plan_file(&fixture, &plan); + fixture.write("apps/front/devup.json", "{}"); + fixture.write("apps/admin/devup.json", "{}"); + for (app, covered) in [("front", true), ("admin", false)] { + let root = fixture.0.join("apps").join(app); + let request = ExportRequest { + project_root: root.to_str().unwrap(), url: None, + frames: vec!["10:2".into()], all_screens: false, + scaffold_write: true, scaffold_route: Some(""), output_paths: Vec::new(), + }; + assert_eq!(matches!(export_gate(&request).unwrap(), Gate::Route(_)), covered); + } +} From 76d5e55cf0da05f3bd7f2c772d41ea7d9a24b7c9 Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 17:08:29 +0900 Subject: [PATCH 027/149] test(screen-plan): verify section device frames share responsive routes (19) Assume existing section-based device renaming is authoritative; reproduce Main Desktop/Mobile alongside another section without a mobile URL. --- .../src/server/screen_plan_tests/prevention.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/crates/devup-mcp/src/server/screen_plan_tests/prevention.rs b/crates/devup-mcp/src/server/screen_plan_tests/prevention.rs index 93c20ec4..7a1b0df6 100644 --- a/crates/devup-mcp/src/server/screen_plan_tests/prevention.rs +++ b/crates/devup-mcp/src/server/screen_plan_tests/prevention.rs @@ -72,3 +72,20 @@ async fn near_identical_widths_in_one_section_can_be_states_of_one_url() { assert_eq!(array(&routes[0]["states"]).len(), 2); assert_eq!(routes[0]["states"][0]["nodeId"], "30:2"); } +#[tokio::test] +async fn r19_device_only_frames_in_each_section_confirm_as_one_responsive_url() { + let fixture = Fixture::new(); + let screens = [ + sectioned("45:2066", "Desktop", 1440.0, "Main"), + sectioned("51:6599", "Mobile", 360.0, "Main"), + sectioned("60:1", "Desktop", 1440.0, "Company"), + sectioned("60:2", "Mobile", 360.0, "Company"), + ]; + let mut request = drafted(&fixture, &screens).await; + request.accept_recommended = true; + let result = confirm(&request, &fixture.policy()).unwrap(); + let routes = array(&result["routes"]); + assert_eq!(routes.len(), 2); + assert!(routes.iter().all(|route| array(&route["nodeIds"]).len() == 2)); + assert!(!routes.iter().any(|route| route["url"] == "/mobile")); +} From 42a96d463c6edced55d7434982e80dc5353d7e1b Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 17:08:46 +0900 Subject: [PATCH 028/149] fix(project-plan): retain unanswered intake questions on draft (01) --- .../src/server/project_plan/rounds.rs | 5 +++- .../src/server/project_plan/tests.rs | 1 + .../src/server/project_plan/tests/feedback.rs | 27 +++++++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 crates/devup-mcp/src/server/project_plan/tests/feedback.rs diff --git a/crates/devup-mcp/src/server/project_plan/rounds.rs b/crates/devup-mcp/src/server/project_plan/rounds.rs index 0dd5eda7..bb3ea899 100644 --- a/crates/devup-mcp/src/server/project_plan/rounds.rs +++ b/crates/devup-mcp/src/server/project_plan/rounds.rs @@ -135,7 +135,10 @@ pub(super) fn detail( plan: &Value, extra: &[screen_plan::ExtraQuestionInput], ) -> Result, DevupError> { - let mut questions = vec![]; + let mut questions = intake(&plan["facts"]) + .into_iter() + .filter(|question| plan["answers"][text(&question["id"])].is_null()) + .collect::>(); if array(&plan["features"]).is_empty() && array(&plan["issues"]).is_empty() { return Err(invalid( "draft needs features or Jira issues read from sources", diff --git a/crates/devup-mcp/src/server/project_plan/tests.rs b/crates/devup-mcp/src/server/project_plan/tests.rs index eced6d38..adc9b0a9 100644 --- a/crates/devup-mcp/src/server/project_plan/tests.rs +++ b/crates/devup-mcp/src/server/project_plan/tests.rs @@ -19,6 +19,7 @@ impl Drop for TestDir { } } use super::*; +mod feedback; fn input(root: &Path, action: &str) -> ProjectPlanInput { serde_json::from_value(json!({"action":action,"projectRoot":root})).unwrap() } diff --git a/crates/devup-mcp/src/server/project_plan/tests/feedback.rs b/crates/devup-mcp/src/server/project_plan/tests/feedback.rs new file mode 100644 index 00000000..8b27fcac --- /dev/null +++ b/crates/devup-mcp/src/server/project_plan/tests/feedback.rs @@ -0,0 +1,27 @@ +use super::*; + +#[tokio::test] +async fn draft_accepts_new_evidence_before_intake_answers_without_losing_them() { + let dir = TestDir::new(); + let policy = OutputPolicy::from_roots(vec![dir.path().to_path_buf()]).unwrap(); + let mut request = input(dir.path(), "intake"); + let intake = run(&request, &policy).await.unwrap(); + request.plan_id = intake["planId"].as_str().map(str::to_owned); + request.action = "draft".into(); + request.features = vec![Feature { + id: "home".into(), scope: "Company introduction".into(), + screens: vec!["Home".into()], acceptance: vec![], entities: vec![], + }]; + request.evidence = vec![serde_json::from_value(json!({ + "source":"requirements.pdf", "facts":{"goal":"Publish the company site"} + })).unwrap()]; + let draft = run(&request, &policy).await.unwrap(); + assert_eq!(draft["state"], "draft"); + assert!(array(&draft["openQuestionIds"]).contains(&json!("parts"))); + assert_eq!(read(&request.project_root).unwrap()["facts"]["goal"], "Publish the company site"); + request.features.clear(); + request.evidence.clear(); + request.answers.insert("parts".into(), json!("front only")); + run(&request, &policy).await.unwrap(); + assert_eq!(read(&request.project_root).unwrap()["answers"]["parts"], "front only"); +} From b786f397b74c900e88dc04343836f8b9c70fb1ca Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 17:08:48 +0900 Subject: [PATCH 029/149] fix(runtime-audit): exclude clipped marquees and recognize descending rows --- harness/runtime-audit/checks.mjs | 14 +++++------- harness/runtime-audit/run.mjs | 13 ++++++++++- harness/runtime-audit/test/checks.test.mjs | 4 ++-- harness/runtime-audit/test/layout.test.mjs | 25 ++++++++++++++++++++++ 4 files changed, 44 insertions(+), 12 deletions(-) diff --git a/harness/runtime-audit/checks.mjs b/harness/runtime-audit/checks.mjs index 6d2bb8e7..53080271 100644 --- a/harness/runtime-audit/checks.mjs +++ b/harness/runtime-audit/checks.mjs @@ -37,19 +37,16 @@ export function evaluateCheck(check, measured) { const tolerance = check.tolerancePx ?? DEFAULT_TOLERANCE; if (check.kind === 'no-horizontal-overflow') { const { viewportWidth, scrollWidth, maxRight, overflowHidden } = measured.page; - const widest = Math.max(scrollWidth, maxRight); - const overflow = widest - viewportWidth; + const overflow = scrollWidth - viewportWidth; const evidence = { viewportWidth, scrollWidth: round(scrollWidth), maxRight: round(maxRight), overflowHidden }; if (overflow > tolerance) { return { status: 'fail', - reason: maxRight > scrollWidth + tolerance - ? `content reaches ${round(maxRight)}px in a ${viewportWidth}px viewport, clipped by overflow so the page does not scroll` - : `the page scrolls to ${round(scrollWidth)}px in a ${viewportWidth}px viewport`, + reason: `the page scrolls to ${round(scrollWidth)}px in a ${viewportWidth}px viewport`, measured: evidence, }; } - return { status: 'pass', reason: 'nothing reaches past the viewport', measured: evidence }; + return { status: 'pass', reason: maxRight > viewportWidth + tolerance ? 'content is clipped by overflow; the page does not scroll horizontally' : 'nothing reaches past the viewport', measured: evidence }; } const subjectProblem = missing('subject', measured.subjectCount); @@ -471,9 +468,6 @@ export function evaluateSequenceOrder(observed) { const numbers = group.map((item) => item.number).sort((a, b) => a - b); const consecutive = numbers.every((n, i) => i === 0 || n === numbers[i - 1] + 1); if (!consecutive) continue; - const reading = [...group].sort((a, b) => a.y - b.y || a.x - b.x).map((item) => item.number); - if (reading.length > 1 && reading.every((n, i) => i === 0 || n === reading[i - 1] - 1)) continue; - compared += 1; const remaining = [...group]; const ordered = []; while (remaining.length) { @@ -482,6 +476,8 @@ export function evaluateSequenceOrder(observed) { ordered.push(...row); for (const item of row) remaining.splice(remaining.indexOf(item), 1); } + if (ordered.length > 1 && ordered.every((item, index) => index === 0 || item.number === ordered[index - 1].number - 1)) continue; + compared += 1; for (let index = 1; index < ordered.length; index += 1) { if (ordered[index].number < ordered[index - 1].number) failures.push(`${ordered[index - 1].name} before ${ordered[index].name}`); } diff --git a/harness/runtime-audit/run.mjs b/harness/runtime-audit/run.mjs index f66f2a63..63ff1f69 100644 --- a/harness/runtime-audit/run.mjs +++ b/harness/runtime-audit/run.mjs @@ -326,7 +326,17 @@ export function observeCompleteness(page, completeness, width, subject) { for (let index = 0; index < walker.length; index += 1) { const element = walker[index]; const own = [...element.childNodes].filter((node) => node.nodeType === 3).map((node) => node.textContent.trim()).join(' ').trim(); - if (!own || !shown(element) || element.closest('[aria-hidden="true"]')) continue; + if (!own || !shown(element) || element.closest('[aria-hidden="true"], [role="presentation"], [role="none"]')) continue; + let marquee = false; + for (let track = element; track && track !== document.body; track = track.parentElement) { + const animations = track.getAnimations?.() ?? []; + const translates = animations.some((animation) => animation.effect?.getKeyframes().some((frame) => frame.transform || frame.translate)); + if (!translates) continue; + for (let clip = track.parentElement; clip; clip = clip.parentElement) { + if (['hidden', 'clip'].includes(getComputedStyle(clip).overflowX) && track.scrollWidth > clip.clientWidth + 1) marquee = true; + } + } + if (marquee) continue; const rect = element.getBoundingClientRect(); if (rect.width === 0 || rect.height === 0) continue; const style = getComputedStyle(element); @@ -404,6 +414,7 @@ export function observeSequence(page) { for (const element of document.querySelectorAll('body *')) { if (element.children.length || !element.checkVisibility({ opacityProperty: true, visibilityProperty: true })) continue; const label = element.textContent.trim(); + if (element.closest('nav[aria-label*="pag" i], [role="navigation"][aria-label*="pag" i]')) continue; const match = /^(?:(STEP)\s*(\d{1,3})|(0\d|[1-9]\d?))[.?:]?$/i.exec(label); if (!match) continue; const box = element.getBoundingClientRect(); diff --git a/harness/runtime-audit/test/checks.test.mjs b/harness/runtime-audit/test/checks.test.mjs index 4554b0b9..64ab284f 100644 --- a/harness/runtime-audit/test/checks.test.mjs +++ b/harness/runtime-audit/test/checks.test.mjs @@ -18,13 +18,13 @@ import { validateDimensionsPlan } from '../run.mjs'; const page = { viewportWidth: 360, scrollWidth: 360, maxRight: 340, overflowHidden: false }; const box = (x, y, width, height) => ({ x, y, width, height }); -test('overflow counts content clipped by overflow:hidden, not only scrolling', () => { +test('overflow fails only when the document actually scrolls horizontally', () => { assert.equal(evaluateCheck({ kind: 'no-horizontal-overflow' }, { page }).status, 'pass'); const clipped = evaluateCheck( { kind: 'no-horizontal-overflow' }, { page: { ...page, maxRight: 400, overflowHidden: true } }, ); - assert.equal(clipped.status, 'fail'); + assert.equal(clipped.status, 'pass'); assert.match(clipped.reason, /clipped by overflow/); const scrolling = evaluateCheck({ kind: 'no-horizontal-overflow' }, { page: { ...page, scrollWidth: 420 } }); assert.equal(scrolling.status, 'fail'); diff --git a/harness/runtime-audit/test/layout.test.mjs b/harness/runtime-audit/test/layout.test.mjs index b3cc5174..fe0cf2d3 100644 --- a/harness/runtime-audit/test/layout.test.mjs +++ b/harness/runtime-audit/test/layout.test.mjs @@ -4,6 +4,31 @@ import { evaluatePageHealth, evaluateTextAlignment, evaluateSequenceOrder, evalu import { observeSequence, observeSectionLayout, observeClippedContent, observeStylesApplied, runPlan } from '../run.mjs'; import { observeInteraction } from '../run.mjs'; import { evaluateInteraction } from '../checks.mjs'; +import { observeCompleteness } from '../run.mjs'; + +test('decorative and marquee contrast is excluded without hiding ordinary animated text', async () => { + const { chromium } = await import('playwright'); + const browser = await chromium.launch(); + try { + const page = await browser.newPage({ viewport: { width: 390, height: 900 } }); + await page.setContent('
decoration
marquee

ordinary

'); + const observed = await observeCompleteness(page, {}, 390); + assert.deepEqual(observed.texts.map((item) => item.text), ['ordinary']); + assert.deepEqual((await observeClippedContent(page)).clipped, []); + } finally { await browser.close(); } +}); + +test('browser sequence separates descending notices and pagination from numbered cards', async () => { + const { chromium } = await import('playwright'); + const browser = await chromium.launch(); + try { + const page = await browser.newPage(); + await page.setContent('
6
5
4
'); + assert.equal(evaluateSequenceOrder(await observeSequence(page)).status, 'pass'); + await page.setContent('
060504
'); + assert.equal(evaluateSequenceOrder(await observeSequence(page)).status, 'pass'); + } finally { await browser.close(); } +}); test('browser interaction reports hover styles on a child of the focusable link', async () => { const { chromium } = await import('playwright'); From 467e664ea4edba81c8ffa15e4c4df61db80e9718 Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 17:10:02 +0900 Subject: [PATCH 030/149] fix(project-plan): keep status summaries compact (03) --- crates/devup-mcp/src/server/project_plan/mod.rs | 6 ++++++ .../src/server/project_plan/tests/feedback.rs | 15 +++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/crates/devup-mcp/src/server/project_plan/mod.rs b/crates/devup-mcp/src/server/project_plan/mod.rs index a733a8e0..c1905d32 100644 --- a/crates/devup-mcp/src/server/project_plan/mod.rs +++ b/crates/devup-mcp/src/server/project_plan/mod.rs @@ -601,6 +601,12 @@ pub(super) async fn run( write(&mut tx, policy, &input.project_root, &plan)?; tx.commit()?; let mut out = response(plan, &input.project_root, full); + if input.action == "status" && !full { + if let Some(summary) = out.as_object_mut() { + summary.remove("questionRounds"); + summary.remove("proposalMarkdown"); + } + } if !forward.is_null() { out["screenPlanForward"] = forward; } diff --git a/crates/devup-mcp/src/server/project_plan/tests/feedback.rs b/crates/devup-mcp/src/server/project_plan/tests/feedback.rs index 8b27fcac..7fa2e9fd 100644 --- a/crates/devup-mcp/src/server/project_plan/tests/feedback.rs +++ b/crates/devup-mcp/src/server/project_plan/tests/feedback.rs @@ -1,5 +1,20 @@ use super::*; +#[tokio::test] +async fn status_summary_does_not_duplicate_questions_or_source_facts() { + let dir = TestDir::new(); + let (mut request, policy, _) = drafted(dir.path(), false).await; + request.action = "status".into(); + request.detail = None; + let summary = run(&request, &policy).await.unwrap(); + assert!(summary.get("plan").is_none()); + assert!(summary.get("questionRounds").is_none()); + assert!(summary.get("proposalMarkdown").is_none()); + assert!(summary["openQuestionCount"].as_u64().unwrap() > 0); + assert!(summary["nextAction"].is_object()); + assert!(serde_json::to_vec(&summary).unwrap().len() < 8192); +} + #[tokio::test] async fn draft_accepts_new_evidence_before_intake_answers_without_losing_them() { let dir = TestDir::new(); From c0091285e3de2e020f9e1067acdf5d636d0c90bb Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 17:10:47 +0900 Subject: [PATCH 031/149] fix(screen-plan): block completion until every URL is verified (33) Assume export progress counts written pages, explicitly labeled as requiring runtime verification; status remains the implementation authority. Response attachment and its tests form one contract. --- .../mod_parts/exploration_and_responses.rs | 5 ++++ .../src/server/screen_plan/status.rs | 30 +++++++++++++++++++ .../server/screen_plan_tests/prevention.rs | 26 ++++++++++++++++ 3 files changed, 61 insertions(+) diff --git a/crates/devup-mcp/src/server/mod_parts/exploration_and_responses.rs b/crates/devup-mcp/src/server/mod_parts/exploration_and_responses.rs index 1b1c020d..871a4fb1 100644 --- a/crates/devup-mcp/src/server/mod_parts/exploration_and_responses.rs +++ b/crates/devup-mcp/src/server/mod_parts/exploration_and_responses.rs @@ -447,6 +447,11 @@ fn with_project_checks( let Some(project_root) = project_root else { return result; }; + match screen_plan::export_progress(project_root) { + Ok(progress) if !progress.is_null() => result["planProgress"] = progress, + Err(error) => result["planProgress"] = json!({"state": "unavailable", "reason": error.message}), + _ => {} + } if generated.is_empty() { // Resource delivery hands the code back as a link, so there is no // source here to check. Saying so is the point: an absent verdict diff --git a/crates/devup-mcp/src/server/screen_plan/status.rs b/crates/devup-mcp/src/server/screen_plan/status.rs index 991f5691..0b0384bf 100644 --- a/crates/devup-mcp/src/server/screen_plan/status.rs +++ b/crates/devup-mcp/src/server/screen_plan/status.rs @@ -316,6 +316,10 @@ pub(super) fn status_report( for item in &mut queue { item["done"] = json!(false); } } let final_report = super::audit_plan::final_report(plan, &rows, &trailing) + &link_markdown; + let blocking_urls = rows.iter().filter(|row| { + row["implemented"] != "yes" || array(&row["widths"]).iter().any(|width| width["result"] != "pass") + }).map(|row| row["url"].clone()).collect::>(); + let done = blocking_urls.is_empty() && !links_fail && incomplete == 0 && open.is_empty(); let converter_bugs = plan["audit"]["converterBugs"] .as_object() .map(|bugs| bugs.values().cloned().collect::>()) @@ -325,6 +329,9 @@ pub(super) fn status_report( "unresolved": unresolved, "converterBugs": converter_bugs, "workQueue": queue, + "done": done, + "blockingUrls": blocking_urls, + "completion": {"done": done, "blockingUrls": blocking_urls, "reportAllowed": done}, "pendingDecisions": plan["pendingDecisions"], "finalReport": final_report, "linkTable": link_table, @@ -717,3 +724,26 @@ fn assumption_warnings(plan: &Value) -> Vec { } out } + +pub(super) fn export_progress(project_root: &str) -> Result { + for root in Path::new(project_root).ancestors() { + let Some(plan) = read_plan(&root.to_string_lossy())? else { continue; }; + if plan["status"] != "confirmed" { continue; } + let requested = dunce::canonicalize(project_root).ok(); + let routes = array(&plan["routes"]).iter().filter(|route| { + route["url"].is_string() && route["implementation"] != "state" + && route["app"].as_str().is_none_or(|app| { + requested.as_ref().is_some_and(|requested| { + dunce::canonicalize(root.join(app)).ok().as_ref() == Some(requested) + || dunce::canonicalize(root).ok().as_ref() == Some(requested) + }) + }) + }).collect::>(); + if routes.is_empty() { continue; } + let implemented = routes.iter().filter(|route| route["file"].as_str().is_some_and(|file| root.join(file).is_file())).count(); + let next = routes.iter().find(|route| !route["file"].as_str().is_some_and(|file| root.join(file).is_file())).map(|route| route["url"].clone()); + return Ok(json!({"planId": plan["planId"], "implemented": implemented, "total": routes.len(), "next": next, + "implementationEvidence": "page files on disk; runtime verification remains required by screen-plan status"})); + } + Ok(Value::Null) +} diff --git a/crates/devup-mcp/src/server/screen_plan_tests/prevention.rs b/crates/devup-mcp/src/server/screen_plan_tests/prevention.rs index 7a1b0df6..257c575b 100644 --- a/crates/devup-mcp/src/server/screen_plan_tests/prevention.rs +++ b/crates/devup-mcp/src/server/screen_plan_tests/prevention.rs @@ -89,3 +89,29 @@ async fn r19_device_only_frames_in_each_section_confirm_as_one_responsive_url() assert!(routes.iter().all(|route| array(&route["nodeIds"]).len() == 2)); assert!(!routes.iter().any(|route| route["url"] == "/mobile")); } +#[test] +fn r33_one_built_route_does_not_complete_a_fifteen_url_plan() { + let mut plan = confirmed_plan(); + let routes = (0..15).map(|index| json!({ + "url": if index == 0 { "/".to_owned() } else { format!("/page-{index}") }, + "screen": format!("frame-{index}"), "nodeIds": [format!("frame-{index}")], + "implementation": "page", "drawnWidths": [1440], "file": format!("src/app/page-{index}/page.tsx") + })).collect::>(); + plan["routes"] = json!(routes); + let audit = json!({"widthTable": [{"width":1440}], "screenTable": + ([1440,1100,858,390].map(|width| json!({"routePath":"/", "width":width,"status":"pass","columns":{"completeness":"pass"}})))}); + let report = status_report(&plan, &evidence(), Some(&audit)); + assert_eq!(array(&report["workQueue"]).len(), 15); + assert_eq!(array(&report["blockingUrls"]).len(), 14); + assert_eq!(report["done"], false); + assert_eq!(report["completion"]["reportAllowed"], false); +} +#[test] +fn r33_export_progress_retains_the_next_unwritten_url() { + let fixture = Fixture::new(); + write_plan_file(&fixture, &confirmed_plan()); + let progress = export_progress(fixture.root()).unwrap(); + assert_eq!(progress["implemented"], 1); + assert_eq!(progress["total"], 2); + assert_eq!(progress["next"], "/about"); +} From 813bc1170b783b85332ba11786b3c5a1fe2951df Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 17:12:27 +0900 Subject: [PATCH 032/149] fix(project-plan): apply new feature answers in one draft (09) --- .../devup-mcp/src/server/project_plan/mod.rs | 13 ++++++++++++- .../src/server/project_plan/rounds.rs | 7 ++----- .../src/server/project_plan/tests/feedback.rs | 18 ++++++++++++++++++ 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/crates/devup-mcp/src/server/project_plan/mod.rs b/crates/devup-mcp/src/server/project_plan/mod.rs index c1905d32..6a1ae438 100644 --- a/crates/devup-mcp/src/server/project_plan/mod.rs +++ b/crates/devup-mcp/src/server/project_plan/mod.rs @@ -472,7 +472,11 @@ pub(super) async fn run( if plan["state"] == "confirmed" { return Err(invalid("Confirmed plans cannot be redrafted")); } - rounds::answers(&mut plan, &answers)?; + let previous_answers = answers.iter() + .filter(|(id, _)| array(&plan["questions"]).iter().any(|question| question["id"] == **id)) + .map(|(id, answer)| (id.clone(), answer.clone())) + .collect(); + rounds::answers(&mut plan, &previous_answers)?; if input.accept_recommended { rounds::accept_recommended(&mut plan)?; } @@ -509,6 +513,13 @@ pub(super) async fn run( } erd::draft(input, &mut plan)?; plan["questions"] = json!(rounds::detail(&plan, &input.extra_questions)?); + let new_answers = answers.into_iter() + .filter(|(id, _)| !previous_answers.contains_key(id)) + .collect(); + rounds::answers(&mut plan, &new_answers)?; + if input.accept_recommended { + rounds::accept_recommended(&mut plan)?; + } plan["state"] = json!("draft"); } "confirm" => { diff --git a/crates/devup-mcp/src/server/project_plan/rounds.rs b/crates/devup-mcp/src/server/project_plan/rounds.rs index bb3ea899..6b5589a6 100644 --- a/crates/devup-mcp/src/server/project_plan/rounds.rs +++ b/crates/devup-mcp/src/server/project_plan/rounds.rs @@ -153,9 +153,7 @@ pub(super) fn detail( .or_else(|| feature["key"].as_str()) .unwrap_or("feature"); let haystack = format!("{} {}", text(&feature["scope"]), feature["acceptance"]).to_lowercase(); - let has_screens = !array(&feature["screens"]).is_empty(); - let topics: [(&str, &str, &[&str], [&str; 3]); 5] = [ - ("screens", "the screens/routes and user roles (the feature lists none)", &[], ["Derive screens from the acceptance criteria", "List screens now", "Exclude this capability"]), + let topics: [(&str, &str, &[&str], [&str; 3]); 4] = [ ("states", "empty, loading and error states", &[], ["Use recorded feature acceptance criteria", "Standard empty/loading/error states", "Exclude this capability"]), ("permissions", "permissions and ownership", &["admin", "auth", "login", "role", "permission", "owner", "member"], ["Admins only", "Signed-in users", "Public"]), ("forms", "forms and field validation", &["form", "input", "inquiry", "contact", "signup", "register", "submit", "search", "write", "edit"], ["Required fields with inline validation", "Use recorded feature acceptance criteria", "Exclude this capability"]), @@ -163,13 +161,12 @@ pub(super) fn detail( ]; for (topic, prompt, keywords, options) in topics { let relevant = match topic { - "screens" => !has_screens, "states" => true, _ => keywords.iter().any(|k| haystack.contains(k)), }; let key = format!("{id}.{topic}"); if relevant && plan["facts"][&key].is_null() { - questions.push(ask(&key, format!("For {} ({id}), specify {prompt}. Screens: {}. Record a concrete decision or explicitly exclude the capability.", text(&feature["scope"]), feature["screens"]), &options)); + questions.push(ask(&key, format!("For {} ({id}), specify {prompt}. Screens: {}. Acceptance: {}. Record a concrete decision or explicitly exclude the capability.", text(&feature["scope"]), feature["screens"], feature["acceptance"]), &options)); } } } diff --git a/crates/devup-mcp/src/server/project_plan/tests/feedback.rs b/crates/devup-mcp/src/server/project_plan/tests/feedback.rs index 7fa2e9fd..dad70171 100644 --- a/crates/devup-mcp/src/server/project_plan/tests/feedback.rs +++ b/crates/devup-mcp/src/server/project_plan/tests/feedback.rs @@ -1,5 +1,23 @@ use super::*; +#[tokio::test] +async fn new_feature_acceptance_and_answers_are_applied_in_one_draft() { + let dir = TestDir::new(); + let (mut request, policy, _) = drafted(dir.path(), false).await; + request.action = "draft".into(); + request.answers.insert("inquiry.forms".into(), json!("Require staff email")); + request.features = vec![Feature { + id:"inquiry".into(), scope:"Contact form".into(), screens:vec![], + acceptance:vec!["Validate email before submission".into()], entities:vec![], + }]; + let draft = run(&request, &policy).await.unwrap(); + assert_eq!(draft["plan"]["answers"]["inquiry.forms"], "Require staff email"); + assert!(!array(&draft["plan"]["questions"]).iter().any(|question| question["id"] == "inquiry.screens")); + assert_eq!(draft["featuresWithoutScreens"]["features"], json!(["inquiry"])); + let form = array(&draft["plan"]["questions"]).iter().find(|question| question["id"] == "inquiry.forms").unwrap(); + assert!(text(&form["question"]).contains("Validate email before submission")); +} + #[tokio::test] async fn status_summary_does_not_duplicate_questions_or_source_facts() { let dir = TestDir::new(); From cfef5cc322a49aaccaf477b0bca60fdf933177f0 Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 17:12:31 +0900 Subject: [PATCH 033/149] fix(screen-plan): report frame owning pages in status and audit plans (47) Assume missing page evidence stays null, never infer it from viewer context. The shared table feeds both response surfaces and persisted audit metadata with its regression test. --- crates/devup-mcp/src/server/screen_plan/actions.rs | 2 ++ crates/devup-mcp/src/server/screen_plan/remap.rs | 14 +++++++++++++- crates/devup-mcp/src/server/screen_plan/status.rs | 1 + .../src/server/screen_plan_tests/prevention.rs | 14 ++++++++++++++ 4 files changed, 30 insertions(+), 1 deletion(-) diff --git a/crates/devup-mcp/src/server/screen_plan/actions.rs b/crates/devup-mcp/src/server/screen_plan/actions.rs index eebb82b9..c325b213 100644 --- a/crates/devup-mcp/src/server/screen_plan/actions.rs +++ b/crates/devup-mcp/src/server/screen_plan/actions.rs @@ -627,6 +627,8 @@ pub(super) async fn audit_plan( plan["audit"]["plans"] = json!(entries); plan["audit"]["maxFixAttempts"] = json!(max_fix_attempts); plan["audit"]["generatedAt"] = json!(now()); + result["framePages"] = json!(frame_page_table(&plan)); + plan["audit"]["framePages"] = result["framePages"].clone(); result["writtenTo"] = write_plan(policy, root, &plan)?; result["maxFixAttempts"] = json!(max_fix_attempts); result["doneWhen"] = json!(DONE_RULE); diff --git a/crates/devup-mcp/src/server/screen_plan/remap.rs b/crates/devup-mcp/src/server/screen_plan/remap.rs index ab9ee1b2..9c70640f 100644 --- a/crates/devup-mcp/src/server/screen_plan/remap.rs +++ b/crates/devup-mcp/src/server/screen_plan/remap.rs @@ -106,4 +106,16 @@ fn page_warnings(plan: &Value) -> Vec { ) }) .collect() -} \ No newline at end of file +} + +fn frame_page_table(plan: &Value) -> Vec { + array(&plan["routes"]).iter().flat_map(|route| { + array(&route["nodeIds"]).iter().map(move |node| { + let frame = array(&plan["screens"]).iter().flat_map(|screen| array(&screen["frames"])) + .find(|frame| frame["nodeId"] == *node); + json!({"url": route["url"], "nodeId": node, + "pageId": frame.map(|frame| &frame["pageId"]), + "pageName": frame.map(|frame| &frame["pageName"])}) + }) + }).collect() +} diff --git a/crates/devup-mcp/src/server/screen_plan/status.rs b/crates/devup-mcp/src/server/screen_plan/status.rs index 0b0384bf..06e08b87 100644 --- a/crates/devup-mcp/src/server/screen_plan/status.rs +++ b/crates/devup-mcp/src/server/screen_plan/status.rs @@ -353,6 +353,7 @@ pub(super) fn status_report( "warnings": warnings, "excluded": excluded, "frameInventory": plan["frameInventory"], + "framePages": frame_page_table(plan), "linkLabelCheck": plan["linkLabelCheck"], "inventoryNote": evidence.inventory_note, "rule": "A width without a runtime audit item is not-run, never pass. Implemented requires rendered completeness at every drawn width; done requires every URL at every required width to pass.", diff --git a/crates/devup-mcp/src/server/screen_plan_tests/prevention.rs b/crates/devup-mcp/src/server/screen_plan_tests/prevention.rs index 257c575b..00b85e05 100644 --- a/crates/devup-mcp/src/server/screen_plan_tests/prevention.rs +++ b/crates/devup-mcp/src/server/screen_plan_tests/prevention.rs @@ -115,3 +115,17 @@ fn r33_export_progress_retains_the_next_unwritten_url() { assert_eq!(progress["total"], 2); assert_eq!(progress["next"], "/about"); } +#[test] +fn r47_status_reports_the_owning_page_for_every_planned_frame() { + let mut plan = confirmed_plan(); + plan["screens"] = json!([{"frames":[ + {"nodeId":"10:1","pageId":"0:1","pageName":"Final"}, + {"nodeId":"10:2","pageId":"0:1","pageName":"Final"}, + {"nodeId":"10:3","pageId":"0:2","pageName":"Archive"} + ]}]); + let report = status_report(&plan, &evidence(), None); + assert_eq!(array(&report["framePages"]).len(), 4); + assert_eq!(report["framePages"][0]["pageName"], "Final"); + assert_eq!(report["framePages"][2]["pageId"], "0:2"); + assert_eq!(report["framePages"][3]["pageName"], Value::Null); +} From 4e7bcf7ca2a3c76a629930b41db2a21dd5408871 Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 17:13:42 +0900 Subject: [PATCH 034/149] test(project-plan): reproduce Korean specification discovery (06) --- .../src/server/project_plan/tests/feedback.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/crates/devup-mcp/src/server/project_plan/tests/feedback.rs b/crates/devup-mcp/src/server/project_plan/tests/feedback.rs index dad70171..4afb8dab 100644 --- a/crates/devup-mcp/src/server/project_plan/tests/feedback.rs +++ b/crates/devup-mcp/src/server/project_plan/tests/feedback.rs @@ -1,5 +1,21 @@ use super::*; + +#[tokio::test] +async fn intake_discovers_korean_spec_documents_without_extracting_contents() { + let dir = TestDir::new(); + let spec = "동국대학교 콘드로젠 요구사항 정의서 ver 0.5.pdf"; + std::fs::write(dir.path().join(spec), b"pdf contents are not evidence").unwrap(); + std::fs::write(dir.path().join("unrelated.pdf"), b"not a spec").unwrap(); + let policy = OutputPolicy::from_roots(vec![dir.path().to_path_buf()]).unwrap(); + let request = input(dir.path(), "intake"); + run(&request, &policy).await.unwrap(); + let saved = read(&request.project_root).unwrap(); + assert_eq!(saved["facts"]["specCandidates"], json!([spec])); + assert!(array(&saved["questions"]).iter().any(|question| question["id"] == "sources" && text(&question["question"]).contains(spec))); + assert!(array(&saved["evidence"]).is_empty()); +} + #[tokio::test] async fn new_feature_acceptance_and_answers_are_applied_in_one_draft() { let dir = TestDir::new(); From e7634c758d8f91cafa2ecd4d12f0d5fe731efcdf Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 17:13:47 +0900 Subject: [PATCH 035/149] fix(validation): reject unfinished data wiring and missing assets --- crates/devup-mcp-devup-ui/src/ui_structure.rs | 1 + .../src/ui_validate/types_and_helpers.rs | 5 +- .../src/ui_validate/wiring.rs | 202 ++++++++++++++++++ crates/devup-mcp/src/server/asset_guard.rs | 6 + .../ui_validate_response/section_coverage.rs | 3 + 5 files changed, 216 insertions(+), 1 deletion(-) create mode 100644 crates/devup-mcp-devup-ui/src/ui_validate/wiring.rs diff --git a/crates/devup-mcp-devup-ui/src/ui_structure.rs b/crates/devup-mcp-devup-ui/src/ui_structure.rs index 44920094..22aae20c 100644 --- a/crates/devup-mcp-devup-ui/src/ui_structure.rs +++ b/crates/devup-mcp-devup-ui/src/ui_structure.rs @@ -82,6 +82,7 @@ pub fn validate_bundle( let mut report = validate_source(&file.content, theme, strict, source_type(&file.path)); if !report.violations.iter().any(|v| v.rule == "invalid-syntax") { report.violations.extend(structural(file, boundary)); + report.violations.extend(super::wiring::fixture_findings(files, &file.path, &file.content)); report .violations .extend(super::site::interaction_findings(&file.path, &file.content)); diff --git a/crates/devup-mcp-devup-ui/src/ui_validate/types_and_helpers.rs b/crates/devup-mcp-devup-ui/src/ui_validate/types_and_helpers.rs index 9ab1ffa7..d6602c59 100644 --- a/crates/devup-mcp-devup-ui/src/ui_validate/types_and_helpers.rs +++ b/crates/devup-mcp-devup-ui/src/ui_validate/types_and_helpers.rs @@ -4,6 +4,8 @@ use std::collections::BTreeSet; mod blur_layers; #[path = "site.rs"] pub mod site; +#[path = "wiring.rs"] +pub mod wiring; use oxc_allocator::Allocator; use oxc_ast::ast::{ @@ -147,6 +149,7 @@ fn validate_source( }; visitor.visit_program(&parsed.program); violations.extend(visitor.violations); + violations.extend(wiring::findings(tsx)); let checked_tokens = visitor.checked_tokens; let attributes = crate::provenance::attributes::generated_attributes(tsx); violations.extend(blur_layers::check(tsx, &attributes)); @@ -693,4 +696,4 @@ pub fn rule_names() -> BTreeSet<&'static str> { ] .into_iter() .collect() -} \ No newline at end of file +} diff --git a/crates/devup-mcp-devup-ui/src/ui_validate/wiring.rs b/crates/devup-mcp-devup-ui/src/ui_validate/wiring.rs new file mode 100644 index 00000000..1cf07eec --- /dev/null +++ b/crates/devup-mcp-devup-ui/src/ui_validate/wiring.rs @@ -0,0 +1,202 @@ +use super::{Severity, Violation}; +use oxc_allocator::Allocator; +use oxc_ast::ast::*; +use oxc_ast_visit::{Visit, walk}; +use oxc_parser::Parser; +use oxc_span::{SourceType, Span}; + +fn error(span: Span, rule: &'static str, message: &str) -> Violation { + Violation { + rule, + severity: Severity::Error, + byte_range: [span.start as usize, span.end as usize], + context: Default::default(), + message: message.into(), + suggestion: Some("Wire the control and data to the real application contract before reporting completion.".into()), + } +} + +pub fn findings(source: &str) -> Vec { + let allocator = Allocator::default(); + let parsed = Parser::new(&allocator, source, SourceType::tsx()).parse(); + if !parsed.diagnostics.is_empty() { + return Vec::new(); + } + let mut found = Vec::new(); + for comment in &parsed.program.comments { + let text = &source[comment.span.start as usize..comment.span.end as usize]; + let lower = text.to_lowercase(); + if ["todo", "api 생기면", "replace when"].iter().any(|marker| lower.contains(marker)) { + found.push(error(comment.span, "deferred-wiring", "Deferred implementation comment leaves application wiring unfinished.")); + } + } + let mut collector = Collector { source, found, functions: std::collections::BTreeMap::new(), forms: Vec::new(), submit_depth: 0 }; + collector.visit_program(&parsed.program); + for (span, handler) in &collector.forms { + let body = collector.functions.get(handler).map(String::as_str).unwrap_or(handler); + if fake_success(body) { + collector.found.push(error(*span, "fake-submit", "Form submission only changes local success state without a request.")); + } + } + collector.found +} + +fn fake_success(body: &str) -> bool { + let lower = body.to_lowercase(); + let success = ["setsuccess(", "setsubmitted(", "setsent(", "setissuccess(", "setissubmitted(", "setissent("].iter().any(|name| lower.contains(name)); + success && !["fetch(", ".post(", ".mutate(", ".mutateasync(", "axios(", ".request("].iter().any(|call| lower.contains(call)) +} + +pub fn fixture_findings(files: &[crate::ui_validate::SourceFile], path: &str, source: &str) -> Vec { + struct Imports(Vec<(String, Span)>); + impl<'a> Visit<'a> for Imports { + fn visit_import_declaration(&mut self, node: &ImportDeclaration<'a>) { + self.0.push((node.source.value.to_string(), node.span)); + } + } + struct Records(bool); + impl<'a> Visit<'a> for Records { + fn visit_array_expression(&mut self, node: &ArrayExpression<'a>) { + if !node.elements.is_empty() && node.elements.iter().all(|item| matches!(item, ArrayExpressionElement::ObjectExpression(object) if object.properties.len() >= 2 && object.properties.iter().all(|property| matches!(property, ObjectPropertyKind::ObjectProperty(property) if matches!(property.value, Expression::StringLiteral(_) | Expression::NumericLiteral(_) | Expression::BooleanLiteral(_) | Expression::NullLiteral(_)))))) { + self.0 = true; + } + walk::walk_array_expression(self, node); + } + } + let allocator = Allocator::default(); + let parsed = Parser::new(&allocator, source, SourceType::tsx()).parse(); + let mut imports = Imports(Vec::new()); + imports.visit_program(&parsed.program); + imports.0.into_iter().filter_map(|(import, span)| { + let import = import.replace('\\', "/"); + let tail = import.split("utils/").nth(1)?; + let target = files.iter().find(|file| { + let normalized = file.path.replace('\\', "/"); + normalized.ends_with(&format!("utils/{tail}")) || normalized.ends_with(&format!("utils/{tail}.ts")) + })?; + if target.path == path { return None; } + let parsed = Parser::new(&allocator, &target.content, SourceType::ts()).parse(); + let mut records = Records(false); + records.visit_program(&parsed.program); + records.0.then(|| { + let mut finding = error(span, "fixture-instead-of-api", "UI imports a hardcoded record array from utils instead of an API data source."); + finding.context.insert("fixturePath".into(), serde_json::json!(target.path)); + finding.context.insert("confidence".into(), serde_json::json!("medium")); + finding + }) + }).collect() +} + +struct Collector<'s> { + source: &'s str, + found: Vec, + functions: std::collections::BTreeMap, + forms: Vec<(Span, String)>, + submit_depth: usize, +} + +fn attr_name<'a>(item: &'a JSXAttributeItem<'a>) -> Option<&'a str> { + match item { + JSXAttributeItem::Attribute(attribute) => attribute.name.as_identifier().map(|name| name.name.as_str()), + _ => None, + } +} + +fn value_text(value: &JSXAttributeValue<'_>) -> Option { + match value { + JSXAttributeValue::StringLiteral(value) => Some(value.value.to_string()), + JSXAttributeValue::ExpressionContainer(value) => match &value.expression { + JSXExpression::Identifier(value) => Some(value.name.to_string()), + JSXExpression::StringLiteral(value) => Some(value.value.to_string()), + _ => None, + }, + _ => None, + } +} + +impl<'a> Visit<'a> for Collector<'_> { + fn visit_variable_declarator(&mut self, node: &VariableDeclarator<'a>) { + if let BindingPattern::BindingIdentifier(id) = &node.id + && let Some(Expression::ArrowFunctionExpression(function)) = &node.init + { + self.functions.insert(id.name.to_string(), self.source[function.span.start as usize..function.span.end as usize].into()); + } + walk::walk_variable_declarator(self, node); + } + + fn visit_statement(&mut self, node: &Statement<'a>) { + if let Statement::FunctionDeclaration(function) = node + && let Some(id) = &function.id + { + self.functions.insert(id.name.to_string(), self.source[function.span.start as usize..function.span.end as usize].into()); + } + walk::walk_statement(self, node); + } + + fn visit_jsx_element(&mut self, node: &JSXElement<'a>) { + let opening = &node.opening_element; + let tag = opening.name.get_identifier_name().map(|name| name.as_str()).unwrap_or_default(); + let attribute = |name: &str| opening.attributes.iter().find_map(|item| match item { + JSXAttributeItem::Attribute(value) if attr_name(item) == Some(name) => value.value.as_ref(), + _ => None, + }); + let has = |name: &str| opening.attributes.iter().any(|item| attr_name(item) == Some(name)); + let effective = attribute("as").and_then(value_text).unwrap_or_else(|| tag.into()); + let spread = opening.attributes.iter().any(|item| matches!(item, JSXAttributeItem::SpreadAttribute(_))); + let form = effective == "form"; + let submits = form && (has("onSubmit") || has("action")); + if let Some(handler) = attribute("onSubmit") { + let text = value_text(handler).unwrap_or_else(|| { + use oxc_span::GetSpan; + let span = handler.span(); + self.source[span.start as usize..span.end as usize].into() + }); + self.forms.push((opening.span, text)); + } + if !spread && !has("disabled") && !has("readOnly") { + let input = matches!(effective.as_str(), "input" | "Input" | "select" | "Select" | "textarea" | "Textarea"); + let button = matches!(effective.as_str(), "button" | "Button"); + let submit = attribute("type").and_then(value_text).is_none_or(|kind| kind == "submit") && self.submit_depth > 0; + if (input && !["value", "onChange", "name", "defaultValue", "checked", "onInput", "props"].iter().any(|name| has(name))) + || (button && !submit && !["onClick", "href", "formAction", "form", "props"].iter().any(|name| has(name))) + { + self.found.push(error(opening.span, "dead-control", "Visible control has no value, form binding, handler or destination.")); + } + } + self.submit_depth += usize::from(submits); + walk::walk_jsx_element(self, node); + self.submit_depth -= usize::from(submits); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn dead_controls_and_deferred_comments_are_errors_not_string_matches() { + let source = r#"// TODO wire API +const label = 'TODO'; +const view = <>;"#; + let rules = findings(source).into_iter().map(|item| item.rule).collect::>(); + assert_eq!(rules, ["deferred-wiring", "dead-control", "dead-control"]); + } + + #[test] + fn imported_utils_records_fail_but_navigation_strings_do_not() { + let mut files = vec![crate::ui_validate::SourceFile { path: "src/utils/news.ts".into(), content: "export const news = [{ id: 1, title: 'News' }];".into() }]; + let source = "import { news } from '@/utils/news'; export default function NewsPage(){return
{news.length}
}"; + assert_eq!(fixture_findings(&files, "src/app/news/page.tsx", source)[0].rule, "fixture-instead-of-api"); + files[0].content = "export const news = ['Home', 'About'];".into(); + assert!(fixture_findings(&files, "src/app/news/page.tsx", source).is_empty()); + } + + #[test] + fn submit_success_requires_a_request_in_its_handler_not_elsewhere() { + let source = r#"const load = () => fetch('/news'); +const submit = (event) => { event.preventDefault(); setSuccess(true); }; +const view =
;"#; + assert!(findings(source).iter().any(|item| item.rule == "fake-submit")); + assert!(!findings(&source.replace("setSuccess(true)", "await fetch('/contact', { method: 'POST' }); setSuccess(true)").replace("(event) =>", "async (event) =>")).iter().any(|item| item.rule == "fake-submit")); + } +} diff --git a/crates/devup-mcp/src/server/asset_guard.rs b/crates/devup-mcp/src/server/asset_guard.rs index 5f0d9394..f7577ba7 100644 --- a/crates/devup-mcp/src/server/asset_guard.rs +++ b/crates/devup-mcp/src/server/asset_guard.rs @@ -290,6 +290,12 @@ pub(super) fn violations(sources: &[(&str, &str)], project_root: Option<&str>) - let mut context = Map::new(); context.insert("assetUrl".into(), json!(url)); context.insert("publicDirs".into(), json!(looked_in)); + context.insert("nextAction".into(), json!({ + "tool": "devup_figma_export", + "how": "Use the original Figma URL and the discovered icon nodeId in assetRequests; set outputPath under public/ to serve this assetUrl. Do not invent node IDs or replace the missing icon with a placeholder.", + "assetUrl": url, + "requiredInputs": ["url", "assetRequests[].nodeId", "assetRequests[].outputPath"] + })); if !name.is_empty() { context.insert("sourceName".into(), json!(name)); } diff --git a/crates/devup-mcp/tests/ui_validate_response/section_coverage.rs b/crates/devup-mcp/tests/ui_validate_response/section_coverage.rs index a6d799d1..f2646b24 100644 --- a/crates/devup-mcp/tests/ui_validate_response/section_coverage.rs +++ b/crates/devup-mcp/tests/ui_validate_response/section_coverage.rs @@ -9,6 +9,9 @@ async fn missing_sections_and_asset_files_fail_even_without_strict() -> anyhow:: assert_eq!(result["ok"], false); assert_eq!(result["okReason"], "error-violations"); let violations = result["violations"].as_array().unwrap(); + let missing_asset = violations.iter().find(|finding| finding["rule"] == "asset-missing").unwrap(); + assert_eq!(missing_asset["nextAction"]["tool"], "devup_figma_export"); + assert_eq!(missing_asset["nextAction"]["assetUrl"], "/missing-photo.png"); for rule in ["asset-missing", "section-coverage"] { assert!( violations From 8857d01722f65f37d9988f0dd61ab7b9962d9a11 Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 17:14:24 +0900 Subject: [PATCH 036/149] fix(screen-plan): prefer final pages and pin linked owning pages (43) Assume nonexcluded pages compete as source pages; final markers and numeric dates rank recommendations. Carry acquisition ownership into draft, conditional questions and confirmation together. --- .../src/server/mod_parts/acquisition.rs | 3 +- .../src/server/screen_plan/actions.rs | 4 ++ .../src/server/screen_plan/draft_plan_with.rs | 38 ++++++++++++++++++- .../server/screen_plan_tests/prevention.rs | 16 ++++++++ 4 files changed, 59 insertions(+), 2 deletions(-) diff --git a/crates/devup-mcp/src/server/mod_parts/acquisition.rs b/crates/devup-mcp/src/server/mod_parts/acquisition.rs index 5601c2c8..ab5ed774 100644 --- a/crates/devup-mcp/src/server/mod_parts/acquisition.rs +++ b/crates/devup-mcp/src/server/mod_parts/acquisition.rs @@ -517,6 +517,7 @@ impl DevupServer { "nodeId": explored["anchor"]["nodeId"], "anchorName": explored["anchor"]["name"], "anchorBreadcrumb": explored["anchor"]["breadcrumb"], + "nodePage": explored["nodePage"], "targetKind": explored["targetKind"], "fileKey": explored["source"]["fileKey"], "truncation": explored["truncation"], @@ -954,4 +955,4 @@ fn apply_search_scope( response["matches"] = Value::Array(inside); response["scope"] = scope_report; response -} \ No newline at end of file +} diff --git a/crates/devup-mcp/src/server/screen_plan/actions.rs b/crates/devup-mcp/src/server/screen_plan/actions.rs index c325b213..c0c4f600 100644 --- a/crates/devup-mcp/src/server/screen_plan/actions.rs +++ b/crates/devup-mcp/src/server/screen_plan/actions.rs @@ -116,6 +116,10 @@ pub(super) fn confirm(input: &ScreenPlanInput, policy: &OutputPolicy) -> Result< for screen in &screens { let id = text(&screen["id"]).to_owned(); let frames = array(&screen["frames"]).to_vec(); + if let Some(page) = actions.get("base-page").and_then(|action| action["pageId"].as_str()) + && !frames.iter().any(|frame| frame["pageId"] == page) { + continue; + } let mut deciding = array(&screen["structureQuestions"]) .iter() .map(text) diff --git a/crates/devup-mcp/src/server/screen_plan/draft_plan_with.rs b/crates/devup-mcp/src/server/screen_plan/draft_plan_with.rs index 70b9930d..ab087440 100644 --- a/crates/devup-mcp/src/server/screen_plan/draft_plan_with.rs +++ b/crates/devup-mcp/src/server/screen_plan/draft_plan_with.rs @@ -336,7 +336,40 @@ pub(super) fn draft_plan_with( for asked in site_asked { questions.push(with_when(asked, &base_when)); } - let mut drafts = group_screens(&kept, &evidence.config); + let linked_page = source["nodePage"]["id"].as_str().or_else(|| { + screens.iter().find(|screen| Some(screen.node_id.as_str()) == anchor_id) + .and_then(|screen| screen.page_id.as_deref()) + }); + let mut pages = kept.iter().filter_map(|screen| { + Some((screen.page_id.clone()?, screen.page_name.clone().or_else(|| screen.breadcrumb.first().cloned())?)) + }).collect::>().into_iter().collect::>(); + pages.sort_by(|a, b| { + let rank = |name: &str| { + let lower = name.to_lowercase(); + let preferred = ["시안", "확정", "final", "최종"].iter().any(|word| lower.contains(word)); + let date = name.chars().filter(char::is_ascii_digit).collect::(); + (preferred, date) + }; + rank(&b.1).cmp(&rank(&a.1)).then_with(|| a.0.cmp(&b.0)) + }); + if linked_page.is_none() && pages.len() > 1 { + questions.insert(0, question("base-page".into(), "Base Figma page".into(), + "Screens span several Figma pages. Which page is the implementation source?".into(), + pages.iter().map(|(id, name)| choice(name, "Use frames from this owning page only.", json!({"pageId": id}))).collect(), "base-page")); + } + let page_sets = if pages.len() > 1 { + pages.iter().filter(|(id, _)| linked_page.is_none_or(|linked| linked == id)) + .map(|(id, _)| kept.iter().filter(|screen| screen.page_id.as_deref() == Some(id)).cloned().collect::>()) + .collect::>() + } else { vec![kept.clone()] }; + let mut drafts = page_sets.iter().flat_map(|screens| group_screens(screens, &evidence.config)).collect::>(); + if linked_page.is_none() && pages.len() > 1 { + for draft in &mut drafts { + if let Some(page) = draft.frames.first().and_then(|frame| frame.page_id.as_ref()) { + draft.extra_when.push(when("base-page", "", json!({"pageId": page}))); + } + } + } for draft in &mut drafts { draft.extra_when.extend(base_when.iter().cloned()); } @@ -395,6 +428,9 @@ pub(super) fn draft_plan_with( } let mut route_conflicts = Vec::new(); for (route, indices) in owners.iter().filter(|(_, indices)| indices.len() > 1) { + if pages.len() > 1 && indices.iter().filter_map(|index| drafts[*index].frames.first()?.page_id.as_ref()).collect::>().len() == indices.len() { + continue; + } route_conflicts.push(json!({"url": route, "screens": indices.iter().map(|index| drafts[*index].id().to_owned()).collect::>(), "names": indices.iter().map(|index| drafts[*index].name().to_owned()).collect::>()})); for index in indices { if let Some((route, _)) = decided[*index].1.take() { diff --git a/crates/devup-mcp/src/server/screen_plan_tests/prevention.rs b/crates/devup-mcp/src/server/screen_plan_tests/prevention.rs index 00b85e05..46e56486 100644 --- a/crates/devup-mcp/src/server/screen_plan_tests/prevention.rs +++ b/crates/devup-mcp/src/server/screen_plan_tests/prevention.rs @@ -129,3 +129,19 @@ fn r47_status_reports_the_owning_page_for_every_planned_frame() { assert_eq!(report["framePages"][2]["pageId"], "0:2"); assert_eq!(report["framePages"][3]["pageName"], Value::Null); } +#[test] +fn r43_final_page_is_recommended_and_linked_frame_pins_its_page() { + let mut old = sectioned("old:1", "Home desktop", 1440.0, "Home"); + old.page_id = Some("page:old".into()); + old.page_name = Some("Design system".into()); + let mut final_frame = sectioned("final:1", "Home desktop", 1440.0, "Home"); + final_frame.page_id = Some("page:final".into()); + final_frame.page_name = Some("최종 시안 2026-10-14".into()); + let screens = [old, final_frame]; + let plan = draft_plan(&screens, &BTreeMap::new(), &evidence(), json!({}), None).unwrap(); + assert_eq!(find(&plan, "base-page")["resolve"][0]["action"]["pageId"], "page:final"); + let pinned = draft_plan(&screens, &BTreeMap::new(), &evidence(), json!({"nodeId":"old:1"}), None).unwrap(); + assert!(!question_ids(&pinned).contains(&"base-page".to_owned())); + assert_eq!(array(&pinned["screens"]).len(), 1); + assert_eq!(pinned["screens"][0]["frames"][0]["pageId"], "page:old"); +} From c8116147f274c0c70229e4df628d9caa01edd957 Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 17:14:41 +0900 Subject: [PATCH 037/149] test(project-plan): retain decisions across answers-only redrafts (10) --- .../src/server/project_plan/tests/feedback.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/crates/devup-mcp/src/server/project_plan/tests/feedback.rs b/crates/devup-mcp/src/server/project_plan/tests/feedback.rs index 4afb8dab..837cfb4e 100644 --- a/crates/devup-mcp/src/server/project_plan/tests/feedback.rs +++ b/crates/devup-mcp/src/server/project_plan/tests/feedback.rs @@ -1,5 +1,20 @@ use super::*; +#[tokio::test] +async fn draft_decisions_survive_an_answers_only_redraft() { + let dir = TestDir::new(); + let (mut request, policy, _) = drafted(dir.path(), false).await; + request.action = "draft".into(); + request.decisions = vec![decision("session-expiry"), decision("upload-limit")]; + run(&request, &policy).await.unwrap(); + request.decisions.clear(); + request.answers.clear(); + request.answers.insert("posts.states".into(), json!("Show an empty list")); + let draft = run(&request, &policy).await.unwrap(); + assert!(kept(&draft["plan"]["decisions"], "session-expiry")); + assert!(kept(&draft["plan"]["decisions"], "upload-limit")); +} + #[tokio::test] async fn intake_discovers_korean_spec_documents_without_extracting_contents() { From fd073aa8da29bc8ad5df27ea76a9d1beff77b975 Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 17:15:22 +0900 Subject: [PATCH 038/149] test(project-plan): reject jsonb before transactional model writes (24) --- .../src/server/project_plan/tests/feedback.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/crates/devup-mcp/src/server/project_plan/tests/feedback.rs b/crates/devup-mcp/src/server/project_plan/tests/feedback.rs index 837cfb4e..511e3753 100644 --- a/crates/devup-mcp/src/server/project_plan/tests/feedback.rs +++ b/crates/devup-mcp/src/server/project_plan/tests/feedback.rs @@ -1,5 +1,18 @@ use super::*; +#[tokio::test] +async fn confirm_rejects_jsonb_before_writing_any_models() { + let dir = TestDir::new(); + let (mut request, policy, draft) = drafted(dir.path(), true).await; + request.entities = array(&draft["plan"]["entities"]).to_vec(); + request.entities[1]["columns"][1]["type"] = json!("jsonb"); + let error = run(&request, &policy).await.unwrap_err(); + assert!(error.message.contains("post.title")); + assert!(error.message.contains("jsonb")); + assert!(!dir.path().join("models").exists()); + assert_eq!(read(&request.project_root).unwrap()["state"], "draft"); +} + #[tokio::test] async fn draft_decisions_survive_an_answers_only_redraft() { let dir = TestDir::new(); From d6fafacdb2391a2b3d2cd3f9fd0a81c7e5472a9c Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 17:16:59 +0900 Subject: [PATCH 039/149] fix(screen-plan): expose actionable page inventory truncation details (12) Assume callers combine Section inventories before one draft; separate draft calls replace, never silently merge the plan. --- .../src/server/screen_plan/draft_plan_with.rs | 11 +++++++++-- .../src/server/screen_plan_tests/prevention.rs | 11 +++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/crates/devup-mcp/src/server/screen_plan/draft_plan_with.rs b/crates/devup-mcp/src/server/screen_plan/draft_plan_with.rs index ab087440..20e88edf 100644 --- a/crates/devup-mcp/src/server/screen_plan/draft_plan_with.rs +++ b/crates/devup-mcp/src/server/screen_plan/draft_plan_with.rs @@ -32,10 +32,17 @@ pub(super) fn draft_plan_with( } else { sections.join(", ") }; - return Err(invalid(format!( + let section_calls = screens.iter().filter_map(|screen| screen.section.as_ref().map(|id| (id.clone(), screen.section_name.clone()))) + .collect::>().into_iter().map(|(id, name)| { + let url = source["url"].as_str().and_then(|url| url.split('?').next()).map(|base| format!("{base}?node-id={}", id.replace(':', "-"))); + json!({"nodeId": id, "name": name, "draftCall": {"tool": "devup_screen_plan", "arguments": {"action": "draft", "url": url}}}) + }).collect::>(); + return Err(DevupError::with_details(ErrorCode::DevupInvalidInput, format!( "The frame inventory is incomplete: exploration was truncated or reached its ceiling ({}). Sections seen so far: {listed}. Call draft with one Section link at a time (devup_figma_search lists the page's top-level SECTION/FRAME nodes) or supply the complete frame list before drafting.", hit.join(", ") - ))); + ), false, json!({"frameCount": screens.len(), "limits": {"maxScreens": MAX_SCREENS, "triggers": hit}, + "sections": section_calls, "nextAction": {"tool":"devup_screen_plan", "arguments": {"action":"draft", "screens":"complete combined frame inventory from the listed Sections"}}, + "merge": "Collect all Section inventories, combine screens, and draft once; separate drafts replace the saved plan."}))); } let mut ids = BTreeSet::new(); if screens diff --git a/crates/devup-mcp/src/server/screen_plan_tests/prevention.rs b/crates/devup-mcp/src/server/screen_plan_tests/prevention.rs index 46e56486..127bd2cd 100644 --- a/crates/devup-mcp/src/server/screen_plan_tests/prevention.rs +++ b/crates/devup-mcp/src/server/screen_plan_tests/prevention.rs @@ -145,3 +145,14 @@ fn r43_final_page_is_recommended_and_linked_frame_pins_its_page() { assert_eq!(array(&pinned["screens"]).len(), 1); assert_eq!(pinned["screens"][0]["frames"][0]["pageId"], "page:old"); } +#[test] +fn r12_truncated_page_inventory_reports_counts_limits_and_section_calls() { + let screens = [sectioned("1:1", "Home", 1440.0, "Main"), sectioned("2:1", "Company", 1440.0, "Company")]; + let error = draft_plan(&screens, &BTreeMap::new(), &evidence(), + json!({"url":"https://www.figma.com/design/key/file?node-id=0-1", "truncation":{"projection":true}}), None).unwrap_err(); + assert_eq!(error.details["frameCount"], 2); + assert!(error.details["limits"]["maxScreens"].as_u64().unwrap() >= 2); + assert_eq!(array(&error.details["sections"]).len(), 2); + assert_eq!(error.details["sections"][0]["draftCall"]["arguments"]["action"], "draft"); + assert!(error.details["sections"][0]["draftCall"]["arguments"]["url"].is_string()); +} From 266326da81ced5bdd91ec8ab0f07e38aa14429e1 Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 17:19:09 +0900 Subject: [PATCH 040/149] fix(project-plan): report written models and unsplit state (21) --- crates/devup-mcp/src/server/project_plan/mod.rs | 7 +++++++ .../src/server/project_plan/tests/feedback.rs | 16 ++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/crates/devup-mcp/src/server/project_plan/mod.rs b/crates/devup-mcp/src/server/project_plan/mod.rs index 6a1ae438..290d94c1 100644 --- a/crates/devup-mcp/src/server/project_plan/mod.rs +++ b/crates/devup-mcp/src/server/project_plan/mod.rs @@ -350,6 +350,8 @@ fn blockers(plan: &Value, root: &str) -> Vec { fn extras(plan: &Value, root: &str) -> Value { let no_screens = without_screens(plan); let mut extra = json!({ + "writtenTo": plan["writtenTo"], + "unitState": if array(&plan["units"]).is_empty() {"not-split"} else {"split"}, "sharedChangeRequests": units::open_requests(plan), "apiWiring": wiring::report(plan, root), "backendChecks": wiring::backend_checks(plan, root), @@ -559,6 +561,11 @@ pub(super) async fn run( ))); } erd::write_models(&mut tx, policy, &input.project_root, &plan)?; + plan["writtenTo"] = json!(if erd::api(&plan) { + array(&plan["entities"]).iter() + .map(|entity| format!("{}/{}.json", text(&plan["modelsDir"]), text(&entity["name"]))) + .collect::>() + } else { vec![] }); plan["state"] = json!("confirmed"); plan["confirmedAt"] = json!(now()); } diff --git a/crates/devup-mcp/src/server/project_plan/tests/feedback.rs b/crates/devup-mcp/src/server/project_plan/tests/feedback.rs index 511e3753..e8d54eab 100644 --- a/crates/devup-mcp/src/server/project_plan/tests/feedback.rs +++ b/crates/devup-mcp/src/server/project_plan/tests/feedback.rs @@ -1,5 +1,21 @@ use super::*; +#[tokio::test] +async fn confirm_reports_written_models_and_backend_commands_in_summary() { + let dir = TestDir::new(); + let (mut request, policy, _) = drafted(dir.path(), true).await; + request.detail = None; + let confirmed = run(&request, &policy).await.unwrap(); + assert_eq!(confirmed["nextAction"]["arguments"]["action"], "split"); + assert_eq!(confirmed["writtenTo"], json!(["models/user.json", "models/post.json"])); + assert_eq!(confirmed["backendCommands"][0], "vespertide diff"); + assert_eq!(confirmed["modelsWorkingDirectory"], "."); + assert_eq!(confirmed["unitState"], "not-split"); + for path in array(&confirmed["writtenTo"]) { + assert!(dir.path().join(text(path)).is_file()); + } +} + #[tokio::test] async fn confirm_rejects_jsonb_before_writing_any_models() { let dir = TestDir::new(); From f0f569ca7f8f77b6a7def0e9d27374133a7fa7ee Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 17:19:58 +0900 Subject: [PATCH 041/149] test(project-plan): expose Figma prerequisite before confirm (11) --- .../src/server/project_plan/tests/feedback.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/crates/devup-mcp/src/server/project_plan/tests/feedback.rs b/crates/devup-mcp/src/server/project_plan/tests/feedback.rs index e8d54eab..4068b02f 100644 --- a/crates/devup-mcp/src/server/project_plan/tests/feedback.rs +++ b/crates/devup-mcp/src/server/project_plan/tests/feedback.rs @@ -1,5 +1,23 @@ use super::*; +#[tokio::test] +async fn figma_prerequisite_is_reported_at_intake_and_draft() { + let dir = TestDir::new(); + let policy = OutputPolicy::from_roots(vec![dir.path().to_path_buf()]).unwrap(); + let mut request = input(dir.path(), "intake"); + let url = "https://www.figma.com/design/fixture/site?node-id=1-2"; + request.evidence = vec![serde_json::from_value(json!({"source":"Figma", "facts":{"figma":[url],"goal":"Company site"}})).unwrap()]; + let intake = run(&request, &policy).await.unwrap(); + assert_eq!(intake["nextAction"]["tool"], "devup_screen_plan"); + assert_eq!(intake["nextAction"]["arguments"]["url"], url); + request.plan_id = intake["planId"].as_str().map(str::to_owned); + request.action = "draft".into(); + request.features = vec![Feature {id:"home".into(),scope:"Company".into(),screens:vec!["Home".into()],acceptance:vec![],entities:vec![]}]; + let draft = run(&request, &policy).await.unwrap(); + assert!(array(&draft["blockers"]).contains(&json!("screenPlan: not confirmed"))); + assert_eq!(draft["nextAction"]["arguments"]["projectRoot"], request.project_root); +} + #[tokio::test] async fn confirm_reports_written_models_and_backend_commands_in_summary() { let dir = TestDir::new(); From dea9c60a42f43d4ccde4615c9997b00fb369dab7 Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 17:20:57 +0900 Subject: [PATCH 042/149] test(project-plan): verify merged screen answer forwarding (17) --- .../src/server/project_plan/tests/feedback.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/crates/devup-mcp/src/server/project_plan/tests/feedback.rs b/crates/devup-mcp/src/server/project_plan/tests/feedback.rs index 4068b02f..4adcdddf 100644 --- a/crates/devup-mcp/src/server/project_plan/tests/feedback.rs +++ b/crates/devup-mcp/src/server/project_plan/tests/feedback.rs @@ -1,5 +1,24 @@ use super::*; +#[test] +fn merged_screen_answers_are_forwarded_without_becoming_project_answers() { + let dir = TestDir::new(); + std::fs::create_dir_all(dir.path().join(".devup")).unwrap(); + std::fs::write(dir.path().join(".devup/screen-plan.json"), json!({ + "kind":"devup-screen-plan", "schemaVersion":1,"planId":"screen-id","state":"draft", + "questions":[{"id":"route:1:2"}] + }).to_string()).unwrap(); + let answers = BTreeMap::from([ + ("route:1:2".into(), json!("/contact")), + ("parts".into(), json!("front only")) + ]); + let (own, forwarded) = forward_screen_answers(&dir.path().to_string_lossy(), &answers, false); + assert_eq!(own, BTreeMap::from([("parts".into(), json!("front only"))])); + assert_eq!(forwarded["arguments"]["answers"], json!({"route:1:2":"/contact"})); + assert_eq!(forwarded["arguments"]["planId"], "screen-id"); + assert_eq!(forwarded["arguments"]["action"], "confirm"); +} + #[tokio::test] async fn figma_prerequisite_is_reported_at_intake_and_draft() { let dir = TestDir::new(); From dd01d8f8eb962e6d492fc0faaf8f9740cc61e897 Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 17:22:02 +0900 Subject: [PATCH 043/149] test(project-plan): report exported entity prerequisites (25) --- .../src/server/project_plan/tests/feedback.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/crates/devup-mcp/src/server/project_plan/tests/feedback.rs b/crates/devup-mcp/src/server/project_plan/tests/feedback.rs index 4adcdddf..744d4b18 100644 --- a/crates/devup-mcp/src/server/project_plan/tests/feedback.rs +++ b/crates/devup-mcp/src/server/project_plan/tests/feedback.rs @@ -1,5 +1,19 @@ use super::*; +#[tokio::test] +async fn backend_checks_report_missing_serde_and_model_module() { + let dir = TestDir::new(); + let (request, policy, _) = drafted(dir.path(), true).await; + std::fs::create_dir_all(dir.path().join("src")).unwrap(); + std::fs::write(dir.path().join("Cargo.toml"), "[dependencies]\nsea-orm = \"2\"\n").unwrap(); + std::fs::write(dir.path().join("src/main.rs"), "fn main() {}\n").unwrap(); + let result = run(&request, &policy).await.unwrap(); + assert_eq!(array(&result["backendChecks"]).len(), 3); + std::fs::write(dir.path().join("Cargo.toml"), "[dependencies]\nserde = { version = \"1\", features = [\"derive\"] }\nserde_json = \"1\"\n").unwrap(); + std::fs::write(dir.path().join("src/main.rs"), "mod models;\nfn main() {}\n").unwrap(); + assert!(wiring::backend_checks(&read(&request.project_root).unwrap(), &request.project_root).is_empty()); +} + #[test] fn merged_screen_answers_are_forwarded_without_becoming_project_answers() { let dir = TestDir::new(); From 4d08a584c3e440b0790a5e66de912d8ecd0e1f85 Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 17:22:46 +0900 Subject: [PATCH 044/149] test(project-plan): distinguish static pages from form integrations (04) --- .../src/server/project_plan/tests/feedback.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/crates/devup-mcp/src/server/project_plan/tests/feedback.rs b/crates/devup-mcp/src/server/project_plan/tests/feedback.rs index 744d4b18..b7608d1b 100644 --- a/crates/devup-mcp/src/server/project_plan/tests/feedback.rs +++ b/crates/devup-mcp/src/server/project_plan/tests/feedback.rs @@ -1,5 +1,22 @@ use super::*; +#[test] +fn static_features_only_ask_relevant_topics_and_sourced_answers_suppress_them() { + let mut plan = json!({"facts":{},"answers":{},"features":[ + {"id":"products","scope":"Product introduction","screens":["Products"],"acceptance":["Display descriptions"]} + ],"issues":[],"entities":[]}); + let questions = rounds::detail(&plan, &[]).unwrap(); + assert!(!questions.iter().any(|question| ["products.forms","products.permissions","products.notifications"].contains(&text(&question["id"])))); + plan["features"][0] = json!({"id":"contact","scope":"Contact form with SMTP staff email","screens":["Contact"],"acceptance":["Validate email"]}); + let questions = rounds::detail(&plan, &[]).unwrap(); + assert!(questions.iter().any(|question| question["id"] == "contact.forms")); + assert!(questions.iter().any(|question| question["id"] == "contact.notifications")); + plan["facts"]["contact.forms"] = json!("Required email"); + plan["facts"]["contact.notifications"] = json!("SMTP staff email"); + let questions = rounds::detail(&plan, &[]).unwrap(); + assert!(!questions.iter().any(|question| ["contact.forms","contact.notifications"].contains(&text(&question["id"])))); +} + #[tokio::test] async fn backend_checks_report_missing_serde_and_model_module() { let dir = TestDir::new(); From 7435855e6574ea65f9de13802faa9d879164a0bb Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 17:23:42 +0900 Subject: [PATCH 045/149] fix(screen-plan): ask base page only for competing screens (43) Regression scope preserves distinct screens intentionally supplied from different pages; all 90 screen-plan tests pass. --- crates/devup-mcp/src/server/screen_plan/draft_plan_with.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/devup-mcp/src/server/screen_plan/draft_plan_with.rs b/crates/devup-mcp/src/server/screen_plan/draft_plan_with.rs index 20e88edf..87487b0a 100644 --- a/crates/devup-mcp/src/server/screen_plan/draft_plan_with.rs +++ b/crates/devup-mcp/src/server/screen_plan/draft_plan_with.rs @@ -359,7 +359,10 @@ pub(super) fn draft_plan_with( }; rank(&b.1).cmp(&rank(&a.1)).then_with(|| a.0.cmp(&b.0)) }); - if linked_page.is_none() && pages.len() > 1 { + let competing_pages = kept.iter().any(|screen| kept.iter().any(|other| { + screen.page_id != other.page_id && base_words(&screen.name) == base_words(&other.name) + })); + if linked_page.is_none() && competing_pages { questions.insert(0, question("base-page".into(), "Base Figma page".into(), "Screens span several Figma pages. Which page is the implementation source?".into(), pages.iter().map(|(id, name)| choice(name, "Use frames from this owning page only.", json!({"pageId": id}))).collect(), "base-page")); @@ -370,7 +373,7 @@ pub(super) fn draft_plan_with( .collect::>() } else { vec![kept.clone()] }; let mut drafts = page_sets.iter().flat_map(|screens| group_screens(screens, &evidence.config)).collect::>(); - if linked_page.is_none() && pages.len() > 1 { + if linked_page.is_none() && competing_pages { for draft in &mut drafts { if let Some(page) = draft.frames.first().and_then(|frame| frame.page_id.as_ref()) { draft.extra_when.push(when("base-page", "", json!({"pageId": page}))); From a0caba6390b3d9377fcaf44cd3a762d5d3563aaa Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 17:24:02 +0900 Subject: [PATCH 046/149] fix(screen-plan): queue missing Figma theme tokens before pages (38) Assume the generated file theme is the authoritative comparison; preserve existing values and queue additions only. Acquisition, comparison, confirm/status queue wiring and regression must ship together. --- .../src/server/mod_parts/acquisition.rs | 15 +++++++++++++++ .../src/server/screen_plan/actions.rs | 7 ++++--- .../devup-mcp/src/server/screen_plan/remap.rs | 19 +++++++++++++++++++ .../src/server/screen_plan/status.rs | 5 ++++- .../server/screen_plan_tests/prevention.rs | 13 +++++++++++++ 5 files changed, 55 insertions(+), 4 deletions(-) diff --git a/crates/devup-mcp/src/server/mod_parts/acquisition.rs b/crates/devup-mcp/src/server/mod_parts/acquisition.rs index ab5ed774..bc3ad6be 100644 --- a/crates/devup-mcp/src/server/mod_parts/acquisition.rs +++ b/crates/devup-mcp/src/server/mod_parts/acquisition.rs @@ -485,6 +485,20 @@ impl DevupServer { refresh: input.refresh, }) .await?; + let mut explored = explored; + if input.project_root.is_some() { + let target = self.bind_to_bridge(parse_link(input.url.as_deref())?, Unnamed::Selection, + &json!({"tool":"devup_screen_plan","arguments":{"action":"draft"}})).await?; + let mut request = CollectionRequest::new(target, CollectionScope::File); + request.variables_only = true; + request.resource_scope = ResourceScope::File; + let parts = self.run_direct(request, &PendingOperation::Collect, None).await?; + if let Some(variables) = parts.variables { + let snapshot = devup_mcp_devup_ui::theme::variable_snapshot_from_result(&variables)?; + let theme = devup_mcp_devup_ui::theme::generate_devup_json(&snapshot, devup_mcp_devup_ui::theme::ThemeScope::File)?; + explored["figmaTheme"] = serde_json::from_str(&theme.json).map_err(|error| DevupError::new(ErrorCode::DevupInvalidInput, error.to_string(), false))?; + } + } let candidates = explored["candidates"] .as_array() .cloned() @@ -518,6 +532,7 @@ impl DevupServer { "anchorName": explored["anchor"]["name"], "anchorBreadcrumb": explored["anchor"]["breadcrumb"], "nodePage": explored["nodePage"], + "figmaTheme": explored["figmaTheme"], "targetKind": explored["targetKind"], "fileKey": explored["source"]["fileKey"], "truncation": explored["truncation"], diff --git a/crates/devup-mcp/src/server/screen_plan/actions.rs b/crates/devup-mcp/src/server/screen_plan/actions.rs index c0c4f600..e1a89c4c 100644 --- a/crates/devup-mcp/src/server/screen_plan/actions.rs +++ b/crates/devup-mcp/src/server/screen_plan/actions.rs @@ -377,7 +377,8 @@ pub(super) fn confirm(input: &ScreenPlanInput, policy: &OutputPolicy) -> Result< plan["assumptions"] = json!([]); } let written = write_plan(policy, root, &plan)?; - let work_queue = routes + let mut work_queue = theme_sync_queue(&plan, root); + work_queue.extend(routes .iter() .filter(|route| route["url"].is_string()) .map(|route| { @@ -390,10 +391,10 @@ pub(super) fn confirm(input: &ScreenPlanInput, policy: &OutputPolicy) -> Result< "drawnWidths": route["drawnWidths"], }) }) - .collect::>(); + .collect::>()); let urls = work_queue .iter() - .map(|item| text(&item["url"]).to_owned()) + .filter_map(|item| item["url"].as_str().map(str::to_owned)) .collect::>(); Ok(json!({ "status": "confirmed", diff --git a/crates/devup-mcp/src/server/screen_plan/remap.rs b/crates/devup-mcp/src/server/screen_plan/remap.rs index 9c70640f..bd03697b 100644 --- a/crates/devup-mcp/src/server/screen_plan/remap.rs +++ b/crates/devup-mcp/src/server/screen_plan/remap.rs @@ -119,3 +119,22 @@ fn frame_page_table(plan: &Value) -> Vec { }) }).collect() } + +fn theme_sync_queue(plan: &Value, root: &str) -> Vec { + let theme_root = plan["targetApp"]["dir"].as_str().map_or_else(|| PathBuf::from(root), |app| Path::new(root).join(app)); + let current = std::fs::read_to_string(theme_root.join("devup.json")).ok() + .and_then(|text| serde_json::from_str::(&text).ok()).unwrap_or(Value::Null); + fn missing(figma: &Value, current: &Value) -> usize { + match figma.as_object() { + Some(entries) => entries.iter().map(|(key, value)| { + if current.get(key).is_none() { 1 } else { missing(value, ¤t[key]) } + }).sum(), + None => 0, + } + } + let count = missing(&plan["source"]["figmaTheme"], ¤t); + if count == 0 { return Vec::new(); } + vec![json!({"do":"theme-sync", "state":"todo", "missingTokenCount":count, + "exportCall":{"tool":"devup_figma_export", "arguments":{"url":plan["source"]["url"], + "scope":"file", "outputs":["devupJson"], "devupJsonMode":"missing-only", "projectRoot":theme_root}}})] +} diff --git a/crates/devup-mcp/src/server/screen_plan/status.rs b/crates/devup-mcp/src/server/screen_plan/status.rs index 06e08b87..1e6516df 100644 --- a/crates/devup-mcp/src/server/screen_plan/status.rs +++ b/crates/devup-mcp/src/server/screen_plan/status.rs @@ -312,6 +312,9 @@ pub(super) fn status_report( link_markdown.push_str(&format!("| {} | {} | {} | {} | {} |\n", cell(&row["from"]), cell(&row["label"]), cell(&row["href"]), cell(&row["status"]), cell(&row["planned"]))); } let mut queue = super::audit_plan::work_queue(plan, &rows, &[]); + let theme_queue = theme_sync_queue(plan, &evidence.root.to_string_lossy()); + let theme_pending = !theme_queue.is_empty(); + queue.splice(0..0, theme_queue); if links_fail { for item in &mut queue { item["done"] = json!(false); } } @@ -319,7 +322,7 @@ pub(super) fn status_report( let blocking_urls = rows.iter().filter(|row| { row["implemented"] != "yes" || array(&row["widths"]).iter().any(|width| width["result"] != "pass") }).map(|row| row["url"].clone()).collect::>(); - let done = blocking_urls.is_empty() && !links_fail && incomplete == 0 && open.is_empty(); + let done = blocking_urls.is_empty() && !theme_pending && !links_fail && incomplete == 0 && open.is_empty(); let converter_bugs = plan["audit"]["converterBugs"] .as_object() .map(|bugs| bugs.values().cloned().collect::>()) diff --git a/crates/devup-mcp/src/server/screen_plan_tests/prevention.rs b/crates/devup-mcp/src/server/screen_plan_tests/prevention.rs index 127bd2cd..39cc9198 100644 --- a/crates/devup-mcp/src/server/screen_plan_tests/prevention.rs +++ b/crates/devup-mcp/src/server/screen_plan_tests/prevention.rs @@ -156,3 +156,16 @@ fn r12_truncated_page_inventory_reports_counts_limits_and_section_calls() { assert_eq!(error.details["sections"][0]["draftCall"]["arguments"]["action"], "draft"); assert!(error.details["sections"][0]["draftCall"]["arguments"]["url"].is_string()); } +#[test] +fn r38_missing_figma_tokens_put_theme_sync_before_page_work() { + let fixture = Fixture::new(); + fixture.write("devup.json", r#"{"theme":{"colors":{"light":{"primary":"red"}}}}"#); + let mut plan = confirmed_plan(); + plan["source"]["figmaTheme"] = json!({"theme":{"colors":{"light":{"primary":"red","borderLight":"gray"}}}}); + let mut project = evidence(); + project.root = fixture.0.clone(); + let report = status_report(&plan, &project, None); + assert_eq!(report["workQueue"][0]["do"], "theme-sync"); + assert_eq!(report["workQueue"][0]["missingTokenCount"], 1); + assert_eq!(report["workQueue"][0]["exportCall"]["arguments"]["devupJsonMode"], "missing-only"); +} From 7753b9043fbe467574fa5c945da784dea3ac8972 Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 17:24:27 +0900 Subject: [PATCH 047/149] test(project-plan): verify shared table ownership and static layer pruning (23) --- .../src/server/project_plan/tests/feedback.rs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/crates/devup-mcp/src/server/project_plan/tests/feedback.rs b/crates/devup-mcp/src/server/project_plan/tests/feedback.rs index b7608d1b..7b1b8790 100644 --- a/crates/devup-mcp/src/server/project_plan/tests/feedback.rs +++ b/crates/devup-mcp/src/server/project_plan/tests/feedback.rs @@ -1,5 +1,30 @@ use super::*; +#[tokio::test] +async fn split_uses_one_shared_table_owner_and_skips_static_backend_layers() { + let dir = TestDir::new(); + let (mut request, policy, _) = drafted(dir.path(), true).await; + request.action = "draft".into(); + request.features = vec![ + Feature {id:"layout".into(),scope:"Header and footer".into(),screens:vec!["Layout".into()],acceptance:vec![],entities:vec![]}, + Feature {id:"news".into(),scope:"Public news".into(),screens:vec!["News".into()],acceptance:vec![],entities:vec!["post".into()]}, + Feature {id:"admin-news".into(),scope:"Manage news".into(),screens:vec!["Editor".into()],acceptance:vec![],entities:vec!["post".into()]}, + ]; + run(&request, &policy).await.unwrap(); + request.action = "confirm".into(); + request.answers.clear(); + request.accept_recommended = true; + run(&request, &policy).await.unwrap(); + request.action = "split".into(); + let result = run(&request, &policy).await.unwrap(); + let units = array(&result["plan"]["units"]); + assert_eq!(units.iter().filter(|unit| unit["id"] == "model-post").count(), 1); + assert!(!units.iter().any(|unit| ["layout-api","layout-client","layout-models"].contains(&text(&unit["id"])))); + for id in ["news-api", "admin-news-api"] { + assert_eq!(units.iter().find(|unit| unit["id"] == id).unwrap()["dependencies"], json!(["model-post"])); + } +} + #[test] fn static_features_only_ask_relevant_topics_and_sourced_answers_suppress_them() { let mut plan = json!({"facts":{},"answers":{},"features":[ From eb6afcc8f7c1ba8be47b05b94b40ac8772fe575e Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 17:24:28 +0900 Subject: [PATCH 048/149] fix(project-plan): satisfy compact status lint gate (03) --- crates/devup-mcp/src/server/project_plan/mod.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/devup-mcp/src/server/project_plan/mod.rs b/crates/devup-mcp/src/server/project_plan/mod.rs index 290d94c1..6fe7730c 100644 --- a/crates/devup-mcp/src/server/project_plan/mod.rs +++ b/crates/devup-mcp/src/server/project_plan/mod.rs @@ -619,11 +619,11 @@ pub(super) async fn run( write(&mut tx, policy, &input.project_root, &plan)?; tx.commit()?; let mut out = response(plan, &input.project_root, full); - if input.action == "status" && !full { - if let Some(summary) = out.as_object_mut() { - summary.remove("questionRounds"); - summary.remove("proposalMarkdown"); - } + if input.action == "status" && !full + && let Some(summary) = out.as_object_mut() + { + summary.remove("questionRounds"); + summary.remove("proposalMarkdown"); } if !forward.is_null() { out["screenPlanForward"] = forward; From 4312afc29bdc8a5bc29be889dfca74aa676ec27f Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 17:32:58 +0900 Subject: [PATCH 049/149] fix(screen-plan): keep page inventory shallow at section screens (12) Assume PAGE planning needs SECTION and screen geometry only; do not traverse decorative internals or measure navigation. Include the executed-script regression and rebuilt plugin delivery together. --- crates/devup-mcp-figma/src/collector/w5_tests.rs | 11 +++++++++++ crates/devup-mcp-figma/src/scripts/explore.js | 4 +++- plugin/dist/code.js | 2 +- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/crates/devup-mcp-figma/src/collector/w5_tests.rs b/crates/devup-mcp-figma/src/collector/w5_tests.rs index 721d7e50..3004e2ee 100644 --- a/crates/devup-mcp-figma/src/collector/w5_tests.rs +++ b/crates/devup-mcp-figma/src/collector/w5_tests.rs @@ -207,6 +207,17 @@ mod w5_tests { } } + #[test] + fn r12_page_inventory_stops_at_screen_frames_instead_of_walking_their_contents() { + let setup = format!("{TREE}\nfigma.getNodeByIdAsync = async () => page;\nfor(let index=0;index<2000;index++) node('deep:'+index, 'GROUP', 'Decorative', frame);\n"); + let output = run_script(include_str!("../scripts/explore.js"), &setup, + "__DEVUP_EXPLORE__", json!({"projectionLimit":200,"textPreviewLimit":0})); + let nodes = output["nodes"].as_array().unwrap(); + assert!(nodes.iter().any(|node| node["id"] == "3:1")); + assert!(!nodes.iter().any(|node| node["id"].as_str().unwrap().starts_with("deep:"))); + assert_eq!(nodes[0]["fields"]["projectionTruncated"], false); + } + #[test] fn w5_previews_are_retained_when_the_envelope_has_room() { let setup = diff --git a/crates/devup-mcp-figma/src/scripts/explore.js b/crates/devup-mcp-figma/src/scripts/explore.js index 0fc94bcc..0e0720ed 100644 --- a/crates/devup-mcp-figma/src/scripts/explore.js +++ b/crates/devup-mcp-figma/src/scripts/explore.js @@ -206,6 +206,8 @@ if (descendantScope && "children" in scopeAnchor) { } if ("children" in node) { for (const child of node.children) { + if (requestedPage && node.type !== "SECTION") continue; + if (requestedPage && !["SECTION", "FRAME", "COMPONENT", "INSTANCE", "COMPONENT_SET"].includes(child.type)) continue; nestedQueue.push({ node: child, ancestors: [...ancestors, node] }); } } @@ -230,7 +232,7 @@ const compact = [...included.values()] childCount: "children" in node ? node.children.length : 0, // Select the structural projection before spending space on previews. textPreview: "", - ...navigationSummary(node), + ...(requestedPage ? {} : navigationSummary(node)), pageChildIndex: pageChildIndex >= 0 ? pageChildIndex : null, // A page or the document itself has no `visible`, and Figma throws on // reading a property a node does not have rather than returning diff --git a/plugin/dist/code.js b/plugin/dist/code.js index 79b3d07d..86376ae2 100644 --- a/plugin/dist/code.js +++ b/plugin/dist/code.js @@ -1 +1 @@ -(()=>{"use strict";let e={assets:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=e.asset;function r(e){let t=[0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,0xe49b69c1,0xefbe4786,0xfc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x6ca6351,0x14292967,0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2],r=64*Math.ceil((e.length+9)/64),n=new Uint8Array(r);n.set(e),n[e.length]=128;let i=8*e.length;for(let e=0;e<8;e+=1)n[r-1-e]=255&Math.floor(i/2**(8*e));let a=[0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19],o=(e,t)=>e>>>t|e<<32-t;for(let e=0;e>>3,n=o(r[e-2],17)^o(r[e-2],19)^r[e-2]>>>10;r[e]=r[e-16]+t+r[e-7]+n>>>0}let[i,l,s,d,f,c,u,p]=a;for(let e=0;e<64;e+=1){let n=p+(o(f,6)^o(f,11)^o(f,25))+(f&c^~f&u)+t[e]+r[e]>>>0,a=(o(i,2)^o(i,13)^o(i,22))+(i&l^i&s^l&s)>>>0;p=u,u=c,c=f,f=d+n>>>0,d=s,s=l,l=i,i=n+a>>>0}for(let[e,t]of[i,l,s,d,f,c,u,p].entries())a[e]=a[e]+t>>>0}return a.map(e=>e.toString(16).padStart(8,"0")).join("")}async function n(e){try{let t=await figma.getNodeByIdAsync(e);if(t)return t}catch(r){if(!e.startsWith("I")||!e.includes(";"))throw r;try{return await t()}catch(e){throw Error(`${r.message||r}; nested lookup fallback: ${e.message||e}`)}}if(!e.startsWith("I")||!e.includes(";"))return null;return t();async function t(){let t=await figma.getNodeByIdAsync(e.slice(1).split(";")[0]),r=t?[t]:[],n=new Set;for(;r.length;){let t=r.pop();if(t.id===e)return t;if(!n.has(t.id)){if(n.add(t.id),n.size>1e4)throw Error("nested lookup exceeds 10000 nodes");Array.isArray(t.children)&&r.push(...t.children)}}throw Error(`nested node ${e} not found in its instance`)}}async function i(e,t,r=85e3,n=()=>{}){let a,o=Date.now();try{let i=await Promise.race([e.exportAsync(t),new Promise((e,t)=>{a=setTimeout(()=>t(Error(`DEVUP_ASSET_TIMEOUT: native export exceeded ${r} ms`)),r)})]);return n("native-export",Date.now()-o),i}catch(i){if(n("native-export",Date.now()-o,String(i.message||i).slice(0,500)),"SVG_STRING"!==t.format)throw i;let r=Date.now();try{let t=function(e){let t=e=>"number"==typeof e&&Number.isFinite(e),r=(e,t)=>{throw Error(`geometry fallback unsupported: ${e.type||"unknown"} ${e.id||""}: ${t}`)},n=0,i=0;function a(e,i,a){(void 0!==a&&!Array.isArray(a)||void 0===a&&Array.isArray(i)&&i.length)&&r(e,"unresolved paint requires native export");let o=Array.isArray(a)?a.filter(e=>!1!==e.visible):[];return o.length?(Array.isArray(i)&&i.length||r(e,"paint has no rendered geometry"),o.map(a=>{("SOLID"!==a.type||a.blendMode&&"NORMAL"!==a.blendMode)&&r(e,"paint type or blending requires native export");let o=a.color;o&&[o.r,o.g,o.b].every(e=>t(e)&&e>=0&&e<=1)||r(e,"invalid solid color");let l=void 0===a.opacity?1:a.opacity;return(!t(l)||l<0||l>1)&&r(e,"invalid paint opacity"),["NONZERO","EVENODD"].map(t=>{let a=i.filter(e=>e.windingRule===t);if(!a.length)return"";for(let t of a)"string"==typeof t.data&&t.data.trim()||r(e,"empty path"),n++;return`"']/g,e=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[e])}" fill="rgb(${100*o.r}%,${100*o.g}%,${100*o.b}%)" fill-opacity="${l}" fill-rule="${"EVENODD"===t?"evenodd":"nonzero"}"/>`}).join("")}).join("")):""}let o=e.absoluteRenderBounds,l=e.absoluteTransform;(!o||![o.x,o.y,o.width,o.height].every(t)||o.width<=0||o.height<=0)&&r(e,"missing render bounds"),(!Array.isArray(l)||2!==l.length||!l.every(e=>Array.isArray(e)&&3===e.length&&e.every(t))||0!==l[0][1]||0!==l[1][0]||l[0][0]<=0||l[1][1]<=0)&&r(e,"root rotation, reflection or shear requires native export");let s=(o.x-l[0][2])/l[0][0],d=(o.y-l[1][2])/l[1][1],f=o.width/l[0][0],c=o.height/l[1][1],u=function e(n,o){if(++i>1e4&&r(n,"subtree exceeds 10000 nodes"),n.isMask&&r(n,"mask requires native export"),!1===n.visible||0===n.opacity)return"";n.clipsContent&&r(n,"clipping requires native export"),Array.isArray(n.effects)&&n.effects.some(e=>!1!==e.visible)&&r(n,"effect requires native export"),n.blendMode&&!["NORMAL","PASS_THROUGH"].includes(n.blendMode)&&r(n,"layer blending requires native export");let l=void 0===n.opacity?1:n.opacity;(!t(l)||l<0||l>1)&&r(n,"invalid layer opacity");let s="";if(!o){let e=n.relativeTransform;Array.isArray(e)&&2===e.length&&e.every(e=>Array.isArray(e)&&3===e.length&&e.every(t))||r(n,"missing relative transform"),s=` transform="matrix(${e[0][0]} ${e[1][0]} ${e[0][1]} ${e[1][1]} ${e[0][2]} ${e[1][2]})"`}let d="";if(["GROUP","FRAME","INSTANCE","COMPONENT"].includes(n.type))(Array.isArray(n.fills)&&n.fills.some(e=>!1!==e.visible)||Array.isArray(n.strokes)&&n.strokes.some(e=>!1!==e.visible))&&r(n,"container paint requires native export"),Array.isArray(n.children)||r(n,"missing children"),d=(!0===n.itemReverseZIndex?[...n.children].reverse():n.children).map(t=>e(t,!1)).join("");else{for(let e of(["VECTOR","BOOLEAN_OPERATION","RECTANGLE","ELLIPSE","POLYGON","STAR","LINE"].includes(n.type)||r(n,"node type requires native export"),Array.isArray(n.strokes)&&n.strokes.some(e=>!1!==e.visible)&&"CENTER"!==n.strokeAlign&&r(n,"non-center stroke alignment requires native export"),Array.isArray(n.dashPattern)&&n.dashPattern.length&&r(n,"dashed stroke requires native export"),[n.fillGeometry,n.strokeGeometry]))void 0!==e&&(!Array.isArray(e)||e.some(e=>!["NONZERO","EVENODD"].includes(e.windingRule)))&&r(n,"unknown winding rule");d=a(n,n.fillGeometry,n.fills)+a(n,n.strokeGeometry,n.strokes)}return`${d}`}(e,!0);return n||r(e,"no painted geometry"),`${u}`}(e);return n("geometry-fallback",Date.now()-r),t}catch(e){throw Error(`native export: ${i.message||i}; ${e.message||e}`)}}finally{clearTimeout(a)}}let a=Date.now(),o="lookup",l="unknown",s=Math.min(6e5,Math.max(1e3,Number(t.timeoutMs)||9e4)),d={method:"native",stages:[]};function f(e,t,r){d.stages.push({stage:e,elapsedMs:t}),r&&(d.nativeError=r),"geometry-fallback"===e&&(d.method="geometry")}async function c(e){let t;try{return await Promise.race([e(),new Promise((e,r)=>{t=setTimeout(()=>r(Error(`DEVUP_ASSET_TIMEOUT: ${o} exceeded ${s} ms asset budget`)),Math.max(0,s-(Date.now()-a)))})])}finally{clearTimeout(t)}}function u(e){let r=e&&"string"==typeof e.message?e.message:String(e),n="string"==typeof figma.editorType?` (editor: ${figma.editorType})`:"";return`${o} failed after ${Date.now()-a} ms${n} (node: ${t.nodeId}, type: ${l}): ${r}`.slice(0,500)}function p(e,r){return e&&d.renderInfo&&(d.renderInfo.attemptedRepresentation=d.renderInfo.representation,d.renderInfo.representation="not-exported",d.renderInfo.renderedAs="not-exported",d.renderInfo.effectsBakedIntoAsset=!1,d.renderInfo.fidelity="unverified"),{kind:"devupAssetExport",fileKey:figma.fileKey||"",version:t.version,assetId:t.assetId,nodeId:t.nodeId,field:t.field,imageHash:t.imageHash,format:t.format,scale:t.scale,renderMode:h,usageAspectRatio:t.usageAspectRatio??null,maxWidth:t.maxWidth??null,srcset:!0===t.srcset,visibleScopeNodeId:t.visibleScopeNodeId??null,status:"failed",byteLength:null,sha256:null,errorCode:e,exportDiagnostics:d,...r?{errorDetail:r}:{}}}let g="string"==typeof t.field&&t.field.startsWith("$original-image/fills/"),h=void 0===t.renderMode?g?"fill":["svg","pdf"].includes(t.format)?"node":"original-source":t.renderMode,y=["auto","original-source"].includes(h)?"auto"===h?"node":"fill":h;function m(e,r){let n=e=>"number"==typeof e&&Number.isFinite(e)&&e>0,i=Array.isArray(e.effects)?e.effects:null,a=[],o=e=>[...e].slice(0,128).join(""),l=["string"==typeof e.name?o(e.name):"layer"],s=new Set,d=e;for(let t=e.parent;t&&a.length<32&&!s.has(t);t=t.parent)s.add(t),l.length<4&&"string"==typeof t.name&&t.name.length&&!["PAGE","DOCUMENT"].includes(t.type)&&l.push(o(t.name)),a.push({type:t.type||"unknown",clipsContent:t.clipsContent??null,isMask:t.isMask??null,effects:Array.isArray(t.effects)?t.effects:null,opacity:t.opacity??null,blendMode:t.blendMode??null,visible:t.visible??null,siblingMaskPresent:Array.isArray(t.children)?t.children.some(e=>e!==d&&!0===e.isMask):null}),d=t;return{sourceKind:r?"image-fill":t.imageHash?"image-node":"vector-node",renderedAs:"fill"===y?"raw-fill":"node-render",representation:"fill"===y?"original-fill":"native-node",layerHierarchy:l.reverse(),scope:"fill"===y?"selected-paint-original-bytes":"exact-node-subtree",exportSettings:"node"===y?{contentsOnly:!0,useAbsoluteBounds:!1}:null,requestedScale:t.scale,scaleSemantics:"fill"===y?"intrinsic-original-bytes-no-resize":["png","jpg"].includes(t.format)?"native-node-SCALE-constraint":"not-applied-to-vector-document",nodeSize:n(e.width)&&n(e.height)?{width:e.width,height:e.height,aspectRatio:e.width/e.height}:null,outputSize:null,dimensionEvidence:"unknown-until-byte-inspection",aspectRatioComparison:"unknown",scaleMode:r?r.scaleMode??null:null,imageTransform:r?r.imageTransform??null:null,visibleSourceRatio:null,visibleSourceRatioEvidence:"not-computed",effects:i,extraFills:Array.isArray(e.fills)?Math.max(0,e.fills.filter(e=>!1!==e.visible).length-!!r):null,effectsBakedIntoAsset:"node"===y&&["png","jpg"].includes(t.format)&&null!==i,bakedEffects:"node"===y&&["png","jpg"].includes(t.format)&&i?i.filter(e=>!1!==e.visible):[],instanceOverride:"unknown",instanceOverrideEvidence:"no-component-paint-comparison",sourceProperties:{scaleMode:r?r.scaleMode??null:null,imageTransform:r?r.imageTransform??null:null,rotation:r?r.rotation??null:null,filters:r?r.filters??null:null,opacity:r?r.opacity??null:null,effects:i,blurEffects:i?i.filter(e=>!1!==e.visible&&String(e.type).includes("BLUR")):null,isMask:e.isMask??null,clipsContent:e.clipsContent??null,ancestors:a},unappliedProperties:"fill"===y?["scaleMode","imageTransform","rotation","filters","paintOpacity","nodeGeometry","nodeTransforms","nodeEffectsIncludingBlur","nodeAndAncestorMasks","ancestorClipsAndEffects"]:[],warnings:"fill"===y?["original-fill-properties-unapplied","aspect-ratio-unknown"]:["ancestor-canvas-appearance-unverified","aspect-ratio-unknown"],fidelity:"unverified"}}function b(e,t){let r=0;for(let n=e;n&&r++<32;n=n.parent){if("INSTANCE"===n.type&&Array.isArray(n.overrides)&&n.overrides.find(t=>t.id===e.id&&Array.isArray(t.overriddenFields)&&t.overriddenFields.includes("fills"))){t.instanceOverride=!0,t.instanceOverrideEvidence={kind:"figma-instance-overrides",instanceId:n.id,nodeId:e.id,overriddenFields:["fills"]};return}if(["PAGE","DOCUMENT"].includes(n.type))break}}try{let e;if(!["original-source","fill","node","visible","auto"].includes(h))return p("DEVUP_ASSET_RENDER_MODE_UNSUPPORTED");if(!g&&"fill"===h&&(!String(t.field).startsWith("fills/")||!["png","jpg","webp"].includes(t.format)||1!==t.scale))return p("DEVUP_ASSET_FILL_SETTINGS_UNSUPPORTED","Original fill requires fills/N, png or jpg matching its codec, and scale 1; no resizing or transcoding.");if((g||["fill","original-source"].includes(h))&&"bridge"!==t.transport)return p(g?"DEVUP_ORIGINAL_IMAGE_REQUIRES_BRIDGE":"DEVUP_ASSET_FILL_REQUIRES_BRIDGE");let x=await c(()=>n(t.nodeId));if(f("lookup",Date.now()-a),l=x&&x.type||"unknown",!x||["node","visible"].includes(h)&&"function"!=typeof x.exportAsync)return p("DEVUP_ASSET_UNSUPPORTED_BY_UPSTREAM",u(Error(x?"node has no exportAsync":"node not found")));let E=null;if(g||"string"==typeof t.field&&t.field.startsWith("fills/")){let e=Number(t.field.slice(g?22:6)),r="fills"in x&&Array.isArray(x.fills)?x.fills:[],n=Number.isInteger(e)?r[e]:null,i=n&&"IMAGE"===n.type?n.imageHash||n.imageRef:null;if(!n||i!==t.imageHash)return p("DEVUP_ASSET_SOURCE_CHANGED");E=n}else if("node"!==t.field)return p("DEVUP_ASSET_FIELD_UNSUPPORTED");if("original-source"===h&&(!E||!t.imageHash))return p("DEVUP_ORIGINAL_SOURCE_UNAVAILABLE","No IMAGE fill source exists for this selection. Choose an IMAGE fill or explicitly renderMode node; vector assets should retain SVG.");let A=x;if("visible"===h){let e=t.visibleScopeNodeId;if(!e)return p("DEVUP_VISIBLE_CONTEXT_UNAVAILABLE","Recollect the parent frame and provide visibleScopeNodeId.");let r=new Set;for(;A&&A.id!==e&&!r.has(A)&&r.size<64;)r.add(A),A=A.parent;if(!A||A.id!==e||["PAGE","DOCUMENT"].includes(A.type)||"function"!=typeof A.exportAsync)return p("DEVUP_VISIBLE_SCOPE_UNSUPPORTED","The selected capture scope is not an exportable ancestor of the requested node.")}if(d.renderInfo=m(x,E),"visible"===h){let e=d.renderInfo;e.representation="native-visible",e.renderedAs="visible-scoped-capture",e.captureNodeId=A.id,e.captureBox={width:A.width,height:A.height,aspectRatio:A.width/A.height},e.contextScope="selected-ancestor-subtree",e.captureEffects=Array.isArray(A.effects)?A.effects:null,e.requestedNodeEffects=e.effects,e.effects=e.captureEffects,e.sourceProperties.captureEffects=e.captureEffects,e.visibleContext={requestedNodeId:x.id,captureNodeId:A.id,included:["selected-scope-children","selected-scope-clipping","selected-scope-masks","selected-scope-rounding"],outsideScope:"unknown; ancestors, page backdrop and external layers are not included",canvasReferenceCompared:!1},e.warnings.push("visible-scoped-capture","outside-capture-context-unverified")}b(x,d.renderInfo);let I=null;if(E)try{let r=figma.getImageByHash(t.imageHash),n=r&&await Promise.race([r.getSizeAsync(),new Promise(t=>{e=setTimeout(()=>t(null),250)})]);I=n&&n.width>0&&n.height>0&&Number.isFinite(n.width)&&Number.isFinite(n.height)?{...n,aspectRatio:n.width/n.height}:null}catch(e){}finally{clearTimeout(e)}let S=null;if("auto"===h){let e=function(e,r,n){let i,a=[],o=(e,t)=>{e||a.push(t)},l=e=>Array.isArray(e)&&2===e.length&&e.every(e=>Array.isArray(e)&&3===e.length&&e.every(Number.isFinite))&&1===e[0][0]&&1===e[1][1]&&0===e[0][1]&&0===e[1][0]&&Number.isInteger(e[0][2])&&Number.isInteger(e[1][2]);for(let a of(o("bridge"===t.transport,"raw-requires-bridge"),o(1===t.scale&&["png","jpg"].includes(t.format),"raw-requires-unscaled-raster"),o(!!r&&!0===r.visible&&1===r.opacity&&"NORMAL"===r.blendMode,"paint-compositing-or-unknown"),o(r&&"FILL"===r.scaleMode&&Array.isArray(i=r.imageTransform)&&2===i.length&&i.every((e,t)=>Array.isArray(e)&&3===e.length&&e.every((e,r)=>e===[[1,0,0],[0,1,0]][t][r]))&&0===r.rotation,"paint-transform-or-unknown"),o(r&&r.filters&&Object.values(r.filters).every(e=>0===e),"image-filters-or-unknown"),o(Array.isArray(e.fills)&&1===e.fills.length,"multiple-fills-or-unknown"),o("RECTANGLE"===e.type&&0===e.cornerRadius&&Array.isArray(e.strokes)&&0===e.strokes.length&&(!e.children||0===e.children.length),"node-geometry-or-unknown"),o(Array.isArray(e.effects)&&0===e.effects.length&&!1===e.isMask&&(void 0===e.clipsContent||!1===e.clipsContent)&&1===e.opacity&&!0===e.visible&&"NORMAL"===e.blendMode,"node-effects-mask-compositing-or-unknown"),o(l(e.absoluteTransform)&&l(e.relativeTransform),"node-transform-or-unknown"),o(n.sourceProperties.ancestors.length>0&&n.sourceProperties.ancestors.some(e=>["PAGE","DOCUMENT"].includes(e.type)),"ancestor-chain-incomplete"),o(n.sourceProperties.ancestors.every(e=>!1===e.siblingMaskPresent),"ancestor-sibling-mask-or-unknown"),n.sourceProperties.ancestors.filter(e=>!["PAGE","DOCUMENT"].includes(e.type))))o(!1===a.clipsContent&&!1===a.isMask&&!1===a.siblingMaskPresent&&Array.isArray(a.effects)&&0===a.effects.length&&1===a.opacity&&!0===a.visible&&["NORMAL","PASS_THROUGH"].includes(a.blendMode),"ancestor-dependency-or-unknown");return{eligible:0===a.length,reasons:[...new Set(a)],evidence:"explicit-plugin-properties; intrinsic-dimensions-and-codec-required"}}(x,E,d.renderInfo);if(e.eligible)try{let r=figma.getImageByHash(t.imageHash),n=I;n&&n.width===x.width&&n.height===x.height?(S=await c(()=>r.getBytesAsync()),(137===S[0]&&80===S[1]&&78===S[2]&&71===S[3]?"png":255===S[0]&&216===S[1]&&255===S[2]?"jpg":"unknown")!==t.format&&e.reasons.push("source-codec-mismatch-or-unknown")):e.reasons.push("intrinsic-dimensions-mismatch-or-unknown")}catch(t){e.reasons.push("source-inspection-unavailable")}e.eligible=0===e.reasons.length,y=e.eligible?"fill":"node",d.renderInfo=m(x,E),b(x,d.renderInfo),d.renderInfo.autoDecision={...e,selected:y}}if(d.renderInfo.sourceSize=I,d.renderInfo.sourceSizeEvidence=I?"Figma-image-getSizeAsync":"unknown",g||"fill"===y){d.method="original-fill",o="reading the original image";let e=figma.getImageByHash(t.imageHash);if(!e)return p("DEVUP_ORIGINAL_IMAGE_NOT_FOUND");let n=S||await c(()=>e.getBytesAsync());if(0===n.length||n.length>0x5000000)return p("DEVUP_ASSET_RESPONSE_TOO_LARGE",`${n.length} bytes; one answer carries 1 to 83886080`);let i=e=>e.every((e,t)=>n[t]===e),a=i([137,80,78,71,13,10,26,10])?"image/png":i([255,216,255])?"image/jpeg":i([71,73,70,56])?"image/gif":i([82,73,70,70])&&87===n[8]&&69===n[9]&&66===n[10]&&80===n[11]?"image/webp":null;if(!a)return p("DEVUP_ORIGINAL_IMAGE_CODEC_UNSUPPORTED");if(d.renderInfo.originalMimeType=a,!g&&"fill"===h&&"webp"!==t.format&&a!==({png:"image/png",jpg:"image/jpeg"})[t.format])return p("DEVUP_ASSET_FILL_CODEC_MISMATCH",`Original fill codec is ${a}, but ${t.format} was requested; choose the matching format or renderMode node. No transcoding was performed.`);if(!g)return{...p(null),status:"exported",mimeType:a,byteLength:n.length,sha256:r(n),data:figma.base64Encode(n)};let l=await c(()=>e.getSizeAsync());return{...p(null),status:"exported",kind:"devupOriginalImage",representation:"original-image-v1",format:null,scale:null,mimeType:a,width:l.width,height:l.height,byteLength:n.length,sha256:r(n),data:figma.base64Encode(n)}}let w="webp"===t.format?"PNG":String(t.format||"").toUpperCase();if(!["PNG","JPG","SVG","PDF"].includes(w))return p("DEVUP_ASSET_FORMAT_UNSUPPORTED");let v=Number(t.scale??2);if(!Number.isFinite(v)||v<=0||v>4)return p("DEVUP_ASSET_SCALE_INVALID");let N="SVG"===w,O={format:N?"SVG_STRING":w,contentsOnly:!0,useAbsoluteBounds:!1};("PNG"===w||"JPG"===w)&&(O.constraint={type:"SCALE",value:v}),o="export";let T=Math.max(1,s-(Date.now()-a)),_=N?Math.max(1,T-Math.min(5e3,Math.floor(T/5))):T,P=Date.now(),M=await c(()=>"visible"===h?A.exportAsync(O):E?x.exportAsync(O):i(x,O,_,f));if(E&&f("native",Date.now()-P),"geometry"===d.method&&(d.renderInfo.representation="geometry-fallback",d.renderInfo.renderedAs="geometry-fallback",d.renderInfo.effectsBakedIntoAsset=!1,d.renderInfo.warnings.push("geometry-fallback-not-native-render")),Date.now()-a>=s)throw Error(`DEVUP_ASSET_TIMEOUT: export exceeded ${s} ms asset budget`);o="encode";let C=Date.now(),k=N&&"string"==typeof M?M:null,R=null===k?M instanceof Uint8Array?M:new Uint8Array(M):function(e){let t=[];for(let r=0;r=55296&&n<=56319){let t=r+1=56320&&t<=57343?(n=65536+(n-55296<<10)+(t-56320),r+=1):n=65533}else n>=56320&&n<=57343&&(n=65533);n<128?t.push(n):n<2048?t.push(192|n>>6,128|63&n):n<65536?t.push(224|n>>12,128|n>>6&63,128|63&n):t.push(240|n>>18,128|n>>12&63,128|n>>6&63,128|63&n)}return new Uint8Array(t)}(k);if(0===R.length||R.length>0x5000000)return p("DEVUP_ASSET_RESPONSE_TOO_LARGE",`${w} at scale ${v} is ${R.length} bytes; one answer carries 1 to 83886080`);let D=r(R);f("encode",Date.now()-C);let L=figma.io&&"function"==typeof figma.io.write,$="PNG"===w&&R.length>786432;if(L&&(null!==k&&R.length>12288||$))return{kind:"devupAssetExport",fileKey:figma.fileKey||"",version:t.version,assetId:t.assetId,nodeId:t.nodeId,field:t.field,imageHash:t.imageHash,format:t.format,scale:v,renderMode:h,usageAspectRatio:t.usageAspectRatio??null,maxWidth:t.maxWidth??null,srcset:!0===t.srcset,visibleScopeNodeId:t.visibleScopeNodeId??null,status:"chunked",byteLength:R.length,sha256:D,cursor:{nextOffset:0,maxChunkBytes:12288},errorCode:null,exportDiagnostics:d};return L&&figma.io.write(`devup-asset-${t.assetId.replace(/[^A-Za-z0-9_-]/g,"_")}.${w.toLowerCase()}`,R),{kind:"devupAssetExport",fileKey:figma.fileKey||"",version:t.version,assetId:t.assetId,nodeId:t.nodeId,field:t.field,imageHash:t.imageHash,format:t.format,scale:v,renderMode:h,usageAspectRatio:t.usageAspectRatio??null,maxWidth:t.maxWidth??null,srcset:!0===t.srcset,visibleScopeNodeId:t.visibleScopeNodeId??null,status:"exported",byteLength:R.length,sha256:D,mimeType:L?null===k?null:"image/svg+xml":({PNG:"image/png",JPG:"image/jpeg",SVG:"image/svg+xml",PDF:"application/pdf"})[w],...!L&&null===k?{data:figma.base64Encode(R)}:{},text:k,errorCode:null,exportDiagnostics:d}}catch(e){return p(String(e.message||e).includes("DEVUP_ASSET_TIMEOUT:")?"DEVUP_ASSET_TIMEOUT":g||["fill","original-source"].includes(h)?"DEVUP_ORIGINAL_IMAGE_READ_FAILED":"DEVUP_ASSET_EXPORT_FAILED",u(e))}},bridgeContext:async function(e){let t=figma.currentPage,r=t.selection,n=e=>({id:e.id,name:String(e.name).slice(0,256),type:e.type});return{kind:"devup-bridge-context",currentPage:n(t),selection:r.slice(0,20).map(n),selectionCount:r.length,zoom:Number.isFinite(figma.viewport?.zoom)?figma.viewport.zoom:null,observedAt:Date.now(),lastKnownSelection:globalThis.__devupLastSelection??null,lastKnownPage:globalThis.__devupLastSelectionPage??null}},explore:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=await figma.getNodeByIdAsync(e.nodeId);if(!t)throw Error("DEVUP_NODE_NOT_FOUND");let r=e.explore,n=Math.max(20,Math.min(400,Number.isInteger(r.projectionLimit)?r.projectionLimit:200)),i=Math.max(0,Math.min(500,Number.isInteger(r.textPreviewLimit)?r.textPreviewLimit:160));function a(e){return"string"==typeof e?e.slice(0,240):""}let o="PAGE"===t.type,l=t,s="SECTION"===t.type?t:null;for(;!o&&l.parent&&"PAGE"!==l.parent.type;)l=l.parent,s||"SECTION"!==l.type||(s=l);let d="PAGE"===t.type?t:l.parent;if(!d||"PAGE"!==d.type)throw Error("DEVUP_PAGE_NOT_FOUND");function f(e){let t=e.absoluteBoundingBox||{x:"number"==typeof e.x?e.x:0,y:"number"==typeof e.y?e.y:0,width:"number"==typeof e.width?e.width:0,height:"number"==typeof e.height?e.height:0};return[t.x,t.y,t.width,t.height].every(Number.isFinite)?{x:t.x,y:t.y,width:t.width,height:t.height}:null}function c(e){let t=f(e);return t&&["FRAME","COMPONENT","INSTANCE","COMPONENT_SET"].includes(e.type)&&t.width>=240&&t.width<=1800&&t.height>=300&&t.height<=2e3&&t.width/Math.max(1,t.height)>=.25&&t.width/Math.max(1,t.height)<=2.5}function u(e){let t=[],r=e;for(;r&&"DOCUMENT"!==r.type&&t.length<12;){let e=a(r.name);e&&t.push(e),r=r.parent}return t.reverse()}await figma.setCurrentPageAsync(d);let p=o?d:!c(t)&&s?s:l,g=f(p)||f(t);if(!g)throw Error("DEVUP_NODE_BOUNDS_UNAVAILABLE");let h="children"in d?d.children:[],y=h.map((e,t)=>({node:e,pageChildIndex:t,bounds:f(e)})).filter(e=>e.bounds).filter(e=>{var t;return!!o||e.node.id===p.id||e.bounds.y>=g.y-240&&e.bounds.y<=g.y+12e3&&(t=e.bounds,Math.min(t.x+t.width,g.x+g.width)-Math.max(t.x,g.x)>0)}).sort((e,t)=>e.bounds.y-t.bounds.y||e.bounds.x-t.bounds.x||e.node.id.localeCompare(t.node.id)),m="SECTION"===p.type?y.filter(e=>e.node.id===p.id):y.slice(0,n),b=new Map(m.map(e=>[e.node.id,e]));b.has(l.id)||b.set(l.id,{node:l,pageChildIndex:-1,bounds:f(l)}),b.has(p.id)||b.set(p.id,{node:p,pageChildIndex:-1,bounds:f(p)}),b.has(t.id)||b.set(t.id,{node:t,pageChildIndex:-1,bounds:f(t)});let x=o?null:t.parent;for(;x&&"PAGE"!==x.type&&(b.has(x.id)||b.set(x.id,{node:x,pageChildIndex:-1,bounds:f(x)}),x.id!==p.id);)x=x.parent;if("children"in p)for(let e of p.children.slice(0,n))b.has(e.id)||b.set(e.id,{node:e,pageChildIndex:-1,bounds:f(e)});let E="SECTION"===p.type||o,A=E&&"children"in p&&p.children.length>n;if(E&&"children"in p){let e=8*n,t=p.children.filter(e=>!c(e)).map(e=>({node:e,ancestors:[]})),r=0;for(;t.length&&r!b.has(e.id));if(b.size+t.length>n+2){A=!0;continue}for(let e of t)b.set(e.id,{node:e,pageChildIndex:-1,bounds:f(e)});continue}if("children"in e)for(let r of e.children)t.push({node:r,ancestors:[...i,e]})}A||=t.length>0}let I=[...b.values()].filter(e=>e.bounds).slice(0,n+2).map(({node:e,pageChildIndex:t,bounds:r})=>{let n;return{id:e.id,type:e.type,fields:{name:a(e.name),parentId:e.parent&&"DOCUMENT"!==e.parent.type?e.parent.id:null,childrenIds:[],x:r.x,y:r.y,width:r.width,height:r.height,childCount:"children"in e?e.children.length:0,textPreview:"",...null===(n=function(e){let t=[e],r=0,n=null;for(;t.length&&r++<400;){let e=t.shift();if("visible"in e&&!1===e.visible)continue;let r="children"in e?e.children.filter(e=>!("visible"in e)||!1!==e.visible):[];if(/header|nav|gnb/i.test(e.name||"")&&r.length>=2&&"layoutMode"in e&&"HORIZONTAL"===e.layoutMode){let t=r.map(f);t.every(Boolean)&&(n=Math.max(n||0,t.reduce((e,t)=>e+t.width,0)+(Number(e.itemSpacing)||0)*(t.length-1)))}t.push(...r.slice(0,80))}return n}(e))?{}:{navigationContentWidth:n},pageChildIndex:t>=0?t:null,visible:!("visible"in e)||!1!==e.visible,breadcrumb:u(e)},extra:{},fieldErrors:{}}}),S=I.reduce((e,t)=>{let r=t.fields;return e?{x:Math.min(e.x,r.x),y:Math.min(e.y,r.y),right:Math.max(e.right,r.x+r.width),bottom:Math.max(e.bottom,r.y+r.height)}:{x:r.x,y:r.y,right:r.x+r.width,bottom:r.y+r.height}},null)||{x:0,y:0,right:0,bottom:0},w="SECTION"!==p.type&&y.length>m.length||b.size>n+2||A,v={id:d.id,type:d.type,fields:{name:a(d.name),parentId:null,childrenIds:[],x:S.x,y:S.y,width:S.right-S.x,height:S.bottom-S.y,childCount:h.length,textPreview:"",projectionTruncated:w,visible:!0,breadcrumb:u(d),pageChildIndex:null},extra:{},fieldErrors:{}},N={fileKey:figma.fileKey||"",version:null,rootIds:[d.id],nodes:[v,...I.filter(e=>e.id!==d.id)],diagnostics:[]},O=new Set([d.id,l.id,p.id,t.id]);for(;JSON.stringify(N).length>14e3;){let e=N.nodes.length-1;for(;e>=0&&O.has(N.nodes[e].id);)e-=1;if(e<0)break;N.nodes.splice(e,1),v.fields.projectionTruncated=!0}if(JSON.stringify(N).length>14e3&&(N.nodes=N.nodes.filter(e=>O.has(e.id)).map(e=>({...e,fields:{...e.fields,name:a(e.fields.name).slice(0,80),textPreview:"",breadcrumb:e.fields.breadcrumb.slice(-4).map(e=>e.slice(0,80)),projectionTruncated:e.id===d.id||e.fields.projectionTruncated}}))),JSON.stringify(N).length>14e3)throw Error("DEVUP_EXPLORE_PROJECTION_TOO_LARGE");function T(e){let t=0;for(let r of e){let e=r.codePointAt(0);t+=e<128?1:e<2048?2:e<65536?3:4}return t}if(o){for(;T(JSON.stringify(N))>19456&&N.nodes.length>1;)N.nodes.pop(),v.fields.projectionTruncated=!0;if(T(JSON.stringify(N))>19456)throw Error("DEVUP_EXPLORE_PROJECTION_TOO_LARGE")}let _=14e3-JSON.stringify(N).length;if(o&&(_=Math.min(_,19456-T(JSON.stringify(N)))),i>0&&_>0){let e=N.nodes.filter(e=>b.get(e.id)&&e.id!==d.id),t=e.length;for(let r of e){let e=Math.floor(_/t);if(t-=1,e<=0)continue;let n="",a=0;for(let t of function(e){if(0===i)return"";let t=[],r=[e],n=0;for(;r.length&&n<80&&t.join(" ").lengthe)break;n+=t,a+=r}r.fields.textPreview=n,_-=a}}return N},fastSnapshot:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=e.rootIds;if(!Array.isArray(t)||0===t.length)throw Error("DEVUP_ROOTS_INVALID");let r=await Promise.all(t.map(e=>figma.getNodeByIdAsync(e)));if(r.some(e=>!e))throw Error("DEVUP_NODE_NOT_FOUND");if(1===r.length&&"SECTION"===r[0].type)throw Error("DEVUP_TARGET_IS_SECTION");let n=["mobile","tablet","desktop"],i=e=>{let t=String(e.name||"").trim().toLowerCase(),r=n.indexOf(t);if(r>=0)return r;let i=t.match(/^(\d+)\s*[~–]\s*(\d+)$/);if(!i)return -1;let a=Number(i[1]),o=Number(i[2]);return Number.isSafeInteger(a)&&Number.isSafeInteger(o)&&a>0&&o>=a&&o<=0xffffffff?1:-1};if(1===r.length&&i(r[0])>=0){let e=r[0].parent;if(e&&"SECTION"===e.type&&"children"in e){let t=e.children.filter(e=>e.id===r[0].id||i(e)>=0).filter(e=>!1!==e.visible);t.length>1&&(r.length=0,r.push(...t))}}if(1===t.length){let e=[],t=new Set(r.map(e=>e.id)),n=[],i=e=>{if("reactions"in e&&Array.isArray(e.reactions)){for(let r of e.reactions)if(r&&r.trigger&&"AFTER_TIMEOUT"===r.trigger.type)for(let e of r.actions||[])e&&"NODE"===e.type&&e.transition&&"SMART_ANIMATE"===e.transition.type&&"string"==typeof e.destinationId&&!t.has(e.destinationId)&&(t.add(e.destinationId),n.push(e.destinationId));if("children"in e)for(let t of e.children)i(t)}};for(let e of r)i(e);for(;n.length>0;){let t=n.shift(),r=await figma.getNodeByIdAsync(t);r&&"DOCUMENT"!==r.type&&"PAGE"!==r.type&&(e.push(r),i(r))}r.push(...e)}let a=e.nodeId,o=["absoluteBoundingBox","absoluteRenderBounds","arcData","backgroundStyleId","blendMode","bottomLeftRadius","bottomRightRadius","boundVariables","characters","clipsContent","componentProperties","componentPropertyDefinitions","componentPropertyReferences","constraints","cornerRadius","counterAxisAlignItems","dashPattern","defaultVariant","effectStyleId","effects","fillStyleId","fills","fontName","fontSize","gridColumnAnchorIndex","gridColumnCount","gridColumnGap","gridColumnSizes","gridColumnSpan","gridRowAnchorIndex","gridRowCount","gridRowGap","gridRowSizes","gridRowSpan","gridStyleId","height","inferredAutoLayout","isAsset","isMask","itemSpacing","layoutGrow","layoutMode","layoutWrap","layoutPositioning","layoutSizingHorizontal","layoutSizingVertical","letterSpacing","lineHeight","maxHeight","maxLines","maxWidth","minHeight","minWidth","name","opacity","overflowDirection","paddingBottom","paddingLeft","paddingRight","paddingTop","primaryAxisAlignItems","reactions","rotation","strokeAlign","strokeBottomWeight","strokeLeftWeight","strokeRightWeight","strokeStyleId","strokeTopWeight","strokeWeight","strokes","targetAspectRatio","textAlignHorizontal","textAlignVertical","textAutoResize","textCase","textDecoration","textStyleId","textTruncation","topLeftRadius","topRightRadius","variantProperties","visible","width","x","y"],l=["fontName","fontWeight","fontSize","textDecoration","textCase","lineHeight","letterSpacing","fills","textStyleId","fillStyleId","listOptions","indentation","hyperlink"],s=e.snapshot,d=Math.max(0,Math.floor(Number(s.offset)||0)),f=Math.max(8192,Math.floor(Number(s.maxEnvelopeBytes)||19456)),c=Math.min(s.maxEnvelopeBytes?f-1024:18e3,Math.max(4096,Math.floor(Number(s.maxPayloadBytes)||15e3))),u=new Set(["backgroundStyleId","effectStyleId","fillStyleId","gridStyleId","strokeStyleId","textStyleId"]),p=new Set(["maxWidth","maxHeight","absoluteRenderBounds"]),g=new Map([["rotation",0],["cornerRadius",0],["isAsset",!1],["isMask",!1],["clipsContent",!1],["blendMode","PASS_THROUGH"],["strokeAlign","INSIDE"],["textCase","ORIGINAL"],["textDecoration","NONE"],["textAlignHorizontal","LEFT"],["textAlignVertical","TOP"],["counterAxisAlignItems","MIN"],["primaryAxisAlignItems","MIN"],["gridColumnCount",0],["gridRowCount",0],["gridColumnGap",0],["gridRowGap",0],["gridColumnAnchorIndex",-1],["gridRowAnchorIndex",-1],["gridColumnSpan",1],["gridRowSpan",1]]),h=new Set(["start","end","characters","fontWeight","textStyleId","fillStyleId","listOptions","indentation","hyperlink"]),y=new Set(["parent","children","consumers"]);function m(e,t=!1,r=new WeakSet,n=0){let i;if(null===e||["string","number","boolean"].includes(typeof e))return e;if(void 0===e)return{$undefined:!0};if("bigint"==typeof e)return{$bigint:e.toString()};if(["function","symbol"].includes(typeof e))return{$unsupported:typeof e};if(n>12)return{$truncated:"max-depth"};if("object"==typeof e&&"parent"in e&&"string"==typeof e.id&&"string"==typeof e.type)return{$nodeId:e.id,$nodeType:e.type};if(Array.isArray(e))return e.map(e=>m(e,t,r,n+1));if(ArrayBuffer.isView(e))return{$binary:e.constructor.name,byteLength:e.byteLength};if(e instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:e.byteLength};if(r.has(e))return{$circular:!0};if(r.add(e),t){let t=new Set(Object.keys(e)),r=e;for(;r&&r!==Object.prototype;){for(let e of Object.getOwnPropertyNames(r))t.add(e);r=Object.getPrototypeOf(r)}i=[...t].sort().filter(e=>!e.startsWith("_")&&!y.has(e))}else i=Object.keys(e).sort();let a={};for(let o of i)try{let i=m(e[o],t,r,n+1);i&&"function"===i.$unsupported||(a[o]=i)}catch(e){a[o]=t?{$error:"unavailable"}:{$error:String(e&&e.message?e.message:e)}}return r.delete(e),a}let b=new Map;async function x(e,t,r){if("INSTANCE"!==e.type||"function"!=typeof e.getMainComponentAsync)return;b.has(e.id)||b.set(e.id,(async()=>{try{let t=await e.getMainComponentAsync();return{fields:{mainComponentId:t?t.id:null,mainComponentName:t?t.name:null,mainComponentSetId:t&&t.parent&&"COMPONENT_SET"===t.parent.type?t.parent.id:null}}}catch(e){return{error:String(e&&e.message?e.message:e)}}})());let n=await b.get(e.id);n.error?r.mainComponentId=n.error:Object.assign(t,n.fields)}async function E(e){let t={},r={};e.parent&&(t.parentId=e.parent.id),e.parent&&("PAGE"===e.parent.type||"SECTION"===e.parent.type||"COMPONENT_SET"===e.parent.type)&&(t.parentType=e.parent.type,"SECTION"===e.parent.type&&(t.parentName=e.parent.name));let n="children"in e?e.children.map(e=>e.id):[];for(let i of(n.length>0&&(t.childrenIds=n),o)){let n;if("componentPropertyDefinitions"!==i||"COMPONENT"!==e.type||!e.parent||"COMPONENT_SET"!==e.parent.type)try{if(!(i in e)){"overflowDirection"===i&&["FRAME","COMPONENT","INSTANCE","COMPONENT_SET"].includes(e.type)&&(t[i]=null);continue}if(n=e[i],"function"==typeof n)continue;let r=m(n);if("overflowDirection"===i&&null==n){t[i]=null;continue}(null===r?!p.has(i):Array.isArray(r)?0===r.length:"object"==typeof r?0===Object.keys(r).length:!!(""===r&&u.has(i))||g.has(i)&&g.get(i)===r)||(t[i]=r)}catch(e){r[i]=String(e&&e.message?e.message:e)}}if("TEXT"===e.type&&"function"==typeof e.getStyledTextSegments)try{let r=m(e.getStyledTextSegments(l));if(1===r.length){let e=r[0];for(let t of Object.keys(e))h.has(t)||delete e[t]}r.length>0&&(t.styledTextSegments=r)}catch(e){r.styledTextSegments=String(e&&e.message?e.message:e)}await x(e,t,r);let i={id:e.id,type:e.type,fields:t};return Object.keys(r).length>0&&(i.fieldErrors=r),i}let A=[],I=[...r],S=new Set;for(let e=0;e=A.length&&A.length>0)throw Error("DEVUP_SNAPSHOT_RANGE_INVALID");function w(e){let t=0;for(let r=0;r=55296&&n<=56319&&r+1({id:e,styleType:t})).sort((e,t)=>e.id.localeCompare(t.id));async function a(e){try{let t=await figma.variables.getVariableByIdAsync(e);return t?{kind:"variable",value:m(t,!0),collectionId:t.variableCollectionId,aliasIds:Object.values(t.valuesByMode||{}).filter(e=>e&&"VARIABLE_ALIAS"===e.type&&"string"==typeof e.id&&e.id).map(e=>e.id)}:{kind:"unresolved",value:{id:e,kind:"variable",reason:"notFoundOrUnavailable"}}}catch(t){return{kind:"unresolved",value:{id:e,kind:"variable",reason:"notFoundOrUnavailable",error:String(t&&t.message||t).slice(0,300)}}}}let o=await Promise.all([...n.map(a),...i.map(async({id:e,styleType:t})=>{try{let r=await figma.getStyleByIdAsync(e);if(!r)return{kind:"unresolved",value:{id:e,kind:"style",reason:"notFoundOrUnavailable"}};return{kind:"style",value:{...m(r,!0),styleType:t,value:m("PAINT"===t?r.paints:"EFFECT"===t?r.effects:"GRID"===t?r.layoutGrids:r,!0)}}}catch(t){return{kind:"unresolved",value:{id:e,kind:"style",reason:"notFoundOrUnavailable",error:String(t&&t.message||t).slice(0,300)}}}})]),l=new Set(n),s=o.flatMap(e=>e.aliasIds||[]);for(;s.length>0;){let e=[...new Set(s)].filter(e=>!l.has(e)).sort();for(let t of e)l.add(t);let t=await Promise.all(e.map(a));o.push(...t),s=t.flatMap(e=>e.aliasIds||[])}let d=[...new Set(o.filter(e=>"variable"===e.kind&&e.collectionId).map(e=>e.collectionId))].sort(),f=(await Promise.all(d.map(async e=>{try{let t=await figma.variables.getVariableCollectionByIdAsync(e);return t?m(t,!0):null}catch(e){return null}}))).filter(e=>null!==e),c=o.filter(e=>"variable"===e.kind).map(e=>e.value),u=o.filter(e=>"style"===e.kind).map(e=>e.value),p=o.filter(e=>"unresolved"===e.kind).map(e=>e.value);return{collections:f,variables:c,styles:u,usedRemoteVariables:c.filter(e=>!0===e.remote),usedVariableIds:n,usedStyleIds:i.map(e=>e.id),localComplete:!1,usedRemoteComplete:0===p.length,unresolved:p,$variableRefCount:n.length,$styleRefCount:i.length}}async function N(e){let t=[],r=2;for(let n=d;ne)break;t.push(i),r+=a}return{pageNodes:t,packedBytes:r}}let O=c-1024,T=null;for(let e=0;e<5;e+=1){let{pageNodes:e,packedBytes:t}=await N(O);if(0===e.length)throw Error("DEVUP_SNAPSHOT_RANGE_INVALID");let n=function(e,t){let n=Math.min(A.length,d+e.length),{$variableRefCount:i,$styleRefCount:o,...l}=t,s=[...e,{id:"__DEVUP_SNAPSHOT_CURSOR__",type:"DEVUP_INTERNAL",fields:{offset:d,nextOffset:n,complete:n>=A.length,totalNodes:A.length},extra:{},fieldErrors:{}}],f={kind:"devupFastSnapshotEnvelope",schemaVersion:1,source:{fileKey:figma.fileKey||"",rootId:a},snapshot:{fileKey:figma.fileKey||"",version:null,rootIds:r.map(e=>e.id),nodes:s,diagnostics:[]},resources:l,integrity:{nodeCount:s.length,variableRefCount:i,styleRefCount:o,utf8Bytes:0}},c=0;for(let e=0;e<8&&(c=w(JSON.stringify(f)),f.integrity.utf8Bytes!==c);e+=1)f.integrity.utf8Bytes=c;if(f.integrity.utf8Bytes!==w(JSON.stringify(f)))throw Error("DEVUP_ENVELOPE_LENGTH_UNSTABLE");return{envelope:f,bytes:c}}(e,await v(e));if(n.bytes<=f){T=n;break}if(1===e.length)throw Error("DEVUP_ENVELOPE_TOO_LARGE");O=Math.max(1,Math.min(O-1,t-(n.bytes-f)-256))}if(!T)throw Error("DEVUP_ENVELOPE_TOO_LARGE");return T.envelope},fastTheme:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=Math.max(0,Math.floor(Number(e.theme.offset)||0));function r(e,t=new WeakSet,n=0){if(null===e||["string","number","boolean"].includes(typeof e))return e;if(void 0===e)return{$undefined:!0};if("bigint"==typeof e)return{$bigint:e.toString()};if(["function","symbol"].includes(typeof e))return{$unsupported:typeof e};if(n>12)return{$truncated:"max-depth"};if("object"==typeof e&&"parent"in e&&"string"==typeof e.id&&"string"==typeof e.type)return{$nodeId:e.id,$nodeType:e.type};if(Array.isArray(e))return e.map(e=>r(e,t,n+1));if(ArrayBuffer.isView(e))return{$binary:e.constructor.name,byteLength:e.byteLength};if(e instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:e.byteLength};if(t.has(e))return{$circular:!0};t.add(e);let i={};for(let a of function(e){let t=new Set(Object.keys(e)),r=e;for(;r&&r!==Object.prototype;){for(let e of Object.getOwnPropertyNames(r))t.add(e);r=Object.getPrototypeOf(r)}return[...t].sort()}(e))if(!(a.startsWith("_")||["parent","children","consumers"].includes(a)))try{let o=r(e[a],t,n+1);o&&"function"===o.$unsupported||(i[a]=o)}catch(e){i[a]={$error:"unavailable"}}return t.delete(e),i}let n=new Set,i=new Map;for(let e of figma.root.children)await e.loadAsync();for(let e of[figma.root,...figma.root.findAll(()=>!0)])for(let t of["boundVariables","fills","strokes","effects","layoutGrids","textStyleId","fillStyleId","strokeStyleId","backgroundStyleId","effectStyleId","gridStyleId"])try{!function e(t,r="",a=new WeakSet,o=0){if(!(o>16)&&null!==t&&"object"==typeof t&&!a.has(t)){if(a.add(t),Array.isArray(t)){for(let n of t)e(n,r,a,o+1);return}for(let[l,s]of("VARIABLE_ALIAS"===t.type&&"string"==typeof t.id&&t.id&&"figma.mixed"!==t.id&&"MIXED"!==t.id&&n.add(t.id),Object.entries(t))){let t="textStyleId"===l?"TEXT":["fillStyleId","strokeStyleId","backgroundStyleId"].includes(l)?"PAINT":"effectStyleId"===l?"EFFECT":"gridStyleId"===l?"GRID":null;t&&"string"==typeof s&&s&&"figma.mixed"!==s&&"MIXED"!==s&&!i.has(s)&&i.set(s,t),e(s,l||r,a,o+1)}}}({[t]:e[t]})}catch(e){}let[a,o,l,s,d,f]=await Promise.all([figma.variables.getLocalVariableCollectionsAsync(),figma.variables.getLocalVariablesAsync(),figma.getLocalPaintStylesAsync(),figma.getLocalTextStylesAsync(),figma.getLocalEffectStylesAsync(),figma.getLocalGridStylesAsync()]),c=new Set(o.map(e=>e.id)),u=new Set([...l,...s,...d,...f].map(e=>e.id)),p=[];async function g(e){try{return await figma.variables.getVariableByIdAsync(e)||null}catch(t){return p.push({id:e,kind:"variable",reason:"notFoundOrUnavailable",error:String(t&&t.message||t).slice(0,300)}),null}}function h(e){return Object.values(e.valuesByMode||{}).filter(e=>e&&"VARIABLE_ALIAS"===e.type&&"string"==typeof e.id&&e.id).map(e=>e.id)}let y=[...i.entries()].filter(([e])=>!u.has(e)).sort(([e],[t])=>e.localeCompare(t)).map(async([e,t])=>{try{let r=await figma.getStyleByIdAsync(e);return r?{style:r,styleType:t}:null}catch(t){return p.push({id:e,kind:"style",reason:"notFoundOrUnavailable",error:String(t&&t.message||t).slice(0,300)}),null}}),m=new Set,b=[],x=[...n,...o.flatMap(h)];for(;x.length>0;){let e=[...new Set(x)].filter(e=>!c.has(e)&&!m.has(e)).sort();for(let t of e)m.add(t);let t=(await Promise.all(e.map(g))).filter(Boolean);b.push(...t),x=t.flatMap(h)}let E=(await Promise.all(y)).filter(Boolean);for(let e of m)b.some(t=>t.id===e)||p.some(t=>t.id===e)||p.push({id:e,kind:"variable",reason:"notFoundOrUnavailable"});for(let[e]of[...i.entries()].filter(([e])=>!u.has(e)))E.some(t=>t.style.id===e)||p.some(t=>t.id===e)||p.push({id:e,kind:"style",reason:"notFoundOrUnavailable"});let A=[...new Set(b.map(e=>e.variableCollectionId))].filter(e=>!a.some(t=>t.id===e)).sort(),I=(await Promise.all(A.map(async e=>{try{return await figma.variables.getVariableCollectionByIdAsync(e)}catch(e){return null}}))).filter(Boolean);function S(e,t){return{...r(e),styleType:t,value:r("PAINT"===t?e.paints:"EFFECT"===t?e.effects:"GRID"===t?e.layoutGrids:e)}}let w=[...l.map(e=>S(e,"PAINT")),...s.map(e=>S(e,"TEXT")),...d.map(e=>S(e,"EFFECT")),...f.map(e=>S(e,"GRID")),...E.map(({style:e,styleType:t})=>S(e,t))].sort((e,t)=>e.id.localeCompare(t.id)),v=[...o,...b].map(e=>r(e)).sort((e,t)=>e.id.localeCompare(t.id)),N=[...a,...I].map(e=>r(e)).sort((e,t)=>e.id.localeCompare(t.id));function O(e){let t=[];for(let r=0;r=55296&&n<=56319){let t=r+1=56320&&t<=57343?(n=65536+(n-55296<<10)+(t-56320),r+=1):n=65533}else n>=56320&&n<=57343&&(n=65533);n<128?t.push(n):n<2048?t.push(192|n>>6,128|63&n):n<65536?t.push(224|n>>12,128|n>>6&63,128|63&n):t.push(240|n>>18,128|n>>12&63,128|n>>6&63,128|63&n)}return new Uint8Array(t)}p.sort((e,t)=>e.kind.localeCompare(t.kind)||e.id.localeCompare(t.id));let T=[...N.map(e=>({kind:"collection",value:e})),...v.map(e=>({kind:"variable",value:e})),...w.map(e=>({kind:"style",value:e})),...[...n].sort().map(e=>({kind:"usedVariableId",value:e})),...[...i.keys()].sort().map(e=>({kind:"usedStyleId",value:e})),...p.map(e=>({kind:"unresolved",value:e}))];if(t>T.length)throw Error("DEVUP_SNAPSHOT_RANGE_INVALID");let _=T.map(e=>O(JSON.stringify(e.value)).length+1),P=18432,M=null;for(let e=0;e<6;e+=1){let{pageItems:e,packed:r}=function(e){let r=[],n=0;for(let i=t;i0)||!(n+_[i]>e));i+=1)r.push(T[i]),n+=_[i];return{pageItems:r,packed:n}}(P),n=function(e,r){let n=t=>e.filter(e=>e.kind===t).map(e=>e.value),i=n("collection"),a=n("variable"),o=n("style"),l=n("unresolved"),s={kind:"devupFastThemeEnvelope",schemaVersion:1,source:{fileKey:figma.fileKey||"",version:null},resources:{collections:i,variables:a,styles:o,usedRemoteVariables:[],usedVariableIds:n("usedVariableId"),usedStyleIds:n("usedStyleId"),localComplete:!0,usedRemoteComplete:0===p.length,unresolved:l},page:{offset:t,nextOffset:r,complete:r>=T.length,totalItems:T.length},integrity:{collectionCount:i.length,variableCount:a.length,styleCount:o.length,unresolvedCount:l.length,utf8Bytes:0}},d=new Uint8Array;for(let e=0;e<8&&(d=O(JSON.stringify(s)),s.integrity.utf8Bytes!==d.length);e+=1)s.integrity.utf8Bytes=d.length;if(d=O(JSON.stringify(s)),s.integrity.utf8Bytes!==d.length)throw Error("DEVUP_ENVELOPE_LENGTH_UNSTABLE");return{envelope:s,bytes:d.length}}(e,t+e.length);if(n.bytes<=19456){M=n;break}if(e.length<=1)break;P=Math.max(1,Math.min(P-1,r-(n.bytes-19456)-256))}if(null===M||M.bytes>8388608)throw Error("DEVUP_ENVELOPE_TOO_LARGE");return M.envelope},largeValue:async function(e){let t;figma.skipInvisibleInstanceChildren=!1;let r=e.largeValue;async function n(e){try{let t=await figma.getNodeByIdAsync(e);if(t)return t}catch(r){if(!e.startsWith("I")||!e.includes(";"))throw r;try{return await t()}catch(e){throw Error(`${r.message||r}; nested lookup fallback: ${e.message||e}`)}}if(!e.startsWith("I")||!e.includes(";"))return null;return t();async function t(){let t=await figma.getNodeByIdAsync(e.slice(1).split(";")[0]),r=t?[t]:[],n=new Set;for(;r.length;){let t=r.pop();if(t.id===e)return t;if(!n.has(t.id)){if(n.add(t.id),n.size>1e4)throw Error("nested lookup exceeds 10000 nodes");Array.isArray(t.children)&&r.push(...t.children)}}throw Error(`nested node ${e} not found in its instance`)}}async function i(e,t,r=85e3,n=()=>{}){let a,o=Date.now();try{let i=await Promise.race([e.exportAsync(t),new Promise((e,t)=>{a=setTimeout(()=>t(Error(`DEVUP_ASSET_TIMEOUT: native export exceeded ${r} ms`)),r)})]);return n("native-export",Date.now()-o),i}catch(i){if(n("native-export",Date.now()-o,String(i.message||i).slice(0,500)),"SVG_STRING"!==t.format)throw i;let r=Date.now();try{let t=function(e){let t=e=>"number"==typeof e&&Number.isFinite(e),r=(e,t)=>{throw Error(`geometry fallback unsupported: ${e.type||"unknown"} ${e.id||""}: ${t}`)},n=0,i=0;function a(e,i,a){(void 0!==a&&!Array.isArray(a)||void 0===a&&Array.isArray(i)&&i.length)&&r(e,"unresolved paint requires native export");let o=Array.isArray(a)?a.filter(e=>!1!==e.visible):[];return o.length?(Array.isArray(i)&&i.length||r(e,"paint has no rendered geometry"),o.map(a=>{("SOLID"!==a.type||a.blendMode&&"NORMAL"!==a.blendMode)&&r(e,"paint type or blending requires native export");let o=a.color;o&&[o.r,o.g,o.b].every(e=>t(e)&&e>=0&&e<=1)||r(e,"invalid solid color");let l=void 0===a.opacity?1:a.opacity;return(!t(l)||l<0||l>1)&&r(e,"invalid paint opacity"),["NONZERO","EVENODD"].map(t=>{let a=i.filter(e=>e.windingRule===t);if(!a.length)return"";for(let t of a)"string"==typeof t.data&&t.data.trim()||r(e,"empty path"),n++;return`"']/g,e=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[e])}" fill="rgb(${100*o.r}%,${100*o.g}%,${100*o.b}%)" fill-opacity="${l}" fill-rule="${"EVENODD"===t?"evenodd":"nonzero"}"/>`}).join("")}).join("")):""}let o=e.absoluteRenderBounds,l=e.absoluteTransform;(!o||![o.x,o.y,o.width,o.height].every(t)||o.width<=0||o.height<=0)&&r(e,"missing render bounds"),(!Array.isArray(l)||2!==l.length||!l.every(e=>Array.isArray(e)&&3===e.length&&e.every(t))||0!==l[0][1]||0!==l[1][0]||l[0][0]<=0||l[1][1]<=0)&&r(e,"root rotation, reflection or shear requires native export");let s=(o.x-l[0][2])/l[0][0],d=(o.y-l[1][2])/l[1][1],f=o.width/l[0][0],c=o.height/l[1][1],u=function e(n,o){if(++i>1e4&&r(n,"subtree exceeds 10000 nodes"),n.isMask&&r(n,"mask requires native export"),!1===n.visible||0===n.opacity)return"";n.clipsContent&&r(n,"clipping requires native export"),Array.isArray(n.effects)&&n.effects.some(e=>!1!==e.visible)&&r(n,"effect requires native export"),n.blendMode&&!["NORMAL","PASS_THROUGH"].includes(n.blendMode)&&r(n,"layer blending requires native export");let l=void 0===n.opacity?1:n.opacity;(!t(l)||l<0||l>1)&&r(n,"invalid layer opacity");let s="";if(!o){let e=n.relativeTransform;Array.isArray(e)&&2===e.length&&e.every(e=>Array.isArray(e)&&3===e.length&&e.every(t))||r(n,"missing relative transform"),s=` transform="matrix(${e[0][0]} ${e[1][0]} ${e[0][1]} ${e[1][1]} ${e[0][2]} ${e[1][2]})"`}let d="";if(["GROUP","FRAME","INSTANCE","COMPONENT"].includes(n.type))(Array.isArray(n.fills)&&n.fills.some(e=>!1!==e.visible)||Array.isArray(n.strokes)&&n.strokes.some(e=>!1!==e.visible))&&r(n,"container paint requires native export"),Array.isArray(n.children)||r(n,"missing children"),d=(!0===n.itemReverseZIndex?[...n.children].reverse():n.children).map(t=>e(t,!1)).join("");else{for(let e of(["VECTOR","BOOLEAN_OPERATION","RECTANGLE","ELLIPSE","POLYGON","STAR","LINE"].includes(n.type)||r(n,"node type requires native export"),Array.isArray(n.strokes)&&n.strokes.some(e=>!1!==e.visible)&&"CENTER"!==n.strokeAlign&&r(n,"non-center stroke alignment requires native export"),Array.isArray(n.dashPattern)&&n.dashPattern.length&&r(n,"dashed stroke requires native export"),[n.fillGeometry,n.strokeGeometry]))void 0!==e&&(!Array.isArray(e)||e.some(e=>!["NONZERO","EVENODD"].includes(e.windingRule)))&&r(n,"unknown winding rule");d=a(n,n.fillGeometry,n.fills)+a(n,n.strokeGeometry,n.strokes)}return`${d}`}(e,!0);return n||r(e,"no painted geometry"),`${u}`}(e);return n("geometry-fallback",Date.now()-r),t}catch(e){throw Error(`native export: ${i.message||i}; ${e.message||e}`)}}finally{clearTimeout(a)}}let a=await n(r.nodeId);if(!a)throw Error("DEVUP_NODE_NOT_FOUND");function o(e){let t=[];for(let r=0;r=55296&&n<=56319){let t=r+1=56320&&t<=57343?(n=65536+(n-55296<<10)+(t-56320),r+=1):n=65533}else n>=56320&&n<=57343&&(n=65533);n<128?t.push(n):n<2048?t.push(192|n>>6,128|63&n):n<65536?t.push(224|n>>12,128|n>>6&63,128|63&n):t.push(240|n>>18,128|n>>12&63,128|n>>6&63,128|63&n)}return new Uint8Array(t)}let l=null;try{if("$export:svg"===r.field){if("function"!=typeof a.exportAsync)throw Error("unsupported");let e=await i(a,{format:"SVG_STRING"},Math.max(1,(Number(r.timeoutMs)||9e4)-5e3));if("string"!=typeof e)throw Error("unsupported");l=o(e)}else if("string"==typeof r.field&&r.field.startsWith("$export:png")){if("function"!=typeof a.exportAsync)throw Error("unsupported");let e=Number(r.field.split("@")[1]||2);if(!Number.isFinite(e)||e<=0||e>4)throw Error("invalid scale");let t=r.field.split("@"),n=a;if("visible"===t[2]){let e=new Set;for(;n&&n.id!==t[3]&&!e.has(n)&&e.size<64;)e.add(n),n=n.parent;if(!n||n.id!==t[3]||["PAGE","DOCUMENT"].includes(n.type))throw Error("invalid visible scope")}else if(t.length>2)throw Error("invalid export field");let i=await n.exportAsync({format:"PNG",contentsOnly:!0,useAbsoluteBounds:!1,constraint:{type:"SCALE",value:e}});l=i instanceof Uint8Array?i:new Uint8Array(i)}else if("styledTextSegments"===r.field&&"TEXT"===a.type&&"function"==typeof a.getStyledTextSegments)t=a.getStyledTextSegments(["fontName","fontWeight","fontSize","textDecoration","textCase","lineHeight","letterSpacing","fills","textStyleId","fillStyleId","listOptions","indentation","hyperlink"]);else if(r.field in a)t=a[r.field];else throw Error("unsupported")}catch(e){return{kind:"devupLargeValueUnsupported",fileKey:figma.fileKey||"",version:r.version,nodeId:r.nodeId,field:r.field,byteLength:r.byteLength,sha256:r.sha256,errorCode:"DEVUP_FIELD_UNSUPPORTED_BY_UPSTREAM",errorDetail:`${r.field} failed: ${e.message||e}`.slice(0,500)}}let s=null===l?o(JSON.stringify(function e(t,r=new WeakSet,n=0){if(null===t||["string","number","boolean"].includes(typeof t))return t;if(void 0===t)return{$undefined:!0};if("bigint"==typeof t)return{$bigint:t.toString()};if(["function","symbol"].includes(typeof t))return{$unsupported:typeof t};if(n>12)return{$truncated:"max-depth"};if("object"==typeof t&&"parent"in t&&"string"==typeof t.id&&"string"==typeof t.type)return{$nodeId:t.id,$nodeType:t.type};if(Array.isArray(t))return t.map(t=>e(t,r,n+1));if(ArrayBuffer.isView(t))return{$binary:t.constructor.name,byteLength:t.byteLength};if(t instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:t.byteLength};if(r.has(t))return{$circular:!0};r.add(t);let i={};for(let a of Object.keys(t).sort())try{let o=e(t[a],r,n+1);o&&"function"===o.$unsupported||(i[a]=o)}catch(e){i[a]={$error:String(e&&e.message?e.message:e)}}return r.delete(t),i}(t))):l,d=function(e){let t=[0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,0xe49b69c1,0xefbe4786,0xfc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x6ca6351,0x14292967,0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2],r=e.length,n=64*Math.ceil((r+9)/64),i=new Uint8Array(n);i.set(e),i[r]=128;let a=8*r;for(let e=0;e<8;e+=1)i[n-1-e]=255&Math.floor(a/2**(8*e));let o=[0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19],l=(e,t)=>e>>>t|e<<32-t;for(let e=0;e>>3,n=l(r[e-2],17)^l(r[e-2],19)^r[e-2]>>>10;r[e]=r[e-16]+t+r[e-7]+n>>>0}let[n,a,s,d,f,c,u,p]=o;for(let e=0;e<64;e+=1){let i=p+(l(f,6)^l(f,11)^l(f,25))+(f&c^~f&u)+t[e]+r[e]>>>0,o=(l(n,2)^l(n,13)^l(n,22))+(n&a^n&s^a&s)>>>0;p=u,u=c,c=f,f=d+i>>>0,d=s,s=a,a=n,n=i+o>>>0}for(let[e,t]of[n,a,s,d,f,c,u,p].entries())o[e]=o[e]+t>>>0}return o.map(e=>e.toString(16).padStart(8,"0")).join("")}(s);if(s.length!==r.byteLength||d!==r.sha256)throw Error("DEVUP_LARGE_VALUE_CHANGED");let f=Math.max(0,Math.floor(Number(r.offset)||0)),c=Math.min(65536,Math.max(1,Math.floor(Number(r.maxChunkBytes)||8192)));if(f>=s.length)throw Error("DEVUP_LARGE_VALUE_RANGE_INVALID");let u=Math.min(s.length,f+c);return{kind:"devupLargeValueFragment",fileKey:figma.fileKey||"",version:r.version,nodeId:r.nodeId,field:r.field,offset:f,nextOffset:u,byteLength:s.length,sha256:d,dataBase64:function(e){let t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",r="";for(let n=0;n>2],r+=t[(3&i)<<4|a>>4],r+=n+1>6]:"=",r+=n+210||t.some(e=>"string"!=typeof e||e.length>128||!/^(?:[0-9]+:[0-9]+|I[0-9]+:[0-9]+(?:;[0-9]+:[0-9]+)+)$/.test(e))||new Set(t).size!==t.length)throw Error("DEVUP_INVALID_INPUT: nodeIds must be 1..10 distinct Figma IDs");let r=(e,t)=>{try{let r=e[t];return"number"==typeof r&&Number.isFinite(r)?r:null}catch{return null}},n=(e,t)=>{try{let r=e[t];return"string"==typeof r&&r.length<=32&&/^[A-Z_]+$/.test(r)?r:null}catch{return null}},i=[];for(let e of t)try{let t=await figma.getNodeByIdAsync(e);if(!t||"DOCUMENT"===t.type||"PAGE"===t.type){i.push({nodeId:e,status:"unavailable",reason:t?"not-scene-node":"not-found"});continue}let a={width:r(t,"width"),height:r(t,"height"),padding:{top:r(t,"paddingTop"),right:r(t,"paddingRight"),bottom:r(t,"paddingBottom"),left:r(t,"paddingLeft")},gap:r(t,"itemSpacing"),layoutMode:n(t,"layoutMode"),align:{primary:n(t,"primaryAxisAlignItems"),counter:n(t,"counterAxisAlignItems")}},o=null!==a.width&&null!==a.height;i.push({nodeId:e,status:o?"measured":"partial",...a})}catch{i.push({nodeId:e,status:"unavailable",reason:"read-failed"})}return{kind:"devup-measure",schemaVersion:1,coordinateSpace:"figma-logical",nodes:i,partial:i.some(e=>"measured"!==e.status),evidenceLimit:"Null means unavailable or not applicable; no screenshot dimensions, descendants, theme, assets or code were read."}},metadata:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=await figma.getNodeByIdAsync(e.nodeId);if(!t)throw Error("DEVUP_NODE_NOT_FOUND");let r=t;for(;r&&"PAGE"!==r.type;)r=r.parent;r&&"PAGE"===r.type&&await figma.setCurrentPageAsync(r);let n=[];return!function(e){let t=[{node:e,visited:!1}],r=new Map;for(;t.length>0;){let e=t[t.length-1],i=e.node,a="children"in i?i.children:[];if(!e.visited){e.visited=!0;for(let e=a.length-1;e>=0;e-=1)t.push({node:a[e],visited:!1});continue}t.pop();let o=0,l=[];for(let e of a)l.push(e.id),o+=1+(r.get(e.id)||0);r.set(i.id,o),n.push({id:i.id,type:i.type,name:i.name,childrenIds:l,descendantCount:o})}}(t),{fileKey:figma.fileKey||"",version:null,rootId:t.id,nodes:n}},pageCatalog:async function(e){let t=figma.root.children;return{fileKey:figma.fileKey||"",version:null,rootIds:t.map(e=>e.id),nodes:t.map(e=>({id:e.id,type:e.type,fields:{name:e.name,parentId:null,childrenIds:[]},extra:{},fieldErrors:{}})),diagnostics:[]}},referencePng:async function(e){let t,r=e.nodeId;globalThis.__devupReferencePngAnswers||(globalThis.__devupReferencePngAnswers=new Map);let n=globalThis.__devupReferencePngAnswers,i=0;for(let[e,t]of n)Date.now()-t.at>12e4?n.delete(e):i+=t.text.length;if(i>=0xc000000)throw Error(`DEVUP_REFERENCE_PNG_BUSY ${i} characters are held for other reads`);let a=await figma.getNodeByIdAsync(r);if(!a||"function"!=typeof a.exportAsync)throw Error(`DEVUP_NODE_NOT_FOUND ${r}`);let o=a.absoluteRenderBounds||a.absoluteBoundingBox,l=Math.max(o.width,o.height),s=l>8192?8192/l:1,d=(e,t)=>({format:"PNG",constraint:t&&s<1?{type:o.width>=o.height?"WIDTH":"HEIGHT",value:8192}:{type:"SCALE",value:s},contentsOnly:e,useAbsoluteBounds:!1,colorProfile:"SRGB"}),f=e=>(Array.isArray(e)?e:[]).filter(e=>e&&!1!==e.visible),c=(e,t)=>!!e&&!!t&&e.x!!e&&e.x+t<=o.x&&e.y+t<=o.y&&e.x+e.width-t>=o.x+o.width&&e.y+e.height-t>=o.y+o.height,p=e=>`${e.type} ${JSON.stringify(e.name)}`,g=e=>JSON.parse(JSON.stringify(e===figma.mixed?null:e)),h=e=>(void 0===e.opacity||1===e.opacity)&&(!e.blendMode||["PASS_THROUGH","NORMAL"].includes(e.blendMode)),y=[],m=a.parent;for(;m&&"PAGE"!==m.type;)y.unshift(m),m=m.parent;let b=[255,255,255];for(let e of f(m&&m.backgrounds)){if("SOLID"!==e.type)continue;let t=void 0===e.opacity?1:e.opacity;["r","g","b"].forEach((r,n)=>{b[n]=t*e.color[r]*255+(1-t)*b[n]})}let x=0,E=e=>!1!==e.visible&&!e.isMask&&!!c(e.absoluteRenderBounds,o)&&((x+=1)>5e3||"TEXT"===e.type||!!f(e.fills).length||!!f(e.strokes).length||!!f(e.effects).length||"children"in e&&e.children.some(E)),A=e=>h(e)&&u(e.absoluteBoundingBox,0)&&1e-9>Math.abs(e.absoluteTransform[0][1])&&1e-9>Math.abs(e.absoluteTransform[1][0])&&0===Math.max(0,...[e.topLeftRadius,e.topRightRadius,e.bottomRightRadius,e.bottomLeftRadius].filter(e=>"number"==typeof e))&&f(e.fills).some(e=>"NORMAL"===(e.blendMode||"NORMAL")&&(void 0===e.opacity||1===e.opacity)&&("SOLID"===e.type||e.type.startsWith("GRADIENT_")&&e.gradientStops.every(e=>1===e.color.a))),I=[...y,a],S=[],w=[],v=new Map;for(let e=0;eA(e)&&i.slice(0,t).every(h));r.forEach((e,r)=>{e===t||(e.isMask&&!1!==e.visible&&r{let t=await a.exportAsync(d(!1,!0));return{kind:"devupReferencePng",nodeId:r,mimeType:"image/png",byteLength:t.length,data:figma.base64Encode(t),backdrop:N,...e}},T=(e,t)=>{let r=[...new Set(e)];return{approximate:r.join("; "),note:`Drawn as the canvas shows this area: devup-mcp cannot yet compose what get_screenshot draws here (${r.slice(0,3).join("; ")}). Layers reaching into it may show here: ${t}; get_screenshot leaves other layers out.`}},_=e=>{let t=JSON.stringify(e).replace(/[\u0080-\uffff]/g,e=>`\\u${e.charCodeAt(0).toString(16).padStart(4,"0")}`);if(t.length>0x5a00000){if(e.compose){let{compose:r,reaching:n,...i}=e;return _({...i,...T([`the answer would be ${t.length} characters`],n)})}throw Error(`DEVUP_REFERENCE_PNG_TOO_LARGE ${t.length} characters, over the 94371840 one answer is read back up to`)}if(t.length<=6291456)return e;let i=`${r}@${Date.now()}.${Math.random().toString(36).slice(2,10)}`;return n.set(i,{text:t,at:Date.now()}),{kind:"devupReferencePngChunked",nodeId:r,id:i,length:t.length}};if(0===S.length)return _(await O({}));let P=I.some(e=>e.isMask),M=[],C=new Set,k=(e,t)=>{let r=p(e);for(let t of f(e.fills))"IMAGE"===t.type?((t.rotation||f([t.filters]).some(e=>Object.values(e).some(e=>e)))&&M.push(`${r} has an image fill with a rotation or filters`),C.add(t.imageHash)):"SOLID"===t.type||t.type.startsWith("GRADIENT_")||M.push(`${r} is painted with ${t.type}`);for(let t of f(e.effects))["LAYER_BLUR","DROP_SHADOW","INNER_SHADOW","BACKGROUND_BLUR"].includes(t.type)||M.push(`${r} has a ${t.type}`);let n=f(e.strokes);if((e=>{let t=0===f(e.strokes).length?[]:[e.strokeWeight,e.strokeTopWeight,e.strokeRightWeight,e.strokeBottomWeight,e.strokeLeftWeight].filter(e=>"number"==typeof e&&e>0);if(0===t.length)return!1;let r=Math.max(...t),n="OUTSIDE"===e.strokeAlign?0:"CENTER"===e.strokeAlign?r/2:r,i="INSIDE"===e.strokeAlign?0:"CENTER"===e.strokeAlign?r/2:r,a=e.absoluteBoundingBox;return c(a&&{x:a.x-i,y:a.y-i,width:a.width+2*i,height:a.height+2*i},o)&&!u(a,n)})(e)){let t=["FRAME","RECTANGLE","COMPONENT","COMPONENT_SET","INSTANCE","SECTION"].includes(e.type),i=n.every(e=>"SOLID"===e.type||e.type.startsWith("GRADIENT_")),a=Array.isArray(e.dashPattern)&&e.dashPattern.length>0,o="INSIDE"===e.strokeAlign||!e.strokeJoin||"MITER"===e.strokeJoin;t&&i&&!a&&o||M.push(`${r}'s stroke reaches it`)}return{transform:g(e.absoluteTransform),width:e.width,height:e.height,radii:[e.topLeftRadius,e.topRightRadius,e.bottomRightRadius,e.bottomLeftRadius].map(e=>"number"==typeof e?e:0),fills:g(f(e.fills)),opacity:void 0===e.opacity?1:e.opacity,blendMode:e.blendMode||null,clips:!!e.clipsContent,effects:g(f(e.effects)),strokes:g(n),strokeAlign:e.strokeAlign||null,strokeWeights:["strokeTopWeight","strokeRightWeight","strokeBottomWeight","strokeLeftWeight"].map(t=>"number"==typeof e[t]?e[t]:"number"==typeof e.strokeWeight?e.strokeWeight:0)}},R=async e=>{let t=await e.exportAsync(d(!0,e===a));return{data:figma.base64Encode(t),area:e.absoluteRenderBounds||e.absoluteBoundingBox,blendMode:e.blendMode||null,size:[16,20].map(e=>(t[e]<<24|t[e+1]<<16|t[e+2]<<8|t[e+3])>>>0)}},D=async e=>{let t=v.get(e);if(!t)return null;let r=await t.exportAsync({...d(!0,!1),useAbsoluteBounds:!0});return{data:figma.base64Encode(r),area:t.absoluteBoundingBox,luminance:"LUMINANCE"===t.maskType}},L=[],$={},U={};try{for(let e of y){let t=k(e,!1),r=await D(e);r&&(t.mask=r),L.push(t)}let e=e=>f(e.effects).some(e=>"BACKGROUND_BLUR"===e.type);if(w.length>0&&(e(a)||"findOne"in a&&a.findOne(e))){let r="children"in a&&a.children.some(t=>e(t)||"findOne"in t&&!!t.findOne(e));if("children"in a&&!r&&!f(a.strokes).length){for(let e of($.own=k(a,!0),$.children=[],a.children)){if(!1===e.visible||e.isMask||!e.absoluteRenderBounds)continue;let t=await R(e);$.children.push({data:t.data,area:t.area,blendMode:t.blendMode})}t=[Math.ceil(o.width*s-1e-6),Math.ceil(o.height*s-1e-6)]}else M.push(`${p(a)} blurs what lies behind it`)}else if(P)t=[Math.ceil(o.width*s-1e-6),Math.ceil(o.height*s-1e-6)];else{let e=await R(a);$.picture={data:e.data,area:e.area,blendMode:e.blendMode},t=e.size}let r=await D(a);r&&($.mask=r),t&&t[0]*t[1]>8e6&&M.push(`the area is ${t[0]}x${t[1]} pixels`);let n=globalThis.__devupNormalizeImage,i=Date.now()+2e4,l=0;for(let e of C){let t=e&&figma.getImageByHash(e),r=t&&await t.getBytesAsync();if(!r){M.push(`the image ${e} could not be read`);continue}let a=null;if("function"==typeof n){let{width:e,height:o}=await t.getSizeAsync(),l=(...e)=>e.every((e,t)=>r[t]===e),s=l(137,80,78,71)||l(255,216)||l(71,73,70),d=Math.min(1,8192/Math.max(e,o,1)),f=Math.ceil(e*d)*Math.ceil(o*d),c=5*f+65536,u=5*r.length+4*e*o*(d<1?2:1)+4*f+4*c+1048576,p=i-Date.now();if((!s||Math.max(e,o)>8192||r.length>4*e*o+1048576)&&u<=0x20000000&&p>0){let t=await n(r,8192,p,e,o);t&&(r=t,a={width:e,height:o})}}if(l+r.length>0x4000000){M.push("its ancestors' images are over 67108864 bytes");break}l+=r.length,U[e]=a?{data:figma.base64Encode(r),...a}:figma.base64Encode(r)}}catch(e){M.push(`a part could not be exported: ${e instanceof Error?e.message:String(e)}`)}let B=`${S.slice(0,5).join(", ")}${S.length>5?` and ${S.length-5} more`:""}`;return M.length>0?_(await O(T(M,B))):_(await O({reaching:B,compose:{area:o,scale:s,width:t[0],height:t[1],page:b,layers:L,node:$,images:U}}))},referencePngChunk:async function(e){let t=e.largeValue,r=globalThis.__devupReferencePngAnswers,n=r&&r.get(t.id);if(!n||"string"!=typeof n.text)throw Error(`DEVUP_REFERENCE_PNG_ANSWER_GONE ${t.id}`);let i=n.text,a=Math.max(0,Math.floor(Number(t.offset)||0)),o=Math.min(i.length,a+4194304);return o>=i.length&&r.delete(t.id),{kind:"devupReferencePngChunk",id:t.id,offset:a,nextOffset:o,complete:o>=i.length,text:i.slice(a,o)}},search:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=await figma.getNodeByIdAsync(e.nodeId);if(!t)throw Error("DEVUP_NODE_NOT_FOUND");let r=t;for(;r&&"PAGE"!==r.type;)r=r.parent;if(!r||"PAGE"!==r.type)throw Error("DEVUP_PAGE_NOT_FOUND");await figma.setCurrentPageAsync(r);let n=e.search,i=new Set((n.nodeTypes.length?n.nodeTypes:["PAGE","SECTION","FRAME","COMPONENT_SET","COMPONENT"]).map(e=>e.toUpperCase()));function a(e){return e.normalize("NFC").toLocaleLowerCase().replace(/\s/gu,"")}let o=t===r?r.findAll(()=>!0):"findAll"in t?t.findAll(()=>!0):[],l=[t,...o].filter(e=>i.has(e.type)&&"string"==typeof e.name).map(e=>({node:e,score:function(e){let t=n.query.trim();if(""===t||"*"===t)return 100;if(e===n.query)return 400;if("exact"===n.matchKind)return null;let r=a(e),i=a(n.query);if(r===i)return 300;if(r.startsWith(i))return 200;if(r.includes(i))return 100;if("fuzzy"!==n.matchKind)return null;let o=function(e,t){let r=Array.from({length:t.length+1},(e,t)=>t);for(let n=0;nnull!==e.score).sort((e,t)=>t.score-e.score||e.node.name.localeCompare(t.node.name)||e.node.id.localeCompare(t.node.id)).slice(0,n.limit),s=new Map([[r.id,r]]);for(let e of[t,...l.map(e=>e.node)]){let t=e;for(;t&&"DOCUMENT"!==t.type;)s.set(t.id,t),t=t.parent}return{fileKey:figma.fileKey||"",version:null,rootIds:[r.id],nodes:[...s.values()].map(e=>({id:e.id,type:e.type,fields:{name:e.name,parentId:e.parent&&"DOCUMENT"!==e.parent.type?e.parent.id:null,childrenIds:"children"in e?e.children.map(e=>e.id):[]},extra:{},fieldErrors:{}})),diagnostics:[]}},sectionIndex:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=await figma.getNodeByIdAsync(e.nodeId);if(!t)throw Error("DEVUP_NODE_NOT_FOUND");if("SECTION"!==t.type){let e=t.parent,r=new Set;for(;e&&"SECTION"!==e.type&&!r.has(e.id);)r.add(e.id),e=e.parent;let n=e&&"SECTION"===e.type?e.id:null,i=n&&figma.fileKey?`https://www.figma.com/design/${figma.fileKey}?node-id=${n.replace(/:/g,"-")}`:null;throw Error("DEVUP_SECTION_REQUIRED "+JSON.stringify({pluginCode:"DEVUP_SECTION_REQUIRED",stage:"section-index",nodeId:t.id,nodeType:t.type,sectionId:n,nextAction:{tool:"devup_figma_export",how:n?`The url must point to a SECTION. This capture's SECTION is ${n}; select frames with frameIds.`:"The url must point to a SECTION. This node has no ancestor SECTION; choose the intended SECTION in Figma and copy its link.",arguments:i?"FRAME"===t.type&&t.parent===e?{url:i,frameIds:[t.id]}:{url:i}:null,requiredArguments:i?[]:["url"]}}))}let r=2048;function n(e){let t=e.absoluteBoundingBox||{x:"number"==typeof e.x?e.x:0,y:"number"==typeof e.y?e.y:0,width:"number"==typeof e.width?e.width:0,height:"number"==typeof e.height?e.height:0};return[t.x,t.y,t.width,t.height].every(Number.isFinite)?{x:t.x,y:t.y,width:t.width,height:t.height}:null}function i(e,t){if("FRAME"!==e.type||!1===e.visible||!t)return!1;let r=t.width/Math.max(1,t.height);return t.width>=240&&t.width<=1800&&t.height>=300&&t.height<=2e3&&r>=.25&&r<=2.5}function a(e){let t=0;for(let r=0;r=55296&&n<=56319&&r+1[e.id,t.id])),s=[],d=0;for(let e=0;ee.id));for(let r of t.children){if(e.has(r.id))continue;let t=n(r);t&&!1!==r.visible&&s.push({node:r,box:t})}}s.sort((e,t)=>e.box.y-t.box.y||e.box.x-t.box.x||e.node.id.localeCompare(t.node.id));let f=o.length>d||s.length>100,c=s.slice(0,100),u=new Set(c.map(({node:e})=>e.id)),p=new Set(e.rootIds),g={},h=new Map([[t.id,null]]);for(let e=0;e{let r=e.parent;for(;r&&r.id!==t.id&&!u.has(r.id);)r=r.parent;return[e.id,r&&u.has(r.id)?r.id:t.id]})),m=e=>c.filter(({node:t})=>y.get(t.id)===e).map(({node:e})=>e.id),b=n(t);if(!b)throw Error("DEVUP_NODE_BOUNDS_UNAVAILABLE");let x={id:t.id,type:t.type,fields:{name:t.name,parentId:t.parent&&"DOCUMENT"!==t.parent.type?t.parent.id:null,childrenIds:m(t.id),absoluteBoundingBox:b,visible:!1!==t.visible,projectionTruncated:f,nodeScreenIds:g},extra:{},fieldErrors:{}},E=c.length,A=c.map(({node:e,box:t})=>{let o,l=function(e){let t=[e],r=0,n=0;for(let e=0;er}}(e),s=Math.floor(r/E);E-=1;let d=function(e,t){let r=[e],n="",i=0,o=0,l=!1;if(t<=0)return{text:"",state:"budget-exhausted",spent:0};for(let e=0;e=120||o+e>t)return{text:n.trimEnd(),state:o+e>t?"budget-exhausted":"truncated",spent:o};n+=r,i+=1,o+=e}}if("children"in s){let e=Math.max(0,64-r.length);s.children.length>e&&(l=!0),r.push(...s.children.slice(0,e))}}}return{text:n,state:l?"truncated":n?"available":"no-text",spent:o}}(e,s);return r-=d.spent,{id:e.id,type:e.type,fields:{name:"string"==typeof e.name?e.name:"",parentId:y.get(e.id),childrenIds:m(e.id),absoluteBoundingBox:t,visible:!1!==e.visible,breadcrumb:function(e){let t=[],r=e;for(;r&&"DOCUMENT"!==r.type;)"string"==typeof r.name&&r.name&&t.push(r.name),r=r.parent;return t.reverse()}(e),directChildCount:"children"in e?e.children.length:0,textPreview:d.text,...null===(o=function(e){let t=[e],r=0,i=null;for(;t.length&&r++<400;){let e=t.shift();if("visible"in e&&!1===e.visible)continue;let r="children"in e?e.children.filter(e=>!("visible"in e)||!1!==e.visible):[];if(/header|nav|gnb/i.test(e.name||"")&&r.length>=2&&"layoutMode"in e&&"HORIZONTAL"===e.layoutMode){let t=r.map(n);t.every(Boolean)&&(i=Math.max(i||0,t.reduce((e,t)=>e+t.width,0)+(Number(e.itemSpacing)||0)*(t.length-1)))}t.push(...r.slice(0,80))}return i}(e))?{}:{navigationContentWidth:o},textPreviewState:d.state,subtreeNodeCount:l.subtreeNodeCount,estimatedSerializedBytes:l.estimatedSerializedBytes,selectionReasons:[i(e,t)?"screen-like":"explicit-selection-only","inside-section"],estimateTruncated:l.truncated},extra:{},fieldErrors:{}}}),I={fileKey:figma.fileKey||"",version:null,rootIds:[t.id],nodes:[x,...A],diagnostics:[]},S=a(JSON.stringify(I));for(let e=A.length-1;e>=0&&S>19456;e-=1)A[e].fields.textPreview="",A[e].fields.textPreviewState="budget-exhausted",S=a(JSON.stringify(I));let w=()=>I.nodes.length-1,v=A.length;for(;S>19456&&w()>1;){let e=I.nodes.pop();x.fields.projectionTruncated=!0,x.fields.childrenIds=x.fields.childrenIds.filter(t=>t!==e.id),S=a(JSON.stringify(I))}if(S>19456)throw Error("DEVUP_SECTION_INDEX_TOO_LARGE "+JSON.stringify({pluginCode:"DEVUP_SECTION_INDEX_TOO_LARGE",stage:"section-index",responseBytes:S,maxResponseBytes:19456,discoveredCandidates:v,retainedCandidates:w()}));return I},snapshot:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=await figma.getNodeByIdAsync(e.nodeId);if(!t)throw Error("DEVUP_NODE_NOT_FOUND");let r=["absoluteBoundingBox","absoluteRenderBounds","arcData","backgroundStyleId","blendMode","bottomLeftRadius","bottomRightRadius","boundVariables","characters","clipsContent","componentProperties","componentPropertyDefinitions","componentPropertyReferences","constraints","cornerRadius","counterAxisAlignItems","dashPattern","defaultVariant","effectStyleId","effects","fillStyleId","fills","fontName","fontSize","gridColumnAnchorIndex","gridColumnCount","gridColumnGap","gridColumnSizes","gridColumnSpan","gridRowAnchorIndex","gridRowCount","gridRowGap","gridRowSizes","gridRowSpan","gridStyleId","height","inferredAutoLayout","isAsset","isMask","itemSpacing","layoutGrow","layoutMode","layoutWrap","layoutPositioning","layoutSizingHorizontal","layoutSizingVertical","letterSpacing","lineHeight","maxHeight","maxLines","maxWidth","minHeight","minWidth","name","opacity","overflowDirection","paddingBottom","paddingLeft","paddingRight","paddingTop","primaryAxisAlignItems","reactions","rotation","strokeAlign","strokeBottomWeight","strokeLeftWeight","strokeRightWeight","strokeStyleId","strokeTopWeight","strokeWeight","strokes","targetAspectRatio","textAlignHorizontal","textAlignVertical","textAutoResize","textCase","textDecoration","textStyleId","textTruncation","topLeftRadius","topRightRadius","variantProperties","visible","width","x","y"],n=new Set(r),i=["fontName","fontWeight","fontSize","textDecoration","textCase","lineHeight","letterSpacing","fills","textStyleId","fillStyleId","listOptions","indentation","hyperlink"],a=e.snapshot,o=Math.max(0,Math.floor(Number(a.offset)||0)),l=Math.min(a.maxEnvelopeBytes?Math.max(8192,Math.floor(Number(a.maxEnvelopeBytes))):16e3,Math.max(4096,Math.floor(Number(a.maxPayloadBytes)||15e3))),s=Math.min(l-1024,Math.max(512,Math.floor(Number(a.maxFieldBytes)||4096))),d=new Set(["id","type","parent","children","mainComponent","masterComponent","instances"]),f=new Set(["parentId","childrenIds","name","characters","styledTextSegments","boundVariables"]);function c(e,t,r){let n=function(e){let t=[];for(let r=0;r=55296&&n<=56319){let t=r+1=56320&&t<=57343?(n=65536+(n-55296<<10)+(t-56320),r+=1):n=65533}else n>=56320&&n<=57343&&(n=65533);n<128?t.push(n):n<2048?t.push(192|n>>6,128|63&n):n<65536?t.push(224|n>>12,128|n>>6&63,128|63&n):t.push(240|n>>18,128|n>>12&63,128|n>>6&63,128|63&n)}return new Uint8Array(t)}(JSON.stringify(r));return n.length>0x1000000?{$truncated:"max-large-value-bytes",byteLength:n.length}:{$largeValue:{nodeId:e,field:t,byteLength:n.length,sha256:function(e){let t=[0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,0xe49b69c1,0xefbe4786,0xfc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x6ca6351,0x14292967,0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2],r=64*Math.ceil((e.length+9)/64),n=new Uint8Array(r);n.set(e),n[e.length]=128;let i=8*e.length;for(let e=0;e<8;e+=1)n[r-1-e]=255&Math.floor(i/2**(8*e));let a=[0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19],o=(e,t)=>e>>>t|e<<32-t;for(let e=0;e>>3,n=o(r[e-2],17)^o(r[e-2],19)^r[e-2]>>>10;r[e]=r[e-16]+t+r[e-7]+n>>>0}let[i,l,s,d,f,c,u,p]=a;for(let e=0;e<64;e+=1){let n=p+(o(f,6)^o(f,11)^o(f,25))+(f&c^~f&u)+t[e]+r[e]>>>0,a=(o(i,2)^o(i,13)^o(i,22))+(i&l^i&s^l&s)>>>0;p=u,u=c,c=f,f=d+n>>>0,d=s,s=l,l=i,i=n+a>>>0}for(let[e,t]of[i,l,s,d,f,c,u,p].entries())a[e]=a[e]+t>>>0}return a.map(e=>e.toString(16).padStart(8,"0")).join("")}(n),cursor:{nextOffset:0,maxChunkBytes:12288}}}}function u(e){return function(e){let t=0;for(let r=0;r=55296&&n<=56319&&r+112)return{$truncated:"max-depth"};if("object"==typeof t&&"parent"in t&&"string"==typeof t.id&&"string"==typeof t.type)return{$nodeId:t.id,$nodeType:t.type};if(Array.isArray(t))return t.map(t=>e(t,r,n+1));if(ArrayBuffer.isView(t))return{$binary:t.constructor.name,byteLength:t.byteLength};if(t instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:t.byteLength};if(r.has(t))return{$circular:!0};r.add(t);let i={};for(let a of Object.keys(t).sort())try{let o=e(t[a],r,n+1);o&&"function"===o.$unsupported||(i[a]=o)}catch(e){i[a]={$error:String(e&&e.message?e.message:e)}}return r.delete(t),i}(r),a=u(i);if(a<=s)return i;let o=c(e,t,i);return o.$truncated&&(n[t]=`DEVUP_FIELD_VALUE_UNSUPPORTED:${a}>16777216`),o}let g=new Map;async function h(e,t,r){if("INSTANCE"!==e.type||"function"!=typeof e.getMainComponentAsync)return;g.has(e.id)||g.set(e.id,(async()=>{try{let t=await e.getMainComponentAsync();return{fields:{mainComponentId:t?t.id:null,mainComponentName:t?t.name:null,mainComponentSetId:t&&t.parent&&"COMPONENT_SET"===t.parent.type?t.parent.id:null}}}catch(e){return{error:String(e&&e.message?e.message:e)}}})());let n=await g.get(e.id);n.error?r.mainComponentId=n.error:Object.assign(t,n.fields)}async function y(e){let t={},a={},o={};for(let i of(t.parentId=e.parent?e.parent.id:null,e.parent&&("PAGE"===e.parent.type||"SECTION"===e.parent.type||"COMPONENT_SET"===e.parent.type)&&(t.parentType=e.parent.type,"SECTION"===e.parent.type&&(t.parentName=e.parent.name)),t.childrenIds="children"in e?e.children.map(e=>e.id):[],!("overflowDirection"in e)&&["FRAME","COMPONENT","INSTANCE","COMPONENT_SET"].includes(e.type)&&(t.overflowDirection=null),function(e){let t=new Set,n=e;for(;n&&n!==Object.prototype;){for(let e of Object.getOwnPropertyNames(n))t.add(e);n=Object.getPrototypeOf(n)}for(let n of r)try{n in e&&t.add(n)}catch(e){}return[...t].sort()}(e)))if(!(d.has(i)||i.startsWith("_")||"componentPropertyDefinitions"===i&&"COMPONENT"===e.type&&e.parent&&"COMPONENT_SET"===e.parent.type))try{let r=e[i];if("function"==typeof r)continue;let l=p(e.id,i,r,o);(n.has(i)?t:a)[i]=l}catch(e){o[i]=String(e&&e.message?e.message:e)}if("TEXT"===e.type&&"function"==typeof e.getStyledTextSegments)try{t.styledTextSegments=p(e.id,"styledTextSegments",e.getStyledTextSegments(i),o)}catch(e){o.styledTextSegments=String(e&&e.message?e.message:e)}return await h(e,t,o),{id:e.id,type:e.type,fields:t,extra:a,fieldErrors:o}}let m=[],b=[t];for(;b.length;){let e=b.shift();m.push(e),"children"in e&&b.push(...e.children)}let x=[],E=l-1024,A=2;for(let e=o;eNumber(e.protected)-Number(t.protected)||t.byteLength-e.byteLength),n)){if(r<=t)break;let n=c(e.id,i.name,e[i.sectionName][i.name]);e[i.sectionName][i.name]=n,n.$truncated&&(e.fieldErrors[i.name]=`DEVUP_FIELD_VALUE_UNSUPPORTED:${i.byteLength}>16777216`),r=u(e)}return e}(await y(m[e]),E),r=u(t)+ +!!x.length;if(x.length&&A+r>E)break;x.push(t),A+=r}let I=Math.min(m.length,o+x.length);return x.push({id:"__DEVUP_SNAPSHOT_CURSOR__",type:"DEVUP_INTERNAL",fields:{offset:o,nextOffset:I,complete:I>=m.length,totalNodes:m.length},extra:{},fieldErrors:{}}),{fileKey:figma.fileKey||"",version:null,rootIds:[t.id],nodes:x,diagnostics:[]}},usedResources:async function(e){let t=e.resources;function r(e,t=new WeakSet,n=0){if(null===e||["string","number","boolean"].includes(typeof e))return e;if(void 0===e)return{$undefined:!0};if("bigint"==typeof e)return{$bigint:e.toString()};if(["function","symbol"].includes(typeof e))return{$unsupported:typeof e};if(n>12)return{$truncated:"max-depth"};if("object"==typeof e&&"parent"in e&&"string"==typeof e.id&&"string"==typeof e.type)return{$nodeId:e.id,$nodeType:e.type};if(Array.isArray(e))return e.map(e=>r(e,t,n+1));if(ArrayBuffer.isView(e))return{$binary:e.constructor.name,byteLength:e.byteLength};if(e instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:e.byteLength};if(t.has(e))return{$circular:!0};t.add(e);let i={},a=new Set(Object.keys(e)),o=e;for(;o&&o!==Object.prototype;){for(let e of Object.getOwnPropertyNames(o))a.add(e);o=Object.getPrototypeOf(o)}for(let o of[...a].sort())if(!(o.startsWith("_")||["parent","children","consumers"].includes(o)))try{let a=r(e[o],t,n+1);a&&"function"===a.$unsupported||(i[o]=a)}catch(e){i[o]={$error:"unavailable"}}return t.delete(e),i}let n=new Map,i="ok";try{for(let e of(await figma.variables.getLocalVariablesAsync()))n.set(e.id,e)}catch(e){i="unavailable"}function a(e){return Object.values(e.valuesByMode||{}).filter(e=>e&&"VARIABLE_ALIAS"===e.type&&"string"==typeof e.id&&e.id).map(e=>e.id)}async function o(e){let t=n.get(e);if(t)return{value:r(t),collectionId:t.variableCollectionId,aliasIds:a(t)};try{let t=await figma.variables.getVariableByIdAsync(e);return t?{value:r(t),collectionId:t.variableCollectionId,aliasIds:a(t)}:{unresolved:{id:e,kind:"variable",reason:"notInFileAndLookupEmpty"}}}catch(t){return{unresolved:{id:e,kind:"variable",reason:"notInFileAndLookupThrew",error:String(t&&t.message||t).slice(0,300)}}}}let l=await Promise.all(t.variableIds.map(o)),s=new Set(t.variableIds),d=l.flatMap(e=>e.aliasIds||[]);for(;d.length>0;){let e=[...new Set(d)].filter(e=>!s.has(e)).sort();for(let t of e)s.add(t);let t=await Promise.all(e.map(o));l.push(...t),d=t.flatMap(e=>e.aliasIds||[])}let f=[...new Set(l.flatMap(e=>e.collectionId?[e.collectionId]:[]))].sort(),c=await Promise.all(f.map(async e=>{try{let t=await figma.variables.getVariableCollectionByIdAsync(e);return t?[r(t)]:[]}catch(e){return[]}})),u=await Promise.all(t.styles.map(async e=>{try{let t=await figma.getStyleByIdAsync(e.id);if(!t)return{unresolved:{id:e.id,kind:"style",reason:"notFoundOrUnavailable"}};return{value:{...r(t),styleType:e.styleType,value:r("PAINT"===e.styleType?t.paints:"EFFECT"===e.styleType?t.effects:"GRID"===e.styleType?t.layoutGrids:t)}}}catch(t){return{unresolved:{id:e.id,kind:"style",reason:"notFoundOrUnavailable",error:String(t&&t.message||t).slice(0,300)}}}}));return{collections:c.flat(),variables:l.flatMap(e=>e.value?[e.value]:[]),styles:u.flatMap(e=>e.value?[e.value]:[]),usedVariableIds:t.variableIds,usedStyleIds:t.styles.map(e=>e.id),localVariableListing:i,localVariableCount:n.size,unresolved:[...l,...u].flatMap(e=>e.unresolved?[e.unresolved]:[])}},variableCatalog:async function(e){let[t,r,n,i,a]=await Promise.all([figma.variables.getLocalVariableCollectionsAsync(),figma.getLocalPaintStylesAsync(),figma.getLocalTextStylesAsync(),figma.getLocalEffectStylesAsync(),figma.getLocalGridStylesAsync()]),o=["PAINT","TEXT","EFFECT","GRID"];return{collections:t.map(e=>(function e(t,r=new WeakSet,n=0){if(null===t||["string","number","boolean"].includes(typeof t))return t;if(void 0===t)return{$undefined:!0};if("bigint"==typeof t)return{$bigint:t.toString()};if(["function","symbol"].includes(typeof t))return{$unsupported:typeof t};if(n>12)return{$truncated:"max-depth"};if(Array.isArray(t))return t.map(t=>e(t,r,n+1));if(r.has(t))return{$circular:!0};r.add(t);let i={},a=new Set(Object.keys(t)),o=t;for(;o&&o!==Object.prototype;){for(let e of Object.getOwnPropertyNames(o))a.add(e);o=Object.getPrototypeOf(o)}for(let o of[...a].sort())if(!o.startsWith("_"))try{let a=e(t[o],r,n+1);a&&"function"===a.$unsupported||(i[o]=a)}catch(e){i[o]={$error:String(e&&e.message?e.message:e)}}return r.delete(t),i})(e)),variableIds:[...new Set(t.flatMap(e=>e.variableIds))].sort(),styles:[r,n,i,a].flatMap((e,t)=>e.map(e=>({id:e.id,styleType:o[t]}))).sort((e,t)=>e.id.localeCompare(t.id)),localComplete:!0,usedRemoteComplete:!1}},variables:async function(e){let t=e.resources;function r(e,t=new WeakSet,n=0){if(null===e||["string","number","boolean"].includes(typeof e))return e;if(void 0===e)return{$undefined:!0};if("bigint"==typeof e)return{$bigint:e.toString()};if(["function","symbol"].includes(typeof e))return{$unsupported:typeof e};if(n>12)return{$truncated:"max-depth"};if("object"==typeof e&&"parent"in e&&"string"==typeof e.id&&"string"==typeof e.type)return{$nodeId:e.id,$nodeType:e.type};if(Array.isArray(e))return e.map(e=>r(e,t,n+1));if(ArrayBuffer.isView(e))return{$binary:e.constructor.name,byteLength:e.byteLength};if(e instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:e.byteLength};if(t.has(e))return{$circular:!0};t.add(e);let i={},a=new Set(Object.keys(e)),o=e;for(;o&&o!==Object.prototype;){for(let e of Object.getOwnPropertyNames(o))a.add(e);o=Object.getPrototypeOf(o)}for(let o of[...a].sort())if(!(o.startsWith("_")||["parent","children","consumers"].includes(o)))try{let a=r(e[o],t,n+1);a&&"function"===a.$unsupported||(i[o]=a)}catch(e){i[o]={$error:String(e&&e.message?e.message:e)}}return t.delete(e),i}let[n,i]=await Promise.all([Promise.all(t.variableIds.map(e=>figma.variables.getVariableByIdAsync(e))),Promise.all(t.styles.map(e=>figma.getStyleByIdAsync(e.id)))]),a=new Map(t.styles.map(e=>[e.id,e])),o=await Promise.all(i.filter(Boolean).map(async e=>{let t=a.get(e.id),n=t.styleType;if(Number.isInteger(t.consumerStart)&&Number.isInteger(t.consumerEnd)){let i=await e.getStyleConsumersAsync();return{id:e.id,styleType:n,$consumerStart:t.consumerStart,$consumerEntries:i.slice(t.consumerStart,t.consumerEnd).map(e=>[e.node.id,e.node.type,r(e.fields)])}}let i=await e.getStyleConsumersAsync();return{...r(e),styleType:n,$consumerCount:i.length,value:r("PAINT"===n?e.paints:"EFFECT"===n?e.effects:"GRID"===n?e.layoutGrids:e)}}));return{variables:n.filter(Boolean).map(e=>r(e)),styles:o}}},t=new Map,r=0;globalThis.__devupNormalizeImage=(e,n,i,a,o)=>new Promise(l=>{r+=1;let s=`decode-${r}`,d=setTimeout(()=>{t.delete(s),l(null)},i);t.set(s,e=>{clearTimeout(d),t.delete(s),l(e)}),figma.ui.postMessage({kind:"devup-decode",id:s,bytes:e,maxEdge:n,width:a,height:o})});let n=globalThis;function i(){let e=figma.currentPage,t=e.selection;return t.length>0&&(n.__devupLastSelection=t.slice(0,20).map(e=>({id:e.id,name:e.name.slice(0,256),type:e.type})),n.__devupLastSelectionPage={id:e.id,name:e.name.slice(0,256)}),{zoom:Number.isFinite(figma.viewport?.zoom)?figma.viewport.zoom:null,observedAt:Date.now(),lastKnownSelection:n.__devupLastSelection??null,lastKnownPage:n.__devupLastSelectionPage??null,currentPage:{id:e.id,name:e.name.slice(0,256)},selection:t.slice(0,20).map(e=>({id:e.id,name:e.name.slice(0,256),type:e.type})),selectionCount:t.length}}function a(){let e={kind:"devup-context",...i()};figma.ui.postMessage(e)}async function o(t){let r=e[t.script];if(!r)return{kind:"devup-result",requestId:t.requestId,error:`DEVUP_BRIDGE_UNKNOWN_SCRIPT: ${t.script} (아는 스크립트: ${Object.keys(e).join(", ")})`};try{var n;"bridgeContext"===t.script&&i();let e=await r({nodeId:(n=t.params).nodeId??"",rootIds:n.rootIds??[],snapshot:n.snapshot??{},search:n.search??{},explore:n.explore??{},resources:n.resources??{variableIds:[],styles:[]},largeValue:n.largeValue??{},asset:n.asset??{},theme:n.theme??{offset:0}});return{kind:"devup-result",requestId:t.requestId,data:e}}catch(e){return{kind:"devup-result",requestId:t.requestId,error:e instanceof Error?e.message:String(e)}}}let l=[],s=!1,d=null,f=new Set;async function c(){if(!s){s=!0;try{for(;l.length>0;){let e=l.shift();d=e.requestId;let t=await o(e).catch(t=>({kind:"devup-result",requestId:e.requestId,error:t instanceof Error?t.message:String(t)}));d=null,f.delete(e.requestId)||figma.ui.postMessage(t)}}finally{s=!1}}}let u=null;function p(){u??(u=(async()=>{try{await figma.loadAllPagesAsync(),await Promise.all([figma.variables.getLocalVariableCollectionsAsync(),figma.variables.getLocalVariablesAsync(),figma.getLocalPaintStylesAsync(),figma.getLocalTextStylesAsync(),figma.getLocalEffectStylesAsync(),figma.getLocalGridStylesAsync()])}catch{}finally{u=null}})())}figma.showUI(__html__,{width:320,height:220}),p(),figma.on("selectionchange",a),figma.on("currentpagechange",a),figma.ui.onmessage=e=>{if("object"==typeof e&&null!==e){if("devup-ready"===e.kind){let e={kind:"devup-status",fileKey:figma.fileKey??null,fileName:figma.root.name,port:1993,...i()};figma.ui.postMessage(e);return}if("devup-job"===e.kind){l.push(e),c();return}if("devup-cancel"===e.kind){var r;let t;return void(r=e.requestId,(t=l.findIndex(e=>e.requestId===r))>=0?l.splice(t,1):d===r&&f.add(r))}if("devup-decoded"===e.kind)return void t.get(e.id)?.(e.png??null);"devup-in-view"===e.kind&&p()}}})(); \ No newline at end of file +(()=>{"use strict";let e={assets:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=e.asset;function r(e){let t=[0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,0xe49b69c1,0xefbe4786,0xfc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x6ca6351,0x14292967,0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2],r=64*Math.ceil((e.length+9)/64),n=new Uint8Array(r);n.set(e),n[e.length]=128;let i=8*e.length;for(let e=0;e<8;e+=1)n[r-1-e]=255&Math.floor(i/2**(8*e));let a=[0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19],o=(e,t)=>e>>>t|e<<32-t;for(let e=0;e>>3,n=o(r[e-2],17)^o(r[e-2],19)^r[e-2]>>>10;r[e]=r[e-16]+t+r[e-7]+n>>>0}let[i,l,s,d,f,c,u,p]=a;for(let e=0;e<64;e+=1){let n=p+(o(f,6)^o(f,11)^o(f,25))+(f&c^~f&u)+t[e]+r[e]>>>0,a=(o(i,2)^o(i,13)^o(i,22))+(i&l^i&s^l&s)>>>0;p=u,u=c,c=f,f=d+n>>>0,d=s,s=l,l=i,i=n+a>>>0}for(let[e,t]of[i,l,s,d,f,c,u,p].entries())a[e]=a[e]+t>>>0}return a.map(e=>e.toString(16).padStart(8,"0")).join("")}async function n(e){try{let t=await figma.getNodeByIdAsync(e);if(t)return t}catch(r){if(!e.startsWith("I")||!e.includes(";"))throw r;try{return await t()}catch(e){throw Error(`${r.message||r}; nested lookup fallback: ${e.message||e}`)}}if(!e.startsWith("I")||!e.includes(";"))return null;return t();async function t(){let t=await figma.getNodeByIdAsync(e.slice(1).split(";")[0]),r=t?[t]:[],n=new Set;for(;r.length;){let t=r.pop();if(t.id===e)return t;if(!n.has(t.id)){if(n.add(t.id),n.size>1e4)throw Error("nested lookup exceeds 10000 nodes");Array.isArray(t.children)&&r.push(...t.children)}}throw Error(`nested node ${e} not found in its instance`)}}async function i(e,t,r=85e3,n=()=>{}){let a,o=Date.now();try{let i=await Promise.race([e.exportAsync(t),new Promise((e,t)=>{a=setTimeout(()=>t(Error(`DEVUP_ASSET_TIMEOUT: native export exceeded ${r} ms`)),r)})]);return n("native-export",Date.now()-o),i}catch(i){if(n("native-export",Date.now()-o,String(i.message||i).slice(0,500)),"SVG_STRING"!==t.format)throw i;let r=Date.now();try{let t=function(e){let t=e=>"number"==typeof e&&Number.isFinite(e),r=(e,t)=>{throw Error(`geometry fallback unsupported: ${e.type||"unknown"} ${e.id||""}: ${t}`)},n=0,i=0;function a(e,i,a){(void 0!==a&&!Array.isArray(a)||void 0===a&&Array.isArray(i)&&i.length)&&r(e,"unresolved paint requires native export");let o=Array.isArray(a)?a.filter(e=>!1!==e.visible):[];return o.length?(Array.isArray(i)&&i.length||r(e,"paint has no rendered geometry"),o.map(a=>{("SOLID"!==a.type||a.blendMode&&"NORMAL"!==a.blendMode)&&r(e,"paint type or blending requires native export");let o=a.color;o&&[o.r,o.g,o.b].every(e=>t(e)&&e>=0&&e<=1)||r(e,"invalid solid color");let l=void 0===a.opacity?1:a.opacity;return(!t(l)||l<0||l>1)&&r(e,"invalid paint opacity"),["NONZERO","EVENODD"].map(t=>{let a=i.filter(e=>e.windingRule===t);if(!a.length)return"";for(let t of a)"string"==typeof t.data&&t.data.trim()||r(e,"empty path"),n++;return`"']/g,e=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[e])}" fill="rgb(${100*o.r}%,${100*o.g}%,${100*o.b}%)" fill-opacity="${l}" fill-rule="${"EVENODD"===t?"evenodd":"nonzero"}"/>`}).join("")}).join("")):""}let o=e.absoluteRenderBounds,l=e.absoluteTransform;(!o||![o.x,o.y,o.width,o.height].every(t)||o.width<=0||o.height<=0)&&r(e,"missing render bounds"),(!Array.isArray(l)||2!==l.length||!l.every(e=>Array.isArray(e)&&3===e.length&&e.every(t))||0!==l[0][1]||0!==l[1][0]||l[0][0]<=0||l[1][1]<=0)&&r(e,"root rotation, reflection or shear requires native export");let s=(o.x-l[0][2])/l[0][0],d=(o.y-l[1][2])/l[1][1],f=o.width/l[0][0],c=o.height/l[1][1],u=function e(n,o){if(++i>1e4&&r(n,"subtree exceeds 10000 nodes"),n.isMask&&r(n,"mask requires native export"),!1===n.visible||0===n.opacity)return"";n.clipsContent&&r(n,"clipping requires native export"),Array.isArray(n.effects)&&n.effects.some(e=>!1!==e.visible)&&r(n,"effect requires native export"),n.blendMode&&!["NORMAL","PASS_THROUGH"].includes(n.blendMode)&&r(n,"layer blending requires native export");let l=void 0===n.opacity?1:n.opacity;(!t(l)||l<0||l>1)&&r(n,"invalid layer opacity");let s="";if(!o){let e=n.relativeTransform;Array.isArray(e)&&2===e.length&&e.every(e=>Array.isArray(e)&&3===e.length&&e.every(t))||r(n,"missing relative transform"),s=` transform="matrix(${e[0][0]} ${e[1][0]} ${e[0][1]} ${e[1][1]} ${e[0][2]} ${e[1][2]})"`}let d="";if(["GROUP","FRAME","INSTANCE","COMPONENT"].includes(n.type))(Array.isArray(n.fills)&&n.fills.some(e=>!1!==e.visible)||Array.isArray(n.strokes)&&n.strokes.some(e=>!1!==e.visible))&&r(n,"container paint requires native export"),Array.isArray(n.children)||r(n,"missing children"),d=(!0===n.itemReverseZIndex?[...n.children].reverse():n.children).map(t=>e(t,!1)).join("");else{for(let e of(["VECTOR","BOOLEAN_OPERATION","RECTANGLE","ELLIPSE","POLYGON","STAR","LINE"].includes(n.type)||r(n,"node type requires native export"),Array.isArray(n.strokes)&&n.strokes.some(e=>!1!==e.visible)&&"CENTER"!==n.strokeAlign&&r(n,"non-center stroke alignment requires native export"),Array.isArray(n.dashPattern)&&n.dashPattern.length&&r(n,"dashed stroke requires native export"),[n.fillGeometry,n.strokeGeometry]))void 0!==e&&(!Array.isArray(e)||e.some(e=>!["NONZERO","EVENODD"].includes(e.windingRule)))&&r(n,"unknown winding rule");d=a(n,n.fillGeometry,n.fills)+a(n,n.strokeGeometry,n.strokes)}return`${d}`}(e,!0);return n||r(e,"no painted geometry"),`${u}`}(e);return n("geometry-fallback",Date.now()-r),t}catch(e){throw Error(`native export: ${i.message||i}; ${e.message||e}`)}}finally{clearTimeout(a)}}let a=Date.now(),o="lookup",l="unknown",s=Math.min(6e5,Math.max(1e3,Number(t.timeoutMs)||9e4)),d={method:"native",stages:[]};function f(e,t,r){d.stages.push({stage:e,elapsedMs:t}),r&&(d.nativeError=r),"geometry-fallback"===e&&(d.method="geometry")}async function c(e){let t;try{return await Promise.race([e(),new Promise((e,r)=>{t=setTimeout(()=>r(Error(`DEVUP_ASSET_TIMEOUT: ${o} exceeded ${s} ms asset budget`)),Math.max(0,s-(Date.now()-a)))})])}finally{clearTimeout(t)}}function u(e){let r=e&&"string"==typeof e.message?e.message:String(e),n="string"==typeof figma.editorType?` (editor: ${figma.editorType})`:"";return`${o} failed after ${Date.now()-a} ms${n} (node: ${t.nodeId}, type: ${l}): ${r}`.slice(0,500)}function p(e,r){return e&&d.renderInfo&&(d.renderInfo.attemptedRepresentation=d.renderInfo.representation,d.renderInfo.representation="not-exported",d.renderInfo.renderedAs="not-exported",d.renderInfo.effectsBakedIntoAsset=!1,d.renderInfo.fidelity="unverified"),{kind:"devupAssetExport",fileKey:figma.fileKey||"",version:t.version,assetId:t.assetId,nodeId:t.nodeId,field:t.field,imageHash:t.imageHash,format:t.format,scale:t.scale,renderMode:h,usageAspectRatio:t.usageAspectRatio??null,maxWidth:t.maxWidth??null,srcset:!0===t.srcset,visibleScopeNodeId:t.visibleScopeNodeId??null,status:"failed",byteLength:null,sha256:null,errorCode:e,exportDiagnostics:d,...r?{errorDetail:r}:{}}}let g="string"==typeof t.field&&t.field.startsWith("$original-image/fills/"),h=void 0===t.renderMode?g?"fill":["svg","pdf"].includes(t.format)?"node":"original-source":t.renderMode,y=["auto","original-source"].includes(h)?"auto"===h?"node":"fill":h;function m(e,r){let n=e=>"number"==typeof e&&Number.isFinite(e)&&e>0,i=Array.isArray(e.effects)?e.effects:null,a=[],o=e=>[...e].slice(0,128).join(""),l=["string"==typeof e.name?o(e.name):"layer"],s=new Set,d=e;for(let t=e.parent;t&&a.length<32&&!s.has(t);t=t.parent)s.add(t),l.length<4&&"string"==typeof t.name&&t.name.length&&!["PAGE","DOCUMENT"].includes(t.type)&&l.push(o(t.name)),a.push({type:t.type||"unknown",clipsContent:t.clipsContent??null,isMask:t.isMask??null,effects:Array.isArray(t.effects)?t.effects:null,opacity:t.opacity??null,blendMode:t.blendMode??null,visible:t.visible??null,siblingMaskPresent:Array.isArray(t.children)?t.children.some(e=>e!==d&&!0===e.isMask):null}),d=t;return{sourceKind:r?"image-fill":t.imageHash?"image-node":"vector-node",renderedAs:"fill"===y?"raw-fill":"node-render",representation:"fill"===y?"original-fill":"native-node",layerHierarchy:l.reverse(),scope:"fill"===y?"selected-paint-original-bytes":"exact-node-subtree",exportSettings:"node"===y?{contentsOnly:!0,useAbsoluteBounds:!1}:null,requestedScale:t.scale,scaleSemantics:"fill"===y?"intrinsic-original-bytes-no-resize":["png","jpg"].includes(t.format)?"native-node-SCALE-constraint":"not-applied-to-vector-document",nodeSize:n(e.width)&&n(e.height)?{width:e.width,height:e.height,aspectRatio:e.width/e.height}:null,outputSize:null,dimensionEvidence:"unknown-until-byte-inspection",aspectRatioComparison:"unknown",scaleMode:r?r.scaleMode??null:null,imageTransform:r?r.imageTransform??null:null,visibleSourceRatio:null,visibleSourceRatioEvidence:"not-computed",effects:i,extraFills:Array.isArray(e.fills)?Math.max(0,e.fills.filter(e=>!1!==e.visible).length-!!r):null,effectsBakedIntoAsset:"node"===y&&["png","jpg"].includes(t.format)&&null!==i,bakedEffects:"node"===y&&["png","jpg"].includes(t.format)&&i?i.filter(e=>!1!==e.visible):[],instanceOverride:"unknown",instanceOverrideEvidence:"no-component-paint-comparison",sourceProperties:{scaleMode:r?r.scaleMode??null:null,imageTransform:r?r.imageTransform??null:null,rotation:r?r.rotation??null:null,filters:r?r.filters??null:null,opacity:r?r.opacity??null:null,effects:i,blurEffects:i?i.filter(e=>!1!==e.visible&&String(e.type).includes("BLUR")):null,isMask:e.isMask??null,clipsContent:e.clipsContent??null,ancestors:a},unappliedProperties:"fill"===y?["scaleMode","imageTransform","rotation","filters","paintOpacity","nodeGeometry","nodeTransforms","nodeEffectsIncludingBlur","nodeAndAncestorMasks","ancestorClipsAndEffects"]:[],warnings:"fill"===y?["original-fill-properties-unapplied","aspect-ratio-unknown"]:["ancestor-canvas-appearance-unverified","aspect-ratio-unknown"],fidelity:"unverified"}}function b(e,t){let r=0;for(let n=e;n&&r++<32;n=n.parent){if("INSTANCE"===n.type&&Array.isArray(n.overrides)&&n.overrides.find(t=>t.id===e.id&&Array.isArray(t.overriddenFields)&&t.overriddenFields.includes("fills"))){t.instanceOverride=!0,t.instanceOverrideEvidence={kind:"figma-instance-overrides",instanceId:n.id,nodeId:e.id,overriddenFields:["fills"]};return}if(["PAGE","DOCUMENT"].includes(n.type))break}}try{let e;if(!["original-source","fill","node","visible","auto"].includes(h))return p("DEVUP_ASSET_RENDER_MODE_UNSUPPORTED");if(!g&&"fill"===h&&(!String(t.field).startsWith("fills/")||!["png","jpg","webp"].includes(t.format)||1!==t.scale))return p("DEVUP_ASSET_FILL_SETTINGS_UNSUPPORTED","Original fill requires fills/N, png or jpg matching its codec, and scale 1; no resizing or transcoding.");if((g||["fill","original-source"].includes(h))&&"bridge"!==t.transport)return p(g?"DEVUP_ORIGINAL_IMAGE_REQUIRES_BRIDGE":"DEVUP_ASSET_FILL_REQUIRES_BRIDGE");let x=await c(()=>n(t.nodeId));if(f("lookup",Date.now()-a),l=x&&x.type||"unknown",!x||["node","visible"].includes(h)&&"function"!=typeof x.exportAsync)return p("DEVUP_ASSET_UNSUPPORTED_BY_UPSTREAM",u(Error(x?"node has no exportAsync":"node not found")));let E=null;if(g||"string"==typeof t.field&&t.field.startsWith("fills/")){let e=Number(t.field.slice(g?22:6)),r="fills"in x&&Array.isArray(x.fills)?x.fills:[],n=Number.isInteger(e)?r[e]:null,i=n&&"IMAGE"===n.type?n.imageHash||n.imageRef:null;if(!n||i!==t.imageHash)return p("DEVUP_ASSET_SOURCE_CHANGED");E=n}else if("node"!==t.field)return p("DEVUP_ASSET_FIELD_UNSUPPORTED");if("original-source"===h&&(!E||!t.imageHash))return p("DEVUP_ORIGINAL_SOURCE_UNAVAILABLE","No IMAGE fill source exists for this selection. Choose an IMAGE fill or explicitly renderMode node; vector assets should retain SVG.");let A=x;if("visible"===h){let e=t.visibleScopeNodeId;if(!e)return p("DEVUP_VISIBLE_CONTEXT_UNAVAILABLE","Recollect the parent frame and provide visibleScopeNodeId.");let r=new Set;for(;A&&A.id!==e&&!r.has(A)&&r.size<64;)r.add(A),A=A.parent;if(!A||A.id!==e||["PAGE","DOCUMENT"].includes(A.type)||"function"!=typeof A.exportAsync)return p("DEVUP_VISIBLE_SCOPE_UNSUPPORTED","The selected capture scope is not an exportable ancestor of the requested node.")}if(d.renderInfo=m(x,E),"visible"===h){let e=d.renderInfo;e.representation="native-visible",e.renderedAs="visible-scoped-capture",e.captureNodeId=A.id,e.captureBox={width:A.width,height:A.height,aspectRatio:A.width/A.height},e.contextScope="selected-ancestor-subtree",e.captureEffects=Array.isArray(A.effects)?A.effects:null,e.requestedNodeEffects=e.effects,e.effects=e.captureEffects,e.sourceProperties.captureEffects=e.captureEffects,e.visibleContext={requestedNodeId:x.id,captureNodeId:A.id,included:["selected-scope-children","selected-scope-clipping","selected-scope-masks","selected-scope-rounding"],outsideScope:"unknown; ancestors, page backdrop and external layers are not included",canvasReferenceCompared:!1},e.warnings.push("visible-scoped-capture","outside-capture-context-unverified")}b(x,d.renderInfo);let I=null;if(E)try{let r=figma.getImageByHash(t.imageHash),n=r&&await Promise.race([r.getSizeAsync(),new Promise(t=>{e=setTimeout(()=>t(null),250)})]);I=n&&n.width>0&&n.height>0&&Number.isFinite(n.width)&&Number.isFinite(n.height)?{...n,aspectRatio:n.width/n.height}:null}catch(e){}finally{clearTimeout(e)}let S=null;if("auto"===h){let e=function(e,r,n){let i,a=[],o=(e,t)=>{e||a.push(t)},l=e=>Array.isArray(e)&&2===e.length&&e.every(e=>Array.isArray(e)&&3===e.length&&e.every(Number.isFinite))&&1===e[0][0]&&1===e[1][1]&&0===e[0][1]&&0===e[1][0]&&Number.isInteger(e[0][2])&&Number.isInteger(e[1][2]);for(let a of(o("bridge"===t.transport,"raw-requires-bridge"),o(1===t.scale&&["png","jpg"].includes(t.format),"raw-requires-unscaled-raster"),o(!!r&&!0===r.visible&&1===r.opacity&&"NORMAL"===r.blendMode,"paint-compositing-or-unknown"),o(r&&"FILL"===r.scaleMode&&Array.isArray(i=r.imageTransform)&&2===i.length&&i.every((e,t)=>Array.isArray(e)&&3===e.length&&e.every((e,r)=>e===[[1,0,0],[0,1,0]][t][r]))&&0===r.rotation,"paint-transform-or-unknown"),o(r&&r.filters&&Object.values(r.filters).every(e=>0===e),"image-filters-or-unknown"),o(Array.isArray(e.fills)&&1===e.fills.length,"multiple-fills-or-unknown"),o("RECTANGLE"===e.type&&0===e.cornerRadius&&Array.isArray(e.strokes)&&0===e.strokes.length&&(!e.children||0===e.children.length),"node-geometry-or-unknown"),o(Array.isArray(e.effects)&&0===e.effects.length&&!1===e.isMask&&(void 0===e.clipsContent||!1===e.clipsContent)&&1===e.opacity&&!0===e.visible&&"NORMAL"===e.blendMode,"node-effects-mask-compositing-or-unknown"),o(l(e.absoluteTransform)&&l(e.relativeTransform),"node-transform-or-unknown"),o(n.sourceProperties.ancestors.length>0&&n.sourceProperties.ancestors.some(e=>["PAGE","DOCUMENT"].includes(e.type)),"ancestor-chain-incomplete"),o(n.sourceProperties.ancestors.every(e=>!1===e.siblingMaskPresent),"ancestor-sibling-mask-or-unknown"),n.sourceProperties.ancestors.filter(e=>!["PAGE","DOCUMENT"].includes(e.type))))o(!1===a.clipsContent&&!1===a.isMask&&!1===a.siblingMaskPresent&&Array.isArray(a.effects)&&0===a.effects.length&&1===a.opacity&&!0===a.visible&&["NORMAL","PASS_THROUGH"].includes(a.blendMode),"ancestor-dependency-or-unknown");return{eligible:0===a.length,reasons:[...new Set(a)],evidence:"explicit-plugin-properties; intrinsic-dimensions-and-codec-required"}}(x,E,d.renderInfo);if(e.eligible)try{let r=figma.getImageByHash(t.imageHash),n=I;n&&n.width===x.width&&n.height===x.height?(S=await c(()=>r.getBytesAsync()),(137===S[0]&&80===S[1]&&78===S[2]&&71===S[3]?"png":255===S[0]&&216===S[1]&&255===S[2]?"jpg":"unknown")!==t.format&&e.reasons.push("source-codec-mismatch-or-unknown")):e.reasons.push("intrinsic-dimensions-mismatch-or-unknown")}catch(t){e.reasons.push("source-inspection-unavailable")}e.eligible=0===e.reasons.length,y=e.eligible?"fill":"node",d.renderInfo=m(x,E),b(x,d.renderInfo),d.renderInfo.autoDecision={...e,selected:y}}if(d.renderInfo.sourceSize=I,d.renderInfo.sourceSizeEvidence=I?"Figma-image-getSizeAsync":"unknown",g||"fill"===y){d.method="original-fill",o="reading the original image";let e=figma.getImageByHash(t.imageHash);if(!e)return p("DEVUP_ORIGINAL_IMAGE_NOT_FOUND");let n=S||await c(()=>e.getBytesAsync());if(0===n.length||n.length>0x5000000)return p("DEVUP_ASSET_RESPONSE_TOO_LARGE",`${n.length} bytes; one answer carries 1 to 83886080`);let i=e=>e.every((e,t)=>n[t]===e),a=i([137,80,78,71,13,10,26,10])?"image/png":i([255,216,255])?"image/jpeg":i([71,73,70,56])?"image/gif":i([82,73,70,70])&&87===n[8]&&69===n[9]&&66===n[10]&&80===n[11]?"image/webp":null;if(!a)return p("DEVUP_ORIGINAL_IMAGE_CODEC_UNSUPPORTED");if(d.renderInfo.originalMimeType=a,!g&&"fill"===h&&"webp"!==t.format&&a!==({png:"image/png",jpg:"image/jpeg"})[t.format])return p("DEVUP_ASSET_FILL_CODEC_MISMATCH",`Original fill codec is ${a}, but ${t.format} was requested; choose the matching format or renderMode node. No transcoding was performed.`);if(!g)return{...p(null),status:"exported",mimeType:a,byteLength:n.length,sha256:r(n),data:figma.base64Encode(n)};let l=await c(()=>e.getSizeAsync());return{...p(null),status:"exported",kind:"devupOriginalImage",representation:"original-image-v1",format:null,scale:null,mimeType:a,width:l.width,height:l.height,byteLength:n.length,sha256:r(n),data:figma.base64Encode(n)}}let w="webp"===t.format?"PNG":String(t.format||"").toUpperCase();if(!["PNG","JPG","SVG","PDF"].includes(w))return p("DEVUP_ASSET_FORMAT_UNSUPPORTED");let v=Number(t.scale??2);if(!Number.isFinite(v)||v<=0||v>4)return p("DEVUP_ASSET_SCALE_INVALID");let N="SVG"===w,O={format:N?"SVG_STRING":w,contentsOnly:!0,useAbsoluteBounds:!1};("PNG"===w||"JPG"===w)&&(O.constraint={type:"SCALE",value:v}),o="export";let T=Math.max(1,s-(Date.now()-a)),P=N?Math.max(1,T-Math.min(5e3,Math.floor(T/5))):T,_=Date.now(),C=await c(()=>"visible"===h?A.exportAsync(O):E?x.exportAsync(O):i(x,O,P,f));if(E&&f("native",Date.now()-_),"geometry"===d.method&&(d.renderInfo.representation="geometry-fallback",d.renderInfo.renderedAs="geometry-fallback",d.renderInfo.effectsBakedIntoAsset=!1,d.renderInfo.warnings.push("geometry-fallback-not-native-render")),Date.now()-a>=s)throw Error(`DEVUP_ASSET_TIMEOUT: export exceeded ${s} ms asset budget`);o="encode";let M=Date.now(),k=N&&"string"==typeof C?C:null,R=null===k?C instanceof Uint8Array?C:new Uint8Array(C):function(e){let t=[];for(let r=0;r=55296&&n<=56319){let t=r+1=56320&&t<=57343?(n=65536+(n-55296<<10)+(t-56320),r+=1):n=65533}else n>=56320&&n<=57343&&(n=65533);n<128?t.push(n):n<2048?t.push(192|n>>6,128|63&n):n<65536?t.push(224|n>>12,128|n>>6&63,128|63&n):t.push(240|n>>18,128|n>>12&63,128|n>>6&63,128|63&n)}return new Uint8Array(t)}(k);if(0===R.length||R.length>0x5000000)return p("DEVUP_ASSET_RESPONSE_TOO_LARGE",`${w} at scale ${v} is ${R.length} bytes; one answer carries 1 to 83886080`);let D=r(R);f("encode",Date.now()-M);let L=figma.io&&"function"==typeof figma.io.write,$="PNG"===w&&R.length>786432;if(L&&(null!==k&&R.length>12288||$))return{kind:"devupAssetExport",fileKey:figma.fileKey||"",version:t.version,assetId:t.assetId,nodeId:t.nodeId,field:t.field,imageHash:t.imageHash,format:t.format,scale:v,renderMode:h,usageAspectRatio:t.usageAspectRatio??null,maxWidth:t.maxWidth??null,srcset:!0===t.srcset,visibleScopeNodeId:t.visibleScopeNodeId??null,status:"chunked",byteLength:R.length,sha256:D,cursor:{nextOffset:0,maxChunkBytes:12288},errorCode:null,exportDiagnostics:d};return L&&figma.io.write(`devup-asset-${t.assetId.replace(/[^A-Za-z0-9_-]/g,"_")}.${w.toLowerCase()}`,R),{kind:"devupAssetExport",fileKey:figma.fileKey||"",version:t.version,assetId:t.assetId,nodeId:t.nodeId,field:t.field,imageHash:t.imageHash,format:t.format,scale:v,renderMode:h,usageAspectRatio:t.usageAspectRatio??null,maxWidth:t.maxWidth??null,srcset:!0===t.srcset,visibleScopeNodeId:t.visibleScopeNodeId??null,status:"exported",byteLength:R.length,sha256:D,mimeType:L?null===k?null:"image/svg+xml":({PNG:"image/png",JPG:"image/jpeg",SVG:"image/svg+xml",PDF:"application/pdf"})[w],...!L&&null===k?{data:figma.base64Encode(R)}:{},text:k,errorCode:null,exportDiagnostics:d}}catch(e){return p(String(e.message||e).includes("DEVUP_ASSET_TIMEOUT:")?"DEVUP_ASSET_TIMEOUT":g||["fill","original-source"].includes(h)?"DEVUP_ORIGINAL_IMAGE_READ_FAILED":"DEVUP_ASSET_EXPORT_FAILED",u(e))}},bridgeContext:async function(e){let t=figma.currentPage,r=t.selection,n=e=>({id:e.id,name:String(e.name).slice(0,256),type:e.type});return{kind:"devup-bridge-context",currentPage:n(t),selection:r.slice(0,20).map(n),selectionCount:r.length,zoom:Number.isFinite(figma.viewport?.zoom)?figma.viewport.zoom:null,observedAt:Date.now(),lastKnownSelection:globalThis.__devupLastSelection??null,lastKnownPage:globalThis.__devupLastSelectionPage??null}},explore:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=await figma.getNodeByIdAsync(e.nodeId);if(!t)throw Error("DEVUP_NODE_NOT_FOUND");let r=e.explore,n=Math.max(20,Math.min(400,Number.isInteger(r.projectionLimit)?r.projectionLimit:200)),i=Math.max(0,Math.min(500,Number.isInteger(r.textPreviewLimit)?r.textPreviewLimit:160));function a(e){return"string"==typeof e?e.slice(0,240):""}let o="PAGE"===t.type,l=t,s="SECTION"===t.type?t:null;for(;!o&&l.parent&&"PAGE"!==l.parent.type;)l=l.parent,s||"SECTION"!==l.type||(s=l);let d="PAGE"===t.type?t:l.parent;if(!d||"PAGE"!==d.type)throw Error("DEVUP_PAGE_NOT_FOUND");function f(e){let t=e.absoluteBoundingBox||{x:"number"==typeof e.x?e.x:0,y:"number"==typeof e.y?e.y:0,width:"number"==typeof e.width?e.width:0,height:"number"==typeof e.height?e.height:0};return[t.x,t.y,t.width,t.height].every(Number.isFinite)?{x:t.x,y:t.y,width:t.width,height:t.height}:null}function c(e){let t=f(e);return t&&["FRAME","COMPONENT","INSTANCE","COMPONENT_SET"].includes(e.type)&&t.width>=240&&t.width<=1800&&t.height>=300&&t.height<=2e3&&t.width/Math.max(1,t.height)>=.25&&t.width/Math.max(1,t.height)<=2.5}function u(e){let t=[],r=e;for(;r&&"DOCUMENT"!==r.type&&t.length<12;){let e=a(r.name);e&&t.push(e),r=r.parent}return t.reverse()}await figma.setCurrentPageAsync(d);let p=o?d:!c(t)&&s?s:l,g=f(p)||f(t);if(!g)throw Error("DEVUP_NODE_BOUNDS_UNAVAILABLE");let h="children"in d?d.children:[],y=h.map((e,t)=>({node:e,pageChildIndex:t,bounds:f(e)})).filter(e=>e.bounds).filter(e=>{var t;return!!o||e.node.id===p.id||e.bounds.y>=g.y-240&&e.bounds.y<=g.y+12e3&&(t=e.bounds,Math.min(t.x+t.width,g.x+g.width)-Math.max(t.x,g.x)>0)}).sort((e,t)=>e.bounds.y-t.bounds.y||e.bounds.x-t.bounds.x||e.node.id.localeCompare(t.node.id)),m="SECTION"===p.type?y.filter(e=>e.node.id===p.id):y.slice(0,n),b=new Map(m.map(e=>[e.node.id,e]));b.has(l.id)||b.set(l.id,{node:l,pageChildIndex:-1,bounds:f(l)}),b.has(p.id)||b.set(p.id,{node:p,pageChildIndex:-1,bounds:f(p)}),b.has(t.id)||b.set(t.id,{node:t,pageChildIndex:-1,bounds:f(t)});let x=o?null:t.parent;for(;x&&"PAGE"!==x.type&&(b.has(x.id)||b.set(x.id,{node:x,pageChildIndex:-1,bounds:f(x)}),x.id!==p.id);)x=x.parent;if("children"in p)for(let e of p.children.slice(0,n))b.has(e.id)||b.set(e.id,{node:e,pageChildIndex:-1,bounds:f(e)});let E="SECTION"===p.type||o,A=E&&"children"in p&&p.children.length>n;if(E&&"children"in p){let e=8*n,t=p.children.filter(e=>!c(e)).map(e=>({node:e,ancestors:[]})),r=0;for(;t.length&&r!b.has(e.id));if(b.size+t.length>n+2){A=!0;continue}for(let e of t)b.set(e.id,{node:e,pageChildIndex:-1,bounds:f(e)});continue}if("children"in e)for(let r of e.children)(!o||"SECTION"===e.type)&&(!o||["SECTION","FRAME","COMPONENT","INSTANCE","COMPONENT_SET"].includes(r.type))&&t.push({node:r,ancestors:[...i,e]})}A||=t.length>0}let I=[...b.values()].filter(e=>e.bounds).slice(0,n+2).map(({node:e,pageChildIndex:t,bounds:r})=>{let n;return{id:e.id,type:e.type,fields:{name:a(e.name),parentId:e.parent&&"DOCUMENT"!==e.parent.type?e.parent.id:null,childrenIds:[],x:r.x,y:r.y,width:r.width,height:r.height,childCount:"children"in e?e.children.length:0,textPreview:"",...o||null===(n=function(e){let t=[e],r=0,n=null;for(;t.length&&r++<400;){let e=t.shift();if("visible"in e&&!1===e.visible)continue;let r="children"in e?e.children.filter(e=>!("visible"in e)||!1!==e.visible):[];if(/header|nav|gnb/i.test(e.name||"")&&r.length>=2&&"layoutMode"in e&&"HORIZONTAL"===e.layoutMode){let t=r.map(f);t.every(Boolean)&&(n=Math.max(n||0,t.reduce((e,t)=>e+t.width,0)+(Number(e.itemSpacing)||0)*(t.length-1)))}t.push(...r.slice(0,80))}return n}(e))?{}:{navigationContentWidth:n},pageChildIndex:t>=0?t:null,visible:!("visible"in e)||!1!==e.visible,breadcrumb:u(e)},extra:{},fieldErrors:{}}}),S=I.reduce((e,t)=>{let r=t.fields;return e?{x:Math.min(e.x,r.x),y:Math.min(e.y,r.y),right:Math.max(e.right,r.x+r.width),bottom:Math.max(e.bottom,r.y+r.height)}:{x:r.x,y:r.y,right:r.x+r.width,bottom:r.y+r.height}},null)||{x:0,y:0,right:0,bottom:0},w="SECTION"!==p.type&&y.length>m.length||b.size>n+2||A,v={id:d.id,type:d.type,fields:{name:a(d.name),parentId:null,childrenIds:[],x:S.x,y:S.y,width:S.right-S.x,height:S.bottom-S.y,childCount:h.length,textPreview:"",projectionTruncated:w,visible:!0,breadcrumb:u(d),pageChildIndex:null},extra:{},fieldErrors:{}},N={fileKey:figma.fileKey||"",version:null,rootIds:[d.id],nodes:[v,...I.filter(e=>e.id!==d.id)],diagnostics:[]},O=new Set([d.id,l.id,p.id,t.id]);for(;JSON.stringify(N).length>14e3;){let e=N.nodes.length-1;for(;e>=0&&O.has(N.nodes[e].id);)e-=1;if(e<0)break;N.nodes.splice(e,1),v.fields.projectionTruncated=!0}if(JSON.stringify(N).length>14e3&&(N.nodes=N.nodes.filter(e=>O.has(e.id)).map(e=>({...e,fields:{...e.fields,name:a(e.fields.name).slice(0,80),textPreview:"",breadcrumb:e.fields.breadcrumb.slice(-4).map(e=>e.slice(0,80)),projectionTruncated:e.id===d.id||e.fields.projectionTruncated}}))),JSON.stringify(N).length>14e3)throw Error("DEVUP_EXPLORE_PROJECTION_TOO_LARGE");function T(e){let t=0;for(let r of e){let e=r.codePointAt(0);t+=e<128?1:e<2048?2:e<65536?3:4}return t}if(o){for(;T(JSON.stringify(N))>19456&&N.nodes.length>1;)N.nodes.pop(),v.fields.projectionTruncated=!0;if(T(JSON.stringify(N))>19456)throw Error("DEVUP_EXPLORE_PROJECTION_TOO_LARGE")}let P=14e3-JSON.stringify(N).length;if(o&&(P=Math.min(P,19456-T(JSON.stringify(N)))),i>0&&P>0){let e=N.nodes.filter(e=>b.get(e.id)&&e.id!==d.id),t=e.length;for(let r of e){let e=Math.floor(P/t);if(t-=1,e<=0)continue;let n="",a=0;for(let t of function(e){if(0===i)return"";let t=[],r=[e],n=0;for(;r.length&&n<80&&t.join(" ").lengthe)break;n+=t,a+=r}r.fields.textPreview=n,P-=a}}return N},fastSnapshot:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=e.rootIds;if(!Array.isArray(t)||0===t.length)throw Error("DEVUP_ROOTS_INVALID");let r=await Promise.all(t.map(e=>figma.getNodeByIdAsync(e)));if(r.some(e=>!e))throw Error("DEVUP_NODE_NOT_FOUND");if(1===r.length&&"SECTION"===r[0].type)throw Error("DEVUP_TARGET_IS_SECTION");let n=["mobile","tablet","desktop"],i=e=>{let t=String(e.name||"").trim().toLowerCase(),r=n.indexOf(t);if(r>=0)return r;let i=t.match(/^(\d+)\s*[~–]\s*(\d+)$/);if(!i)return -1;let a=Number(i[1]),o=Number(i[2]);return Number.isSafeInteger(a)&&Number.isSafeInteger(o)&&a>0&&o>=a&&o<=0xffffffff?1:-1};if(1===r.length&&i(r[0])>=0){let e=r[0].parent;if(e&&"SECTION"===e.type&&"children"in e){let t=e.children.filter(e=>e.id===r[0].id||i(e)>=0).filter(e=>!1!==e.visible);t.length>1&&(r.length=0,r.push(...t))}}if(1===t.length){let e=[],t=new Set(r.map(e=>e.id)),n=[],i=e=>{if("reactions"in e&&Array.isArray(e.reactions)){for(let r of e.reactions)if(r&&r.trigger&&"AFTER_TIMEOUT"===r.trigger.type)for(let e of r.actions||[])e&&"NODE"===e.type&&e.transition&&"SMART_ANIMATE"===e.transition.type&&"string"==typeof e.destinationId&&!t.has(e.destinationId)&&(t.add(e.destinationId),n.push(e.destinationId));if("children"in e)for(let t of e.children)i(t)}};for(let e of r)i(e);for(;n.length>0;){let t=n.shift(),r=await figma.getNodeByIdAsync(t);r&&"DOCUMENT"!==r.type&&"PAGE"!==r.type&&(e.push(r),i(r))}r.push(...e)}let a=e.nodeId,o=["absoluteBoundingBox","absoluteRenderBounds","arcData","backgroundStyleId","blendMode","bottomLeftRadius","bottomRightRadius","boundVariables","characters","clipsContent","componentProperties","componentPropertyDefinitions","componentPropertyReferences","constraints","cornerRadius","counterAxisAlignItems","dashPattern","defaultVariant","effectStyleId","effects","fillStyleId","fills","fontName","fontSize","gridColumnAnchorIndex","gridColumnCount","gridColumnGap","gridColumnSizes","gridColumnSpan","gridRowAnchorIndex","gridRowCount","gridRowGap","gridRowSizes","gridRowSpan","gridStyleId","height","inferredAutoLayout","isAsset","isMask","itemSpacing","layoutGrow","layoutMode","layoutWrap","layoutPositioning","layoutSizingHorizontal","layoutSizingVertical","letterSpacing","lineHeight","maxHeight","maxLines","maxWidth","minHeight","minWidth","name","opacity","overflowDirection","paddingBottom","paddingLeft","paddingRight","paddingTop","primaryAxisAlignItems","reactions","rotation","strokeAlign","strokeBottomWeight","strokeLeftWeight","strokeRightWeight","strokeStyleId","strokeTopWeight","strokeWeight","strokes","targetAspectRatio","textAlignHorizontal","textAlignVertical","textAutoResize","textCase","textDecoration","textStyleId","textTruncation","topLeftRadius","topRightRadius","variantProperties","visible","width","x","y"],l=["fontName","fontWeight","fontSize","textDecoration","textCase","lineHeight","letterSpacing","fills","textStyleId","fillStyleId","listOptions","indentation","hyperlink"],s=e.snapshot,d=Math.max(0,Math.floor(Number(s.offset)||0)),f=Math.max(8192,Math.floor(Number(s.maxEnvelopeBytes)||19456)),c=Math.min(s.maxEnvelopeBytes?f-1024:18e3,Math.max(4096,Math.floor(Number(s.maxPayloadBytes)||15e3))),u=new Set(["backgroundStyleId","effectStyleId","fillStyleId","gridStyleId","strokeStyleId","textStyleId"]),p=new Set(["maxWidth","maxHeight","absoluteRenderBounds"]),g=new Map([["rotation",0],["cornerRadius",0],["isAsset",!1],["isMask",!1],["clipsContent",!1],["blendMode","PASS_THROUGH"],["strokeAlign","INSIDE"],["textCase","ORIGINAL"],["textDecoration","NONE"],["textAlignHorizontal","LEFT"],["textAlignVertical","TOP"],["counterAxisAlignItems","MIN"],["primaryAxisAlignItems","MIN"],["gridColumnCount",0],["gridRowCount",0],["gridColumnGap",0],["gridRowGap",0],["gridColumnAnchorIndex",-1],["gridRowAnchorIndex",-1],["gridColumnSpan",1],["gridRowSpan",1]]),h=new Set(["start","end","characters","fontWeight","textStyleId","fillStyleId","listOptions","indentation","hyperlink"]),y=new Set(["parent","children","consumers"]);function m(e,t=!1,r=new WeakSet,n=0){let i;if(null===e||["string","number","boolean"].includes(typeof e))return e;if(void 0===e)return{$undefined:!0};if("bigint"==typeof e)return{$bigint:e.toString()};if(["function","symbol"].includes(typeof e))return{$unsupported:typeof e};if(n>12)return{$truncated:"max-depth"};if("object"==typeof e&&"parent"in e&&"string"==typeof e.id&&"string"==typeof e.type)return{$nodeId:e.id,$nodeType:e.type};if(Array.isArray(e))return e.map(e=>m(e,t,r,n+1));if(ArrayBuffer.isView(e))return{$binary:e.constructor.name,byteLength:e.byteLength};if(e instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:e.byteLength};if(r.has(e))return{$circular:!0};if(r.add(e),t){let t=new Set(Object.keys(e)),r=e;for(;r&&r!==Object.prototype;){for(let e of Object.getOwnPropertyNames(r))t.add(e);r=Object.getPrototypeOf(r)}i=[...t].sort().filter(e=>!e.startsWith("_")&&!y.has(e))}else i=Object.keys(e).sort();let a={};for(let o of i)try{let i=m(e[o],t,r,n+1);i&&"function"===i.$unsupported||(a[o]=i)}catch(e){a[o]=t?{$error:"unavailable"}:{$error:String(e&&e.message?e.message:e)}}return r.delete(e),a}let b=new Map;async function x(e,t,r){if("INSTANCE"!==e.type||"function"!=typeof e.getMainComponentAsync)return;b.has(e.id)||b.set(e.id,(async()=>{try{let t=await e.getMainComponentAsync();return{fields:{mainComponentId:t?t.id:null,mainComponentName:t?t.name:null,mainComponentSetId:t&&t.parent&&"COMPONENT_SET"===t.parent.type?t.parent.id:null}}}catch(e){return{error:String(e&&e.message?e.message:e)}}})());let n=await b.get(e.id);n.error?r.mainComponentId=n.error:Object.assign(t,n.fields)}async function E(e){let t={},r={};e.parent&&(t.parentId=e.parent.id),e.parent&&("PAGE"===e.parent.type||"SECTION"===e.parent.type||"COMPONENT_SET"===e.parent.type)&&(t.parentType=e.parent.type,"SECTION"===e.parent.type&&(t.parentName=e.parent.name));let n="children"in e?e.children.map(e=>e.id):[];for(let i of(n.length>0&&(t.childrenIds=n),o)){let n;if("componentPropertyDefinitions"!==i||"COMPONENT"!==e.type||!e.parent||"COMPONENT_SET"!==e.parent.type)try{if(!(i in e)){"overflowDirection"===i&&["FRAME","COMPONENT","INSTANCE","COMPONENT_SET"].includes(e.type)&&(t[i]=null);continue}if(n=e[i],"function"==typeof n)continue;let r=m(n);if("overflowDirection"===i&&null==n){t[i]=null;continue}(null===r?!p.has(i):Array.isArray(r)?0===r.length:"object"==typeof r?0===Object.keys(r).length:!!(""===r&&u.has(i))||g.has(i)&&g.get(i)===r)||(t[i]=r)}catch(e){r[i]=String(e&&e.message?e.message:e)}}if("TEXT"===e.type&&"function"==typeof e.getStyledTextSegments)try{let r=m(e.getStyledTextSegments(l));if(1===r.length){let e=r[0];for(let t of Object.keys(e))h.has(t)||delete e[t]}r.length>0&&(t.styledTextSegments=r)}catch(e){r.styledTextSegments=String(e&&e.message?e.message:e)}await x(e,t,r);let i={id:e.id,type:e.type,fields:t};return Object.keys(r).length>0&&(i.fieldErrors=r),i}let A=[],I=[...r],S=new Set;for(let e=0;e=A.length&&A.length>0)throw Error("DEVUP_SNAPSHOT_RANGE_INVALID");function w(e){let t=0;for(let r=0;r=55296&&n<=56319&&r+1({id:e,styleType:t})).sort((e,t)=>e.id.localeCompare(t.id));async function a(e){try{let t=await figma.variables.getVariableByIdAsync(e);return t?{kind:"variable",value:m(t,!0),collectionId:t.variableCollectionId,aliasIds:Object.values(t.valuesByMode||{}).filter(e=>e&&"VARIABLE_ALIAS"===e.type&&"string"==typeof e.id&&e.id).map(e=>e.id)}:{kind:"unresolved",value:{id:e,kind:"variable",reason:"notFoundOrUnavailable"}}}catch(t){return{kind:"unresolved",value:{id:e,kind:"variable",reason:"notFoundOrUnavailable",error:String(t&&t.message||t).slice(0,300)}}}}let o=await Promise.all([...n.map(a),...i.map(async({id:e,styleType:t})=>{try{let r=await figma.getStyleByIdAsync(e);if(!r)return{kind:"unresolved",value:{id:e,kind:"style",reason:"notFoundOrUnavailable"}};return{kind:"style",value:{...m(r,!0),styleType:t,value:m("PAINT"===t?r.paints:"EFFECT"===t?r.effects:"GRID"===t?r.layoutGrids:r,!0)}}}catch(t){return{kind:"unresolved",value:{id:e,kind:"style",reason:"notFoundOrUnavailable",error:String(t&&t.message||t).slice(0,300)}}}})]),l=new Set(n),s=o.flatMap(e=>e.aliasIds||[]);for(;s.length>0;){let e=[...new Set(s)].filter(e=>!l.has(e)).sort();for(let t of e)l.add(t);let t=await Promise.all(e.map(a));o.push(...t),s=t.flatMap(e=>e.aliasIds||[])}let d=[...new Set(o.filter(e=>"variable"===e.kind&&e.collectionId).map(e=>e.collectionId))].sort(),f=(await Promise.all(d.map(async e=>{try{let t=await figma.variables.getVariableCollectionByIdAsync(e);return t?m(t,!0):null}catch(e){return null}}))).filter(e=>null!==e),c=o.filter(e=>"variable"===e.kind).map(e=>e.value),u=o.filter(e=>"style"===e.kind).map(e=>e.value),p=o.filter(e=>"unresolved"===e.kind).map(e=>e.value);return{collections:f,variables:c,styles:u,usedRemoteVariables:c.filter(e=>!0===e.remote),usedVariableIds:n,usedStyleIds:i.map(e=>e.id),localComplete:!1,usedRemoteComplete:0===p.length,unresolved:p,$variableRefCount:n.length,$styleRefCount:i.length}}async function N(e){let t=[],r=2;for(let n=d;ne)break;t.push(i),r+=a}return{pageNodes:t,packedBytes:r}}let O=c-1024,T=null;for(let e=0;e<5;e+=1){let{pageNodes:e,packedBytes:t}=await N(O);if(0===e.length)throw Error("DEVUP_SNAPSHOT_RANGE_INVALID");let n=function(e,t){let n=Math.min(A.length,d+e.length),{$variableRefCount:i,$styleRefCount:o,...l}=t,s=[...e,{id:"__DEVUP_SNAPSHOT_CURSOR__",type:"DEVUP_INTERNAL",fields:{offset:d,nextOffset:n,complete:n>=A.length,totalNodes:A.length},extra:{},fieldErrors:{}}],f={kind:"devupFastSnapshotEnvelope",schemaVersion:1,source:{fileKey:figma.fileKey||"",rootId:a},snapshot:{fileKey:figma.fileKey||"",version:null,rootIds:r.map(e=>e.id),nodes:s,diagnostics:[]},resources:l,integrity:{nodeCount:s.length,variableRefCount:i,styleRefCount:o,utf8Bytes:0}},c=0;for(let e=0;e<8&&(c=w(JSON.stringify(f)),f.integrity.utf8Bytes!==c);e+=1)f.integrity.utf8Bytes=c;if(f.integrity.utf8Bytes!==w(JSON.stringify(f)))throw Error("DEVUP_ENVELOPE_LENGTH_UNSTABLE");return{envelope:f,bytes:c}}(e,await v(e));if(n.bytes<=f){T=n;break}if(1===e.length)throw Error("DEVUP_ENVELOPE_TOO_LARGE");O=Math.max(1,Math.min(O-1,t-(n.bytes-f)-256))}if(!T)throw Error("DEVUP_ENVELOPE_TOO_LARGE");return T.envelope},fastTheme:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=Math.max(0,Math.floor(Number(e.theme.offset)||0));function r(e,t=new WeakSet,n=0){if(null===e||["string","number","boolean"].includes(typeof e))return e;if(void 0===e)return{$undefined:!0};if("bigint"==typeof e)return{$bigint:e.toString()};if(["function","symbol"].includes(typeof e))return{$unsupported:typeof e};if(n>12)return{$truncated:"max-depth"};if("object"==typeof e&&"parent"in e&&"string"==typeof e.id&&"string"==typeof e.type)return{$nodeId:e.id,$nodeType:e.type};if(Array.isArray(e))return e.map(e=>r(e,t,n+1));if(ArrayBuffer.isView(e))return{$binary:e.constructor.name,byteLength:e.byteLength};if(e instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:e.byteLength};if(t.has(e))return{$circular:!0};t.add(e);let i={};for(let a of function(e){let t=new Set(Object.keys(e)),r=e;for(;r&&r!==Object.prototype;){for(let e of Object.getOwnPropertyNames(r))t.add(e);r=Object.getPrototypeOf(r)}return[...t].sort()}(e))if(!(a.startsWith("_")||["parent","children","consumers"].includes(a)))try{let o=r(e[a],t,n+1);o&&"function"===o.$unsupported||(i[a]=o)}catch(e){i[a]={$error:"unavailable"}}return t.delete(e),i}let n=new Set,i=new Map;for(let e of figma.root.children)await e.loadAsync();for(let e of[figma.root,...figma.root.findAll(()=>!0)])for(let t of["boundVariables","fills","strokes","effects","layoutGrids","textStyleId","fillStyleId","strokeStyleId","backgroundStyleId","effectStyleId","gridStyleId"])try{!function e(t,r="",a=new WeakSet,o=0){if(!(o>16)&&null!==t&&"object"==typeof t&&!a.has(t)){if(a.add(t),Array.isArray(t)){for(let n of t)e(n,r,a,o+1);return}for(let[l,s]of("VARIABLE_ALIAS"===t.type&&"string"==typeof t.id&&t.id&&"figma.mixed"!==t.id&&"MIXED"!==t.id&&n.add(t.id),Object.entries(t))){let t="textStyleId"===l?"TEXT":["fillStyleId","strokeStyleId","backgroundStyleId"].includes(l)?"PAINT":"effectStyleId"===l?"EFFECT":"gridStyleId"===l?"GRID":null;t&&"string"==typeof s&&s&&"figma.mixed"!==s&&"MIXED"!==s&&!i.has(s)&&i.set(s,t),e(s,l||r,a,o+1)}}}({[t]:e[t]})}catch(e){}let[a,o,l,s,d,f]=await Promise.all([figma.variables.getLocalVariableCollectionsAsync(),figma.variables.getLocalVariablesAsync(),figma.getLocalPaintStylesAsync(),figma.getLocalTextStylesAsync(),figma.getLocalEffectStylesAsync(),figma.getLocalGridStylesAsync()]),c=new Set(o.map(e=>e.id)),u=new Set([...l,...s,...d,...f].map(e=>e.id)),p=[];async function g(e){try{return await figma.variables.getVariableByIdAsync(e)||null}catch(t){return p.push({id:e,kind:"variable",reason:"notFoundOrUnavailable",error:String(t&&t.message||t).slice(0,300)}),null}}function h(e){return Object.values(e.valuesByMode||{}).filter(e=>e&&"VARIABLE_ALIAS"===e.type&&"string"==typeof e.id&&e.id).map(e=>e.id)}let y=[...i.entries()].filter(([e])=>!u.has(e)).sort(([e],[t])=>e.localeCompare(t)).map(async([e,t])=>{try{let r=await figma.getStyleByIdAsync(e);return r?{style:r,styleType:t}:null}catch(t){return p.push({id:e,kind:"style",reason:"notFoundOrUnavailable",error:String(t&&t.message||t).slice(0,300)}),null}}),m=new Set,b=[],x=[...n,...o.flatMap(h)];for(;x.length>0;){let e=[...new Set(x)].filter(e=>!c.has(e)&&!m.has(e)).sort();for(let t of e)m.add(t);let t=(await Promise.all(e.map(g))).filter(Boolean);b.push(...t),x=t.flatMap(h)}let E=(await Promise.all(y)).filter(Boolean);for(let e of m)b.some(t=>t.id===e)||p.some(t=>t.id===e)||p.push({id:e,kind:"variable",reason:"notFoundOrUnavailable"});for(let[e]of[...i.entries()].filter(([e])=>!u.has(e)))E.some(t=>t.style.id===e)||p.some(t=>t.id===e)||p.push({id:e,kind:"style",reason:"notFoundOrUnavailable"});let A=[...new Set(b.map(e=>e.variableCollectionId))].filter(e=>!a.some(t=>t.id===e)).sort(),I=(await Promise.all(A.map(async e=>{try{return await figma.variables.getVariableCollectionByIdAsync(e)}catch(e){return null}}))).filter(Boolean);function S(e,t){return{...r(e),styleType:t,value:r("PAINT"===t?e.paints:"EFFECT"===t?e.effects:"GRID"===t?e.layoutGrids:e)}}let w=[...l.map(e=>S(e,"PAINT")),...s.map(e=>S(e,"TEXT")),...d.map(e=>S(e,"EFFECT")),...f.map(e=>S(e,"GRID")),...E.map(({style:e,styleType:t})=>S(e,t))].sort((e,t)=>e.id.localeCompare(t.id)),v=[...o,...b].map(e=>r(e)).sort((e,t)=>e.id.localeCompare(t.id)),N=[...a,...I].map(e=>r(e)).sort((e,t)=>e.id.localeCompare(t.id));function O(e){let t=[];for(let r=0;r=55296&&n<=56319){let t=r+1=56320&&t<=57343?(n=65536+(n-55296<<10)+(t-56320),r+=1):n=65533}else n>=56320&&n<=57343&&(n=65533);n<128?t.push(n):n<2048?t.push(192|n>>6,128|63&n):n<65536?t.push(224|n>>12,128|n>>6&63,128|63&n):t.push(240|n>>18,128|n>>12&63,128|n>>6&63,128|63&n)}return new Uint8Array(t)}p.sort((e,t)=>e.kind.localeCompare(t.kind)||e.id.localeCompare(t.id));let T=[...N.map(e=>({kind:"collection",value:e})),...v.map(e=>({kind:"variable",value:e})),...w.map(e=>({kind:"style",value:e})),...[...n].sort().map(e=>({kind:"usedVariableId",value:e})),...[...i.keys()].sort().map(e=>({kind:"usedStyleId",value:e})),...p.map(e=>({kind:"unresolved",value:e}))];if(t>T.length)throw Error("DEVUP_SNAPSHOT_RANGE_INVALID");let P=T.map(e=>O(JSON.stringify(e.value)).length+1),_=18432,C=null;for(let e=0;e<6;e+=1){let{pageItems:e,packed:r}=function(e){let r=[],n=0;for(let i=t;i0)||!(n+P[i]>e));i+=1)r.push(T[i]),n+=P[i];return{pageItems:r,packed:n}}(_),n=function(e,r){let n=t=>e.filter(e=>e.kind===t).map(e=>e.value),i=n("collection"),a=n("variable"),o=n("style"),l=n("unresolved"),s={kind:"devupFastThemeEnvelope",schemaVersion:1,source:{fileKey:figma.fileKey||"",version:null},resources:{collections:i,variables:a,styles:o,usedRemoteVariables:[],usedVariableIds:n("usedVariableId"),usedStyleIds:n("usedStyleId"),localComplete:!0,usedRemoteComplete:0===p.length,unresolved:l},page:{offset:t,nextOffset:r,complete:r>=T.length,totalItems:T.length},integrity:{collectionCount:i.length,variableCount:a.length,styleCount:o.length,unresolvedCount:l.length,utf8Bytes:0}},d=new Uint8Array;for(let e=0;e<8&&(d=O(JSON.stringify(s)),s.integrity.utf8Bytes!==d.length);e+=1)s.integrity.utf8Bytes=d.length;if(d=O(JSON.stringify(s)),s.integrity.utf8Bytes!==d.length)throw Error("DEVUP_ENVELOPE_LENGTH_UNSTABLE");return{envelope:s,bytes:d.length}}(e,t+e.length);if(n.bytes<=19456){C=n;break}if(e.length<=1)break;_=Math.max(1,Math.min(_-1,r-(n.bytes-19456)-256))}if(null===C||C.bytes>8388608)throw Error("DEVUP_ENVELOPE_TOO_LARGE");return C.envelope},largeValue:async function(e){let t;figma.skipInvisibleInstanceChildren=!1;let r=e.largeValue;async function n(e){try{let t=await figma.getNodeByIdAsync(e);if(t)return t}catch(r){if(!e.startsWith("I")||!e.includes(";"))throw r;try{return await t()}catch(e){throw Error(`${r.message||r}; nested lookup fallback: ${e.message||e}`)}}if(!e.startsWith("I")||!e.includes(";"))return null;return t();async function t(){let t=await figma.getNodeByIdAsync(e.slice(1).split(";")[0]),r=t?[t]:[],n=new Set;for(;r.length;){let t=r.pop();if(t.id===e)return t;if(!n.has(t.id)){if(n.add(t.id),n.size>1e4)throw Error("nested lookup exceeds 10000 nodes");Array.isArray(t.children)&&r.push(...t.children)}}throw Error(`nested node ${e} not found in its instance`)}}async function i(e,t,r=85e3,n=()=>{}){let a,o=Date.now();try{let i=await Promise.race([e.exportAsync(t),new Promise((e,t)=>{a=setTimeout(()=>t(Error(`DEVUP_ASSET_TIMEOUT: native export exceeded ${r} ms`)),r)})]);return n("native-export",Date.now()-o),i}catch(i){if(n("native-export",Date.now()-o,String(i.message||i).slice(0,500)),"SVG_STRING"!==t.format)throw i;let r=Date.now();try{let t=function(e){let t=e=>"number"==typeof e&&Number.isFinite(e),r=(e,t)=>{throw Error(`geometry fallback unsupported: ${e.type||"unknown"} ${e.id||""}: ${t}`)},n=0,i=0;function a(e,i,a){(void 0!==a&&!Array.isArray(a)||void 0===a&&Array.isArray(i)&&i.length)&&r(e,"unresolved paint requires native export");let o=Array.isArray(a)?a.filter(e=>!1!==e.visible):[];return o.length?(Array.isArray(i)&&i.length||r(e,"paint has no rendered geometry"),o.map(a=>{("SOLID"!==a.type||a.blendMode&&"NORMAL"!==a.blendMode)&&r(e,"paint type or blending requires native export");let o=a.color;o&&[o.r,o.g,o.b].every(e=>t(e)&&e>=0&&e<=1)||r(e,"invalid solid color");let l=void 0===a.opacity?1:a.opacity;return(!t(l)||l<0||l>1)&&r(e,"invalid paint opacity"),["NONZERO","EVENODD"].map(t=>{let a=i.filter(e=>e.windingRule===t);if(!a.length)return"";for(let t of a)"string"==typeof t.data&&t.data.trim()||r(e,"empty path"),n++;return`"']/g,e=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[e])}" fill="rgb(${100*o.r}%,${100*o.g}%,${100*o.b}%)" fill-opacity="${l}" fill-rule="${"EVENODD"===t?"evenodd":"nonzero"}"/>`}).join("")}).join("")):""}let o=e.absoluteRenderBounds,l=e.absoluteTransform;(!o||![o.x,o.y,o.width,o.height].every(t)||o.width<=0||o.height<=0)&&r(e,"missing render bounds"),(!Array.isArray(l)||2!==l.length||!l.every(e=>Array.isArray(e)&&3===e.length&&e.every(t))||0!==l[0][1]||0!==l[1][0]||l[0][0]<=0||l[1][1]<=0)&&r(e,"root rotation, reflection or shear requires native export");let s=(o.x-l[0][2])/l[0][0],d=(o.y-l[1][2])/l[1][1],f=o.width/l[0][0],c=o.height/l[1][1],u=function e(n,o){if(++i>1e4&&r(n,"subtree exceeds 10000 nodes"),n.isMask&&r(n,"mask requires native export"),!1===n.visible||0===n.opacity)return"";n.clipsContent&&r(n,"clipping requires native export"),Array.isArray(n.effects)&&n.effects.some(e=>!1!==e.visible)&&r(n,"effect requires native export"),n.blendMode&&!["NORMAL","PASS_THROUGH"].includes(n.blendMode)&&r(n,"layer blending requires native export");let l=void 0===n.opacity?1:n.opacity;(!t(l)||l<0||l>1)&&r(n,"invalid layer opacity");let s="";if(!o){let e=n.relativeTransform;Array.isArray(e)&&2===e.length&&e.every(e=>Array.isArray(e)&&3===e.length&&e.every(t))||r(n,"missing relative transform"),s=` transform="matrix(${e[0][0]} ${e[1][0]} ${e[0][1]} ${e[1][1]} ${e[0][2]} ${e[1][2]})"`}let d="";if(["GROUP","FRAME","INSTANCE","COMPONENT"].includes(n.type))(Array.isArray(n.fills)&&n.fills.some(e=>!1!==e.visible)||Array.isArray(n.strokes)&&n.strokes.some(e=>!1!==e.visible))&&r(n,"container paint requires native export"),Array.isArray(n.children)||r(n,"missing children"),d=(!0===n.itemReverseZIndex?[...n.children].reverse():n.children).map(t=>e(t,!1)).join("");else{for(let e of(["VECTOR","BOOLEAN_OPERATION","RECTANGLE","ELLIPSE","POLYGON","STAR","LINE"].includes(n.type)||r(n,"node type requires native export"),Array.isArray(n.strokes)&&n.strokes.some(e=>!1!==e.visible)&&"CENTER"!==n.strokeAlign&&r(n,"non-center stroke alignment requires native export"),Array.isArray(n.dashPattern)&&n.dashPattern.length&&r(n,"dashed stroke requires native export"),[n.fillGeometry,n.strokeGeometry]))void 0!==e&&(!Array.isArray(e)||e.some(e=>!["NONZERO","EVENODD"].includes(e.windingRule)))&&r(n,"unknown winding rule");d=a(n,n.fillGeometry,n.fills)+a(n,n.strokeGeometry,n.strokes)}return`${d}`}(e,!0);return n||r(e,"no painted geometry"),`${u}`}(e);return n("geometry-fallback",Date.now()-r),t}catch(e){throw Error(`native export: ${i.message||i}; ${e.message||e}`)}}finally{clearTimeout(a)}}let a=await n(r.nodeId);if(!a)throw Error("DEVUP_NODE_NOT_FOUND");function o(e){let t=[];for(let r=0;r=55296&&n<=56319){let t=r+1=56320&&t<=57343?(n=65536+(n-55296<<10)+(t-56320),r+=1):n=65533}else n>=56320&&n<=57343&&(n=65533);n<128?t.push(n):n<2048?t.push(192|n>>6,128|63&n):n<65536?t.push(224|n>>12,128|n>>6&63,128|63&n):t.push(240|n>>18,128|n>>12&63,128|n>>6&63,128|63&n)}return new Uint8Array(t)}let l=null;try{if("$export:svg"===r.field){if("function"!=typeof a.exportAsync)throw Error("unsupported");let e=await i(a,{format:"SVG_STRING"},Math.max(1,(Number(r.timeoutMs)||9e4)-5e3));if("string"!=typeof e)throw Error("unsupported");l=o(e)}else if("string"==typeof r.field&&r.field.startsWith("$export:png")){if("function"!=typeof a.exportAsync)throw Error("unsupported");let e=Number(r.field.split("@")[1]||2);if(!Number.isFinite(e)||e<=0||e>4)throw Error("invalid scale");let t=r.field.split("@"),n=a;if("visible"===t[2]){let e=new Set;for(;n&&n.id!==t[3]&&!e.has(n)&&e.size<64;)e.add(n),n=n.parent;if(!n||n.id!==t[3]||["PAGE","DOCUMENT"].includes(n.type))throw Error("invalid visible scope")}else if(t.length>2)throw Error("invalid export field");let i=await n.exportAsync({format:"PNG",contentsOnly:!0,useAbsoluteBounds:!1,constraint:{type:"SCALE",value:e}});l=i instanceof Uint8Array?i:new Uint8Array(i)}else if("styledTextSegments"===r.field&&"TEXT"===a.type&&"function"==typeof a.getStyledTextSegments)t=a.getStyledTextSegments(["fontName","fontWeight","fontSize","textDecoration","textCase","lineHeight","letterSpacing","fills","textStyleId","fillStyleId","listOptions","indentation","hyperlink"]);else if(r.field in a)t=a[r.field];else throw Error("unsupported")}catch(e){return{kind:"devupLargeValueUnsupported",fileKey:figma.fileKey||"",version:r.version,nodeId:r.nodeId,field:r.field,byteLength:r.byteLength,sha256:r.sha256,errorCode:"DEVUP_FIELD_UNSUPPORTED_BY_UPSTREAM",errorDetail:`${r.field} failed: ${e.message||e}`.slice(0,500)}}let s=null===l?o(JSON.stringify(function e(t,r=new WeakSet,n=0){if(null===t||["string","number","boolean"].includes(typeof t))return t;if(void 0===t)return{$undefined:!0};if("bigint"==typeof t)return{$bigint:t.toString()};if(["function","symbol"].includes(typeof t))return{$unsupported:typeof t};if(n>12)return{$truncated:"max-depth"};if("object"==typeof t&&"parent"in t&&"string"==typeof t.id&&"string"==typeof t.type)return{$nodeId:t.id,$nodeType:t.type};if(Array.isArray(t))return t.map(t=>e(t,r,n+1));if(ArrayBuffer.isView(t))return{$binary:t.constructor.name,byteLength:t.byteLength};if(t instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:t.byteLength};if(r.has(t))return{$circular:!0};r.add(t);let i={};for(let a of Object.keys(t).sort())try{let o=e(t[a],r,n+1);o&&"function"===o.$unsupported||(i[a]=o)}catch(e){i[a]={$error:String(e&&e.message?e.message:e)}}return r.delete(t),i}(t))):l,d=function(e){let t=[0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,0xe49b69c1,0xefbe4786,0xfc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x6ca6351,0x14292967,0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2],r=e.length,n=64*Math.ceil((r+9)/64),i=new Uint8Array(n);i.set(e),i[r]=128;let a=8*r;for(let e=0;e<8;e+=1)i[n-1-e]=255&Math.floor(a/2**(8*e));let o=[0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19],l=(e,t)=>e>>>t|e<<32-t;for(let e=0;e>>3,n=l(r[e-2],17)^l(r[e-2],19)^r[e-2]>>>10;r[e]=r[e-16]+t+r[e-7]+n>>>0}let[n,a,s,d,f,c,u,p]=o;for(let e=0;e<64;e+=1){let i=p+(l(f,6)^l(f,11)^l(f,25))+(f&c^~f&u)+t[e]+r[e]>>>0,o=(l(n,2)^l(n,13)^l(n,22))+(n&a^n&s^a&s)>>>0;p=u,u=c,c=f,f=d+i>>>0,d=s,s=a,a=n,n=i+o>>>0}for(let[e,t]of[n,a,s,d,f,c,u,p].entries())o[e]=o[e]+t>>>0}return o.map(e=>e.toString(16).padStart(8,"0")).join("")}(s);if(s.length!==r.byteLength||d!==r.sha256)throw Error("DEVUP_LARGE_VALUE_CHANGED");let f=Math.max(0,Math.floor(Number(r.offset)||0)),c=Math.min(65536,Math.max(1,Math.floor(Number(r.maxChunkBytes)||8192)));if(f>=s.length)throw Error("DEVUP_LARGE_VALUE_RANGE_INVALID");let u=Math.min(s.length,f+c);return{kind:"devupLargeValueFragment",fileKey:figma.fileKey||"",version:r.version,nodeId:r.nodeId,field:r.field,offset:f,nextOffset:u,byteLength:s.length,sha256:d,dataBase64:function(e){let t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",r="";for(let n=0;n>2],r+=t[(3&i)<<4|a>>4],r+=n+1>6]:"=",r+=n+210||t.some(e=>"string"!=typeof e||e.length>128||!/^(?:[0-9]+:[0-9]+|I[0-9]+:[0-9]+(?:;[0-9]+:[0-9]+)+)$/.test(e))||new Set(t).size!==t.length)throw Error("DEVUP_INVALID_INPUT: nodeIds must be 1..10 distinct Figma IDs");let r=(e,t)=>{try{let r=e[t];return"number"==typeof r&&Number.isFinite(r)?r:null}catch{return null}},n=(e,t)=>{try{let r=e[t];return"string"==typeof r&&r.length<=32&&/^[A-Z_]+$/.test(r)?r:null}catch{return null}},i=[];for(let e of t)try{let t=await figma.getNodeByIdAsync(e);if(!t||"DOCUMENT"===t.type||"PAGE"===t.type){i.push({nodeId:e,status:"unavailable",reason:t?"not-scene-node":"not-found"});continue}let a={width:r(t,"width"),height:r(t,"height"),padding:{top:r(t,"paddingTop"),right:r(t,"paddingRight"),bottom:r(t,"paddingBottom"),left:r(t,"paddingLeft")},gap:r(t,"itemSpacing"),layoutMode:n(t,"layoutMode"),align:{primary:n(t,"primaryAxisAlignItems"),counter:n(t,"counterAxisAlignItems")}},o=null!==a.width&&null!==a.height;i.push({nodeId:e,status:o?"measured":"partial",...a})}catch{i.push({nodeId:e,status:"unavailable",reason:"read-failed"})}return{kind:"devup-measure",schemaVersion:1,coordinateSpace:"figma-logical",nodes:i,partial:i.some(e=>"measured"!==e.status),evidenceLimit:"Null means unavailable or not applicable; no screenshot dimensions, descendants, theme, assets or code were read."}},metadata:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=await figma.getNodeByIdAsync(e.nodeId);if(!t)throw Error("DEVUP_NODE_NOT_FOUND");let r=t;for(;r&&"PAGE"!==r.type;)r=r.parent;r&&"PAGE"===r.type&&await figma.setCurrentPageAsync(r);let n=[];return!function(e){let t=[{node:e,visited:!1}],r=new Map;for(;t.length>0;){let e=t[t.length-1],i=e.node,a="children"in i?i.children:[];if(!e.visited){e.visited=!0;for(let e=a.length-1;e>=0;e-=1)t.push({node:a[e],visited:!1});continue}t.pop();let o=0,l=[];for(let e of a)l.push(e.id),o+=1+(r.get(e.id)||0);r.set(i.id,o),n.push({id:i.id,type:i.type,name:i.name,childrenIds:l,descendantCount:o})}}(t),{fileKey:figma.fileKey||"",version:null,rootId:t.id,nodes:n}},pageCatalog:async function(e){let t=figma.root.children;return{fileKey:figma.fileKey||"",version:null,rootIds:t.map(e=>e.id),nodes:t.map(e=>({id:e.id,type:e.type,fields:{name:e.name,parentId:null,childrenIds:[]},extra:{},fieldErrors:{}})),diagnostics:[]}},referencePng:async function(e){let t,r=e.nodeId;globalThis.__devupReferencePngAnswers||(globalThis.__devupReferencePngAnswers=new Map);let n=globalThis.__devupReferencePngAnswers,i=0;for(let[e,t]of n)Date.now()-t.at>12e4?n.delete(e):i+=t.text.length;if(i>=0xc000000)throw Error(`DEVUP_REFERENCE_PNG_BUSY ${i} characters are held for other reads`);let a=await figma.getNodeByIdAsync(r);if(!a||"function"!=typeof a.exportAsync)throw Error(`DEVUP_NODE_NOT_FOUND ${r}`);let o=a.absoluteRenderBounds||a.absoluteBoundingBox,l=Math.max(o.width,o.height),s=l>8192?8192/l:1,d=(e,t)=>({format:"PNG",constraint:t&&s<1?{type:o.width>=o.height?"WIDTH":"HEIGHT",value:8192}:{type:"SCALE",value:s},contentsOnly:e,useAbsoluteBounds:!1,colorProfile:"SRGB"}),f=e=>(Array.isArray(e)?e:[]).filter(e=>e&&!1!==e.visible),c=(e,t)=>!!e&&!!t&&e.x!!e&&e.x+t<=o.x&&e.y+t<=o.y&&e.x+e.width-t>=o.x+o.width&&e.y+e.height-t>=o.y+o.height,p=e=>`${e.type} ${JSON.stringify(e.name)}`,g=e=>JSON.parse(JSON.stringify(e===figma.mixed?null:e)),h=e=>(void 0===e.opacity||1===e.opacity)&&(!e.blendMode||["PASS_THROUGH","NORMAL"].includes(e.blendMode)),y=[],m=a.parent;for(;m&&"PAGE"!==m.type;)y.unshift(m),m=m.parent;let b=[255,255,255];for(let e of f(m&&m.backgrounds)){if("SOLID"!==e.type)continue;let t=void 0===e.opacity?1:e.opacity;["r","g","b"].forEach((r,n)=>{b[n]=t*e.color[r]*255+(1-t)*b[n]})}let x=0,E=e=>!1!==e.visible&&!e.isMask&&!!c(e.absoluteRenderBounds,o)&&((x+=1)>5e3||"TEXT"===e.type||!!f(e.fills).length||!!f(e.strokes).length||!!f(e.effects).length||"children"in e&&e.children.some(E)),A=e=>h(e)&&u(e.absoluteBoundingBox,0)&&1e-9>Math.abs(e.absoluteTransform[0][1])&&1e-9>Math.abs(e.absoluteTransform[1][0])&&0===Math.max(0,...[e.topLeftRadius,e.topRightRadius,e.bottomRightRadius,e.bottomLeftRadius].filter(e=>"number"==typeof e))&&f(e.fills).some(e=>"NORMAL"===(e.blendMode||"NORMAL")&&(void 0===e.opacity||1===e.opacity)&&("SOLID"===e.type||e.type.startsWith("GRADIENT_")&&e.gradientStops.every(e=>1===e.color.a))),I=[...y,a],S=[],w=[],v=new Map;for(let e=0;eA(e)&&i.slice(0,t).every(h));r.forEach((e,r)=>{e===t||(e.isMask&&!1!==e.visible&&r{let t=await a.exportAsync(d(!1,!0));return{kind:"devupReferencePng",nodeId:r,mimeType:"image/png",byteLength:t.length,data:figma.base64Encode(t),backdrop:N,...e}},T=(e,t)=>{let r=[...new Set(e)];return{approximate:r.join("; "),note:`Drawn as the canvas shows this area: devup-mcp cannot yet compose what get_screenshot draws here (${r.slice(0,3).join("; ")}). Layers reaching into it may show here: ${t}; get_screenshot leaves other layers out.`}},P=e=>{let t=JSON.stringify(e).replace(/[\u0080-\uffff]/g,e=>`\\u${e.charCodeAt(0).toString(16).padStart(4,"0")}`);if(t.length>0x5a00000){if(e.compose){let{compose:r,reaching:n,...i}=e;return P({...i,...T([`the answer would be ${t.length} characters`],n)})}throw Error(`DEVUP_REFERENCE_PNG_TOO_LARGE ${t.length} characters, over the 94371840 one answer is read back up to`)}if(t.length<=6291456)return e;let i=`${r}@${Date.now()}.${Math.random().toString(36).slice(2,10)}`;return n.set(i,{text:t,at:Date.now()}),{kind:"devupReferencePngChunked",nodeId:r,id:i,length:t.length}};if(0===S.length)return P(await O({}));let _=I.some(e=>e.isMask),C=[],M=new Set,k=(e,t)=>{let r=p(e);for(let t of f(e.fills))"IMAGE"===t.type?((t.rotation||f([t.filters]).some(e=>Object.values(e).some(e=>e)))&&C.push(`${r} has an image fill with a rotation or filters`),M.add(t.imageHash)):"SOLID"===t.type||t.type.startsWith("GRADIENT_")||C.push(`${r} is painted with ${t.type}`);for(let t of f(e.effects))["LAYER_BLUR","DROP_SHADOW","INNER_SHADOW","BACKGROUND_BLUR"].includes(t.type)||C.push(`${r} has a ${t.type}`);let n=f(e.strokes);if((e=>{let t=0===f(e.strokes).length?[]:[e.strokeWeight,e.strokeTopWeight,e.strokeRightWeight,e.strokeBottomWeight,e.strokeLeftWeight].filter(e=>"number"==typeof e&&e>0);if(0===t.length)return!1;let r=Math.max(...t),n="OUTSIDE"===e.strokeAlign?0:"CENTER"===e.strokeAlign?r/2:r,i="INSIDE"===e.strokeAlign?0:"CENTER"===e.strokeAlign?r/2:r,a=e.absoluteBoundingBox;return c(a&&{x:a.x-i,y:a.y-i,width:a.width+2*i,height:a.height+2*i},o)&&!u(a,n)})(e)){let t=["FRAME","RECTANGLE","COMPONENT","COMPONENT_SET","INSTANCE","SECTION"].includes(e.type),i=n.every(e=>"SOLID"===e.type||e.type.startsWith("GRADIENT_")),a=Array.isArray(e.dashPattern)&&e.dashPattern.length>0,o="INSIDE"===e.strokeAlign||!e.strokeJoin||"MITER"===e.strokeJoin;t&&i&&!a&&o||C.push(`${r}'s stroke reaches it`)}return{transform:g(e.absoluteTransform),width:e.width,height:e.height,radii:[e.topLeftRadius,e.topRightRadius,e.bottomRightRadius,e.bottomLeftRadius].map(e=>"number"==typeof e?e:0),fills:g(f(e.fills)),opacity:void 0===e.opacity?1:e.opacity,blendMode:e.blendMode||null,clips:!!e.clipsContent,effects:g(f(e.effects)),strokes:g(n),strokeAlign:e.strokeAlign||null,strokeWeights:["strokeTopWeight","strokeRightWeight","strokeBottomWeight","strokeLeftWeight"].map(t=>"number"==typeof e[t]?e[t]:"number"==typeof e.strokeWeight?e.strokeWeight:0)}},R=async e=>{let t=await e.exportAsync(d(!0,e===a));return{data:figma.base64Encode(t),area:e.absoluteRenderBounds||e.absoluteBoundingBox,blendMode:e.blendMode||null,size:[16,20].map(e=>(t[e]<<24|t[e+1]<<16|t[e+2]<<8|t[e+3])>>>0)}},D=async e=>{let t=v.get(e);if(!t)return null;let r=await t.exportAsync({...d(!0,!1),useAbsoluteBounds:!0});return{data:figma.base64Encode(r),area:t.absoluteBoundingBox,luminance:"LUMINANCE"===t.maskType}},L=[],$={},U={};try{for(let e of y){let t=k(e,!1),r=await D(e);r&&(t.mask=r),L.push(t)}let e=e=>f(e.effects).some(e=>"BACKGROUND_BLUR"===e.type);if(w.length>0&&(e(a)||"findOne"in a&&a.findOne(e))){let r="children"in a&&a.children.some(t=>e(t)||"findOne"in t&&!!t.findOne(e));if("children"in a&&!r&&!f(a.strokes).length){for(let e of($.own=k(a,!0),$.children=[],a.children)){if(!1===e.visible||e.isMask||!e.absoluteRenderBounds)continue;let t=await R(e);$.children.push({data:t.data,area:t.area,blendMode:t.blendMode})}t=[Math.ceil(o.width*s-1e-6),Math.ceil(o.height*s-1e-6)]}else C.push(`${p(a)} blurs what lies behind it`)}else if(_)t=[Math.ceil(o.width*s-1e-6),Math.ceil(o.height*s-1e-6)];else{let e=await R(a);$.picture={data:e.data,area:e.area,blendMode:e.blendMode},t=e.size}let r=await D(a);r&&($.mask=r),t&&t[0]*t[1]>8e6&&C.push(`the area is ${t[0]}x${t[1]} pixels`);let n=globalThis.__devupNormalizeImage,i=Date.now()+2e4,l=0;for(let e of M){let t=e&&figma.getImageByHash(e),r=t&&await t.getBytesAsync();if(!r){C.push(`the image ${e} could not be read`);continue}let a=null;if("function"==typeof n){let{width:e,height:o}=await t.getSizeAsync(),l=(...e)=>e.every((e,t)=>r[t]===e),s=l(137,80,78,71)||l(255,216)||l(71,73,70),d=Math.min(1,8192/Math.max(e,o,1)),f=Math.ceil(e*d)*Math.ceil(o*d),c=5*f+65536,u=5*r.length+4*e*o*(d<1?2:1)+4*f+4*c+1048576,p=i-Date.now();if((!s||Math.max(e,o)>8192||r.length>4*e*o+1048576)&&u<=0x20000000&&p>0){let t=await n(r,8192,p,e,o);t&&(r=t,a={width:e,height:o})}}if(l+r.length>0x4000000){C.push("its ancestors' images are over 67108864 bytes");break}l+=r.length,U[e]=a?{data:figma.base64Encode(r),...a}:figma.base64Encode(r)}}catch(e){C.push(`a part could not be exported: ${e instanceof Error?e.message:String(e)}`)}let B=`${S.slice(0,5).join(", ")}${S.length>5?` and ${S.length-5} more`:""}`;return C.length>0?P(await O(T(C,B))):P(await O({reaching:B,compose:{area:o,scale:s,width:t[0],height:t[1],page:b,layers:L,node:$,images:U}}))},referencePngChunk:async function(e){let t=e.largeValue,r=globalThis.__devupReferencePngAnswers,n=r&&r.get(t.id);if(!n||"string"!=typeof n.text)throw Error(`DEVUP_REFERENCE_PNG_ANSWER_GONE ${t.id}`);let i=n.text,a=Math.max(0,Math.floor(Number(t.offset)||0)),o=Math.min(i.length,a+4194304);return o>=i.length&&r.delete(t.id),{kind:"devupReferencePngChunk",id:t.id,offset:a,nextOffset:o,complete:o>=i.length,text:i.slice(a,o)}},search:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=await figma.getNodeByIdAsync(e.nodeId);if(!t)throw Error("DEVUP_NODE_NOT_FOUND");let r=t;for(;r&&"PAGE"!==r.type;)r=r.parent;if(!r||"PAGE"!==r.type)throw Error("DEVUP_PAGE_NOT_FOUND");await figma.setCurrentPageAsync(r);let n=e.search,i=new Set((n.nodeTypes.length?n.nodeTypes:["PAGE","SECTION","FRAME","COMPONENT_SET","COMPONENT"]).map(e=>e.toUpperCase()));function a(e){return e.normalize("NFC").toLocaleLowerCase().replace(/\s/gu,"")}let o=t===r?r.findAll(()=>!0):"findAll"in t?t.findAll(()=>!0):[],l=[t,...o].filter(e=>i.has(e.type)&&"string"==typeof e.name).map(e=>({node:e,score:function(e){let t=n.query.trim();if(""===t||"*"===t)return 100;if(e===n.query)return 400;if("exact"===n.matchKind)return null;let r=a(e),i=a(n.query);if(r===i)return 300;if(r.startsWith(i))return 200;if(r.includes(i))return 100;if("fuzzy"!==n.matchKind)return null;let o=function(e,t){let r=Array.from({length:t.length+1},(e,t)=>t);for(let n=0;nnull!==e.score).sort((e,t)=>t.score-e.score||e.node.name.localeCompare(t.node.name)||e.node.id.localeCompare(t.node.id)).slice(0,n.limit),s=new Map([[r.id,r]]);for(let e of[t,...l.map(e=>e.node)]){let t=e;for(;t&&"DOCUMENT"!==t.type;)s.set(t.id,t),t=t.parent}return{fileKey:figma.fileKey||"",version:null,rootIds:[r.id],nodes:[...s.values()].map(e=>({id:e.id,type:e.type,fields:{name:e.name,parentId:e.parent&&"DOCUMENT"!==e.parent.type?e.parent.id:null,childrenIds:"children"in e?e.children.map(e=>e.id):[]},extra:{},fieldErrors:{}})),diagnostics:[]}},sectionIndex:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=await figma.getNodeByIdAsync(e.nodeId);if(!t)throw Error("DEVUP_NODE_NOT_FOUND");if("SECTION"!==t.type){let e=t.parent,r=new Set;for(;e&&"SECTION"!==e.type&&!r.has(e.id);)r.add(e.id),e=e.parent;let n=e&&"SECTION"===e.type?e.id:null,i=n&&figma.fileKey?`https://www.figma.com/design/${figma.fileKey}?node-id=${n.replace(/:/g,"-")}`:null;throw Error("DEVUP_SECTION_REQUIRED "+JSON.stringify({pluginCode:"DEVUP_SECTION_REQUIRED",stage:"section-index",nodeId:t.id,nodeType:t.type,sectionId:n,nextAction:{tool:"devup_figma_export",how:n?`The url must point to a SECTION. This capture's SECTION is ${n}; select frames with frameIds.`:"The url must point to a SECTION. This node has no ancestor SECTION; choose the intended SECTION in Figma and copy its link.",arguments:i?"FRAME"===t.type&&t.parent===e?{url:i,frameIds:[t.id]}:{url:i}:null,requiredArguments:i?[]:["url"]}}))}let r=2048;function n(e){let t=e.absoluteBoundingBox||{x:"number"==typeof e.x?e.x:0,y:"number"==typeof e.y?e.y:0,width:"number"==typeof e.width?e.width:0,height:"number"==typeof e.height?e.height:0};return[t.x,t.y,t.width,t.height].every(Number.isFinite)?{x:t.x,y:t.y,width:t.width,height:t.height}:null}function i(e,t){if("FRAME"!==e.type||!1===e.visible||!t)return!1;let r=t.width/Math.max(1,t.height);return t.width>=240&&t.width<=1800&&t.height>=300&&t.height<=2e3&&r>=.25&&r<=2.5}function a(e){let t=0;for(let r=0;r=55296&&n<=56319&&r+1[e.id,t.id])),s=[],d=0;for(let e=0;ee.id));for(let r of t.children){if(e.has(r.id))continue;let t=n(r);t&&!1!==r.visible&&s.push({node:r,box:t})}}s.sort((e,t)=>e.box.y-t.box.y||e.box.x-t.box.x||e.node.id.localeCompare(t.node.id));let f=o.length>d||s.length>100,c=s.slice(0,100),u=new Set(c.map(({node:e})=>e.id)),p=new Set(e.rootIds),g={},h=new Map([[t.id,null]]);for(let e=0;e{let r=e.parent;for(;r&&r.id!==t.id&&!u.has(r.id);)r=r.parent;return[e.id,r&&u.has(r.id)?r.id:t.id]})),m=e=>c.filter(({node:t})=>y.get(t.id)===e).map(({node:e})=>e.id),b=n(t);if(!b)throw Error("DEVUP_NODE_BOUNDS_UNAVAILABLE");let x={id:t.id,type:t.type,fields:{name:t.name,parentId:t.parent&&"DOCUMENT"!==t.parent.type?t.parent.id:null,childrenIds:m(t.id),absoluteBoundingBox:b,visible:!1!==t.visible,projectionTruncated:f,nodeScreenIds:g},extra:{},fieldErrors:{}},E=c.length,A=c.map(({node:e,box:t})=>{let o,l=function(e){let t=[e],r=0,n=0;for(let e=0;er}}(e),s=Math.floor(r/E);E-=1;let d=function(e,t){let r=[e],n="",i=0,o=0,l=!1;if(t<=0)return{text:"",state:"budget-exhausted",spent:0};for(let e=0;e=120||o+e>t)return{text:n.trimEnd(),state:o+e>t?"budget-exhausted":"truncated",spent:o};n+=r,i+=1,o+=e}}if("children"in s){let e=Math.max(0,64-r.length);s.children.length>e&&(l=!0),r.push(...s.children.slice(0,e))}}}return{text:n,state:l?"truncated":n?"available":"no-text",spent:o}}(e,s);return r-=d.spent,{id:e.id,type:e.type,fields:{name:"string"==typeof e.name?e.name:"",parentId:y.get(e.id),childrenIds:m(e.id),absoluteBoundingBox:t,visible:!1!==e.visible,breadcrumb:function(e){let t=[],r=e;for(;r&&"DOCUMENT"!==r.type;)"string"==typeof r.name&&r.name&&t.push(r.name),r=r.parent;return t.reverse()}(e),directChildCount:"children"in e?e.children.length:0,textPreview:d.text,...null===(o=function(e){let t=[e],r=0,i=null;for(;t.length&&r++<400;){let e=t.shift();if("visible"in e&&!1===e.visible)continue;let r="children"in e?e.children.filter(e=>!("visible"in e)||!1!==e.visible):[];if(/header|nav|gnb/i.test(e.name||"")&&r.length>=2&&"layoutMode"in e&&"HORIZONTAL"===e.layoutMode){let t=r.map(n);t.every(Boolean)&&(i=Math.max(i||0,t.reduce((e,t)=>e+t.width,0)+(Number(e.itemSpacing)||0)*(t.length-1)))}t.push(...r.slice(0,80))}return i}(e))?{}:{navigationContentWidth:o},textPreviewState:d.state,subtreeNodeCount:l.subtreeNodeCount,estimatedSerializedBytes:l.estimatedSerializedBytes,selectionReasons:[i(e,t)?"screen-like":"explicit-selection-only","inside-section"],estimateTruncated:l.truncated},extra:{},fieldErrors:{}}}),I={fileKey:figma.fileKey||"",version:null,rootIds:[t.id],nodes:[x,...A],diagnostics:[]},S=a(JSON.stringify(I));for(let e=A.length-1;e>=0&&S>19456;e-=1)A[e].fields.textPreview="",A[e].fields.textPreviewState="budget-exhausted",S=a(JSON.stringify(I));let w=()=>I.nodes.length-1,v=A.length;for(;S>19456&&w()>1;){let e=I.nodes.pop();x.fields.projectionTruncated=!0,x.fields.childrenIds=x.fields.childrenIds.filter(t=>t!==e.id),S=a(JSON.stringify(I))}if(S>19456)throw Error("DEVUP_SECTION_INDEX_TOO_LARGE "+JSON.stringify({pluginCode:"DEVUP_SECTION_INDEX_TOO_LARGE",stage:"section-index",responseBytes:S,maxResponseBytes:19456,discoveredCandidates:v,retainedCandidates:w()}));return I},snapshot:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=await figma.getNodeByIdAsync(e.nodeId);if(!t)throw Error("DEVUP_NODE_NOT_FOUND");let r=["absoluteBoundingBox","absoluteRenderBounds","arcData","backgroundStyleId","blendMode","bottomLeftRadius","bottomRightRadius","boundVariables","characters","clipsContent","componentProperties","componentPropertyDefinitions","componentPropertyReferences","constraints","cornerRadius","counterAxisAlignItems","dashPattern","defaultVariant","effectStyleId","effects","fillStyleId","fills","fontName","fontSize","gridColumnAnchorIndex","gridColumnCount","gridColumnGap","gridColumnSizes","gridColumnSpan","gridRowAnchorIndex","gridRowCount","gridRowGap","gridRowSizes","gridRowSpan","gridStyleId","height","inferredAutoLayout","isAsset","isMask","itemSpacing","layoutGrow","layoutMode","layoutWrap","layoutPositioning","layoutSizingHorizontal","layoutSizingVertical","letterSpacing","lineHeight","maxHeight","maxLines","maxWidth","minHeight","minWidth","name","opacity","overflowDirection","paddingBottom","paddingLeft","paddingRight","paddingTop","primaryAxisAlignItems","reactions","rotation","strokeAlign","strokeBottomWeight","strokeLeftWeight","strokeRightWeight","strokeStyleId","strokeTopWeight","strokeWeight","strokes","targetAspectRatio","textAlignHorizontal","textAlignVertical","textAutoResize","textCase","textDecoration","textStyleId","textTruncation","topLeftRadius","topRightRadius","variantProperties","visible","width","x","y"],n=new Set(r),i=["fontName","fontWeight","fontSize","textDecoration","textCase","lineHeight","letterSpacing","fills","textStyleId","fillStyleId","listOptions","indentation","hyperlink"],a=e.snapshot,o=Math.max(0,Math.floor(Number(a.offset)||0)),l=Math.min(a.maxEnvelopeBytes?Math.max(8192,Math.floor(Number(a.maxEnvelopeBytes))):16e3,Math.max(4096,Math.floor(Number(a.maxPayloadBytes)||15e3))),s=Math.min(l-1024,Math.max(512,Math.floor(Number(a.maxFieldBytes)||4096))),d=new Set(["id","type","parent","children","mainComponent","masterComponent","instances"]),f=new Set(["parentId","childrenIds","name","characters","styledTextSegments","boundVariables"]);function c(e,t,r){let n=function(e){let t=[];for(let r=0;r=55296&&n<=56319){let t=r+1=56320&&t<=57343?(n=65536+(n-55296<<10)+(t-56320),r+=1):n=65533}else n>=56320&&n<=57343&&(n=65533);n<128?t.push(n):n<2048?t.push(192|n>>6,128|63&n):n<65536?t.push(224|n>>12,128|n>>6&63,128|63&n):t.push(240|n>>18,128|n>>12&63,128|n>>6&63,128|63&n)}return new Uint8Array(t)}(JSON.stringify(r));return n.length>0x1000000?{$truncated:"max-large-value-bytes",byteLength:n.length}:{$largeValue:{nodeId:e,field:t,byteLength:n.length,sha256:function(e){let t=[0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,0xe49b69c1,0xefbe4786,0xfc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x6ca6351,0x14292967,0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2],r=64*Math.ceil((e.length+9)/64),n=new Uint8Array(r);n.set(e),n[e.length]=128;let i=8*e.length;for(let e=0;e<8;e+=1)n[r-1-e]=255&Math.floor(i/2**(8*e));let a=[0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19],o=(e,t)=>e>>>t|e<<32-t;for(let e=0;e>>3,n=o(r[e-2],17)^o(r[e-2],19)^r[e-2]>>>10;r[e]=r[e-16]+t+r[e-7]+n>>>0}let[i,l,s,d,f,c,u,p]=a;for(let e=0;e<64;e+=1){let n=p+(o(f,6)^o(f,11)^o(f,25))+(f&c^~f&u)+t[e]+r[e]>>>0,a=(o(i,2)^o(i,13)^o(i,22))+(i&l^i&s^l&s)>>>0;p=u,u=c,c=f,f=d+n>>>0,d=s,s=l,l=i,i=n+a>>>0}for(let[e,t]of[i,l,s,d,f,c,u,p].entries())a[e]=a[e]+t>>>0}return a.map(e=>e.toString(16).padStart(8,"0")).join("")}(n),cursor:{nextOffset:0,maxChunkBytes:12288}}}}function u(e){return function(e){let t=0;for(let r=0;r=55296&&n<=56319&&r+112)return{$truncated:"max-depth"};if("object"==typeof t&&"parent"in t&&"string"==typeof t.id&&"string"==typeof t.type)return{$nodeId:t.id,$nodeType:t.type};if(Array.isArray(t))return t.map(t=>e(t,r,n+1));if(ArrayBuffer.isView(t))return{$binary:t.constructor.name,byteLength:t.byteLength};if(t instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:t.byteLength};if(r.has(t))return{$circular:!0};r.add(t);let i={};for(let a of Object.keys(t).sort())try{let o=e(t[a],r,n+1);o&&"function"===o.$unsupported||(i[a]=o)}catch(e){i[a]={$error:String(e&&e.message?e.message:e)}}return r.delete(t),i}(r),a=u(i);if(a<=s)return i;let o=c(e,t,i);return o.$truncated&&(n[t]=`DEVUP_FIELD_VALUE_UNSUPPORTED:${a}>16777216`),o}let g=new Map;async function h(e,t,r){if("INSTANCE"!==e.type||"function"!=typeof e.getMainComponentAsync)return;g.has(e.id)||g.set(e.id,(async()=>{try{let t=await e.getMainComponentAsync();return{fields:{mainComponentId:t?t.id:null,mainComponentName:t?t.name:null,mainComponentSetId:t&&t.parent&&"COMPONENT_SET"===t.parent.type?t.parent.id:null}}}catch(e){return{error:String(e&&e.message?e.message:e)}}})());let n=await g.get(e.id);n.error?r.mainComponentId=n.error:Object.assign(t,n.fields)}async function y(e){let t={},a={},o={};for(let i of(t.parentId=e.parent?e.parent.id:null,e.parent&&("PAGE"===e.parent.type||"SECTION"===e.parent.type||"COMPONENT_SET"===e.parent.type)&&(t.parentType=e.parent.type,"SECTION"===e.parent.type&&(t.parentName=e.parent.name)),t.childrenIds="children"in e?e.children.map(e=>e.id):[],!("overflowDirection"in e)&&["FRAME","COMPONENT","INSTANCE","COMPONENT_SET"].includes(e.type)&&(t.overflowDirection=null),function(e){let t=new Set,n=e;for(;n&&n!==Object.prototype;){for(let e of Object.getOwnPropertyNames(n))t.add(e);n=Object.getPrototypeOf(n)}for(let n of r)try{n in e&&t.add(n)}catch(e){}return[...t].sort()}(e)))if(!(d.has(i)||i.startsWith("_")||"componentPropertyDefinitions"===i&&"COMPONENT"===e.type&&e.parent&&"COMPONENT_SET"===e.parent.type))try{let r=e[i];if("function"==typeof r)continue;let l=p(e.id,i,r,o);(n.has(i)?t:a)[i]=l}catch(e){o[i]=String(e&&e.message?e.message:e)}if("TEXT"===e.type&&"function"==typeof e.getStyledTextSegments)try{t.styledTextSegments=p(e.id,"styledTextSegments",e.getStyledTextSegments(i),o)}catch(e){o.styledTextSegments=String(e&&e.message?e.message:e)}return await h(e,t,o),{id:e.id,type:e.type,fields:t,extra:a,fieldErrors:o}}let m=[],b=[t];for(;b.length;){let e=b.shift();m.push(e),"children"in e&&b.push(...e.children)}let x=[],E=l-1024,A=2;for(let e=o;eNumber(e.protected)-Number(t.protected)||t.byteLength-e.byteLength),n)){if(r<=t)break;let n=c(e.id,i.name,e[i.sectionName][i.name]);e[i.sectionName][i.name]=n,n.$truncated&&(e.fieldErrors[i.name]=`DEVUP_FIELD_VALUE_UNSUPPORTED:${i.byteLength}>16777216`),r=u(e)}return e}(await y(m[e]),E),r=u(t)+ +!!x.length;if(x.length&&A+r>E)break;x.push(t),A+=r}let I=Math.min(m.length,o+x.length);return x.push({id:"__DEVUP_SNAPSHOT_CURSOR__",type:"DEVUP_INTERNAL",fields:{offset:o,nextOffset:I,complete:I>=m.length,totalNodes:m.length},extra:{},fieldErrors:{}}),{fileKey:figma.fileKey||"",version:null,rootIds:[t.id],nodes:x,diagnostics:[]}},usedResources:async function(e){let t=e.resources;function r(e,t=new WeakSet,n=0){if(null===e||["string","number","boolean"].includes(typeof e))return e;if(void 0===e)return{$undefined:!0};if("bigint"==typeof e)return{$bigint:e.toString()};if(["function","symbol"].includes(typeof e))return{$unsupported:typeof e};if(n>12)return{$truncated:"max-depth"};if("object"==typeof e&&"parent"in e&&"string"==typeof e.id&&"string"==typeof e.type)return{$nodeId:e.id,$nodeType:e.type};if(Array.isArray(e))return e.map(e=>r(e,t,n+1));if(ArrayBuffer.isView(e))return{$binary:e.constructor.name,byteLength:e.byteLength};if(e instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:e.byteLength};if(t.has(e))return{$circular:!0};t.add(e);let i={},a=new Set(Object.keys(e)),o=e;for(;o&&o!==Object.prototype;){for(let e of Object.getOwnPropertyNames(o))a.add(e);o=Object.getPrototypeOf(o)}for(let o of[...a].sort())if(!(o.startsWith("_")||["parent","children","consumers"].includes(o)))try{let a=r(e[o],t,n+1);a&&"function"===a.$unsupported||(i[o]=a)}catch(e){i[o]={$error:"unavailable"}}return t.delete(e),i}let n=new Map,i="ok";try{for(let e of(await figma.variables.getLocalVariablesAsync()))n.set(e.id,e)}catch(e){i="unavailable"}function a(e){return Object.values(e.valuesByMode||{}).filter(e=>e&&"VARIABLE_ALIAS"===e.type&&"string"==typeof e.id&&e.id).map(e=>e.id)}async function o(e){let t=n.get(e);if(t)return{value:r(t),collectionId:t.variableCollectionId,aliasIds:a(t)};try{let t=await figma.variables.getVariableByIdAsync(e);return t?{value:r(t),collectionId:t.variableCollectionId,aliasIds:a(t)}:{unresolved:{id:e,kind:"variable",reason:"notInFileAndLookupEmpty"}}}catch(t){return{unresolved:{id:e,kind:"variable",reason:"notInFileAndLookupThrew",error:String(t&&t.message||t).slice(0,300)}}}}let l=await Promise.all(t.variableIds.map(o)),s=new Set(t.variableIds),d=l.flatMap(e=>e.aliasIds||[]);for(;d.length>0;){let e=[...new Set(d)].filter(e=>!s.has(e)).sort();for(let t of e)s.add(t);let t=await Promise.all(e.map(o));l.push(...t),d=t.flatMap(e=>e.aliasIds||[])}let f=[...new Set(l.flatMap(e=>e.collectionId?[e.collectionId]:[]))].sort(),c=await Promise.all(f.map(async e=>{try{let t=await figma.variables.getVariableCollectionByIdAsync(e);return t?[r(t)]:[]}catch(e){return[]}})),u=await Promise.all(t.styles.map(async e=>{try{let t=await figma.getStyleByIdAsync(e.id);if(!t)return{unresolved:{id:e.id,kind:"style",reason:"notFoundOrUnavailable"}};return{value:{...r(t),styleType:e.styleType,value:r("PAINT"===e.styleType?t.paints:"EFFECT"===e.styleType?t.effects:"GRID"===e.styleType?t.layoutGrids:t)}}}catch(t){return{unresolved:{id:e.id,kind:"style",reason:"notFoundOrUnavailable",error:String(t&&t.message||t).slice(0,300)}}}}));return{collections:c.flat(),variables:l.flatMap(e=>e.value?[e.value]:[]),styles:u.flatMap(e=>e.value?[e.value]:[]),usedVariableIds:t.variableIds,usedStyleIds:t.styles.map(e=>e.id),localVariableListing:i,localVariableCount:n.size,unresolved:[...l,...u].flatMap(e=>e.unresolved?[e.unresolved]:[])}},variableCatalog:async function(e){let[t,r,n,i,a]=await Promise.all([figma.variables.getLocalVariableCollectionsAsync(),figma.getLocalPaintStylesAsync(),figma.getLocalTextStylesAsync(),figma.getLocalEffectStylesAsync(),figma.getLocalGridStylesAsync()]),o=["PAINT","TEXT","EFFECT","GRID"];return{collections:t.map(e=>(function e(t,r=new WeakSet,n=0){if(null===t||["string","number","boolean"].includes(typeof t))return t;if(void 0===t)return{$undefined:!0};if("bigint"==typeof t)return{$bigint:t.toString()};if(["function","symbol"].includes(typeof t))return{$unsupported:typeof t};if(n>12)return{$truncated:"max-depth"};if(Array.isArray(t))return t.map(t=>e(t,r,n+1));if(r.has(t))return{$circular:!0};r.add(t);let i={},a=new Set(Object.keys(t)),o=t;for(;o&&o!==Object.prototype;){for(let e of Object.getOwnPropertyNames(o))a.add(e);o=Object.getPrototypeOf(o)}for(let o of[...a].sort())if(!o.startsWith("_"))try{let a=e(t[o],r,n+1);a&&"function"===a.$unsupported||(i[o]=a)}catch(e){i[o]={$error:String(e&&e.message?e.message:e)}}return r.delete(t),i})(e)),variableIds:[...new Set(t.flatMap(e=>e.variableIds))].sort(),styles:[r,n,i,a].flatMap((e,t)=>e.map(e=>({id:e.id,styleType:o[t]}))).sort((e,t)=>e.id.localeCompare(t.id)),localComplete:!0,usedRemoteComplete:!1}},variables:async function(e){let t=e.resources;function r(e,t=new WeakSet,n=0){if(null===e||["string","number","boolean"].includes(typeof e))return e;if(void 0===e)return{$undefined:!0};if("bigint"==typeof e)return{$bigint:e.toString()};if(["function","symbol"].includes(typeof e))return{$unsupported:typeof e};if(n>12)return{$truncated:"max-depth"};if("object"==typeof e&&"parent"in e&&"string"==typeof e.id&&"string"==typeof e.type)return{$nodeId:e.id,$nodeType:e.type};if(Array.isArray(e))return e.map(e=>r(e,t,n+1));if(ArrayBuffer.isView(e))return{$binary:e.constructor.name,byteLength:e.byteLength};if(e instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:e.byteLength};if(t.has(e))return{$circular:!0};t.add(e);let i={},a=new Set(Object.keys(e)),o=e;for(;o&&o!==Object.prototype;){for(let e of Object.getOwnPropertyNames(o))a.add(e);o=Object.getPrototypeOf(o)}for(let o of[...a].sort())if(!(o.startsWith("_")||["parent","children","consumers"].includes(o)))try{let a=r(e[o],t,n+1);a&&"function"===a.$unsupported||(i[o]=a)}catch(e){i[o]={$error:String(e&&e.message?e.message:e)}}return t.delete(e),i}let[n,i]=await Promise.all([Promise.all(t.variableIds.map(e=>figma.variables.getVariableByIdAsync(e))),Promise.all(t.styles.map(e=>figma.getStyleByIdAsync(e.id)))]),a=new Map(t.styles.map(e=>[e.id,e])),o=await Promise.all(i.filter(Boolean).map(async e=>{let t=a.get(e.id),n=t.styleType;if(Number.isInteger(t.consumerStart)&&Number.isInteger(t.consumerEnd)){let i=await e.getStyleConsumersAsync();return{id:e.id,styleType:n,$consumerStart:t.consumerStart,$consumerEntries:i.slice(t.consumerStart,t.consumerEnd).map(e=>[e.node.id,e.node.type,r(e.fields)])}}let i=await e.getStyleConsumersAsync();return{...r(e),styleType:n,$consumerCount:i.length,value:r("PAINT"===n?e.paints:"EFFECT"===n?e.effects:"GRID"===n?e.layoutGrids:e)}}));return{variables:n.filter(Boolean).map(e=>r(e)),styles:o}}},t=new Map,r=0;globalThis.__devupNormalizeImage=(e,n,i,a,o)=>new Promise(l=>{r+=1;let s=`decode-${r}`,d=setTimeout(()=>{t.delete(s),l(null)},i);t.set(s,e=>{clearTimeout(d),t.delete(s),l(e)}),figma.ui.postMessage({kind:"devup-decode",id:s,bytes:e,maxEdge:n,width:a,height:o})});let n=globalThis;function i(){let e=figma.currentPage,t=e.selection;return t.length>0&&(n.__devupLastSelection=t.slice(0,20).map(e=>({id:e.id,name:e.name.slice(0,256),type:e.type})),n.__devupLastSelectionPage={id:e.id,name:e.name.slice(0,256)}),{zoom:Number.isFinite(figma.viewport?.zoom)?figma.viewport.zoom:null,observedAt:Date.now(),lastKnownSelection:n.__devupLastSelection??null,lastKnownPage:n.__devupLastSelectionPage??null,currentPage:{id:e.id,name:e.name.slice(0,256)},selection:t.slice(0,20).map(e=>({id:e.id,name:e.name.slice(0,256),type:e.type})),selectionCount:t.length}}function a(){let e={kind:"devup-context",...i()};figma.ui.postMessage(e)}async function o(t){let r=e[t.script];if(!r)return{kind:"devup-result",requestId:t.requestId,error:`DEVUP_BRIDGE_UNKNOWN_SCRIPT: ${t.script} (아는 스크립트: ${Object.keys(e).join(", ")})`};try{var n;"bridgeContext"===t.script&&i();let e=await r({nodeId:(n=t.params).nodeId??"",rootIds:n.rootIds??[],snapshot:n.snapshot??{},search:n.search??{},explore:n.explore??{},resources:n.resources??{variableIds:[],styles:[]},largeValue:n.largeValue??{},asset:n.asset??{},theme:n.theme??{offset:0}});return{kind:"devup-result",requestId:t.requestId,data:e}}catch(e){return{kind:"devup-result",requestId:t.requestId,error:e instanceof Error?e.message:String(e)}}}let l=[],s=!1,d=null,f=new Set;async function c(){if(!s){s=!0;try{for(;l.length>0;){let e=l.shift();d=e.requestId;let t=await o(e).catch(t=>({kind:"devup-result",requestId:e.requestId,error:t instanceof Error?t.message:String(t)}));d=null,f.delete(e.requestId)||figma.ui.postMessage(t)}}finally{s=!1}}}let u=null;function p(){u??(u=(async()=>{try{await figma.loadAllPagesAsync(),await Promise.all([figma.variables.getLocalVariableCollectionsAsync(),figma.variables.getLocalVariablesAsync(),figma.getLocalPaintStylesAsync(),figma.getLocalTextStylesAsync(),figma.getLocalEffectStylesAsync(),figma.getLocalGridStylesAsync()])}catch{}finally{u=null}})())}figma.showUI(__html__,{width:320,height:220}),p(),figma.on("selectionchange",a),figma.on("currentpagechange",a),figma.ui.onmessage=e=>{if("object"==typeof e&&null!==e){if("devup-ready"===e.kind){let e={kind:"devup-status",fileKey:figma.fileKey??null,fileName:figma.root.name,port:1993,...i()};figma.ui.postMessage(e);return}if("devup-job"===e.kind){l.push(e),c();return}if("devup-cancel"===e.kind){var r;let t;return void(r=e.requestId,(t=l.findIndex(e=>e.requestId===r))>=0?l.splice(t,1):d===r&&f.add(r))}if("devup-decoded"===e.kind)return void t.get(e.id)?.(e.png??null);"devup-in-view"===e.kind&&p()}}})(); \ No newline at end of file From 3521c9e975a71a2030ebd1c6ada82c53719771e2 Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 17:38:34 +0900 Subject: [PATCH 050/149] fix(screen-plan): keep planning theme projection outside router (38) --- crates/devup-mcp/src/server/mod_parts/acquisition.rs | 4 +--- crates/devup-mcp/src/server/screen_plan/remap.rs | 6 ++++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/crates/devup-mcp/src/server/mod_parts/acquisition.rs b/crates/devup-mcp/src/server/mod_parts/acquisition.rs index bc3ad6be..71238680 100644 --- a/crates/devup-mcp/src/server/mod_parts/acquisition.rs +++ b/crates/devup-mcp/src/server/mod_parts/acquisition.rs @@ -494,9 +494,7 @@ impl DevupServer { request.resource_scope = ResourceScope::File; let parts = self.run_direct(request, &PendingOperation::Collect, None).await?; if let Some(variables) = parts.variables { - let snapshot = devup_mcp_devup_ui::theme::variable_snapshot_from_result(&variables)?; - let theme = devup_mcp_devup_ui::theme::generate_devup_json(&snapshot, devup_mcp_devup_ui::theme::ThemeScope::File)?; - explored["figmaTheme"] = serde_json::from_str(&theme.json).map_err(|error| DevupError::new(ErrorCode::DevupInvalidInput, error.to_string(), false))?; + explored["figmaTheme"] = screen_plan::planning_theme(&variables)?; } } let candidates = explored["candidates"] diff --git a/crates/devup-mcp/src/server/screen_plan/remap.rs b/crates/devup-mcp/src/server/screen_plan/remap.rs index bd03697b..0e1c1c0f 100644 --- a/crates/devup-mcp/src/server/screen_plan/remap.rs +++ b/crates/devup-mcp/src/server/screen_plan/remap.rs @@ -138,3 +138,9 @@ fn theme_sync_queue(plan: &Value, root: &str) -> Vec { "exportCall":{"tool":"devup_figma_export", "arguments":{"url":plan["source"]["url"], "scope":"file", "outputs":["devupJson"], "devupJsonMode":"missing-only", "projectRoot":theme_root}}})] } + +pub(super) fn planning_theme(variables: &devup_mcp_figma::UpstreamResult) -> Result { + let snapshot = devup_mcp_devup_ui::theme::variable_snapshot_from_result(variables)?; + let theme = devup_mcp_devup_ui::theme::generate_devup_json(&snapshot, devup_mcp_devup_ui::theme::ThemeScope::File)?; + serde_json::from_str(&theme.json).map_err(|error| invalid(error.to_string())) +} From 4436de245ec61581e40e6d21e1cfa524751fcd5d Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 17:38:34 +0900 Subject: [PATCH 051/149] fix(screen-plan): retain router source limit after progress attachment (33) --- .../src/server/mod_parts/exploration_and_responses.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/crates/devup-mcp/src/server/mod_parts/exploration_and_responses.rs b/crates/devup-mcp/src/server/mod_parts/exploration_and_responses.rs index 871a4fb1..706ad08a 100644 --- a/crates/devup-mcp/src/server/mod_parts/exploration_and_responses.rs +++ b/crates/devup-mcp/src/server/mod_parts/exploration_and_responses.rs @@ -418,11 +418,6 @@ fn with_project_checks( .map(|tsx| (key.to_owned(), tsx.to_owned())) }) .collect(); - // A Section export answers under `frames`, one entry per selected frame, - // and nothing at the top level. Reading only the top level checked the - // single-node `url` case and silently skipped every `frameIds` call — - // which is every Section workflow, and so every screen of the Braillify - // Studio handover this exists for. if let Some(frames) = result.get("frames").and_then(Value::as_array) { for frame in frames { let node = frame From 38703a885c0433e8da7a46758dd70853d54731f7 Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 17:38:34 +0900 Subject: [PATCH 052/149] fix(screen-plan): retain page inventory through structural groups (12) --- crates/devup-mcp-figma/src/scripts/explore.js | 3 +-- plugin/dist/code.js | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/devup-mcp-figma/src/scripts/explore.js b/crates/devup-mcp-figma/src/scripts/explore.js index 0e0720ed..3f44a79d 100644 --- a/crates/devup-mcp-figma/src/scripts/explore.js +++ b/crates/devup-mcp-figma/src/scripts/explore.js @@ -206,8 +206,7 @@ if (descendantScope && "children" in scopeAnchor) { } if ("children" in node) { for (const child of node.children) { - if (requestedPage && node.type !== "SECTION") continue; - if (requestedPage && !["SECTION", "FRAME", "COMPONENT", "INSTANCE", "COMPONENT_SET"].includes(child.type)) continue; + if (requestedPage && ["FRAME", "COMPONENT", "INSTANCE", "COMPONENT_SET"].includes(node.type)) continue; nestedQueue.push({ node: child, ancestors: [...ancestors, node] }); } } diff --git a/plugin/dist/code.js b/plugin/dist/code.js index 86376ae2..b4cf6639 100644 --- a/plugin/dist/code.js +++ b/plugin/dist/code.js @@ -1 +1 @@ -(()=>{"use strict";let e={assets:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=e.asset;function r(e){let t=[0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,0xe49b69c1,0xefbe4786,0xfc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x6ca6351,0x14292967,0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2],r=64*Math.ceil((e.length+9)/64),n=new Uint8Array(r);n.set(e),n[e.length]=128;let i=8*e.length;for(let e=0;e<8;e+=1)n[r-1-e]=255&Math.floor(i/2**(8*e));let a=[0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19],o=(e,t)=>e>>>t|e<<32-t;for(let e=0;e>>3,n=o(r[e-2],17)^o(r[e-2],19)^r[e-2]>>>10;r[e]=r[e-16]+t+r[e-7]+n>>>0}let[i,l,s,d,f,c,u,p]=a;for(let e=0;e<64;e+=1){let n=p+(o(f,6)^o(f,11)^o(f,25))+(f&c^~f&u)+t[e]+r[e]>>>0,a=(o(i,2)^o(i,13)^o(i,22))+(i&l^i&s^l&s)>>>0;p=u,u=c,c=f,f=d+n>>>0,d=s,s=l,l=i,i=n+a>>>0}for(let[e,t]of[i,l,s,d,f,c,u,p].entries())a[e]=a[e]+t>>>0}return a.map(e=>e.toString(16).padStart(8,"0")).join("")}async function n(e){try{let t=await figma.getNodeByIdAsync(e);if(t)return t}catch(r){if(!e.startsWith("I")||!e.includes(";"))throw r;try{return await t()}catch(e){throw Error(`${r.message||r}; nested lookup fallback: ${e.message||e}`)}}if(!e.startsWith("I")||!e.includes(";"))return null;return t();async function t(){let t=await figma.getNodeByIdAsync(e.slice(1).split(";")[0]),r=t?[t]:[],n=new Set;for(;r.length;){let t=r.pop();if(t.id===e)return t;if(!n.has(t.id)){if(n.add(t.id),n.size>1e4)throw Error("nested lookup exceeds 10000 nodes");Array.isArray(t.children)&&r.push(...t.children)}}throw Error(`nested node ${e} not found in its instance`)}}async function i(e,t,r=85e3,n=()=>{}){let a,o=Date.now();try{let i=await Promise.race([e.exportAsync(t),new Promise((e,t)=>{a=setTimeout(()=>t(Error(`DEVUP_ASSET_TIMEOUT: native export exceeded ${r} ms`)),r)})]);return n("native-export",Date.now()-o),i}catch(i){if(n("native-export",Date.now()-o,String(i.message||i).slice(0,500)),"SVG_STRING"!==t.format)throw i;let r=Date.now();try{let t=function(e){let t=e=>"number"==typeof e&&Number.isFinite(e),r=(e,t)=>{throw Error(`geometry fallback unsupported: ${e.type||"unknown"} ${e.id||""}: ${t}`)},n=0,i=0;function a(e,i,a){(void 0!==a&&!Array.isArray(a)||void 0===a&&Array.isArray(i)&&i.length)&&r(e,"unresolved paint requires native export");let o=Array.isArray(a)?a.filter(e=>!1!==e.visible):[];return o.length?(Array.isArray(i)&&i.length||r(e,"paint has no rendered geometry"),o.map(a=>{("SOLID"!==a.type||a.blendMode&&"NORMAL"!==a.blendMode)&&r(e,"paint type or blending requires native export");let o=a.color;o&&[o.r,o.g,o.b].every(e=>t(e)&&e>=0&&e<=1)||r(e,"invalid solid color");let l=void 0===a.opacity?1:a.opacity;return(!t(l)||l<0||l>1)&&r(e,"invalid paint opacity"),["NONZERO","EVENODD"].map(t=>{let a=i.filter(e=>e.windingRule===t);if(!a.length)return"";for(let t of a)"string"==typeof t.data&&t.data.trim()||r(e,"empty path"),n++;return`"']/g,e=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[e])}" fill="rgb(${100*o.r}%,${100*o.g}%,${100*o.b}%)" fill-opacity="${l}" fill-rule="${"EVENODD"===t?"evenodd":"nonzero"}"/>`}).join("")}).join("")):""}let o=e.absoluteRenderBounds,l=e.absoluteTransform;(!o||![o.x,o.y,o.width,o.height].every(t)||o.width<=0||o.height<=0)&&r(e,"missing render bounds"),(!Array.isArray(l)||2!==l.length||!l.every(e=>Array.isArray(e)&&3===e.length&&e.every(t))||0!==l[0][1]||0!==l[1][0]||l[0][0]<=0||l[1][1]<=0)&&r(e,"root rotation, reflection or shear requires native export");let s=(o.x-l[0][2])/l[0][0],d=(o.y-l[1][2])/l[1][1],f=o.width/l[0][0],c=o.height/l[1][1],u=function e(n,o){if(++i>1e4&&r(n,"subtree exceeds 10000 nodes"),n.isMask&&r(n,"mask requires native export"),!1===n.visible||0===n.opacity)return"";n.clipsContent&&r(n,"clipping requires native export"),Array.isArray(n.effects)&&n.effects.some(e=>!1!==e.visible)&&r(n,"effect requires native export"),n.blendMode&&!["NORMAL","PASS_THROUGH"].includes(n.blendMode)&&r(n,"layer blending requires native export");let l=void 0===n.opacity?1:n.opacity;(!t(l)||l<0||l>1)&&r(n,"invalid layer opacity");let s="";if(!o){let e=n.relativeTransform;Array.isArray(e)&&2===e.length&&e.every(e=>Array.isArray(e)&&3===e.length&&e.every(t))||r(n,"missing relative transform"),s=` transform="matrix(${e[0][0]} ${e[1][0]} ${e[0][1]} ${e[1][1]} ${e[0][2]} ${e[1][2]})"`}let d="";if(["GROUP","FRAME","INSTANCE","COMPONENT"].includes(n.type))(Array.isArray(n.fills)&&n.fills.some(e=>!1!==e.visible)||Array.isArray(n.strokes)&&n.strokes.some(e=>!1!==e.visible))&&r(n,"container paint requires native export"),Array.isArray(n.children)||r(n,"missing children"),d=(!0===n.itemReverseZIndex?[...n.children].reverse():n.children).map(t=>e(t,!1)).join("");else{for(let e of(["VECTOR","BOOLEAN_OPERATION","RECTANGLE","ELLIPSE","POLYGON","STAR","LINE"].includes(n.type)||r(n,"node type requires native export"),Array.isArray(n.strokes)&&n.strokes.some(e=>!1!==e.visible)&&"CENTER"!==n.strokeAlign&&r(n,"non-center stroke alignment requires native export"),Array.isArray(n.dashPattern)&&n.dashPattern.length&&r(n,"dashed stroke requires native export"),[n.fillGeometry,n.strokeGeometry]))void 0!==e&&(!Array.isArray(e)||e.some(e=>!["NONZERO","EVENODD"].includes(e.windingRule)))&&r(n,"unknown winding rule");d=a(n,n.fillGeometry,n.fills)+a(n,n.strokeGeometry,n.strokes)}return`${d}`}(e,!0);return n||r(e,"no painted geometry"),`${u}`}(e);return n("geometry-fallback",Date.now()-r),t}catch(e){throw Error(`native export: ${i.message||i}; ${e.message||e}`)}}finally{clearTimeout(a)}}let a=Date.now(),o="lookup",l="unknown",s=Math.min(6e5,Math.max(1e3,Number(t.timeoutMs)||9e4)),d={method:"native",stages:[]};function f(e,t,r){d.stages.push({stage:e,elapsedMs:t}),r&&(d.nativeError=r),"geometry-fallback"===e&&(d.method="geometry")}async function c(e){let t;try{return await Promise.race([e(),new Promise((e,r)=>{t=setTimeout(()=>r(Error(`DEVUP_ASSET_TIMEOUT: ${o} exceeded ${s} ms asset budget`)),Math.max(0,s-(Date.now()-a)))})])}finally{clearTimeout(t)}}function u(e){let r=e&&"string"==typeof e.message?e.message:String(e),n="string"==typeof figma.editorType?` (editor: ${figma.editorType})`:"";return`${o} failed after ${Date.now()-a} ms${n} (node: ${t.nodeId}, type: ${l}): ${r}`.slice(0,500)}function p(e,r){return e&&d.renderInfo&&(d.renderInfo.attemptedRepresentation=d.renderInfo.representation,d.renderInfo.representation="not-exported",d.renderInfo.renderedAs="not-exported",d.renderInfo.effectsBakedIntoAsset=!1,d.renderInfo.fidelity="unverified"),{kind:"devupAssetExport",fileKey:figma.fileKey||"",version:t.version,assetId:t.assetId,nodeId:t.nodeId,field:t.field,imageHash:t.imageHash,format:t.format,scale:t.scale,renderMode:h,usageAspectRatio:t.usageAspectRatio??null,maxWidth:t.maxWidth??null,srcset:!0===t.srcset,visibleScopeNodeId:t.visibleScopeNodeId??null,status:"failed",byteLength:null,sha256:null,errorCode:e,exportDiagnostics:d,...r?{errorDetail:r}:{}}}let g="string"==typeof t.field&&t.field.startsWith("$original-image/fills/"),h=void 0===t.renderMode?g?"fill":["svg","pdf"].includes(t.format)?"node":"original-source":t.renderMode,y=["auto","original-source"].includes(h)?"auto"===h?"node":"fill":h;function m(e,r){let n=e=>"number"==typeof e&&Number.isFinite(e)&&e>0,i=Array.isArray(e.effects)?e.effects:null,a=[],o=e=>[...e].slice(0,128).join(""),l=["string"==typeof e.name?o(e.name):"layer"],s=new Set,d=e;for(let t=e.parent;t&&a.length<32&&!s.has(t);t=t.parent)s.add(t),l.length<4&&"string"==typeof t.name&&t.name.length&&!["PAGE","DOCUMENT"].includes(t.type)&&l.push(o(t.name)),a.push({type:t.type||"unknown",clipsContent:t.clipsContent??null,isMask:t.isMask??null,effects:Array.isArray(t.effects)?t.effects:null,opacity:t.opacity??null,blendMode:t.blendMode??null,visible:t.visible??null,siblingMaskPresent:Array.isArray(t.children)?t.children.some(e=>e!==d&&!0===e.isMask):null}),d=t;return{sourceKind:r?"image-fill":t.imageHash?"image-node":"vector-node",renderedAs:"fill"===y?"raw-fill":"node-render",representation:"fill"===y?"original-fill":"native-node",layerHierarchy:l.reverse(),scope:"fill"===y?"selected-paint-original-bytes":"exact-node-subtree",exportSettings:"node"===y?{contentsOnly:!0,useAbsoluteBounds:!1}:null,requestedScale:t.scale,scaleSemantics:"fill"===y?"intrinsic-original-bytes-no-resize":["png","jpg"].includes(t.format)?"native-node-SCALE-constraint":"not-applied-to-vector-document",nodeSize:n(e.width)&&n(e.height)?{width:e.width,height:e.height,aspectRatio:e.width/e.height}:null,outputSize:null,dimensionEvidence:"unknown-until-byte-inspection",aspectRatioComparison:"unknown",scaleMode:r?r.scaleMode??null:null,imageTransform:r?r.imageTransform??null:null,visibleSourceRatio:null,visibleSourceRatioEvidence:"not-computed",effects:i,extraFills:Array.isArray(e.fills)?Math.max(0,e.fills.filter(e=>!1!==e.visible).length-!!r):null,effectsBakedIntoAsset:"node"===y&&["png","jpg"].includes(t.format)&&null!==i,bakedEffects:"node"===y&&["png","jpg"].includes(t.format)&&i?i.filter(e=>!1!==e.visible):[],instanceOverride:"unknown",instanceOverrideEvidence:"no-component-paint-comparison",sourceProperties:{scaleMode:r?r.scaleMode??null:null,imageTransform:r?r.imageTransform??null:null,rotation:r?r.rotation??null:null,filters:r?r.filters??null:null,opacity:r?r.opacity??null:null,effects:i,blurEffects:i?i.filter(e=>!1!==e.visible&&String(e.type).includes("BLUR")):null,isMask:e.isMask??null,clipsContent:e.clipsContent??null,ancestors:a},unappliedProperties:"fill"===y?["scaleMode","imageTransform","rotation","filters","paintOpacity","nodeGeometry","nodeTransforms","nodeEffectsIncludingBlur","nodeAndAncestorMasks","ancestorClipsAndEffects"]:[],warnings:"fill"===y?["original-fill-properties-unapplied","aspect-ratio-unknown"]:["ancestor-canvas-appearance-unverified","aspect-ratio-unknown"],fidelity:"unverified"}}function b(e,t){let r=0;for(let n=e;n&&r++<32;n=n.parent){if("INSTANCE"===n.type&&Array.isArray(n.overrides)&&n.overrides.find(t=>t.id===e.id&&Array.isArray(t.overriddenFields)&&t.overriddenFields.includes("fills"))){t.instanceOverride=!0,t.instanceOverrideEvidence={kind:"figma-instance-overrides",instanceId:n.id,nodeId:e.id,overriddenFields:["fills"]};return}if(["PAGE","DOCUMENT"].includes(n.type))break}}try{let e;if(!["original-source","fill","node","visible","auto"].includes(h))return p("DEVUP_ASSET_RENDER_MODE_UNSUPPORTED");if(!g&&"fill"===h&&(!String(t.field).startsWith("fills/")||!["png","jpg","webp"].includes(t.format)||1!==t.scale))return p("DEVUP_ASSET_FILL_SETTINGS_UNSUPPORTED","Original fill requires fills/N, png or jpg matching its codec, and scale 1; no resizing or transcoding.");if((g||["fill","original-source"].includes(h))&&"bridge"!==t.transport)return p(g?"DEVUP_ORIGINAL_IMAGE_REQUIRES_BRIDGE":"DEVUP_ASSET_FILL_REQUIRES_BRIDGE");let x=await c(()=>n(t.nodeId));if(f("lookup",Date.now()-a),l=x&&x.type||"unknown",!x||["node","visible"].includes(h)&&"function"!=typeof x.exportAsync)return p("DEVUP_ASSET_UNSUPPORTED_BY_UPSTREAM",u(Error(x?"node has no exportAsync":"node not found")));let E=null;if(g||"string"==typeof t.field&&t.field.startsWith("fills/")){let e=Number(t.field.slice(g?22:6)),r="fills"in x&&Array.isArray(x.fills)?x.fills:[],n=Number.isInteger(e)?r[e]:null,i=n&&"IMAGE"===n.type?n.imageHash||n.imageRef:null;if(!n||i!==t.imageHash)return p("DEVUP_ASSET_SOURCE_CHANGED");E=n}else if("node"!==t.field)return p("DEVUP_ASSET_FIELD_UNSUPPORTED");if("original-source"===h&&(!E||!t.imageHash))return p("DEVUP_ORIGINAL_SOURCE_UNAVAILABLE","No IMAGE fill source exists for this selection. Choose an IMAGE fill or explicitly renderMode node; vector assets should retain SVG.");let A=x;if("visible"===h){let e=t.visibleScopeNodeId;if(!e)return p("DEVUP_VISIBLE_CONTEXT_UNAVAILABLE","Recollect the parent frame and provide visibleScopeNodeId.");let r=new Set;for(;A&&A.id!==e&&!r.has(A)&&r.size<64;)r.add(A),A=A.parent;if(!A||A.id!==e||["PAGE","DOCUMENT"].includes(A.type)||"function"!=typeof A.exportAsync)return p("DEVUP_VISIBLE_SCOPE_UNSUPPORTED","The selected capture scope is not an exportable ancestor of the requested node.")}if(d.renderInfo=m(x,E),"visible"===h){let e=d.renderInfo;e.representation="native-visible",e.renderedAs="visible-scoped-capture",e.captureNodeId=A.id,e.captureBox={width:A.width,height:A.height,aspectRatio:A.width/A.height},e.contextScope="selected-ancestor-subtree",e.captureEffects=Array.isArray(A.effects)?A.effects:null,e.requestedNodeEffects=e.effects,e.effects=e.captureEffects,e.sourceProperties.captureEffects=e.captureEffects,e.visibleContext={requestedNodeId:x.id,captureNodeId:A.id,included:["selected-scope-children","selected-scope-clipping","selected-scope-masks","selected-scope-rounding"],outsideScope:"unknown; ancestors, page backdrop and external layers are not included",canvasReferenceCompared:!1},e.warnings.push("visible-scoped-capture","outside-capture-context-unverified")}b(x,d.renderInfo);let I=null;if(E)try{let r=figma.getImageByHash(t.imageHash),n=r&&await Promise.race([r.getSizeAsync(),new Promise(t=>{e=setTimeout(()=>t(null),250)})]);I=n&&n.width>0&&n.height>0&&Number.isFinite(n.width)&&Number.isFinite(n.height)?{...n,aspectRatio:n.width/n.height}:null}catch(e){}finally{clearTimeout(e)}let S=null;if("auto"===h){let e=function(e,r,n){let i,a=[],o=(e,t)=>{e||a.push(t)},l=e=>Array.isArray(e)&&2===e.length&&e.every(e=>Array.isArray(e)&&3===e.length&&e.every(Number.isFinite))&&1===e[0][0]&&1===e[1][1]&&0===e[0][1]&&0===e[1][0]&&Number.isInteger(e[0][2])&&Number.isInteger(e[1][2]);for(let a of(o("bridge"===t.transport,"raw-requires-bridge"),o(1===t.scale&&["png","jpg"].includes(t.format),"raw-requires-unscaled-raster"),o(!!r&&!0===r.visible&&1===r.opacity&&"NORMAL"===r.blendMode,"paint-compositing-or-unknown"),o(r&&"FILL"===r.scaleMode&&Array.isArray(i=r.imageTransform)&&2===i.length&&i.every((e,t)=>Array.isArray(e)&&3===e.length&&e.every((e,r)=>e===[[1,0,0],[0,1,0]][t][r]))&&0===r.rotation,"paint-transform-or-unknown"),o(r&&r.filters&&Object.values(r.filters).every(e=>0===e),"image-filters-or-unknown"),o(Array.isArray(e.fills)&&1===e.fills.length,"multiple-fills-or-unknown"),o("RECTANGLE"===e.type&&0===e.cornerRadius&&Array.isArray(e.strokes)&&0===e.strokes.length&&(!e.children||0===e.children.length),"node-geometry-or-unknown"),o(Array.isArray(e.effects)&&0===e.effects.length&&!1===e.isMask&&(void 0===e.clipsContent||!1===e.clipsContent)&&1===e.opacity&&!0===e.visible&&"NORMAL"===e.blendMode,"node-effects-mask-compositing-or-unknown"),o(l(e.absoluteTransform)&&l(e.relativeTransform),"node-transform-or-unknown"),o(n.sourceProperties.ancestors.length>0&&n.sourceProperties.ancestors.some(e=>["PAGE","DOCUMENT"].includes(e.type)),"ancestor-chain-incomplete"),o(n.sourceProperties.ancestors.every(e=>!1===e.siblingMaskPresent),"ancestor-sibling-mask-or-unknown"),n.sourceProperties.ancestors.filter(e=>!["PAGE","DOCUMENT"].includes(e.type))))o(!1===a.clipsContent&&!1===a.isMask&&!1===a.siblingMaskPresent&&Array.isArray(a.effects)&&0===a.effects.length&&1===a.opacity&&!0===a.visible&&["NORMAL","PASS_THROUGH"].includes(a.blendMode),"ancestor-dependency-or-unknown");return{eligible:0===a.length,reasons:[...new Set(a)],evidence:"explicit-plugin-properties; intrinsic-dimensions-and-codec-required"}}(x,E,d.renderInfo);if(e.eligible)try{let r=figma.getImageByHash(t.imageHash),n=I;n&&n.width===x.width&&n.height===x.height?(S=await c(()=>r.getBytesAsync()),(137===S[0]&&80===S[1]&&78===S[2]&&71===S[3]?"png":255===S[0]&&216===S[1]&&255===S[2]?"jpg":"unknown")!==t.format&&e.reasons.push("source-codec-mismatch-or-unknown")):e.reasons.push("intrinsic-dimensions-mismatch-or-unknown")}catch(t){e.reasons.push("source-inspection-unavailable")}e.eligible=0===e.reasons.length,y=e.eligible?"fill":"node",d.renderInfo=m(x,E),b(x,d.renderInfo),d.renderInfo.autoDecision={...e,selected:y}}if(d.renderInfo.sourceSize=I,d.renderInfo.sourceSizeEvidence=I?"Figma-image-getSizeAsync":"unknown",g||"fill"===y){d.method="original-fill",o="reading the original image";let e=figma.getImageByHash(t.imageHash);if(!e)return p("DEVUP_ORIGINAL_IMAGE_NOT_FOUND");let n=S||await c(()=>e.getBytesAsync());if(0===n.length||n.length>0x5000000)return p("DEVUP_ASSET_RESPONSE_TOO_LARGE",`${n.length} bytes; one answer carries 1 to 83886080`);let i=e=>e.every((e,t)=>n[t]===e),a=i([137,80,78,71,13,10,26,10])?"image/png":i([255,216,255])?"image/jpeg":i([71,73,70,56])?"image/gif":i([82,73,70,70])&&87===n[8]&&69===n[9]&&66===n[10]&&80===n[11]?"image/webp":null;if(!a)return p("DEVUP_ORIGINAL_IMAGE_CODEC_UNSUPPORTED");if(d.renderInfo.originalMimeType=a,!g&&"fill"===h&&"webp"!==t.format&&a!==({png:"image/png",jpg:"image/jpeg"})[t.format])return p("DEVUP_ASSET_FILL_CODEC_MISMATCH",`Original fill codec is ${a}, but ${t.format} was requested; choose the matching format or renderMode node. No transcoding was performed.`);if(!g)return{...p(null),status:"exported",mimeType:a,byteLength:n.length,sha256:r(n),data:figma.base64Encode(n)};let l=await c(()=>e.getSizeAsync());return{...p(null),status:"exported",kind:"devupOriginalImage",representation:"original-image-v1",format:null,scale:null,mimeType:a,width:l.width,height:l.height,byteLength:n.length,sha256:r(n),data:figma.base64Encode(n)}}let w="webp"===t.format?"PNG":String(t.format||"").toUpperCase();if(!["PNG","JPG","SVG","PDF"].includes(w))return p("DEVUP_ASSET_FORMAT_UNSUPPORTED");let v=Number(t.scale??2);if(!Number.isFinite(v)||v<=0||v>4)return p("DEVUP_ASSET_SCALE_INVALID");let N="SVG"===w,O={format:N?"SVG_STRING":w,contentsOnly:!0,useAbsoluteBounds:!1};("PNG"===w||"JPG"===w)&&(O.constraint={type:"SCALE",value:v}),o="export";let T=Math.max(1,s-(Date.now()-a)),P=N?Math.max(1,T-Math.min(5e3,Math.floor(T/5))):T,_=Date.now(),C=await c(()=>"visible"===h?A.exportAsync(O):E?x.exportAsync(O):i(x,O,P,f));if(E&&f("native",Date.now()-_),"geometry"===d.method&&(d.renderInfo.representation="geometry-fallback",d.renderInfo.renderedAs="geometry-fallback",d.renderInfo.effectsBakedIntoAsset=!1,d.renderInfo.warnings.push("geometry-fallback-not-native-render")),Date.now()-a>=s)throw Error(`DEVUP_ASSET_TIMEOUT: export exceeded ${s} ms asset budget`);o="encode";let M=Date.now(),k=N&&"string"==typeof C?C:null,R=null===k?C instanceof Uint8Array?C:new Uint8Array(C):function(e){let t=[];for(let r=0;r=55296&&n<=56319){let t=r+1=56320&&t<=57343?(n=65536+(n-55296<<10)+(t-56320),r+=1):n=65533}else n>=56320&&n<=57343&&(n=65533);n<128?t.push(n):n<2048?t.push(192|n>>6,128|63&n):n<65536?t.push(224|n>>12,128|n>>6&63,128|63&n):t.push(240|n>>18,128|n>>12&63,128|n>>6&63,128|63&n)}return new Uint8Array(t)}(k);if(0===R.length||R.length>0x5000000)return p("DEVUP_ASSET_RESPONSE_TOO_LARGE",`${w} at scale ${v} is ${R.length} bytes; one answer carries 1 to 83886080`);let D=r(R);f("encode",Date.now()-M);let L=figma.io&&"function"==typeof figma.io.write,$="PNG"===w&&R.length>786432;if(L&&(null!==k&&R.length>12288||$))return{kind:"devupAssetExport",fileKey:figma.fileKey||"",version:t.version,assetId:t.assetId,nodeId:t.nodeId,field:t.field,imageHash:t.imageHash,format:t.format,scale:v,renderMode:h,usageAspectRatio:t.usageAspectRatio??null,maxWidth:t.maxWidth??null,srcset:!0===t.srcset,visibleScopeNodeId:t.visibleScopeNodeId??null,status:"chunked",byteLength:R.length,sha256:D,cursor:{nextOffset:0,maxChunkBytes:12288},errorCode:null,exportDiagnostics:d};return L&&figma.io.write(`devup-asset-${t.assetId.replace(/[^A-Za-z0-9_-]/g,"_")}.${w.toLowerCase()}`,R),{kind:"devupAssetExport",fileKey:figma.fileKey||"",version:t.version,assetId:t.assetId,nodeId:t.nodeId,field:t.field,imageHash:t.imageHash,format:t.format,scale:v,renderMode:h,usageAspectRatio:t.usageAspectRatio??null,maxWidth:t.maxWidth??null,srcset:!0===t.srcset,visibleScopeNodeId:t.visibleScopeNodeId??null,status:"exported",byteLength:R.length,sha256:D,mimeType:L?null===k?null:"image/svg+xml":({PNG:"image/png",JPG:"image/jpeg",SVG:"image/svg+xml",PDF:"application/pdf"})[w],...!L&&null===k?{data:figma.base64Encode(R)}:{},text:k,errorCode:null,exportDiagnostics:d}}catch(e){return p(String(e.message||e).includes("DEVUP_ASSET_TIMEOUT:")?"DEVUP_ASSET_TIMEOUT":g||["fill","original-source"].includes(h)?"DEVUP_ORIGINAL_IMAGE_READ_FAILED":"DEVUP_ASSET_EXPORT_FAILED",u(e))}},bridgeContext:async function(e){let t=figma.currentPage,r=t.selection,n=e=>({id:e.id,name:String(e.name).slice(0,256),type:e.type});return{kind:"devup-bridge-context",currentPage:n(t),selection:r.slice(0,20).map(n),selectionCount:r.length,zoom:Number.isFinite(figma.viewport?.zoom)?figma.viewport.zoom:null,observedAt:Date.now(),lastKnownSelection:globalThis.__devupLastSelection??null,lastKnownPage:globalThis.__devupLastSelectionPage??null}},explore:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=await figma.getNodeByIdAsync(e.nodeId);if(!t)throw Error("DEVUP_NODE_NOT_FOUND");let r=e.explore,n=Math.max(20,Math.min(400,Number.isInteger(r.projectionLimit)?r.projectionLimit:200)),i=Math.max(0,Math.min(500,Number.isInteger(r.textPreviewLimit)?r.textPreviewLimit:160));function a(e){return"string"==typeof e?e.slice(0,240):""}let o="PAGE"===t.type,l=t,s="SECTION"===t.type?t:null;for(;!o&&l.parent&&"PAGE"!==l.parent.type;)l=l.parent,s||"SECTION"!==l.type||(s=l);let d="PAGE"===t.type?t:l.parent;if(!d||"PAGE"!==d.type)throw Error("DEVUP_PAGE_NOT_FOUND");function f(e){let t=e.absoluteBoundingBox||{x:"number"==typeof e.x?e.x:0,y:"number"==typeof e.y?e.y:0,width:"number"==typeof e.width?e.width:0,height:"number"==typeof e.height?e.height:0};return[t.x,t.y,t.width,t.height].every(Number.isFinite)?{x:t.x,y:t.y,width:t.width,height:t.height}:null}function c(e){let t=f(e);return t&&["FRAME","COMPONENT","INSTANCE","COMPONENT_SET"].includes(e.type)&&t.width>=240&&t.width<=1800&&t.height>=300&&t.height<=2e3&&t.width/Math.max(1,t.height)>=.25&&t.width/Math.max(1,t.height)<=2.5}function u(e){let t=[],r=e;for(;r&&"DOCUMENT"!==r.type&&t.length<12;){let e=a(r.name);e&&t.push(e),r=r.parent}return t.reverse()}await figma.setCurrentPageAsync(d);let p=o?d:!c(t)&&s?s:l,g=f(p)||f(t);if(!g)throw Error("DEVUP_NODE_BOUNDS_UNAVAILABLE");let h="children"in d?d.children:[],y=h.map((e,t)=>({node:e,pageChildIndex:t,bounds:f(e)})).filter(e=>e.bounds).filter(e=>{var t;return!!o||e.node.id===p.id||e.bounds.y>=g.y-240&&e.bounds.y<=g.y+12e3&&(t=e.bounds,Math.min(t.x+t.width,g.x+g.width)-Math.max(t.x,g.x)>0)}).sort((e,t)=>e.bounds.y-t.bounds.y||e.bounds.x-t.bounds.x||e.node.id.localeCompare(t.node.id)),m="SECTION"===p.type?y.filter(e=>e.node.id===p.id):y.slice(0,n),b=new Map(m.map(e=>[e.node.id,e]));b.has(l.id)||b.set(l.id,{node:l,pageChildIndex:-1,bounds:f(l)}),b.has(p.id)||b.set(p.id,{node:p,pageChildIndex:-1,bounds:f(p)}),b.has(t.id)||b.set(t.id,{node:t,pageChildIndex:-1,bounds:f(t)});let x=o?null:t.parent;for(;x&&"PAGE"!==x.type&&(b.has(x.id)||b.set(x.id,{node:x,pageChildIndex:-1,bounds:f(x)}),x.id!==p.id);)x=x.parent;if("children"in p)for(let e of p.children.slice(0,n))b.has(e.id)||b.set(e.id,{node:e,pageChildIndex:-1,bounds:f(e)});let E="SECTION"===p.type||o,A=E&&"children"in p&&p.children.length>n;if(E&&"children"in p){let e=8*n,t=p.children.filter(e=>!c(e)).map(e=>({node:e,ancestors:[]})),r=0;for(;t.length&&r!b.has(e.id));if(b.size+t.length>n+2){A=!0;continue}for(let e of t)b.set(e.id,{node:e,pageChildIndex:-1,bounds:f(e)});continue}if("children"in e)for(let r of e.children)(!o||"SECTION"===e.type)&&(!o||["SECTION","FRAME","COMPONENT","INSTANCE","COMPONENT_SET"].includes(r.type))&&t.push({node:r,ancestors:[...i,e]})}A||=t.length>0}let I=[...b.values()].filter(e=>e.bounds).slice(0,n+2).map(({node:e,pageChildIndex:t,bounds:r})=>{let n;return{id:e.id,type:e.type,fields:{name:a(e.name),parentId:e.parent&&"DOCUMENT"!==e.parent.type?e.parent.id:null,childrenIds:[],x:r.x,y:r.y,width:r.width,height:r.height,childCount:"children"in e?e.children.length:0,textPreview:"",...o||null===(n=function(e){let t=[e],r=0,n=null;for(;t.length&&r++<400;){let e=t.shift();if("visible"in e&&!1===e.visible)continue;let r="children"in e?e.children.filter(e=>!("visible"in e)||!1!==e.visible):[];if(/header|nav|gnb/i.test(e.name||"")&&r.length>=2&&"layoutMode"in e&&"HORIZONTAL"===e.layoutMode){let t=r.map(f);t.every(Boolean)&&(n=Math.max(n||0,t.reduce((e,t)=>e+t.width,0)+(Number(e.itemSpacing)||0)*(t.length-1)))}t.push(...r.slice(0,80))}return n}(e))?{}:{navigationContentWidth:n},pageChildIndex:t>=0?t:null,visible:!("visible"in e)||!1!==e.visible,breadcrumb:u(e)},extra:{},fieldErrors:{}}}),S=I.reduce((e,t)=>{let r=t.fields;return e?{x:Math.min(e.x,r.x),y:Math.min(e.y,r.y),right:Math.max(e.right,r.x+r.width),bottom:Math.max(e.bottom,r.y+r.height)}:{x:r.x,y:r.y,right:r.x+r.width,bottom:r.y+r.height}},null)||{x:0,y:0,right:0,bottom:0},w="SECTION"!==p.type&&y.length>m.length||b.size>n+2||A,v={id:d.id,type:d.type,fields:{name:a(d.name),parentId:null,childrenIds:[],x:S.x,y:S.y,width:S.right-S.x,height:S.bottom-S.y,childCount:h.length,textPreview:"",projectionTruncated:w,visible:!0,breadcrumb:u(d),pageChildIndex:null},extra:{},fieldErrors:{}},N={fileKey:figma.fileKey||"",version:null,rootIds:[d.id],nodes:[v,...I.filter(e=>e.id!==d.id)],diagnostics:[]},O=new Set([d.id,l.id,p.id,t.id]);for(;JSON.stringify(N).length>14e3;){let e=N.nodes.length-1;for(;e>=0&&O.has(N.nodes[e].id);)e-=1;if(e<0)break;N.nodes.splice(e,1),v.fields.projectionTruncated=!0}if(JSON.stringify(N).length>14e3&&(N.nodes=N.nodes.filter(e=>O.has(e.id)).map(e=>({...e,fields:{...e.fields,name:a(e.fields.name).slice(0,80),textPreview:"",breadcrumb:e.fields.breadcrumb.slice(-4).map(e=>e.slice(0,80)),projectionTruncated:e.id===d.id||e.fields.projectionTruncated}}))),JSON.stringify(N).length>14e3)throw Error("DEVUP_EXPLORE_PROJECTION_TOO_LARGE");function T(e){let t=0;for(let r of e){let e=r.codePointAt(0);t+=e<128?1:e<2048?2:e<65536?3:4}return t}if(o){for(;T(JSON.stringify(N))>19456&&N.nodes.length>1;)N.nodes.pop(),v.fields.projectionTruncated=!0;if(T(JSON.stringify(N))>19456)throw Error("DEVUP_EXPLORE_PROJECTION_TOO_LARGE")}let P=14e3-JSON.stringify(N).length;if(o&&(P=Math.min(P,19456-T(JSON.stringify(N)))),i>0&&P>0){let e=N.nodes.filter(e=>b.get(e.id)&&e.id!==d.id),t=e.length;for(let r of e){let e=Math.floor(P/t);if(t-=1,e<=0)continue;let n="",a=0;for(let t of function(e){if(0===i)return"";let t=[],r=[e],n=0;for(;r.length&&n<80&&t.join(" ").lengthe)break;n+=t,a+=r}r.fields.textPreview=n,P-=a}}return N},fastSnapshot:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=e.rootIds;if(!Array.isArray(t)||0===t.length)throw Error("DEVUP_ROOTS_INVALID");let r=await Promise.all(t.map(e=>figma.getNodeByIdAsync(e)));if(r.some(e=>!e))throw Error("DEVUP_NODE_NOT_FOUND");if(1===r.length&&"SECTION"===r[0].type)throw Error("DEVUP_TARGET_IS_SECTION");let n=["mobile","tablet","desktop"],i=e=>{let t=String(e.name||"").trim().toLowerCase(),r=n.indexOf(t);if(r>=0)return r;let i=t.match(/^(\d+)\s*[~–]\s*(\d+)$/);if(!i)return -1;let a=Number(i[1]),o=Number(i[2]);return Number.isSafeInteger(a)&&Number.isSafeInteger(o)&&a>0&&o>=a&&o<=0xffffffff?1:-1};if(1===r.length&&i(r[0])>=0){let e=r[0].parent;if(e&&"SECTION"===e.type&&"children"in e){let t=e.children.filter(e=>e.id===r[0].id||i(e)>=0).filter(e=>!1!==e.visible);t.length>1&&(r.length=0,r.push(...t))}}if(1===t.length){let e=[],t=new Set(r.map(e=>e.id)),n=[],i=e=>{if("reactions"in e&&Array.isArray(e.reactions)){for(let r of e.reactions)if(r&&r.trigger&&"AFTER_TIMEOUT"===r.trigger.type)for(let e of r.actions||[])e&&"NODE"===e.type&&e.transition&&"SMART_ANIMATE"===e.transition.type&&"string"==typeof e.destinationId&&!t.has(e.destinationId)&&(t.add(e.destinationId),n.push(e.destinationId));if("children"in e)for(let t of e.children)i(t)}};for(let e of r)i(e);for(;n.length>0;){let t=n.shift(),r=await figma.getNodeByIdAsync(t);r&&"DOCUMENT"!==r.type&&"PAGE"!==r.type&&(e.push(r),i(r))}r.push(...e)}let a=e.nodeId,o=["absoluteBoundingBox","absoluteRenderBounds","arcData","backgroundStyleId","blendMode","bottomLeftRadius","bottomRightRadius","boundVariables","characters","clipsContent","componentProperties","componentPropertyDefinitions","componentPropertyReferences","constraints","cornerRadius","counterAxisAlignItems","dashPattern","defaultVariant","effectStyleId","effects","fillStyleId","fills","fontName","fontSize","gridColumnAnchorIndex","gridColumnCount","gridColumnGap","gridColumnSizes","gridColumnSpan","gridRowAnchorIndex","gridRowCount","gridRowGap","gridRowSizes","gridRowSpan","gridStyleId","height","inferredAutoLayout","isAsset","isMask","itemSpacing","layoutGrow","layoutMode","layoutWrap","layoutPositioning","layoutSizingHorizontal","layoutSizingVertical","letterSpacing","lineHeight","maxHeight","maxLines","maxWidth","minHeight","minWidth","name","opacity","overflowDirection","paddingBottom","paddingLeft","paddingRight","paddingTop","primaryAxisAlignItems","reactions","rotation","strokeAlign","strokeBottomWeight","strokeLeftWeight","strokeRightWeight","strokeStyleId","strokeTopWeight","strokeWeight","strokes","targetAspectRatio","textAlignHorizontal","textAlignVertical","textAutoResize","textCase","textDecoration","textStyleId","textTruncation","topLeftRadius","topRightRadius","variantProperties","visible","width","x","y"],l=["fontName","fontWeight","fontSize","textDecoration","textCase","lineHeight","letterSpacing","fills","textStyleId","fillStyleId","listOptions","indentation","hyperlink"],s=e.snapshot,d=Math.max(0,Math.floor(Number(s.offset)||0)),f=Math.max(8192,Math.floor(Number(s.maxEnvelopeBytes)||19456)),c=Math.min(s.maxEnvelopeBytes?f-1024:18e3,Math.max(4096,Math.floor(Number(s.maxPayloadBytes)||15e3))),u=new Set(["backgroundStyleId","effectStyleId","fillStyleId","gridStyleId","strokeStyleId","textStyleId"]),p=new Set(["maxWidth","maxHeight","absoluteRenderBounds"]),g=new Map([["rotation",0],["cornerRadius",0],["isAsset",!1],["isMask",!1],["clipsContent",!1],["blendMode","PASS_THROUGH"],["strokeAlign","INSIDE"],["textCase","ORIGINAL"],["textDecoration","NONE"],["textAlignHorizontal","LEFT"],["textAlignVertical","TOP"],["counterAxisAlignItems","MIN"],["primaryAxisAlignItems","MIN"],["gridColumnCount",0],["gridRowCount",0],["gridColumnGap",0],["gridRowGap",0],["gridColumnAnchorIndex",-1],["gridRowAnchorIndex",-1],["gridColumnSpan",1],["gridRowSpan",1]]),h=new Set(["start","end","characters","fontWeight","textStyleId","fillStyleId","listOptions","indentation","hyperlink"]),y=new Set(["parent","children","consumers"]);function m(e,t=!1,r=new WeakSet,n=0){let i;if(null===e||["string","number","boolean"].includes(typeof e))return e;if(void 0===e)return{$undefined:!0};if("bigint"==typeof e)return{$bigint:e.toString()};if(["function","symbol"].includes(typeof e))return{$unsupported:typeof e};if(n>12)return{$truncated:"max-depth"};if("object"==typeof e&&"parent"in e&&"string"==typeof e.id&&"string"==typeof e.type)return{$nodeId:e.id,$nodeType:e.type};if(Array.isArray(e))return e.map(e=>m(e,t,r,n+1));if(ArrayBuffer.isView(e))return{$binary:e.constructor.name,byteLength:e.byteLength};if(e instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:e.byteLength};if(r.has(e))return{$circular:!0};if(r.add(e),t){let t=new Set(Object.keys(e)),r=e;for(;r&&r!==Object.prototype;){for(let e of Object.getOwnPropertyNames(r))t.add(e);r=Object.getPrototypeOf(r)}i=[...t].sort().filter(e=>!e.startsWith("_")&&!y.has(e))}else i=Object.keys(e).sort();let a={};for(let o of i)try{let i=m(e[o],t,r,n+1);i&&"function"===i.$unsupported||(a[o]=i)}catch(e){a[o]=t?{$error:"unavailable"}:{$error:String(e&&e.message?e.message:e)}}return r.delete(e),a}let b=new Map;async function x(e,t,r){if("INSTANCE"!==e.type||"function"!=typeof e.getMainComponentAsync)return;b.has(e.id)||b.set(e.id,(async()=>{try{let t=await e.getMainComponentAsync();return{fields:{mainComponentId:t?t.id:null,mainComponentName:t?t.name:null,mainComponentSetId:t&&t.parent&&"COMPONENT_SET"===t.parent.type?t.parent.id:null}}}catch(e){return{error:String(e&&e.message?e.message:e)}}})());let n=await b.get(e.id);n.error?r.mainComponentId=n.error:Object.assign(t,n.fields)}async function E(e){let t={},r={};e.parent&&(t.parentId=e.parent.id),e.parent&&("PAGE"===e.parent.type||"SECTION"===e.parent.type||"COMPONENT_SET"===e.parent.type)&&(t.parentType=e.parent.type,"SECTION"===e.parent.type&&(t.parentName=e.parent.name));let n="children"in e?e.children.map(e=>e.id):[];for(let i of(n.length>0&&(t.childrenIds=n),o)){let n;if("componentPropertyDefinitions"!==i||"COMPONENT"!==e.type||!e.parent||"COMPONENT_SET"!==e.parent.type)try{if(!(i in e)){"overflowDirection"===i&&["FRAME","COMPONENT","INSTANCE","COMPONENT_SET"].includes(e.type)&&(t[i]=null);continue}if(n=e[i],"function"==typeof n)continue;let r=m(n);if("overflowDirection"===i&&null==n){t[i]=null;continue}(null===r?!p.has(i):Array.isArray(r)?0===r.length:"object"==typeof r?0===Object.keys(r).length:!!(""===r&&u.has(i))||g.has(i)&&g.get(i)===r)||(t[i]=r)}catch(e){r[i]=String(e&&e.message?e.message:e)}}if("TEXT"===e.type&&"function"==typeof e.getStyledTextSegments)try{let r=m(e.getStyledTextSegments(l));if(1===r.length){let e=r[0];for(let t of Object.keys(e))h.has(t)||delete e[t]}r.length>0&&(t.styledTextSegments=r)}catch(e){r.styledTextSegments=String(e&&e.message?e.message:e)}await x(e,t,r);let i={id:e.id,type:e.type,fields:t};return Object.keys(r).length>0&&(i.fieldErrors=r),i}let A=[],I=[...r],S=new Set;for(let e=0;e=A.length&&A.length>0)throw Error("DEVUP_SNAPSHOT_RANGE_INVALID");function w(e){let t=0;for(let r=0;r=55296&&n<=56319&&r+1({id:e,styleType:t})).sort((e,t)=>e.id.localeCompare(t.id));async function a(e){try{let t=await figma.variables.getVariableByIdAsync(e);return t?{kind:"variable",value:m(t,!0),collectionId:t.variableCollectionId,aliasIds:Object.values(t.valuesByMode||{}).filter(e=>e&&"VARIABLE_ALIAS"===e.type&&"string"==typeof e.id&&e.id).map(e=>e.id)}:{kind:"unresolved",value:{id:e,kind:"variable",reason:"notFoundOrUnavailable"}}}catch(t){return{kind:"unresolved",value:{id:e,kind:"variable",reason:"notFoundOrUnavailable",error:String(t&&t.message||t).slice(0,300)}}}}let o=await Promise.all([...n.map(a),...i.map(async({id:e,styleType:t})=>{try{let r=await figma.getStyleByIdAsync(e);if(!r)return{kind:"unresolved",value:{id:e,kind:"style",reason:"notFoundOrUnavailable"}};return{kind:"style",value:{...m(r,!0),styleType:t,value:m("PAINT"===t?r.paints:"EFFECT"===t?r.effects:"GRID"===t?r.layoutGrids:r,!0)}}}catch(t){return{kind:"unresolved",value:{id:e,kind:"style",reason:"notFoundOrUnavailable",error:String(t&&t.message||t).slice(0,300)}}}})]),l=new Set(n),s=o.flatMap(e=>e.aliasIds||[]);for(;s.length>0;){let e=[...new Set(s)].filter(e=>!l.has(e)).sort();for(let t of e)l.add(t);let t=await Promise.all(e.map(a));o.push(...t),s=t.flatMap(e=>e.aliasIds||[])}let d=[...new Set(o.filter(e=>"variable"===e.kind&&e.collectionId).map(e=>e.collectionId))].sort(),f=(await Promise.all(d.map(async e=>{try{let t=await figma.variables.getVariableCollectionByIdAsync(e);return t?m(t,!0):null}catch(e){return null}}))).filter(e=>null!==e),c=o.filter(e=>"variable"===e.kind).map(e=>e.value),u=o.filter(e=>"style"===e.kind).map(e=>e.value),p=o.filter(e=>"unresolved"===e.kind).map(e=>e.value);return{collections:f,variables:c,styles:u,usedRemoteVariables:c.filter(e=>!0===e.remote),usedVariableIds:n,usedStyleIds:i.map(e=>e.id),localComplete:!1,usedRemoteComplete:0===p.length,unresolved:p,$variableRefCount:n.length,$styleRefCount:i.length}}async function N(e){let t=[],r=2;for(let n=d;ne)break;t.push(i),r+=a}return{pageNodes:t,packedBytes:r}}let O=c-1024,T=null;for(let e=0;e<5;e+=1){let{pageNodes:e,packedBytes:t}=await N(O);if(0===e.length)throw Error("DEVUP_SNAPSHOT_RANGE_INVALID");let n=function(e,t){let n=Math.min(A.length,d+e.length),{$variableRefCount:i,$styleRefCount:o,...l}=t,s=[...e,{id:"__DEVUP_SNAPSHOT_CURSOR__",type:"DEVUP_INTERNAL",fields:{offset:d,nextOffset:n,complete:n>=A.length,totalNodes:A.length},extra:{},fieldErrors:{}}],f={kind:"devupFastSnapshotEnvelope",schemaVersion:1,source:{fileKey:figma.fileKey||"",rootId:a},snapshot:{fileKey:figma.fileKey||"",version:null,rootIds:r.map(e=>e.id),nodes:s,diagnostics:[]},resources:l,integrity:{nodeCount:s.length,variableRefCount:i,styleRefCount:o,utf8Bytes:0}},c=0;for(let e=0;e<8&&(c=w(JSON.stringify(f)),f.integrity.utf8Bytes!==c);e+=1)f.integrity.utf8Bytes=c;if(f.integrity.utf8Bytes!==w(JSON.stringify(f)))throw Error("DEVUP_ENVELOPE_LENGTH_UNSTABLE");return{envelope:f,bytes:c}}(e,await v(e));if(n.bytes<=f){T=n;break}if(1===e.length)throw Error("DEVUP_ENVELOPE_TOO_LARGE");O=Math.max(1,Math.min(O-1,t-(n.bytes-f)-256))}if(!T)throw Error("DEVUP_ENVELOPE_TOO_LARGE");return T.envelope},fastTheme:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=Math.max(0,Math.floor(Number(e.theme.offset)||0));function r(e,t=new WeakSet,n=0){if(null===e||["string","number","boolean"].includes(typeof e))return e;if(void 0===e)return{$undefined:!0};if("bigint"==typeof e)return{$bigint:e.toString()};if(["function","symbol"].includes(typeof e))return{$unsupported:typeof e};if(n>12)return{$truncated:"max-depth"};if("object"==typeof e&&"parent"in e&&"string"==typeof e.id&&"string"==typeof e.type)return{$nodeId:e.id,$nodeType:e.type};if(Array.isArray(e))return e.map(e=>r(e,t,n+1));if(ArrayBuffer.isView(e))return{$binary:e.constructor.name,byteLength:e.byteLength};if(e instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:e.byteLength};if(t.has(e))return{$circular:!0};t.add(e);let i={};for(let a of function(e){let t=new Set(Object.keys(e)),r=e;for(;r&&r!==Object.prototype;){for(let e of Object.getOwnPropertyNames(r))t.add(e);r=Object.getPrototypeOf(r)}return[...t].sort()}(e))if(!(a.startsWith("_")||["parent","children","consumers"].includes(a)))try{let o=r(e[a],t,n+1);o&&"function"===o.$unsupported||(i[a]=o)}catch(e){i[a]={$error:"unavailable"}}return t.delete(e),i}let n=new Set,i=new Map;for(let e of figma.root.children)await e.loadAsync();for(let e of[figma.root,...figma.root.findAll(()=>!0)])for(let t of["boundVariables","fills","strokes","effects","layoutGrids","textStyleId","fillStyleId","strokeStyleId","backgroundStyleId","effectStyleId","gridStyleId"])try{!function e(t,r="",a=new WeakSet,o=0){if(!(o>16)&&null!==t&&"object"==typeof t&&!a.has(t)){if(a.add(t),Array.isArray(t)){for(let n of t)e(n,r,a,o+1);return}for(let[l,s]of("VARIABLE_ALIAS"===t.type&&"string"==typeof t.id&&t.id&&"figma.mixed"!==t.id&&"MIXED"!==t.id&&n.add(t.id),Object.entries(t))){let t="textStyleId"===l?"TEXT":["fillStyleId","strokeStyleId","backgroundStyleId"].includes(l)?"PAINT":"effectStyleId"===l?"EFFECT":"gridStyleId"===l?"GRID":null;t&&"string"==typeof s&&s&&"figma.mixed"!==s&&"MIXED"!==s&&!i.has(s)&&i.set(s,t),e(s,l||r,a,o+1)}}}({[t]:e[t]})}catch(e){}let[a,o,l,s,d,f]=await Promise.all([figma.variables.getLocalVariableCollectionsAsync(),figma.variables.getLocalVariablesAsync(),figma.getLocalPaintStylesAsync(),figma.getLocalTextStylesAsync(),figma.getLocalEffectStylesAsync(),figma.getLocalGridStylesAsync()]),c=new Set(o.map(e=>e.id)),u=new Set([...l,...s,...d,...f].map(e=>e.id)),p=[];async function g(e){try{return await figma.variables.getVariableByIdAsync(e)||null}catch(t){return p.push({id:e,kind:"variable",reason:"notFoundOrUnavailable",error:String(t&&t.message||t).slice(0,300)}),null}}function h(e){return Object.values(e.valuesByMode||{}).filter(e=>e&&"VARIABLE_ALIAS"===e.type&&"string"==typeof e.id&&e.id).map(e=>e.id)}let y=[...i.entries()].filter(([e])=>!u.has(e)).sort(([e],[t])=>e.localeCompare(t)).map(async([e,t])=>{try{let r=await figma.getStyleByIdAsync(e);return r?{style:r,styleType:t}:null}catch(t){return p.push({id:e,kind:"style",reason:"notFoundOrUnavailable",error:String(t&&t.message||t).slice(0,300)}),null}}),m=new Set,b=[],x=[...n,...o.flatMap(h)];for(;x.length>0;){let e=[...new Set(x)].filter(e=>!c.has(e)&&!m.has(e)).sort();for(let t of e)m.add(t);let t=(await Promise.all(e.map(g))).filter(Boolean);b.push(...t),x=t.flatMap(h)}let E=(await Promise.all(y)).filter(Boolean);for(let e of m)b.some(t=>t.id===e)||p.some(t=>t.id===e)||p.push({id:e,kind:"variable",reason:"notFoundOrUnavailable"});for(let[e]of[...i.entries()].filter(([e])=>!u.has(e)))E.some(t=>t.style.id===e)||p.some(t=>t.id===e)||p.push({id:e,kind:"style",reason:"notFoundOrUnavailable"});let A=[...new Set(b.map(e=>e.variableCollectionId))].filter(e=>!a.some(t=>t.id===e)).sort(),I=(await Promise.all(A.map(async e=>{try{return await figma.variables.getVariableCollectionByIdAsync(e)}catch(e){return null}}))).filter(Boolean);function S(e,t){return{...r(e),styleType:t,value:r("PAINT"===t?e.paints:"EFFECT"===t?e.effects:"GRID"===t?e.layoutGrids:e)}}let w=[...l.map(e=>S(e,"PAINT")),...s.map(e=>S(e,"TEXT")),...d.map(e=>S(e,"EFFECT")),...f.map(e=>S(e,"GRID")),...E.map(({style:e,styleType:t})=>S(e,t))].sort((e,t)=>e.id.localeCompare(t.id)),v=[...o,...b].map(e=>r(e)).sort((e,t)=>e.id.localeCompare(t.id)),N=[...a,...I].map(e=>r(e)).sort((e,t)=>e.id.localeCompare(t.id));function O(e){let t=[];for(let r=0;r=55296&&n<=56319){let t=r+1=56320&&t<=57343?(n=65536+(n-55296<<10)+(t-56320),r+=1):n=65533}else n>=56320&&n<=57343&&(n=65533);n<128?t.push(n):n<2048?t.push(192|n>>6,128|63&n):n<65536?t.push(224|n>>12,128|n>>6&63,128|63&n):t.push(240|n>>18,128|n>>12&63,128|n>>6&63,128|63&n)}return new Uint8Array(t)}p.sort((e,t)=>e.kind.localeCompare(t.kind)||e.id.localeCompare(t.id));let T=[...N.map(e=>({kind:"collection",value:e})),...v.map(e=>({kind:"variable",value:e})),...w.map(e=>({kind:"style",value:e})),...[...n].sort().map(e=>({kind:"usedVariableId",value:e})),...[...i.keys()].sort().map(e=>({kind:"usedStyleId",value:e})),...p.map(e=>({kind:"unresolved",value:e}))];if(t>T.length)throw Error("DEVUP_SNAPSHOT_RANGE_INVALID");let P=T.map(e=>O(JSON.stringify(e.value)).length+1),_=18432,C=null;for(let e=0;e<6;e+=1){let{pageItems:e,packed:r}=function(e){let r=[],n=0;for(let i=t;i0)||!(n+P[i]>e));i+=1)r.push(T[i]),n+=P[i];return{pageItems:r,packed:n}}(_),n=function(e,r){let n=t=>e.filter(e=>e.kind===t).map(e=>e.value),i=n("collection"),a=n("variable"),o=n("style"),l=n("unresolved"),s={kind:"devupFastThemeEnvelope",schemaVersion:1,source:{fileKey:figma.fileKey||"",version:null},resources:{collections:i,variables:a,styles:o,usedRemoteVariables:[],usedVariableIds:n("usedVariableId"),usedStyleIds:n("usedStyleId"),localComplete:!0,usedRemoteComplete:0===p.length,unresolved:l},page:{offset:t,nextOffset:r,complete:r>=T.length,totalItems:T.length},integrity:{collectionCount:i.length,variableCount:a.length,styleCount:o.length,unresolvedCount:l.length,utf8Bytes:0}},d=new Uint8Array;for(let e=0;e<8&&(d=O(JSON.stringify(s)),s.integrity.utf8Bytes!==d.length);e+=1)s.integrity.utf8Bytes=d.length;if(d=O(JSON.stringify(s)),s.integrity.utf8Bytes!==d.length)throw Error("DEVUP_ENVELOPE_LENGTH_UNSTABLE");return{envelope:s,bytes:d.length}}(e,t+e.length);if(n.bytes<=19456){C=n;break}if(e.length<=1)break;_=Math.max(1,Math.min(_-1,r-(n.bytes-19456)-256))}if(null===C||C.bytes>8388608)throw Error("DEVUP_ENVELOPE_TOO_LARGE");return C.envelope},largeValue:async function(e){let t;figma.skipInvisibleInstanceChildren=!1;let r=e.largeValue;async function n(e){try{let t=await figma.getNodeByIdAsync(e);if(t)return t}catch(r){if(!e.startsWith("I")||!e.includes(";"))throw r;try{return await t()}catch(e){throw Error(`${r.message||r}; nested lookup fallback: ${e.message||e}`)}}if(!e.startsWith("I")||!e.includes(";"))return null;return t();async function t(){let t=await figma.getNodeByIdAsync(e.slice(1).split(";")[0]),r=t?[t]:[],n=new Set;for(;r.length;){let t=r.pop();if(t.id===e)return t;if(!n.has(t.id)){if(n.add(t.id),n.size>1e4)throw Error("nested lookup exceeds 10000 nodes");Array.isArray(t.children)&&r.push(...t.children)}}throw Error(`nested node ${e} not found in its instance`)}}async function i(e,t,r=85e3,n=()=>{}){let a,o=Date.now();try{let i=await Promise.race([e.exportAsync(t),new Promise((e,t)=>{a=setTimeout(()=>t(Error(`DEVUP_ASSET_TIMEOUT: native export exceeded ${r} ms`)),r)})]);return n("native-export",Date.now()-o),i}catch(i){if(n("native-export",Date.now()-o,String(i.message||i).slice(0,500)),"SVG_STRING"!==t.format)throw i;let r=Date.now();try{let t=function(e){let t=e=>"number"==typeof e&&Number.isFinite(e),r=(e,t)=>{throw Error(`geometry fallback unsupported: ${e.type||"unknown"} ${e.id||""}: ${t}`)},n=0,i=0;function a(e,i,a){(void 0!==a&&!Array.isArray(a)||void 0===a&&Array.isArray(i)&&i.length)&&r(e,"unresolved paint requires native export");let o=Array.isArray(a)?a.filter(e=>!1!==e.visible):[];return o.length?(Array.isArray(i)&&i.length||r(e,"paint has no rendered geometry"),o.map(a=>{("SOLID"!==a.type||a.blendMode&&"NORMAL"!==a.blendMode)&&r(e,"paint type or blending requires native export");let o=a.color;o&&[o.r,o.g,o.b].every(e=>t(e)&&e>=0&&e<=1)||r(e,"invalid solid color");let l=void 0===a.opacity?1:a.opacity;return(!t(l)||l<0||l>1)&&r(e,"invalid paint opacity"),["NONZERO","EVENODD"].map(t=>{let a=i.filter(e=>e.windingRule===t);if(!a.length)return"";for(let t of a)"string"==typeof t.data&&t.data.trim()||r(e,"empty path"),n++;return`"']/g,e=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[e])}" fill="rgb(${100*o.r}%,${100*o.g}%,${100*o.b}%)" fill-opacity="${l}" fill-rule="${"EVENODD"===t?"evenodd":"nonzero"}"/>`}).join("")}).join("")):""}let o=e.absoluteRenderBounds,l=e.absoluteTransform;(!o||![o.x,o.y,o.width,o.height].every(t)||o.width<=0||o.height<=0)&&r(e,"missing render bounds"),(!Array.isArray(l)||2!==l.length||!l.every(e=>Array.isArray(e)&&3===e.length&&e.every(t))||0!==l[0][1]||0!==l[1][0]||l[0][0]<=0||l[1][1]<=0)&&r(e,"root rotation, reflection or shear requires native export");let s=(o.x-l[0][2])/l[0][0],d=(o.y-l[1][2])/l[1][1],f=o.width/l[0][0],c=o.height/l[1][1],u=function e(n,o){if(++i>1e4&&r(n,"subtree exceeds 10000 nodes"),n.isMask&&r(n,"mask requires native export"),!1===n.visible||0===n.opacity)return"";n.clipsContent&&r(n,"clipping requires native export"),Array.isArray(n.effects)&&n.effects.some(e=>!1!==e.visible)&&r(n,"effect requires native export"),n.blendMode&&!["NORMAL","PASS_THROUGH"].includes(n.blendMode)&&r(n,"layer blending requires native export");let l=void 0===n.opacity?1:n.opacity;(!t(l)||l<0||l>1)&&r(n,"invalid layer opacity");let s="";if(!o){let e=n.relativeTransform;Array.isArray(e)&&2===e.length&&e.every(e=>Array.isArray(e)&&3===e.length&&e.every(t))||r(n,"missing relative transform"),s=` transform="matrix(${e[0][0]} ${e[1][0]} ${e[0][1]} ${e[1][1]} ${e[0][2]} ${e[1][2]})"`}let d="";if(["GROUP","FRAME","INSTANCE","COMPONENT"].includes(n.type))(Array.isArray(n.fills)&&n.fills.some(e=>!1!==e.visible)||Array.isArray(n.strokes)&&n.strokes.some(e=>!1!==e.visible))&&r(n,"container paint requires native export"),Array.isArray(n.children)||r(n,"missing children"),d=(!0===n.itemReverseZIndex?[...n.children].reverse():n.children).map(t=>e(t,!1)).join("");else{for(let e of(["VECTOR","BOOLEAN_OPERATION","RECTANGLE","ELLIPSE","POLYGON","STAR","LINE"].includes(n.type)||r(n,"node type requires native export"),Array.isArray(n.strokes)&&n.strokes.some(e=>!1!==e.visible)&&"CENTER"!==n.strokeAlign&&r(n,"non-center stroke alignment requires native export"),Array.isArray(n.dashPattern)&&n.dashPattern.length&&r(n,"dashed stroke requires native export"),[n.fillGeometry,n.strokeGeometry]))void 0!==e&&(!Array.isArray(e)||e.some(e=>!["NONZERO","EVENODD"].includes(e.windingRule)))&&r(n,"unknown winding rule");d=a(n,n.fillGeometry,n.fills)+a(n,n.strokeGeometry,n.strokes)}return`${d}`}(e,!0);return n||r(e,"no painted geometry"),`${u}`}(e);return n("geometry-fallback",Date.now()-r),t}catch(e){throw Error(`native export: ${i.message||i}; ${e.message||e}`)}}finally{clearTimeout(a)}}let a=await n(r.nodeId);if(!a)throw Error("DEVUP_NODE_NOT_FOUND");function o(e){let t=[];for(let r=0;r=55296&&n<=56319){let t=r+1=56320&&t<=57343?(n=65536+(n-55296<<10)+(t-56320),r+=1):n=65533}else n>=56320&&n<=57343&&(n=65533);n<128?t.push(n):n<2048?t.push(192|n>>6,128|63&n):n<65536?t.push(224|n>>12,128|n>>6&63,128|63&n):t.push(240|n>>18,128|n>>12&63,128|n>>6&63,128|63&n)}return new Uint8Array(t)}let l=null;try{if("$export:svg"===r.field){if("function"!=typeof a.exportAsync)throw Error("unsupported");let e=await i(a,{format:"SVG_STRING"},Math.max(1,(Number(r.timeoutMs)||9e4)-5e3));if("string"!=typeof e)throw Error("unsupported");l=o(e)}else if("string"==typeof r.field&&r.field.startsWith("$export:png")){if("function"!=typeof a.exportAsync)throw Error("unsupported");let e=Number(r.field.split("@")[1]||2);if(!Number.isFinite(e)||e<=0||e>4)throw Error("invalid scale");let t=r.field.split("@"),n=a;if("visible"===t[2]){let e=new Set;for(;n&&n.id!==t[3]&&!e.has(n)&&e.size<64;)e.add(n),n=n.parent;if(!n||n.id!==t[3]||["PAGE","DOCUMENT"].includes(n.type))throw Error("invalid visible scope")}else if(t.length>2)throw Error("invalid export field");let i=await n.exportAsync({format:"PNG",contentsOnly:!0,useAbsoluteBounds:!1,constraint:{type:"SCALE",value:e}});l=i instanceof Uint8Array?i:new Uint8Array(i)}else if("styledTextSegments"===r.field&&"TEXT"===a.type&&"function"==typeof a.getStyledTextSegments)t=a.getStyledTextSegments(["fontName","fontWeight","fontSize","textDecoration","textCase","lineHeight","letterSpacing","fills","textStyleId","fillStyleId","listOptions","indentation","hyperlink"]);else if(r.field in a)t=a[r.field];else throw Error("unsupported")}catch(e){return{kind:"devupLargeValueUnsupported",fileKey:figma.fileKey||"",version:r.version,nodeId:r.nodeId,field:r.field,byteLength:r.byteLength,sha256:r.sha256,errorCode:"DEVUP_FIELD_UNSUPPORTED_BY_UPSTREAM",errorDetail:`${r.field} failed: ${e.message||e}`.slice(0,500)}}let s=null===l?o(JSON.stringify(function e(t,r=new WeakSet,n=0){if(null===t||["string","number","boolean"].includes(typeof t))return t;if(void 0===t)return{$undefined:!0};if("bigint"==typeof t)return{$bigint:t.toString()};if(["function","symbol"].includes(typeof t))return{$unsupported:typeof t};if(n>12)return{$truncated:"max-depth"};if("object"==typeof t&&"parent"in t&&"string"==typeof t.id&&"string"==typeof t.type)return{$nodeId:t.id,$nodeType:t.type};if(Array.isArray(t))return t.map(t=>e(t,r,n+1));if(ArrayBuffer.isView(t))return{$binary:t.constructor.name,byteLength:t.byteLength};if(t instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:t.byteLength};if(r.has(t))return{$circular:!0};r.add(t);let i={};for(let a of Object.keys(t).sort())try{let o=e(t[a],r,n+1);o&&"function"===o.$unsupported||(i[a]=o)}catch(e){i[a]={$error:String(e&&e.message?e.message:e)}}return r.delete(t),i}(t))):l,d=function(e){let t=[0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,0xe49b69c1,0xefbe4786,0xfc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x6ca6351,0x14292967,0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2],r=e.length,n=64*Math.ceil((r+9)/64),i=new Uint8Array(n);i.set(e),i[r]=128;let a=8*r;for(let e=0;e<8;e+=1)i[n-1-e]=255&Math.floor(a/2**(8*e));let o=[0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19],l=(e,t)=>e>>>t|e<<32-t;for(let e=0;e>>3,n=l(r[e-2],17)^l(r[e-2],19)^r[e-2]>>>10;r[e]=r[e-16]+t+r[e-7]+n>>>0}let[n,a,s,d,f,c,u,p]=o;for(let e=0;e<64;e+=1){let i=p+(l(f,6)^l(f,11)^l(f,25))+(f&c^~f&u)+t[e]+r[e]>>>0,o=(l(n,2)^l(n,13)^l(n,22))+(n&a^n&s^a&s)>>>0;p=u,u=c,c=f,f=d+i>>>0,d=s,s=a,a=n,n=i+o>>>0}for(let[e,t]of[n,a,s,d,f,c,u,p].entries())o[e]=o[e]+t>>>0}return o.map(e=>e.toString(16).padStart(8,"0")).join("")}(s);if(s.length!==r.byteLength||d!==r.sha256)throw Error("DEVUP_LARGE_VALUE_CHANGED");let f=Math.max(0,Math.floor(Number(r.offset)||0)),c=Math.min(65536,Math.max(1,Math.floor(Number(r.maxChunkBytes)||8192)));if(f>=s.length)throw Error("DEVUP_LARGE_VALUE_RANGE_INVALID");let u=Math.min(s.length,f+c);return{kind:"devupLargeValueFragment",fileKey:figma.fileKey||"",version:r.version,nodeId:r.nodeId,field:r.field,offset:f,nextOffset:u,byteLength:s.length,sha256:d,dataBase64:function(e){let t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",r="";for(let n=0;n>2],r+=t[(3&i)<<4|a>>4],r+=n+1>6]:"=",r+=n+210||t.some(e=>"string"!=typeof e||e.length>128||!/^(?:[0-9]+:[0-9]+|I[0-9]+:[0-9]+(?:;[0-9]+:[0-9]+)+)$/.test(e))||new Set(t).size!==t.length)throw Error("DEVUP_INVALID_INPUT: nodeIds must be 1..10 distinct Figma IDs");let r=(e,t)=>{try{let r=e[t];return"number"==typeof r&&Number.isFinite(r)?r:null}catch{return null}},n=(e,t)=>{try{let r=e[t];return"string"==typeof r&&r.length<=32&&/^[A-Z_]+$/.test(r)?r:null}catch{return null}},i=[];for(let e of t)try{let t=await figma.getNodeByIdAsync(e);if(!t||"DOCUMENT"===t.type||"PAGE"===t.type){i.push({nodeId:e,status:"unavailable",reason:t?"not-scene-node":"not-found"});continue}let a={width:r(t,"width"),height:r(t,"height"),padding:{top:r(t,"paddingTop"),right:r(t,"paddingRight"),bottom:r(t,"paddingBottom"),left:r(t,"paddingLeft")},gap:r(t,"itemSpacing"),layoutMode:n(t,"layoutMode"),align:{primary:n(t,"primaryAxisAlignItems"),counter:n(t,"counterAxisAlignItems")}},o=null!==a.width&&null!==a.height;i.push({nodeId:e,status:o?"measured":"partial",...a})}catch{i.push({nodeId:e,status:"unavailable",reason:"read-failed"})}return{kind:"devup-measure",schemaVersion:1,coordinateSpace:"figma-logical",nodes:i,partial:i.some(e=>"measured"!==e.status),evidenceLimit:"Null means unavailable or not applicable; no screenshot dimensions, descendants, theme, assets or code were read."}},metadata:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=await figma.getNodeByIdAsync(e.nodeId);if(!t)throw Error("DEVUP_NODE_NOT_FOUND");let r=t;for(;r&&"PAGE"!==r.type;)r=r.parent;r&&"PAGE"===r.type&&await figma.setCurrentPageAsync(r);let n=[];return!function(e){let t=[{node:e,visited:!1}],r=new Map;for(;t.length>0;){let e=t[t.length-1],i=e.node,a="children"in i?i.children:[];if(!e.visited){e.visited=!0;for(let e=a.length-1;e>=0;e-=1)t.push({node:a[e],visited:!1});continue}t.pop();let o=0,l=[];for(let e of a)l.push(e.id),o+=1+(r.get(e.id)||0);r.set(i.id,o),n.push({id:i.id,type:i.type,name:i.name,childrenIds:l,descendantCount:o})}}(t),{fileKey:figma.fileKey||"",version:null,rootId:t.id,nodes:n}},pageCatalog:async function(e){let t=figma.root.children;return{fileKey:figma.fileKey||"",version:null,rootIds:t.map(e=>e.id),nodes:t.map(e=>({id:e.id,type:e.type,fields:{name:e.name,parentId:null,childrenIds:[]},extra:{},fieldErrors:{}})),diagnostics:[]}},referencePng:async function(e){let t,r=e.nodeId;globalThis.__devupReferencePngAnswers||(globalThis.__devupReferencePngAnswers=new Map);let n=globalThis.__devupReferencePngAnswers,i=0;for(let[e,t]of n)Date.now()-t.at>12e4?n.delete(e):i+=t.text.length;if(i>=0xc000000)throw Error(`DEVUP_REFERENCE_PNG_BUSY ${i} characters are held for other reads`);let a=await figma.getNodeByIdAsync(r);if(!a||"function"!=typeof a.exportAsync)throw Error(`DEVUP_NODE_NOT_FOUND ${r}`);let o=a.absoluteRenderBounds||a.absoluteBoundingBox,l=Math.max(o.width,o.height),s=l>8192?8192/l:1,d=(e,t)=>({format:"PNG",constraint:t&&s<1?{type:o.width>=o.height?"WIDTH":"HEIGHT",value:8192}:{type:"SCALE",value:s},contentsOnly:e,useAbsoluteBounds:!1,colorProfile:"SRGB"}),f=e=>(Array.isArray(e)?e:[]).filter(e=>e&&!1!==e.visible),c=(e,t)=>!!e&&!!t&&e.x!!e&&e.x+t<=o.x&&e.y+t<=o.y&&e.x+e.width-t>=o.x+o.width&&e.y+e.height-t>=o.y+o.height,p=e=>`${e.type} ${JSON.stringify(e.name)}`,g=e=>JSON.parse(JSON.stringify(e===figma.mixed?null:e)),h=e=>(void 0===e.opacity||1===e.opacity)&&(!e.blendMode||["PASS_THROUGH","NORMAL"].includes(e.blendMode)),y=[],m=a.parent;for(;m&&"PAGE"!==m.type;)y.unshift(m),m=m.parent;let b=[255,255,255];for(let e of f(m&&m.backgrounds)){if("SOLID"!==e.type)continue;let t=void 0===e.opacity?1:e.opacity;["r","g","b"].forEach((r,n)=>{b[n]=t*e.color[r]*255+(1-t)*b[n]})}let x=0,E=e=>!1!==e.visible&&!e.isMask&&!!c(e.absoluteRenderBounds,o)&&((x+=1)>5e3||"TEXT"===e.type||!!f(e.fills).length||!!f(e.strokes).length||!!f(e.effects).length||"children"in e&&e.children.some(E)),A=e=>h(e)&&u(e.absoluteBoundingBox,0)&&1e-9>Math.abs(e.absoluteTransform[0][1])&&1e-9>Math.abs(e.absoluteTransform[1][0])&&0===Math.max(0,...[e.topLeftRadius,e.topRightRadius,e.bottomRightRadius,e.bottomLeftRadius].filter(e=>"number"==typeof e))&&f(e.fills).some(e=>"NORMAL"===(e.blendMode||"NORMAL")&&(void 0===e.opacity||1===e.opacity)&&("SOLID"===e.type||e.type.startsWith("GRADIENT_")&&e.gradientStops.every(e=>1===e.color.a))),I=[...y,a],S=[],w=[],v=new Map;for(let e=0;eA(e)&&i.slice(0,t).every(h));r.forEach((e,r)=>{e===t||(e.isMask&&!1!==e.visible&&r{let t=await a.exportAsync(d(!1,!0));return{kind:"devupReferencePng",nodeId:r,mimeType:"image/png",byteLength:t.length,data:figma.base64Encode(t),backdrop:N,...e}},T=(e,t)=>{let r=[...new Set(e)];return{approximate:r.join("; "),note:`Drawn as the canvas shows this area: devup-mcp cannot yet compose what get_screenshot draws here (${r.slice(0,3).join("; ")}). Layers reaching into it may show here: ${t}; get_screenshot leaves other layers out.`}},P=e=>{let t=JSON.stringify(e).replace(/[\u0080-\uffff]/g,e=>`\\u${e.charCodeAt(0).toString(16).padStart(4,"0")}`);if(t.length>0x5a00000){if(e.compose){let{compose:r,reaching:n,...i}=e;return P({...i,...T([`the answer would be ${t.length} characters`],n)})}throw Error(`DEVUP_REFERENCE_PNG_TOO_LARGE ${t.length} characters, over the 94371840 one answer is read back up to`)}if(t.length<=6291456)return e;let i=`${r}@${Date.now()}.${Math.random().toString(36).slice(2,10)}`;return n.set(i,{text:t,at:Date.now()}),{kind:"devupReferencePngChunked",nodeId:r,id:i,length:t.length}};if(0===S.length)return P(await O({}));let _=I.some(e=>e.isMask),C=[],M=new Set,k=(e,t)=>{let r=p(e);for(let t of f(e.fills))"IMAGE"===t.type?((t.rotation||f([t.filters]).some(e=>Object.values(e).some(e=>e)))&&C.push(`${r} has an image fill with a rotation or filters`),M.add(t.imageHash)):"SOLID"===t.type||t.type.startsWith("GRADIENT_")||C.push(`${r} is painted with ${t.type}`);for(let t of f(e.effects))["LAYER_BLUR","DROP_SHADOW","INNER_SHADOW","BACKGROUND_BLUR"].includes(t.type)||C.push(`${r} has a ${t.type}`);let n=f(e.strokes);if((e=>{let t=0===f(e.strokes).length?[]:[e.strokeWeight,e.strokeTopWeight,e.strokeRightWeight,e.strokeBottomWeight,e.strokeLeftWeight].filter(e=>"number"==typeof e&&e>0);if(0===t.length)return!1;let r=Math.max(...t),n="OUTSIDE"===e.strokeAlign?0:"CENTER"===e.strokeAlign?r/2:r,i="INSIDE"===e.strokeAlign?0:"CENTER"===e.strokeAlign?r/2:r,a=e.absoluteBoundingBox;return c(a&&{x:a.x-i,y:a.y-i,width:a.width+2*i,height:a.height+2*i},o)&&!u(a,n)})(e)){let t=["FRAME","RECTANGLE","COMPONENT","COMPONENT_SET","INSTANCE","SECTION"].includes(e.type),i=n.every(e=>"SOLID"===e.type||e.type.startsWith("GRADIENT_")),a=Array.isArray(e.dashPattern)&&e.dashPattern.length>0,o="INSIDE"===e.strokeAlign||!e.strokeJoin||"MITER"===e.strokeJoin;t&&i&&!a&&o||C.push(`${r}'s stroke reaches it`)}return{transform:g(e.absoluteTransform),width:e.width,height:e.height,radii:[e.topLeftRadius,e.topRightRadius,e.bottomRightRadius,e.bottomLeftRadius].map(e=>"number"==typeof e?e:0),fills:g(f(e.fills)),opacity:void 0===e.opacity?1:e.opacity,blendMode:e.blendMode||null,clips:!!e.clipsContent,effects:g(f(e.effects)),strokes:g(n),strokeAlign:e.strokeAlign||null,strokeWeights:["strokeTopWeight","strokeRightWeight","strokeBottomWeight","strokeLeftWeight"].map(t=>"number"==typeof e[t]?e[t]:"number"==typeof e.strokeWeight?e.strokeWeight:0)}},R=async e=>{let t=await e.exportAsync(d(!0,e===a));return{data:figma.base64Encode(t),area:e.absoluteRenderBounds||e.absoluteBoundingBox,blendMode:e.blendMode||null,size:[16,20].map(e=>(t[e]<<24|t[e+1]<<16|t[e+2]<<8|t[e+3])>>>0)}},D=async e=>{let t=v.get(e);if(!t)return null;let r=await t.exportAsync({...d(!0,!1),useAbsoluteBounds:!0});return{data:figma.base64Encode(r),area:t.absoluteBoundingBox,luminance:"LUMINANCE"===t.maskType}},L=[],$={},U={};try{for(let e of y){let t=k(e,!1),r=await D(e);r&&(t.mask=r),L.push(t)}let e=e=>f(e.effects).some(e=>"BACKGROUND_BLUR"===e.type);if(w.length>0&&(e(a)||"findOne"in a&&a.findOne(e))){let r="children"in a&&a.children.some(t=>e(t)||"findOne"in t&&!!t.findOne(e));if("children"in a&&!r&&!f(a.strokes).length){for(let e of($.own=k(a,!0),$.children=[],a.children)){if(!1===e.visible||e.isMask||!e.absoluteRenderBounds)continue;let t=await R(e);$.children.push({data:t.data,area:t.area,blendMode:t.blendMode})}t=[Math.ceil(o.width*s-1e-6),Math.ceil(o.height*s-1e-6)]}else C.push(`${p(a)} blurs what lies behind it`)}else if(_)t=[Math.ceil(o.width*s-1e-6),Math.ceil(o.height*s-1e-6)];else{let e=await R(a);$.picture={data:e.data,area:e.area,blendMode:e.blendMode},t=e.size}let r=await D(a);r&&($.mask=r),t&&t[0]*t[1]>8e6&&C.push(`the area is ${t[0]}x${t[1]} pixels`);let n=globalThis.__devupNormalizeImage,i=Date.now()+2e4,l=0;for(let e of M){let t=e&&figma.getImageByHash(e),r=t&&await t.getBytesAsync();if(!r){C.push(`the image ${e} could not be read`);continue}let a=null;if("function"==typeof n){let{width:e,height:o}=await t.getSizeAsync(),l=(...e)=>e.every((e,t)=>r[t]===e),s=l(137,80,78,71)||l(255,216)||l(71,73,70),d=Math.min(1,8192/Math.max(e,o,1)),f=Math.ceil(e*d)*Math.ceil(o*d),c=5*f+65536,u=5*r.length+4*e*o*(d<1?2:1)+4*f+4*c+1048576,p=i-Date.now();if((!s||Math.max(e,o)>8192||r.length>4*e*o+1048576)&&u<=0x20000000&&p>0){let t=await n(r,8192,p,e,o);t&&(r=t,a={width:e,height:o})}}if(l+r.length>0x4000000){C.push("its ancestors' images are over 67108864 bytes");break}l+=r.length,U[e]=a?{data:figma.base64Encode(r),...a}:figma.base64Encode(r)}}catch(e){C.push(`a part could not be exported: ${e instanceof Error?e.message:String(e)}`)}let B=`${S.slice(0,5).join(", ")}${S.length>5?` and ${S.length-5} more`:""}`;return C.length>0?P(await O(T(C,B))):P(await O({reaching:B,compose:{area:o,scale:s,width:t[0],height:t[1],page:b,layers:L,node:$,images:U}}))},referencePngChunk:async function(e){let t=e.largeValue,r=globalThis.__devupReferencePngAnswers,n=r&&r.get(t.id);if(!n||"string"!=typeof n.text)throw Error(`DEVUP_REFERENCE_PNG_ANSWER_GONE ${t.id}`);let i=n.text,a=Math.max(0,Math.floor(Number(t.offset)||0)),o=Math.min(i.length,a+4194304);return o>=i.length&&r.delete(t.id),{kind:"devupReferencePngChunk",id:t.id,offset:a,nextOffset:o,complete:o>=i.length,text:i.slice(a,o)}},search:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=await figma.getNodeByIdAsync(e.nodeId);if(!t)throw Error("DEVUP_NODE_NOT_FOUND");let r=t;for(;r&&"PAGE"!==r.type;)r=r.parent;if(!r||"PAGE"!==r.type)throw Error("DEVUP_PAGE_NOT_FOUND");await figma.setCurrentPageAsync(r);let n=e.search,i=new Set((n.nodeTypes.length?n.nodeTypes:["PAGE","SECTION","FRAME","COMPONENT_SET","COMPONENT"]).map(e=>e.toUpperCase()));function a(e){return e.normalize("NFC").toLocaleLowerCase().replace(/\s/gu,"")}let o=t===r?r.findAll(()=>!0):"findAll"in t?t.findAll(()=>!0):[],l=[t,...o].filter(e=>i.has(e.type)&&"string"==typeof e.name).map(e=>({node:e,score:function(e){let t=n.query.trim();if(""===t||"*"===t)return 100;if(e===n.query)return 400;if("exact"===n.matchKind)return null;let r=a(e),i=a(n.query);if(r===i)return 300;if(r.startsWith(i))return 200;if(r.includes(i))return 100;if("fuzzy"!==n.matchKind)return null;let o=function(e,t){let r=Array.from({length:t.length+1},(e,t)=>t);for(let n=0;nnull!==e.score).sort((e,t)=>t.score-e.score||e.node.name.localeCompare(t.node.name)||e.node.id.localeCompare(t.node.id)).slice(0,n.limit),s=new Map([[r.id,r]]);for(let e of[t,...l.map(e=>e.node)]){let t=e;for(;t&&"DOCUMENT"!==t.type;)s.set(t.id,t),t=t.parent}return{fileKey:figma.fileKey||"",version:null,rootIds:[r.id],nodes:[...s.values()].map(e=>({id:e.id,type:e.type,fields:{name:e.name,parentId:e.parent&&"DOCUMENT"!==e.parent.type?e.parent.id:null,childrenIds:"children"in e?e.children.map(e=>e.id):[]},extra:{},fieldErrors:{}})),diagnostics:[]}},sectionIndex:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=await figma.getNodeByIdAsync(e.nodeId);if(!t)throw Error("DEVUP_NODE_NOT_FOUND");if("SECTION"!==t.type){let e=t.parent,r=new Set;for(;e&&"SECTION"!==e.type&&!r.has(e.id);)r.add(e.id),e=e.parent;let n=e&&"SECTION"===e.type?e.id:null,i=n&&figma.fileKey?`https://www.figma.com/design/${figma.fileKey}?node-id=${n.replace(/:/g,"-")}`:null;throw Error("DEVUP_SECTION_REQUIRED "+JSON.stringify({pluginCode:"DEVUP_SECTION_REQUIRED",stage:"section-index",nodeId:t.id,nodeType:t.type,sectionId:n,nextAction:{tool:"devup_figma_export",how:n?`The url must point to a SECTION. This capture's SECTION is ${n}; select frames with frameIds.`:"The url must point to a SECTION. This node has no ancestor SECTION; choose the intended SECTION in Figma and copy its link.",arguments:i?"FRAME"===t.type&&t.parent===e?{url:i,frameIds:[t.id]}:{url:i}:null,requiredArguments:i?[]:["url"]}}))}let r=2048;function n(e){let t=e.absoluteBoundingBox||{x:"number"==typeof e.x?e.x:0,y:"number"==typeof e.y?e.y:0,width:"number"==typeof e.width?e.width:0,height:"number"==typeof e.height?e.height:0};return[t.x,t.y,t.width,t.height].every(Number.isFinite)?{x:t.x,y:t.y,width:t.width,height:t.height}:null}function i(e,t){if("FRAME"!==e.type||!1===e.visible||!t)return!1;let r=t.width/Math.max(1,t.height);return t.width>=240&&t.width<=1800&&t.height>=300&&t.height<=2e3&&r>=.25&&r<=2.5}function a(e){let t=0;for(let r=0;r=55296&&n<=56319&&r+1[e.id,t.id])),s=[],d=0;for(let e=0;ee.id));for(let r of t.children){if(e.has(r.id))continue;let t=n(r);t&&!1!==r.visible&&s.push({node:r,box:t})}}s.sort((e,t)=>e.box.y-t.box.y||e.box.x-t.box.x||e.node.id.localeCompare(t.node.id));let f=o.length>d||s.length>100,c=s.slice(0,100),u=new Set(c.map(({node:e})=>e.id)),p=new Set(e.rootIds),g={},h=new Map([[t.id,null]]);for(let e=0;e{let r=e.parent;for(;r&&r.id!==t.id&&!u.has(r.id);)r=r.parent;return[e.id,r&&u.has(r.id)?r.id:t.id]})),m=e=>c.filter(({node:t})=>y.get(t.id)===e).map(({node:e})=>e.id),b=n(t);if(!b)throw Error("DEVUP_NODE_BOUNDS_UNAVAILABLE");let x={id:t.id,type:t.type,fields:{name:t.name,parentId:t.parent&&"DOCUMENT"!==t.parent.type?t.parent.id:null,childrenIds:m(t.id),absoluteBoundingBox:b,visible:!1!==t.visible,projectionTruncated:f,nodeScreenIds:g},extra:{},fieldErrors:{}},E=c.length,A=c.map(({node:e,box:t})=>{let o,l=function(e){let t=[e],r=0,n=0;for(let e=0;er}}(e),s=Math.floor(r/E);E-=1;let d=function(e,t){let r=[e],n="",i=0,o=0,l=!1;if(t<=0)return{text:"",state:"budget-exhausted",spent:0};for(let e=0;e=120||o+e>t)return{text:n.trimEnd(),state:o+e>t?"budget-exhausted":"truncated",spent:o};n+=r,i+=1,o+=e}}if("children"in s){let e=Math.max(0,64-r.length);s.children.length>e&&(l=!0),r.push(...s.children.slice(0,e))}}}return{text:n,state:l?"truncated":n?"available":"no-text",spent:o}}(e,s);return r-=d.spent,{id:e.id,type:e.type,fields:{name:"string"==typeof e.name?e.name:"",parentId:y.get(e.id),childrenIds:m(e.id),absoluteBoundingBox:t,visible:!1!==e.visible,breadcrumb:function(e){let t=[],r=e;for(;r&&"DOCUMENT"!==r.type;)"string"==typeof r.name&&r.name&&t.push(r.name),r=r.parent;return t.reverse()}(e),directChildCount:"children"in e?e.children.length:0,textPreview:d.text,...null===(o=function(e){let t=[e],r=0,i=null;for(;t.length&&r++<400;){let e=t.shift();if("visible"in e&&!1===e.visible)continue;let r="children"in e?e.children.filter(e=>!("visible"in e)||!1!==e.visible):[];if(/header|nav|gnb/i.test(e.name||"")&&r.length>=2&&"layoutMode"in e&&"HORIZONTAL"===e.layoutMode){let t=r.map(n);t.every(Boolean)&&(i=Math.max(i||0,t.reduce((e,t)=>e+t.width,0)+(Number(e.itemSpacing)||0)*(t.length-1)))}t.push(...r.slice(0,80))}return i}(e))?{}:{navigationContentWidth:o},textPreviewState:d.state,subtreeNodeCount:l.subtreeNodeCount,estimatedSerializedBytes:l.estimatedSerializedBytes,selectionReasons:[i(e,t)?"screen-like":"explicit-selection-only","inside-section"],estimateTruncated:l.truncated},extra:{},fieldErrors:{}}}),I={fileKey:figma.fileKey||"",version:null,rootIds:[t.id],nodes:[x,...A],diagnostics:[]},S=a(JSON.stringify(I));for(let e=A.length-1;e>=0&&S>19456;e-=1)A[e].fields.textPreview="",A[e].fields.textPreviewState="budget-exhausted",S=a(JSON.stringify(I));let w=()=>I.nodes.length-1,v=A.length;for(;S>19456&&w()>1;){let e=I.nodes.pop();x.fields.projectionTruncated=!0,x.fields.childrenIds=x.fields.childrenIds.filter(t=>t!==e.id),S=a(JSON.stringify(I))}if(S>19456)throw Error("DEVUP_SECTION_INDEX_TOO_LARGE "+JSON.stringify({pluginCode:"DEVUP_SECTION_INDEX_TOO_LARGE",stage:"section-index",responseBytes:S,maxResponseBytes:19456,discoveredCandidates:v,retainedCandidates:w()}));return I},snapshot:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=await figma.getNodeByIdAsync(e.nodeId);if(!t)throw Error("DEVUP_NODE_NOT_FOUND");let r=["absoluteBoundingBox","absoluteRenderBounds","arcData","backgroundStyleId","blendMode","bottomLeftRadius","bottomRightRadius","boundVariables","characters","clipsContent","componentProperties","componentPropertyDefinitions","componentPropertyReferences","constraints","cornerRadius","counterAxisAlignItems","dashPattern","defaultVariant","effectStyleId","effects","fillStyleId","fills","fontName","fontSize","gridColumnAnchorIndex","gridColumnCount","gridColumnGap","gridColumnSizes","gridColumnSpan","gridRowAnchorIndex","gridRowCount","gridRowGap","gridRowSizes","gridRowSpan","gridStyleId","height","inferredAutoLayout","isAsset","isMask","itemSpacing","layoutGrow","layoutMode","layoutWrap","layoutPositioning","layoutSizingHorizontal","layoutSizingVertical","letterSpacing","lineHeight","maxHeight","maxLines","maxWidth","minHeight","minWidth","name","opacity","overflowDirection","paddingBottom","paddingLeft","paddingRight","paddingTop","primaryAxisAlignItems","reactions","rotation","strokeAlign","strokeBottomWeight","strokeLeftWeight","strokeRightWeight","strokeStyleId","strokeTopWeight","strokeWeight","strokes","targetAspectRatio","textAlignHorizontal","textAlignVertical","textAutoResize","textCase","textDecoration","textStyleId","textTruncation","topLeftRadius","topRightRadius","variantProperties","visible","width","x","y"],n=new Set(r),i=["fontName","fontWeight","fontSize","textDecoration","textCase","lineHeight","letterSpacing","fills","textStyleId","fillStyleId","listOptions","indentation","hyperlink"],a=e.snapshot,o=Math.max(0,Math.floor(Number(a.offset)||0)),l=Math.min(a.maxEnvelopeBytes?Math.max(8192,Math.floor(Number(a.maxEnvelopeBytes))):16e3,Math.max(4096,Math.floor(Number(a.maxPayloadBytes)||15e3))),s=Math.min(l-1024,Math.max(512,Math.floor(Number(a.maxFieldBytes)||4096))),d=new Set(["id","type","parent","children","mainComponent","masterComponent","instances"]),f=new Set(["parentId","childrenIds","name","characters","styledTextSegments","boundVariables"]);function c(e,t,r){let n=function(e){let t=[];for(let r=0;r=55296&&n<=56319){let t=r+1=56320&&t<=57343?(n=65536+(n-55296<<10)+(t-56320),r+=1):n=65533}else n>=56320&&n<=57343&&(n=65533);n<128?t.push(n):n<2048?t.push(192|n>>6,128|63&n):n<65536?t.push(224|n>>12,128|n>>6&63,128|63&n):t.push(240|n>>18,128|n>>12&63,128|n>>6&63,128|63&n)}return new Uint8Array(t)}(JSON.stringify(r));return n.length>0x1000000?{$truncated:"max-large-value-bytes",byteLength:n.length}:{$largeValue:{nodeId:e,field:t,byteLength:n.length,sha256:function(e){let t=[0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,0xe49b69c1,0xefbe4786,0xfc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x6ca6351,0x14292967,0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2],r=64*Math.ceil((e.length+9)/64),n=new Uint8Array(r);n.set(e),n[e.length]=128;let i=8*e.length;for(let e=0;e<8;e+=1)n[r-1-e]=255&Math.floor(i/2**(8*e));let a=[0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19],o=(e,t)=>e>>>t|e<<32-t;for(let e=0;e>>3,n=o(r[e-2],17)^o(r[e-2],19)^r[e-2]>>>10;r[e]=r[e-16]+t+r[e-7]+n>>>0}let[i,l,s,d,f,c,u,p]=a;for(let e=0;e<64;e+=1){let n=p+(o(f,6)^o(f,11)^o(f,25))+(f&c^~f&u)+t[e]+r[e]>>>0,a=(o(i,2)^o(i,13)^o(i,22))+(i&l^i&s^l&s)>>>0;p=u,u=c,c=f,f=d+n>>>0,d=s,s=l,l=i,i=n+a>>>0}for(let[e,t]of[i,l,s,d,f,c,u,p].entries())a[e]=a[e]+t>>>0}return a.map(e=>e.toString(16).padStart(8,"0")).join("")}(n),cursor:{nextOffset:0,maxChunkBytes:12288}}}}function u(e){return function(e){let t=0;for(let r=0;r=55296&&n<=56319&&r+112)return{$truncated:"max-depth"};if("object"==typeof t&&"parent"in t&&"string"==typeof t.id&&"string"==typeof t.type)return{$nodeId:t.id,$nodeType:t.type};if(Array.isArray(t))return t.map(t=>e(t,r,n+1));if(ArrayBuffer.isView(t))return{$binary:t.constructor.name,byteLength:t.byteLength};if(t instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:t.byteLength};if(r.has(t))return{$circular:!0};r.add(t);let i={};for(let a of Object.keys(t).sort())try{let o=e(t[a],r,n+1);o&&"function"===o.$unsupported||(i[a]=o)}catch(e){i[a]={$error:String(e&&e.message?e.message:e)}}return r.delete(t),i}(r),a=u(i);if(a<=s)return i;let o=c(e,t,i);return o.$truncated&&(n[t]=`DEVUP_FIELD_VALUE_UNSUPPORTED:${a}>16777216`),o}let g=new Map;async function h(e,t,r){if("INSTANCE"!==e.type||"function"!=typeof e.getMainComponentAsync)return;g.has(e.id)||g.set(e.id,(async()=>{try{let t=await e.getMainComponentAsync();return{fields:{mainComponentId:t?t.id:null,mainComponentName:t?t.name:null,mainComponentSetId:t&&t.parent&&"COMPONENT_SET"===t.parent.type?t.parent.id:null}}}catch(e){return{error:String(e&&e.message?e.message:e)}}})());let n=await g.get(e.id);n.error?r.mainComponentId=n.error:Object.assign(t,n.fields)}async function y(e){let t={},a={},o={};for(let i of(t.parentId=e.parent?e.parent.id:null,e.parent&&("PAGE"===e.parent.type||"SECTION"===e.parent.type||"COMPONENT_SET"===e.parent.type)&&(t.parentType=e.parent.type,"SECTION"===e.parent.type&&(t.parentName=e.parent.name)),t.childrenIds="children"in e?e.children.map(e=>e.id):[],!("overflowDirection"in e)&&["FRAME","COMPONENT","INSTANCE","COMPONENT_SET"].includes(e.type)&&(t.overflowDirection=null),function(e){let t=new Set,n=e;for(;n&&n!==Object.prototype;){for(let e of Object.getOwnPropertyNames(n))t.add(e);n=Object.getPrototypeOf(n)}for(let n of r)try{n in e&&t.add(n)}catch(e){}return[...t].sort()}(e)))if(!(d.has(i)||i.startsWith("_")||"componentPropertyDefinitions"===i&&"COMPONENT"===e.type&&e.parent&&"COMPONENT_SET"===e.parent.type))try{let r=e[i];if("function"==typeof r)continue;let l=p(e.id,i,r,o);(n.has(i)?t:a)[i]=l}catch(e){o[i]=String(e&&e.message?e.message:e)}if("TEXT"===e.type&&"function"==typeof e.getStyledTextSegments)try{t.styledTextSegments=p(e.id,"styledTextSegments",e.getStyledTextSegments(i),o)}catch(e){o.styledTextSegments=String(e&&e.message?e.message:e)}return await h(e,t,o),{id:e.id,type:e.type,fields:t,extra:a,fieldErrors:o}}let m=[],b=[t];for(;b.length;){let e=b.shift();m.push(e),"children"in e&&b.push(...e.children)}let x=[],E=l-1024,A=2;for(let e=o;eNumber(e.protected)-Number(t.protected)||t.byteLength-e.byteLength),n)){if(r<=t)break;let n=c(e.id,i.name,e[i.sectionName][i.name]);e[i.sectionName][i.name]=n,n.$truncated&&(e.fieldErrors[i.name]=`DEVUP_FIELD_VALUE_UNSUPPORTED:${i.byteLength}>16777216`),r=u(e)}return e}(await y(m[e]),E),r=u(t)+ +!!x.length;if(x.length&&A+r>E)break;x.push(t),A+=r}let I=Math.min(m.length,o+x.length);return x.push({id:"__DEVUP_SNAPSHOT_CURSOR__",type:"DEVUP_INTERNAL",fields:{offset:o,nextOffset:I,complete:I>=m.length,totalNodes:m.length},extra:{},fieldErrors:{}}),{fileKey:figma.fileKey||"",version:null,rootIds:[t.id],nodes:x,diagnostics:[]}},usedResources:async function(e){let t=e.resources;function r(e,t=new WeakSet,n=0){if(null===e||["string","number","boolean"].includes(typeof e))return e;if(void 0===e)return{$undefined:!0};if("bigint"==typeof e)return{$bigint:e.toString()};if(["function","symbol"].includes(typeof e))return{$unsupported:typeof e};if(n>12)return{$truncated:"max-depth"};if("object"==typeof e&&"parent"in e&&"string"==typeof e.id&&"string"==typeof e.type)return{$nodeId:e.id,$nodeType:e.type};if(Array.isArray(e))return e.map(e=>r(e,t,n+1));if(ArrayBuffer.isView(e))return{$binary:e.constructor.name,byteLength:e.byteLength};if(e instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:e.byteLength};if(t.has(e))return{$circular:!0};t.add(e);let i={},a=new Set(Object.keys(e)),o=e;for(;o&&o!==Object.prototype;){for(let e of Object.getOwnPropertyNames(o))a.add(e);o=Object.getPrototypeOf(o)}for(let o of[...a].sort())if(!(o.startsWith("_")||["parent","children","consumers"].includes(o)))try{let a=r(e[o],t,n+1);a&&"function"===a.$unsupported||(i[o]=a)}catch(e){i[o]={$error:"unavailable"}}return t.delete(e),i}let n=new Map,i="ok";try{for(let e of(await figma.variables.getLocalVariablesAsync()))n.set(e.id,e)}catch(e){i="unavailable"}function a(e){return Object.values(e.valuesByMode||{}).filter(e=>e&&"VARIABLE_ALIAS"===e.type&&"string"==typeof e.id&&e.id).map(e=>e.id)}async function o(e){let t=n.get(e);if(t)return{value:r(t),collectionId:t.variableCollectionId,aliasIds:a(t)};try{let t=await figma.variables.getVariableByIdAsync(e);return t?{value:r(t),collectionId:t.variableCollectionId,aliasIds:a(t)}:{unresolved:{id:e,kind:"variable",reason:"notInFileAndLookupEmpty"}}}catch(t){return{unresolved:{id:e,kind:"variable",reason:"notInFileAndLookupThrew",error:String(t&&t.message||t).slice(0,300)}}}}let l=await Promise.all(t.variableIds.map(o)),s=new Set(t.variableIds),d=l.flatMap(e=>e.aliasIds||[]);for(;d.length>0;){let e=[...new Set(d)].filter(e=>!s.has(e)).sort();for(let t of e)s.add(t);let t=await Promise.all(e.map(o));l.push(...t),d=t.flatMap(e=>e.aliasIds||[])}let f=[...new Set(l.flatMap(e=>e.collectionId?[e.collectionId]:[]))].sort(),c=await Promise.all(f.map(async e=>{try{let t=await figma.variables.getVariableCollectionByIdAsync(e);return t?[r(t)]:[]}catch(e){return[]}})),u=await Promise.all(t.styles.map(async e=>{try{let t=await figma.getStyleByIdAsync(e.id);if(!t)return{unresolved:{id:e.id,kind:"style",reason:"notFoundOrUnavailable"}};return{value:{...r(t),styleType:e.styleType,value:r("PAINT"===e.styleType?t.paints:"EFFECT"===e.styleType?t.effects:"GRID"===e.styleType?t.layoutGrids:t)}}}catch(t){return{unresolved:{id:e.id,kind:"style",reason:"notFoundOrUnavailable",error:String(t&&t.message||t).slice(0,300)}}}}));return{collections:c.flat(),variables:l.flatMap(e=>e.value?[e.value]:[]),styles:u.flatMap(e=>e.value?[e.value]:[]),usedVariableIds:t.variableIds,usedStyleIds:t.styles.map(e=>e.id),localVariableListing:i,localVariableCount:n.size,unresolved:[...l,...u].flatMap(e=>e.unresolved?[e.unresolved]:[])}},variableCatalog:async function(e){let[t,r,n,i,a]=await Promise.all([figma.variables.getLocalVariableCollectionsAsync(),figma.getLocalPaintStylesAsync(),figma.getLocalTextStylesAsync(),figma.getLocalEffectStylesAsync(),figma.getLocalGridStylesAsync()]),o=["PAINT","TEXT","EFFECT","GRID"];return{collections:t.map(e=>(function e(t,r=new WeakSet,n=0){if(null===t||["string","number","boolean"].includes(typeof t))return t;if(void 0===t)return{$undefined:!0};if("bigint"==typeof t)return{$bigint:t.toString()};if(["function","symbol"].includes(typeof t))return{$unsupported:typeof t};if(n>12)return{$truncated:"max-depth"};if(Array.isArray(t))return t.map(t=>e(t,r,n+1));if(r.has(t))return{$circular:!0};r.add(t);let i={},a=new Set(Object.keys(t)),o=t;for(;o&&o!==Object.prototype;){for(let e of Object.getOwnPropertyNames(o))a.add(e);o=Object.getPrototypeOf(o)}for(let o of[...a].sort())if(!o.startsWith("_"))try{let a=e(t[o],r,n+1);a&&"function"===a.$unsupported||(i[o]=a)}catch(e){i[o]={$error:String(e&&e.message?e.message:e)}}return r.delete(t),i})(e)),variableIds:[...new Set(t.flatMap(e=>e.variableIds))].sort(),styles:[r,n,i,a].flatMap((e,t)=>e.map(e=>({id:e.id,styleType:o[t]}))).sort((e,t)=>e.id.localeCompare(t.id)),localComplete:!0,usedRemoteComplete:!1}},variables:async function(e){let t=e.resources;function r(e,t=new WeakSet,n=0){if(null===e||["string","number","boolean"].includes(typeof e))return e;if(void 0===e)return{$undefined:!0};if("bigint"==typeof e)return{$bigint:e.toString()};if(["function","symbol"].includes(typeof e))return{$unsupported:typeof e};if(n>12)return{$truncated:"max-depth"};if("object"==typeof e&&"parent"in e&&"string"==typeof e.id&&"string"==typeof e.type)return{$nodeId:e.id,$nodeType:e.type};if(Array.isArray(e))return e.map(e=>r(e,t,n+1));if(ArrayBuffer.isView(e))return{$binary:e.constructor.name,byteLength:e.byteLength};if(e instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:e.byteLength};if(t.has(e))return{$circular:!0};t.add(e);let i={},a=new Set(Object.keys(e)),o=e;for(;o&&o!==Object.prototype;){for(let e of Object.getOwnPropertyNames(o))a.add(e);o=Object.getPrototypeOf(o)}for(let o of[...a].sort())if(!(o.startsWith("_")||["parent","children","consumers"].includes(o)))try{let a=r(e[o],t,n+1);a&&"function"===a.$unsupported||(i[o]=a)}catch(e){i[o]={$error:String(e&&e.message?e.message:e)}}return t.delete(e),i}let[n,i]=await Promise.all([Promise.all(t.variableIds.map(e=>figma.variables.getVariableByIdAsync(e))),Promise.all(t.styles.map(e=>figma.getStyleByIdAsync(e.id)))]),a=new Map(t.styles.map(e=>[e.id,e])),o=await Promise.all(i.filter(Boolean).map(async e=>{let t=a.get(e.id),n=t.styleType;if(Number.isInteger(t.consumerStart)&&Number.isInteger(t.consumerEnd)){let i=await e.getStyleConsumersAsync();return{id:e.id,styleType:n,$consumerStart:t.consumerStart,$consumerEntries:i.slice(t.consumerStart,t.consumerEnd).map(e=>[e.node.id,e.node.type,r(e.fields)])}}let i=await e.getStyleConsumersAsync();return{...r(e),styleType:n,$consumerCount:i.length,value:r("PAINT"===n?e.paints:"EFFECT"===n?e.effects:"GRID"===n?e.layoutGrids:e)}}));return{variables:n.filter(Boolean).map(e=>r(e)),styles:o}}},t=new Map,r=0;globalThis.__devupNormalizeImage=(e,n,i,a,o)=>new Promise(l=>{r+=1;let s=`decode-${r}`,d=setTimeout(()=>{t.delete(s),l(null)},i);t.set(s,e=>{clearTimeout(d),t.delete(s),l(e)}),figma.ui.postMessage({kind:"devup-decode",id:s,bytes:e,maxEdge:n,width:a,height:o})});let n=globalThis;function i(){let e=figma.currentPage,t=e.selection;return t.length>0&&(n.__devupLastSelection=t.slice(0,20).map(e=>({id:e.id,name:e.name.slice(0,256),type:e.type})),n.__devupLastSelectionPage={id:e.id,name:e.name.slice(0,256)}),{zoom:Number.isFinite(figma.viewport?.zoom)?figma.viewport.zoom:null,observedAt:Date.now(),lastKnownSelection:n.__devupLastSelection??null,lastKnownPage:n.__devupLastSelectionPage??null,currentPage:{id:e.id,name:e.name.slice(0,256)},selection:t.slice(0,20).map(e=>({id:e.id,name:e.name.slice(0,256),type:e.type})),selectionCount:t.length}}function a(){let e={kind:"devup-context",...i()};figma.ui.postMessage(e)}async function o(t){let r=e[t.script];if(!r)return{kind:"devup-result",requestId:t.requestId,error:`DEVUP_BRIDGE_UNKNOWN_SCRIPT: ${t.script} (아는 스크립트: ${Object.keys(e).join(", ")})`};try{var n;"bridgeContext"===t.script&&i();let e=await r({nodeId:(n=t.params).nodeId??"",rootIds:n.rootIds??[],snapshot:n.snapshot??{},search:n.search??{},explore:n.explore??{},resources:n.resources??{variableIds:[],styles:[]},largeValue:n.largeValue??{},asset:n.asset??{},theme:n.theme??{offset:0}});return{kind:"devup-result",requestId:t.requestId,data:e}}catch(e){return{kind:"devup-result",requestId:t.requestId,error:e instanceof Error?e.message:String(e)}}}let l=[],s=!1,d=null,f=new Set;async function c(){if(!s){s=!0;try{for(;l.length>0;){let e=l.shift();d=e.requestId;let t=await o(e).catch(t=>({kind:"devup-result",requestId:e.requestId,error:t instanceof Error?t.message:String(t)}));d=null,f.delete(e.requestId)||figma.ui.postMessage(t)}}finally{s=!1}}}let u=null;function p(){u??(u=(async()=>{try{await figma.loadAllPagesAsync(),await Promise.all([figma.variables.getLocalVariableCollectionsAsync(),figma.variables.getLocalVariablesAsync(),figma.getLocalPaintStylesAsync(),figma.getLocalTextStylesAsync(),figma.getLocalEffectStylesAsync(),figma.getLocalGridStylesAsync()])}catch{}finally{u=null}})())}figma.showUI(__html__,{width:320,height:220}),p(),figma.on("selectionchange",a),figma.on("currentpagechange",a),figma.ui.onmessage=e=>{if("object"==typeof e&&null!==e){if("devup-ready"===e.kind){let e={kind:"devup-status",fileKey:figma.fileKey??null,fileName:figma.root.name,port:1993,...i()};figma.ui.postMessage(e);return}if("devup-job"===e.kind){l.push(e),c();return}if("devup-cancel"===e.kind){var r;let t;return void(r=e.requestId,(t=l.findIndex(e=>e.requestId===r))>=0?l.splice(t,1):d===r&&f.add(r))}if("devup-decoded"===e.kind)return void t.get(e.id)?.(e.png??null);"devup-in-view"===e.kind&&p()}}})(); \ No newline at end of file +(()=>{"use strict";let e={assets:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=e.asset;function r(e){let t=[0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,0xe49b69c1,0xefbe4786,0xfc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x6ca6351,0x14292967,0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2],r=64*Math.ceil((e.length+9)/64),n=new Uint8Array(r);n.set(e),n[e.length]=128;let i=8*e.length;for(let e=0;e<8;e+=1)n[r-1-e]=255&Math.floor(i/2**(8*e));let a=[0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19],o=(e,t)=>e>>>t|e<<32-t;for(let e=0;e>>3,n=o(r[e-2],17)^o(r[e-2],19)^r[e-2]>>>10;r[e]=r[e-16]+t+r[e-7]+n>>>0}let[i,l,s,d,f,c,u,p]=a;for(let e=0;e<64;e+=1){let n=p+(o(f,6)^o(f,11)^o(f,25))+(f&c^~f&u)+t[e]+r[e]>>>0,a=(o(i,2)^o(i,13)^o(i,22))+(i&l^i&s^l&s)>>>0;p=u,u=c,c=f,f=d+n>>>0,d=s,s=l,l=i,i=n+a>>>0}for(let[e,t]of[i,l,s,d,f,c,u,p].entries())a[e]=a[e]+t>>>0}return a.map(e=>e.toString(16).padStart(8,"0")).join("")}async function n(e){try{let t=await figma.getNodeByIdAsync(e);if(t)return t}catch(r){if(!e.startsWith("I")||!e.includes(";"))throw r;try{return await t()}catch(e){throw Error(`${r.message||r}; nested lookup fallback: ${e.message||e}`)}}if(!e.startsWith("I")||!e.includes(";"))return null;return t();async function t(){let t=await figma.getNodeByIdAsync(e.slice(1).split(";")[0]),r=t?[t]:[],n=new Set;for(;r.length;){let t=r.pop();if(t.id===e)return t;if(!n.has(t.id)){if(n.add(t.id),n.size>1e4)throw Error("nested lookup exceeds 10000 nodes");Array.isArray(t.children)&&r.push(...t.children)}}throw Error(`nested node ${e} not found in its instance`)}}async function i(e,t,r=85e3,n=()=>{}){let a,o=Date.now();try{let i=await Promise.race([e.exportAsync(t),new Promise((e,t)=>{a=setTimeout(()=>t(Error(`DEVUP_ASSET_TIMEOUT: native export exceeded ${r} ms`)),r)})]);return n("native-export",Date.now()-o),i}catch(i){if(n("native-export",Date.now()-o,String(i.message||i).slice(0,500)),"SVG_STRING"!==t.format)throw i;let r=Date.now();try{let t=function(e){let t=e=>"number"==typeof e&&Number.isFinite(e),r=(e,t)=>{throw Error(`geometry fallback unsupported: ${e.type||"unknown"} ${e.id||""}: ${t}`)},n=0,i=0;function a(e,i,a){(void 0!==a&&!Array.isArray(a)||void 0===a&&Array.isArray(i)&&i.length)&&r(e,"unresolved paint requires native export");let o=Array.isArray(a)?a.filter(e=>!1!==e.visible):[];return o.length?(Array.isArray(i)&&i.length||r(e,"paint has no rendered geometry"),o.map(a=>{("SOLID"!==a.type||a.blendMode&&"NORMAL"!==a.blendMode)&&r(e,"paint type or blending requires native export");let o=a.color;o&&[o.r,o.g,o.b].every(e=>t(e)&&e>=0&&e<=1)||r(e,"invalid solid color");let l=void 0===a.opacity?1:a.opacity;return(!t(l)||l<0||l>1)&&r(e,"invalid paint opacity"),["NONZERO","EVENODD"].map(t=>{let a=i.filter(e=>e.windingRule===t);if(!a.length)return"";for(let t of a)"string"==typeof t.data&&t.data.trim()||r(e,"empty path"),n++;return`"']/g,e=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[e])}" fill="rgb(${100*o.r}%,${100*o.g}%,${100*o.b}%)" fill-opacity="${l}" fill-rule="${"EVENODD"===t?"evenodd":"nonzero"}"/>`}).join("")}).join("")):""}let o=e.absoluteRenderBounds,l=e.absoluteTransform;(!o||![o.x,o.y,o.width,o.height].every(t)||o.width<=0||o.height<=0)&&r(e,"missing render bounds"),(!Array.isArray(l)||2!==l.length||!l.every(e=>Array.isArray(e)&&3===e.length&&e.every(t))||0!==l[0][1]||0!==l[1][0]||l[0][0]<=0||l[1][1]<=0)&&r(e,"root rotation, reflection or shear requires native export");let s=(o.x-l[0][2])/l[0][0],d=(o.y-l[1][2])/l[1][1],f=o.width/l[0][0],c=o.height/l[1][1],u=function e(n,o){if(++i>1e4&&r(n,"subtree exceeds 10000 nodes"),n.isMask&&r(n,"mask requires native export"),!1===n.visible||0===n.opacity)return"";n.clipsContent&&r(n,"clipping requires native export"),Array.isArray(n.effects)&&n.effects.some(e=>!1!==e.visible)&&r(n,"effect requires native export"),n.blendMode&&!["NORMAL","PASS_THROUGH"].includes(n.blendMode)&&r(n,"layer blending requires native export");let l=void 0===n.opacity?1:n.opacity;(!t(l)||l<0||l>1)&&r(n,"invalid layer opacity");let s="";if(!o){let e=n.relativeTransform;Array.isArray(e)&&2===e.length&&e.every(e=>Array.isArray(e)&&3===e.length&&e.every(t))||r(n,"missing relative transform"),s=` transform="matrix(${e[0][0]} ${e[1][0]} ${e[0][1]} ${e[1][1]} ${e[0][2]} ${e[1][2]})"`}let d="";if(["GROUP","FRAME","INSTANCE","COMPONENT"].includes(n.type))(Array.isArray(n.fills)&&n.fills.some(e=>!1!==e.visible)||Array.isArray(n.strokes)&&n.strokes.some(e=>!1!==e.visible))&&r(n,"container paint requires native export"),Array.isArray(n.children)||r(n,"missing children"),d=(!0===n.itemReverseZIndex?[...n.children].reverse():n.children).map(t=>e(t,!1)).join("");else{for(let e of(["VECTOR","BOOLEAN_OPERATION","RECTANGLE","ELLIPSE","POLYGON","STAR","LINE"].includes(n.type)||r(n,"node type requires native export"),Array.isArray(n.strokes)&&n.strokes.some(e=>!1!==e.visible)&&"CENTER"!==n.strokeAlign&&r(n,"non-center stroke alignment requires native export"),Array.isArray(n.dashPattern)&&n.dashPattern.length&&r(n,"dashed stroke requires native export"),[n.fillGeometry,n.strokeGeometry]))void 0!==e&&(!Array.isArray(e)||e.some(e=>!["NONZERO","EVENODD"].includes(e.windingRule)))&&r(n,"unknown winding rule");d=a(n,n.fillGeometry,n.fills)+a(n,n.strokeGeometry,n.strokes)}return`${d}`}(e,!0);return n||r(e,"no painted geometry"),`${u}`}(e);return n("geometry-fallback",Date.now()-r),t}catch(e){throw Error(`native export: ${i.message||i}; ${e.message||e}`)}}finally{clearTimeout(a)}}let a=Date.now(),o="lookup",l="unknown",s=Math.min(6e5,Math.max(1e3,Number(t.timeoutMs)||9e4)),d={method:"native",stages:[]};function f(e,t,r){d.stages.push({stage:e,elapsedMs:t}),r&&(d.nativeError=r),"geometry-fallback"===e&&(d.method="geometry")}async function c(e){let t;try{return await Promise.race([e(),new Promise((e,r)=>{t=setTimeout(()=>r(Error(`DEVUP_ASSET_TIMEOUT: ${o} exceeded ${s} ms asset budget`)),Math.max(0,s-(Date.now()-a)))})])}finally{clearTimeout(t)}}function u(e){let r=e&&"string"==typeof e.message?e.message:String(e),n="string"==typeof figma.editorType?` (editor: ${figma.editorType})`:"";return`${o} failed after ${Date.now()-a} ms${n} (node: ${t.nodeId}, type: ${l}): ${r}`.slice(0,500)}function p(e,r){return e&&d.renderInfo&&(d.renderInfo.attemptedRepresentation=d.renderInfo.representation,d.renderInfo.representation="not-exported",d.renderInfo.renderedAs="not-exported",d.renderInfo.effectsBakedIntoAsset=!1,d.renderInfo.fidelity="unverified"),{kind:"devupAssetExport",fileKey:figma.fileKey||"",version:t.version,assetId:t.assetId,nodeId:t.nodeId,field:t.field,imageHash:t.imageHash,format:t.format,scale:t.scale,renderMode:h,usageAspectRatio:t.usageAspectRatio??null,maxWidth:t.maxWidth??null,srcset:!0===t.srcset,visibleScopeNodeId:t.visibleScopeNodeId??null,status:"failed",byteLength:null,sha256:null,errorCode:e,exportDiagnostics:d,...r?{errorDetail:r}:{}}}let g="string"==typeof t.field&&t.field.startsWith("$original-image/fills/"),h=void 0===t.renderMode?g?"fill":["svg","pdf"].includes(t.format)?"node":"original-source":t.renderMode,y=["auto","original-source"].includes(h)?"auto"===h?"node":"fill":h;function m(e,r){let n=e=>"number"==typeof e&&Number.isFinite(e)&&e>0,i=Array.isArray(e.effects)?e.effects:null,a=[],o=e=>[...e].slice(0,128).join(""),l=["string"==typeof e.name?o(e.name):"layer"],s=new Set,d=e;for(let t=e.parent;t&&a.length<32&&!s.has(t);t=t.parent)s.add(t),l.length<4&&"string"==typeof t.name&&t.name.length&&!["PAGE","DOCUMENT"].includes(t.type)&&l.push(o(t.name)),a.push({type:t.type||"unknown",clipsContent:t.clipsContent??null,isMask:t.isMask??null,effects:Array.isArray(t.effects)?t.effects:null,opacity:t.opacity??null,blendMode:t.blendMode??null,visible:t.visible??null,siblingMaskPresent:Array.isArray(t.children)?t.children.some(e=>e!==d&&!0===e.isMask):null}),d=t;return{sourceKind:r?"image-fill":t.imageHash?"image-node":"vector-node",renderedAs:"fill"===y?"raw-fill":"node-render",representation:"fill"===y?"original-fill":"native-node",layerHierarchy:l.reverse(),scope:"fill"===y?"selected-paint-original-bytes":"exact-node-subtree",exportSettings:"node"===y?{contentsOnly:!0,useAbsoluteBounds:!1}:null,requestedScale:t.scale,scaleSemantics:"fill"===y?"intrinsic-original-bytes-no-resize":["png","jpg"].includes(t.format)?"native-node-SCALE-constraint":"not-applied-to-vector-document",nodeSize:n(e.width)&&n(e.height)?{width:e.width,height:e.height,aspectRatio:e.width/e.height}:null,outputSize:null,dimensionEvidence:"unknown-until-byte-inspection",aspectRatioComparison:"unknown",scaleMode:r?r.scaleMode??null:null,imageTransform:r?r.imageTransform??null:null,visibleSourceRatio:null,visibleSourceRatioEvidence:"not-computed",effects:i,extraFills:Array.isArray(e.fills)?Math.max(0,e.fills.filter(e=>!1!==e.visible).length-!!r):null,effectsBakedIntoAsset:"node"===y&&["png","jpg"].includes(t.format)&&null!==i,bakedEffects:"node"===y&&["png","jpg"].includes(t.format)&&i?i.filter(e=>!1!==e.visible):[],instanceOverride:"unknown",instanceOverrideEvidence:"no-component-paint-comparison",sourceProperties:{scaleMode:r?r.scaleMode??null:null,imageTransform:r?r.imageTransform??null:null,rotation:r?r.rotation??null:null,filters:r?r.filters??null:null,opacity:r?r.opacity??null:null,effects:i,blurEffects:i?i.filter(e=>!1!==e.visible&&String(e.type).includes("BLUR")):null,isMask:e.isMask??null,clipsContent:e.clipsContent??null,ancestors:a},unappliedProperties:"fill"===y?["scaleMode","imageTransform","rotation","filters","paintOpacity","nodeGeometry","nodeTransforms","nodeEffectsIncludingBlur","nodeAndAncestorMasks","ancestorClipsAndEffects"]:[],warnings:"fill"===y?["original-fill-properties-unapplied","aspect-ratio-unknown"]:["ancestor-canvas-appearance-unverified","aspect-ratio-unknown"],fidelity:"unverified"}}function b(e,t){let r=0;for(let n=e;n&&r++<32;n=n.parent){if("INSTANCE"===n.type&&Array.isArray(n.overrides)&&n.overrides.find(t=>t.id===e.id&&Array.isArray(t.overriddenFields)&&t.overriddenFields.includes("fills"))){t.instanceOverride=!0,t.instanceOverrideEvidence={kind:"figma-instance-overrides",instanceId:n.id,nodeId:e.id,overriddenFields:["fills"]};return}if(["PAGE","DOCUMENT"].includes(n.type))break}}try{let e;if(!["original-source","fill","node","visible","auto"].includes(h))return p("DEVUP_ASSET_RENDER_MODE_UNSUPPORTED");if(!g&&"fill"===h&&(!String(t.field).startsWith("fills/")||!["png","jpg","webp"].includes(t.format)||1!==t.scale))return p("DEVUP_ASSET_FILL_SETTINGS_UNSUPPORTED","Original fill requires fills/N, png or jpg matching its codec, and scale 1; no resizing or transcoding.");if((g||["fill","original-source"].includes(h))&&"bridge"!==t.transport)return p(g?"DEVUP_ORIGINAL_IMAGE_REQUIRES_BRIDGE":"DEVUP_ASSET_FILL_REQUIRES_BRIDGE");let x=await c(()=>n(t.nodeId));if(f("lookup",Date.now()-a),l=x&&x.type||"unknown",!x||["node","visible"].includes(h)&&"function"!=typeof x.exportAsync)return p("DEVUP_ASSET_UNSUPPORTED_BY_UPSTREAM",u(Error(x?"node has no exportAsync":"node not found")));let E=null;if(g||"string"==typeof t.field&&t.field.startsWith("fills/")){let e=Number(t.field.slice(g?22:6)),r="fills"in x&&Array.isArray(x.fills)?x.fills:[],n=Number.isInteger(e)?r[e]:null,i=n&&"IMAGE"===n.type?n.imageHash||n.imageRef:null;if(!n||i!==t.imageHash)return p("DEVUP_ASSET_SOURCE_CHANGED");E=n}else if("node"!==t.field)return p("DEVUP_ASSET_FIELD_UNSUPPORTED");if("original-source"===h&&(!E||!t.imageHash))return p("DEVUP_ORIGINAL_SOURCE_UNAVAILABLE","No IMAGE fill source exists for this selection. Choose an IMAGE fill or explicitly renderMode node; vector assets should retain SVG.");let A=x;if("visible"===h){let e=t.visibleScopeNodeId;if(!e)return p("DEVUP_VISIBLE_CONTEXT_UNAVAILABLE","Recollect the parent frame and provide visibleScopeNodeId.");let r=new Set;for(;A&&A.id!==e&&!r.has(A)&&r.size<64;)r.add(A),A=A.parent;if(!A||A.id!==e||["PAGE","DOCUMENT"].includes(A.type)||"function"!=typeof A.exportAsync)return p("DEVUP_VISIBLE_SCOPE_UNSUPPORTED","The selected capture scope is not an exportable ancestor of the requested node.")}if(d.renderInfo=m(x,E),"visible"===h){let e=d.renderInfo;e.representation="native-visible",e.renderedAs="visible-scoped-capture",e.captureNodeId=A.id,e.captureBox={width:A.width,height:A.height,aspectRatio:A.width/A.height},e.contextScope="selected-ancestor-subtree",e.captureEffects=Array.isArray(A.effects)?A.effects:null,e.requestedNodeEffects=e.effects,e.effects=e.captureEffects,e.sourceProperties.captureEffects=e.captureEffects,e.visibleContext={requestedNodeId:x.id,captureNodeId:A.id,included:["selected-scope-children","selected-scope-clipping","selected-scope-masks","selected-scope-rounding"],outsideScope:"unknown; ancestors, page backdrop and external layers are not included",canvasReferenceCompared:!1},e.warnings.push("visible-scoped-capture","outside-capture-context-unverified")}b(x,d.renderInfo);let I=null;if(E)try{let r=figma.getImageByHash(t.imageHash),n=r&&await Promise.race([r.getSizeAsync(),new Promise(t=>{e=setTimeout(()=>t(null),250)})]);I=n&&n.width>0&&n.height>0&&Number.isFinite(n.width)&&Number.isFinite(n.height)?{...n,aspectRatio:n.width/n.height}:null}catch(e){}finally{clearTimeout(e)}let S=null;if("auto"===h){let e=function(e,r,n){let i,a=[],o=(e,t)=>{e||a.push(t)},l=e=>Array.isArray(e)&&2===e.length&&e.every(e=>Array.isArray(e)&&3===e.length&&e.every(Number.isFinite))&&1===e[0][0]&&1===e[1][1]&&0===e[0][1]&&0===e[1][0]&&Number.isInteger(e[0][2])&&Number.isInteger(e[1][2]);for(let a of(o("bridge"===t.transport,"raw-requires-bridge"),o(1===t.scale&&["png","jpg"].includes(t.format),"raw-requires-unscaled-raster"),o(!!r&&!0===r.visible&&1===r.opacity&&"NORMAL"===r.blendMode,"paint-compositing-or-unknown"),o(r&&"FILL"===r.scaleMode&&Array.isArray(i=r.imageTransform)&&2===i.length&&i.every((e,t)=>Array.isArray(e)&&3===e.length&&e.every((e,r)=>e===[[1,0,0],[0,1,0]][t][r]))&&0===r.rotation,"paint-transform-or-unknown"),o(r&&r.filters&&Object.values(r.filters).every(e=>0===e),"image-filters-or-unknown"),o(Array.isArray(e.fills)&&1===e.fills.length,"multiple-fills-or-unknown"),o("RECTANGLE"===e.type&&0===e.cornerRadius&&Array.isArray(e.strokes)&&0===e.strokes.length&&(!e.children||0===e.children.length),"node-geometry-or-unknown"),o(Array.isArray(e.effects)&&0===e.effects.length&&!1===e.isMask&&(void 0===e.clipsContent||!1===e.clipsContent)&&1===e.opacity&&!0===e.visible&&"NORMAL"===e.blendMode,"node-effects-mask-compositing-or-unknown"),o(l(e.absoluteTransform)&&l(e.relativeTransform),"node-transform-or-unknown"),o(n.sourceProperties.ancestors.length>0&&n.sourceProperties.ancestors.some(e=>["PAGE","DOCUMENT"].includes(e.type)),"ancestor-chain-incomplete"),o(n.sourceProperties.ancestors.every(e=>!1===e.siblingMaskPresent),"ancestor-sibling-mask-or-unknown"),n.sourceProperties.ancestors.filter(e=>!["PAGE","DOCUMENT"].includes(e.type))))o(!1===a.clipsContent&&!1===a.isMask&&!1===a.siblingMaskPresent&&Array.isArray(a.effects)&&0===a.effects.length&&1===a.opacity&&!0===a.visible&&["NORMAL","PASS_THROUGH"].includes(a.blendMode),"ancestor-dependency-or-unknown");return{eligible:0===a.length,reasons:[...new Set(a)],evidence:"explicit-plugin-properties; intrinsic-dimensions-and-codec-required"}}(x,E,d.renderInfo);if(e.eligible)try{let r=figma.getImageByHash(t.imageHash),n=I;n&&n.width===x.width&&n.height===x.height?(S=await c(()=>r.getBytesAsync()),(137===S[0]&&80===S[1]&&78===S[2]&&71===S[3]?"png":255===S[0]&&216===S[1]&&255===S[2]?"jpg":"unknown")!==t.format&&e.reasons.push("source-codec-mismatch-or-unknown")):e.reasons.push("intrinsic-dimensions-mismatch-or-unknown")}catch(t){e.reasons.push("source-inspection-unavailable")}e.eligible=0===e.reasons.length,y=e.eligible?"fill":"node",d.renderInfo=m(x,E),b(x,d.renderInfo),d.renderInfo.autoDecision={...e,selected:y}}if(d.renderInfo.sourceSize=I,d.renderInfo.sourceSizeEvidence=I?"Figma-image-getSizeAsync":"unknown",g||"fill"===y){d.method="original-fill",o="reading the original image";let e=figma.getImageByHash(t.imageHash);if(!e)return p("DEVUP_ORIGINAL_IMAGE_NOT_FOUND");let n=S||await c(()=>e.getBytesAsync());if(0===n.length||n.length>0x5000000)return p("DEVUP_ASSET_RESPONSE_TOO_LARGE",`${n.length} bytes; one answer carries 1 to 83886080`);let i=e=>e.every((e,t)=>n[t]===e),a=i([137,80,78,71,13,10,26,10])?"image/png":i([255,216,255])?"image/jpeg":i([71,73,70,56])?"image/gif":i([82,73,70,70])&&87===n[8]&&69===n[9]&&66===n[10]&&80===n[11]?"image/webp":null;if(!a)return p("DEVUP_ORIGINAL_IMAGE_CODEC_UNSUPPORTED");if(d.renderInfo.originalMimeType=a,!g&&"fill"===h&&"webp"!==t.format&&a!==({png:"image/png",jpg:"image/jpeg"})[t.format])return p("DEVUP_ASSET_FILL_CODEC_MISMATCH",`Original fill codec is ${a}, but ${t.format} was requested; choose the matching format or renderMode node. No transcoding was performed.`);if(!g)return{...p(null),status:"exported",mimeType:a,byteLength:n.length,sha256:r(n),data:figma.base64Encode(n)};let l=await c(()=>e.getSizeAsync());return{...p(null),status:"exported",kind:"devupOriginalImage",representation:"original-image-v1",format:null,scale:null,mimeType:a,width:l.width,height:l.height,byteLength:n.length,sha256:r(n),data:figma.base64Encode(n)}}let w="webp"===t.format?"PNG":String(t.format||"").toUpperCase();if(!["PNG","JPG","SVG","PDF"].includes(w))return p("DEVUP_ASSET_FORMAT_UNSUPPORTED");let v=Number(t.scale??2);if(!Number.isFinite(v)||v<=0||v>4)return p("DEVUP_ASSET_SCALE_INVALID");let N="SVG"===w,O={format:N?"SVG_STRING":w,contentsOnly:!0,useAbsoluteBounds:!1};("PNG"===w||"JPG"===w)&&(O.constraint={type:"SCALE",value:v}),o="export";let T=Math.max(1,s-(Date.now()-a)),P=N?Math.max(1,T-Math.min(5e3,Math.floor(T/5))):T,_=Date.now(),M=await c(()=>"visible"===h?A.exportAsync(O):E?x.exportAsync(O):i(x,O,P,f));if(E&&f("native",Date.now()-_),"geometry"===d.method&&(d.renderInfo.representation="geometry-fallback",d.renderInfo.renderedAs="geometry-fallback",d.renderInfo.effectsBakedIntoAsset=!1,d.renderInfo.warnings.push("geometry-fallback-not-native-render")),Date.now()-a>=s)throw Error(`DEVUP_ASSET_TIMEOUT: export exceeded ${s} ms asset budget`);o="encode";let C=Date.now(),k=N&&"string"==typeof M?M:null,R=null===k?M instanceof Uint8Array?M:new Uint8Array(M):function(e){let t=[];for(let r=0;r=55296&&n<=56319){let t=r+1=56320&&t<=57343?(n=65536+(n-55296<<10)+(t-56320),r+=1):n=65533}else n>=56320&&n<=57343&&(n=65533);n<128?t.push(n):n<2048?t.push(192|n>>6,128|63&n):n<65536?t.push(224|n>>12,128|n>>6&63,128|63&n):t.push(240|n>>18,128|n>>12&63,128|n>>6&63,128|63&n)}return new Uint8Array(t)}(k);if(0===R.length||R.length>0x5000000)return p("DEVUP_ASSET_RESPONSE_TOO_LARGE",`${w} at scale ${v} is ${R.length} bytes; one answer carries 1 to 83886080`);let D=r(R);f("encode",Date.now()-C);let L=figma.io&&"function"==typeof figma.io.write,$="PNG"===w&&R.length>786432;if(L&&(null!==k&&R.length>12288||$))return{kind:"devupAssetExport",fileKey:figma.fileKey||"",version:t.version,assetId:t.assetId,nodeId:t.nodeId,field:t.field,imageHash:t.imageHash,format:t.format,scale:v,renderMode:h,usageAspectRatio:t.usageAspectRatio??null,maxWidth:t.maxWidth??null,srcset:!0===t.srcset,visibleScopeNodeId:t.visibleScopeNodeId??null,status:"chunked",byteLength:R.length,sha256:D,cursor:{nextOffset:0,maxChunkBytes:12288},errorCode:null,exportDiagnostics:d};return L&&figma.io.write(`devup-asset-${t.assetId.replace(/[^A-Za-z0-9_-]/g,"_")}.${w.toLowerCase()}`,R),{kind:"devupAssetExport",fileKey:figma.fileKey||"",version:t.version,assetId:t.assetId,nodeId:t.nodeId,field:t.field,imageHash:t.imageHash,format:t.format,scale:v,renderMode:h,usageAspectRatio:t.usageAspectRatio??null,maxWidth:t.maxWidth??null,srcset:!0===t.srcset,visibleScopeNodeId:t.visibleScopeNodeId??null,status:"exported",byteLength:R.length,sha256:D,mimeType:L?null===k?null:"image/svg+xml":({PNG:"image/png",JPG:"image/jpeg",SVG:"image/svg+xml",PDF:"application/pdf"})[w],...!L&&null===k?{data:figma.base64Encode(R)}:{},text:k,errorCode:null,exportDiagnostics:d}}catch(e){return p(String(e.message||e).includes("DEVUP_ASSET_TIMEOUT:")?"DEVUP_ASSET_TIMEOUT":g||["fill","original-source"].includes(h)?"DEVUP_ORIGINAL_IMAGE_READ_FAILED":"DEVUP_ASSET_EXPORT_FAILED",u(e))}},bridgeContext:async function(e){let t=figma.currentPage,r=t.selection,n=e=>({id:e.id,name:String(e.name).slice(0,256),type:e.type});return{kind:"devup-bridge-context",currentPage:n(t),selection:r.slice(0,20).map(n),selectionCount:r.length,zoom:Number.isFinite(figma.viewport?.zoom)?figma.viewport.zoom:null,observedAt:Date.now(),lastKnownSelection:globalThis.__devupLastSelection??null,lastKnownPage:globalThis.__devupLastSelectionPage??null}},explore:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=await figma.getNodeByIdAsync(e.nodeId);if(!t)throw Error("DEVUP_NODE_NOT_FOUND");let r=e.explore,n=Math.max(20,Math.min(400,Number.isInteger(r.projectionLimit)?r.projectionLimit:200)),i=Math.max(0,Math.min(500,Number.isInteger(r.textPreviewLimit)?r.textPreviewLimit:160));function a(e){return"string"==typeof e?e.slice(0,240):""}let o="PAGE"===t.type,l=t,s="SECTION"===t.type?t:null;for(;!o&&l.parent&&"PAGE"!==l.parent.type;)l=l.parent,s||"SECTION"!==l.type||(s=l);let d="PAGE"===t.type?t:l.parent;if(!d||"PAGE"!==d.type)throw Error("DEVUP_PAGE_NOT_FOUND");function f(e){let t=e.absoluteBoundingBox||{x:"number"==typeof e.x?e.x:0,y:"number"==typeof e.y?e.y:0,width:"number"==typeof e.width?e.width:0,height:"number"==typeof e.height?e.height:0};return[t.x,t.y,t.width,t.height].every(Number.isFinite)?{x:t.x,y:t.y,width:t.width,height:t.height}:null}function c(e){let t=f(e);return t&&["FRAME","COMPONENT","INSTANCE","COMPONENT_SET"].includes(e.type)&&t.width>=240&&t.width<=1800&&t.height>=300&&t.height<=2e3&&t.width/Math.max(1,t.height)>=.25&&t.width/Math.max(1,t.height)<=2.5}function u(e){let t=[],r=e;for(;r&&"DOCUMENT"!==r.type&&t.length<12;){let e=a(r.name);e&&t.push(e),r=r.parent}return t.reverse()}await figma.setCurrentPageAsync(d);let p=o?d:!c(t)&&s?s:l,g=f(p)||f(t);if(!g)throw Error("DEVUP_NODE_BOUNDS_UNAVAILABLE");let h="children"in d?d.children:[],y=h.map((e,t)=>({node:e,pageChildIndex:t,bounds:f(e)})).filter(e=>e.bounds).filter(e=>{var t;return!!o||e.node.id===p.id||e.bounds.y>=g.y-240&&e.bounds.y<=g.y+12e3&&(t=e.bounds,Math.min(t.x+t.width,g.x+g.width)-Math.max(t.x,g.x)>0)}).sort((e,t)=>e.bounds.y-t.bounds.y||e.bounds.x-t.bounds.x||e.node.id.localeCompare(t.node.id)),m="SECTION"===p.type?y.filter(e=>e.node.id===p.id):y.slice(0,n),b=new Map(m.map(e=>[e.node.id,e]));b.has(l.id)||b.set(l.id,{node:l,pageChildIndex:-1,bounds:f(l)}),b.has(p.id)||b.set(p.id,{node:p,pageChildIndex:-1,bounds:f(p)}),b.has(t.id)||b.set(t.id,{node:t,pageChildIndex:-1,bounds:f(t)});let x=o?null:t.parent;for(;x&&"PAGE"!==x.type&&(b.has(x.id)||b.set(x.id,{node:x,pageChildIndex:-1,bounds:f(x)}),x.id!==p.id);)x=x.parent;if("children"in p)for(let e of p.children.slice(0,n))b.has(e.id)||b.set(e.id,{node:e,pageChildIndex:-1,bounds:f(e)});let E="SECTION"===p.type||o,A=E&&"children"in p&&p.children.length>n;if(E&&"children"in p){let e=8*n,t=p.children.filter(e=>!c(e)).map(e=>({node:e,ancestors:[]})),r=0;for(;t.length&&r!b.has(e.id));if(b.size+t.length>n+2){A=!0;continue}for(let e of t)b.set(e.id,{node:e,pageChildIndex:-1,bounds:f(e)});continue}if("children"in e)for(let r of e.children)o&&["FRAME","COMPONENT","INSTANCE","COMPONENT_SET"].includes(e.type)||t.push({node:r,ancestors:[...i,e]})}A||=t.length>0}let I=[...b.values()].filter(e=>e.bounds).slice(0,n+2).map(({node:e,pageChildIndex:t,bounds:r})=>{let n;return{id:e.id,type:e.type,fields:{name:a(e.name),parentId:e.parent&&"DOCUMENT"!==e.parent.type?e.parent.id:null,childrenIds:[],x:r.x,y:r.y,width:r.width,height:r.height,childCount:"children"in e?e.children.length:0,textPreview:"",...o||null===(n=function(e){let t=[e],r=0,n=null;for(;t.length&&r++<400;){let e=t.shift();if("visible"in e&&!1===e.visible)continue;let r="children"in e?e.children.filter(e=>!("visible"in e)||!1!==e.visible):[];if(/header|nav|gnb/i.test(e.name||"")&&r.length>=2&&"layoutMode"in e&&"HORIZONTAL"===e.layoutMode){let t=r.map(f);t.every(Boolean)&&(n=Math.max(n||0,t.reduce((e,t)=>e+t.width,0)+(Number(e.itemSpacing)||0)*(t.length-1)))}t.push(...r.slice(0,80))}return n}(e))?{}:{navigationContentWidth:n},pageChildIndex:t>=0?t:null,visible:!("visible"in e)||!1!==e.visible,breadcrumb:u(e)},extra:{},fieldErrors:{}}}),S=I.reduce((e,t)=>{let r=t.fields;return e?{x:Math.min(e.x,r.x),y:Math.min(e.y,r.y),right:Math.max(e.right,r.x+r.width),bottom:Math.max(e.bottom,r.y+r.height)}:{x:r.x,y:r.y,right:r.x+r.width,bottom:r.y+r.height}},null)||{x:0,y:0,right:0,bottom:0},w="SECTION"!==p.type&&y.length>m.length||b.size>n+2||A,v={id:d.id,type:d.type,fields:{name:a(d.name),parentId:null,childrenIds:[],x:S.x,y:S.y,width:S.right-S.x,height:S.bottom-S.y,childCount:h.length,textPreview:"",projectionTruncated:w,visible:!0,breadcrumb:u(d),pageChildIndex:null},extra:{},fieldErrors:{}},N={fileKey:figma.fileKey||"",version:null,rootIds:[d.id],nodes:[v,...I.filter(e=>e.id!==d.id)],diagnostics:[]},O=new Set([d.id,l.id,p.id,t.id]);for(;JSON.stringify(N).length>14e3;){let e=N.nodes.length-1;for(;e>=0&&O.has(N.nodes[e].id);)e-=1;if(e<0)break;N.nodes.splice(e,1),v.fields.projectionTruncated=!0}if(JSON.stringify(N).length>14e3&&(N.nodes=N.nodes.filter(e=>O.has(e.id)).map(e=>({...e,fields:{...e.fields,name:a(e.fields.name).slice(0,80),textPreview:"",breadcrumb:e.fields.breadcrumb.slice(-4).map(e=>e.slice(0,80)),projectionTruncated:e.id===d.id||e.fields.projectionTruncated}}))),JSON.stringify(N).length>14e3)throw Error("DEVUP_EXPLORE_PROJECTION_TOO_LARGE");function T(e){let t=0;for(let r of e){let e=r.codePointAt(0);t+=e<128?1:e<2048?2:e<65536?3:4}return t}if(o){for(;T(JSON.stringify(N))>19456&&N.nodes.length>1;)N.nodes.pop(),v.fields.projectionTruncated=!0;if(T(JSON.stringify(N))>19456)throw Error("DEVUP_EXPLORE_PROJECTION_TOO_LARGE")}let P=14e3-JSON.stringify(N).length;if(o&&(P=Math.min(P,19456-T(JSON.stringify(N)))),i>0&&P>0){let e=N.nodes.filter(e=>b.get(e.id)&&e.id!==d.id),t=e.length;for(let r of e){let e=Math.floor(P/t);if(t-=1,e<=0)continue;let n="",a=0;for(let t of function(e){if(0===i)return"";let t=[],r=[e],n=0;for(;r.length&&n<80&&t.join(" ").lengthe)break;n+=t,a+=r}r.fields.textPreview=n,P-=a}}return N},fastSnapshot:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=e.rootIds;if(!Array.isArray(t)||0===t.length)throw Error("DEVUP_ROOTS_INVALID");let r=await Promise.all(t.map(e=>figma.getNodeByIdAsync(e)));if(r.some(e=>!e))throw Error("DEVUP_NODE_NOT_FOUND");if(1===r.length&&"SECTION"===r[0].type)throw Error("DEVUP_TARGET_IS_SECTION");let n=["mobile","tablet","desktop"],i=e=>{let t=String(e.name||"").trim().toLowerCase(),r=n.indexOf(t);if(r>=0)return r;let i=t.match(/^(\d+)\s*[~–]\s*(\d+)$/);if(!i)return -1;let a=Number(i[1]),o=Number(i[2]);return Number.isSafeInteger(a)&&Number.isSafeInteger(o)&&a>0&&o>=a&&o<=0xffffffff?1:-1};if(1===r.length&&i(r[0])>=0){let e=r[0].parent;if(e&&"SECTION"===e.type&&"children"in e){let t=e.children.filter(e=>e.id===r[0].id||i(e)>=0).filter(e=>!1!==e.visible);t.length>1&&(r.length=0,r.push(...t))}}if(1===t.length){let e=[],t=new Set(r.map(e=>e.id)),n=[],i=e=>{if("reactions"in e&&Array.isArray(e.reactions)){for(let r of e.reactions)if(r&&r.trigger&&"AFTER_TIMEOUT"===r.trigger.type)for(let e of r.actions||[])e&&"NODE"===e.type&&e.transition&&"SMART_ANIMATE"===e.transition.type&&"string"==typeof e.destinationId&&!t.has(e.destinationId)&&(t.add(e.destinationId),n.push(e.destinationId));if("children"in e)for(let t of e.children)i(t)}};for(let e of r)i(e);for(;n.length>0;){let t=n.shift(),r=await figma.getNodeByIdAsync(t);r&&"DOCUMENT"!==r.type&&"PAGE"!==r.type&&(e.push(r),i(r))}r.push(...e)}let a=e.nodeId,o=["absoluteBoundingBox","absoluteRenderBounds","arcData","backgroundStyleId","blendMode","bottomLeftRadius","bottomRightRadius","boundVariables","characters","clipsContent","componentProperties","componentPropertyDefinitions","componentPropertyReferences","constraints","cornerRadius","counterAxisAlignItems","dashPattern","defaultVariant","effectStyleId","effects","fillStyleId","fills","fontName","fontSize","gridColumnAnchorIndex","gridColumnCount","gridColumnGap","gridColumnSizes","gridColumnSpan","gridRowAnchorIndex","gridRowCount","gridRowGap","gridRowSizes","gridRowSpan","gridStyleId","height","inferredAutoLayout","isAsset","isMask","itemSpacing","layoutGrow","layoutMode","layoutWrap","layoutPositioning","layoutSizingHorizontal","layoutSizingVertical","letterSpacing","lineHeight","maxHeight","maxLines","maxWidth","minHeight","minWidth","name","opacity","overflowDirection","paddingBottom","paddingLeft","paddingRight","paddingTop","primaryAxisAlignItems","reactions","rotation","strokeAlign","strokeBottomWeight","strokeLeftWeight","strokeRightWeight","strokeStyleId","strokeTopWeight","strokeWeight","strokes","targetAspectRatio","textAlignHorizontal","textAlignVertical","textAutoResize","textCase","textDecoration","textStyleId","textTruncation","topLeftRadius","topRightRadius","variantProperties","visible","width","x","y"],l=["fontName","fontWeight","fontSize","textDecoration","textCase","lineHeight","letterSpacing","fills","textStyleId","fillStyleId","listOptions","indentation","hyperlink"],s=e.snapshot,d=Math.max(0,Math.floor(Number(s.offset)||0)),f=Math.max(8192,Math.floor(Number(s.maxEnvelopeBytes)||19456)),c=Math.min(s.maxEnvelopeBytes?f-1024:18e3,Math.max(4096,Math.floor(Number(s.maxPayloadBytes)||15e3))),u=new Set(["backgroundStyleId","effectStyleId","fillStyleId","gridStyleId","strokeStyleId","textStyleId"]),p=new Set(["maxWidth","maxHeight","absoluteRenderBounds"]),g=new Map([["rotation",0],["cornerRadius",0],["isAsset",!1],["isMask",!1],["clipsContent",!1],["blendMode","PASS_THROUGH"],["strokeAlign","INSIDE"],["textCase","ORIGINAL"],["textDecoration","NONE"],["textAlignHorizontal","LEFT"],["textAlignVertical","TOP"],["counterAxisAlignItems","MIN"],["primaryAxisAlignItems","MIN"],["gridColumnCount",0],["gridRowCount",0],["gridColumnGap",0],["gridRowGap",0],["gridColumnAnchorIndex",-1],["gridRowAnchorIndex",-1],["gridColumnSpan",1],["gridRowSpan",1]]),h=new Set(["start","end","characters","fontWeight","textStyleId","fillStyleId","listOptions","indentation","hyperlink"]),y=new Set(["parent","children","consumers"]);function m(e,t=!1,r=new WeakSet,n=0){let i;if(null===e||["string","number","boolean"].includes(typeof e))return e;if(void 0===e)return{$undefined:!0};if("bigint"==typeof e)return{$bigint:e.toString()};if(["function","symbol"].includes(typeof e))return{$unsupported:typeof e};if(n>12)return{$truncated:"max-depth"};if("object"==typeof e&&"parent"in e&&"string"==typeof e.id&&"string"==typeof e.type)return{$nodeId:e.id,$nodeType:e.type};if(Array.isArray(e))return e.map(e=>m(e,t,r,n+1));if(ArrayBuffer.isView(e))return{$binary:e.constructor.name,byteLength:e.byteLength};if(e instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:e.byteLength};if(r.has(e))return{$circular:!0};if(r.add(e),t){let t=new Set(Object.keys(e)),r=e;for(;r&&r!==Object.prototype;){for(let e of Object.getOwnPropertyNames(r))t.add(e);r=Object.getPrototypeOf(r)}i=[...t].sort().filter(e=>!e.startsWith("_")&&!y.has(e))}else i=Object.keys(e).sort();let a={};for(let o of i)try{let i=m(e[o],t,r,n+1);i&&"function"===i.$unsupported||(a[o]=i)}catch(e){a[o]=t?{$error:"unavailable"}:{$error:String(e&&e.message?e.message:e)}}return r.delete(e),a}let b=new Map;async function x(e,t,r){if("INSTANCE"!==e.type||"function"!=typeof e.getMainComponentAsync)return;b.has(e.id)||b.set(e.id,(async()=>{try{let t=await e.getMainComponentAsync();return{fields:{mainComponentId:t?t.id:null,mainComponentName:t?t.name:null,mainComponentSetId:t&&t.parent&&"COMPONENT_SET"===t.parent.type?t.parent.id:null}}}catch(e){return{error:String(e&&e.message?e.message:e)}}})());let n=await b.get(e.id);n.error?r.mainComponentId=n.error:Object.assign(t,n.fields)}async function E(e){let t={},r={};e.parent&&(t.parentId=e.parent.id),e.parent&&("PAGE"===e.parent.type||"SECTION"===e.parent.type||"COMPONENT_SET"===e.parent.type)&&(t.parentType=e.parent.type,"SECTION"===e.parent.type&&(t.parentName=e.parent.name));let n="children"in e?e.children.map(e=>e.id):[];for(let i of(n.length>0&&(t.childrenIds=n),o)){let n;if("componentPropertyDefinitions"!==i||"COMPONENT"!==e.type||!e.parent||"COMPONENT_SET"!==e.parent.type)try{if(!(i in e)){"overflowDirection"===i&&["FRAME","COMPONENT","INSTANCE","COMPONENT_SET"].includes(e.type)&&(t[i]=null);continue}if(n=e[i],"function"==typeof n)continue;let r=m(n);if("overflowDirection"===i&&null==n){t[i]=null;continue}(null===r?!p.has(i):Array.isArray(r)?0===r.length:"object"==typeof r?0===Object.keys(r).length:!!(""===r&&u.has(i))||g.has(i)&&g.get(i)===r)||(t[i]=r)}catch(e){r[i]=String(e&&e.message?e.message:e)}}if("TEXT"===e.type&&"function"==typeof e.getStyledTextSegments)try{let r=m(e.getStyledTextSegments(l));if(1===r.length){let e=r[0];for(let t of Object.keys(e))h.has(t)||delete e[t]}r.length>0&&(t.styledTextSegments=r)}catch(e){r.styledTextSegments=String(e&&e.message?e.message:e)}await x(e,t,r);let i={id:e.id,type:e.type,fields:t};return Object.keys(r).length>0&&(i.fieldErrors=r),i}let A=[],I=[...r],S=new Set;for(let e=0;e=A.length&&A.length>0)throw Error("DEVUP_SNAPSHOT_RANGE_INVALID");function w(e){let t=0;for(let r=0;r=55296&&n<=56319&&r+1({id:e,styleType:t})).sort((e,t)=>e.id.localeCompare(t.id));async function a(e){try{let t=await figma.variables.getVariableByIdAsync(e);return t?{kind:"variable",value:m(t,!0),collectionId:t.variableCollectionId,aliasIds:Object.values(t.valuesByMode||{}).filter(e=>e&&"VARIABLE_ALIAS"===e.type&&"string"==typeof e.id&&e.id).map(e=>e.id)}:{kind:"unresolved",value:{id:e,kind:"variable",reason:"notFoundOrUnavailable"}}}catch(t){return{kind:"unresolved",value:{id:e,kind:"variable",reason:"notFoundOrUnavailable",error:String(t&&t.message||t).slice(0,300)}}}}let o=await Promise.all([...n.map(a),...i.map(async({id:e,styleType:t})=>{try{let r=await figma.getStyleByIdAsync(e);if(!r)return{kind:"unresolved",value:{id:e,kind:"style",reason:"notFoundOrUnavailable"}};return{kind:"style",value:{...m(r,!0),styleType:t,value:m("PAINT"===t?r.paints:"EFFECT"===t?r.effects:"GRID"===t?r.layoutGrids:r,!0)}}}catch(t){return{kind:"unresolved",value:{id:e,kind:"style",reason:"notFoundOrUnavailable",error:String(t&&t.message||t).slice(0,300)}}}})]),l=new Set(n),s=o.flatMap(e=>e.aliasIds||[]);for(;s.length>0;){let e=[...new Set(s)].filter(e=>!l.has(e)).sort();for(let t of e)l.add(t);let t=await Promise.all(e.map(a));o.push(...t),s=t.flatMap(e=>e.aliasIds||[])}let d=[...new Set(o.filter(e=>"variable"===e.kind&&e.collectionId).map(e=>e.collectionId))].sort(),f=(await Promise.all(d.map(async e=>{try{let t=await figma.variables.getVariableCollectionByIdAsync(e);return t?m(t,!0):null}catch(e){return null}}))).filter(e=>null!==e),c=o.filter(e=>"variable"===e.kind).map(e=>e.value),u=o.filter(e=>"style"===e.kind).map(e=>e.value),p=o.filter(e=>"unresolved"===e.kind).map(e=>e.value);return{collections:f,variables:c,styles:u,usedRemoteVariables:c.filter(e=>!0===e.remote),usedVariableIds:n,usedStyleIds:i.map(e=>e.id),localComplete:!1,usedRemoteComplete:0===p.length,unresolved:p,$variableRefCount:n.length,$styleRefCount:i.length}}async function N(e){let t=[],r=2;for(let n=d;ne)break;t.push(i),r+=a}return{pageNodes:t,packedBytes:r}}let O=c-1024,T=null;for(let e=0;e<5;e+=1){let{pageNodes:e,packedBytes:t}=await N(O);if(0===e.length)throw Error("DEVUP_SNAPSHOT_RANGE_INVALID");let n=function(e,t){let n=Math.min(A.length,d+e.length),{$variableRefCount:i,$styleRefCount:o,...l}=t,s=[...e,{id:"__DEVUP_SNAPSHOT_CURSOR__",type:"DEVUP_INTERNAL",fields:{offset:d,nextOffset:n,complete:n>=A.length,totalNodes:A.length},extra:{},fieldErrors:{}}],f={kind:"devupFastSnapshotEnvelope",schemaVersion:1,source:{fileKey:figma.fileKey||"",rootId:a},snapshot:{fileKey:figma.fileKey||"",version:null,rootIds:r.map(e=>e.id),nodes:s,diagnostics:[]},resources:l,integrity:{nodeCount:s.length,variableRefCount:i,styleRefCount:o,utf8Bytes:0}},c=0;for(let e=0;e<8&&(c=w(JSON.stringify(f)),f.integrity.utf8Bytes!==c);e+=1)f.integrity.utf8Bytes=c;if(f.integrity.utf8Bytes!==w(JSON.stringify(f)))throw Error("DEVUP_ENVELOPE_LENGTH_UNSTABLE");return{envelope:f,bytes:c}}(e,await v(e));if(n.bytes<=f){T=n;break}if(1===e.length)throw Error("DEVUP_ENVELOPE_TOO_LARGE");O=Math.max(1,Math.min(O-1,t-(n.bytes-f)-256))}if(!T)throw Error("DEVUP_ENVELOPE_TOO_LARGE");return T.envelope},fastTheme:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=Math.max(0,Math.floor(Number(e.theme.offset)||0));function r(e,t=new WeakSet,n=0){if(null===e||["string","number","boolean"].includes(typeof e))return e;if(void 0===e)return{$undefined:!0};if("bigint"==typeof e)return{$bigint:e.toString()};if(["function","symbol"].includes(typeof e))return{$unsupported:typeof e};if(n>12)return{$truncated:"max-depth"};if("object"==typeof e&&"parent"in e&&"string"==typeof e.id&&"string"==typeof e.type)return{$nodeId:e.id,$nodeType:e.type};if(Array.isArray(e))return e.map(e=>r(e,t,n+1));if(ArrayBuffer.isView(e))return{$binary:e.constructor.name,byteLength:e.byteLength};if(e instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:e.byteLength};if(t.has(e))return{$circular:!0};t.add(e);let i={};for(let a of function(e){let t=new Set(Object.keys(e)),r=e;for(;r&&r!==Object.prototype;){for(let e of Object.getOwnPropertyNames(r))t.add(e);r=Object.getPrototypeOf(r)}return[...t].sort()}(e))if(!(a.startsWith("_")||["parent","children","consumers"].includes(a)))try{let o=r(e[a],t,n+1);o&&"function"===o.$unsupported||(i[a]=o)}catch(e){i[a]={$error:"unavailable"}}return t.delete(e),i}let n=new Set,i=new Map;for(let e of figma.root.children)await e.loadAsync();for(let e of[figma.root,...figma.root.findAll(()=>!0)])for(let t of["boundVariables","fills","strokes","effects","layoutGrids","textStyleId","fillStyleId","strokeStyleId","backgroundStyleId","effectStyleId","gridStyleId"])try{!function e(t,r="",a=new WeakSet,o=0){if(!(o>16)&&null!==t&&"object"==typeof t&&!a.has(t)){if(a.add(t),Array.isArray(t)){for(let n of t)e(n,r,a,o+1);return}for(let[l,s]of("VARIABLE_ALIAS"===t.type&&"string"==typeof t.id&&t.id&&"figma.mixed"!==t.id&&"MIXED"!==t.id&&n.add(t.id),Object.entries(t))){let t="textStyleId"===l?"TEXT":["fillStyleId","strokeStyleId","backgroundStyleId"].includes(l)?"PAINT":"effectStyleId"===l?"EFFECT":"gridStyleId"===l?"GRID":null;t&&"string"==typeof s&&s&&"figma.mixed"!==s&&"MIXED"!==s&&!i.has(s)&&i.set(s,t),e(s,l||r,a,o+1)}}}({[t]:e[t]})}catch(e){}let[a,o,l,s,d,f]=await Promise.all([figma.variables.getLocalVariableCollectionsAsync(),figma.variables.getLocalVariablesAsync(),figma.getLocalPaintStylesAsync(),figma.getLocalTextStylesAsync(),figma.getLocalEffectStylesAsync(),figma.getLocalGridStylesAsync()]),c=new Set(o.map(e=>e.id)),u=new Set([...l,...s,...d,...f].map(e=>e.id)),p=[];async function g(e){try{return await figma.variables.getVariableByIdAsync(e)||null}catch(t){return p.push({id:e,kind:"variable",reason:"notFoundOrUnavailable",error:String(t&&t.message||t).slice(0,300)}),null}}function h(e){return Object.values(e.valuesByMode||{}).filter(e=>e&&"VARIABLE_ALIAS"===e.type&&"string"==typeof e.id&&e.id).map(e=>e.id)}let y=[...i.entries()].filter(([e])=>!u.has(e)).sort(([e],[t])=>e.localeCompare(t)).map(async([e,t])=>{try{let r=await figma.getStyleByIdAsync(e);return r?{style:r,styleType:t}:null}catch(t){return p.push({id:e,kind:"style",reason:"notFoundOrUnavailable",error:String(t&&t.message||t).slice(0,300)}),null}}),m=new Set,b=[],x=[...n,...o.flatMap(h)];for(;x.length>0;){let e=[...new Set(x)].filter(e=>!c.has(e)&&!m.has(e)).sort();for(let t of e)m.add(t);let t=(await Promise.all(e.map(g))).filter(Boolean);b.push(...t),x=t.flatMap(h)}let E=(await Promise.all(y)).filter(Boolean);for(let e of m)b.some(t=>t.id===e)||p.some(t=>t.id===e)||p.push({id:e,kind:"variable",reason:"notFoundOrUnavailable"});for(let[e]of[...i.entries()].filter(([e])=>!u.has(e)))E.some(t=>t.style.id===e)||p.some(t=>t.id===e)||p.push({id:e,kind:"style",reason:"notFoundOrUnavailable"});let A=[...new Set(b.map(e=>e.variableCollectionId))].filter(e=>!a.some(t=>t.id===e)).sort(),I=(await Promise.all(A.map(async e=>{try{return await figma.variables.getVariableCollectionByIdAsync(e)}catch(e){return null}}))).filter(Boolean);function S(e,t){return{...r(e),styleType:t,value:r("PAINT"===t?e.paints:"EFFECT"===t?e.effects:"GRID"===t?e.layoutGrids:e)}}let w=[...l.map(e=>S(e,"PAINT")),...s.map(e=>S(e,"TEXT")),...d.map(e=>S(e,"EFFECT")),...f.map(e=>S(e,"GRID")),...E.map(({style:e,styleType:t})=>S(e,t))].sort((e,t)=>e.id.localeCompare(t.id)),v=[...o,...b].map(e=>r(e)).sort((e,t)=>e.id.localeCompare(t.id)),N=[...a,...I].map(e=>r(e)).sort((e,t)=>e.id.localeCompare(t.id));function O(e){let t=[];for(let r=0;r=55296&&n<=56319){let t=r+1=56320&&t<=57343?(n=65536+(n-55296<<10)+(t-56320),r+=1):n=65533}else n>=56320&&n<=57343&&(n=65533);n<128?t.push(n):n<2048?t.push(192|n>>6,128|63&n):n<65536?t.push(224|n>>12,128|n>>6&63,128|63&n):t.push(240|n>>18,128|n>>12&63,128|n>>6&63,128|63&n)}return new Uint8Array(t)}p.sort((e,t)=>e.kind.localeCompare(t.kind)||e.id.localeCompare(t.id));let T=[...N.map(e=>({kind:"collection",value:e})),...v.map(e=>({kind:"variable",value:e})),...w.map(e=>({kind:"style",value:e})),...[...n].sort().map(e=>({kind:"usedVariableId",value:e})),...[...i.keys()].sort().map(e=>({kind:"usedStyleId",value:e})),...p.map(e=>({kind:"unresolved",value:e}))];if(t>T.length)throw Error("DEVUP_SNAPSHOT_RANGE_INVALID");let P=T.map(e=>O(JSON.stringify(e.value)).length+1),_=18432,M=null;for(let e=0;e<6;e+=1){let{pageItems:e,packed:r}=function(e){let r=[],n=0;for(let i=t;i0)||!(n+P[i]>e));i+=1)r.push(T[i]),n+=P[i];return{pageItems:r,packed:n}}(_),n=function(e,r){let n=t=>e.filter(e=>e.kind===t).map(e=>e.value),i=n("collection"),a=n("variable"),o=n("style"),l=n("unresolved"),s={kind:"devupFastThemeEnvelope",schemaVersion:1,source:{fileKey:figma.fileKey||"",version:null},resources:{collections:i,variables:a,styles:o,usedRemoteVariables:[],usedVariableIds:n("usedVariableId"),usedStyleIds:n("usedStyleId"),localComplete:!0,usedRemoteComplete:0===p.length,unresolved:l},page:{offset:t,nextOffset:r,complete:r>=T.length,totalItems:T.length},integrity:{collectionCount:i.length,variableCount:a.length,styleCount:o.length,unresolvedCount:l.length,utf8Bytes:0}},d=new Uint8Array;for(let e=0;e<8&&(d=O(JSON.stringify(s)),s.integrity.utf8Bytes!==d.length);e+=1)s.integrity.utf8Bytes=d.length;if(d=O(JSON.stringify(s)),s.integrity.utf8Bytes!==d.length)throw Error("DEVUP_ENVELOPE_LENGTH_UNSTABLE");return{envelope:s,bytes:d.length}}(e,t+e.length);if(n.bytes<=19456){M=n;break}if(e.length<=1)break;_=Math.max(1,Math.min(_-1,r-(n.bytes-19456)-256))}if(null===M||M.bytes>8388608)throw Error("DEVUP_ENVELOPE_TOO_LARGE");return M.envelope},largeValue:async function(e){let t;figma.skipInvisibleInstanceChildren=!1;let r=e.largeValue;async function n(e){try{let t=await figma.getNodeByIdAsync(e);if(t)return t}catch(r){if(!e.startsWith("I")||!e.includes(";"))throw r;try{return await t()}catch(e){throw Error(`${r.message||r}; nested lookup fallback: ${e.message||e}`)}}if(!e.startsWith("I")||!e.includes(";"))return null;return t();async function t(){let t=await figma.getNodeByIdAsync(e.slice(1).split(";")[0]),r=t?[t]:[],n=new Set;for(;r.length;){let t=r.pop();if(t.id===e)return t;if(!n.has(t.id)){if(n.add(t.id),n.size>1e4)throw Error("nested lookup exceeds 10000 nodes");Array.isArray(t.children)&&r.push(...t.children)}}throw Error(`nested node ${e} not found in its instance`)}}async function i(e,t,r=85e3,n=()=>{}){let a,o=Date.now();try{let i=await Promise.race([e.exportAsync(t),new Promise((e,t)=>{a=setTimeout(()=>t(Error(`DEVUP_ASSET_TIMEOUT: native export exceeded ${r} ms`)),r)})]);return n("native-export",Date.now()-o),i}catch(i){if(n("native-export",Date.now()-o,String(i.message||i).slice(0,500)),"SVG_STRING"!==t.format)throw i;let r=Date.now();try{let t=function(e){let t=e=>"number"==typeof e&&Number.isFinite(e),r=(e,t)=>{throw Error(`geometry fallback unsupported: ${e.type||"unknown"} ${e.id||""}: ${t}`)},n=0,i=0;function a(e,i,a){(void 0!==a&&!Array.isArray(a)||void 0===a&&Array.isArray(i)&&i.length)&&r(e,"unresolved paint requires native export");let o=Array.isArray(a)?a.filter(e=>!1!==e.visible):[];return o.length?(Array.isArray(i)&&i.length||r(e,"paint has no rendered geometry"),o.map(a=>{("SOLID"!==a.type||a.blendMode&&"NORMAL"!==a.blendMode)&&r(e,"paint type or blending requires native export");let o=a.color;o&&[o.r,o.g,o.b].every(e=>t(e)&&e>=0&&e<=1)||r(e,"invalid solid color");let l=void 0===a.opacity?1:a.opacity;return(!t(l)||l<0||l>1)&&r(e,"invalid paint opacity"),["NONZERO","EVENODD"].map(t=>{let a=i.filter(e=>e.windingRule===t);if(!a.length)return"";for(let t of a)"string"==typeof t.data&&t.data.trim()||r(e,"empty path"),n++;return`"']/g,e=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[e])}" fill="rgb(${100*o.r}%,${100*o.g}%,${100*o.b}%)" fill-opacity="${l}" fill-rule="${"EVENODD"===t?"evenodd":"nonzero"}"/>`}).join("")}).join("")):""}let o=e.absoluteRenderBounds,l=e.absoluteTransform;(!o||![o.x,o.y,o.width,o.height].every(t)||o.width<=0||o.height<=0)&&r(e,"missing render bounds"),(!Array.isArray(l)||2!==l.length||!l.every(e=>Array.isArray(e)&&3===e.length&&e.every(t))||0!==l[0][1]||0!==l[1][0]||l[0][0]<=0||l[1][1]<=0)&&r(e,"root rotation, reflection or shear requires native export");let s=(o.x-l[0][2])/l[0][0],d=(o.y-l[1][2])/l[1][1],f=o.width/l[0][0],c=o.height/l[1][1],u=function e(n,o){if(++i>1e4&&r(n,"subtree exceeds 10000 nodes"),n.isMask&&r(n,"mask requires native export"),!1===n.visible||0===n.opacity)return"";n.clipsContent&&r(n,"clipping requires native export"),Array.isArray(n.effects)&&n.effects.some(e=>!1!==e.visible)&&r(n,"effect requires native export"),n.blendMode&&!["NORMAL","PASS_THROUGH"].includes(n.blendMode)&&r(n,"layer blending requires native export");let l=void 0===n.opacity?1:n.opacity;(!t(l)||l<0||l>1)&&r(n,"invalid layer opacity");let s="";if(!o){let e=n.relativeTransform;Array.isArray(e)&&2===e.length&&e.every(e=>Array.isArray(e)&&3===e.length&&e.every(t))||r(n,"missing relative transform"),s=` transform="matrix(${e[0][0]} ${e[1][0]} ${e[0][1]} ${e[1][1]} ${e[0][2]} ${e[1][2]})"`}let d="";if(["GROUP","FRAME","INSTANCE","COMPONENT"].includes(n.type))(Array.isArray(n.fills)&&n.fills.some(e=>!1!==e.visible)||Array.isArray(n.strokes)&&n.strokes.some(e=>!1!==e.visible))&&r(n,"container paint requires native export"),Array.isArray(n.children)||r(n,"missing children"),d=(!0===n.itemReverseZIndex?[...n.children].reverse():n.children).map(t=>e(t,!1)).join("");else{for(let e of(["VECTOR","BOOLEAN_OPERATION","RECTANGLE","ELLIPSE","POLYGON","STAR","LINE"].includes(n.type)||r(n,"node type requires native export"),Array.isArray(n.strokes)&&n.strokes.some(e=>!1!==e.visible)&&"CENTER"!==n.strokeAlign&&r(n,"non-center stroke alignment requires native export"),Array.isArray(n.dashPattern)&&n.dashPattern.length&&r(n,"dashed stroke requires native export"),[n.fillGeometry,n.strokeGeometry]))void 0!==e&&(!Array.isArray(e)||e.some(e=>!["NONZERO","EVENODD"].includes(e.windingRule)))&&r(n,"unknown winding rule");d=a(n,n.fillGeometry,n.fills)+a(n,n.strokeGeometry,n.strokes)}return`${d}`}(e,!0);return n||r(e,"no painted geometry"),`${u}`}(e);return n("geometry-fallback",Date.now()-r),t}catch(e){throw Error(`native export: ${i.message||i}; ${e.message||e}`)}}finally{clearTimeout(a)}}let a=await n(r.nodeId);if(!a)throw Error("DEVUP_NODE_NOT_FOUND");function o(e){let t=[];for(let r=0;r=55296&&n<=56319){let t=r+1=56320&&t<=57343?(n=65536+(n-55296<<10)+(t-56320),r+=1):n=65533}else n>=56320&&n<=57343&&(n=65533);n<128?t.push(n):n<2048?t.push(192|n>>6,128|63&n):n<65536?t.push(224|n>>12,128|n>>6&63,128|63&n):t.push(240|n>>18,128|n>>12&63,128|n>>6&63,128|63&n)}return new Uint8Array(t)}let l=null;try{if("$export:svg"===r.field){if("function"!=typeof a.exportAsync)throw Error("unsupported");let e=await i(a,{format:"SVG_STRING"},Math.max(1,(Number(r.timeoutMs)||9e4)-5e3));if("string"!=typeof e)throw Error("unsupported");l=o(e)}else if("string"==typeof r.field&&r.field.startsWith("$export:png")){if("function"!=typeof a.exportAsync)throw Error("unsupported");let e=Number(r.field.split("@")[1]||2);if(!Number.isFinite(e)||e<=0||e>4)throw Error("invalid scale");let t=r.field.split("@"),n=a;if("visible"===t[2]){let e=new Set;for(;n&&n.id!==t[3]&&!e.has(n)&&e.size<64;)e.add(n),n=n.parent;if(!n||n.id!==t[3]||["PAGE","DOCUMENT"].includes(n.type))throw Error("invalid visible scope")}else if(t.length>2)throw Error("invalid export field");let i=await n.exportAsync({format:"PNG",contentsOnly:!0,useAbsoluteBounds:!1,constraint:{type:"SCALE",value:e}});l=i instanceof Uint8Array?i:new Uint8Array(i)}else if("styledTextSegments"===r.field&&"TEXT"===a.type&&"function"==typeof a.getStyledTextSegments)t=a.getStyledTextSegments(["fontName","fontWeight","fontSize","textDecoration","textCase","lineHeight","letterSpacing","fills","textStyleId","fillStyleId","listOptions","indentation","hyperlink"]);else if(r.field in a)t=a[r.field];else throw Error("unsupported")}catch(e){return{kind:"devupLargeValueUnsupported",fileKey:figma.fileKey||"",version:r.version,nodeId:r.nodeId,field:r.field,byteLength:r.byteLength,sha256:r.sha256,errorCode:"DEVUP_FIELD_UNSUPPORTED_BY_UPSTREAM",errorDetail:`${r.field} failed: ${e.message||e}`.slice(0,500)}}let s=null===l?o(JSON.stringify(function e(t,r=new WeakSet,n=0){if(null===t||["string","number","boolean"].includes(typeof t))return t;if(void 0===t)return{$undefined:!0};if("bigint"==typeof t)return{$bigint:t.toString()};if(["function","symbol"].includes(typeof t))return{$unsupported:typeof t};if(n>12)return{$truncated:"max-depth"};if("object"==typeof t&&"parent"in t&&"string"==typeof t.id&&"string"==typeof t.type)return{$nodeId:t.id,$nodeType:t.type};if(Array.isArray(t))return t.map(t=>e(t,r,n+1));if(ArrayBuffer.isView(t))return{$binary:t.constructor.name,byteLength:t.byteLength};if(t instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:t.byteLength};if(r.has(t))return{$circular:!0};r.add(t);let i={};for(let a of Object.keys(t).sort())try{let o=e(t[a],r,n+1);o&&"function"===o.$unsupported||(i[a]=o)}catch(e){i[a]={$error:String(e&&e.message?e.message:e)}}return r.delete(t),i}(t))):l,d=function(e){let t=[0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,0xe49b69c1,0xefbe4786,0xfc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x6ca6351,0x14292967,0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2],r=e.length,n=64*Math.ceil((r+9)/64),i=new Uint8Array(n);i.set(e),i[r]=128;let a=8*r;for(let e=0;e<8;e+=1)i[n-1-e]=255&Math.floor(a/2**(8*e));let o=[0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19],l=(e,t)=>e>>>t|e<<32-t;for(let e=0;e>>3,n=l(r[e-2],17)^l(r[e-2],19)^r[e-2]>>>10;r[e]=r[e-16]+t+r[e-7]+n>>>0}let[n,a,s,d,f,c,u,p]=o;for(let e=0;e<64;e+=1){let i=p+(l(f,6)^l(f,11)^l(f,25))+(f&c^~f&u)+t[e]+r[e]>>>0,o=(l(n,2)^l(n,13)^l(n,22))+(n&a^n&s^a&s)>>>0;p=u,u=c,c=f,f=d+i>>>0,d=s,s=a,a=n,n=i+o>>>0}for(let[e,t]of[n,a,s,d,f,c,u,p].entries())o[e]=o[e]+t>>>0}return o.map(e=>e.toString(16).padStart(8,"0")).join("")}(s);if(s.length!==r.byteLength||d!==r.sha256)throw Error("DEVUP_LARGE_VALUE_CHANGED");let f=Math.max(0,Math.floor(Number(r.offset)||0)),c=Math.min(65536,Math.max(1,Math.floor(Number(r.maxChunkBytes)||8192)));if(f>=s.length)throw Error("DEVUP_LARGE_VALUE_RANGE_INVALID");let u=Math.min(s.length,f+c);return{kind:"devupLargeValueFragment",fileKey:figma.fileKey||"",version:r.version,nodeId:r.nodeId,field:r.field,offset:f,nextOffset:u,byteLength:s.length,sha256:d,dataBase64:function(e){let t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",r="";for(let n=0;n>2],r+=t[(3&i)<<4|a>>4],r+=n+1>6]:"=",r+=n+210||t.some(e=>"string"!=typeof e||e.length>128||!/^(?:[0-9]+:[0-9]+|I[0-9]+:[0-9]+(?:;[0-9]+:[0-9]+)+)$/.test(e))||new Set(t).size!==t.length)throw Error("DEVUP_INVALID_INPUT: nodeIds must be 1..10 distinct Figma IDs");let r=(e,t)=>{try{let r=e[t];return"number"==typeof r&&Number.isFinite(r)?r:null}catch{return null}},n=(e,t)=>{try{let r=e[t];return"string"==typeof r&&r.length<=32&&/^[A-Z_]+$/.test(r)?r:null}catch{return null}},i=[];for(let e of t)try{let t=await figma.getNodeByIdAsync(e);if(!t||"DOCUMENT"===t.type||"PAGE"===t.type){i.push({nodeId:e,status:"unavailable",reason:t?"not-scene-node":"not-found"});continue}let a={width:r(t,"width"),height:r(t,"height"),padding:{top:r(t,"paddingTop"),right:r(t,"paddingRight"),bottom:r(t,"paddingBottom"),left:r(t,"paddingLeft")},gap:r(t,"itemSpacing"),layoutMode:n(t,"layoutMode"),align:{primary:n(t,"primaryAxisAlignItems"),counter:n(t,"counterAxisAlignItems")}},o=null!==a.width&&null!==a.height;i.push({nodeId:e,status:o?"measured":"partial",...a})}catch{i.push({nodeId:e,status:"unavailable",reason:"read-failed"})}return{kind:"devup-measure",schemaVersion:1,coordinateSpace:"figma-logical",nodes:i,partial:i.some(e=>"measured"!==e.status),evidenceLimit:"Null means unavailable or not applicable; no screenshot dimensions, descendants, theme, assets or code were read."}},metadata:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=await figma.getNodeByIdAsync(e.nodeId);if(!t)throw Error("DEVUP_NODE_NOT_FOUND");let r=t;for(;r&&"PAGE"!==r.type;)r=r.parent;r&&"PAGE"===r.type&&await figma.setCurrentPageAsync(r);let n=[];return!function(e){let t=[{node:e,visited:!1}],r=new Map;for(;t.length>0;){let e=t[t.length-1],i=e.node,a="children"in i?i.children:[];if(!e.visited){e.visited=!0;for(let e=a.length-1;e>=0;e-=1)t.push({node:a[e],visited:!1});continue}t.pop();let o=0,l=[];for(let e of a)l.push(e.id),o+=1+(r.get(e.id)||0);r.set(i.id,o),n.push({id:i.id,type:i.type,name:i.name,childrenIds:l,descendantCount:o})}}(t),{fileKey:figma.fileKey||"",version:null,rootId:t.id,nodes:n}},pageCatalog:async function(e){let t=figma.root.children;return{fileKey:figma.fileKey||"",version:null,rootIds:t.map(e=>e.id),nodes:t.map(e=>({id:e.id,type:e.type,fields:{name:e.name,parentId:null,childrenIds:[]},extra:{},fieldErrors:{}})),diagnostics:[]}},referencePng:async function(e){let t,r=e.nodeId;globalThis.__devupReferencePngAnswers||(globalThis.__devupReferencePngAnswers=new Map);let n=globalThis.__devupReferencePngAnswers,i=0;for(let[e,t]of n)Date.now()-t.at>12e4?n.delete(e):i+=t.text.length;if(i>=0xc000000)throw Error(`DEVUP_REFERENCE_PNG_BUSY ${i} characters are held for other reads`);let a=await figma.getNodeByIdAsync(r);if(!a||"function"!=typeof a.exportAsync)throw Error(`DEVUP_NODE_NOT_FOUND ${r}`);let o=a.absoluteRenderBounds||a.absoluteBoundingBox,l=Math.max(o.width,o.height),s=l>8192?8192/l:1,d=(e,t)=>({format:"PNG",constraint:t&&s<1?{type:o.width>=o.height?"WIDTH":"HEIGHT",value:8192}:{type:"SCALE",value:s},contentsOnly:e,useAbsoluteBounds:!1,colorProfile:"SRGB"}),f=e=>(Array.isArray(e)?e:[]).filter(e=>e&&!1!==e.visible),c=(e,t)=>!!e&&!!t&&e.x!!e&&e.x+t<=o.x&&e.y+t<=o.y&&e.x+e.width-t>=o.x+o.width&&e.y+e.height-t>=o.y+o.height,p=e=>`${e.type} ${JSON.stringify(e.name)}`,g=e=>JSON.parse(JSON.stringify(e===figma.mixed?null:e)),h=e=>(void 0===e.opacity||1===e.opacity)&&(!e.blendMode||["PASS_THROUGH","NORMAL"].includes(e.blendMode)),y=[],m=a.parent;for(;m&&"PAGE"!==m.type;)y.unshift(m),m=m.parent;let b=[255,255,255];for(let e of f(m&&m.backgrounds)){if("SOLID"!==e.type)continue;let t=void 0===e.opacity?1:e.opacity;["r","g","b"].forEach((r,n)=>{b[n]=t*e.color[r]*255+(1-t)*b[n]})}let x=0,E=e=>!1!==e.visible&&!e.isMask&&!!c(e.absoluteRenderBounds,o)&&((x+=1)>5e3||"TEXT"===e.type||!!f(e.fills).length||!!f(e.strokes).length||!!f(e.effects).length||"children"in e&&e.children.some(E)),A=e=>h(e)&&u(e.absoluteBoundingBox,0)&&1e-9>Math.abs(e.absoluteTransform[0][1])&&1e-9>Math.abs(e.absoluteTransform[1][0])&&0===Math.max(0,...[e.topLeftRadius,e.topRightRadius,e.bottomRightRadius,e.bottomLeftRadius].filter(e=>"number"==typeof e))&&f(e.fills).some(e=>"NORMAL"===(e.blendMode||"NORMAL")&&(void 0===e.opacity||1===e.opacity)&&("SOLID"===e.type||e.type.startsWith("GRADIENT_")&&e.gradientStops.every(e=>1===e.color.a))),I=[...y,a],S=[],w=[],v=new Map;for(let e=0;eA(e)&&i.slice(0,t).every(h));r.forEach((e,r)=>{e===t||(e.isMask&&!1!==e.visible&&r{let t=await a.exportAsync(d(!1,!0));return{kind:"devupReferencePng",nodeId:r,mimeType:"image/png",byteLength:t.length,data:figma.base64Encode(t),backdrop:N,...e}},T=(e,t)=>{let r=[...new Set(e)];return{approximate:r.join("; "),note:`Drawn as the canvas shows this area: devup-mcp cannot yet compose what get_screenshot draws here (${r.slice(0,3).join("; ")}). Layers reaching into it may show here: ${t}; get_screenshot leaves other layers out.`}},P=e=>{let t=JSON.stringify(e).replace(/[\u0080-\uffff]/g,e=>`\\u${e.charCodeAt(0).toString(16).padStart(4,"0")}`);if(t.length>0x5a00000){if(e.compose){let{compose:r,reaching:n,...i}=e;return P({...i,...T([`the answer would be ${t.length} characters`],n)})}throw Error(`DEVUP_REFERENCE_PNG_TOO_LARGE ${t.length} characters, over the 94371840 one answer is read back up to`)}if(t.length<=6291456)return e;let i=`${r}@${Date.now()}.${Math.random().toString(36).slice(2,10)}`;return n.set(i,{text:t,at:Date.now()}),{kind:"devupReferencePngChunked",nodeId:r,id:i,length:t.length}};if(0===S.length)return P(await O({}));let _=I.some(e=>e.isMask),M=[],C=new Set,k=(e,t)=>{let r=p(e);for(let t of f(e.fills))"IMAGE"===t.type?((t.rotation||f([t.filters]).some(e=>Object.values(e).some(e=>e)))&&M.push(`${r} has an image fill with a rotation or filters`),C.add(t.imageHash)):"SOLID"===t.type||t.type.startsWith("GRADIENT_")||M.push(`${r} is painted with ${t.type}`);for(let t of f(e.effects))["LAYER_BLUR","DROP_SHADOW","INNER_SHADOW","BACKGROUND_BLUR"].includes(t.type)||M.push(`${r} has a ${t.type}`);let n=f(e.strokes);if((e=>{let t=0===f(e.strokes).length?[]:[e.strokeWeight,e.strokeTopWeight,e.strokeRightWeight,e.strokeBottomWeight,e.strokeLeftWeight].filter(e=>"number"==typeof e&&e>0);if(0===t.length)return!1;let r=Math.max(...t),n="OUTSIDE"===e.strokeAlign?0:"CENTER"===e.strokeAlign?r/2:r,i="INSIDE"===e.strokeAlign?0:"CENTER"===e.strokeAlign?r/2:r,a=e.absoluteBoundingBox;return c(a&&{x:a.x-i,y:a.y-i,width:a.width+2*i,height:a.height+2*i},o)&&!u(a,n)})(e)){let t=["FRAME","RECTANGLE","COMPONENT","COMPONENT_SET","INSTANCE","SECTION"].includes(e.type),i=n.every(e=>"SOLID"===e.type||e.type.startsWith("GRADIENT_")),a=Array.isArray(e.dashPattern)&&e.dashPattern.length>0,o="INSIDE"===e.strokeAlign||!e.strokeJoin||"MITER"===e.strokeJoin;t&&i&&!a&&o||M.push(`${r}'s stroke reaches it`)}return{transform:g(e.absoluteTransform),width:e.width,height:e.height,radii:[e.topLeftRadius,e.topRightRadius,e.bottomRightRadius,e.bottomLeftRadius].map(e=>"number"==typeof e?e:0),fills:g(f(e.fills)),opacity:void 0===e.opacity?1:e.opacity,blendMode:e.blendMode||null,clips:!!e.clipsContent,effects:g(f(e.effects)),strokes:g(n),strokeAlign:e.strokeAlign||null,strokeWeights:["strokeTopWeight","strokeRightWeight","strokeBottomWeight","strokeLeftWeight"].map(t=>"number"==typeof e[t]?e[t]:"number"==typeof e.strokeWeight?e.strokeWeight:0)}},R=async e=>{let t=await e.exportAsync(d(!0,e===a));return{data:figma.base64Encode(t),area:e.absoluteRenderBounds||e.absoluteBoundingBox,blendMode:e.blendMode||null,size:[16,20].map(e=>(t[e]<<24|t[e+1]<<16|t[e+2]<<8|t[e+3])>>>0)}},D=async e=>{let t=v.get(e);if(!t)return null;let r=await t.exportAsync({...d(!0,!1),useAbsoluteBounds:!0});return{data:figma.base64Encode(r),area:t.absoluteBoundingBox,luminance:"LUMINANCE"===t.maskType}},L=[],$={},U={};try{for(let e of y){let t=k(e,!1),r=await D(e);r&&(t.mask=r),L.push(t)}let e=e=>f(e.effects).some(e=>"BACKGROUND_BLUR"===e.type);if(w.length>0&&(e(a)||"findOne"in a&&a.findOne(e))){let r="children"in a&&a.children.some(t=>e(t)||"findOne"in t&&!!t.findOne(e));if("children"in a&&!r&&!f(a.strokes).length){for(let e of($.own=k(a,!0),$.children=[],a.children)){if(!1===e.visible||e.isMask||!e.absoluteRenderBounds)continue;let t=await R(e);$.children.push({data:t.data,area:t.area,blendMode:t.blendMode})}t=[Math.ceil(o.width*s-1e-6),Math.ceil(o.height*s-1e-6)]}else M.push(`${p(a)} blurs what lies behind it`)}else if(_)t=[Math.ceil(o.width*s-1e-6),Math.ceil(o.height*s-1e-6)];else{let e=await R(a);$.picture={data:e.data,area:e.area,blendMode:e.blendMode},t=e.size}let r=await D(a);r&&($.mask=r),t&&t[0]*t[1]>8e6&&M.push(`the area is ${t[0]}x${t[1]} pixels`);let n=globalThis.__devupNormalizeImage,i=Date.now()+2e4,l=0;for(let e of C){let t=e&&figma.getImageByHash(e),r=t&&await t.getBytesAsync();if(!r){M.push(`the image ${e} could not be read`);continue}let a=null;if("function"==typeof n){let{width:e,height:o}=await t.getSizeAsync(),l=(...e)=>e.every((e,t)=>r[t]===e),s=l(137,80,78,71)||l(255,216)||l(71,73,70),d=Math.min(1,8192/Math.max(e,o,1)),f=Math.ceil(e*d)*Math.ceil(o*d),c=5*f+65536,u=5*r.length+4*e*o*(d<1?2:1)+4*f+4*c+1048576,p=i-Date.now();if((!s||Math.max(e,o)>8192||r.length>4*e*o+1048576)&&u<=0x20000000&&p>0){let t=await n(r,8192,p,e,o);t&&(r=t,a={width:e,height:o})}}if(l+r.length>0x4000000){M.push("its ancestors' images are over 67108864 bytes");break}l+=r.length,U[e]=a?{data:figma.base64Encode(r),...a}:figma.base64Encode(r)}}catch(e){M.push(`a part could not be exported: ${e instanceof Error?e.message:String(e)}`)}let B=`${S.slice(0,5).join(", ")}${S.length>5?` and ${S.length-5} more`:""}`;return M.length>0?P(await O(T(M,B))):P(await O({reaching:B,compose:{area:o,scale:s,width:t[0],height:t[1],page:b,layers:L,node:$,images:U}}))},referencePngChunk:async function(e){let t=e.largeValue,r=globalThis.__devupReferencePngAnswers,n=r&&r.get(t.id);if(!n||"string"!=typeof n.text)throw Error(`DEVUP_REFERENCE_PNG_ANSWER_GONE ${t.id}`);let i=n.text,a=Math.max(0,Math.floor(Number(t.offset)||0)),o=Math.min(i.length,a+4194304);return o>=i.length&&r.delete(t.id),{kind:"devupReferencePngChunk",id:t.id,offset:a,nextOffset:o,complete:o>=i.length,text:i.slice(a,o)}},search:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=await figma.getNodeByIdAsync(e.nodeId);if(!t)throw Error("DEVUP_NODE_NOT_FOUND");let r=t;for(;r&&"PAGE"!==r.type;)r=r.parent;if(!r||"PAGE"!==r.type)throw Error("DEVUP_PAGE_NOT_FOUND");await figma.setCurrentPageAsync(r);let n=e.search,i=new Set((n.nodeTypes.length?n.nodeTypes:["PAGE","SECTION","FRAME","COMPONENT_SET","COMPONENT"]).map(e=>e.toUpperCase()));function a(e){return e.normalize("NFC").toLocaleLowerCase().replace(/\s/gu,"")}let o=t===r?r.findAll(()=>!0):"findAll"in t?t.findAll(()=>!0):[],l=[t,...o].filter(e=>i.has(e.type)&&"string"==typeof e.name).map(e=>({node:e,score:function(e){let t=n.query.trim();if(""===t||"*"===t)return 100;if(e===n.query)return 400;if("exact"===n.matchKind)return null;let r=a(e),i=a(n.query);if(r===i)return 300;if(r.startsWith(i))return 200;if(r.includes(i))return 100;if("fuzzy"!==n.matchKind)return null;let o=function(e,t){let r=Array.from({length:t.length+1},(e,t)=>t);for(let n=0;nnull!==e.score).sort((e,t)=>t.score-e.score||e.node.name.localeCompare(t.node.name)||e.node.id.localeCompare(t.node.id)).slice(0,n.limit),s=new Map([[r.id,r]]);for(let e of[t,...l.map(e=>e.node)]){let t=e;for(;t&&"DOCUMENT"!==t.type;)s.set(t.id,t),t=t.parent}return{fileKey:figma.fileKey||"",version:null,rootIds:[r.id],nodes:[...s.values()].map(e=>({id:e.id,type:e.type,fields:{name:e.name,parentId:e.parent&&"DOCUMENT"!==e.parent.type?e.parent.id:null,childrenIds:"children"in e?e.children.map(e=>e.id):[]},extra:{},fieldErrors:{}})),diagnostics:[]}},sectionIndex:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=await figma.getNodeByIdAsync(e.nodeId);if(!t)throw Error("DEVUP_NODE_NOT_FOUND");if("SECTION"!==t.type){let e=t.parent,r=new Set;for(;e&&"SECTION"!==e.type&&!r.has(e.id);)r.add(e.id),e=e.parent;let n=e&&"SECTION"===e.type?e.id:null,i=n&&figma.fileKey?`https://www.figma.com/design/${figma.fileKey}?node-id=${n.replace(/:/g,"-")}`:null;throw Error("DEVUP_SECTION_REQUIRED "+JSON.stringify({pluginCode:"DEVUP_SECTION_REQUIRED",stage:"section-index",nodeId:t.id,nodeType:t.type,sectionId:n,nextAction:{tool:"devup_figma_export",how:n?`The url must point to a SECTION. This capture's SECTION is ${n}; select frames with frameIds.`:"The url must point to a SECTION. This node has no ancestor SECTION; choose the intended SECTION in Figma and copy its link.",arguments:i?"FRAME"===t.type&&t.parent===e?{url:i,frameIds:[t.id]}:{url:i}:null,requiredArguments:i?[]:["url"]}}))}let r=2048;function n(e){let t=e.absoluteBoundingBox||{x:"number"==typeof e.x?e.x:0,y:"number"==typeof e.y?e.y:0,width:"number"==typeof e.width?e.width:0,height:"number"==typeof e.height?e.height:0};return[t.x,t.y,t.width,t.height].every(Number.isFinite)?{x:t.x,y:t.y,width:t.width,height:t.height}:null}function i(e,t){if("FRAME"!==e.type||!1===e.visible||!t)return!1;let r=t.width/Math.max(1,t.height);return t.width>=240&&t.width<=1800&&t.height>=300&&t.height<=2e3&&r>=.25&&r<=2.5}function a(e){let t=0;for(let r=0;r=55296&&n<=56319&&r+1[e.id,t.id])),s=[],d=0;for(let e=0;ee.id));for(let r of t.children){if(e.has(r.id))continue;let t=n(r);t&&!1!==r.visible&&s.push({node:r,box:t})}}s.sort((e,t)=>e.box.y-t.box.y||e.box.x-t.box.x||e.node.id.localeCompare(t.node.id));let f=o.length>d||s.length>100,c=s.slice(0,100),u=new Set(c.map(({node:e})=>e.id)),p=new Set(e.rootIds),g={},h=new Map([[t.id,null]]);for(let e=0;e{let r=e.parent;for(;r&&r.id!==t.id&&!u.has(r.id);)r=r.parent;return[e.id,r&&u.has(r.id)?r.id:t.id]})),m=e=>c.filter(({node:t})=>y.get(t.id)===e).map(({node:e})=>e.id),b=n(t);if(!b)throw Error("DEVUP_NODE_BOUNDS_UNAVAILABLE");let x={id:t.id,type:t.type,fields:{name:t.name,parentId:t.parent&&"DOCUMENT"!==t.parent.type?t.parent.id:null,childrenIds:m(t.id),absoluteBoundingBox:b,visible:!1!==t.visible,projectionTruncated:f,nodeScreenIds:g},extra:{},fieldErrors:{}},E=c.length,A=c.map(({node:e,box:t})=>{let o,l=function(e){let t=[e],r=0,n=0;for(let e=0;er}}(e),s=Math.floor(r/E);E-=1;let d=function(e,t){let r=[e],n="",i=0,o=0,l=!1;if(t<=0)return{text:"",state:"budget-exhausted",spent:0};for(let e=0;e=120||o+e>t)return{text:n.trimEnd(),state:o+e>t?"budget-exhausted":"truncated",spent:o};n+=r,i+=1,o+=e}}if("children"in s){let e=Math.max(0,64-r.length);s.children.length>e&&(l=!0),r.push(...s.children.slice(0,e))}}}return{text:n,state:l?"truncated":n?"available":"no-text",spent:o}}(e,s);return r-=d.spent,{id:e.id,type:e.type,fields:{name:"string"==typeof e.name?e.name:"",parentId:y.get(e.id),childrenIds:m(e.id),absoluteBoundingBox:t,visible:!1!==e.visible,breadcrumb:function(e){let t=[],r=e;for(;r&&"DOCUMENT"!==r.type;)"string"==typeof r.name&&r.name&&t.push(r.name),r=r.parent;return t.reverse()}(e),directChildCount:"children"in e?e.children.length:0,textPreview:d.text,...null===(o=function(e){let t=[e],r=0,i=null;for(;t.length&&r++<400;){let e=t.shift();if("visible"in e&&!1===e.visible)continue;let r="children"in e?e.children.filter(e=>!("visible"in e)||!1!==e.visible):[];if(/header|nav|gnb/i.test(e.name||"")&&r.length>=2&&"layoutMode"in e&&"HORIZONTAL"===e.layoutMode){let t=r.map(n);t.every(Boolean)&&(i=Math.max(i||0,t.reduce((e,t)=>e+t.width,0)+(Number(e.itemSpacing)||0)*(t.length-1)))}t.push(...r.slice(0,80))}return i}(e))?{}:{navigationContentWidth:o},textPreviewState:d.state,subtreeNodeCount:l.subtreeNodeCount,estimatedSerializedBytes:l.estimatedSerializedBytes,selectionReasons:[i(e,t)?"screen-like":"explicit-selection-only","inside-section"],estimateTruncated:l.truncated},extra:{},fieldErrors:{}}}),I={fileKey:figma.fileKey||"",version:null,rootIds:[t.id],nodes:[x,...A],diagnostics:[]},S=a(JSON.stringify(I));for(let e=A.length-1;e>=0&&S>19456;e-=1)A[e].fields.textPreview="",A[e].fields.textPreviewState="budget-exhausted",S=a(JSON.stringify(I));let w=()=>I.nodes.length-1,v=A.length;for(;S>19456&&w()>1;){let e=I.nodes.pop();x.fields.projectionTruncated=!0,x.fields.childrenIds=x.fields.childrenIds.filter(t=>t!==e.id),S=a(JSON.stringify(I))}if(S>19456)throw Error("DEVUP_SECTION_INDEX_TOO_LARGE "+JSON.stringify({pluginCode:"DEVUP_SECTION_INDEX_TOO_LARGE",stage:"section-index",responseBytes:S,maxResponseBytes:19456,discoveredCandidates:v,retainedCandidates:w()}));return I},snapshot:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=await figma.getNodeByIdAsync(e.nodeId);if(!t)throw Error("DEVUP_NODE_NOT_FOUND");let r=["absoluteBoundingBox","absoluteRenderBounds","arcData","backgroundStyleId","blendMode","bottomLeftRadius","bottomRightRadius","boundVariables","characters","clipsContent","componentProperties","componentPropertyDefinitions","componentPropertyReferences","constraints","cornerRadius","counterAxisAlignItems","dashPattern","defaultVariant","effectStyleId","effects","fillStyleId","fills","fontName","fontSize","gridColumnAnchorIndex","gridColumnCount","gridColumnGap","gridColumnSizes","gridColumnSpan","gridRowAnchorIndex","gridRowCount","gridRowGap","gridRowSizes","gridRowSpan","gridStyleId","height","inferredAutoLayout","isAsset","isMask","itemSpacing","layoutGrow","layoutMode","layoutWrap","layoutPositioning","layoutSizingHorizontal","layoutSizingVertical","letterSpacing","lineHeight","maxHeight","maxLines","maxWidth","minHeight","minWidth","name","opacity","overflowDirection","paddingBottom","paddingLeft","paddingRight","paddingTop","primaryAxisAlignItems","reactions","rotation","strokeAlign","strokeBottomWeight","strokeLeftWeight","strokeRightWeight","strokeStyleId","strokeTopWeight","strokeWeight","strokes","targetAspectRatio","textAlignHorizontal","textAlignVertical","textAutoResize","textCase","textDecoration","textStyleId","textTruncation","topLeftRadius","topRightRadius","variantProperties","visible","width","x","y"],n=new Set(r),i=["fontName","fontWeight","fontSize","textDecoration","textCase","lineHeight","letterSpacing","fills","textStyleId","fillStyleId","listOptions","indentation","hyperlink"],a=e.snapshot,o=Math.max(0,Math.floor(Number(a.offset)||0)),l=Math.min(a.maxEnvelopeBytes?Math.max(8192,Math.floor(Number(a.maxEnvelopeBytes))):16e3,Math.max(4096,Math.floor(Number(a.maxPayloadBytes)||15e3))),s=Math.min(l-1024,Math.max(512,Math.floor(Number(a.maxFieldBytes)||4096))),d=new Set(["id","type","parent","children","mainComponent","masterComponent","instances"]),f=new Set(["parentId","childrenIds","name","characters","styledTextSegments","boundVariables"]);function c(e,t,r){let n=function(e){let t=[];for(let r=0;r=55296&&n<=56319){let t=r+1=56320&&t<=57343?(n=65536+(n-55296<<10)+(t-56320),r+=1):n=65533}else n>=56320&&n<=57343&&(n=65533);n<128?t.push(n):n<2048?t.push(192|n>>6,128|63&n):n<65536?t.push(224|n>>12,128|n>>6&63,128|63&n):t.push(240|n>>18,128|n>>12&63,128|n>>6&63,128|63&n)}return new Uint8Array(t)}(JSON.stringify(r));return n.length>0x1000000?{$truncated:"max-large-value-bytes",byteLength:n.length}:{$largeValue:{nodeId:e,field:t,byteLength:n.length,sha256:function(e){let t=[0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,0xe49b69c1,0xefbe4786,0xfc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x6ca6351,0x14292967,0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2],r=64*Math.ceil((e.length+9)/64),n=new Uint8Array(r);n.set(e),n[e.length]=128;let i=8*e.length;for(let e=0;e<8;e+=1)n[r-1-e]=255&Math.floor(i/2**(8*e));let a=[0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19],o=(e,t)=>e>>>t|e<<32-t;for(let e=0;e>>3,n=o(r[e-2],17)^o(r[e-2],19)^r[e-2]>>>10;r[e]=r[e-16]+t+r[e-7]+n>>>0}let[i,l,s,d,f,c,u,p]=a;for(let e=0;e<64;e+=1){let n=p+(o(f,6)^o(f,11)^o(f,25))+(f&c^~f&u)+t[e]+r[e]>>>0,a=(o(i,2)^o(i,13)^o(i,22))+(i&l^i&s^l&s)>>>0;p=u,u=c,c=f,f=d+n>>>0,d=s,s=l,l=i,i=n+a>>>0}for(let[e,t]of[i,l,s,d,f,c,u,p].entries())a[e]=a[e]+t>>>0}return a.map(e=>e.toString(16).padStart(8,"0")).join("")}(n),cursor:{nextOffset:0,maxChunkBytes:12288}}}}function u(e){return function(e){let t=0;for(let r=0;r=55296&&n<=56319&&r+112)return{$truncated:"max-depth"};if("object"==typeof t&&"parent"in t&&"string"==typeof t.id&&"string"==typeof t.type)return{$nodeId:t.id,$nodeType:t.type};if(Array.isArray(t))return t.map(t=>e(t,r,n+1));if(ArrayBuffer.isView(t))return{$binary:t.constructor.name,byteLength:t.byteLength};if(t instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:t.byteLength};if(r.has(t))return{$circular:!0};r.add(t);let i={};for(let a of Object.keys(t).sort())try{let o=e(t[a],r,n+1);o&&"function"===o.$unsupported||(i[a]=o)}catch(e){i[a]={$error:String(e&&e.message?e.message:e)}}return r.delete(t),i}(r),a=u(i);if(a<=s)return i;let o=c(e,t,i);return o.$truncated&&(n[t]=`DEVUP_FIELD_VALUE_UNSUPPORTED:${a}>16777216`),o}let g=new Map;async function h(e,t,r){if("INSTANCE"!==e.type||"function"!=typeof e.getMainComponentAsync)return;g.has(e.id)||g.set(e.id,(async()=>{try{let t=await e.getMainComponentAsync();return{fields:{mainComponentId:t?t.id:null,mainComponentName:t?t.name:null,mainComponentSetId:t&&t.parent&&"COMPONENT_SET"===t.parent.type?t.parent.id:null}}}catch(e){return{error:String(e&&e.message?e.message:e)}}})());let n=await g.get(e.id);n.error?r.mainComponentId=n.error:Object.assign(t,n.fields)}async function y(e){let t={},a={},o={};for(let i of(t.parentId=e.parent?e.parent.id:null,e.parent&&("PAGE"===e.parent.type||"SECTION"===e.parent.type||"COMPONENT_SET"===e.parent.type)&&(t.parentType=e.parent.type,"SECTION"===e.parent.type&&(t.parentName=e.parent.name)),t.childrenIds="children"in e?e.children.map(e=>e.id):[],!("overflowDirection"in e)&&["FRAME","COMPONENT","INSTANCE","COMPONENT_SET"].includes(e.type)&&(t.overflowDirection=null),function(e){let t=new Set,n=e;for(;n&&n!==Object.prototype;){for(let e of Object.getOwnPropertyNames(n))t.add(e);n=Object.getPrototypeOf(n)}for(let n of r)try{n in e&&t.add(n)}catch(e){}return[...t].sort()}(e)))if(!(d.has(i)||i.startsWith("_")||"componentPropertyDefinitions"===i&&"COMPONENT"===e.type&&e.parent&&"COMPONENT_SET"===e.parent.type))try{let r=e[i];if("function"==typeof r)continue;let l=p(e.id,i,r,o);(n.has(i)?t:a)[i]=l}catch(e){o[i]=String(e&&e.message?e.message:e)}if("TEXT"===e.type&&"function"==typeof e.getStyledTextSegments)try{t.styledTextSegments=p(e.id,"styledTextSegments",e.getStyledTextSegments(i),o)}catch(e){o.styledTextSegments=String(e&&e.message?e.message:e)}return await h(e,t,o),{id:e.id,type:e.type,fields:t,extra:a,fieldErrors:o}}let m=[],b=[t];for(;b.length;){let e=b.shift();m.push(e),"children"in e&&b.push(...e.children)}let x=[],E=l-1024,A=2;for(let e=o;eNumber(e.protected)-Number(t.protected)||t.byteLength-e.byteLength),n)){if(r<=t)break;let n=c(e.id,i.name,e[i.sectionName][i.name]);e[i.sectionName][i.name]=n,n.$truncated&&(e.fieldErrors[i.name]=`DEVUP_FIELD_VALUE_UNSUPPORTED:${i.byteLength}>16777216`),r=u(e)}return e}(await y(m[e]),E),r=u(t)+ +!!x.length;if(x.length&&A+r>E)break;x.push(t),A+=r}let I=Math.min(m.length,o+x.length);return x.push({id:"__DEVUP_SNAPSHOT_CURSOR__",type:"DEVUP_INTERNAL",fields:{offset:o,nextOffset:I,complete:I>=m.length,totalNodes:m.length},extra:{},fieldErrors:{}}),{fileKey:figma.fileKey||"",version:null,rootIds:[t.id],nodes:x,diagnostics:[]}},usedResources:async function(e){let t=e.resources;function r(e,t=new WeakSet,n=0){if(null===e||["string","number","boolean"].includes(typeof e))return e;if(void 0===e)return{$undefined:!0};if("bigint"==typeof e)return{$bigint:e.toString()};if(["function","symbol"].includes(typeof e))return{$unsupported:typeof e};if(n>12)return{$truncated:"max-depth"};if("object"==typeof e&&"parent"in e&&"string"==typeof e.id&&"string"==typeof e.type)return{$nodeId:e.id,$nodeType:e.type};if(Array.isArray(e))return e.map(e=>r(e,t,n+1));if(ArrayBuffer.isView(e))return{$binary:e.constructor.name,byteLength:e.byteLength};if(e instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:e.byteLength};if(t.has(e))return{$circular:!0};t.add(e);let i={},a=new Set(Object.keys(e)),o=e;for(;o&&o!==Object.prototype;){for(let e of Object.getOwnPropertyNames(o))a.add(e);o=Object.getPrototypeOf(o)}for(let o of[...a].sort())if(!(o.startsWith("_")||["parent","children","consumers"].includes(o)))try{let a=r(e[o],t,n+1);a&&"function"===a.$unsupported||(i[o]=a)}catch(e){i[o]={$error:"unavailable"}}return t.delete(e),i}let n=new Map,i="ok";try{for(let e of(await figma.variables.getLocalVariablesAsync()))n.set(e.id,e)}catch(e){i="unavailable"}function a(e){return Object.values(e.valuesByMode||{}).filter(e=>e&&"VARIABLE_ALIAS"===e.type&&"string"==typeof e.id&&e.id).map(e=>e.id)}async function o(e){let t=n.get(e);if(t)return{value:r(t),collectionId:t.variableCollectionId,aliasIds:a(t)};try{let t=await figma.variables.getVariableByIdAsync(e);return t?{value:r(t),collectionId:t.variableCollectionId,aliasIds:a(t)}:{unresolved:{id:e,kind:"variable",reason:"notInFileAndLookupEmpty"}}}catch(t){return{unresolved:{id:e,kind:"variable",reason:"notInFileAndLookupThrew",error:String(t&&t.message||t).slice(0,300)}}}}let l=await Promise.all(t.variableIds.map(o)),s=new Set(t.variableIds),d=l.flatMap(e=>e.aliasIds||[]);for(;d.length>0;){let e=[...new Set(d)].filter(e=>!s.has(e)).sort();for(let t of e)s.add(t);let t=await Promise.all(e.map(o));l.push(...t),d=t.flatMap(e=>e.aliasIds||[])}let f=[...new Set(l.flatMap(e=>e.collectionId?[e.collectionId]:[]))].sort(),c=await Promise.all(f.map(async e=>{try{let t=await figma.variables.getVariableCollectionByIdAsync(e);return t?[r(t)]:[]}catch(e){return[]}})),u=await Promise.all(t.styles.map(async e=>{try{let t=await figma.getStyleByIdAsync(e.id);if(!t)return{unresolved:{id:e.id,kind:"style",reason:"notFoundOrUnavailable"}};return{value:{...r(t),styleType:e.styleType,value:r("PAINT"===e.styleType?t.paints:"EFFECT"===e.styleType?t.effects:"GRID"===e.styleType?t.layoutGrids:t)}}}catch(t){return{unresolved:{id:e.id,kind:"style",reason:"notFoundOrUnavailable",error:String(t&&t.message||t).slice(0,300)}}}}));return{collections:c.flat(),variables:l.flatMap(e=>e.value?[e.value]:[]),styles:u.flatMap(e=>e.value?[e.value]:[]),usedVariableIds:t.variableIds,usedStyleIds:t.styles.map(e=>e.id),localVariableListing:i,localVariableCount:n.size,unresolved:[...l,...u].flatMap(e=>e.unresolved?[e.unresolved]:[])}},variableCatalog:async function(e){let[t,r,n,i,a]=await Promise.all([figma.variables.getLocalVariableCollectionsAsync(),figma.getLocalPaintStylesAsync(),figma.getLocalTextStylesAsync(),figma.getLocalEffectStylesAsync(),figma.getLocalGridStylesAsync()]),o=["PAINT","TEXT","EFFECT","GRID"];return{collections:t.map(e=>(function e(t,r=new WeakSet,n=0){if(null===t||["string","number","boolean"].includes(typeof t))return t;if(void 0===t)return{$undefined:!0};if("bigint"==typeof t)return{$bigint:t.toString()};if(["function","symbol"].includes(typeof t))return{$unsupported:typeof t};if(n>12)return{$truncated:"max-depth"};if(Array.isArray(t))return t.map(t=>e(t,r,n+1));if(r.has(t))return{$circular:!0};r.add(t);let i={},a=new Set(Object.keys(t)),o=t;for(;o&&o!==Object.prototype;){for(let e of Object.getOwnPropertyNames(o))a.add(e);o=Object.getPrototypeOf(o)}for(let o of[...a].sort())if(!o.startsWith("_"))try{let a=e(t[o],r,n+1);a&&"function"===a.$unsupported||(i[o]=a)}catch(e){i[o]={$error:String(e&&e.message?e.message:e)}}return r.delete(t),i})(e)),variableIds:[...new Set(t.flatMap(e=>e.variableIds))].sort(),styles:[r,n,i,a].flatMap((e,t)=>e.map(e=>({id:e.id,styleType:o[t]}))).sort((e,t)=>e.id.localeCompare(t.id)),localComplete:!0,usedRemoteComplete:!1}},variables:async function(e){let t=e.resources;function r(e,t=new WeakSet,n=0){if(null===e||["string","number","boolean"].includes(typeof e))return e;if(void 0===e)return{$undefined:!0};if("bigint"==typeof e)return{$bigint:e.toString()};if(["function","symbol"].includes(typeof e))return{$unsupported:typeof e};if(n>12)return{$truncated:"max-depth"};if("object"==typeof e&&"parent"in e&&"string"==typeof e.id&&"string"==typeof e.type)return{$nodeId:e.id,$nodeType:e.type};if(Array.isArray(e))return e.map(e=>r(e,t,n+1));if(ArrayBuffer.isView(e))return{$binary:e.constructor.name,byteLength:e.byteLength};if(e instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:e.byteLength};if(t.has(e))return{$circular:!0};t.add(e);let i={},a=new Set(Object.keys(e)),o=e;for(;o&&o!==Object.prototype;){for(let e of Object.getOwnPropertyNames(o))a.add(e);o=Object.getPrototypeOf(o)}for(let o of[...a].sort())if(!(o.startsWith("_")||["parent","children","consumers"].includes(o)))try{let a=r(e[o],t,n+1);a&&"function"===a.$unsupported||(i[o]=a)}catch(e){i[o]={$error:String(e&&e.message?e.message:e)}}return t.delete(e),i}let[n,i]=await Promise.all([Promise.all(t.variableIds.map(e=>figma.variables.getVariableByIdAsync(e))),Promise.all(t.styles.map(e=>figma.getStyleByIdAsync(e.id)))]),a=new Map(t.styles.map(e=>[e.id,e])),o=await Promise.all(i.filter(Boolean).map(async e=>{let t=a.get(e.id),n=t.styleType;if(Number.isInteger(t.consumerStart)&&Number.isInteger(t.consumerEnd)){let i=await e.getStyleConsumersAsync();return{id:e.id,styleType:n,$consumerStart:t.consumerStart,$consumerEntries:i.slice(t.consumerStart,t.consumerEnd).map(e=>[e.node.id,e.node.type,r(e.fields)])}}let i=await e.getStyleConsumersAsync();return{...r(e),styleType:n,$consumerCount:i.length,value:r("PAINT"===n?e.paints:"EFFECT"===n?e.effects:"GRID"===n?e.layoutGrids:e)}}));return{variables:n.filter(Boolean).map(e=>r(e)),styles:o}}},t=new Map,r=0;globalThis.__devupNormalizeImage=(e,n,i,a,o)=>new Promise(l=>{r+=1;let s=`decode-${r}`,d=setTimeout(()=>{t.delete(s),l(null)},i);t.set(s,e=>{clearTimeout(d),t.delete(s),l(e)}),figma.ui.postMessage({kind:"devup-decode",id:s,bytes:e,maxEdge:n,width:a,height:o})});let n=globalThis;function i(){let e=figma.currentPage,t=e.selection;return t.length>0&&(n.__devupLastSelection=t.slice(0,20).map(e=>({id:e.id,name:e.name.slice(0,256),type:e.type})),n.__devupLastSelectionPage={id:e.id,name:e.name.slice(0,256)}),{zoom:Number.isFinite(figma.viewport?.zoom)?figma.viewport.zoom:null,observedAt:Date.now(),lastKnownSelection:n.__devupLastSelection??null,lastKnownPage:n.__devupLastSelectionPage??null,currentPage:{id:e.id,name:e.name.slice(0,256)},selection:t.slice(0,20).map(e=>({id:e.id,name:e.name.slice(0,256),type:e.type})),selectionCount:t.length}}function a(){let e={kind:"devup-context",...i()};figma.ui.postMessage(e)}async function o(t){let r=e[t.script];if(!r)return{kind:"devup-result",requestId:t.requestId,error:`DEVUP_BRIDGE_UNKNOWN_SCRIPT: ${t.script} (아는 스크립트: ${Object.keys(e).join(", ")})`};try{var n;"bridgeContext"===t.script&&i();let e=await r({nodeId:(n=t.params).nodeId??"",rootIds:n.rootIds??[],snapshot:n.snapshot??{},search:n.search??{},explore:n.explore??{},resources:n.resources??{variableIds:[],styles:[]},largeValue:n.largeValue??{},asset:n.asset??{},theme:n.theme??{offset:0}});return{kind:"devup-result",requestId:t.requestId,data:e}}catch(e){return{kind:"devup-result",requestId:t.requestId,error:e instanceof Error?e.message:String(e)}}}let l=[],s=!1,d=null,f=new Set;async function c(){if(!s){s=!0;try{for(;l.length>0;){let e=l.shift();d=e.requestId;let t=await o(e).catch(t=>({kind:"devup-result",requestId:e.requestId,error:t instanceof Error?t.message:String(t)}));d=null,f.delete(e.requestId)||figma.ui.postMessage(t)}}finally{s=!1}}}let u=null;function p(){u??(u=(async()=>{try{await figma.loadAllPagesAsync(),await Promise.all([figma.variables.getLocalVariableCollectionsAsync(),figma.variables.getLocalVariablesAsync(),figma.getLocalPaintStylesAsync(),figma.getLocalTextStylesAsync(),figma.getLocalEffectStylesAsync(),figma.getLocalGridStylesAsync()])}catch{}finally{u=null}})())}figma.showUI(__html__,{width:320,height:220}),p(),figma.on("selectionchange",a),figma.on("currentpagechange",a),figma.ui.onmessage=e=>{if("object"==typeof e&&null!==e){if("devup-ready"===e.kind){let e={kind:"devup-status",fileKey:figma.fileKey??null,fileName:figma.root.name,port:1993,...i()};figma.ui.postMessage(e);return}if("devup-job"===e.kind){l.push(e),c();return}if("devup-cancel"===e.kind){var r;let t;return void(r=e.requestId,(t=l.findIndex(e=>e.requestId===r))>=0?l.splice(t,1):d===r&&f.add(r))}if("devup-decoded"===e.kind)return void t.get(e.id)?.(e.png??null);"devup-in-view"===e.kind&&p()}}})(); \ No newline at end of file From a3494c83eb92b4f086de64b3bd47d73f15c46a1b Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 17:43:58 +0900 Subject: [PATCH 053/149] fix(screen-plan): persist the selected base page at confirmation (43) --- .../devup-mcp/src/server/screen_plan/actions.rs | 5 +++++ .../src/server/screen_plan_tests/prevention.rs | 16 ++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/crates/devup-mcp/src/server/screen_plan/actions.rs b/crates/devup-mcp/src/server/screen_plan/actions.rs index e1a89c4c..8cc77332 100644 --- a/crates/devup-mcp/src/server/screen_plan/actions.rs +++ b/crates/devup-mcp/src/server/screen_plan/actions.rs @@ -370,6 +370,11 @@ pub(super) fn confirm(input: &ScreenPlanInput, policy: &OutputPolicy) -> Result< plan["excluded"] = json!(excluded); plan["designFixes"] = json!(design_fixes); plan["confirmedPage"] = confirmed_page(&plan); + if let Some(page) = actions.get("base-page").and_then(|action| action["pageId"].as_str()) { + let name = screens.iter().flat_map(|screen| array(&screen["frames"])) + .find(|frame| frame["pageId"] == page).map(|frame| frame["pageName"].clone()); + plan["confirmedPage"] = json!({"id":page,"name":name,"basis":"base-page answer"}); + } plan["status"] = json!("confirmed"); plan["confirmedAt"] = json!(now()); refresh_inventory_checks(&mut plan); diff --git a/crates/devup-mcp/src/server/screen_plan_tests/prevention.rs b/crates/devup-mcp/src/server/screen_plan_tests/prevention.rs index 39cc9198..37f35088 100644 --- a/crates/devup-mcp/src/server/screen_plan_tests/prevention.rs +++ b/crates/devup-mcp/src/server/screen_plan_tests/prevention.rs @@ -169,3 +169,19 @@ fn r38_missing_figma_tokens_put_theme_sync_before_page_work() { assert_eq!(report["workQueue"][0]["missingTokenCount"], 1); assert_eq!(report["workQueue"][0]["exportCall"]["arguments"]["devupJsonMode"], "missing-only"); } +#[tokio::test] +async fn r43_confirm_keeps_only_the_selected_source_page_and_records_it() { + let fixture = Fixture::new(); + let mut old = sectioned("old:1", "Home desktop", 1440.0, "Home"); + old.page_id = Some("page:old".into()); + old.page_name = Some("Design system".into()); + let mut final_frame = sectioned("final:1", "Home desktop", 1440.0, "Home"); + final_frame.page_id = Some("page:final".into()); + final_frame.page_name = Some("최종 시안 2026-10-14".into()); + let mut request = drafted(&fixture, &[old, final_frame]).await; + request.accept_recommended = true; + let confirmed = confirm(&request, &fixture.policy()).unwrap(); + assert_eq!(array(&confirmed["routes"]).len(), 1); + assert_eq!(confirmed["routes"][0]["nodeIds"], json!(["final:1"])); + assert_eq!(confirmed["confirmedPage"]["id"], "page:final"); +} From daf1c5bce15c4d600021ca5499622c9bdab81068 Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 17:43:58 +0900 Subject: [PATCH 054/149] fix(screen-plan): recognize owning page ids in draft scope (12) --- crates/devup-mcp/src/server/screen_plan/draft.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/devup-mcp/src/server/screen_plan/draft.rs b/crates/devup-mcp/src/server/screen_plan/draft.rs index a23ff7dc..124d7686 100644 --- a/crates/devup-mcp/src/server/screen_plan/draft.rs +++ b/crates/devup-mcp/src/server/screen_plan/draft.rs @@ -98,6 +98,7 @@ pub(super) fn in_scope( return true; }; if screen.node_id == anchor + || screen.page_id.as_deref() == Some(anchor) || screen.section.as_deref() == Some(anchor) || screen.ancestors.iter().any(|id| id == anchor) { From d6aa5a6abcb96a43804d0a7872b1c0d3cad9fa05 Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 17:44:37 +0900 Subject: [PATCH 055/149] fix(project-plan): guide API scopes without ERD entities (05) --- crates/devup-mcp/src/server/project_plan/mod.rs | 8 ++++++++ .../src/server/project_plan/tests/feedback.rs | 17 +++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/crates/devup-mcp/src/server/project_plan/mod.rs b/crates/devup-mcp/src/server/project_plan/mod.rs index 6fe7730c..d469a3ee 100644 --- a/crates/devup-mcp/src/server/project_plan/mod.rs +++ b/crates/devup-mcp/src/server/project_plan/mod.rs @@ -356,6 +356,14 @@ fn extras(plan: &Value, root: &str) -> Value { "apiWiring": wiring::report(plan, root), "backendChecks": wiring::backend_checks(plan, root), }); + if erd::api(plan) && array(&plan["entities"]).is_empty() { + extra["erdDraftRequired"] = json!({ + "reason":"API scope has no entities; draft the ERD before approving it.", + "nextAction":{"tool":"devup_project_plan","arguments":{ + "action":"draft","projectRoot":root,"planId":plan["planId"]}, + "how":"Supply entities and relations from the recorded requirements."} + }); + } if !no_screens.is_empty() { extra["featuresWithoutScreens"] = json!({"features":no_screens, "how":"These features list no screens. Pass features[].screens (page or route names) in draft; no screens question is asked."}); diff --git a/crates/devup-mcp/src/server/project_plan/tests/feedback.rs b/crates/devup-mcp/src/server/project_plan/tests/feedback.rs index 7b1b8790..95afc58c 100644 --- a/crates/devup-mcp/src/server/project_plan/tests/feedback.rs +++ b/crates/devup-mcp/src/server/project_plan/tests/feedback.rs @@ -1,5 +1,22 @@ use super::*; +#[tokio::test] +async fn api_without_entities_reports_erd_draft_guidance() { + let dir = TestDir::new(); + let policy = OutputPolicy::from_roots(vec![dir.path().to_path_buf()]).unwrap(); + let mut request = input(dir.path(), "intake"); + request.evidence = vec![serde_json::from_value(json!({ + "source":"scope", "facts":{"parts":"front + API"} + })).unwrap()]; + let result = run(&request, &policy).await.unwrap(); + assert_eq!(result["erdDraftRequired"]["nextAction"]["arguments"]["action"], "draft"); + assert_eq!(result["erdDraftRequired"]["nextAction"]["arguments"]["planId"], result["planId"]); + assert!(text(&result["erdDraftRequired"]["reason"]).contains("API")); + request.action = "status".into(); + let result = run(&request, &policy).await.unwrap(); + assert!(result["erdDraftRequired"].is_object()); +} + #[tokio::test] async fn split_uses_one_shared_table_owner_and_skips_static_backend_layers() { let dir = TestDir::new(); From 8c472e720dd5e94d95574637493b15ddef2e50d6 Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 17:46:53 +0900 Subject: [PATCH 056/149] fix(project-plan): report persisted decision count (20) --- crates/devup-mcp/src/server/project_plan/mod.rs | 1 + .../src/server/project_plan/tests/feedback.rs | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/crates/devup-mcp/src/server/project_plan/mod.rs b/crates/devup-mcp/src/server/project_plan/mod.rs index d469a3ee..f038036d 100644 --- a/crates/devup-mcp/src/server/project_plan/mod.rs +++ b/crates/devup-mcp/src/server/project_plan/mod.rs @@ -351,6 +351,7 @@ fn extras(plan: &Value, root: &str) -> Value { let no_screens = without_screens(plan); let mut extra = json!({ "writtenTo": plan["writtenTo"], + "decisionCount": array(&plan["decisions"]).len(), "unitState": if array(&plan["units"]).is_empty() {"not-split"} else {"split"}, "sharedChangeRequests": units::open_requests(plan), "apiWiring": wiring::report(plan, root), diff --git a/crates/devup-mcp/src/server/project_plan/tests/feedback.rs b/crates/devup-mcp/src/server/project_plan/tests/feedback.rs index 95afc58c..9e7529af 100644 --- a/crates/devup-mcp/src/server/project_plan/tests/feedback.rs +++ b/crates/devup-mcp/src/server/project_plan/tests/feedback.rs @@ -1,5 +1,18 @@ use super::*; +#[tokio::test] +async fn confirm_summary_counts_all_persisted_decisions() { + let dir = TestDir::new(); + let (mut request, policy, _) = drafted(dir.path(), false).await; + request.decisions = vec![decision("session-expiry"), decision("no-locale-prefix")]; + request.detail = None; + let result = run(&request, &policy).await.unwrap(); + assert_eq!(result["decisionCount"], 2); + let stored = read(&request.project_root).unwrap(); + assert!(kept(&stored["decisions"], "session-expiry")); + assert!(kept(&stored["decisions"], "no-locale-prefix")); +} + #[tokio::test] async fn api_without_entities_reports_erd_draft_guidance() { let dir = TestDir::new(); From 87ab0e1eedfa9a8602663b29a8e2b38c99705766 Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 17:55:36 +0900 Subject: [PATCH 057/149] test(validation): bind polymorphic input fixtures to a form name for the dead-control rule (41) The r12 fixtures were bare inputs; the new dead-control rule correctly flags them. Adding name= keeps the test about polymorphic attributes, not wiring. --- crates/devup-mcp-devup-ui/tests/polymorphic_props.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/devup-mcp-devup-ui/tests/polymorphic_props.rs b/crates/devup-mcp-devup-ui/tests/polymorphic_props.rs index 14a68d92..62d02d33 100644 --- a/crates/devup-mcp-devup-ui/tests/polymorphic_props.rs +++ b/crates/devup-mcp-devup-ui/tests/polymorphic_props.rs @@ -3,9 +3,9 @@ use devup_mcp_devup_ui::ui_validate::validate_devup_ui_tsx; #[test] fn r12_intrinsic_as_accepts_the_rendered_elements_attributes() { for tsx in [ - r#""#, - r#""#, - r#""#, + r#""#, + r#""#, + r#""#, r#""#, r#""#, r#""#, From 5d751ed8ff384964d184088624e5e8d655d0dba1 Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 18:01:47 +0900 Subject: [PATCH 058/149] fix(project-plan): provide executable confirm recovery (01) --- crates/devup-mcp/src/server/project_plan/mod.rs | 12 +++++++++++- .../src/server/project_plan/tests/feedback.rs | 13 +++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/crates/devup-mcp/src/server/project_plan/mod.rs b/crates/devup-mcp/src/server/project_plan/mod.rs index f038036d..88bd805e 100644 --- a/crates/devup-mcp/src/server/project_plan/mod.rs +++ b/crates/devup-mcp/src/server/project_plan/mod.rs @@ -541,7 +541,17 @@ pub(super) async fn run( if input.accept_recommended { rounds::accept_recommended(&mut plan)?; } - rounds::require_answers(&plan)?; + rounds::require_answers(&plan).map_err(|mut error| { + error.details = json!({ + "missingQuestionIds": array(&plan["questions"]).iter() + .filter(|q| plan["answers"][text(&q["id"])].is_null()) + .map(|q| q["id"].clone()).collect::>(), + "nextAction":{"tool":"devup_project_plan","arguments":{ + "action":"confirm","projectRoot":input.project_root, + "planId":plan["planId"],"answers":{},"acceptRecommended":true}} + }); + error + })?; record_decisions(&mut plan, &input.decisions, "pending")?; if plan["answers"] .as_object() diff --git a/crates/devup-mcp/src/server/project_plan/tests/feedback.rs b/crates/devup-mcp/src/server/project_plan/tests/feedback.rs index 9e7529af..5401b890 100644 --- a/crates/devup-mcp/src/server/project_plan/tests/feedback.rs +++ b/crates/devup-mcp/src/server/project_plan/tests/feedback.rs @@ -1,5 +1,18 @@ use super::*; +#[tokio::test] +async fn confirm_missing_answers_returns_executable_recovery() { + let dir = TestDir::new(); + let (mut request, policy, _) = drafted(dir.path(), false).await; + request.answers.clear(); + let error = run(&request, &policy).await.unwrap_err(); + let details = error.details; + assert_eq!(details["nextAction"]["tool"], "devup_project_plan"); + assert!(!array(&details["missingQuestionIds"]).is_empty()); + let recovery: ProjectPlanInput = serde_json::from_value(details["nextAction"]["arguments"].clone()).unwrap(); + assert_eq!(run(&recovery, &policy).await.unwrap()["state"], "confirmed"); +} + #[tokio::test] async fn confirm_summary_counts_all_persisted_decisions() { let dir = TestDir::new(); From 630c392b64d349933e6a31f28574f3a057370f64 Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 18:05:22 +0900 Subject: [PATCH 059/149] fix(project-plan): merge shared feature question topics (04) --- .../src/server/project_plan/rounds.rs | 19 ++++++++++++++++++- .../src/server/project_plan/tests/feedback.rs | 15 +++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/crates/devup-mcp/src/server/project_plan/rounds.rs b/crates/devup-mcp/src/server/project_plan/rounds.rs index 6b5589a6..8fb20c60 100644 --- a/crates/devup-mcp/src/server/project_plan/rounds.rs +++ b/crates/devup-mcp/src/server/project_plan/rounds.rs @@ -157,7 +157,7 @@ pub(super) fn detail( ("states", "empty, loading and error states", &[], ["Use recorded feature acceptance criteria", "Standard empty/loading/error states", "Exclude this capability"]), ("permissions", "permissions and ownership", &["admin", "auth", "login", "role", "permission", "owner", "member"], ["Admins only", "Signed-in users", "Public"]), ("forms", "forms and field validation", &["form", "input", "inquiry", "contact", "signup", "register", "submit", "search", "write", "edit"], ["Required fields with inline validation", "Use recorded feature acceptance criteria", "Exclude this capability"]), - ("notifications", "notifications and external integrations (payment, mail, maps, login providers)", &["mail", "email", "smtp", "notif", "payment", "map", "sms", "alert", "analytics", "oauth"], ["No notifications or integrations", "Email to a staff address", "Other integration (custom answer)"]), + ("notifications", "notifications and external integrations (payment, mail, maps, login providers)", &["mail", "email", "smtp", "notif", "payment", "map", "sms", "alert", "analytics", "oauth"], ["No notifications or integrations", "SMTP email to a staff address", "Other integration (custom answer)"]), ]; for (topic, prompt, keywords, options) in topics { let relevant = match topic { @@ -170,6 +170,23 @@ pub(super) fn detail( } } } + let mut grouped: Vec = Vec::new(); + for mut question in questions { + let id = text(&question["id"]).to_owned(); + if let Some((feature, topic)) = id.rsplit_once('.') { + if let Some(existing) = grouped.iter_mut().find(|q| { + text(&q["id"]).rsplit_once('.').is_some_and(|(_, other)| other == topic) + && q["options"] == question["options"] + }) { + existing["features"].as_array_mut().unwrap().push(json!(feature)); + existing["question"] = json!(format!("{}\n{}", text(&existing["question"]), text(&question["question"]))); + continue; + } + question["features"] = json!([feature]); + } + grouped.push(question); + } + let mut questions = grouped; if erd::api(plan) && !array(&plan["entities"]).is_empty() { for (key, topic) in [ ( diff --git a/crates/devup-mcp/src/server/project_plan/tests/feedback.rs b/crates/devup-mcp/src/server/project_plan/tests/feedback.rs index 5401b890..89f32a40 100644 --- a/crates/devup-mcp/src/server/project_plan/tests/feedback.rs +++ b/crates/devup-mcp/src/server/project_plan/tests/feedback.rs @@ -1,5 +1,20 @@ use super::*; +#[test] +fn shared_feature_topics_are_one_question_with_concrete_choices() { + let plan = json!({"facts":{},"answers":{},"issues":[],"entities":[],"features":[ + {"id":"contact","scope":"Contact form with SMTP email","acceptance":[]}, + {"id":"signup","scope":"Signup form with SMTP email","acceptance":[]} + ]}); + let questions = rounds::detail(&plan, &[]).unwrap(); + let forms = questions.iter().filter(|q| text(&q["id"]).ends_with(".forms")).collect::>(); + assert_eq!(forms.len(), 1); + assert_eq!(forms[0]["features"], json!(["contact", "signup"])); + let notifications = questions.iter().find(|q| text(&q["id"]).ends_with(".notifications")).unwrap(); + assert_eq!(array(¬ifications["options"]).len(), 3); + assert!(array(¬ifications["resolve"]).iter().any(|option| option["action"] == "SMTP email to a staff address")); +} + #[tokio::test] async fn confirm_missing_answers_returns_executable_recovery() { let dir = TestDir::new(); From 04e582341e69bfcb22e3e3afd7bb936ca90f5292 Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 18:08:42 +0900 Subject: [PATCH 060/149] fix(screen-plan): retain state variants in audits and export guidance (16) --- .../server/audit_plan/planning_and_capture.rs | 18 +++++++---- .../src/server/screen_plan/actions.rs | 5 +++- .../src/server/screen_plan/state_variants.rs | 30 +++++++++++++++++-- .../src/server/screen_plan/status.rs | 22 ++++++++++++++ .../server/screen_plan_tests/prevention.rs | 15 ++++++++++ 5 files changed, 82 insertions(+), 8 deletions(-) diff --git a/crates/devup-mcp/src/server/audit_plan/planning_and_capture.rs b/crates/devup-mcp/src/server/audit_plan/planning_and_capture.rs index e50960d6..2f61bda6 100644 --- a/crates/devup-mcp/src/server/audit_plan/planning_and_capture.rs +++ b/crates/devup-mcp/src/server/audit_plan/planning_and_capture.rs @@ -679,10 +679,8 @@ pub(super) async fn write( } = *request; let (sources, expired) = Sources::load(artifact_ids, store).await; let evidence = super::screen_plan::project_evidence(root).await?; - let routes = plan["routes"] - .as_array() - .into_iter() - .flatten() + let audit_routes = super::screen_plan::audit_routes(plan); + let routes = audit_routes.iter() .filter(|route| route["implementation"] == "page" && route["url"].is_string()) .collect::>(); let records = super::export_record::records(Path::new(root)); @@ -751,11 +749,14 @@ pub(super) async fn write( let route = routes[*index]; let url = url.as_str(); let app = route["app"].as_str().unwrap_or_default(); - let screen = if several_apps && !app.is_empty() { + let mut screen = if several_apps && !app.is_empty() { format!("{}-{}", slug(app), slug(url)) } else { slug(url) }; + if let Some(node) = route["state"]["nodeId"].as_str() { + screen.push_str(&format!("-state-{}", slug(node))); + } let frames = route["nodeIds"] .as_array() .into_iter() @@ -782,6 +783,12 @@ pub(super) async fn write( }; let base = base_urls.get(app).map_or(base_url, String::as_str); let mut built = url_plan(&target, base, &sources, thresholds)?; + if let Some(node) = route["state"]["nodeId"].as_str() { + built.notes.push(format!("State {} ({node}) on {url}: configure the runtime screen's steps to reach this state before running; no interaction is inferred from the frame name.", route["state"]["name"].as_str().unwrap_or_default())); + for check in &mut built.plan.screens[0].checks { + check.label = Some(format!("State {node}: {}", check.label.as_deref().unwrap_or(&check.kind))); + } + } for check in &mut built.plan.screens[0].checks { if check.kind == "links-resolve" { check.planned_targets = routes.iter().filter_map(|route| route["url"].as_str().map(str::to_owned)).collect(); @@ -817,6 +824,7 @@ pub(super) async fn write( written.push(json!({ "url": url, "screen": screen, + "state": route["state"], "planPath": created["writtenTo"], "planId": created["planId"], "reportDirectory": report_dir.display().to_string(), diff --git a/crates/devup-mcp/src/server/screen_plan/actions.rs b/crates/devup-mcp/src/server/screen_plan/actions.rs index 8cc77332..f1d270e9 100644 --- a/crates/devup-mcp/src/server/screen_plan/actions.rs +++ b/crates/devup-mcp/src/server/screen_plan/actions.rs @@ -394,6 +394,7 @@ pub(super) fn confirm(input: &ScreenPlanInput, policy: &OutputPolicy) -> Result< "file": route["file"], "nodeIds": route["nodeIds"], "drawnWidths": route["drawnWidths"], + "states": state_exports(route), }) }) .collect::>()); @@ -614,9 +615,11 @@ pub(super) async fn audit_plan( ) .await?; let mut entries = array(&plan["audit"]["plans"]).to_vec(); + let replaced = array(&result["plans"]).iter().map(|entry| entry["url"].clone()).collect::>(); + entries.retain(|entry| !replaced.contains(&entry["url"])); for written in array(&result["plans"]) { - entries.retain(|entry| entry["url"] != written["url"]); entries.push(json!({"url": written["url"], "screen": written["screen"], + "state": written["state"], "missingReferences": written["missingReferences"], "planPath": written["planPath"], "reportPath": format!("{}/report.json", text(&written["reportDirectory"]).replace('\\', "/"))})); } // A dynamic route sampled from its Figma frame is an assumption, and is diff --git a/crates/devup-mcp/src/server/screen_plan/state_variants.rs b/crates/devup-mcp/src/server/screen_plan/state_variants.rs index a435dff6..5ff36675 100644 --- a/crates/devup-mcp/src/server/screen_plan/state_variants.rs +++ b/crates/devup-mcp/src/server/screen_plan/state_variants.rs @@ -61,10 +61,36 @@ fn record_states(routes: &mut [Value], folds: &[(String, Vec)]) { .iter() .filter(|(first, _)| *first == text(&route["screen"])) .flat_map(|(_, rest)| rest.iter()) - .map(|frame| json!({"nodeId": frame["nodeId"], "name": frame["name"], "width": frame["width"]})) + .map(|frame| json!({"nodeId": frame["nodeId"], "name": frame["name"], "width": frame["width"], "height": frame["height"]})) .collect::>(); if !states.is_empty() { route["states"] = json!(states); } } -} \ No newline at end of file +} + +pub(super) fn state_exports(route: &Value) -> Vec { + array(&route["states"]).iter().map(|state| { + let mut state = state.clone(); + state["hostUrl"] = route["url"].clone(); + state["exportCall"] = json!({"tool": "devup_figma_export", "arguments": { + "frameIds": [state["nodeId"]], "outputs": ["tsx", "referencePng"] + }}); + state + }).collect() +} + +pub(super) fn audit_routes(plan: &Value) -> Vec { + array(&plan["routes"]).iter().flat_map(|route| { + let mut targets = vec![route.clone()]; + for state in array(&route["states"]) { + let mut target = route.clone(); + target["nodeIds"] = json!([state["nodeId"]]); + target["drawnWidths"] = json!([state["width"].as_f64().map(|width| width.round() as u64)]); + target["state"] = state.clone(); + target["states"] = json!([]); + targets.push(target); + } + targets + }).collect() +} diff --git a/crates/devup-mcp/src/server/screen_plan/status.rs b/crates/devup-mcp/src/server/screen_plan/status.rs index 1e6516df..eeb81a4c 100644 --- a/crates/devup-mcp/src/server/screen_plan/status.rs +++ b/crates/devup-mcp/src/server/screen_plan/status.rs @@ -48,6 +48,18 @@ pub(super) fn status_report( let Some(url) = route["url"].as_str() else { continue; }; + let route_states = state_exports(route).into_iter().map(|mut state| { + let suffix = format!("-state-{}", super::audit_plan::slug(text(&state["nodeId"]))); + let mut required_widths = DEFAULT_WIDTHS.to_vec(); + if let Some(width) = state["width"].as_f64() { required_widths.push(width.round() as u32); } + let results = required_widths.iter().map(|width| { + audit_rows.iter().find(|row| row["routePath"] == url && row["width"] == *width && text(&row["screen"]).ends_with(&suffix)) + .map_or("not-run", |row| text(&row["status"])) + }).collect::>(); + state["result"] = json!(results.iter().fold("pass", |result, next| worst(result, next))); + state + }).collect::>(); + states.extend(route_states.clone()); let page = evidence .routes .iter() @@ -80,6 +92,10 @@ pub(super) fn status_report( .iter() .fold("pass", |acc, row| worst(acc, text(&row["status"]))); let mut reasons = matching.iter().flat_map(|row| array(&row["reasons"]).to_vec()).collect::>(); + if route_states.iter().any(|state| state["result"] != "pass") { + for state in &route_states { result = worst(result, text(&state["result"])); } + reasons.push(json!("state variant audit missing or not passing")); + } let missing_reference = array(&plan["audit"]["plans"]).iter().filter(|entry| entry["url"] == url) .flat_map(|entry| array(&entry["missingReferences"])) .any(|reference| reference["width"].is_null() || reference["width"] == *width); @@ -104,6 +120,7 @@ pub(super) fn status_report( "figmaScreen": route["name"], "nodeIds": route["nodeIds"], "figmaPages": route["pages"], + "states": route_states, "file": page.clone().map_or_else(|| route["file"].clone(), Value::from), "implemented": implemented_state(page.is_some(), &route["drawnWidths"], &per_width), "widths": per_width, @@ -312,6 +329,11 @@ pub(super) fn status_report( link_markdown.push_str(&format!("| {} | {} | {} | {} | {} |\n", cell(&row["from"]), cell(&row["label"]), cell(&row["href"]), cell(&row["status"]), cell(&row["planned"]))); } let mut queue = super::audit_plan::work_queue(plan, &rows, &[]); + for item in &mut queue { + if let Some(row) = rows.iter().find(|row| row["url"] == item["url"]) { + item["states"] = row["states"].clone(); + } + } let theme_queue = theme_sync_queue(plan, &evidence.root.to_string_lossy()); let theme_pending = !theme_queue.is_empty(); queue.splice(0..0, theme_queue); diff --git a/crates/devup-mcp/src/server/screen_plan_tests/prevention.rs b/crates/devup-mcp/src/server/screen_plan_tests/prevention.rs index 37f35088..b05775fe 100644 --- a/crates/devup-mcp/src/server/screen_plan_tests/prevention.rs +++ b/crates/devup-mcp/src/server/screen_plan_tests/prevention.rs @@ -71,6 +71,21 @@ async fn near_identical_widths_in_one_section_can_be_states_of_one_url() { assert_eq!(routes.len(), 1, "{confirmed}"); assert_eq!(array(&routes[0]["states"]).len(), 2); assert_eq!(routes[0]["states"][0]["nodeId"], "30:2"); + assert_eq!(array(&confirmed["workQueue"][0]["states"]).len(), 2); + let saved = read_plan(fixture.root()).unwrap().unwrap(); + let report = status_report(&saved, &evidence(), None); + assert_eq!(array(&report["states"]).len(), 2); + assert_eq!(report["states"][0]["nodeId"], "30:2"); + assert_eq!(report["workQueue"][0]["states"][0]["exportCall"]["arguments"]["frameIds"], json!(["30:2"])); + let result = audit_plan(&input(&fixture, "auditPlan"), &fixture.policy(), &ArtifactStore::default()).await.unwrap(); + assert_eq!(array(&result["plans"]).len(), 3, "{result}"); + for (entry, node) in array(&result["plans"]).iter().zip(["30:1", "30:2", "30:3"]) { + let audit: Value = serde_json::from_slice(&std::fs::read(text(&entry["planPath"])).unwrap()).unwrap(); + assert_eq!(audit["plan"]["screens"][0]["figma"]["nodeId"], node); + assert_eq!(entry["missingReferences"][0]["nodeId"], node); + } + assert_eq!(result["plans"][1]["state"]["nodeId"], "30:2"); + assert_eq!(array(&read_plan(fixture.root()).unwrap().unwrap()["audit"]["plans"]).len(), 3); } #[tokio::test] async fn r19_device_only_frames_in_each_section_confirm_as_one_responsive_url() { From ddd6f0a8f34d17b2ed916fd771f9ae91942f86af Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 18:08:48 +0900 Subject: [PATCH 061/149] fix(project-plan): bind ERD choices to actual entities (05) --- .../src/server/project_plan/rounds.rs | 20 ++++++++++++++++++- .../src/server/project_plan/tests/feedback.rs | 14 +++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/crates/devup-mcp/src/server/project_plan/rounds.rs b/crates/devup-mcp/src/server/project_plan/rounds.rs index 8fb20c60..d1677f0c 100644 --- a/crates/devup-mcp/src/server/project_plan/rounds.rs +++ b/crates/devup-mcp/src/server/project_plan/rounds.rs @@ -209,7 +209,25 @@ pub(super) fn detail( ("storage", "Set file/image storage and nullable metadata"), ] { if plan["facts"][key].is_null() { - questions.push(ask(key, format!("{topic}. Draft ERD: {}. Supply changes as entities/relations with confirm; a prose correction requires redraft.", plan["entities"]), &["Approve recorded ERD", "Revise ERD before confirming"])); + let names = array(&plan["entities"]).iter().map(|e| text(&e["name"])).collect::>(); + let entities = names.join(", "); + let columns = array(&plan["entities"]).iter().flat_map(|e| { + array(&e["columns"]).iter().filter(|c| !c["index"].is_null() || !c["unique"].is_null()) + .map(|c| format!("{}.{}", text(&e["name"]), text(&c["name"]))) + }).collect::>().join(", "); + let choices = match key { + "erd" => vec![format!("Keep recorded columns and relations for {entities}"), "Revise ERD before confirming".into()], + "tenancy" => vec![format!("Single tenant for {entities}"), format!("Tenant ownership required for {entities}")], + "auth" => vec![format!("Use recorded auth and role columns for {entities}"), format!("External identity provider for {entities}")], + "lifecycle" => vec![format!("Keep recorded lifecycle for {entities}"), format!("Soft delete required for {entities}"), format!("Hard delete with recorded FK actions for {entities}")], + "indexes" => vec![format!("Keep recorded indexes: {columns}"), format!("Revise filter and sort indexes for {entities}")], + "storage" => vec![format!("Use recorded file metadata for {entities}"), format!("Object storage with nullable metadata for {entities}"), format!("No file storage for {entities}")], + _ => unreachable!(), + }; + let options = choices.iter().map(String::as_str).collect::>(); + let mut question = ask(key, format!("{topic}. Draft ERD: {}. Supply changes as entities/relations with confirm; a prose correction requires redraft.", plan["entities"]), &options); + question["entities"] = json!(names); + questions.push(question); } } } diff --git a/crates/devup-mcp/src/server/project_plan/tests/feedback.rs b/crates/devup-mcp/src/server/project_plan/tests/feedback.rs index 89f32a40..2d1a3465 100644 --- a/crates/devup-mcp/src/server/project_plan/tests/feedback.rs +++ b/crates/devup-mcp/src/server/project_plan/tests/feedback.rs @@ -1,5 +1,19 @@ use super::*; +#[test] +fn populated_erd_choices_bind_entity_columns_and_lifecycle() { + let plan = json!({"facts":{"parts":"API"},"answers":{},"issues":[], + "features":[{"id":"news","scope":"News","acceptance":[]}], + "entities":[{"name":"post","columns":[{"name":"published_at","type":"timestamptz","nullable":true,"index":true}]}]}); + let questions = rounds::detail(&plan, &[]).unwrap(); + let lifecycle = questions.iter().find(|q| q["id"] == "lifecycle").unwrap(); + let indexes = questions.iter().find(|q| q["id"] == "indexes").unwrap(); + assert_eq!(lifecycle["entities"], json!(["post"])); + assert_ne!(lifecycle["options"], indexes["options"]); + assert!(text(&indexes["resolve"][0]["action"]).contains("post.published_at")); + assert!(text(&lifecycle["resolve"][1]["action"]).contains("post")); +} + #[test] fn shared_feature_topics_are_one_question_with_concrete_choices() { let plan = json!({"facts":{},"answers":{},"issues":[],"entities":[],"features":[ From 095d7ba6ce5087496d2459d6924cd5e740621197 Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 18:11:20 +0900 Subject: [PATCH 062/149] fix(project-plan): summarize draft record changes (10) --- .../devup-mcp/src/server/project_plan/mod.rs | 31 +++++++++++++++++++ .../src/server/project_plan/tests/feedback.rs | 16 ++++++++++ 2 files changed, 47 insertions(+) diff --git a/crates/devup-mcp/src/server/project_plan/mod.rs b/crates/devup-mcp/src/server/project_plan/mod.rs index 88bd805e..63d732a1 100644 --- a/crates/devup-mcp/src/server/project_plan/mod.rs +++ b/crates/devup-mcp/src/server/project_plan/mod.rs @@ -326,6 +326,32 @@ fn without_screens(plan: &Value) -> Vec { .map(|f| f["id"].clone()) .collect() } +fn change_summary(before: &Value, after: &Value) -> Value { + let mut summary = json!({}); + for (field, key) in [("decisions", "id"), ("features", "id"), ("entities", "name"), ("answers", "")] { + let records = |plan: &Value| -> BTreeMap { + if key.is_empty() { + plan[field].as_object().into_iter().flatten().map(|(k, v)| (k.clone(), v.clone())).collect() + } else { + array(&plan[field]).iter().map(|v| (text(&v[key]).to_owned(), v.clone())).collect() + } + }; + let old = records(before); + let new = records(after); + let mut changes = json!({"added":[],"changed":[],"kept":[],"removed":[]}); + for (id, value) in &new { + let kind = match old.get(id) { + None => "added", + Some(previous) if previous == value => "kept", + Some(_) => "changed", + }; + changes[kind].as_array_mut().unwrap().push(json!(id)); + } + changes["removed"] = json!(old.keys().filter(|id| !new.contains_key(*id)).collect::>()); + summary[field] = changes; + } + summary +} const DATA_UI: [&str; 9] = [ "list", "detail", "form", "search", "inquiry", "filter", "pagination", "board", "table", ]; @@ -464,6 +490,7 @@ pub(super) async fn run( return Ok(response(plan, &input.project_root, full)); } let mut plan = read(&input.project_root)?; + let previous = plan.clone(); // status is a read: it may omit planId and reads it from the plan file. let provided = input.plan_id.is_some() || input.action != "status"; if provided && input.plan_id.as_deref() != plan["planId"].as_str() { @@ -637,7 +664,11 @@ pub(super) async fn run( wiring::record(&mut plan, &input.api_wiring)?; write(&mut tx, policy, &input.project_root, &plan)?; tx.commit()?; + let changes = (input.action == "draft").then(|| change_summary(&previous, &plan)); let mut out = response(plan, &input.project_root, full); + if let Some(changes) = changes { + out["changeSummary"] = changes; + } if input.action == "status" && !full && let Some(summary) = out.as_object_mut() { diff --git a/crates/devup-mcp/src/server/project_plan/tests/feedback.rs b/crates/devup-mcp/src/server/project_plan/tests/feedback.rs index 2d1a3465..ad8029f8 100644 --- a/crates/devup-mcp/src/server/project_plan/tests/feedback.rs +++ b/crates/devup-mcp/src/server/project_plan/tests/feedback.rs @@ -1,5 +1,21 @@ use super::*; +#[tokio::test] +async fn redraft_reports_added_changed_and_kept_records() { + let dir = TestDir::new(); + let (mut request, policy, _) = drafted(dir.path(), false).await; + request.action = "draft".into(); + request.answers.clear(); + request.decisions = vec![decision("keep"), decision("change")]; + run(&request, &policy).await.unwrap(); + request.decisions = vec![decision("new"), Decision {recommended:"Different".into(), ..decision("change")}]; + request.answers.insert("posts.states".into(), json!("Empty list")); + let result = run(&request, &policy).await.unwrap(); + assert_eq!(result["changeSummary"]["decisions"], json!({"added":["new"],"changed":["change"],"kept":["keep"],"removed":[]})); + assert_eq!(result["changeSummary"]["features"]["kept"], json!(["posts"])); + assert_eq!(result["changeSummary"]["answers"]["added"], json!(["posts.states"])); +} + #[test] fn populated_erd_choices_bind_entity_columns_and_lifecycle() { let plan = json!({"facts":{"parts":"API"},"answers":{},"issues":[], From 40f3dcccacf960408c72e5a0ce18c1dcd3a352f0 Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 18:12:56 +0900 Subject: [PATCH 063/149] fix(project-plan): forward shared site facts as screen answers (17) --- .../devup-mcp/src/server/project_plan/mod.rs | 16 +++++++++++++++- .../src/server/project_plan/tests/feedback.rs | 19 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/crates/devup-mcp/src/server/project_plan/mod.rs b/crates/devup-mcp/src/server/project_plan/mod.rs index 63d732a1..1f06a332 100644 --- a/crates/devup-mcp/src/server/project_plan/mod.rs +++ b/crates/devup-mcp/src/server/project_plan/mod.rs @@ -501,7 +501,21 @@ pub(super) async fn run( ))); } let (answers, forward) = if matches!(input.action.as_str(), "draft" | "confirm") { - forward_screen_answers(&input.project_root, &input.answers, input.accept_recommended) + let mut shared = BTreeMap::new(); + let fresh = rounds::facts(input)?; + for key in ["siteName", "siteUrl", "favicon", "ogImage", "motion"] { + let value = input.answers.get(key) + .or_else(|| plan["answers"].get(key)) + .or_else(|| fresh.get(key)) + .or_else(|| plan["facts"].get(key)); + if let Some(value) = value.filter(|v| v.as_str().is_some_and(|s| !s.trim().is_empty())) + && open_screen_questions(&input.project_root).iter().any(|q| q["id"] == key) + { + shared.insert(key.to_owned(), value.clone()); + } + } + shared.extend(input.answers.clone()); + forward_screen_answers(&input.project_root, &shared, input.accept_recommended) } else { (input.answers.clone(), Value::Null) }; diff --git a/crates/devup-mcp/src/server/project_plan/tests/feedback.rs b/crates/devup-mcp/src/server/project_plan/tests/feedback.rs index ad8029f8..0cb99b63 100644 --- a/crates/devup-mcp/src/server/project_plan/tests/feedback.rs +++ b/crates/devup-mcp/src/server/project_plan/tests/feedback.rs @@ -1,5 +1,24 @@ use super::*; +#[tokio::test] +async fn project_facts_auto_fill_screen_answers_with_explicit_answer_precedence() { + let dir = TestDir::new(); + let (mut request, policy, _) = drafted(dir.path(), false).await; + std::fs::write(dir.path().join(".devup/screen-plan.json"), json!({ + "kind":"devup-screen-plan","schemaVersion":1,"planId":"screen","state":"draft", + "questions":[{"id":"siteName"},{"id":"siteUrl"}] + }).to_string()).unwrap(); + request.action = "draft".into(); + request.answers.clear(); + request.answers.insert("siteUrl".into(), json!("https://explicit.example")); + request.evidence = vec![serde_json::from_value(json!({"source":"spec","facts":{ + "siteName":"Chondrogen","siteUrl":"https://source.example" + }})).unwrap()]; + let result = run(&request, &policy).await.unwrap(); + assert_eq!(result["screenPlanForward"]["arguments"]["answers"], json!({"siteName":"Chondrogen","siteUrl":"https://explicit.example"})); + assert!(read(&request.project_root).unwrap()["answers"]["siteName"].is_null()); +} + #[tokio::test] async fn redraft_reports_added_changed_and_kept_records() { let dir = TestDir::new(); From 4934d9a2adaef68d0343f39a719ef1fdda83a376 Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 18:14:13 +0900 Subject: [PATCH 064/149] fix(assets): resolve omitted vector formats to SVG (30) Defer source format selection until the snapshot identifies IMAGE fills versus native vectors. Reconcile resolved formats and output extensions before delivery. Cover vector SVG and photo WebP resolution and committed file bytes. --- .../src/assets/asset_manifest.rs | 20 +++++++--- crates/devup-mcp-figma/src/webp.rs | 4 +- .../tests/hero_source_selection.rs | 28 ++++++++++++++ .../server/projection/project_operation.rs | 9 +++++ .../projection/w1_regressions_hero_source.rs | 38 +++++++++++++++++++ crates/devup-mcp/src/server/validation.rs | 13 ++++--- 6 files changed, 100 insertions(+), 12 deletions(-) diff --git a/crates/devup-mcp-figma/src/assets/asset_manifest.rs b/crates/devup-mcp-figma/src/assets/asset_manifest.rs index 4b1c2093..b92c7369 100644 --- a/crates/devup-mcp-figma/src/assets/asset_manifest.rs +++ b/crates/devup-mcp-figma/src/assets/asset_manifest.rs @@ -48,9 +48,11 @@ pub struct AssetExportStage { pub elapsed_ms: u64, } -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum AssetFormat { + #[default] + Auto, Png, Webp, Jpg, @@ -61,6 +63,7 @@ pub enum AssetFormat { impl AssetFormat { pub fn mime_type(self) -> &'static str { match self { + Self::Auto => "application/octet-stream", Self::Png => "image/png", Self::Webp => "image/webp", Self::Jpg => "image/jpeg", @@ -71,6 +74,7 @@ impl AssetFormat { pub fn extension(self) -> &'static str { match self { + Self::Auto => "", Self::Png => "png", Self::Webp => "webp", Self::Jpg => "jpg", @@ -137,6 +141,7 @@ pub struct AssetSelection { #[serde(default)] pub render_mode: AssetRenderMode, pub asset_id: String, + #[serde(default)] pub format: AssetFormat, pub scale: f64, } @@ -425,7 +430,7 @@ pub fn validate_asset_options( } if max_width.is_some_and(|w| w == 0 || w > crate::MAX_WEBP_DIMENSION) || ((max_width.is_some() || srcset) - && (format != AssetFormat::Webp + && (!matches!(format, AssetFormat::Webp | AssetFormat::Auto) || !mode.is_original() || mode == AssetRenderMode::Fill)) { @@ -922,7 +927,12 @@ pub fn resolve_asset_selections( }) .flatten(); let asset = source_fill.unwrap_or(asset); - let render_mode = if source_fill.is_some() { + let format = if selection.format == AssetFormat::Auto { + if asset.image_hash.is_some() { AssetFormat::Webp } else { AssetFormat::Svg } + } else { selection.format }; + let render_mode = if format == AssetFormat::Svg && selection.format == AssetFormat::Auto { + AssetRenderMode::Node + } else if source_fill.is_some() { AssetRenderMode::OriginalSource } else { selection.render_mode @@ -945,10 +955,10 @@ pub fn resolve_asset_selections( node_id: asset.node_id.clone(), field: asset.field.clone(), image_hash: asset.image_hash.clone(), - format: selection.format, + format, scale: selection.scale, render_mode, - quality: selection.quality, + quality: selection.quality.or((selection.format == AssetFormat::Auto && format == AssetFormat::Webp).then_some(85)), lossless: selection.lossless, }) }) diff --git a/crates/devup-mcp-figma/src/webp.rs b/crates/devup-mcp-figma/src/webp.rs index f6942f12..f7e194eb 100644 --- a/crates/devup-mcp-figma/src/webp.rs +++ b/crates/devup-mcp-figma/src/webp.rs @@ -28,7 +28,9 @@ pub fn validate_webp_options( if quality.is_some_and(|q| q > 100) { return Err(invalid("WebP quality must be an integer from 0 to 100.")); } - if format != AssetFormat::Webp && (quality.is_some() || lossless.is_some()) { + if !matches!(format, AssetFormat::Webp | AssetFormat::Auto) + && (quality.is_some() || lossless.is_some()) + { return Err(invalid( "quality and lossless apply only to format webp; remove them or request webp.", )); diff --git a/crates/devup-mcp-figma/tests/hero_source_selection.rs b/crates/devup-mcp-figma/tests/hero_source_selection.rs index c5115009..5d40b6f8 100644 --- a/crates/devup-mcp-figma/tests/hero_source_selection.rs +++ b/crates/devup-mcp-figma/tests/hero_source_selection.rs @@ -4,6 +4,34 @@ use devup_mcp_figma::{ }; use serde_json::json; +#[test] +fn omitted_source_format_resolves_vector_svg_and_photo_webp() { + let snapshot: Snapshot = serde_json::from_value(json!({ + "fileKey":"defaults", "roots":["icon","photo"], "diagnostics":[], "nodes":{ + "icon":{"id":"icon","type":"VECTOR","fields":{"name":"Icon","width":24,"height":24,"fills":[{"type":"SOLID","color":{"r":0,"g":0,"b":0}}]}}, + "photo":{"id":"photo","type":"RECTANGLE","fields":{"name":"Photo","width":300,"height":200,"fills":[{"type":"IMAGE","imageHash":"photo","scaleMode":"FILL"}]}} + } + })).unwrap(); + for (node, extension) in [("icon", "svg"), ("photo", "webp")] { + let selection: AssetSelection = serde_json::from_value(json!({ + "assetId":format!("{node}:node"),"scale":2 + })) + .unwrap(); + let request = resolve_asset_selections(&snapshot, &[selection]) + .unwrap() + .remove(0); + assert_eq!(request.format.extension(), extension); + assert_eq!( + request.render_mode, + if node == "icon" { + AssetRenderMode::Node + } else { + AssetRenderMode::OriginalSource + } + ); + } +} + #[test] fn hero_node_target_selects_original_photo_without_baking_text_or_buttons() { let snapshot: Snapshot = serde_json::from_value(json!({ diff --git a/crates/devup-mcp/src/server/projection/project_operation.rs b/crates/devup-mcp/src/server/projection/project_operation.rs index 3de92a35..3ed789bb 100644 --- a/crates/devup-mcp/src/server/projection/project_operation.rs +++ b/crates/devup-mcp/src/server/projection/project_operation.rs @@ -125,6 +125,15 @@ async fn project_operation( // Keep requested file destinations and capture reporting attached // to the resolved asset rather than the convenience node alias. for capture in &mut asset_captures { + if capture.format == devup_mcp_figma::AssetFormat::Auto { + let resolved = devup_mcp_figma::resolve_asset_selections(&payload.snapshot, std::slice::from_ref(capture))?.remove(0); + capture.format = resolved.format; + capture.quality = resolved.quality; + capture.render_mode = resolved.render_mode; + if let Some(path) = asset_output_paths.get_mut(&capture.asset_id) { + *path = std::path::Path::new(path).with_extension(resolved.format.extension()).to_string_lossy().into_owned(); + } + } if !matches!(capture.render_mode, devup_mcp_figma::AssetRenderMode::OriginalSource | devup_mcp_figma::AssetRenderMode::Auto) { continue; } diff --git a/crates/devup-mcp/src/server/projection/w1_regressions_hero_source.rs b/crates/devup-mcp/src/server/projection/w1_regressions_hero_source.rs index 6071d702..4537a059 100644 --- a/crates/devup-mcp/src/server/projection/w1_regressions_hero_source.rs +++ b/crates/devup-mcp/src/server/projection/w1_regressions_hero_source.rs @@ -1,3 +1,41 @@ +#[tokio::test] +async fn omitted_format_writes_vector_svg_and_photo_webp() { + for (vector, extension) in [(true, "svg"), (false, "webp")] { + let mut data = original_source_payload(); + let node = data.snapshot.nodes.get_mut("1:2").unwrap(); + if vector { + node.node_type = "VECTOR".into(); + node.fields.insert("fills".into(), json!([{ "type":"SOLID", "color":{"r":0,"g":0,"b":0} }])); + } + let (selections, _) = crate::server::validation::parse_asset_requests(&[ + serde_json::from_value(json!({"nodeId":"1:2"})).unwrap() + ]).unwrap(); + let request = devup_mcp_figma::resolve_asset_selections(&data.snapshot, &selections).unwrap().remove(0); + let bytes = if vector { b"".to_vec() } else { STANDARD.decode(data.assets[0].data_base64.as_ref().unwrap()).unwrap() }; + if vector { + data.assets = vec![devup_mcp_figma::exported_asset_from_bytes(&request, &bytes).unwrap()]; + } else { + data.assets[0].usage_aspect_ratio = None; + data.assets[0].max_width = None; + data.assets[0].srcset = false; + data.assets[0].lossless = None; + } + let root = std::env::temp_dir().join(format!("default-format-{}", rand::random::())); + std::fs::create_dir_all(&root).unwrap(); + let mut op = operation(&["assetManifest"]); + if let PendingOperation::Export { asset_captures, asset_output_paths, .. } = &mut op { + *asset_captures = selections; + asset_output_paths.insert("1:2:node".into(), root.join("asset.png").to_string_lossy().into_owned()); + } + let result = project(data, op).await.unwrap(); + let output = result["assetManifest"]["assets"].as_array().unwrap().iter().find(|asset| asset["status"] == "exported").unwrap(); + let path = std::path::Path::new(output["outputPath"].as_str().unwrap()); + assert_eq!(path.extension().unwrap(), extension); + assert_eq!(std::fs::read(path).unwrap(), bytes); + std::fs::remove_dir_all(root).unwrap(); + } +} + #[tokio::test] async fn hero_node_source_selection_preserves_the_requested_file_destination() { let mut encoded = std::io::Cursor::new(Vec::new()); diff --git a/crates/devup-mcp/src/server/validation.rs b/crates/devup-mcp/src/server/validation.rs index c1c1ee0f..6ddf15c3 100644 --- a/crates/devup-mcp/src/server/validation.rs +++ b/crates/devup-mcp/src/server/validation.rs @@ -302,8 +302,9 @@ pub(super) fn parse_asset_requests( false, )); } - let defaulted_webp = request.format.is_none() && request.render_mode == "original-source"; - let format = match request.format.as_deref().unwrap_or(if defaulted_webp { "webp" } else { "png" }) { + let defaulted_source = request.format.is_none() && request.render_mode == "original-source"; + let format = match request.format.as_deref().unwrap_or(if defaulted_source { "auto" } else { "png" }) { + "auto" if defaulted_source => AssetFormat::Auto, "png" => AssetFormat::Png, "webp" => AssetFormat::Webp, "jpg" | "jpeg" => AssetFormat::Jpg, @@ -334,7 +335,7 @@ pub(super) fn parse_asset_requests( )); } }; - let quality = request.quality.or(defaulted_webp.then_some(85)); + let quality = request.quality; devup_mcp_figma::validate_webp_options(format, quality, request.lossless)?; if render_mode == devup_mcp_figma::AssetRenderMode::Fill && (!matches!( @@ -933,13 +934,13 @@ mod r30_tests { use super::*; #[test] - fn omitted_format_defaults_original_source_to_webp_q85_and_explicit_formats_are_kept() { + fn omitted_source_format_is_deferred_and_explicit_formats_are_kept() { let parse = |v: serde_json::Value| { parse_asset_requests(&[serde_json::from_value(v).unwrap()]).unwrap() }; let (defaulted, _) = parse(json!({"assetId":"picture:fills:0"})); - assert_eq!(defaulted[0].format, AssetFormat::Webp); - assert_eq!(defaulted[0].quality, Some(85)); + assert_eq!(defaulted[0].format, AssetFormat::Auto); + assert_eq!(defaulted[0].quality, None); let (png, _) = parse(json!({"assetId":"picture:fills:0","format":"png"})); assert_eq!(png[0].format, AssetFormat::Png); assert_eq!(png[0].quality, None); From 87baf6ced4aaffaf361d8e4a055c99ce7389148e Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 18:15:59 +0900 Subject: [PATCH 065/149] fix(assets): review code-only and unmapped asset URLs (31) Parse generated JSX image, mask and background URLs even when no manifest was requested. Require review for references without written files and written manifest URLs absent from generated code. Reuse the validator parser and cover both gaps. --- crates/devup-mcp/src/server/asset_guard.rs | 2 +- crates/devup-mcp/src/server/review.rs | 35 ++++++++++++++++++++-- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/crates/devup-mcp/src/server/asset_guard.rs b/crates/devup-mcp/src/server/asset_guard.rs index 5f0d9394..ceaae7f6 100644 --- a/crates/devup-mcp/src/server/asset_guard.rs +++ b/crates/devup-mcp/src/server/asset_guard.rs @@ -331,7 +331,7 @@ fn served(dirs: &[PathBuf], url: &str) -> bool { /// Root-relative asset URLs with their byte ranges: the quoted value of a /// `src` prop or property, and the inside of every `url(...)`. -fn asset_urls(source: &str) -> Vec<(String, [usize; 2])> { +pub(super) fn asset_urls(source: &str) -> Vec<(String, [usize; 2])> { #[derive(Default)] struct Literals { src: Vec<[usize; 2]>, diff --git a/crates/devup-mcp/src/server/review.rs b/crates/devup-mcp/src/server/review.rs index e7c38c9a..26305bc7 100644 --- a/crates/devup-mcp/src/server/review.rs +++ b/crates/devup-mcp/src/server/review.rs @@ -71,13 +71,24 @@ fn has_line_height(value: &Value) -> bool { } fn collect(response: &Value, scope: &str, out: &mut Vec) { - let placeholders = response["assetManifest"]["assets"].as_array().into_iter().flatten() + let sources = ["tsx", "componentTsx", "responsiveTsx"].into_iter() + .filter_map(|field| response[field].as_str()).collect::>(); + let referenced = sources.iter().flat_map(|source| super::asset_guard::asset_urls(source).into_iter().map(|(url, _)| url)) + .collect::>(); + let written = response["assetManifest"]["assets"].as_array().into_iter().flatten() + .filter(|asset| asset["status"] == "exported" && asset["outputPath"].is_string()) + .filter_map(|asset| asset["path"].as_str()).collect::>(); + let mut placeholders = response["assetManifest"]["assets"].as_array().into_iter().flatten() .filter(|asset| asset["status"] != "exported" || asset["outputPath"].is_null()) - .filter_map(|asset| asset["path"].as_str()) + .filter_map(|asset| asset["path"].as_str().map(str::to_owned)) .collect::>(); + placeholders.extend(referenced.iter().filter(|url| !written.contains(url.as_str())).cloned()); + if !sources.is_empty() { + placeholders.extend(written.iter().filter(|url| !referenced.contains(**url)).map(|url| (*url).to_owned())); + } if !placeholders.is_empty() { out.push(json!({"severity":"required","scope":scope,"code":"ASSET_PLACEHOLDERS", - "paths":placeholders,"what":"Generated asset URLs are placeholders without a committed output file.", + "paths":placeholders,"what":"Asset URLs lack a committed output file or a matching generated code reference.", "how":"Export the assets with outputPath and assetPublicRoot before using the generated code."})); } for item in response["responsiveReviewChecklist"]["items"] @@ -203,6 +214,24 @@ mod tests { assert_eq!(value["reviewChecklist"][0]["paths"], json!(["/images/main-hero.webp"])); } + #[test] + fn code_only_export_requires_each_unwritten_asset_url() { + let mut value = json!({"tsx":r#"export const Page = () => <>;"#}); + attach(&mut value); + assert_eq!(value["reviewChecklist"][0]["code"], "ASSET_PLACEHOLDERS"); + assert_eq!(value["reviewChecklist"][0]["severity"], "required"); + assert_eq!(value["reviewChecklist"][0]["paths"], json!(["/icons/arrow.svg", "/images/back.webp", "/images/photo.webp"])); + } + + #[test] + fn written_file_without_matching_code_url_requires_review() { + let mut value = json!({"tsx":r#"export const Page = () => ;"#, + "assetManifest":{"assets":[{"status":"exported","path":"/images/photo.webp","outputPath":"public/images/photo.webp"}]}}); + attach(&mut value); + assert_eq!(value["reviewChecklist"][0]["code"], "ASSET_PLACEHOLDERS"); + assert_eq!(value["reviewChecklist"][0]["paths"], json!(["/images/photo.webp", "/images/placeholder.png"])); + } + #[test] fn issues_group_by_code_and_lossy_is_required() { let mut value = json!({"status":"partial","quality":{"projection":"lossy"}, From fbeca520b51bf1684cef319e688ccae9650349fc Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 18:22:43 +0900 Subject: [PATCH 066/149] fix(project-plan): scope worker route and file contracts (22) --- .../src/server/project_plan/tests/feedback.rs | 31 +++++++++++++++++++ .../src/server/project_plan/units.rs | 18 ++++++++--- 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/crates/devup-mcp/src/server/project_plan/tests/feedback.rs b/crates/devup-mcp/src/server/project_plan/tests/feedback.rs index 0cb99b63..d6a9f010 100644 --- a/crates/devup-mcp/src/server/project_plan/tests/feedback.rs +++ b/crates/devup-mcp/src/server/project_plan/tests/feedback.rs @@ -1,5 +1,36 @@ use super::*; +#[tokio::test] +async fn split_briefs_contain_only_owned_route_and_worker_files() { + let dir = TestDir::new(); + let (request, policy, _) = drafted(dir.path(), false).await; + run(&request, &policy).await.unwrap(); + let mut plan = read(&request.project_root).unwrap(); + plan["features"][0]["screens"] = json!(["/posts"]); + plan["facts"]["posts.exports"] = json!(["exports/posts.tsx"]); + plan["facts"]["posts.assets"] = json!([{"path":"/posts.webp","url":"figma-bridge://current?node-id=1-2"}]); + plan["facts"]["posts.imports"] = json!(["@/components/common/PageHero"]); + plan["facts"]["protectedFiles"] = json!(["src/components/layout/Header.tsx"]); + plan["screenPlan"] = json!({"state":"confirmed","routes":[ + {"url":"/posts","nodeIds":["1:2"],"file":"src/app/posts/page.tsx"}, + {"url":"/unrelated","nodeIds":["3:4"],"file":"src/app/unrelated/page.tsx"} + ]}); + let mut tx = OutputTransaction::new(); + let mut split = request.clone(); + split.action = "split".into(); + units::split(&split, &mut plan, &mut tx, &policy).unwrap(); + tx.commit().unwrap(); + let unit = array(&plan["units"]).iter().find(|u| u["id"] == "posts-front").unwrap(); + assert_eq!(unit["contract"]["routes"].as_array().unwrap().len(), 1); + assert_eq!(unit["contract"]["exports"], json!(["exports/posts.tsx"])); + assert_eq!(unit["contract"]["imports"], json!(["@/components/common/PageHero"])); + assert_eq!(unit["contract"]["assets"][0]["path"], "/posts.webp"); + assert_eq!(unit["contract"]["protectedFiles"], json!(["src/components/layout/Header.tsx"])); + let brief = std::fs::read_to_string(dir.path().join(".devup/units/posts-front.md")).unwrap(); + assert!(brief.contains("exports/posts.tsx")); + assert!(!brief.contains("/unrelated")); +} + #[tokio::test] async fn project_facts_auto_fill_screen_answers_with_explicit_answer_precedence() { let dir = TestDir::new(); diff --git a/crates/devup-mcp/src/server/project_plan/units.rs b/crates/devup-mcp/src/server/project_plan/units.rs index f9cf3bcd..5d5ce03b 100644 --- a/crates/devup-mcp/src/server/project_plan/units.rs +++ b/crates/devup-mcp/src/server/project_plan/units.rs @@ -71,12 +71,13 @@ fn unit_entities(plan: &Value, u: &Value) -> Vec { .cloned() .collect() } -fn slim_screens(screen: &Value) -> Value { +fn slim_screens(screen: &Value, screens: &[Value]) -> Value { if screen.is_null() { return Value::Null; } let routes = array(&screen["routes"]) .iter() + .filter(|route| screens.contains(&route["url"]) || array(&route["nodeIds"]).iter().any(|node| screens.contains(node))) .map(|r| json!({"url": r["url"], "nodeIds": r["nodeIds"], "file": r["file"]})) .collect::>(); json!({"fullPlan": ".devup/screen-plan.json", "state": screen["state"], "routes": routes}) @@ -196,7 +197,16 @@ pub(super) fn split( context.remove(key); } } - for u in &units { + for u in &mut units { + let owner = unit_owner(u); + let screens = array(&plan["features"]).iter().find(|feature| feature["id"] == owner) + .map(|feature| array(&feature["screens"])).unwrap_or_default(); + let screen_slice = slim_screens(&screen_context, screens); + let mut contract = json!({"routes":screen_slice["routes"],"protectedFiles":plan["facts"]["protectedFiles"]}); + for field in ["exports", "assets", "imports"] { + contract[field] = plan["facts"][format!("{owner}.{field}")].clone(); + } + u["contract"] = contract; let brief = format!( include_str!("../text/units-split-bf50d8a5.md"), text(&u["id"]), @@ -205,8 +215,8 @@ pub(super) fn split( text(&plan["unitQuestions"]), slim_context(plan, u), erd::mermaid(&unit_entities(plan, u)), - slim_screens(&screen_context), - json!({"acceptance":u["acceptance"],"checklist":u["checklist"]}) + screen_slice, + json!({"acceptance":u["acceptance"],"checklist":u["checklist"],"contract":u["contract"]}) ); stage( tx, From 40e424ac431abf987048269cc183232964a91d51 Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 18:23:28 +0900 Subject: [PATCH 067/149] fix(screen-plan): make draft route recommendations collision-free (18) --- .../server/audit_plan/planning_and_capture.rs | 5 +- .../src/server/screen_plan/draft_plan_with.rs | 1 + .../src/server/screen_plan/state_variants.rs | 51 +++++++++++++++++++ .../server/screen_plan_tests/prevention.rs | 33 ++++++++++++ crates/devup-mcp/tests/screen_plan_tool.rs | 26 ++++++++++ 5 files changed, 112 insertions(+), 4 deletions(-) diff --git a/crates/devup-mcp/src/server/audit_plan/planning_and_capture.rs b/crates/devup-mcp/src/server/audit_plan/planning_and_capture.rs index 2f61bda6..a302f1b4 100644 --- a/crates/devup-mcp/src/server/audit_plan/planning_and_capture.rs +++ b/crates/devup-mcp/src/server/audit_plan/planning_and_capture.rs @@ -795,10 +795,7 @@ pub(super) async fn write( check.existing_routes = evidence.routes.iter().map(|(route, _)| route.clone()).collect(); } } - let plan_path = Path::new(root) - .join(AUDIT_DIR) - .join(&screen) - .join("plan.json"); + let plan_path = Path::new(root).join(AUDIT_DIR).join(&screen).join("plan.json"); let report_dir = Path::new(root).join(AUDIT_DIR).join(&screen).join("report"); let counts = built.plan.screens[0].checks.iter().fold( BTreeMap::::new(), diff --git a/crates/devup-mcp/src/server/screen_plan/draft_plan_with.rs b/crates/devup-mcp/src/server/screen_plan/draft_plan_with.rs index 87487b0a..62817947 100644 --- a/crates/devup-mcp/src/server/screen_plan/draft_plan_with.rs +++ b/crates/devup-mcp/src/server/screen_plan/draft_plan_with.rs @@ -613,6 +613,7 @@ pub(super) fn draft_plan_with( { *asked = with_when(asked.clone(), &base_when); } + distinct_recommended_routes(&rows, &mut questions); let inventory = frame_inventory(screens, &out_of_scope, &excluded_rows, &kept); let labels = link_label_check(screens, &out_of_scope, &excluded_rows, &kept); let body = json!({"source": source, "screens": rows, "questions": questions, "notes": notes, diff --git a/crates/devup-mcp/src/server/screen_plan/state_variants.rs b/crates/devup-mcp/src/server/screen_plan/state_variants.rs index 5ff36675..1b444dff 100644 --- a/crates/devup-mcp/src/server/screen_plan/state_variants.rs +++ b/crates/devup-mcp/src/server/screen_plan/state_variants.rs @@ -94,3 +94,54 @@ pub(super) fn audit_routes(plan: &Value) -> Vec { targets }).collect() } + +fn distinct_recommended_routes(screens: &[Value], questions: &mut [Value]) { + let mut ordered = questions.iter().collect::>(); + ordered.sort_by_key(|question| depth(question, questions)); + let mut actions = BTreeMap::new(); + for question in ordered { + if required(question, &actions) { + actions.insert(text(&question["id"]).to_owned(), question["resolve"][0]["action"].clone()); + } + } + let folded = folded_ids(&state_folds(questions, &actions)); + let mut placements = Vec::new(); + for screen in screens { + let id = text(&screen["id"]); + if folded.contains(id) || array(&screen["structureQuestions"]).iter().any(|question| actions.get(text(question)).is_some_and(|action| action["designFix"] == true)) { + continue; + } + if let Some(page) = actions.get("base-page").and_then(|action| action["pageId"].as_str()) + && !array(&screen["frames"]).iter().any(|frame| frame["pageId"] == page) { continue; } + let separate = actions.get(text(&screen["groupingQuestion"])).is_some_and(|action| action["group"] == "separate"); + let ids = if separate { + array(&screen["frames"]).iter().map(|frame| format!("split-route:{}", text(&frame["nodeId"]))).collect::>() + } else { + ["route", "modal", "review"].iter().map(|prefix| format!("{prefix}:{id}")).filter(|id| actions.contains_key(id)).take(1).collect() + }; + if ids.is_empty() { + if let Some(route) = screen["proposedRoute"].as_str() { placements.push((screen, None, route.to_owned())); } + } else { + for id in ids { + if let Some(route) = actions[&id]["route"].as_str() { placements.push((screen, Some(id), route.to_owned())); } + } + } + } + let mut taken = BTreeSet::new(); + placements.sort_by_key(|(_, question, _)| question.is_some()); + for (screen, question_id, route) in placements { + let app = text(&screen["app"]).to_owned(); + if taken.insert((app.clone(), route.clone())) { continue; } + let Some(id) = question_id else { continue; }; + let mut suggestion = suggested_route(text(&screen["name"]), text(&screen["id"]), &route); + while !taken.insert((app.clone(), suggestion.clone())) { + suggestion.push_str(&format!("-{}", text(&screen["id"]).replace(':', "-"))); + } + let asked = questions.iter_mut().find(|question| question["id"] == id).expect("placement question exists"); + let mut choices = vec![choice(&suggestion, format!("Distinct suggested URL: {route} is already recommended for another screen in this app."), json!({"route":suggestion}))]; + choices.extend(array(&asked["resolve"]).iter().zip(array(&asked["options"])).map(|(resolve, option)| choice(text(&resolve["label"]), text(&option["description"]), resolve["action"].clone()))); + let replacement = question(id, text(&asked["header"]).to_owned(), text(&asked["question"]).to_owned(), choices, text(&asked["kind"])); + asked["resolve"] = replacement["resolve"].clone(); + asked["options"] = replacement["options"].clone(); + } +} diff --git a/crates/devup-mcp/src/server/screen_plan_tests/prevention.rs b/crates/devup-mcp/src/server/screen_plan_tests/prevention.rs index b05775fe..24e8551b 100644 --- a/crates/devup-mcp/src/server/screen_plan_tests/prevention.rs +++ b/crates/devup-mcp/src/server/screen_plan_tests/prevention.rs @@ -87,6 +87,39 @@ async fn near_identical_widths_in_one_section_can_be_states_of_one_url() { assert_eq!(result["plans"][1]["state"]["nodeId"], "30:2"); assert_eq!(array(&read_plan(fixture.root()).unwrap().unwrap()["audit"]["plans"]).len(), 3); } +#[tokio::test] +async fn r18_contact_and_news_recommendations_confirm_without_route_conflicts() { + let fixture = Fixture::new(); + let mut screens = Vec::new(); + for (section, count) in [("Contact", 4), ("News", 3), ("Patents", 2), ("Research", 2)] { + for index in 0..count { + screens.push(sectioned(&format!("{section}:desktop-{index}"), "Desktop", 1440.0 + f64::from(index), section)); + screens.push(sectioned(&format!("{section}:mobile-{index}"), "Mobile", 360.0 + f64::from(index), section)); + } + } + let mut request = drafted(&fixture, &screens).await; + request.accept_recommended = true; + let confirmed = confirm(&request, &fixture.policy()).unwrap(); + let routes = array(&confirmed["routes"]); + let urls = routes.iter().filter_map(|route| route["url"].as_str()).collect::>(); + assert_eq!(urls.len(), routes.len()); + assert!(!urls.contains("/desktop")); +} + +#[tokio::test] +async fn r18_shared_explicit_route_recommendations_are_distinct_before_confirm() { + let fixture = Fixture::new(); + let mut request = input(&fixture, "draft"); + request.navigation_targets = BTreeMap::from([("44:4721".into(), "/contact".into()), ("44:5652".into(), "/contact".into())]); + let result = draft(&request, &[sectioned("44:4721", "Desktop", 1440.0, "Contact"), sectioned("44:5652", "Desktop", 1440.0, "Contact other")], json!({}), &fixture.policy()).await.unwrap(); + request.action = "confirm".into(); + request.plan_id = result["planId"].as_str().map(str::to_owned); + request.accept_recommended = true; + let confirmed = confirm(&request, &fixture.policy()).unwrap(); + assert_eq!(array(&confirmed["routes"]).len(), 2); + assert_ne!(confirmed["routes"][0]["url"], confirmed["routes"][1]["url"]); +} + #[tokio::test] async fn r19_device_only_frames_in_each_section_confirm_as_one_responsive_url() { let fixture = Fixture::new(); diff --git a/crates/devup-mcp/tests/screen_plan_tool.rs b/crates/devup-mcp/tests/screen_plan_tool.rs index 67e02c45..820ce888 100644 --- a/crates/devup-mcp/tests/screen_plan_tool.rs +++ b/crates/devup-mcp/tests/screen_plan_tool.rs @@ -95,6 +95,32 @@ fn project(name: &str) -> std::path::PathBuf { const FILE: &str = "https://www.figma.com/design/FileKey123/Site"; +#[tokio::test] +async fn recommended_contact_routes_confirm_through_the_tool_surface() -> anyhow::Result<()> { + let root = project("recommended-contact"); + let (failed, draft) = call( + &root, + "devup_screen_plan", + json!({ + "action":"draft", "projectRoot":root, "screens":[ + {"nodeId":"44:4721", "name":"Contact desktop", "width":1440}, + {"nodeId":"44:5652", "name":"Contact mobile", "width":360, "section":"other"} + ], "navigationTargets":{"44:4721":"/contact", "44:5652":"/contact"} + }), + ) + .await?; + assert!(!failed, "{draft}"); + let (failed, confirmed) = call(&root, "devup_screen_plan", json!({ + "action":"confirm", "projectRoot":root, "planId":draft["planId"], "acceptRecommended":true + })).await?; + assert!(!failed, "{confirmed}"); + let routes = confirmed["routes"].as_array().unwrap(); + assert_eq!(routes.len(), 2); + assert_ne!(routes[0]["url"], routes[1]["url"]); + std::fs::remove_dir_all(root)?; + Ok(()) +} + #[tokio::test] async fn draft_asks_everything_at_once_and_confirm_needs_every_answer() -> anyhow::Result<()> { let root = project("flow"); From eb8e608fe53105eca4721ed771ca4205d66f57d8 Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 18:24:41 +0900 Subject: [PATCH 068/149] fix(project-plan): prioritize confirmed scope overrides (23) --- .../src/server/project_plan/tests/feedback.rs | 15 +++++++++++++++ crates/devup-mcp/src/server/project_plan/units.rs | 4 ++++ 2 files changed, 19 insertions(+) diff --git a/crates/devup-mcp/src/server/project_plan/tests/feedback.rs b/crates/devup-mcp/src/server/project_plan/tests/feedback.rs index d6a9f010..a1e90a11 100644 --- a/crates/devup-mcp/src/server/project_plan/tests/feedback.rs +++ b/crates/devup-mcp/src/server/project_plan/tests/feedback.rs @@ -1,5 +1,20 @@ use super::*; +#[tokio::test] +async fn split_scope_prioritizes_confirmed_feature_answers() { + let dir = TestDir::new(); + let (request, policy, _) = drafted(dir.path(), false).await; + run(&request, &policy).await.unwrap(); + let mut plan = read(&request.project_root).unwrap(); + plan["features"][0]["scope"] = json!("Posts with /ko prefix"); + plan["answers"]["posts.locale"] = json!("No locale prefix"); + let mut tx = OutputTransaction::new(); + units::split(&request, &mut plan, &mut tx, &policy).unwrap(); + let front = array(&plan["units"]).iter().find(|u| u["id"] == "posts-front").unwrap(); + assert_eq!(front["scopeOverrides"]["posts.locale"], "No locale prefix"); + assert_eq!(front["scopePrecedence"], json!(["answers", "decisions", "scope"])); +} + #[tokio::test] async fn split_briefs_contain_only_owned_route_and_worker_files() { let dir = TestDir::new(); diff --git a/crates/devup-mcp/src/server/project_plan/units.rs b/crates/devup-mcp/src/server/project_plan/units.rs index 5d5ce03b..540b0880 100644 --- a/crates/devup-mcp/src/server/project_plan/units.rs +++ b/crates/devup-mcp/src/server/project_plan/units.rs @@ -56,6 +56,8 @@ fn slim_context(plan: &Value, u: &Value) -> Value { json!({ "fullPlan": ".devup/project-plan.json", "answers": pick(&plan["answers"], &owner, &[]), + "scopePrecedence": u["scopePrecedence"], + "scopeOverrides": u["scopeOverrides"], "facts": pick(&plan["facts"], &owner, &["goal", "parts", "workType"]), "decisions": plan["decisions"], "backendCommands": plan["backendCommands"], @@ -199,6 +201,8 @@ pub(super) fn split( } for u in &mut units { let owner = unit_owner(u); + u["scopeOverrides"] = pick(&plan["answers"], &owner, &[]); + u["scopePrecedence"] = json!(["answers", "decisions", "scope"]); let screens = array(&plan["features"]).iter().find(|feature| feature["id"] == owner) .map(|feature| array(&feature["screens"])).unwrap_or_default(); let screen_slice = slim_screens(&screen_context, screens); From c6050e48cb627e24df0f2ac5188610f34dfb4c42 Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 18:26:58 +0900 Subject: [PATCH 069/149] fix(project-plan): verify runtime backend prerequisites (25) --- .../src/server/project_plan/tests/feedback.rs | 13 +++++++++++++ crates/devup-mcp/src/server/project_plan/wiring.rs | 9 +++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/crates/devup-mcp/src/server/project_plan/tests/feedback.rs b/crates/devup-mcp/src/server/project_plan/tests/feedback.rs index a1e90a11..a75bd29b 100644 --- a/crates/devup-mcp/src/server/project_plan/tests/feedback.rs +++ b/crates/devup-mcp/src/server/project_plan/tests/feedback.rs @@ -1,5 +1,18 @@ use super::*; +#[test] +fn backend_checks_require_runtime_dependencies_and_real_module() { + let dir = TestDir::new(); + std::fs::create_dir_all(dir.path().join("src")).unwrap(); + std::fs::write(dir.path().join("Cargo.toml"), "[dev-dependencies]\nserde = { version = \"1\", features = [\"derive\"] }\nserde_json = \"1\"\n").unwrap(); + std::fs::write(dir.path().join("src/main.rs"), "// mod models;\nfn main() {}\n").unwrap(); + let plan = json!({"state":"confirmed","modelsWorkingDirectory":"."}); + assert_eq!(wiring::backend_checks(&plan, &dir.path().to_string_lossy()).len(), 3); + std::fs::write(dir.path().join("Cargo.toml"), "[dependencies]\nserde = { version = \"1\", features = [\"derive\"] }\nserde_json = \"1\"\n").unwrap(); + std::fs::write(dir.path().join("src/main.rs"), "mod models;\nfn main() {}\n").unwrap(); + assert!(wiring::backend_checks(&plan, &dir.path().to_string_lossy()).is_empty()); +} + #[tokio::test] async fn split_scope_prioritizes_confirmed_feature_answers() { let dir = TestDir::new(); diff --git a/crates/devup-mcp/src/server/project_plan/wiring.rs b/crates/devup-mcp/src/server/project_plan/wiring.rs index 8fc85ef3..52a47bc2 100644 --- a/crates/devup-mcp/src/server/project_plan/wiring.rs +++ b/crates/devup-mcp/src/server/project_plan/wiring.rs @@ -98,8 +98,13 @@ pub(super) fn blockers(plan: &Value, root: &str) -> Vec { } fn declares(cargo: &str, name: &str) -> bool { + let mut runtime = false; cargo.lines().any(|line| { - line.trim_start().strip_prefix(name).is_some_and(|rest| { + let line = line.trim(); + if line.starts_with('[') { + runtime = line == "[dependencies]" || (line.starts_with("[target.") && line.ends_with(".dependencies]")); + } + runtime && line.strip_prefix(name).is_some_and(|rest| { rest.starts_with('.') || rest.trim_start().starts_with('=') }) }) @@ -120,7 +125,7 @@ pub(super) fn backend_checks(plan: &Value, root: &str) -> Vec { } } if let Ok(main) = std::fs::read_to_string(base.join("src/main.rs")) - && !main.contains("mod models") + && !syn::parse_file(&main).is_ok_and(|file| file.items.iter().any(|item| matches!(item, syn::Item::Mod(module) if module.ident == "models"))) { found.push(format!("{dir}/src/main.rs lacks `mod models;`; export does not wire src/models")); } From 1fdaf46eb78bdb7de90fe3ad25b40fc201677ff2 Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 18:28:24 +0900 Subject: [PATCH 070/149] fix(codegen): stabilize diagnostic color precision Workspace feature unification exposes last-bit JSON float differences in proofread fill colors (0.9244915843009948/49 and 0.9695513844490052/51). Round diagnostic-only color channels to 12 decimal places at the evidence source, preserving raw design values and geometric precision. Refresh the proofread diagnostic snapshot to that portable representation and regress adjacent-float equivalence. --- .../src/codegen/evidence.rs | 50 +++++++++++++++++++ ...w_151__wquw_151_proofread_diagnostics.snap | 18 +++---- 2 files changed, 59 insertions(+), 9 deletions(-) diff --git a/crates/devup-mcp-devup-ui/src/codegen/evidence.rs b/crates/devup-mcp-devup-ui/src/codegen/evidence.rs index 19fbe981..ee4561f4 100644 --- a/crates/devup-mcp-devup-ui/src/codegen/evidence.rs +++ b/crates/devup-mcp-devup-ui/src/codegen/evidence.rs @@ -525,6 +525,32 @@ pub(super) fn placement_evidence(snapshot: &Snapshot, node_id: &str) -> Value { original } +fn stable_diagnostic_colors(value: &mut Value) { + match value { + Value::Object(object) => { + if ["r", "g", "b"] + .iter() + .all(|key| object.get(*key).is_some_and(Value::is_number)) + { + for key in ["r", "g", "b", "a"] { + if let Some(channel) = object.get_mut(key) + && channel.as_number().is_some_and(serde_json::Number::is_f64) + && let Some(number) = channel.as_f64() + { + *channel = json!((number * 1e12).round() / 1e12); + } + } + } else { + for child in object.values_mut() { + stable_diagnostic_colors(child); + } + } + } + Value::Array(array) => array.iter_mut().for_each(stable_diagnostic_colors), + _ => {} + } +} + fn geometry(node: &devup_mcp_figma::RawNode) -> Value { let view = node.typed_view(); let mut value = json!({"nodeId":node.id,"nodeType":node.node_type}); @@ -567,6 +593,9 @@ fn geometry(node: &devup_mcp_figma::RawNode) -> Value { "fills", ] { value[field] = view.value(field).cloned().unwrap_or(Value::Null); + if matches!(field, "fills" | "effects") { + stable_diagnostic_colors(&mut value[field]); + } } let missing: Vec<_> = [ "x", @@ -597,3 +626,24 @@ pub(super) fn fallback_original(snapshot: &Snapshot, node_id: &str, property: &s .unwrap_or(Value::Null), } } + +#[cfg(test)] +mod diagnostic_color_tests { + use super::*; + + #[test] + fn geometry_color_diagnostics_ignore_last_bit_parse_differences() { + let diagnostic = |blue| { + let node = serde_json::from_value(json!({"id":"icon","type":"VECTOR","fields":{ + "width":24.000000000001,"fills":[{"type":"SOLID","color":{"r":1,"g":0,"b":blue}}] + }})) + .unwrap(); + geometry(&node) + }; + let first = diagnostic(0.9244915843009948); + let second = diagnostic(0.9244915843009949); + assert_eq!(first, second); + assert_eq!(first["fills"][0]["color"]["b"], json!(0.924491584301)); + assert_eq!(first["width"], json!(24.000000000001)); + } +} diff --git a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151__wquw_151_proofread_diagnostics.snap b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151__wquw_151_proofread_diagnostics.snap index 9aa10468..8fa1a3f2 100644 --- a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151__wquw_151_proofread_diagnostics.snap +++ b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151__wquw_151_proofread_diagnostics.snap @@ -209,9 +209,9 @@ expression: json!(output.diagnostics) } }, "color": { - "b": 0.9244915843009948, - "g": 0.9529501795768738, - "r": 0.9695512056350708 + "b": 0.924491584301, + "g": 0.952950179577, + "r": 0.969551205635 }, "opacity": 1, "type": "SOLID", @@ -549,9 +549,9 @@ expression: json!(output.diagnostics) } }, "color": { - "b": 0.9586742520332336, - "g": 0.96465665102005, - "r": 0.9695513844490052 + "b": 0.958674252033, + "g": 0.96465665102, + "r": 0.969551384449 }, "opacity": 1, "type": "SOLID", @@ -716,9 +716,9 @@ expression: json!(output.diagnostics) } }, "color": { - "b": 0.1764705926179886, - "g": 0.1764705926179886, - "r": 0.4588235318660736 + "b": 0.176470592618, + "g": 0.176470592618, + "r": 0.458823531866 }, "opacity": 1, "type": "SOLID", From 4d82111b598adec482d8b0fe281db4ba6c75c576 Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 18:29:13 +0900 Subject: [PATCH 071/149] fix(project-plan): add supervisor integration stage (40) --- crates/devup-mcp/src/server/project_plan/tests.rs | 4 ++-- .../src/server/project_plan/tests/feedback.rs | 14 ++++++++++++++ crates/devup-mcp/src/server/project_plan/units.rs | 10 ++++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/crates/devup-mcp/src/server/project_plan/tests.rs b/crates/devup-mcp/src/server/project_plan/tests.rs index adc9b0a9..2eeb52b2 100644 --- a/crates/devup-mcp/src/server/project_plan/tests.rs +++ b/crates/devup-mcp/src/server/project_plan/tests.rs @@ -194,7 +194,7 @@ async fn split_three_issues_preserves_dag_and_briefs() { i.action = "split".into(); i.answers.clear(); let r = run(&i, &policy).await.unwrap(); - assert_eq!(array(&r["plan"]["units"]).len(), 3); + assert_eq!(array(&r["plan"]["units"]).len(), 4); assert_eq!(r["plan"]["units"][2]["dependencies"], json!(["DEV-2"])); let brief = std::fs::read_to_string(dir.path().join(".devup/units/DEV-3.md")).unwrap(); for required in [ @@ -219,7 +219,7 @@ async fn feature_split_and_decisions_and_state_gates() { i.action = "split".into(); i.answers.clear(); let r = run(&i, &policy).await.unwrap(); - assert_eq!(array(&r["plan"]["units"]).len(), 5); + assert_eq!(array(&r["plan"]["units"]).len(), 6); assert_eq!( r["plan"]["units"][4]["dependencies"], json!(["posts-client"]) diff --git a/crates/devup-mcp/src/server/project_plan/tests/feedback.rs b/crates/devup-mcp/src/server/project_plan/tests/feedback.rs index a75bd29b..cf231f70 100644 --- a/crates/devup-mcp/src/server/project_plan/tests/feedback.rs +++ b/crates/devup-mcp/src/server/project_plan/tests/feedback.rs @@ -1,5 +1,19 @@ use super::*; +#[tokio::test] +async fn split_adds_supervisor_integration_after_all_workers() { + let dir = TestDir::new(); + let (request, policy, _) = drafted(dir.path(), false).await; + run(&request, &policy).await.unwrap(); + let mut plan = read(&request.project_root).unwrap(); + let mut tx = OutputTransaction::new(); + units::split(&request, &mut plan, &mut tx, &policy).unwrap(); + let integration = array(&plan["units"]).iter().find(|u| u["id"] == "integration").unwrap(); + assert_eq!(integration["stage"], 2); + assert_eq!(integration["owner"], "supervisor"); + assert_eq!(integration["dependencies"], json!(["posts-front"])); +} + #[test] fn backend_checks_require_runtime_dependencies_and_real_module() { let dir = TestDir::new(); diff --git a/crates/devup-mcp/src/server/project_plan/units.rs b/crates/devup-mcp/src/server/project_plan/units.rs index 540b0880..c9eb6a9f 100644 --- a/crates/devup-mcp/src/server/project_plan/units.rs +++ b/crates/devup-mcp/src/server/project_plan/units.rs @@ -192,6 +192,13 @@ pub(super) fn split( } } } + let dependencies = units.iter().filter(|unit| unit["stage"] == 1) + .map(|unit| text(&unit["id"]).to_owned()).collect(); + let mut integration = unit("integration".into(), "Supervisor integration and final verification".into(), + dependencies, json!(["All worker outputs integrated and verified"]), "integration"); + integration["stage"] = json!(2); + integration["owner"] = json!("supervisor"); + units.push(integration); validate(&units)?; let mut screen_context = plan["screenPlan"].clone(); if let Some(context) = screen_context.as_object_mut() { @@ -200,6 +207,9 @@ pub(super) fn split( } } for u in &mut units { + if u["stage"] == 0 { + u["owner"] = json!("supervisor"); + } let owner = unit_owner(u); u["scopeOverrides"] = pick(&plan["answers"], &owner, &[]); u["scopePrecedence"] = json!(["answers", "decisions", "scope"]); From 31a3fa7595fa904411ffe390d7aac0b05a4587b0 Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 18:31:31 +0900 Subject: [PATCH 072/149] fix(project-plan): flag unbound data operation contracts (41) --- .../src/server/project_plan/tests/feedback.rs | 16 ++++++++++++++++ .../devup-mcp/src/server/project_plan/wiring.rs | 9 ++++++--- .../src/server/text/devup-project-plan.md | 2 +- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/crates/devup-mcp/src/server/project_plan/tests/feedback.rs b/crates/devup-mcp/src/server/project_plan/tests/feedback.rs index cf231f70..48787758 100644 --- a/crates/devup-mcp/src/server/project_plan/tests/feedback.rs +++ b/crates/devup-mcp/src/server/project_plan/tests/feedback.rs @@ -1,5 +1,21 @@ use super::*; +#[test] +fn api_contract_report_requires_bindings_and_real_http_operations() { + let dir = TestDir::new(); + std::fs::write(dir.path().join("openapi.json"), json!({"paths":{ + "/posts":{"get":{"operationId":"listPosts"},"parameters":{"operationId":"notAnOperation"}} + }}).to_string()).unwrap(); + let mut plan = json!({"features":[{"id":"news","entities":["post"]}],"apiWiring":[]}); + let root = dir.path().to_string_lossy(); + assert_eq!(wiring::report(&plan, &root)["missing"][0]["feature"], "news"); + plan["apiWiring"] = json!([{"feature":"news","operationId":"notAnOperation"}]); + assert_eq!(array(&wiring::report(&plan, &root)["missing"]).len(), 1); + plan["apiWiring"][0]["operationId"] = json!("listPosts"); + assert!(array(&wiring::report(&plan, &root)["missing"]).is_empty()); + assert_eq!(array(&wiring::report(&plan, &root)["wired"]).len(), 1); +} + #[tokio::test] async fn split_adds_supervisor_integration_after_all_workers() { let dir = TestDir::new(); diff --git a/crates/devup-mcp/src/server/project_plan/wiring.rs b/crates/devup-mcp/src/server/project_plan/wiring.rs index 52a47bc2..e0f7f488 100644 --- a/crates/devup-mcp/src/server/project_plan/wiring.rs +++ b/crates/devup-mcp/src/server/project_plan/wiring.rs @@ -25,6 +25,7 @@ fn operation_ids(path: &Path) -> std::collections::BTreeSet { .into_iter() .flatten() .flat_map(|(_, item)| item.as_object().into_iter().flatten()) + .filter(|(method, _)| matches!(method.as_str(), "get" | "post" | "put" | "patch" | "delete" | "head" | "options" | "trace")) .filter_map(|(_, op)| op["operationId"].as_str().map(str::to_owned)) .collect() } @@ -57,6 +58,11 @@ pub(super) fn report(plan: &Value, root: &str) -> Value { let path = openapi_path(root); let ids = path.as_deref().map(operation_ids).unwrap_or_default(); let (mut wired, mut missing, mut deferred) = (vec![], vec![], vec![]); + for feature in array(&plan["features"]).iter().filter(|feature| !array(&feature["entities"]).is_empty()) { + if !array(&plan["apiWiring"]).iter().any(|w| w["feature"] == feature["id"]) { + missing.push(json!({"feature":feature["id"],"operationId":null,"reason":"Data feature has no operationId binding"})); + } + } for w in array(&plan["apiWiring"]) { if w["defer"] == true { deferred.push(json!({"feature":w["feature"],"reason":w["reason"]})); @@ -75,9 +81,6 @@ pub(super) fn report(plan: &Value, root: &str) -> Value { } pub(super) fn blockers(plan: &Value, root: &str) -> Vec { - if array(&plan["apiWiring"]).is_empty() { - return vec![]; - } let r = report(plan, root); let missing = array(&r["missing"]).iter().map(|m| { format!( diff --git a/crates/devup-mcp/src/server/text/devup-project-plan.md b/crates/devup-mcp/src/server/text/devup-project-plan.md index 5071973d..d022ce34 100644 --- a/crates/devup-mcp/src/server/text/devup-project-plan.md +++ b/crates/devup-mcp/src/server/text/devup-project-plan.md @@ -1 +1 @@ -Project intake and supervisor: intake | draft | confirm | status | assume | defer | split. Read Jira/epic children, Figma and devup_project_context all scopes first; pass source-labelled evidence. intake always asks work type, optional sources, goal and parts through the runtime question tool. draft records feature-specific rounds and ERD; confirm requires all answers and the Figma screen plan when applicable, writes vespertide models/*.json transactionally. split creates work units with a dependency DAG and .devup/units/.md briefs, one per Jira issue or feature/layer. Workers escalate to supervisor by default (unitQuestions: supervisor|worker). After confirm never wait: defer recommendations and continue, assume at the end. status reports ERD Mermaid, unit DAG/states and decisions; unitUpdates record supervisor verification. State: .devup/project-plan.json. Follow /devup skill for backend commands, latest-first dependencies, exact layout/visual audits, orchestration and zero-drift completion. \ No newline at end of file +Project intake and supervisor: intake | draft | confirm | status | assume | defer | split. Read Jira/epic children, Figma and devup_project_context all scopes first; pass source-labelled evidence. intake asks work type, optional sources, goal and parts through the runtime question tool. draft records feature-specific rounds and ERD, merges shared question topics, and reports added/changed/kept/removed records in changeSummary. Shared site facts are forwarded to open screen-plan questions; explicit answers take precedence. confirm requires all answers and the Figma screen plan when applicable, writes vespertide models/*.json transactionally, and returns executable recovery for missing answers. split creates a dependency DAG and .devup/units/.md briefs: shared/table foundations, feature workers, then a supervisor integration unit. Static features have no backend layers; each table has one model owner. Briefs prioritize answers and decisions over original scope, filter routes by feature screens (URL or nodeId), and carry facts keyed .exports, .assets and .imports plus protectedFiles when supplied. Unknown file mappings remain null, never invented. Workers escalate to supervisor by default (unitQuestions: supervisor|worker). After confirm never wait: defer recommendations and continue, assume at the end. status reports ERD Mermaid, unit DAG/states, decisions, shared-change requests and backend prerequisites. Data features with explicit entities need apiWiring operationId bindings; status checks them against actual HTTP operations in openapi.json and reports missing or deferred bindings as blockers. These checks do not prove runtime integration. unitUpdates record supervisor verification. State: .devup/project-plan.json. Follow /devup skill for backend commands, latest-first dependencies, exact layout/visual audits, orchestration and zero-drift completion. From a07388a9938a86c1f1bf6344fd931496022851b9 Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 18:51:35 +0900 Subject: [PATCH 073/149] test(assets): refresh wquw_151 frame snapshots for readable asset names (29) After merging fb-assets, frame snapshots still held raw layer names ('Frame 26086041.svg', Korean names, case-only Icons/icons). Item 29 makes them kebab-case [a-z0-9-] with -2 suffixes on collision, so case-only duplicates become icons.svg / icons-2.svg. --- .../wquw_151_frames__wquw_151_frame_3879_35503.snap | 4 ++-- .../wquw_151_frames__wquw_151_frame_3879_35518.snap | 6 +++--- .../wquw_151_frames__wquw_151_frame_3879_35569.snap | 8 ++++---- .../wquw_151_frames__wquw_151_frame_3879_35652.snap | 8 ++++---- .../wquw_151_frames__wquw_151_frame_3879_35729.snap | 8 ++++---- .../wquw_151_frames__wquw_151_frame_3879_35887.snap | 8 ++++---- .../wquw_151_frames__wquw_151_frame_3879_35973.snap | 8 ++++---- .../wquw_151_frames__wquw_151_frame_3879_36059.snap | 12 ++++++------ .../wquw_151_frames__wquw_151_frame_3879_36108.snap | 12 ++++++------ .../wquw_151_frames__wquw_151_frame_3879_36144.snap | 6 +++--- 10 files changed, 40 insertions(+), 40 deletions(-) diff --git a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35503.snap b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35503.snap index add00ae6..76846968 100644 --- a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35503.snap +++ b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35503.snap @@ -25,7 +25,7 @@ export function Wquw151Frame387935503() { aspectRatio="1.9" bg="#752E2E" h="34px" - maskImage="url('/icons/BI - 아이콘.svg')" + maskImage="url(/icons/bi.svg)" maskPos="center" maskRepeat="no-repeat" maskSize="contain" @@ -77,7 +77,7 @@ export function Wquw151Frame387935503() { minW="0" objectFit="cover" objectPos="center" - src="/images/Frame 26086022.png" + src="/images/loading-frame-26086022.png" /> diff --git a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35518.snap b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35518.snap index ea175cb8..df3691b5 100644 --- a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35518.snap +++ b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35518.snap @@ -86,7 +86,7 @@ export function Wquw151Frame387935518() { aspectRatio="1" bg="$caption" boxSize="16px" - maskImage="url(/icons/Icons.svg)" + maskImage="url(/icons/icons.svg)" maskPos="center" maskRepeat="no-repeat" maskSize="contain" @@ -111,7 +111,7 @@ export function Wquw151Frame387935518() { bg="$captionLight" boxSize="40px" flexShrink="0" - maskImage="url(/icons/icons.svg)" + maskImage="url(/icons/icons-2.svg)" maskPos="center" maskRepeat="no-repeat" maskSize="contain" @@ -190,7 +190,7 @@ export function Wquw151Frame387935518() { aspectRatio="1" bg="$text" boxSize="20px" - maskImage="url(/icons/Icons.svg)" + maskImage="url(/icons/icons.svg)" maskPos="center" maskRepeat="no-repeat" maskSize="contain" diff --git a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35569.snap b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35569.snap index 5f8e74ab..0b2a9e57 100644 --- a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35569.snap +++ b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35569.snap @@ -148,7 +148,7 @@ export function Wquw151Frame387935569() { aspectRatio="1" bg="$caption" boxSize="16px" - maskImage="url(/icons/Icons.svg)" + maskImage="url(/icons/icons.svg)" maskPos="center" maskRepeat="no-repeat" maskSize="contain" @@ -173,7 +173,7 @@ export function Wquw151Frame387935569() { bg="$captionLight" boxSize="40px" flexShrink="0" - maskImage="url(/icons/icons.svg)" + maskImage="url(/icons/icons-2.svg)" maskPos="center" maskRepeat="no-repeat" maskSize="contain" @@ -252,7 +252,7 @@ export function Wquw151Frame387935569() { aspectRatio="1" bg="$text" boxSize="20px" - maskImage="url(/icons/Icons.svg)" + maskImage="url(/icons/icons.svg)" maskPos="center" maskRepeat="no-repeat" maskSize="contain" @@ -614,7 +614,7 @@ export function Wquw151Frame387935569() { aspectRatio="1" bg="$text" boxSize="24px" - maskImage="url(/icons/Icons.svg)" + maskImage="url(/icons/icons.svg)" maskPos="center" maskRepeat="no-repeat" maskSize="contain" diff --git a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35652.snap b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35652.snap index b0da753f..4214318a 100644 --- a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35652.snap +++ b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35652.snap @@ -148,7 +148,7 @@ export function Wquw151Frame387935652() { aspectRatio="1" bg="$caption" boxSize="16px" - maskImage="url(/icons/Icons.svg)" + maskImage="url(/icons/icons.svg)" maskPos="center" maskRepeat="no-repeat" maskSize="contain" @@ -173,7 +173,7 @@ export function Wquw151Frame387935652() { bg="$captionLight" boxSize="40px" flexShrink="0" - maskImage="url(/icons/icons.svg)" + maskImage="url(/icons/icons-2.svg)" maskPos="center" maskRepeat="no-repeat" maskSize="contain" @@ -252,7 +252,7 @@ export function Wquw151Frame387935652() { aspectRatio="1" bg="$text" boxSize="20px" - maskImage="url(/icons/Icons.svg)" + maskImage="url(/icons/icons.svg)" maskPos="center" maskRepeat="no-repeat" maskSize="contain" @@ -614,7 +614,7 @@ export function Wquw151Frame387935652() { aspectRatio="1" bg="$text" boxSize="24px" - maskImage="url(/icons/Icons.svg)" + maskImage="url(/icons/icons.svg)" maskPos="center" maskRepeat="no-repeat" maskSize="contain" diff --git a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35729.snap b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35729.snap index cfae75d1..21a960e9 100644 --- a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35729.snap +++ b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35729.snap @@ -148,7 +148,7 @@ export function Wquw151Frame387935729() { aspectRatio="1" bg="$caption" boxSize="16px" - maskImage="url(/icons/Icons.svg)" + maskImage="url(/icons/icons.svg)" maskPos="center" maskRepeat="no-repeat" maskSize="contain" @@ -173,7 +173,7 @@ export function Wquw151Frame387935729() { bg="$captionLight" boxSize="40px" flexShrink="0" - maskImage="url(/icons/icons.svg)" + maskImage="url(/icons/icons-2.svg)" maskPos="center" maskRepeat="no-repeat" maskSize="contain" @@ -252,7 +252,7 @@ export function Wquw151Frame387935729() { aspectRatio="1" bg="$text" boxSize="20px" - maskImage="url(/icons/Icons.svg)" + maskImage="url(/icons/icons.svg)" maskPos="center" maskRepeat="no-repeat" maskSize="contain" @@ -614,7 +614,7 @@ export function Wquw151Frame387935729() { aspectRatio="1" bg="$text" boxSize="24px" - maskImage="url(/icons/Icons.svg)" + maskImage="url(/icons/icons.svg)" maskPos="center" maskRepeat="no-repeat" maskSize="contain" diff --git a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35887.snap b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35887.snap index 6fadaad5..e4900a3d 100644 --- a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35887.snap +++ b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35887.snap @@ -148,7 +148,7 @@ export function Wquw151Frame387935887() { aspectRatio="1" bg="$caption" boxSize="16px" - maskImage="url(/icons/Icons.svg)" + maskImage="url(/icons/icons.svg)" maskPos="center" maskRepeat="no-repeat" maskSize="contain" @@ -173,7 +173,7 @@ export function Wquw151Frame387935887() { bg="$captionLight" boxSize="40px" flexShrink="0" - maskImage="url(/icons/icons.svg)" + maskImage="url(/icons/icons-2.svg)" maskPos="center" maskRepeat="no-repeat" maskSize="contain" @@ -252,7 +252,7 @@ export function Wquw151Frame387935887() { aspectRatio="1" bg="$text" boxSize="20px" - maskImage="url(/icons/Icons.svg)" + maskImage="url(/icons/icons.svg)" maskPos="center" maskRepeat="no-repeat" maskSize="contain" @@ -611,7 +611,7 @@ export function Wquw151Frame387935887() { aspectRatio="1" bg="$text" boxSize="24px" - maskImage="url(/icons/Icons.svg)" + maskImage="url(/icons/icons.svg)" maskPos="center" maskRepeat="no-repeat" maskSize="contain" diff --git a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35973.snap b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35973.snap index f90c41fd..15097227 100644 --- a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35973.snap +++ b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35973.snap @@ -148,7 +148,7 @@ export function Wquw151Frame387935973() { aspectRatio="1" bg="$caption" boxSize="16px" - maskImage="url(/icons/Icons.svg)" + maskImage="url(/icons/icons.svg)" maskPos="center" maskRepeat="no-repeat" maskSize="contain" @@ -173,7 +173,7 @@ export function Wquw151Frame387935973() { bg="$captionLight" boxSize="40px" flexShrink="0" - maskImage="url(/icons/icons.svg)" + maskImage="url(/icons/icons-2.svg)" maskPos="center" maskRepeat="no-repeat" maskSize="contain" @@ -252,7 +252,7 @@ export function Wquw151Frame387935973() { aspectRatio="1" bg="$text" boxSize="20px" - maskImage="url(/icons/Icons.svg)" + maskImage="url(/icons/icons.svg)" maskPos="center" maskRepeat="no-repeat" maskSize="contain" @@ -611,7 +611,7 @@ export function Wquw151Frame387935973() { aspectRatio="1" bg="$text" boxSize="24px" - maskImage="url(/icons/Icons.svg)" + maskImage="url(/icons/icons.svg)" maskPos="center" maskRepeat="no-repeat" maskSize="contain" diff --git a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_36059.snap b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_36059.snap index abd026e0..86cc64e4 100644 --- a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_36059.snap +++ b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_36059.snap @@ -25,7 +25,7 @@ export function Wquw151Frame387936059() { aspectRatio="1" bg="$caption" h="100%" - maskImage="url('/icons/Frame 26086041.svg')" + maskImage="url(/icons/header-frame-26086041.svg)" maskPos="center" maskRepeat="no-repeat" maskSize="contain" @@ -37,7 +37,7 @@ export function Wquw151Frame387936059() { aspectRatio="1" bg="$captionLight" boxSize="20px" - maskImage="url('/icons/arrow_7134700 1.svg')" + maskImage="url(/icons/arrow-7134700-1.svg)" maskPos="center" maskRepeat="no-repeat" maskSize="contain" @@ -87,7 +87,7 @@ export function Wquw151Frame387936059() { aspectRatio="1" bg="$secondary" boxSize="16px" - maskImage="url('/icons/information-button_1176 1.svg')" + maskImage="url(/icons/information-button-1176-1.svg)" maskPos="center" maskRepeat="no-repeat" maskSize="contain" @@ -129,7 +129,7 @@ export function Wquw151Frame387936059() { > 직접 입력 - +
녹음하기 - +
작성 완료 - +
직접 입력 - +
녹음하기 - +
작성 완료 - +
Date: Fri, 23 Oct 2026 18:57:55 +0900 Subject: [PATCH 074/149] test(runtime-audit): write the real-render hero fixture at its readable asset path (29) Item 29 lowercases asset file names, so generated TSX now references /images/hero.png. Vite preview matches paths case-sensitively, so the fixture written as Hero.png never loaded. --- harness/runtime-audit/test/real-render.test.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/harness/runtime-audit/test/real-render.test.mjs b/harness/runtime-audit/test/real-render.test.mjs index 2f94bfba..e8f58638 100644 --- a/harness/runtime-audit/test/real-render.test.mjs +++ b/harness/runtime-audit/test/real-render.test.mjs @@ -43,7 +43,7 @@ test('production codegen renders in Chromium; geometry alone cannot pass broken const visualBin = join(target, 'debug', process.platform === 'win32' ? 'devup-mcp-visual.exe' : 'devup-mcp-visual'); await mkdir(join(dir, 'public/images'), { recursive: true }); const hero = await readFile(join(harness, 'fixtures/render-hero.png')); - await writeFile(join(dir, 'public/images/Hero.png'), hero); + await writeFile(join(dir, 'public/images/hero.png'), hero); await writeFile(join(dir, 'devup.json'), JSON.stringify({ theme: {} })); await writeFile(join(dir, 'index.html'), '
'); await writeFile(join(dir, 'main.tsx'), `import '@devup-ui/reset-css'; From a785103b0b1f3b367ea05652efad3fa9be56986b Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 19:28:18 +0900 Subject: [PATCH 075/149] fix(project-plan): resolve inherited serde prerequisites (25) --- Cargo.lock | 1 + crates/devup-mcp/Cargo.toml | 1 + .../src/server/project_plan/tests/feedback.rs | 25 +++++++++ .../src/server/project_plan/wiring.rs | 54 ++++++++++++++----- 4 files changed, 67 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0ddde4cc..86aa2848 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -764,6 +764,7 @@ dependencies = [ "syn 2.0.119", "tokio", "tokio-tungstenite", + "toml_edit", "tracing-subscriber", "url", ] diff --git a/crates/devup-mcp/Cargo.toml b/crates/devup-mcp/Cargo.toml index d498ce32..f318acaa 100644 --- a/crates/devup-mcp/Cargo.toml +++ b/crates/devup-mcp/Cargo.toml @@ -35,6 +35,7 @@ serde_json.workspace = true sha2.workspace = true syn.workspace = true tokio.workspace = true +toml_edit = { version = "0.25.15", default-features = false, features = ["parse"] } tracing-subscriber.workspace = true url.workspace = true diff --git a/crates/devup-mcp/src/server/project_plan/tests/feedback.rs b/crates/devup-mcp/src/server/project_plan/tests/feedback.rs index 48787758..6325c5e8 100644 --- a/crates/devup-mcp/src/server/project_plan/tests/feedback.rs +++ b/crates/devup-mcp/src/server/project_plan/tests/feedback.rs @@ -1,5 +1,30 @@ use super::*; +#[test] +fn backend_checks_resolve_workspace_aliases_and_derive_features() { + let dir = TestDir::new(); + let api = dir.path().join("apis/api"); + std::fs::create_dir_all(api.join("src")).unwrap(); + std::fs::write(api.join("src/main.rs"), "mod models; fn main() {}").unwrap(); + let plan = json!({"state":"confirmed","modelsWorkingDirectory":"apis/api"}); + let root = dir.path().to_string_lossy(); + std::fs::write(dir.path().join("Cargo.toml"), "[workspace]\nmembers = [\"apis/api\"]\n[workspace.dependencies]\nserialization = { package = \"serde\", version = \"1\", features = [\"derive\"] }\njson = { package = \"serde_json\", version = \"1\" }\n").unwrap(); + std::fs::write(api.join("Cargo.toml"), "[dependencies]\nserialization.workspace = true\njson.workspace = true\n").unwrap(); + assert!(wiring::backend_checks(&plan, &root).is_empty()); + std::fs::write(api.join("Cargo.toml"), "[dependencies.serialization]\npackage = \"serde\"\nversion = \"1\"\n[dependencies.json]\npackage = \"serde_json\"\nversion = \"1\"\n").unwrap(); + let checks = wiring::backend_checks(&plan, &root); + assert_eq!(checks.len(), 1); + assert!(checks[0].contains("cargo add serde --features derive")); + std::fs::write(api.join("Cargo.toml"), "[dependencies]\nserde = { workspace = true }\nserde_json = { workspace = true }\n").unwrap(); + let checks = wiring::backend_checks(&plan, &root); + assert_eq!(checks.len(), 2, "unresolved workspace dependencies are missing"); + assert!(checks[0].contains("cargo add serde --features derive")); + assert!(checks[1].contains("cargo add serde_json")); + std::fs::write(api.join("Cargo.toml"), "[dependencies]\nserialization = { workspace = true, features = [\"derive\"] }\njson.workspace = true\n").unwrap(); + std::fs::write(dir.path().join("Cargo.toml"), "[workspace]\n[workspace.dependencies]\nserialization = { package = \"serde\", version = \"1\" }\njson = { package = \"serde_json\", version = \"1\" }\n").unwrap(); + assert!(wiring::backend_checks(&plan, &root).is_empty()); +} + #[test] fn api_contract_report_requires_bindings_and_real_http_operations() { let dir = TestDir::new(); diff --git a/crates/devup-mcp/src/server/project_plan/wiring.rs b/crates/devup-mcp/src/server/project_plan/wiring.rs index e0f7f488..0759a970 100644 --- a/crates/devup-mcp/src/server/project_plan/wiring.rs +++ b/crates/devup-mcp/src/server/project_plan/wiring.rs @@ -100,19 +100,42 @@ pub(super) fn blockers(plan: &Value, root: &str) -> Vec { missing.chain(deferred).collect() } -fn declares(cargo: &str, name: &str) -> bool { - let mut runtime = false; - cargo.lines().any(|line| { - let line = line.trim(); - if line.starts_with('[') { - runtime = line == "[dependencies]" || (line.starts_with("[target.") && line.ends_with(".dependencies]")); - } - runtime && line.strip_prefix(name).is_some_and(|rest| { - rest.starts_with('.') || rest.trim_start().starts_with('=') - }) +fn cargo_document(path: &Path) -> Option { + std::fs::read_to_string(path).ok()?.parse().ok() +} + +fn workspace_document(base: &Path, cargo: &toml_edit::DocumentMut) -> Option { + if let Some(workspace) = cargo.get("package").and_then(|p| p.get("workspace")).and_then(toml_edit::Item::as_str) { + return cargo_document(&base.join(workspace).join("Cargo.toml")); + } + base.ancestors().find_map(|ancestor| { + cargo_document(&ancestor.join("Cargo.toml")).filter(|doc| doc.get("workspace").is_some()) }) } +fn has_derive(dep: &toml_edit::Item) -> bool { + dep.get("features").and_then(toml_edit::Item::as_array) + .is_some_and(|features| features.iter().any(|feature| feature.as_str() == Some("derive"))) +} + +fn declares(cargo: &toml_edit::DocumentMut, workspace: Option<&toml_edit::DocumentMut>, name: &str) -> bool { + let mut tables = vec![cargo.get("dependencies")]; + if let Some(targets) = cargo.get("target").and_then(toml_edit::Item::as_table_like) { + tables.extend(targets.iter().map(|(_, target)| target.get("dependencies"))); + } + tables.into_iter().flatten().filter_map(toml_edit::Item::as_table_like) + .flat_map(|table| table.iter()).any(|(key, dep)| { + let inherited = dep.get("workspace").and_then(toml_edit::Item::as_bool) == Some(true); + let resolved = if inherited { + workspace.and_then(|doc| doc.get("workspace")?.get("dependencies")?.get(key)) + } else { Some(dep) }; + resolved.is_some_and(|resolved| { + let package = resolved.get("package").and_then(toml_edit::Item::as_str).unwrap_or(key); + package == name && (name != "serde" || has_derive(dep) || has_derive(resolved)) + }) + }) +} + pub(super) fn backend_checks(plan: &Value, root: &str) -> Vec { let dir = text(&plan["modelsWorkingDirectory"]); if plan["state"] != "confirmed" || dir.is_empty() { @@ -120,12 +143,15 @@ pub(super) fn backend_checks(plan: &Value, root: &str) -> Vec { } let base = Path::new(root).join(dir); let mut found = vec![]; - if let Ok(cargo) = std::fs::read_to_string(base.join("Cargo.toml")) { - for dep in ["serde", "serde_json"] { - if !declares(&cargo, dep) { - found.push(format!("{dir}/Cargo.toml lacks {dep}; exported entities do not compile without it")); + if let Some(cargo) = cargo_document(&base.join("Cargo.toml")) { + let workspace = workspace_document(&base, &cargo); + for (dep, command) in [("serde", "cargo add serde --features derive"), ("serde_json", "cargo add serde_json")] { + if !declares(&cargo, workspace.as_ref(), dep) { + found.push(format!("{dir}/Cargo.toml lacks {dep}{}; run `{command}` in {dir}", if dep == "serde" { " with derive" } else { "" })); } } + } else if base.join("Cargo.toml").is_file() { + found.push(format!("{dir}/Cargo.toml cannot be parsed as TOML; fix the manifest before checking exported entity dependencies")); } if let Ok(main) = std::fs::read_to_string(base.join("src/main.rs")) && !syn::parse_file(&main).is_ok_and(|file| file.items.iter().any(|item| matches!(item, syn::Item::Mod(module) if module.ident == "models"))) From 488bbbc80f9b5dd2d017abca4fe15f35571f736b Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 19:29:42 +0900 Subject: [PATCH 076/149] fix(skills): verify with lint fix before manual leftovers (45) --- .../src/server/skills/devfive-frontend/SKILL.md | 6 +++++- .../server/skills/devup-implementation-plan/SKILL.md | 9 ++++++--- crates/devup-mcp/src/server/skills/devup/SKILL.md | 5 ++++- crates/devup-mcp/src/server/skills/manifest.json | 12 ++++++------ crates/devup-mcp/src/server/skills/tests.rs | 9 +++++++++ 5 files changed, 30 insertions(+), 11 deletions(-) diff --git a/crates/devup-mcp/src/server/skills/devfive-frontend/SKILL.md b/crates/devup-mcp/src/server/skills/devfive-frontend/SKILL.md index 4db3b561..9e27e264 100644 --- a/crates/devup-mcp/src/server/skills/devfive-frontend/SKILL.md +++ b/crates/devup-mcp/src/server/skills/devfive-frontend/SKILL.md @@ -397,10 +397,14 @@ export function Card() { ### Required Verification +Run `bun run lint:fix` first, then `bunx oxlint --deny-warnings` at the root. +Never fix JSX prop order, import sorting or formatting errors by hand. +Fix only non-fixable leftovers by hand (type errors, logic and decisions the fixer cannot make). + ```bash bun run lint:fix # FIRST: fixes JSX prop order, formatting, imports; never hand-edit those -bun run lint # cargo clippy/fmt/check (prelint), then oxlint bunx oxlint --deny-warnings # warnings count: keep them at 0 +bun run lint # cargo clippy/fmt/check (prelint), then oxlint bun tsc --noEmit # inside the app you changed (TS 7) bun test bun run build # vinext build for every app diff --git a/crates/devup-mcp/src/server/skills/devup-implementation-plan/SKILL.md b/crates/devup-mcp/src/server/skills/devup-implementation-plan/SKILL.md index 4f6804ae..31e24255 100644 --- a/crates/devup-mcp/src/server/skills/devup-implementation-plan/SKILL.md +++ b/crates/devup-mcp/src/server/skills/devup-implementation-plan/SKILL.md @@ -441,9 +441,12 @@ including a "view transitions: enabled (mechanism) / skipped (reason)" line. status. Missing references, unavailable browser tooling, not-run comparisons, converter bugs and unresolved findings remain incomplete with evidence. Preserve screenshots, reports, Figma reference identities and diffs. - Before reporting done, also: - - - every confirmed URL is implemented; + Before reporting done, also: + + - run `bun run lint:fix` first, then `bunx oxlint --deny-warnings` at the root. + Never fix JSX prop order, import sorting or formatting errors by hand. + Fix only non-fixable leftovers by hand (type errors, logic and decisions the fixer cannot make). + - every confirmed URL is implemented; - the build passes: `bun run build` (`vinext build` for every app); - lint and typecheck pass: `bun run lint` (its prelint runs cargo clippy, fmt and check, then oxlint), `bunx oxlint --deny-warnings`, and `bun tsc diff --git a/crates/devup-mcp/src/server/skills/devup/SKILL.md b/crates/devup-mcp/src/server/skills/devup/SKILL.md index 1af92a06..fcc37f78 100644 --- a/crates/devup-mcp/src/server/skills/devup/SKILL.md +++ b/crates/devup-mcp/src/server/skills/devup/SKILL.md @@ -196,7 +196,10 @@ the full rules; these are the steps. converter bugs and unresolved findings with evidence as incomplete. Also: every URL implemented, the production build free of `data-figma-node-id` (the strip plugin in vite.config; `grep -rl data-figma-node-id dist` finds - nothing), `bun run build`, `bun run lint:fix` first (never hand-sort props or run bare prettier; formatting is owned by oxlint's devup rule), `bun run lint`, `bunx oxlint --deny-warnings`, `bun tsc --noEmit` +nothing). Run `bun run lint:fix` first, then `bunx oxlint --deny-warnings` at the root. +Never fix JSX prop order, import sorting or formatting errors by hand. +Fix only non-fixable leftovers by hand (type errors, logic and decisions the fixer cannot make). +Then run `bun run build`, `bun run lint`, `bun tsc --noEmit` per changed app, `bun test` (on Windows `bun test` and `cargo test`), `devup_ui_validate` without errors, site completeness filled. 13. **Final report.** End with status's `finalReport` and `summary`: URL x diff --git a/crates/devup-mcp/src/server/skills/manifest.json b/crates/devup-mcp/src/server/skills/manifest.json index 94af422f..51f1a550 100644 --- a/crates/devup-mcp/src/server/skills/manifest.json +++ b/crates/devup-mcp/src/server/skills/manifest.json @@ -33,8 +33,8 @@ "documents": [ { "path": "SKILL.md", - "bytes": 25605, - "sha256": "4d494c9aa81394f3c41b6adc332dcbdb648d0dc42c8710381084be55b684d2e8" + "bytes": 25852, + "sha256": "49a1323ed8d5882713d97a8654c8ae347abbed5c102cc46f186ddefae2ccf2b3" }, { "path": "references/critical-rules.md", @@ -71,8 +71,8 @@ "documents": [ { "path": "SKILL.md", - "bytes": 38776, - "sha256": "a45af62f6a3a8c31e478f1dde4af77676a7b45c32a3c5e6dee94ff451deefea8" + "bytes": 39034, + "sha256": "f1f66659d16582b2338fd6c567f8873a81067fc1ca0bd620270d5fc894509bae" } ], "sourceUrl": "https://github.com/dev-five-git/devup-mcp/blob/HEAD/crates/devup-mcp/src/server/skills/devup-implementation-plan", @@ -89,8 +89,8 @@ "documents": [ { "path": "SKILL.md", - "bytes": 19411, - "sha256": "dd8cd591a962069d58feee746637acb1e17fe06d10f3e1d82732e98af75b2139" + "bytes": 19515, + "sha256": "864dfb98444f6ddb2bc13ce21ce0262a6f51b421d95013f1a8c2aa2478e3a0c1" } ], "sourceUrl": "https://github.com/dev-five-git/devup-mcp/blob/HEAD/crates/devup-mcp/src/server/skills/devup", diff --git a/crates/devup-mcp/src/server/skills/tests.rs b/crates/devup-mcp/src/server/skills/tests.rs index fb4ab919..b01704e6 100644 --- a/crates/devup-mcp/src/server/skills/tests.rs +++ b/crates/devup-mcp/src/server/skills/tests.rs @@ -5,6 +5,15 @@ mod tests { use super::*; use sha2::{Digest, Sha256}; + #[test] + fn verification_starts_with_lint_fix_then_oxlint() { + for name in ["devup", "devup-implementation-plan", "devfive-frontend"] { + let text = EMBEDDED.iter().find(|(key, _)| *key == name).unwrap().1[0].1; + assert!(text.contains("bun run lint:fix` first, then `bunx oxlint --deny-warnings`"), "{name}"); + assert!(text.contains("Fix only non-fixable leftovers by hand"), "{name}"); + } + } + #[test] fn user_decisions_override_never_wait_without_prose_questions() { let text = include_str!("devup/SKILL.md"); From 543f25115c644d75f33d7da1a4442749057ec5f4 Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 19:31:38 +0900 Subject: [PATCH 077/149] fix(project-plan): group domain workers and shared parts (40) --- .../devup-mcp/src/server/project_plan/mod.rs | 2 +- .../src/server/project_plan/tests.rs | 8 +- .../src/server/project_plan/tests/feedback.rs | 40 ++++++++- .../src/server/project_plan/units.rs | 84 +++++++++++++++---- .../src/server/text/devup-project-plan.md | 2 +- 5 files changed, 111 insertions(+), 25 deletions(-) diff --git a/crates/devup-mcp/src/server/project_plan/mod.rs b/crates/devup-mcp/src/server/project_plan/mod.rs index 1f06a332..db0fd2f2 100644 --- a/crates/devup-mcp/src/server/project_plan/mod.rs +++ b/crates/devup-mcp/src/server/project_plan/mod.rs @@ -85,7 +85,7 @@ pub struct Feature { pub screens: Vec, #[serde(default)] pub acceptance: Vec, - /// Tables this feature reads or writes; split makes one model unit per table. + /// Tables this feature reads or writes; shared tables group domain workers. #[serde(default)] pub entities: Vec, } diff --git a/crates/devup-mcp/src/server/project_plan/tests.rs b/crates/devup-mcp/src/server/project_plan/tests.rs index 2eeb52b2..851a8a36 100644 --- a/crates/devup-mcp/src/server/project_plan/tests.rs +++ b/crates/devup-mcp/src/server/project_plan/tests.rs @@ -219,14 +219,14 @@ async fn feature_split_and_decisions_and_state_gates() { i.action = "split".into(); i.answers.clear(); let r = run(&i, &policy).await.unwrap(); - assert_eq!(array(&r["plan"]["units"]).len(), 6); + assert_eq!(array(&r["plan"]["units"]).len(), 4); assert_eq!( - r["plan"]["units"][4]["dependencies"], - json!(["posts-client"]) + r["plan"]["units"][2]["dependencies"], + json!(["model-post"]) ); i.action = "status".into(); i.unit_updates.push(UnitUpdate { - id: "posts-api".into(), + id: "posts-domain".into(), state: "running".into(), verification: vec![], ..Default::default() diff --git a/crates/devup-mcp/src/server/project_plan/tests/feedback.rs b/crates/devup-mcp/src/server/project_plan/tests/feedback.rs index 6325c5e8..aec3ce51 100644 --- a/crates/devup-mcp/src/server/project_plan/tests/feedback.rs +++ b/crates/devup-mcp/src/server/project_plan/tests/feedback.rs @@ -1,5 +1,39 @@ use super::*; +#[tokio::test] +async fn split_groups_menu_and_entity_domains_and_supervises_repeated_parts() { + let dir = TestDir::new(); + let (request, policy, _) = drafted(dir.path(), true).await; + run(&request, &policy).await.unwrap(); + let mut plan = read(&request.project_root).unwrap(); + plan["features"] = json!([ + {"id":"news","scope":"News list","screens":["1:1"],"entities":["post"],"acceptance":["List news"]}, + {"id":"editor","scope":"Editor","screens":["1:2"],"entities":["post"],"acceptance":["Edit news"]}, + {"id":"history","scope":"History","screens":["2:1"],"acceptance":[]}, + {"id":"team","scope":"Team","screens":["2:2"],"acceptance":[]} + ]); + plan["facts"]["screens"] = json!([ + {"nodeId":"1:1","name":"News list","section":"news","parts":["PageHero","SubNav","SubNav"]}, + {"nodeId":"1:2","name":"Editor","section":"admin","parts":["PageHero"]}, + {"nodeId":"2:1","name":"History","menu":"company","parts":["SubNav"]}, + {"nodeId":"2:2","name":"Team","menu":"company","parts":["UniqueBlock"]} + ]); + let mut tx = OutputTransaction::new(); + units::split(&request, &mut plan, &mut tx, &policy).unwrap(); + tx.commit().unwrap(); + let workers = array(&plan["units"]).iter().filter(|u| u["stage"] == 1).collect::>(); + assert_eq!(workers.len(), 2); + assert_eq!(workers[0]["features"], json!(["news","editor"])); + assert_eq!(workers[1]["features"], json!(["history","team"])); + let shared = &plan["units"][0]; + assert_eq!(shared["sharedParts"], json!(["PageHero","SubNav"])); + assert_eq!(shared["stage"], 0); + assert_eq!(shared["owner"], "supervisor"); + assert!(std::fs::read_to_string(dir.path().join(".devup/units/shared.md")).unwrap().contains("PageHero")); + let start = ProjectPlanInput {unit_updates:vec![UnitUpdate {id:text(&workers[0]["id"]).into(),state:"running".into(),..Default::default()}],..request}; + assert!(units::update(&start, &mut plan).is_err()); +} + #[test] fn backend_checks_resolve_workspace_aliases_and_derive_features() { let dir = TestDir::new(); @@ -241,9 +275,9 @@ async fn split_uses_one_shared_table_owner_and_skips_static_backend_layers() { let units = array(&result["plan"]["units"]); assert_eq!(units.iter().filter(|unit| unit["id"] == "model-post").count(), 1); assert!(!units.iter().any(|unit| ["layout-api","layout-client","layout-models"].contains(&text(&unit["id"])))); - for id in ["news-api", "admin-news-api"] { - assert_eq!(units.iter().find(|unit| unit["id"] == id).unwrap()["dependencies"], json!(["model-post"])); - } + let news = units.iter().find(|unit| unit["id"] == "news-domain").unwrap(); + assert_eq!(news["dependencies"], json!(["model-post"])); + assert_eq!(news["features"], json!(["news", "admin-news"])); } #[test] diff --git a/crates/devup-mcp/src/server/project_plan/units.rs b/crates/devup-mcp/src/server/project_plan/units.rs index c9eb6a9f..d60659ca 100644 --- a/crates/devup-mcp/src/server/project_plan/units.rs +++ b/crates/devup-mcp/src/server/project_plan/units.rs @@ -84,6 +84,56 @@ fn slim_screens(screen: &Value, screens: &[Value]) -> Value { .collect::>(); json!({"fullPlan": ".devup/screen-plan.json", "state": screen["state"], "routes": routes}) } +fn feature_screens(plan: &Value, feature: &Value) -> Vec { + array(&plan["facts"]["screens"]).iter() + .chain(array(&plan["screenPlan"]["screens"])) + .filter(|screen| ["nodeId", "name", "url"].iter().any(|key| { + !screen[*key].is_null() && array(&feature["screens"]).contains(&screen[*key]) + })).cloned().collect() +} +fn domain_keys(plan: &Value, feature: &Value) -> Vec { + let mut keys = feature_entities(plan, feature).iter().map(|name| format!("entity:{name}")).collect::>(); + for screen in feature_screens(plan, feature) { + for key in ["section", "menu"] { + if let Some(value) = screen[key].as_str().filter(|value| !value.is_empty()) { + keys.push(format!("{key}:{value}")); + } + } + } + keys +} +fn feature_groups(plan: &Value) -> Vec> { + let mut groups: Vec> = vec![]; + for feature in array(&plan["features"]) { + let mut group = vec![feature.clone()]; + let mut index = 0; + while index < groups.len() { + if groups[index].iter().any(|other| group.iter().any(|member| { + let keys = domain_keys(plan, member); + domain_keys(plan, other).iter().any(|key| keys.contains(key)) + })) { + let mut previous = groups.remove(index); + previous.extend(group); + group = previous; + index = 0; + } else { + index += 1; + } + } + groups.push(group); + } + groups +} +fn shared_parts(plan: &Value) -> Vec { + let mut parts: BTreeMap = BTreeMap::new(); + for screen in array(&plan["facts"]["screens"]).iter().chain(array(&plan["screenPlan"]["screens"])) { + let names = array(&screen["parts"]).iter().filter_map(Value::as_str).collect::>(); + for name in names { + *parts.entry(name.to_owned()).or_default() += 1; + } + } + parts.into_iter().filter_map(|(name, count)| (count > 1).then_some(name)).collect() +} fn safe_id(id: &str) -> bool { !id.is_empty() && id.len() <= 80 @@ -149,10 +199,12 @@ pub(super) fn split( json!(["Shared foundations verified"]), "shared", )); + units[0]["sharedParts"] = json!(shared_parts(plan)); let mut model_units = std::collections::BTreeSet::new(); - for feature in array(&plan["features"]) { - let id = text(&feature["id"]); - let used = feature_entities(plan, feature); + for features in feature_groups(plan) { + let id = text(&features[0]["id"]); + let used = features.iter().flat_map(|feature| feature_entities(plan, feature)) + .collect::>().into_iter().collect::>(); let mut previous = vec!["shared".to_owned()]; if !used.is_empty() { for name in &used { @@ -171,24 +223,21 @@ pub(super) fn split( } previous = used.iter().map(|n| format!("model-{n}")).collect(); } - let layers = if used.is_empty() { - vec!["front"] - } else { - vec!["api", "client", "front"] - }; - for layer in layers { + let layer = if used.is_empty() { "front" } else { "domain" }; + { let uid = format!("{id}-{layer}"); let mut u = unit( uid.clone(), - format!("{}: {layer}", text(&feature["scope"])), + features.iter().map(|feature| text(&feature["scope"])).collect::>().join("; "), previous, - feature["acceptance"].clone(), + json!(features.iter().flat_map(|feature| array(&feature["acceptance"]).to_vec()).collect::>()), layer, ); u["feature"] = json!(id); + u["features"] = json!(features.iter().map(|feature| feature["id"].clone()).collect::>()); + u["screens"] = json!(features.iter().flat_map(|feature| array(&feature["screens"]).to_vec()).collect::>()); u["entities"] = json!(used); units.push(u); - previous = vec![uid]; } } } @@ -213,12 +262,12 @@ pub(super) fn split( let owner = unit_owner(u); u["scopeOverrides"] = pick(&plan["answers"], &owner, &[]); u["scopePrecedence"] = json!(["answers", "decisions", "scope"]); - let screens = array(&plan["features"]).iter().find(|feature| feature["id"] == owner) - .map(|feature| array(&feature["screens"])).unwrap_or_default(); + let screens = array(&u["screens"]); let screen_slice = slim_screens(&screen_context, screens); - let mut contract = json!({"routes":screen_slice["routes"],"protectedFiles":plan["facts"]["protectedFiles"]}); + let mut contract = json!({"routes":screen_slice["routes"],"protectedFiles":plan["facts"]["protectedFiles"],"sharedParts":units_shared_parts(plan, u)}); for field in ["exports", "assets", "imports"] { - contract[field] = plan["facts"][format!("{owner}.{field}")].clone(); + let values = array(&u["features"]).iter().flat_map(|feature| array(&plan["facts"][format!("{}.{field}", text(feature))]).to_vec()).collect::>(); + contract[field] = if values.is_empty() {plan["facts"][format!("{owner}.{field}")].clone()} else {json!(values)}; } u["contract"] = contract; let brief = format!( @@ -243,6 +292,9 @@ pub(super) fn split( plan["units"] = json!(units); Ok(()) } +fn units_shared_parts(plan: &Value, unit: &Value) -> Value { + if unit["layer"] == "shared" {unit["sharedParts"].clone()} else {json!(shared_parts(plan))} +} pub(super) fn open_requests(plan: &Value) -> Vec { array(&plan["sharedChangeRequests"]) .iter() diff --git a/crates/devup-mcp/src/server/text/devup-project-plan.md b/crates/devup-mcp/src/server/text/devup-project-plan.md index d022ce34..17c2ff32 100644 --- a/crates/devup-mcp/src/server/text/devup-project-plan.md +++ b/crates/devup-mcp/src/server/text/devup-project-plan.md @@ -1 +1 @@ -Project intake and supervisor: intake | draft | confirm | status | assume | defer | split. Read Jira/epic children, Figma and devup_project_context all scopes first; pass source-labelled evidence. intake asks work type, optional sources, goal and parts through the runtime question tool. draft records feature-specific rounds and ERD, merges shared question topics, and reports added/changed/kept/removed records in changeSummary. Shared site facts are forwarded to open screen-plan questions; explicit answers take precedence. confirm requires all answers and the Figma screen plan when applicable, writes vespertide models/*.json transactionally, and returns executable recovery for missing answers. split creates a dependency DAG and .devup/units/.md briefs: shared/table foundations, feature workers, then a supervisor integration unit. Static features have no backend layers; each table has one model owner. Briefs prioritize answers and decisions over original scope, filter routes by feature screens (URL or nodeId), and carry facts keyed .exports, .assets and .imports plus protectedFiles when supplied. Unknown file mappings remain null, never invented. Workers escalate to supervisor by default (unitQuestions: supervisor|worker). After confirm never wait: defer recommendations and continue, assume at the end. status reports ERD Mermaid, unit DAG/states, decisions, shared-change requests and backend prerequisites. Data features with explicit entities need apiWiring operationId bindings; status checks them against actual HTTP operations in openapi.json and reports missing or deferred bindings as blockers. These checks do not prove runtime integration. unitUpdates record supervisor verification. State: .devup/project-plan.json. Follow /devup skill for backend commands, latest-first dependencies, exact layout/visual audits, orchestration and zero-drift completion. +Project intake and supervisor: intake | draft | confirm | status | assume | defer | split. Read Jira/epic children, Figma and devup_project_context all scopes first; pass source-labelled evidence. intake asks work type, optional sources, goal and parts through the runtime question tool. draft records feature-specific rounds and ERD, merges shared question topics, and reports added/changed/kept/removed records in changeSummary. Shared site facts are forwarded to open screen-plan questions; explicit answers take precedence. confirm requires all answers and the Figma screen plan when applicable, writes vespertide models/*.json transactionally, and returns executable recovery for missing answers. split creates a dependency DAG and .devup/units/.md briefs: supervisor-owned shared/table foundations, parallel domain workers, then supervisor integration. Features sharing screen section/menu or entities form one worker owning API, client and pages together; static domains only own front work. Repeated screen parts become sharedParts in stage 0. Each table has one model owner. Briefs prioritize answers and decisions over original scope, filter routes by owned screens (URL or nodeId), and carry facts keyed .exports, .assets and .imports plus protectedFiles when supplied. Unknown file mappings remain null, never invented. Workers escalate to supervisor by default (unitQuestions: supervisor|worker). After confirm never wait: defer recommendations and continue, assume at the end. status reports ERD Mermaid, unit DAG/states, decisions, shared-change requests and backend prerequisites. Data features with explicit entities need apiWiring operationId bindings; status checks them against actual HTTP operations in openapi.json and reports missing or deferred bindings as blockers. These checks do not prove runtime integration. unitUpdates record supervisor verification. State: .devup/project-plan.json. Follow /devup skill for backend commands, latest-first dependencies, exact layout/visual audits, orchestration and zero-drift completion. From 213d159b1cb77667861c375be9e81bc8529f7afd Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 19:35:21 +0900 Subject: [PATCH 078/149] fix(validation): group prop sorting into one lint fix action (45) --- crates/devup-mcp-devup-ui/src/ui_structure.rs | 4 +++ .../src/ui_validate/tests.rs | 24 ++++++++++++++ .../src/ui_validate/types_and_helpers.rs | 32 +++++++++++++++++++ .../src/server/export_skill_gap_tests.rs | 27 ++++++++++++++++ .../src/server/handlers/devup_ui_validate.rs | 3 ++ .../mod_parts/exploration_and_responses.rs | 2 +- .../src/server/validation_guidance.rs | 13 ++++++++ .../fixtures_and_cases.rs | 13 +++++++- reports/feedback-45-prop-order.md | 20 ++++++++++++ 9 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 reports/feedback-45-prop-order.md diff --git a/crates/devup-mcp-devup-ui/src/ui_structure.rs b/crates/devup-mcp-devup-ui/src/ui_structure.rs index 22aae20c..ca5f2c66 100644 --- a/crates/devup-mcp-devup-ui/src/ui_structure.rs +++ b/crates/devup-mcp-devup-ui/src/ui_structure.rs @@ -72,14 +72,17 @@ pub fn validate_bundle( facts.boundary }); let mut result = UiValidation { + next_action: None, ok: true, violations: vec![], theme_notes: vec![], checked_tokens: 0, available_token_count: theme.map_or(0, ProjectTheme::token_count), }; + let mut fixable = 0; for file in files.iter().filter(|f| is_source(&f.path)) { let mut report = validate_source(&file.content, theme, strict, source_type(&file.path)); + fixable += report.next_action.as_ref().and_then(|action| action["fixable"].as_u64()).unwrap_or(0) as usize; if !report.violations.iter().any(|v| v.rule == "invalid-syntax") { report.violations.extend(structural(file, boundary)); report.violations.extend(super::wiring::fixture_findings(files, &file.path, &file.content)); @@ -100,6 +103,7 @@ pub fn validate_bundle( result.violations.extend(report.violations); } result.theme_notes.sort(); + result.next_action = super::lint_fix_action(fixable); result.theme_notes.dedup(); result.ok = result .violations diff --git a/crates/devup-mcp-devup-ui/src/ui_validate/tests.rs b/crates/devup-mcp-devup-ui/src/ui_validate/tests.rs index 6e621e20..0ebb587d 100644 --- a/crates/devup-mcp-devup-ui/src/ui_validate/tests.rs +++ b/crates/devup-mcp-devup-ui/src/ui_validate/tests.rs @@ -5,6 +5,30 @@ mod tests { use super::*; use crate::theme::parse_project_theme; + #[test] + fn prop_sort_findings_are_one_lint_fix_action() { + let report = serde_json::to_value(validate_devup_ui_tsx( + r#"<>"#, + None, + false, + )).unwrap(); + assert_eq!(report["nextAction"]["command"], "bun run lint:fix"); + assert_eq!(report["nextAction"]["fixable"], 2); + assert_eq!(report["nextAction"]["label"], "bun run lint:fix (fixable 2)"); + } + + #[test] + fn prop_sort_respects_reserved_props_and_spread_boundaries() { + for tsx in [ + r#""#, + r#""#, + r#""#, + ] { + let report = serde_json::to_value(validate_devup_ui_tsx(tsx, None, false)).unwrap(); + assert!(report["nextAction"].is_null(), "{tsx}: {report}"); + } + } + fn fixture_theme() -> ProjectTheme { parse_project_theme( r##"{ "theme": { diff --git a/crates/devup-mcp-devup-ui/src/ui_validate/types_and_helpers.rs b/crates/devup-mcp-devup-ui/src/ui_validate/types_and_helpers.rs index d6602c59..38c6a6f0 100644 --- a/crates/devup-mcp-devup-ui/src/ui_validate/types_and_helpers.rs +++ b/crates/devup-mcp-devup-ui/src/ui_validate/types_and_helpers.rs @@ -59,6 +59,8 @@ pub struct Violation { #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct UiValidation { + #[serde(skip_serializing_if = "Option::is_none")] + pub next_action: Option, /// Whether this TSX may be used as it stands. /// /// `false` when any violation is [`Severity::Error`] — a syntax @@ -141,6 +143,7 @@ fn validate_source( let available_token_count = theme.map(ProjectTheme::token_count).unwrap_or(0); let mut visitor = TsxVisitor { + fixable: 0, theme, checked_tokens: 0, violations: Vec::new(), @@ -237,6 +240,7 @@ fn validate_source( .unwrap_or_default(); UiValidation { + next_action: lint_fix_action(visitor.fixable), ok, violations, theme_notes, @@ -246,6 +250,7 @@ fn validate_source( } struct TsxVisitor<'t> { + fixable: usize, theme: Option<&'t ProjectTheme>, checked_tokens: usize, violations: Vec, @@ -521,6 +526,23 @@ impl<'t> TsxVisitor<'t> { impl<'a, 't> Visit<'a> for TsxVisitor<'t> { fn visit_jsx_opening_element(&mut self, element: &JSXOpeningElement<'a>) { + let mut previous = None; + for attribute in &element.attributes { + let JSXAttributeItem::Attribute(attribute) = attribute else { + previous = None; + continue; + }; + let JSXAttributeName::Identifier(name) = &attribute.name else { + previous = None; + continue; + }; + let name = name.name.as_str(); + let current = (!matches!(name, "key" | "ref"), name); + if previous.is_some_and(|previous| previous > current) { + self.fixable += 1; + } + previous = Some(current); + } let tag_name = jsx_element_name(&element.name); // Resolve before visiting attributes so maxLength before as works too. // A later spread/dynamic as can override a literal; do not assume its tag. @@ -697,3 +719,13 @@ pub fn rule_names() -> BTreeSet<&'static str> { .into_iter() .collect() } + +pub fn lint_fix_action(fixable: usize) -> Option { + (fixable > 0).then(|| serde_json::json!({ + "command": "bun run lint:fix", + "fixable": fixable, + "label": format!("bun run lint:fix (fixable {fixable})"), + "then": "bunx oxlint --deny-warnings", + "reason": "Detected adjacent JSX prop-order inversions under eslint-plugin-devup defaults. Run the project's fixer, never sort or format by hand. Custom lint configuration and other formatting rules are not checked here; fix only non-fixable leftovers after oxlint." + })) +} diff --git a/crates/devup-mcp/src/server/export_skill_gap_tests.rs b/crates/devup-mcp/src/server/export_skill_gap_tests.rs index 14472072..ce992442 100644 --- a/crates/devup-mcp/src/server/export_skill_gap_tests.rs +++ b/crates/devup-mcp/src/server/export_skill_gap_tests.rs @@ -9,6 +9,33 @@ //! `std::fs::` in it at all. use super::*; + +#[test] +fn export_groups_prop_sorting_without_a_project_root() { + let project = scratch("lint-fix"); + let checked = with_project_checks( + json!({"tsx": ""}), + None, + &skills::Lookup::project_only(project.clone(), skills::Runtime::Codex), + ); + assert_eq!(checked["formatting"]["nextAction"]["fixable"], 1); + assert_eq!(checked["nextAction"]["command"], "bun run lint:fix"); + std::fs::remove_dir_all(project).unwrap(); +} + +#[test] +fn export_preserves_an_existing_tool_action_when_grouping_sorting() { + let project = scratch("lint-fix-action"); + let action = json!({"tool": "devup_figma_export", "arguments": {"jobId": "pending"}}); + let checked = with_project_checks( + json!({"frames": [{"nodeId": "1:2", "tsx": ""}], "nextAction": action}), + None, + &skills::Lookup::project_only(project.clone(), skills::Runtime::Codex), + ); + assert_eq!(checked["formatting"]["nextAction"]["fixable"], 1); + assert_eq!(checked["nextAction"], action); + std::fs::remove_dir_all(project).unwrap(); +} fn scratch(label: &str) -> std::path::PathBuf { let path = std::env::temp_dir().join(format!( "devup-export-gap-{label}-{}-{}", diff --git a/crates/devup-mcp/src/server/handlers/devup_ui_validate.rs b/crates/devup-mcp/src/server/handlers/devup_ui_validate.rs index e962bb9d..b796a52a 100644 --- a/crates/devup-mcp/src/server/handlers/devup_ui_validate.rs +++ b/crates/devup-mcp/src/server/handlers/devup_ui_validate.rs @@ -200,6 +200,9 @@ impl DevupServer { .clone(), ); } + if let Some(action) = &report.next_action { + result["nextAction"] = action.clone(); + } // A violation is a located, proven gap in devup-ui knowledge, which // makes this the one moment where naming the skill is a measurement // rather than a nudge. Raised only when the skill is actually absent: diff --git a/crates/devup-mcp/src/server/mod_parts/exploration_and_responses.rs b/crates/devup-mcp/src/server/mod_parts/exploration_and_responses.rs index 706ad08a..cf608bf1 100644 --- a/crates/devup-mcp/src/server/mod_parts/exploration_and_responses.rs +++ b/crates/devup-mcp/src/server/mod_parts/exploration_and_responses.rs @@ -439,6 +439,7 @@ fn with_project_checks( if produced_code { result = with_skill_gap(result, lookup); } + validation_guidance::attach_export_formatting(&mut result, &generated); let Some(project_root) = project_root else { return result; }; @@ -525,7 +526,6 @@ fn with_project_checks( } result } - fn theme_sync_action(result: &Value, project_root: &str) -> Value { let mut arguments = json!({ "outputs": ["devupJson"], diff --git a/crates/devup-mcp/src/server/validation_guidance.rs b/crates/devup-mcp/src/server/validation_guidance.rs index 390bbb98..16c3bbad 100644 --- a/crates/devup-mcp/src/server/validation_guidance.rs +++ b/crates/devup-mcp/src/server/validation_guidance.rs @@ -8,6 +8,19 @@ use serde_json::{Value, json}; use super::UiValidateInput; +pub(super) fn attach_export_formatting(result: &mut Value, generated: &[(String, String)]) { + let fixable = generated.iter().map(|(_, tsx)| { + validate_devup_ui_tsx(tsx, None, false) + .next_action.and_then(|action| action["fixable"].as_u64()).unwrap_or(0) as usize + }).sum(); + if let Some(action) = devup_mcp_devup_ui::ui_validate::lint_fix_action(fixable) { + result["formatting"] = json!({"nextAction": action}); + if result.get("nextAction").is_none_or(Value::is_null) { + result["nextAction"] = action; + } + } +} + pub(super) fn guidance( input: &UiValidateInput, report: &UiValidation, diff --git a/crates/devup-mcp/tests/ui_validate_response/fixtures_and_cases.rs b/crates/devup-mcp/tests/ui_validate_response/fixtures_and_cases.rs index a8002adf..76f599f8 100644 --- a/crates/devup-mcp/tests/ui_validate_response/fixtures_and_cases.rs +++ b/crates/devup-mcp/tests/ui_validate_response/fixtures_and_cases.rs @@ -69,6 +69,17 @@ async fn validate(arguments: Value) -> anyhow::Result { Ok(result.structured_content.unwrap()) } +#[tokio::test] +async fn sort_findings_produce_one_action_across_bundle_files() -> anyhow::Result<()> { + let output = validate(json!({"files": [ + {"path": "one.tsx", "content": ""}, + {"path": "two.tsx", "content": ""} + ]})).await?; + assert_eq!(output["nextAction"]["fixable"], 2); + assert_eq!(output["nextAction"]["command"], "bun run lint:fix"); + Ok(()) +} + #[tokio::test] async fn r12_failed_validation_explains_why_source_cannot_be_corrected_automatically() -> anyhow::Result<()> { @@ -985,4 +996,4 @@ async fn an_ordinary_module_with_two_components_is_still_flagged() -> anyhow::Re .await?; assert!(has_rule(&output, "one-component-per-file"), "{output}"); Ok(()) -} \ No newline at end of file +} diff --git a/reports/feedback-45-prop-order.md b/reports/feedback-45-prop-order.md new file mode 100644 index 00000000..3eaec4bd --- /dev/null +++ b/reports/feedback-45-prop-order.md @@ -0,0 +1,20 @@ +# Prop Ordering (Feedback 45) + +The published eslint-plugin-devup 2.1.0 oxlint config uses +perfectionist/sort-jsx-props with case-sensitive alphabetical ascending ordering, +reserved key/ref first, then unknown props. Generated TSX is not consistently +sorted: render stages append layout, paint, provenance and interaction props. + +A global source rewrite is not cheap or safe: spreads and duplicate attributes +are evaluation/override boundaries, and project configs can override the rule. +Generation is left unchanged; no snapshots need refreshing. The validator counts +adjacent inversions within identifier-only runs separated by spreads/namespaced +attributes, and offers one lint:fix action. Counts describe detected inversions, +not every diagnostic oxlint would emit. It does not claim to validate prettier, +import order or custom perfectionist configuration. The project fixer remains +authoritative for all formatting and sorting. + +Export exposes this action before its optional project-theme gate and keeps an +existing tool nextAction (for example a job continuation) intact. Formatting has +its own single nextAction in that case. Validation aggregates across bundle +files, without individual prop-reordering instructions. From 4ee28fd05eeb4de6f530101362da278abfeb8216 Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 19:35:55 +0900 Subject: [PATCH 079/149] fix(skills): label lint fix formatting obligations explicitly (45) --- crates/devup-mcp/src/server/skills/installation.rs | 7 ++++++- crates/devup-mcp/src/server/skills/tests.rs | 12 ++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/crates/devup-mcp/src/server/skills/installation.rs b/crates/devup-mcp/src/server/skills/installation.rs index 1f2f8681..846a51c8 100644 --- a/crates/devup-mcp/src/server/skills/installation.rs +++ b/crates/devup-mcp/src/server/skills/installation.rs @@ -622,10 +622,15 @@ fn lint_fix_obligation(project: &Path) -> Option { let text = std::fs::read_to_string(project.join("package.json")).ok()?; let package: serde_json::Value = serde_json::from_str(&text).ok()?; let script = package["scripts"]["lint:fix"].as_str()?; + if script.trim().is_empty() { + return None; + } Some(serde_json::json!({ + "label": "format/sort via lint:fix", "detected": "package.json scripts.lint:fix", "script": script, "runFirst": "bun run lint:fix", + "verifyAfter": "bunx oxlint --deny-warnings", "obligation": "Run `bun run lint:fix` before any other verification. Never reorder JSX props, imports or formatting by hand and never run bare prettier: the project's lint rules own them. Fix by hand only what lint:fix cannot (type and logic errors).", })) } @@ -974,4 +979,4 @@ async fn stage_documents( #[cfg(test)] #[path = "fetch_tests.rs"] -mod fetch_tests; \ No newline at end of file +mod fetch_tests; diff --git a/crates/devup-mcp/src/server/skills/tests.rs b/crates/devup-mcp/src/server/skills/tests.rs index b01704e6..b2a669bf 100644 --- a/crates/devup-mcp/src/server/skills/tests.rs +++ b/crates/devup-mcp/src/server/skills/tests.rs @@ -826,7 +826,19 @@ mod tests { let found = report(&unknown_client(&project))["repoObligations"]["lintFix"].clone(); assert_eq!(found["runFirst"], "bun run lint:fix"); assert_eq!(found["script"], "oxlint --fix"); + assert_eq!(found["label"], "format/sort via lint:fix"); + assert_eq!(found["verifyAfter"], "bunx oxlint --deny-warnings"); let _ = std::fs::remove_dir_all(&project); } + + #[test] + fn lint_fix_obligation_requires_an_executable_script() { + let project = scratch("lint-fix-invalid"); + for package in ["not json", r#"{"scripts":{"lint:fix":false}}"#, r#"{"scripts":{"lint:fix":" "}}"#] { + std::fs::write(project.join("package.json"), package).unwrap(); + assert!(report(&unknown_client(&project))["repoObligations"]["lintFix"].is_null()); + } + std::fs::remove_dir_all(project).unwrap(); + } } From 5fe355e236a89d2da681a1e03deb660e16a4f1c3 Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 19:37:36 +0900 Subject: [PATCH 080/149] fix(skills): warn that oxlint owns template formatting (45) --- .../src/server/skills/devfive-frontend/SKILL.md | 5 +++++ .../server/skills/devup-implementation-plan/SKILL.md | 4 ++++ crates/devup-mcp/src/server/skills/devup/SKILL.md | 4 ++++ crates/devup-mcp/src/server/skills/manifest.json | 12 ++++++------ crates/devup-mcp/src/server/skills/tests.rs | 10 ++++++++++ 5 files changed, 29 insertions(+), 6 deletions(-) diff --git a/crates/devup-mcp/src/server/skills/devfive-frontend/SKILL.md b/crates/devup-mcp/src/server/skills/devfive-frontend/SKILL.md index 9e27e264..b34781ab 100644 --- a/crates/devup-mcp/src/server/skills/devfive-frontend/SKILL.md +++ b/crates/devup-mcp/src/server/skills/devfive-frontend/SKILL.md @@ -401,6 +401,11 @@ Run `bun run lint:fix` first, then `bunx oxlint --deny-warnings` at the root. Never fix JSX prop order, import sorting or formatting errors by hand. Fix only non-fixable leftovers by hand (type errors, logic and decisions the fixer cannot make). +**Formatter ownership:** this template has no standalone prettier config. +Formatting is owned by eslint-plugin-devup's `devup(prettier)` rule inside oxlint. +Do not run `bunx prettier --write` or biome: their defaults produce a different +style (semicolons and double quotes), undoing the project's lint formatting. + ```bash bun run lint:fix # FIRST: fixes JSX prop order, formatting, imports; never hand-edit those bunx oxlint --deny-warnings # warnings count: keep them at 0 diff --git a/crates/devup-mcp/src/server/skills/devup-implementation-plan/SKILL.md b/crates/devup-mcp/src/server/skills/devup-implementation-plan/SKILL.md index 31e24255..866bba4f 100644 --- a/crates/devup-mcp/src/server/skills/devup-implementation-plan/SKILL.md +++ b/crates/devup-mcp/src/server/skills/devup-implementation-plan/SKILL.md @@ -446,6 +446,10 @@ including a "view transitions: enabled (mechanism) / skipped (reason)" line. - run `bun run lint:fix` first, then `bunx oxlint --deny-warnings` at the root. Never fix JSX prop order, import sorting or formatting errors by hand. Fix only non-fixable leftovers by hand (type errors, logic and decisions the fixer cannot make). + **Formatter ownership:** this template has no standalone prettier config. + Formatting is owned by eslint-plugin-devup's `devup(prettier)` rule inside oxlint. + Do not run `bunx prettier --write` or biome: their defaults produce a different + style (semicolons and double quotes), undoing the project's lint formatting. - every confirmed URL is implemented; - the build passes: `bun run build` (`vinext build` for every app); - lint and typecheck pass: `bun run lint` (its prelint runs cargo clippy, fmt diff --git a/crates/devup-mcp/src/server/skills/devup/SKILL.md b/crates/devup-mcp/src/server/skills/devup/SKILL.md index fcc37f78..8b2ebf56 100644 --- a/crates/devup-mcp/src/server/skills/devup/SKILL.md +++ b/crates/devup-mcp/src/server/skills/devup/SKILL.md @@ -199,6 +199,10 @@ the full rules; these are the steps. nothing). Run `bun run lint:fix` first, then `bunx oxlint --deny-warnings` at the root. Never fix JSX prop order, import sorting or formatting errors by hand. Fix only non-fixable leftovers by hand (type errors, logic and decisions the fixer cannot make). +**Formatter ownership:** this template has no standalone prettier config. +Formatting is owned by eslint-plugin-devup's `devup(prettier)` rule inside oxlint. +Do not run `bunx prettier --write` or biome: their defaults produce a different +style (semicolons and double quotes), undoing the project's lint formatting. Then run `bun run build`, `bun run lint`, `bun tsc --noEmit` per changed app, `bun test` (on Windows `bun test` and `cargo test`), `devup_ui_validate` without errors, site completeness filled. diff --git a/crates/devup-mcp/src/server/skills/manifest.json b/crates/devup-mcp/src/server/skills/manifest.json index 51f1a550..b621ce78 100644 --- a/crates/devup-mcp/src/server/skills/manifest.json +++ b/crates/devup-mcp/src/server/skills/manifest.json @@ -33,8 +33,8 @@ "documents": [ { "path": "SKILL.md", - "bytes": 25852, - "sha256": "49a1323ed8d5882713d97a8654c8ae347abbed5c102cc46f186ddefae2ccf2b3" + "bytes": 26167, + "sha256": "f6c9feebb9c250549bd442455102b35f699fdc9518946ecd23d00d85e3ff26d3" }, { "path": "references/critical-rules.md", @@ -71,8 +71,8 @@ "documents": [ { "path": "SKILL.md", - "bytes": 39034, - "sha256": "f1f66659d16582b2338fd6c567f8873a81067fc1ca0bd620270d5fc894509bae" + "bytes": 39368, + "sha256": "13bdc8b1508af78f22f4db6b3e4b7a3a39fc637bd4c113432fb15ef6c3bd5ad6" } ], "sourceUrl": "https://github.com/dev-five-git/devup-mcp/blob/HEAD/crates/devup-mcp/src/server/skills/devup-implementation-plan", @@ -89,8 +89,8 @@ "documents": [ { "path": "SKILL.md", - "bytes": 19515, - "sha256": "864dfb98444f6ddb2bc13ce21ce0262a6f51b421d95013f1a8c2aa2478e3a0c1" + "bytes": 19829, + "sha256": "2cdf2561e35f735bfa7ee26e40a8a84e7560d61e48dcc1d4c0094ed6517e7734" } ], "sourceUrl": "https://github.com/dev-five-git/devup-mcp/blob/HEAD/crates/devup-mcp/src/server/skills/devup", diff --git a/crates/devup-mcp/src/server/skills/tests.rs b/crates/devup-mcp/src/server/skills/tests.rs index b2a669bf..c97d19a4 100644 --- a/crates/devup-mcp/src/server/skills/tests.rs +++ b/crates/devup-mcp/src/server/skills/tests.rs @@ -5,6 +5,16 @@ mod tests { use super::*; use sha2::{Digest, Sha256}; + #[test] + fn formatter_ownership_warning_is_carried_in_every_frontend_workflow() { + for name in ["devup", "devup-implementation-plan", "devfive-frontend"] { + let text = EMBEDDED.iter().find(|(key, _)| *key == name).unwrap().1[0].1; + for required in ["no standalone prettier config", "devup(prettier)", "Do not run `bunx prettier --write` or biome", "semicolons and double quotes"] { + assert!(text.contains(required), "{name}: {required}"); + } + } + } + #[test] fn verification_starts_with_lint_fix_then_oxlint() { for name in ["devup", "devup-implementation-plan", "devfive-frontend"] { From f24686f236081d90950dedcb7d787b703172c916 Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 19:54:49 +0900 Subject: [PATCH 081/149] fix(project-plan): derive endpoint contract drafts from screens (41) --- .../devup-mcp/src/server/project_plan/mod.rs | 4 + .../src/server/project_plan/tests/feedback.rs | 42 +++++++++ .../src/server/project_plan/units.rs | 14 +-- .../src/server/project_plan/wiring.rs | 85 ++++++++++++++++++- .../src/server/text/devup-project-plan.md | 2 +- 5 files changed, 138 insertions(+), 9 deletions(-) diff --git a/crates/devup-mcp/src/server/project_plan/mod.rs b/crates/devup-mcp/src/server/project_plan/mod.rs index db0fd2f2..9fa80cff 100644 --- a/crates/devup-mcp/src/server/project_plan/mod.rs +++ b/crates/devup-mcp/src/server/project_plan/mod.rs @@ -382,6 +382,8 @@ fn extras(plan: &Value, root: &str) -> Value { "sharedChangeRequests": units::open_requests(plan), "apiWiring": wiring::report(plan, root), "backendChecks": wiring::backend_checks(plan, root), + "dataMappings": plan["dataMappings"], + "endpointContracts": plan["endpointContracts"], }); if erd::api(plan) && array(&plan["entities"]).is_empty() { extra["erdDraftRequired"] = json!({ @@ -564,6 +566,7 @@ pub(super) async fn run( plan["unitQuestions"] = json!(mode); } erd::draft(input, &mut plan)?; + wiring::draft(&mut plan); plan["questions"] = json!(rounds::detail(&plan, &input.extra_questions)?); let new_answers = answers.into_iter() .filter(|(id, _)| !previous_answers.contains_key(id)) @@ -620,6 +623,7 @@ pub(super) async fn run( screen_plan_hint(&plan, &input.project_root) ))); } + wiring::draft(&mut plan); erd::write_models(&mut tx, policy, &input.project_root, &plan)?; plan["writtenTo"] = json!(if erd::api(&plan) { array(&plan["entities"]).iter() diff --git a/crates/devup-mcp/src/server/project_plan/tests/feedback.rs b/crates/devup-mcp/src/server/project_plan/tests/feedback.rs index aec3ce51..ce20163d 100644 --- a/crates/devup-mcp/src/server/project_plan/tests/feedback.rs +++ b/crates/devup-mcp/src/server/project_plan/tests/feedback.rs @@ -1,5 +1,47 @@ use super::*; +#[tokio::test] +async fn screen_data_contracts_survive_confirm_split_and_check_openapi_fields() { + let dir = TestDir::new(); + let (mut request, policy, _) = drafted(dir.path(), true).await; + request.action = "draft".into(); + request.answers.clear(); + request.features = vec![Feature {id:"catalog".into(),scope:"Catalog".into(),screens:vec!["9:1".into()],acceptance:vec![],entities:vec![]}]; + request.evidence = vec![serde_json::from_value(json!({"source":"Figma","facts":{"screens":[ + {"nodeId":"9:1","name":"Post management","parts":["PostTable","DetailPanel","CreateForm","EditForm"]} + ]}})).unwrap()]; + let draft = run(&request, &policy).await.unwrap(); + assert_eq!(draft["dataMappings"][0]["candidateEntities"], json!(["post"])); + assert_eq!(array(&draft["endpointContracts"]).len(), 4); + request.action = "confirm".into(); + request.accept_recommended = true; + request.evidence.clear(); + request.detail = None; + let confirmed = run(&request, &policy).await.unwrap(); + assert_eq!(confirmed["endpointContracts"], draft["endpointContracts"]); + let create = array(&confirmed["endpointContracts"]).iter().find(|contract| contract["operation"] == "create").unwrap(); + assert_eq!(create["method"], "POST"); + assert_eq!(create["path"], "/posts"); + assert!(!array(&create["requestFields"]).iter().any(|field| field["name"] == "id")); + assert!(array(&create["requestFields"]).iter().any(|field| field["name"] == "title" && field["type"] == "text" && field["required"] == true)); + std::fs::write(dir.path().join("openapi.json"), json!({"paths":{ + "/posts":{"get":{"operationId":"listPosts","responses":{"200":{"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Post"}}}}}}}, + "post":{"operationId":"createPost","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Post"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Post"}}}}}}}, + },"components":{"schemas":{"Post":{"type":"object","properties":{"id":{"type":"integer"},"title":{"type":"string"},"body":{"type":"string"},"user_id":{"type":"integer"}}}}} + }).to_string()).unwrap(); + request.action = "split".into(); + let split = run(&request, &policy).await.unwrap(); + assert_eq!(array(&split["apiWiring"]["contractMissing"]).len(), 2); + let brief = std::fs::read_to_string(dir.path().join(".devup/units/catalog-domain.md")).unwrap(); + assert!(brief.contains("requestFields") && brief.contains("/posts/{id}")); + let mut doc: Value = serde_json::from_slice(&std::fs::read(dir.path().join("openapi.json")).unwrap()).unwrap(); + doc["components"]["schemas"]["Post"]["properties"].as_object_mut().unwrap().remove("title"); + std::fs::write(dir.path().join("openapi.json"), doc.to_string()).unwrap(); + request.action = "status".into(); + let status = run(&request, &policy).await.unwrap(); + assert!(array(&status["apiWiring"]["contractMissing"]).iter().any(|missing| text(&missing["reason"]).contains("title"))); +} + #[tokio::test] async fn split_groups_menu_and_entity_domains_and_supervises_repeated_parts() { let dir = TestDir::new(); diff --git a/crates/devup-mcp/src/server/project_plan/units.rs b/crates/devup-mcp/src/server/project_plan/units.rs index d60659ca..8793360a 100644 --- a/crates/devup-mcp/src/server/project_plan/units.rs +++ b/crates/devup-mcp/src/server/project_plan/units.rs @@ -12,7 +12,7 @@ fn unit( "stage":if matches!(layer, "shared" | "models") {0} else {1}, "checklist":["build", "lint", "typecheck", "tests", "devup_ui_validate", "runtime/visual audit", "devup_stack_diff: zero drift"]}) } -fn feature_entities(plan: &Value, feature: &Value) -> Vec { +pub(super) fn feature_entities(plan: &Value, feature: &Value) -> Vec { if !erd::api(plan) { return vec![]; } @@ -24,10 +24,11 @@ fn feature_entities(plan: &Value, feature: &Value) -> Vec { return explicit; } let haystack = format!( - "{} {} {}", + "{} {} {} {}", text(&feature["id"]), text(&feature["scope"]), - feature["acceptance"] + feature["acceptance"], + json!(feature_screens(plan, feature)) ) .to_lowercase(); array(&plan["entities"]) @@ -84,7 +85,7 @@ fn slim_screens(screen: &Value, screens: &[Value]) -> Value { .collect::>(); json!({"fullPlan": ".devup/screen-plan.json", "state": screen["state"], "routes": routes}) } -fn feature_screens(plan: &Value, feature: &Value) -> Vec { +pub(super) fn feature_screens(plan: &Value, feature: &Value) -> Vec { array(&plan["facts"]["screens"]).iter() .chain(array(&plan["screenPlan"]["screens"])) .filter(|screen| ["nodeId", "name", "url"].iter().any(|key| { @@ -264,7 +265,10 @@ pub(super) fn split( u["scopePrecedence"] = json!(["answers", "decisions", "scope"]); let screens = array(&u["screens"]); let screen_slice = slim_screens(&screen_context, screens); - let mut contract = json!({"routes":screen_slice["routes"],"protectedFiles":plan["facts"]["protectedFiles"],"sharedParts":units_shared_parts(plan, u)}); + let endpoints = array(&plan["endpointContracts"]).iter().filter(|contract| { + u["stage"] != 1 || array(&u["features"]).contains(&contract["feature"]) + }).cloned().collect::>(); + let mut contract = json!({"routes":screen_slice["routes"],"protectedFiles":plan["facts"]["protectedFiles"],"sharedParts":units_shared_parts(plan, u),"endpointContracts":endpoints}); for field in ["exports", "assets", "imports"] { let values = array(&u["features"]).iter().flat_map(|feature| array(&plan["facts"][format!("{}.{field}", text(feature))]).to_vec()).collect::>(); contract[field] = if values.is_empty() {plan["facts"][format!("{owner}.{field}")].clone()} else {json!(values)}; diff --git a/crates/devup-mcp/src/server/project_plan/wiring.rs b/crates/devup-mcp/src/server/project_plan/wiring.rs index 0759a970..a2624a22 100644 --- a/crates/devup-mcp/src/server/project_plan/wiring.rs +++ b/crates/devup-mcp/src/server/project_plan/wiring.rs @@ -1,5 +1,82 @@ use super::*; +pub(super) fn draft(plan: &mut Value) { + if !erd::api(plan) {return;} + let mut mappings = vec![]; + let mut contracts = vec![]; + for feature in array(&plan["features"]) { + let screens = units::feature_screens(plan, feature); + let evidence = format!("{} {} {} {}", feature["scope"], feature["screens"], feature["acceptance"], json!(screens)).to_lowercase(); + let mut operations = vec![]; + for (operation, keywords) in [ + ("list", vec!["list", "table", "search", "filter", "pagination", "목록", "검색", "표"]), + ("get", vec!["detail", "상세"]), + ("create", vec!["form", "create", "signup", "input", "textarea", "폼", "등록", "문의"]), + ("update", vec!["edit", "update", "수정"]), + ] { + if keywords.iter().any(|word| evidence.contains(word)) { operations.push(operation); } + } + let entities = units::feature_entities(plan, feature); + if operations.is_empty() && entities.is_empty() { continue; } + if operations.is_empty() { operations.push("list"); } + mappings.push(json!({"feature":feature["id"],"screens":feature["screens"],"candidateEntities":entities,"requiredOperations":operations,"status":"draft"})); + for name in entities { + let Some(entity) = array(&plan["entities"]).iter().find(|entity| entity["name"] == name) else {continue}; + let fields = array(&entity["columns"]).iter().map(|column| json!({ + "name":column["name"],"type":column["type"],"nullable":column["nullable"],"required":column["nullable"] == false, + })).collect::>(); + let resource = if name.ends_with('s') {name.clone()} else {format!("{name}s")}; + for operation in &operations { + let method = match *operation {"create" => "POST", "update" => "PATCH", _ => "GET"}; + let path = if matches!(*operation, "get" | "update") {format!("/{resource}/{{id}}") } else {format!("/{resource}")}; + let request_fields = if matches!(*operation, "create" | "update") { + array(&entity["columns"]).iter().filter(|column| column["primary_key"].is_null() && column["default"].is_null()) + .map(|column| json!({"name":column["name"],"type":column["type"],"nullable":column["nullable"],"required":*operation == "create" && column["nullable"] == false})).collect::>() + } else {vec![]}; + contracts.push(json!({"feature":feature["id"],"entity":name,"operation":operation,"method":method,"path":path, + "requestFields":request_fields,"responseFields":fields,"status":"draft","pathEvidence":"entity-name convention; confirm API path before implementation"})); + } + } + } + plan["dataMappings"] = json!(mappings); + plan["endpointContracts"] = json!(contracts); +} + +fn schema_fields(doc: &Value, schema: &Value, depth: usize) -> std::collections::BTreeSet { + if depth > 16 {return Default::default();} + if let Some(reference) = schema["$ref"].as_str().and_then(|reference| reference.strip_prefix('#')) { + return schema_fields(doc, &doc.pointer(reference).cloned().unwrap_or(Value::Null), depth + 1); + } + if schema["type"] == "array" {return schema_fields(doc, &schema["items"], depth + 1);} + let mut fields = schema["properties"].as_object().into_iter().flatten().map(|(name, _)| name.clone()).collect::>(); + for part in array(&schema["allOf"]) {fields.extend(schema_fields(doc, part, depth + 1));} + fields +} + +fn contract_missing(plan: &Value, path: Option<&Path>) -> Vec { + let Some(path) = path else {return vec![]}; + let doc = std::fs::read(path).ok().and_then(|bytes| serde_json::from_slice::(&bytes).ok()).unwrap_or(Value::Null); + let mut missing = vec![]; + for contract in array(&plan["endpointContracts"]) { + let operation = &doc["paths"][text(&contract["path"])][text(&contract["method"]).to_lowercase()]; + let mut reasons = vec![]; + if !operation.is_object() { + reasons.push("HTTP method/path absent from openapi.json".to_owned()); + } else { + let request = schema_fields(&doc, &operation["requestBody"]["content"]["application/json"]["schema"], 0); + let response = operation["responses"].as_object().into_iter().flatten() + .filter(|(status, _)| status.starts_with('2')).flat_map(|(_, response)| schema_fields(&doc, &response["content"]["application/json"]["schema"], 0)).collect::>(); + for (key, fields) in [("requestFields", request), ("responseFields", response)] { + for field in array(&contract[key]) { + if !fields.contains(text(&field["name"])) {reasons.push(format!("{key} missing {}", text(&field["name"])));} + } + } + } + if !reasons.is_empty() {missing.push(json!({"feature":contract["feature"],"method":contract["method"],"path":contract["path"],"reason":reasons.join("; ")}));} + } + missing +} + fn openapi_path(root: &str) -> Option { let base = Path::new(root); let mut candidates = vec![base.join("openapi.json")]; @@ -58,7 +135,7 @@ pub(super) fn report(plan: &Value, root: &str) -> Value { let path = openapi_path(root); let ids = path.as_deref().map(operation_ids).unwrap_or_default(); let (mut wired, mut missing, mut deferred) = (vec![], vec![], vec![]); - for feature in array(&plan["features"]).iter().filter(|feature| !array(&feature["entities"]).is_empty()) { + for feature in array(&plan["features"]).iter().filter(|feature| !array(&feature["entities"]).is_empty() || !units::feature_entities(plan, feature).is_empty() || array(&plan["dataMappings"]).iter().any(|mapping| mapping["feature"] == feature["id"])) { if !array(&plan["apiWiring"]).iter().any(|w| w["feature"] == feature["id"]) { missing.push(json!({"feature":feature["id"],"operationId":null,"reason":"Data feature has no operationId binding"})); } @@ -77,7 +154,8 @@ pub(super) fn report(plan: &Value, root: &str) -> Value { missing.push(json!({"feature":w["feature"],"operationId":w["operationId"],"reason":why})); } } - json!({"openapi":path.map(|p| p.display().to_string()),"wired":wired,"missing":missing,"deferred":deferred}) + let contract_missing = contract_missing(plan, path.as_deref()); + json!({"openapi":path.map(|p| p.display().to_string()),"wired":wired,"missing":missing,"deferred":deferred,"contractMissing":contract_missing}) } pub(super) fn blockers(plan: &Value, root: &str) -> Vec { @@ -97,7 +175,8 @@ pub(super) fn blockers(plan: &Value, root: &str) -> Vec { text(&d["reason"]) ) }); - missing.chain(deferred).collect() + let contracts = array(&r["contractMissing"]).iter().map(|contract| format!("endpointContract: {} {} ({})", text(&contract["method"]), text(&contract["path"]), text(&contract["reason"]))); + missing.chain(deferred).chain(contracts).collect() } fn cargo_document(path: &Path) -> Option { diff --git a/crates/devup-mcp/src/server/text/devup-project-plan.md b/crates/devup-mcp/src/server/text/devup-project-plan.md index 17c2ff32..cabf260a 100644 --- a/crates/devup-mcp/src/server/text/devup-project-plan.md +++ b/crates/devup-mcp/src/server/text/devup-project-plan.md @@ -1 +1 @@ -Project intake and supervisor: intake | draft | confirm | status | assume | defer | split. Read Jira/epic children, Figma and devup_project_context all scopes first; pass source-labelled evidence. intake asks work type, optional sources, goal and parts through the runtime question tool. draft records feature-specific rounds and ERD, merges shared question topics, and reports added/changed/kept/removed records in changeSummary. Shared site facts are forwarded to open screen-plan questions; explicit answers take precedence. confirm requires all answers and the Figma screen plan when applicable, writes vespertide models/*.json transactionally, and returns executable recovery for missing answers. split creates a dependency DAG and .devup/units/.md briefs: supervisor-owned shared/table foundations, parallel domain workers, then supervisor integration. Features sharing screen section/menu or entities form one worker owning API, client and pages together; static domains only own front work. Repeated screen parts become sharedParts in stage 0. Each table has one model owner. Briefs prioritize answers and decisions over original scope, filter routes by owned screens (URL or nodeId), and carry facts keyed .exports, .assets and .imports plus protectedFiles when supplied. Unknown file mappings remain null, never invented. Workers escalate to supervisor by default (unitQuestions: supervisor|worker). After confirm never wait: defer recommendations and continue, assume at the end. status reports ERD Mermaid, unit DAG/states, decisions, shared-change requests and backend prerequisites. Data features with explicit entities need apiWiring operationId bindings; status checks them against actual HTTP operations in openapi.json and reports missing or deferred bindings as blockers. These checks do not prove runtime integration. unitUpdates record supervisor verification. State: .devup/project-plan.json. Follow /devup skill for backend commands, latest-first dependencies, exact layout/visual audits, orchestration and zero-drift completion. +Project intake and supervisor: intake | draft | confirm | status | assume | defer | split. Read Jira/epic children, Figma and devup_project_context all scopes first; pass source-labelled evidence. intake asks work type, optional sources, goal and parts through the runtime question tool. draft records feature-specific rounds and ERD, merges shared question topics, and reports added/changed/kept/removed records in changeSummary. Shared site facts are forwarded to open screen-plan questions; explicit answers take precedence. confirm requires all answers and the Figma screen plan when applicable, writes vespertide models/*.json transactionally, and returns executable recovery for missing answers. split creates a dependency DAG and .devup/units/.md briefs: supervisor-owned shared/table foundations, parallel domain workers, then supervisor integration. Features sharing screen section/menu or entities form one worker owning API, client and pages together; static domains only own front work. Repeated screen parts become sharedParts in stage 0. Each table has one model owner. Briefs prioritize answers and decisions over original scope, filter routes by owned screens (URL or nodeId), and carry facts keyed .exports, .assets and .imports plus protectedFiles when supplied. Unknown file mappings remain null, never invented. Workers escalate to supervisor by default (unitQuestions: supervisor|worker). After confirm never wait: defer recommendations and continue, assume at the end. status reports ERD Mermaid, unit DAG/states, decisions, shared-change requests and backend prerequisites. Data mappings infer candidate entities and list/get/create/update operations from feature and screen names/parts. Endpoint contract drafts carry method/path and ERD request/response fields through confirm, split and worker briefs; conventional paths remain drafts requiring API confirmation. Data features need apiWiring operationId bindings. Status checks actual HTTP operations and draft request/response field names in openapi.json, including local schema references; missing/deferred bindings and contractMissing entries are blockers. These static checks do not prove field-type compatibility or runtime integration. unitUpdates record supervisor verification. State: .devup/project-plan.json. Follow /devup skill for backend commands, latest-first dependencies, exact layout/visual audits, orchestration and zero-drift completion. From 46a856695d16ae518169395a2731139c4da97b3f Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 20:55:31 +0900 Subject: [PATCH 082/149] fix(project-plan): infer and verify endpoint contracts (41) --- .../src/server/project_plan/tests.rs | 1 + .../server/project_plan/tests/contracts.rs | 147 +++++++++++++++++ .../src/server/project_plan/tests/feedback.rs | 2 +- .../src/server/project_plan/wiring.rs | 83 +--------- .../server/project_plan/wiring/contract.rs | 67 ++++++++ .../src/server/project_plan/wiring/draft.rs | 148 ++++++++++++++++++ .../src/server/project_plan/wiring/schema.rs | 100 ++++++++++++ 7 files changed, 470 insertions(+), 78 deletions(-) create mode 100644 crates/devup-mcp/src/server/project_plan/tests/contracts.rs create mode 100644 crates/devup-mcp/src/server/project_plan/wiring/contract.rs create mode 100644 crates/devup-mcp/src/server/project_plan/wiring/draft.rs create mode 100644 crates/devup-mcp/src/server/project_plan/wiring/schema.rs diff --git a/crates/devup-mcp/src/server/project_plan/tests.rs b/crates/devup-mcp/src/server/project_plan/tests.rs index 851a8a36..23993bfc 100644 --- a/crates/devup-mcp/src/server/project_plan/tests.rs +++ b/crates/devup-mcp/src/server/project_plan/tests.rs @@ -20,6 +20,7 @@ impl Drop for TestDir { } use super::*; mod feedback; +mod contracts; fn input(root: &Path, action: &str) -> ProjectPlanInput { serde_json::from_value(json!({"action":action,"projectRoot":root})).unwrap() } diff --git a/crates/devup-mcp/src/server/project_plan/tests/contracts.rs b/crates/devup-mcp/src/server/project_plan/tests/contracts.rs new file mode 100644 index 00000000..a32e7d68 --- /dev/null +++ b/crates/devup-mcp/src/server/project_plan/tests/contracts.rs @@ -0,0 +1,147 @@ +use super::*; + +fn plan(scope: &str, columns: Value) -> Value { + json!({"facts":{"parts":"API"},"features":[{"id":"catalog","scope":scope,"entities":["post"],"screens":["1:1"]}], + "entities":[{"name":"post","columns":columns}]}) +} + +#[test] +fn resource_paths_pluralize_the_last_word() { + for (name, path) in [("inquiry","/inquiries"),("category","/categories"),("news","/news"),("series","/series"),("posts","/posts"),("status","/statuses"),("box","/boxes"),("press_release","/press-releases")] { + let mut plan = plan("list", json!([])); + plan["entities"][0]["name"] = json!(name); + plan["features"][0]["entities"] = json!([name]); + wiring::draft(&mut plan); + assert_eq!(plan["endpointContracts"][0]["path"], path, "{name}"); + } +} + +#[test] +fn list_contracts_draft_queries_only_from_relevant_evidence() { + let mut plan = plan("List pagination sort search filter", json!([ + {"name":"title","type":"text","nullable":false}, + {"name":"created_at","type":"timestamptz","nullable":false}, + {"name":"views","type":"integer","nullable":false}, + {"name":"body","type":"text","nullable":false}, + {"name":"category_id","type":"integer","nullable":false}, + {"name":"state","type":{"kind":"enum","values":["draft","published"]},"nullable":false} + ])); + wiring::draft(&mut plan); + let queries = array(&plan["endpointContracts"][0]["queryParams"]); + for name in ["page","pageSize","sort","q","category_id","state"] { + assert!(queries.iter().any(|query| query["name"] == name), "{name}: {queries:?}"); + } + let sort = queries.iter().find(|query| query["name"] == "sort").unwrap(); + assert_eq!(sort["allowedKeys"], json!(["title","created_at","views","category_id"])); + let mut plain = plan.clone(); + plain["features"][0]["scope"] = json!("List"); + wiring::draft(&mut plain); + assert_eq!(plain["endpointContracts"][0]["queryParams"], json!([])); +} + +#[test] +fn write_contracts_include_validation_and_delete_has_no_response_fields() { + let mut plan = plan("Create edit CMS", json!([ + {"name":"email","type":{"kind":"varchar","length":120},"nullable":false}, + {"name":"phone","type":"text","nullable":true}, + {"name":"website_link","type":"text","nullable":true}, + {"name":"privacy_consent","type":"boolean","nullable":false}, + {"name":"state","type":{"kind":"enum","values":["draft","sent"]},"nullable":false} + ])); + wiring::draft(&mut plan); + for operation in ["create","update"] { + let contract = array(&plan["endpointContracts"]).iter().find(|c| c["operation"] == operation).unwrap(); + let fields = array(&contract["requestFields"]); + let email = fields.iter().find(|field| field["name"] == "email").unwrap(); + assert_eq!(email["rules"]["maxLength"], 120); + assert_eq!(email["rules"]["format"], "email"); + assert_eq!(email["rules"]["required"], true); + assert_eq!(fields[1]["rules"]["format"], "phone"); + assert_eq!(fields[2]["rules"]["format"], "url"); + assert_eq!(fields[3]["rules"]["const"], true); + assert_eq!(fields[4]["rules"]["enum"], json!(["draft","sent"])); + } + let delete = array(&plan["endpointContracts"]).iter().find(|c| c["operation"] == "delete").unwrap(); + assert_eq!(delete["method"], "DELETE"); + assert_eq!(delete["path"], "/posts/{id}"); + assert_eq!(delete["responseFields"], json!([])); +} + +#[test] +fn structural_screens_supply_operations_and_form_field_candidates() { + let mut plan = plan("Catalog", json!([{"name":"email_address","type":"text","nullable":false}])); + plan["facts"]["screens"] = json!([{"nodeId":"1:1","url":"/catalog/[slug]","parts":["Tile","Tile","Tile"], + "children":[{"type":"TEXT_INPUT","label":"EmailAddress"},{"name":"TextField","label":"Unknown"}]}]); + wiring::draft(&mut plan); + let mapping = &plan["dataMappings"][0]; + for op in ["list","get","create"] { + assert!(array(&mapping["requiredOperations"]).contains(&json!(op)), "{mapping}"); + assert!(!array(&mapping["operationEvidence"][op]).is_empty()); + } + assert!(array(&mapping["formFields"]).iter().any(|field| field["label"] == "EmailAddress" && field["column"] == "email_address")); + assert!(array(&mapping["formFields"]).iter().any(|field| field["label"] == "Unknown" && field["status"] == "unmatched")); +} + +fn report_contracts(contracts: Value, doc: Value) -> Value { + let dir = TestDir::new(); + std::fs::write(dir.path().join("openapi.json"), doc.to_string()).unwrap(); + wiring::report(&json!({"endpointContracts":contracts}), &dir.path().to_string_lossy()) +} + +#[test] +fn openapi_matches_normalized_fields_structural_paths_and_common_prefix() { + let report = report_contracts(json!([ + {"path":"/posts/{id}","method":"GET","responseFields":[{"name":"image_url","type":"text"}]}, + {"path":"/posts","method":"GET","operation":"list","responseFields":[{"name":"image-url","type":"text"}]} + ]), json!({"paths":{ + "/api/posts/{postId}/":{"get":{"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"ImageUrl":{"type":"string"}}}}}}}}}, + "/api/posts":{"get":{"responses":{"200":{"content":{"application/json":{"schema":{"type":"array","items":{"properties":{"imageUrl":{"type":"string"}}}}}}}}}} + }})); + assert_eq!(report["contractMissing"], json!([]), "{report}"); + assert_eq!(report["contractMatches"][0]["prefix"], "/api"); + assert_eq!(report["contractMatches"][0]["fields"][0]["openapiName"], "ImageUrl"); +} + +#[test] +fn openapi_rejects_ambiguous_or_inconsistent_prefixes() { + for paths in [json!({"/api/posts":{"get":{}},"/cms/posts":{"get":{}}}), json!({"/api/posts":{"get":{}},"/cms/users":{"get":{}}})] { + let report = report_contracts(json!([{"path":"/posts","method":"GET"},{"path":"/users","method":"GET"}]), json!({"paths":paths})); + assert!(!array(&report["contractMissing"]).is_empty()); + } +} + +#[test] +fn openapi_checks_path_and_operation_query_parameters_with_refs() { + let report = report_contracts(json!([{"path":"/posts","method":"GET","queryParams":[{"name":"page"},{"name":"pageSize","aliases":["size"]},{"name":"q"},{"name":"sort"}]}]), + json!({"paths":{"/posts":{"parameters":[{"$ref":"#/components/parameters/Page"}],"get":{"parameters":[{"in":"query","name":"size"},{"in":"header","name":"q"}]}}}, + "components":{"parameters":{"Page":{"in":"query","name":"page"}}}})); + let reason = text(&report["contractMissing"][0]["reason"]); + assert!(reason.contains("queryParams missing q") && reason.contains("queryParams missing sort"), "{report}"); + assert!(!reason.contains("missing page")); +} + +#[test] +fn openapi_reads_composites_wrappers_refs_and_form_bodies() { + for media in ["multipart/form-data", "application/x-www-form-urlencoded"] { + let mut doc = json!({"paths":{"/posts":{"post":{"requestBody":{"content":{}},"responses":{"200":{"$ref":"#/components/responses/List"}}}}}, + "components":{"schemas":{"Post":{"allOf":[{"properties":{"id":{"type":"integer"}}},{"anyOf":[{"properties":{"title":{"$ref":"#/components/schemas/Title"}}},{"properties":{"publishedAt":{"type":["string","null"],"format":"date-time"}}}]}],"required":["id","title"]},"Title":{"type":"string"}}, + "responses":{"List":{"content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Post"}}}}}}}}}}); + doc["paths"]["/posts"]["post"]["requestBody"]["content"][media] = json!({"schema":{"oneOf":[{"properties":{"title":{"type":"string"}},"required":["title"]}]}}); + let report = report_contracts(json!([{"path":"/posts","method":"POST","operation":"list","requestFields":[{"name":"title","type":"text","required":true,"nullable":false}], + "responseFields":[{"name":"id","type":"big_int","required":true,"nullable":false},{"name":"title","type":{"kind":"varchar","length":100},"required":true,"nullable":false},{"name":"published_at","type":"timestamptz","nullable":true}]}]), doc); + assert_eq!(report["contractMissing"], json!([]), "{report}"); + } +} + +#[test] +fn openapi_reports_type_format_nullability_and_required_mismatches() { + for (field, schema, required, fragment) in [ + (json!({"name":"value","type":"integer"}), json!({"type":"string"}), json!([]), "type mismatch value: expected integer got string"), + (json!({"name":"value","type":"date"}), json!({"type":"string","format":"date-time"}), json!([]), "expected string(date) got string(date-time)"), + (json!({"name":"value","type":"boolean","nullable":false}), json!({"type":["boolean","null"]}), json!([]), "nullable mismatch value"), + (json!({"name":"value","type":"text","required":true}), json!({"type":"string"}), json!([]), "required mismatch value") + ] { + let report = report_contracts(json!([{"path":"/posts","method":"GET","responseFields":[field]}]), json!({"paths":{"/posts":{"get":{"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"value":schema},"required":required}}}}}}}}})); + assert!(text(&report["contractMissing"][0]["reason"]).contains(fragment), "{report}"); + } +} diff --git a/crates/devup-mcp/src/server/project_plan/tests/feedback.rs b/crates/devup-mcp/src/server/project_plan/tests/feedback.rs index ce20163d..81c5fd69 100644 --- a/crates/devup-mcp/src/server/project_plan/tests/feedback.rs +++ b/crates/devup-mcp/src/server/project_plan/tests/feedback.rs @@ -27,7 +27,7 @@ async fn screen_data_contracts_survive_confirm_split_and_check_openapi_fields() std::fs::write(dir.path().join("openapi.json"), json!({"paths":{ "/posts":{"get":{"operationId":"listPosts","responses":{"200":{"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Post"}}}}}}}, "post":{"operationId":"createPost","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Post"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Post"}}}}}}}, - },"components":{"schemas":{"Post":{"type":"object","properties":{"id":{"type":"integer"},"title":{"type":"string"},"body":{"type":"string"},"user_id":{"type":"integer"}}}}} + },"components":{"schemas":{"Post":{"type":"object","required":["id","title","body","user_id"],"properties":{"id":{"type":"integer"},"title":{"type":"string"},"body":{"type":"string"},"user_id":{"type":"integer"}}}}} }).to_string()).unwrap(); request.action = "split".into(); let split = run(&request, &policy).await.unwrap(); diff --git a/crates/devup-mcp/src/server/project_plan/wiring.rs b/crates/devup-mcp/src/server/project_plan/wiring.rs index a2624a22..56d3b521 100644 --- a/crates/devup-mcp/src/server/project_plan/wiring.rs +++ b/crates/devup-mcp/src/server/project_plan/wiring.rs @@ -1,81 +1,10 @@ use super::*; +mod draft; +mod contract; +mod schema; +pub(super) use draft::draft; -pub(super) fn draft(plan: &mut Value) { - if !erd::api(plan) {return;} - let mut mappings = vec![]; - let mut contracts = vec![]; - for feature in array(&plan["features"]) { - let screens = units::feature_screens(plan, feature); - let evidence = format!("{} {} {} {}", feature["scope"], feature["screens"], feature["acceptance"], json!(screens)).to_lowercase(); - let mut operations = vec![]; - for (operation, keywords) in [ - ("list", vec!["list", "table", "search", "filter", "pagination", "목록", "검색", "표"]), - ("get", vec!["detail", "상세"]), - ("create", vec!["form", "create", "signup", "input", "textarea", "폼", "등록", "문의"]), - ("update", vec!["edit", "update", "수정"]), - ] { - if keywords.iter().any(|word| evidence.contains(word)) { operations.push(operation); } - } - let entities = units::feature_entities(plan, feature); - if operations.is_empty() && entities.is_empty() { continue; } - if operations.is_empty() { operations.push("list"); } - mappings.push(json!({"feature":feature["id"],"screens":feature["screens"],"candidateEntities":entities,"requiredOperations":operations,"status":"draft"})); - for name in entities { - let Some(entity) = array(&plan["entities"]).iter().find(|entity| entity["name"] == name) else {continue}; - let fields = array(&entity["columns"]).iter().map(|column| json!({ - "name":column["name"],"type":column["type"],"nullable":column["nullable"],"required":column["nullable"] == false, - })).collect::>(); - let resource = if name.ends_with('s') {name.clone()} else {format!("{name}s")}; - for operation in &operations { - let method = match *operation {"create" => "POST", "update" => "PATCH", _ => "GET"}; - let path = if matches!(*operation, "get" | "update") {format!("/{resource}/{{id}}") } else {format!("/{resource}")}; - let request_fields = if matches!(*operation, "create" | "update") { - array(&entity["columns"]).iter().filter(|column| column["primary_key"].is_null() && column["default"].is_null()) - .map(|column| json!({"name":column["name"],"type":column["type"],"nullable":column["nullable"],"required":*operation == "create" && column["nullable"] == false})).collect::>() - } else {vec![]}; - contracts.push(json!({"feature":feature["id"],"entity":name,"operation":operation,"method":method,"path":path, - "requestFields":request_fields,"responseFields":fields,"status":"draft","pathEvidence":"entity-name convention; confirm API path before implementation"})); - } - } - } - plan["dataMappings"] = json!(mappings); - plan["endpointContracts"] = json!(contracts); -} -fn schema_fields(doc: &Value, schema: &Value, depth: usize) -> std::collections::BTreeSet { - if depth > 16 {return Default::default();} - if let Some(reference) = schema["$ref"].as_str().and_then(|reference| reference.strip_prefix('#')) { - return schema_fields(doc, &doc.pointer(reference).cloned().unwrap_or(Value::Null), depth + 1); - } - if schema["type"] == "array" {return schema_fields(doc, &schema["items"], depth + 1);} - let mut fields = schema["properties"].as_object().into_iter().flatten().map(|(name, _)| name.clone()).collect::>(); - for part in array(&schema["allOf"]) {fields.extend(schema_fields(doc, part, depth + 1));} - fields -} - -fn contract_missing(plan: &Value, path: Option<&Path>) -> Vec { - let Some(path) = path else {return vec![]}; - let doc = std::fs::read(path).ok().and_then(|bytes| serde_json::from_slice::(&bytes).ok()).unwrap_or(Value::Null); - let mut missing = vec![]; - for contract in array(&plan["endpointContracts"]) { - let operation = &doc["paths"][text(&contract["path"])][text(&contract["method"]).to_lowercase()]; - let mut reasons = vec![]; - if !operation.is_object() { - reasons.push("HTTP method/path absent from openapi.json".to_owned()); - } else { - let request = schema_fields(&doc, &operation["requestBody"]["content"]["application/json"]["schema"], 0); - let response = operation["responses"].as_object().into_iter().flatten() - .filter(|(status, _)| status.starts_with('2')).flat_map(|(_, response)| schema_fields(&doc, &response["content"]["application/json"]["schema"], 0)).collect::>(); - for (key, fields) in [("requestFields", request), ("responseFields", response)] { - for field in array(&contract[key]) { - if !fields.contains(text(&field["name"])) {reasons.push(format!("{key} missing {}", text(&field["name"])));} - } - } - } - if !reasons.is_empty() {missing.push(json!({"feature":contract["feature"],"method":contract["method"],"path":contract["path"],"reason":reasons.join("; ")}));} - } - missing -} fn openapi_path(root: &str) -> Option { let base = Path::new(root); @@ -154,8 +83,8 @@ pub(super) fn report(plan: &Value, root: &str) -> Value { missing.push(json!({"feature":w["feature"],"operationId":w["operationId"],"reason":why})); } } - let contract_missing = contract_missing(plan, path.as_deref()); - json!({"openapi":path.map(|p| p.display().to_string()),"wired":wired,"missing":missing,"deferred":deferred,"contractMissing":contract_missing}) + let (contract_missing, contract_matches) = contract::report(plan, path.as_deref()); + json!({"openapi":path.map(|p| p.display().to_string()),"wired":wired,"missing":missing,"deferred":deferred,"contractMissing":contract_missing,"contractMatches":contract_matches}) } pub(super) fn blockers(plan: &Value, root: &str) -> Vec { diff --git a/crates/devup-mcp/src/server/project_plan/wiring/contract.rs b/crates/devup-mcp/src/server/project_plan/wiring/contract.rs new file mode 100644 index 00000000..ef88a98a --- /dev/null +++ b/crates/devup-mcp/src/server/project_plan/wiring/contract.rs @@ -0,0 +1,67 @@ +use super::*; + +fn suffix_prefix(actual: &str, expected: &str) -> Option { + let actual = actual.trim_matches('/').split('/').collect::>(); + let expected = expected.trim_matches('/').split('/').collect::>(); + if actual.len() < expected.len() { return None; } + let offset = actual.len() - expected.len(); + actual[offset..].iter().zip(expected).all(|(left, right)| left == &right || (left.starts_with('{') && left.ends_with('}') && right.starts_with('{') && right.ends_with('}'))) + .then(|| if offset == 0 {String::new()} else {format!("/{}", actual[..offset].join("/"))}) +} + +fn candidates<'a>(doc: &'a Value, contract: &Value) -> Vec<(&'a str, &'a Value, String)> { + let method = text(&contract["method"]).to_lowercase(); + let mut found = doc["paths"].as_object().into_iter().flatten().filter_map(|(path, item)| { + let prefix = suffix_prefix(path, text(&contract["path"]))?; + item[&method].is_object().then_some((path.as_str(), item, prefix)) + }).collect::>(); + if found.iter().any(|(_, _, prefix)| prefix.is_empty()) { found.retain(|(_, _, prefix)| prefix.is_empty()); } + found +} + +pub(super) fn report(plan: &Value, path: Option<&Path>) -> (Vec, Vec) { + let Some(path) = path else {return (vec![], vec![])}; + let doc = std::fs::read(path).ok().and_then(|bytes| serde_json::from_slice::(&bytes).ok()).unwrap_or(Value::Null); + let contracts = array(&plan["endpointContracts"]); + let prefixes = contracts.iter().map(|contract| candidates(&doc, contract).into_iter().map(|(_, _, prefix)| prefix).collect::>()).filter(|prefixes| !prefixes.is_empty()).reduce(|left, right| left.intersection(&right).cloned().collect()).unwrap_or_default(); + let (mut missing, mut matched) = (vec![], vec![]); + for contract in contracts { + let found = candidates(&doc, contract).into_iter().filter(|(_, _, prefix)| prefix.is_empty() || prefixes.len() == 1 && prefixes.contains(prefix)).collect::>(); + let mut reasons = vec![]; + let mut field_matches = vec![]; + let mut path_match = Value::Null; + if let [(path, item, prefix)] = found.as_slice() { + let operation = &item[text(&contract["method"]).to_lowercase()]; + let request = schema::fields(&doc, schema::body_schema(&doc, &operation["requestBody"]), false, 0); + let mut response = BTreeMap::::new(); + for (status, body) in operation["responses"].as_object().into_iter().flatten().filter(|(status, _)| status.starts_with('2')) { + let _ = status; + for (name, field) in schema::fields(&doc, schema::body_schema(&doc, body), contract["operation"] == "list", 0) { + let entry = response.entry(name).or_default(); + entry.schemas.extend(field.schemas); + entry.required |= field.required; + } + } + for (key, fields) in [("requestFields", request), ("responseFields", response)] { + for field in array(&contract[key]) { + let name = text(&field["name"]); + if let Some((spelling, actual)) = fields.get_key_value(name).or_else(|| fields.iter().find(|(spelling, _)| draft::normalized(spelling) == draft::normalized(name))) { + field_matches.push(json!({"kind":key,"name":name,"openapiName":spelling})); + reasons.extend(schema::compare(&doc, field, actual)); + } else { reasons.push(format!("{key} missing {name}")); } + } + } + let queries = array(&item["parameters"]).iter().chain(array(&operation["parameters"])).map(|parameter| schema::resolve(&doc, parameter)).filter(|parameter| parameter["in"] == "query").collect::>(); + for query in array(&contract["queryParams"]) { + let name = text(&query["name"]); + if let Some(actual) = queries.iter().find(|actual| draft::normalized(text(&actual["name"])) == draft::normalized(name) || array(&query["aliases"]).contains(&actual["name"])) { + field_matches.push(json!({"kind":"queryParams","name":name,"openapiName":actual["name"]})); + } else { reasons.push(format!("queryParams missing {name}")); } + } + path_match = json!({"feature":contract["feature"],"method":contract["method"],"path":contract["path"],"openapiPath":path,"prefix":prefix,"fields":field_matches}); + } else { reasons.push("HTTP method/path absent or ambiguous in openapi.json".to_owned()); } + if !reasons.is_empty() { missing.push(json!({"feature":contract["feature"],"method":contract["method"],"path":contract["path"],"reason":reasons.join("; ")})); } + if !path_match.is_null() { matched.push(path_match); } + } + (missing, matched) +} diff --git a/crates/devup-mcp/src/server/project_plan/wiring/draft.rs b/crates/devup-mcp/src/server/project_plan/wiring/draft.rs new file mode 100644 index 00000000..8fc77b89 --- /dev/null +++ b/crates/devup-mcp/src/server/project_plan/wiring/draft.rs @@ -0,0 +1,148 @@ +use super::*; + +pub(super) fn normalized(name: &str) -> String { + name.chars().filter(|ch| !matches!(ch, '_' | '-')).flat_map(char::to_lowercase).collect() +} + +fn resource(name: &str) -> String { + let name = name.replace('_', "-"); + let word = name.rsplit('-').next().unwrap_or(&name); + if matches!(word, "news" | "series" | "species") || (word.ends_with('s') && !word.ends_with("ss") && !word.ends_with("us")) { + name + } else if word.ends_with('y') && word.chars().rev().nth(1).is_some_and(|ch| !"aeiou".contains(ch)) { + format!("{}ies", name.strip_suffix('y').unwrap_or(&name)) + } else if ["s", "x", "z", "ch", "sh"].iter().any(|suffix| word.ends_with(suffix)) { + format!("{name}es") + } else { format!("{name}s") } +} + +fn contains(evidence: &str, words: &[&str]) -> bool { + words.iter().any(|word| evidence.contains(word)) +} + +fn structure(node: &Value, counts: &mut BTreeMap, labels: &mut Vec) { + if let Some(name) = node.as_str() { + *counts.entry(name.to_owned()).or_default() += 1; + if contains(&normalized(name), &["input", "textfield", "textarea", "select", "checkbox", "radio"]) { labels.push(name.to_owned()); } + return; + } + let name = node["componentName"].as_str().or_else(|| node["name"].as_str()).unwrap_or_default(); + if !name.is_empty() { + *counts.entry(name.to_owned()).or_default() += node["count"].as_u64().or_else(|| node["instanceCount"].as_u64()).unwrap_or(1); + } + let kind = format!("{} {} {}", name, text(&node["type"]), text(&node["nodeType"])); + if contains(&normalized(&kind), &["input", "textfield", "textarea", "select", "checkbox", "radio"]) { + labels.push(node["label"].as_str().or_else(|| node["fieldName"].as_str()).unwrap_or(name).to_owned()); + } + for key in ["parts", "children", "nodes", "instances"] { + for child in array(&node[key]) { structure(child, counts, labels); } + } + for key in ["repeatedInstances", "instanceCounts"] { + for (name, count) in node[key].as_object().into_iter().flatten() { + *counts.entry(name.clone()).or_default() += count.as_u64().unwrap_or(0); + } + } +} + +fn rules(column: &Value) -> Value { + let name = text(&column["name"]).to_lowercase(); + let mut rules = json!({"required":column["nullable"] == false && column["default"].is_null()}); + if let Some(length) = column["type"]["length"].as_u64().or_else(|| column["length"].as_u64()) { + rules["maxLength"] = json!(length); + } else if let Some(length) = text(&column["type"]).strip_prefix("varchar(").and_then(|value| value.strip_suffix(')')).and_then(|value| value.parse::().ok()) { + rules["maxLength"] = json!(length); + } + for (format, words) in [("email", vec!["email"]), ("phone", vec!["phone", "tel"]), ("url", vec!["url", "link"])] { + if contains(&name, &words) { rules["format"] = json!(format); break; } + } + if matches!(text(&column["type"]), "boolean" | "bool") && contains(&name, &["agree", "consent", "privacy", "동의"]) { + rules["const"] = json!(true); + } + if column["type"]["kind"] == "enum" { rules["enum"] = column["type"]["values"].clone(); } + rules +} + +fn queries(entity: &Value, evidence: &str) -> Vec { + let mut queries = vec![]; + if contains(evidence, &["pagination", "페이지", "page number"]) { + queries.extend([json!({"name":"page","type":"integer"}), json!({"name":"pageSize","type":"integer","aliases":["size"]})]); + } + if contains(evidence, &["sort", "정렬"]) { + let keys = array(&entity["columns"]).iter().filter(|column| { + let kind = column["type"].as_str().or_else(|| column["type"]["kind"].as_str()).unwrap_or_default(); + contains(kind, &["int", "float", "real", "double", "decimal", "numeric", "date", "time"]) || text(&column["name"]).contains("title") + }).map(|column| column["name"].clone()).collect::>(); + queries.push(json!({"name":"sort","type":"string","allowedKeys":keys})); + } + if contains(evidence, &["search", "검색"]) { queries.push(json!({"name":"q","type":"string"})); } + if contains(evidence, &["filter", "category", "필터", "카테고리", "탭"]) { + for column in array(&entity["columns"]) { + let name = text(&column["name"]); + if column["type"]["kind"] == "enum" || name.ends_with("_id") || contains(name, &["category", "카테고리"]) { + queries.push(json!({"name":name,"type":column["type"],"values":column["type"]["values"]})); + } + } + } + queries +} + +pub(in crate::server::project_plan) fn draft(plan: &mut Value) { + if !erd::api(plan) { return; } + let (mut mappings, mut contracts) = (vec![], vec![]); + for feature in array(&plan["features"]) { + let screens = units::feature_screens(plan, feature); + let evidence = format!("{} {} {} {}", feature["scope"], feature["screens"], feature["acceptance"], json!(screens)).to_lowercase(); + let mut driven = BTreeMap::<&str, Vec>::new(); + for (op, words) in [ + ("list", vec!["list", "table", "search", "filter", "pagination", "목록", "검색", "표"]), + ("get", vec!["detail", "상세"]), + ("create", vec!["form", "create", "signup", "input", "textarea", "폼", "등록", "문의"]), + ("update", vec!["edit", "update", "수정"]), + ("delete", vec!["delete", "remove", "삭제", "admin", "cms", "관리"]), + ] { + for word in words { if evidence.contains(word) { driven.entry(op).or_default().push(format!("keyword: {word}")); } } + } + let (mut counts, mut labels) = (BTreeMap::new(), vec![]); + for screen in &screens { + structure(screen, &mut counts, &mut labels); + for key in ["url", "route", "routePath", "proposedRoute"] { + if text(&screen[key]).split('/').any(|segment| segment.starts_with('[') && segment.ends_with(']')) { + driven.entry("get").or_default().push(format!("dynamic route: {}", text(&screen[key]))); + } + } + } + for route in array(&feature["screens"]).iter().filter_map(Value::as_str) { + if route.split('/').any(|segment| segment.starts_with('[') && segment.ends_with(']')) { driven.entry("get").or_default().push(format!("dynamic route: {route}")); } + } + for (name, count) in counts.iter().filter(|(_, count)| **count >= 3) { + driven.entry("list").or_default().push(format!("repeated component: {name} ({count})")); + } + if !labels.is_empty() { driven.entry("create").or_default().push(format!("form controls: {}", labels.join(", "))); } + let entities = units::feature_entities(plan, feature); + if driven.is_empty() && entities.is_empty() { continue; } + if driven.is_empty() { driven.insert("list", vec!["entity mapping".into()]); } + let operations = driven.keys().copied().collect::>(); + let columns = array(&plan["entities"]).iter().filter(|entity| entities.contains(&text(&entity["name"]).to_owned())).flat_map(|entity| array(&entity["columns"])).collect::>(); + let form_fields = labels.iter().map(|label| { + let column = columns.iter().find(|column| normalized(text(&column["name"])) == normalized(label)); + json!({"label":label,"column":column.map(|column| column["name"].clone()),"status":if column.is_some() {"matched"} else {"unmatched"}}) + }).collect::>(); + mappings.push(json!({"feature":feature["id"],"screens":feature["screens"],"candidateEntities":entities,"requiredOperations":operations,"operationEvidence":driven,"formFields":form_fields,"status":"draft"})); + for name in entities { + let Some(entity) = array(&plan["entities"]).iter().find(|entity| entity["name"] == name) else {continue}; + for op in &operations { + let method = match *op {"create" => "POST", "update" => "PATCH", "delete" => "DELETE", _ => "GET"}; + let resource = resource(&name); + let path = if matches!(*op, "get" | "update" | "delete") {format!("/{resource}/{{id}}") } else {format!("/{resource}")}; + let fields = array(&entity["columns"]).iter().map(|column| json!({"name":column["name"],"type":column["type"],"nullable":column["nullable"],"required":column["nullable"] == false})).collect::>(); + let request_fields = if matches!(*op, "create" | "update") { + array(&entity["columns"]).iter().filter(|column| column["primary_key"].is_null() && column["default"].is_null()).map(|column| json!({"name":column["name"],"type":column["type"],"nullable":column["nullable"],"required":*op == "create" && column["nullable"] == false,"rules":rules(column)})).collect::>() + } else {vec![]}; + contracts.push(json!({"feature":feature["id"],"entity":name,"operation":op,"method":method,"path":path,"queryParams":if *op == "list" {queries(entity, &evidence)} else {vec![]}, + "operationEvidence":driven[op],"formFields":form_fields,"requestFields":request_fields,"responseFields":if *op == "delete" {vec![]} else {fields},"status":"draft","pathEvidence":"entity-name convention; confirm API path before implementation"})); + } + } + } + plan["dataMappings"] = json!(mappings); + plan["endpointContracts"] = json!(contracts); +} diff --git a/crates/devup-mcp/src/server/project_plan/wiring/schema.rs b/crates/devup-mcp/src/server/project_plan/wiring/schema.rs new file mode 100644 index 00000000..512d6956 --- /dev/null +++ b/crates/devup-mcp/src/server/project_plan/wiring/schema.rs @@ -0,0 +1,100 @@ +use super::*; + +pub(super) fn resolve<'a>(doc: &'a Value, value: &'a Value) -> &'a Value { + let mut current = value; + for _ in 0..16 { + let Some(reference) = current["$ref"].as_str().and_then(|reference| reference.strip_prefix('#')) else { break }; + let Some(target) = doc.pointer(reference) else { break }; + current = target; + } + current +} + +pub(super) fn body_schema<'a>(doc: &'a Value, body: &'a Value) -> &'a Value { + let content = &resolve(doc, body)["content"]; + ["application/json", "multipart/form-data", "application/x-www-form-urlencoded"].into_iter() + .find_map(|media| content.get(media).map(|value| &value["schema"])) + .unwrap_or(&Value::Null) +} + +#[derive(Default)] +pub(super) struct Field { + pub schemas: Vec, + pub required: bool, +} + +pub(super) fn fields(doc: &Value, schema: &Value, list: bool, depth: usize) -> BTreeMap { + if depth > 16 { return BTreeMap::new(); } + let schema = resolve(doc, schema); + if schema["type"] == "array" { return fields(doc, &schema["items"], false, depth + 1); } + let mut result = BTreeMap::::new(); + for (name, property) in schema["properties"].as_object().into_iter().flatten() { + let target = resolve(doc, property); + if list && ["items", "data", "results"].contains(&name.as_str()) { + let wrapped = fields(doc, target, true, depth + 1); + if !wrapped.is_empty() { result.extend(wrapped); continue; } + } + result.insert(name.clone(), Field {schemas:vec![target.clone()], required:array(&schema["required"]).contains(&json!(name))}); + } + for key in ["allOf", "oneOf", "anyOf"] { + for part in array(&schema[key]) { + for (name, field) in fields(doc, part, list, depth + 1) { + let entry = result.entry(name).or_default(); + entry.schemas.extend(field.schemas); + entry.required |= field.required; + } + } + } + for name in array(&schema["required"]).iter().filter_map(Value::as_str) { + if let Some(field) = result.get_mut(name) { field.required = true; } + } + result +} + +fn variants(doc: &Value, schema: &Value, depth: usize) -> Vec<(String, String)> { + if depth > 16 { return vec![]; } + let schema = resolve(doc, schema); + let mut result = vec![]; + let types = schema["type"].as_str().map(|kind| vec![kind]).unwrap_or_else(|| array(&schema["type"]).iter().filter_map(Value::as_str).collect()); + for kind in types { result.push((kind.to_owned(), text(&schema["format"]).to_owned())); } + if schema["nullable"] == true { result.push(("null".into(), String::new())); } + for key in ["allOf", "oneOf", "anyOf"] { + for part in array(&schema[key]) { result.extend(variants(doc, part, depth + 1)); } + } + result +} + +fn expected(field: &Value) -> (&str, &str) { + let kind = field["type"].as_str().or_else(|| field["type"]["kind"].as_str()).unwrap_or_default(); + match kind { + "integer" | "bigint" | "big_int" | "small_int" | "smallint" => ("integer", ""), + "float" | "real" | "double_precision" | "decimal" | "numeric" => ("number", ""), + "boolean" | "bool" => ("boolean", ""), + "date" => ("string", "date"), + "timestamp" | "timestamptz" | "datetime" => ("string", "date-time"), + "json" => ("object", ""), + "enum" => if array(&field["type"]["values"]).iter().any(|value| value["value"].is_number()) { ("integer", "") } else { ("string", "") }, + "text" | "varchar" | "char" | "uuid" | "string" => ("string", ""), + other if other.starts_with("varchar(") => ("string", ""), + _ => ("", ""), + } +} + +pub(super) fn compare(doc: &Value, field: &Value, actual: &Field) -> Vec { + let name = text(&field["name"]); + let (kind, format) = expected(field); + let types = actual.schemas.iter().flat_map(|schema| variants(doc, schema, 0)).collect::>(); + let mut reasons = vec![]; + if !kind.is_empty() && !types.is_empty() && !types.iter().any(|(found, found_format)| found == kind && (format.is_empty() || format == found_format)) { + let display = |kind: &str, format: &str| if format.is_empty() {kind.to_owned()} else {format!("{kind}({format})")}; + reasons.push(format!("type mismatch {name}: expected {} got {}", display(kind, format), types.iter().map(|(kind, format)| display(kind, format)).collect::>().join("|"))); + } + if let Some(nullable) = field["nullable"].as_bool() + && !types.is_empty() && nullable != types.iter().any(|(kind, _)| kind == "null") { + reasons.push(format!("nullable mismatch {name}: expected {nullable}")); + } + if let Some(required) = field["required"].as_bool() && required != actual.required { + reasons.push(format!("required mismatch {name}: expected {required} got {}", actual.required)); + } + reasons +} From 68b6720048b9e64aec5825b0f21b899256ba63e8 Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 20:55:52 +0900 Subject: [PATCH 083/149] fix(project-plan): infer owned unit file contracts (22) --- .../src/server/project_plan/tests.rs | 1 + .../server/project_plan/tests/inference.rs | 39 ++++++++ .../src/server/project_plan/units.rs | 9 +- .../server/project_plan/units/inference.rs | 93 +++++++++++++++++++ 4 files changed, 138 insertions(+), 4 deletions(-) create mode 100644 crates/devup-mcp/src/server/project_plan/tests/inference.rs create mode 100644 crates/devup-mcp/src/server/project_plan/units/inference.rs diff --git a/crates/devup-mcp/src/server/project_plan/tests.rs b/crates/devup-mcp/src/server/project_plan/tests.rs index 23993bfc..a6ab26f3 100644 --- a/crates/devup-mcp/src/server/project_plan/tests.rs +++ b/crates/devup-mcp/src/server/project_plan/tests.rs @@ -21,6 +21,7 @@ impl Drop for TestDir { use super::*; mod feedback; mod contracts; +mod inference; fn input(root: &Path, action: &str) -> ProjectPlanInput { serde_json::from_value(json!({"action":action,"projectRoot":root})).unwrap() } diff --git a/crates/devup-mcp/src/server/project_plan/tests/inference.rs b/crates/devup-mcp/src/server/project_plan/tests/inference.rs new file mode 100644 index 00000000..4ad83310 --- /dev/null +++ b/crates/devup-mcp/src/server/project_plan/tests/inference.rs @@ -0,0 +1,39 @@ +use super::*; + +#[test] +fn split_infers_owned_exports_assets_and_shared_imports_from_app_evidence() { + let dir = TestDir::new(); + for app in ["front", "admin"] { std::fs::create_dir_all(dir.path().join(format!("apps/{app}/src/app"))).unwrap(); } + let mut plan = json!({"facts":{"parts":"front","screens":[ + {"nodeId":"1:1","parts":["Header","PageHero"],"assets":[{"assetId":"hero","path":"/hero.webp"}]}, + {"nodeId":"2:1","parts":["Header","PageHero"],"assets":[{"assetId":"unrelated"}]} + ]},"features":[{"id":"news","scope":"News","screens":["1:1"]}],"issues":[],"answers":{},"units":[], + "screenPlan":{"routes":[{"url":"/news/[id]","nodeIds":["1:1"],"app":"apps/front"},{"url":"/other","nodeIds":["2:1"],"app":"apps/admin"}]}}); + let policy = OutputPolicy::from_roots(vec![dir.path().to_path_buf()]).unwrap(); + let mut tx = OutputTransaction::new(); + units::split(&input(dir.path(), "split"), &mut plan, &mut tx, &policy).unwrap(); + let contract = &array(&plan["units"]).iter().find(|unit| unit["id"] == "news-front").unwrap()["contract"]; + for path in ["apps/front/src/app/news/[id]/page.tsx","apps/front/src/components/pages/news/[id]/"] { + assert!(array(&contract["exports"]).iter().any(|item| item["path"] == path && item["inferred"] == true), "{contract}"); + } + assert_eq!(contract["assets"][0]["assetId"], "hero"); + assert_eq!(array(&contract["assets"]).len(), 1); + for path in ["apps/front/src/components/layout/Header.tsx","apps/front/src/components/common/PageHero.tsx"] { + assert!(array(&contract["imports"]).iter().any(|item| item["path"] == path && item["inferred"] == true), "{contract}"); + } + assert!(array(&contract["exports"]).iter().chain(array(&contract["assets"])).chain(array(&contract["imports"])).all(|item| item["evidence"].is_string())); +} + +#[test] +fn split_inference_honors_empty_explicit_facts_and_single_app() { + let dir = TestDir::new(); + std::fs::create_dir_all(dir.path().join("src/app")).unwrap(); + let mut plan = json!({"facts":{"parts":"front","news.assets":[],"news.imports":["explicit"]},"features":[{"id":"news","scope":"News","screens":["/news"]}],"issues":[],"answers":{},"units":[],"screenPlan":{"routes":[{"url":"/news"}]}}); + let policy = OutputPolicy::from_roots(vec![dir.path().to_path_buf()]).unwrap(); + let mut tx = OutputTransaction::new(); + units::split(&input(dir.path(), "split"), &mut plan, &mut tx, &policy).unwrap(); + let contract = &array(&plan["units"]).iter().find(|unit| unit["id"] == "news-front").unwrap()["contract"]; + assert_eq!(contract["assets"], json!([])); + assert_eq!(contract["imports"], json!(["explicit"])); + assert_eq!(contract["exports"][0]["path"], "src/app/news/page.tsx"); +} diff --git a/crates/devup-mcp/src/server/project_plan/units.rs b/crates/devup-mcp/src/server/project_plan/units.rs index 8793360a..0466f82c 100644 --- a/crates/devup-mcp/src/server/project_plan/units.rs +++ b/crates/devup-mcp/src/server/project_plan/units.rs @@ -1,4 +1,5 @@ use super::*; +mod inference; fn unit( id: String, @@ -268,12 +269,12 @@ pub(super) fn split( let endpoints = array(&plan["endpointContracts"]).iter().filter(|contract| { u["stage"] != 1 || array(&u["features"]).contains(&contract["feature"]) }).cloned().collect::>(); - let mut contract = json!({"routes":screen_slice["routes"],"protectedFiles":plan["facts"]["protectedFiles"],"sharedParts":units_shared_parts(plan, u),"endpointContracts":endpoints}); + let contract = json!({"routes":screen_slice["routes"],"protectedFiles":plan["facts"]["protectedFiles"],"sharedParts":units_shared_parts(plan, u),"endpointContracts":endpoints}); + u["contract"] = contract; + let drafts = inference::drafts(plan, u, &input.project_root); for field in ["exports", "assets", "imports"] { - let values = array(&u["features"]).iter().flat_map(|feature| array(&plan["facts"][format!("{}.{field}", text(feature))]).to_vec()).collect::>(); - contract[field] = if values.is_empty() {plan["facts"][format!("{owner}.{field}")].clone()} else {json!(values)}; + u["contract"][field] = drafts[field].clone(); } - u["contract"] = contract; let brief = format!( include_str!("../text/units-split-bf50d8a5.md"), text(&u["id"]), diff --git a/crates/devup-mcp/src/server/project_plan/units/inference.rs b/crates/devup-mcp/src/server/project_plan/units/inference.rs new file mode 100644 index 00000000..55c64726 --- /dev/null +++ b/crates/devup-mcp/src/server/project_plan/units/inference.rs @@ -0,0 +1,93 @@ +use super::*; + +fn apps(root: &Path) -> Vec { + let mut found = vec![]; + if root.join("src/app").is_dir() { found.push(String::new()); } + if let Ok(entries) = std::fs::read_dir(root.join("apps")) { + for entry in entries.filter_map(Result::ok) { + if entry.path().join("src/app").is_dir() { found.push(format!("apps/{}", entry.file_name().to_string_lossy())); } + } + } + found.sort(); + found +} + +fn app_for(route: &Value, apps: &[String]) -> Option { + for key in ["file", "app", "appRoot", "projectRoot"] { + if let Some(value) = route[key].as_str() { + let value = value.replace('\\', "/"); + if let Some(app) = apps.iter().filter(|app| !app.is_empty()).find(|app| value == **app || value.starts_with(&format!("{app}/")) || value.ends_with(&format!("/{app}")) || value.contains(&format!("/{app}/"))) { return Some(app.clone()); } + } + } + if apps.len() == 1 { apps.first().cloned() } else { None } +} + +fn annotated(path: String, evidence: &str) -> Value { + json!({"path":path,"inferred":true,"evidence":evidence}) +} + +fn owned(route: &Value, screens: &[Value]) -> bool { + screens.contains(&route["url"]) || array(&route["nodeIds"]).iter().any(|node| screens.contains(node)) +} + +fn assets(source: &Value, output: &mut Vec) { + for key in ["assets", "assetManifest"] { + let value = &source[key]; + let rows = if value.is_array() {array(value)} else {array(&value["assets"])}; + for asset in rows { + let mut asset = if asset.is_object() {asset.clone()} else {json!({"assetId":asset})}; + asset["inferred"] = json!(true); + asset["evidence"] = json!("owned screen/export asset facts"); + if !output.contains(&asset) { output.push(asset); } + } + } +} + +pub(super) fn drafts(plan: &Value, unit: &Value, root: &str) -> Value { + let apps = apps(Path::new(root)); + let mut result = json!({"exports":[],"assets":[],"imports":[]}); + let mut features = array(&unit["features"]).to_vec(); + if features.is_empty() { features.push(json!(unit_owner(unit))); } + for owner in features.iter().filter_map(Value::as_str) { + let feature = array(&plan["features"]).iter().find(|feature| feature["id"] == owner); + let screens = feature.map(|feature| array(&feature["screens"])).unwrap_or_else(|| array(&unit["screens"])); + let routes = array(&plan["screenPlan"]["routes"]).iter().filter(|route| owned(route, screens)).collect::>(); + let mut inferred = json!({"exports":[],"assets":[],"imports":[]}); + let mut imports = vec![]; + for route in &routes { + let Some(app) = app_for(route, &apps) else {continue}; + let prefix = if app.is_empty() {String::new()} else {format!("{app}/")}; + let segment = text(&route["url"]).trim_matches('/'); + let suffix = if segment.is_empty() {String::new()} else {format!("{segment}/")}; + if let Some(exports) = inferred["exports"].as_array_mut() { + exports.push(annotated(format!("{prefix}src/app/{suffix}page.tsx"), "owned route and detected App Router app")); + exports.push(annotated(format!("{prefix}src/components/pages/{suffix}"), "owned route; project page-component convention")); + } + for part in array(&unit["contract"]["sharedParts"]).iter().filter_map(Value::as_str) { + let directory = if ["header", "footer", "nav"].iter().any(|name| part.to_lowercase().contains(name)) {"layout"} else {"common"}; + let item = annotated(format!("{prefix}src/components/{directory}/{part}.tsx"), "sharedParts; project shared-component convention"); + if !imports.contains(&item) { imports.push(item); } + } + } + inferred["imports"] = json!(imports); + let mut asset_rows = vec![]; + if let Some(feature) = feature { + for screen in feature_screens(plan, feature) { assets(&screen, &mut asset_rows); } + } + for route in &routes { assets(route, &mut asset_rows); } + for export in array(&plan["facts"]["exports"]).iter().chain(array(&plan["screenPlan"]["exports"])) { + if screens.contains(&export["nodeId"]) || screens.contains(&export["screenId"]) || routes.iter().any(|route| array(&route["nodeIds"]).contains(&export["nodeId"])) { + assets(export, &mut asset_rows); + } + } + inferred["assets"] = json!(asset_rows); + for field in ["exports", "assets", "imports"] { + let explicit = plan["facts"].get(format!("{owner}.{field}")); + let values = explicit.unwrap_or(&inferred[field]); + if let Some(target) = result[field].as_array_mut() { + for value in array(values) { if !target.contains(value) { target.push(value.clone()); } } + } + } + } + result +} From 3833e1aad75b4e9155e5fcc4b9233d59880a36c6 Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 20:56:10 +0900 Subject: [PATCH 084/149] fix(project-plan): document inferred contract evidence (41) --- .../devup-mcp/src/server/text/devup-project-plan.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/crates/devup-mcp/src/server/text/devup-project-plan.md b/crates/devup-mcp/src/server/text/devup-project-plan.md index cabf260a..4bb977d4 100644 --- a/crates/devup-mcp/src/server/text/devup-project-plan.md +++ b/crates/devup-mcp/src/server/text/devup-project-plan.md @@ -1 +1,11 @@ -Project intake and supervisor: intake | draft | confirm | status | assume | defer | split. Read Jira/epic children, Figma and devup_project_context all scopes first; pass source-labelled evidence. intake asks work type, optional sources, goal and parts through the runtime question tool. draft records feature-specific rounds and ERD, merges shared question topics, and reports added/changed/kept/removed records in changeSummary. Shared site facts are forwarded to open screen-plan questions; explicit answers take precedence. confirm requires all answers and the Figma screen plan when applicable, writes vespertide models/*.json transactionally, and returns executable recovery for missing answers. split creates a dependency DAG and .devup/units/.md briefs: supervisor-owned shared/table foundations, parallel domain workers, then supervisor integration. Features sharing screen section/menu or entities form one worker owning API, client and pages together; static domains only own front work. Repeated screen parts become sharedParts in stage 0. Each table has one model owner. Briefs prioritize answers and decisions over original scope, filter routes by owned screens (URL or nodeId), and carry facts keyed .exports, .assets and .imports plus protectedFiles when supplied. Unknown file mappings remain null, never invented. Workers escalate to supervisor by default (unitQuestions: supervisor|worker). After confirm never wait: defer recommendations and continue, assume at the end. status reports ERD Mermaid, unit DAG/states, decisions, shared-change requests and backend prerequisites. Data mappings infer candidate entities and list/get/create/update operations from feature and screen names/parts. Endpoint contract drafts carry method/path and ERD request/response fields through confirm, split and worker briefs; conventional paths remain drafts requiring API confirmation. Data features need apiWiring operationId bindings. Status checks actual HTTP operations and draft request/response field names in openapi.json, including local schema references; missing/deferred bindings and contractMissing entries are blockers. These static checks do not prove field-type compatibility or runtime integration. unitUpdates record supervisor verification. State: .devup/project-plan.json. Follow /devup skill for backend commands, latest-first dependencies, exact layout/visual audits, orchestration and zero-drift completion. +Project intake and supervisor: intake | draft | confirm | status | assume | defer | split. + +Read Jira/epic children, Figma and devup_project_context all scopes first; pass source-labelled evidence. intake asks work type, optional sources, goal and parts through the runtime question tool. draft records feature-specific rounds and ERD, merges shared question topics, and reports added/changed/kept/removed records in changeSummary. Shared site facts are forwarded to open screen-plan questions; explicit answers take precedence. confirm requires all answers and the Figma screen plan when applicable, writes vespertide models/*.json transactionally, and returns executable recovery for missing answers. + +split creates a dependency DAG and .devup/units/.md briefs: supervisor-owned shared/table foundations, parallel domain workers, then supervisor integration. Features sharing screen section/menu or entities form one worker owning API, client and pages together; static domains only own front work. Repeated screen parts become sharedParts in stage 0. Each table has one model owner. Briefs prioritize answers and decisions over original scope and filter routes by owned screens (URL or nodeId). Facts keyed .exports, .assets and .imports take precedence, including explicit empty lists; protectedFiles is preserved when supplied. When absent, drafts infer exports from owned routes and detected src/app or apps/*/src/app directories, assets from owned screen/export facts, and imports from sharedParts using components/layout for Header/Footer/Nav and components/common otherwise. Inferred entries carry inferred:true and evidence. Multiple apps require route/app evidence; unresolved app ownership is not guessed. Workers escalate to supervisor by default (unitQuestions: supervisor|worker). + +Data mappings infer candidate entities and list/get/create/update/delete operations from feature and screen names/parts. Repeated components (at least three), form controls and dynamic route segments add operationEvidence; formFields records matched and unmatched ERD candidates. Endpoint contract drafts carry method/path, list queryParams and ERD request/response fields through confirm, split and worker briefs. Resource paths pluralize the final entity-name word and replace underscores with hyphens; conventional paths remain drafts requiring API confirmation. List evidence supplies pagination (page, pageSize with size alias), sort allowedKeys, search q and entity-backed filters. Write request fields include rules for required, maxLength, email/phone/url, consent const:true and enums. DELETE has no response fields. + +Data features need apiWiring operationId bindings. Status checks actual HTTP operations in openapi.json and reports contractMissing and contractMatches. Contract matching normalizes field spelling (case, underscores and hyphens) while reporting openapiName, matches structural path parameters and trailing slashes, and reports an unambiguous common API prefix. It checks path-level and operation-level query parameters with local refs and only in:query entries. Request/response comparison resolves local refs, allOf/oneOf/anyOf, arrays and list wrappers (items/data/results), plus JSON, multipart/form-data and application/x-www-form-urlencoded bodies. Type, format, nullable and required mismatches are reported with field-specific reasons. This is static contract evidence, not proof of runtime wiring. + +After confirm never wait: defer recommendations and continue, assume at the end. status reports ERD Mermaid, unit DAG/states, decisions, shared-change requests and backend prerequisites. Missing/deferred API bindings and contractMissing entries remain blockers; unitUpdates record supervisor verification. State: .devup/project-plan.json. Follow /devup for backend commands, latest-first dependencies, exact layout/visual audits, orchestration and zero-drift completion. From 880787eb4d3751a190b0a0b275b3f51224275711 Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 21:09:58 +0900 Subject: [PATCH 085/149] fix(project-plan): require camelCase API contract names (41) --- .../server/project_plan/tests/contracts.rs | 63 ++++++++++++++++--- .../src/server/project_plan/tests/feedback.rs | 2 +- .../src/server/project_plan/wiring.rs | 1 + .../server/project_plan/wiring/contract.rs | 14 ++++- .../src/server/project_plan/wiring/draft.rs | 12 ++-- .../src/server/project_plan/wiring/names.rs | 41 ++++++++++++ .../devup-mcp/src/server/skills/manifest.json | 4 +- crates/devup-mcp/src/server/skills/tests.rs | 8 +++ .../src/server/skills/vespera/SKILL.md | 9 +++ .../src/server/text/devup-project-plan.md | 2 +- 10 files changed, 135 insertions(+), 21 deletions(-) create mode 100644 crates/devup-mcp/src/server/project_plan/wiring/names.rs diff --git a/crates/devup-mcp/src/server/project_plan/tests/contracts.rs b/crates/devup-mcp/src/server/project_plan/tests/contracts.rs index a32e7d68..191b872b 100644 --- a/crates/devup-mcp/src/server/project_plan/tests/contracts.rs +++ b/crates/devup-mcp/src/server/project_plan/tests/contracts.rs @@ -28,11 +28,11 @@ fn list_contracts_draft_queries_only_from_relevant_evidence() { ])); wiring::draft(&mut plan); let queries = array(&plan["endpointContracts"][0]["queryParams"]); - for name in ["page","pageSize","sort","q","category_id","state"] { + for name in ["page","pageSize","sort","q","categoryId","state"] { assert!(queries.iter().any(|query| query["name"] == name), "{name}: {queries:?}"); } let sort = queries.iter().find(|query| query["name"] == "sort").unwrap(); - assert_eq!(sort["allowedKeys"], json!(["title","created_at","views","category_id"])); + assert_eq!(sort["allowedKeys"], json!(["title","createdAt","views","categoryId"])); let mut plain = plan.clone(); plain["features"][0]["scope"] = json!("List"); wiring::draft(&mut plain); @@ -89,17 +89,17 @@ fn report_contracts(contracts: Value, doc: Value) -> Value { } #[test] -fn openapi_matches_normalized_fields_structural_paths_and_common_prefix() { +fn openapi_requires_camel_case_fields_with_structural_paths_and_common_prefix() { let report = report_contracts(json!([ - {"path":"/posts/{id}","method":"GET","responseFields":[{"name":"image_url","type":"text"}]}, - {"path":"/posts","method":"GET","operation":"list","responseFields":[{"name":"image-url","type":"text"}]} + {"path":"/posts/{id}","method":"GET","responseFields":[{"name":"imageUrl","column":"image_url","type":"text"}]}, + {"path":"/posts","method":"GET","operation":"list","responseFields":[{"name":"imageUrl","column":"image_url","type":"text"}]} ]), json!({"paths":{ - "/api/posts/{postId}/":{"get":{"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"ImageUrl":{"type":"string"}}}}}}}}}, + "/api/posts/{postId}/":{"get":{"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"imageUrl":{"type":"string"}}}}}}}}}, "/api/posts":{"get":{"responses":{"200":{"content":{"application/json":{"schema":{"type":"array","items":{"properties":{"imageUrl":{"type":"string"}}}}}}}}}} }})); assert_eq!(report["contractMissing"], json!([]), "{report}"); assert_eq!(report["contractMatches"][0]["prefix"], "/api"); - assert_eq!(report["contractMatches"][0]["fields"][0]["openapiName"], "ImageUrl"); + assert_eq!(report["contractMatches"][0]["fields"][0]["openapiName"], "imageUrl"); } #[test] @@ -128,11 +128,58 @@ fn openapi_reads_composites_wrappers_refs_and_form_bodies() { "responses":{"List":{"content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Post"}}}}}}}}}}); doc["paths"]["/posts"]["post"]["requestBody"]["content"][media] = json!({"schema":{"oneOf":[{"properties":{"title":{"type":"string"}},"required":["title"]}]}}); let report = report_contracts(json!([{"path":"/posts","method":"POST","operation":"list","requestFields":[{"name":"title","type":"text","required":true,"nullable":false}], - "responseFields":[{"name":"id","type":"big_int","required":true,"nullable":false},{"name":"title","type":{"kind":"varchar","length":100},"required":true,"nullable":false},{"name":"published_at","type":"timestamptz","nullable":true}]}]), doc); + "responseFields":[{"name":"id","type":"big_int","required":true,"nullable":false},{"name":"title","type":{"kind":"varchar","length":100},"required":true,"nullable":false},{"name":"publishedAt","column":"published_at","type":"timestamptz","nullable":true}]}]), doc); assert_eq!(report["contractMissing"], json!([]), "{report}"); } } +#[test] +fn drafts_emit_camel_case_names_with_erd_columns() { + let mut plan = plan("Create list sort filter pagination", json!([ + {"name":"image_url","type":"text","nullable":false}, + {"name":"created_at","type":"timestamptz","nullable":false}, + {"name":"category_id","type":"integer","nullable":false} + ])); + plan["facts"]["screens"] = json!([{"nodeId":"1:1","children":[{"type":"TEXT_INPUT","label":"image_url"}]}]); + wiring::draft(&mut plan); + for contract in array(&plan["endpointContracts"]) { + for field in array(&contract["requestFields"]).iter().chain(array(&contract["responseFields"])) { + if field["column"] == "image_url" { assert_eq!(field["name"], "imageUrl"); } + } + assert_eq!(contract["formFields"][0]["name"], "imageUrl"); + assert_eq!(contract["formFields"][0]["column"], "image_url"); + } + let create = array(&plan["endpointContracts"]).iter().find(|item| item["operation"] == "create").unwrap(); + assert_eq!(create["requestFields"][0]["name"], "imageUrl"); + assert_eq!(create["requestFields"][0]["column"], "image_url"); + let list = array(&plan["endpointContracts"]).iter().find(|item| item["operation"] == "list").unwrap(); + assert!(array(&list["queryParams"]).iter().any(|item| item["name"] == "categoryId" && item["column"] == "category_id")); +} + +#[test] +fn openapi_reports_field_casing_violations_in_touched_schemas() { + for spelling in ["image_url", "ImageUrl", "image-url"] { + let report = report_contracts(json!([{"path":"/posts","method":"GET","responseFields":[{"name":"imageUrl"}]}]), + json!({"paths":{"/posts":{"get":{"responses":{"200":{"content":{"application/json":{"schema":{"properties":{spelling:{"type":"string"}}}}}}}}}}})); + let reason = text(&report["contractMissing"][0]["reason"]); + assert!(reason.contains(&format!("casing violation field: expected imageUrl got {spelling}")), "{report}"); + assert!(reason.contains("#[serde(rename_all = \"camelCase\")]"), "{report}"); + assert_eq!(report["contractMatches"][0]["fields"], json!([])); + } + let report = report_contracts(json!([{"path":"/posts","method":"GET"}]), json!({"paths":{"/posts":{"get":{"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"nested":{"properties":{"created_at":{"type":"string"}}}}}}}}}}}}})); + assert!(text(&report["contractMissing"][0]["reason"]).contains("expected createdAt got created_at"), "{report}"); +} + +#[test] +fn openapi_requires_camel_case_query_parameters() { + for (name, passes) in [("page_size", false), ("pageSize", true)] { + let report = report_contracts(json!([{"path":"/posts","method":"GET","queryParams":[{"name":"pageSize"}]}]), + json!({"paths":{"/posts":{"get":{"parameters":[{"in":"query","name":name}]}}}})); + if passes { assert_eq!(report["contractMissing"], json!([]), "{report}"); } + else { assert!(text(&report["contractMissing"][0]["reason"]).contains("casing violation field: expected pageSize got page_size"), "{report}"); } + } +} + #[test] fn openapi_reports_type_format_nullability_and_required_mismatches() { for (field, schema, required, fragment) in [ diff --git a/crates/devup-mcp/src/server/project_plan/tests/feedback.rs b/crates/devup-mcp/src/server/project_plan/tests/feedback.rs index 81c5fd69..77fe8072 100644 --- a/crates/devup-mcp/src/server/project_plan/tests/feedback.rs +++ b/crates/devup-mcp/src/server/project_plan/tests/feedback.rs @@ -27,7 +27,7 @@ async fn screen_data_contracts_survive_confirm_split_and_check_openapi_fields() std::fs::write(dir.path().join("openapi.json"), json!({"paths":{ "/posts":{"get":{"operationId":"listPosts","responses":{"200":{"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Post"}}}}}}}, "post":{"operationId":"createPost","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Post"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Post"}}}}}}}, - },"components":{"schemas":{"Post":{"type":"object","required":["id","title","body","user_id"],"properties":{"id":{"type":"integer"},"title":{"type":"string"},"body":{"type":"string"},"user_id":{"type":"integer"}}}}} + },"components":{"schemas":{"Post":{"type":"object","required":["id","title","body","userId"],"properties":{"id":{"type":"integer"},"title":{"type":"string"},"body":{"type":"string"},"userId":{"type":"integer"}}}}} }).to_string()).unwrap(); request.action = "split".into(); let split = run(&request, &policy).await.unwrap(); diff --git a/crates/devup-mcp/src/server/project_plan/wiring.rs b/crates/devup-mcp/src/server/project_plan/wiring.rs index 56d3b521..4bdec444 100644 --- a/crates/devup-mcp/src/server/project_plan/wiring.rs +++ b/crates/devup-mcp/src/server/project_plan/wiring.rs @@ -2,6 +2,7 @@ use super::*; mod draft; mod contract; mod schema; +mod names; pub(super) use draft::draft; diff --git a/crates/devup-mcp/src/server/project_plan/wiring/contract.rs b/crates/devup-mcp/src/server/project_plan/wiring/contract.rs index ef88a98a..5f691bda 100644 --- a/crates/devup-mcp/src/server/project_plan/wiring/contract.rs +++ b/crates/devup-mcp/src/server/project_plan/wiring/contract.rs @@ -33,9 +33,11 @@ pub(super) fn report(plan: &Value, path: Option<&Path>) -> (Vec, Vec::new(); for (status, body) in operation["responses"].as_object().into_iter().flatten().filter(|(status, _)| status.starts_with('2')) { let _ = status; + reasons.extend(names::schema_violations(&doc, schema::body_schema(&doc, body), 0)); for (name, field) in schema::fields(&doc, schema::body_schema(&doc, body), contract["operation"] == "list", 0) { let entry = response.entry(name).or_default(); entry.schemas.extend(field.schemas); @@ -45,17 +47,23 @@ pub(super) fn report(plan: &Value, path: Option<&Path>) -> (Vec, Vec>(); + for query in &queries { + if let Some(reason) = names::violation(text(&query["name"])) { reasons.push(reason); } + reasons.extend(names::schema_violations(&doc, &query["schema"], 0)); + } for query in array(&contract["queryParams"]) { let name = text(&query["name"]); - if let Some(actual) = queries.iter().find(|actual| draft::normalized(text(&actual["name"])) == draft::normalized(name) || array(&query["aliases"]).contains(&actual["name"])) { - field_matches.push(json!({"kind":"queryParams","name":name,"openapiName":actual["name"]})); + if let Some(reason) = names::violation(name) { reasons.push(reason); } + if let Some(actual) = queries.iter().find(|actual| actual["name"] == name || array(&query["aliases"]).contains(&actual["name"]) && names::violation(text(&actual["name"])).is_none()) { + field_matches.push(json!({"kind":"queryParams","name":name,"matchedName":actual["name"]})); } else { reasons.push(format!("queryParams missing {name}")); } } path_match = json!({"feature":contract["feature"],"method":contract["method"],"path":contract["path"],"openapiPath":path,"prefix":prefix,"fields":field_matches}); diff --git a/crates/devup-mcp/src/server/project_plan/wiring/draft.rs b/crates/devup-mcp/src/server/project_plan/wiring/draft.rs index 8fc77b89..8fe9985a 100644 --- a/crates/devup-mcp/src/server/project_plan/wiring/draft.rs +++ b/crates/devup-mcp/src/server/project_plan/wiring/draft.rs @@ -71,15 +71,15 @@ fn queries(entity: &Value, evidence: &str) -> Vec { let keys = array(&entity["columns"]).iter().filter(|column| { let kind = column["type"].as_str().or_else(|| column["type"]["kind"].as_str()).unwrap_or_default(); contains(kind, &["int", "float", "real", "double", "decimal", "numeric", "date", "time"]) || text(&column["name"]).contains("title") - }).map(|column| column["name"].clone()).collect::>(); - queries.push(json!({"name":"sort","type":"string","allowedKeys":keys})); + }).map(|column| json!({"name":names::camel_case(text(&column["name"])),"column":column["name"]})).collect::>(); + queries.push(json!({"name":"sort","type":"string","allowedKeys":keys.iter().map(|key| key["name"].clone()).collect::>(),"columns":keys})); } if contains(evidence, &["search", "검색"]) { queries.push(json!({"name":"q","type":"string"})); } if contains(evidence, &["filter", "category", "필터", "카테고리", "탭"]) { for column in array(&entity["columns"]) { let name = text(&column["name"]); if column["type"]["kind"] == "enum" || name.ends_with("_id") || contains(name, &["category", "카테고리"]) { - queries.push(json!({"name":name,"type":column["type"],"values":column["type"]["values"]})); + queries.push(json!({"name":names::camel_case(name),"column":name,"type":column["type"],"values":column["type"]["values"]})); } } } @@ -125,7 +125,7 @@ pub(in crate::server::project_plan) fn draft(plan: &mut Value) { let columns = array(&plan["entities"]).iter().filter(|entity| entities.contains(&text(&entity["name"]).to_owned())).flat_map(|entity| array(&entity["columns"])).collect::>(); let form_fields = labels.iter().map(|label| { let column = columns.iter().find(|column| normalized(text(&column["name"])) == normalized(label)); - json!({"label":label,"column":column.map(|column| column["name"].clone()),"status":if column.is_some() {"matched"} else {"unmatched"}}) + json!({"label":label,"name":names::camel_case(column.map(|column| text(&column["name"])).unwrap_or(label)),"column":column.map(|column| column["name"].clone()),"status":if column.is_some() {"matched"} else {"unmatched"}}) }).collect::>(); mappings.push(json!({"feature":feature["id"],"screens":feature["screens"],"candidateEntities":entities,"requiredOperations":operations,"operationEvidence":driven,"formFields":form_fields,"status":"draft"})); for name in entities { @@ -134,9 +134,9 @@ pub(in crate::server::project_plan) fn draft(plan: &mut Value) { let method = match *op {"create" => "POST", "update" => "PATCH", "delete" => "DELETE", _ => "GET"}; let resource = resource(&name); let path = if matches!(*op, "get" | "update" | "delete") {format!("/{resource}/{{id}}") } else {format!("/{resource}")}; - let fields = array(&entity["columns"]).iter().map(|column| json!({"name":column["name"],"type":column["type"],"nullable":column["nullable"],"required":column["nullable"] == false})).collect::>(); + let fields = array(&entity["columns"]).iter().map(|column| json!({"name":names::camel_case(text(&column["name"])),"column":column["name"],"type":column["type"],"nullable":column["nullable"],"required":column["nullable"] == false})).collect::>(); let request_fields = if matches!(*op, "create" | "update") { - array(&entity["columns"]).iter().filter(|column| column["primary_key"].is_null() && column["default"].is_null()).map(|column| json!({"name":column["name"],"type":column["type"],"nullable":column["nullable"],"required":*op == "create" && column["nullable"] == false,"rules":rules(column)})).collect::>() + array(&entity["columns"]).iter().filter(|column| column["primary_key"].is_null() && column["default"].is_null()).map(|column| json!({"name":names::camel_case(text(&column["name"])),"column":column["name"],"type":column["type"],"nullable":column["nullable"],"required":*op == "create" && column["nullable"] == false,"rules":rules(column)})).collect::>() } else {vec![]}; contracts.push(json!({"feature":feature["id"],"entity":name,"operation":op,"method":method,"path":path,"queryParams":if *op == "list" {queries(entity, &evidence)} else {vec![]}, "operationEvidence":driven[op],"formFields":form_fields,"requestFields":request_fields,"responseFields":if *op == "delete" {vec![]} else {fields},"status":"draft","pathEvidence":"entity-name convention; confirm API path before implementation"})); diff --git a/crates/devup-mcp/src/server/project_plan/wiring/names.rs b/crates/devup-mcp/src/server/project_plan/wiring/names.rs new file mode 100644 index 00000000..c2645da2 --- /dev/null +++ b/crates/devup-mcp/src/server/project_plan/wiring/names.rs @@ -0,0 +1,41 @@ +use super::*; + +pub(super) fn camel_case(name: &str) -> String { + let chars = name.chars().collect::>(); + let mut result = String::new(); + let mut uppercase = false; + for (index, ch) in chars.iter().copied().enumerate() { + if ch == '_' || ch == '-' || ch.is_whitespace() { + uppercase = !result.is_empty(); + continue; + } + let boundary = ch.is_uppercase() && index > 0 && + (chars[index - 1].is_lowercase() || chars.get(index + 1).is_some_and(|next| next.is_lowercase())); + if !result.is_empty() && (uppercase || boundary) { result.extend(ch.to_uppercase()); } + else { result.extend(ch.to_lowercase()); } + uppercase = false; + } + result +} + +pub(super) fn violation(name: &str) -> Option { + let expected = camel_case(name); + (name != expected).then(|| format!("casing violation field: expected {expected} got {name}; vespera request/response structs need #[serde(rename_all = \"camelCase\")]") ) +} + +pub(super) fn schema_violations(doc: &Value, schema: &Value, depth: usize) -> Vec { + if depth > 16 { return vec![]; } + let schema = schema::resolve(doc, schema); + let mut reasons = vec![]; + for (name, property) in schema["properties"].as_object().into_iter().flatten() { + if let Some(reason) = violation(name) { reasons.push(reason); } + reasons.extend(schema_violations(doc, property, depth + 1)); + } + for key in ["items", "additionalProperties"] { + if schema[key].is_object() { reasons.extend(schema_violations(doc, &schema[key], depth + 1)); } + } + for key in ["allOf", "oneOf", "anyOf"] { + for part in array(&schema[key]) { reasons.extend(schema_violations(doc, part, depth + 1)); } + } + reasons +} diff --git a/crates/devup-mcp/src/server/skills/manifest.json b/crates/devup-mcp/src/server/skills/manifest.json index b621ce78..d3a12843 100644 --- a/crates/devup-mcp/src/server/skills/manifest.json +++ b/crates/devup-mcp/src/server/skills/manifest.json @@ -129,8 +129,8 @@ "documents": [ { "path": "SKILL.md", - "bytes": 23635, - "sha256": "44db39f98cbb2520ca9a610a59753f39be2e226da7ee11a940234800cf98b27f" + "bytes": 24154, + "sha256": "4076704d04d3059041e445ac00435dd8a1564285a232b0ccd5f96cf385ab7727" } ], "sourceUrl": "https://github.com/dev-five-git/vespera/blob/4030b6c053896fce34477b73616216c86d4b7f85/SKILL.md", diff --git a/crates/devup-mcp/src/server/skills/tests.rs b/crates/devup-mcp/src/server/skills/tests.rs index c97d19a4..74fdecc8 100644 --- a/crates/devup-mcp/src/server/skills/tests.rs +++ b/crates/devup-mcp/src/server/skills/tests.rs @@ -127,6 +127,14 @@ mod tests { /// upstream revision it copied, an `own` one must not pretend to have a /// revision it cannot have, and an external one must carry no bytes at all /// and must say how to install it instead. + #[test] + fn vespera_skill_requires_camel_case_api_fields_and_column_mapping() { + let (_, documents) = EMBEDDED.iter().find(|(name, _)| *name == "vespera").unwrap(); + let (_, text) = documents.iter().find(|(path, _)| *path == ENTRY_DOCUMENT).unwrap(); + assert!(text.contains("API fields and query params are camelCase; ERD columns stay snake_case; drafts carry both (name + column)")); + assert!(text.contains("#[serde(rename_all = \"camelCase\")]")); + } + #[test] fn every_origin_is_well_formed() { let manifest: Manifest = serde_json::from_str(MANIFEST_JSON).unwrap(); diff --git a/crates/devup-mcp/src/server/skills/vespera/SKILL.md b/crates/devup-mcp/src/server/skills/vespera/SKILL.md index 0a3473cb..4a5d92c9 100644 --- a/crates/devup-mcp/src/server/skills/vespera/SKILL.md +++ b/crates/devup-mcp/src/server/skills/vespera/SKILL.md @@ -7,6 +7,15 @@ description: Build APIs with Vespera - FastAPI-like DX for Rust/Axum. Covers rou Vespera = FastAPI DX for Rust. Zero-config OpenAPI 3.1 generation via compile-time macro scanning. +## API Naming Contract (MANDATORY) + +API fields and query params are camelCase; ERD columns stay snake_case; drafts carry both (name + column). +For example, the API field `imageUrl` maps to the ERD column `image_url`. +Request, response and query structs must use `#[serde(rename_all = "camelCase")]`; +use `rename_all = "camelCase"` with `schema_type!` as well. Keep multipart +`#[form_data(field_name = "imageUrl")]` aligned with the same wire name. +Different casing is a contract violation, not an equivalent spelling. + ## Quick Start ```rust diff --git a/crates/devup-mcp/src/server/text/devup-project-plan.md b/crates/devup-mcp/src/server/text/devup-project-plan.md index 4bb977d4..9a16617b 100644 --- a/crates/devup-mcp/src/server/text/devup-project-plan.md +++ b/crates/devup-mcp/src/server/text/devup-project-plan.md @@ -6,6 +6,6 @@ split creates a dependency DAG and .devup/units/.md briefs: supervisor-owned Data mappings infer candidate entities and list/get/create/update/delete operations from feature and screen names/parts. Repeated components (at least three), form controls and dynamic route segments add operationEvidence; formFields records matched and unmatched ERD candidates. Endpoint contract drafts carry method/path, list queryParams and ERD request/response fields through confirm, split and worker briefs. Resource paths pluralize the final entity-name word and replace underscores with hyphens; conventional paths remain drafts requiring API confirmation. List evidence supplies pagination (page, pageSize with size alias), sort allowedKeys, search q and entity-backed filters. Write request fields include rules for required, maxLength, email/phone/url, consent const:true and enums. DELETE has no response fields. -Data features need apiWiring operationId bindings. Status checks actual HTTP operations in openapi.json and reports contractMissing and contractMatches. Contract matching normalizes field spelling (case, underscores and hyphens) while reporting openapiName, matches structural path parameters and trailing slashes, and reports an unambiguous common API prefix. It checks path-level and operation-level query parameters with local refs and only in:query entries. Request/response comparison resolves local refs, allOf/oneOf/anyOf, arrays and list wrappers (items/data/results), plus JSON, multipart/form-data and application/x-www-form-urlencoded bodies. Type, format, nullable and required mismatches are reported with field-specific reasons. This is static contract evidence, not proof of runtime wiring. +API fields and query params are camelCase; ERD columns stay snake_case; drafts carry both (name + column). This includes requestFields, responseFields, formFields and entity-backed filters; rules belong to their camelCase field, and sort allowedKeys use camelCase with separate column mappings. Data features need apiWiring operationId bindings. Status checks actual HTTP operations in openapi.json and reports contractMissing and contractMatches. Field matching is exact, never casing-normalized. Non-camelCase properties and query parameters in touched schemas produce `casing violation field: expected imageUrl got image_url` with a hint to use `#[serde(rename_all = "camelCase")]` on vespera request/response structs. Structural path parameters, trailing slashes and an unambiguous common API prefix are still supported. Query checks resolve path-level and operation-level local refs and only in:query entries. Request/response comparison resolves local refs, allOf/oneOf/anyOf, arrays and list wrappers (items/data/results), plus JSON, multipart/form-data and application/x-www-form-urlencoded bodies. Type, format, nullable and required mismatches are reported with field-specific reasons. This is static contract evidence, not proof of runtime wiring. After confirm never wait: defer recommendations and continue, assume at the end. status reports ERD Mermaid, unit DAG/states, decisions, shared-change requests and backend prerequisites. Missing/deferred API bindings and contractMissing entries remain blockers; unitUpdates record supervisor verification. State: .devup/project-plan.json. Follow /devup for backend commands, latest-first dependencies, exact layout/visual audits, orchestration and zero-drift completion. From 5c4a3a47319418762b303b2f345392c1fac7db3c Mon Sep 17 00:00:00 2001 From: leader Date: Fri, 23 Oct 2026 21:44:05 +0900 Subject: [PATCH 086/149] fix(figma-search): report bad limit and match kind as invalid input --- crates/devup-mcp-figma/src/search.rs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/crates/devup-mcp-figma/src/search.rs b/crates/devup-mcp-figma/src/search.rs index df901f41..3cb25b21 100644 --- a/crates/devup-mcp-figma/src/search.rs +++ b/crates/devup-mcp-figma/src/search.rs @@ -80,7 +80,7 @@ pub fn search_snapshot( } if options.limit == 0 || options.limit > 100 { return Err(DevupError::new( - ErrorCode::DevupFigmaResponseTooLarge, + ErrorCode::DevupInvalidInput, "Search limit must be between 1 and 100.", false, )); @@ -90,7 +90,7 @@ pub fn search_snapshot( "exact" | "normalized" | "fuzzy" ) { return Err(DevupError::new( - ErrorCode::DevupFigmaNodeNotFound, + ErrorCode::DevupInvalidInput, "match must be exact, normalized, or fuzzy.", false, )); @@ -503,6 +503,21 @@ mod tests { assert_eq!(error.code, ErrorCode::DevupInvalidInput); } + #[test] + fn a_bad_limit_or_match_kind_is_invalid_input() { + let (snapshot, target) = typed_snapshot(); + for limit in [0, 101] { + let mut bad = options("a", &[]); + bad.limit = limit; + let error = search_snapshot(&snapshot, &target, &bad).unwrap_err(); + assert_eq!(error.code, ErrorCode::DevupInvalidInput, "limit {limit}"); + } + let mut bad = options("a", &[]); + bad.match_kind = "regex".to_owned(); + let error = search_snapshot(&snapshot, &target, &bad).unwrap_err(); + assert_eq!(error.code, ErrorCode::DevupInvalidInput); + } + #[test] fn fuzzy_matching_is_opt_in() { assert!(match_score("Proofread", "Proofred", "proofred", "normalized").is_none()); From 25de36b1d6f3e7b6aaaf2db596a510906a0ef170 Mon Sep 17 00:00:00 2001 From: leader Date: Sat, 24 Oct 2026 01:14:07 +0900 Subject: [PATCH 087/149] fix(project-plan): return structured stale status recovery (02) --- crates/devup-mcp/src/server/project_plan/mod.rs | 8 ++++++-- crates/devup-mcp/src/server/project_plan/tests.rs | 1 + .../src/server/project_plan/tests/recovery.rs | 14 ++++++++++++++ 3 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 crates/devup-mcp/src/server/project_plan/tests/recovery.rs diff --git a/crates/devup-mcp/src/server/project_plan/mod.rs b/crates/devup-mcp/src/server/project_plan/mod.rs index 9fa80cff..f9a56f2f 100644 --- a/crates/devup-mcp/src/server/project_plan/mod.rs +++ b/crates/devup-mcp/src/server/project_plan/mod.rs @@ -496,11 +496,15 @@ pub(super) async fn run( // status is a read: it may omit planId and reads it from the plan file. let provided = input.plan_id.is_some() || input.action != "status"; if provided && input.plan_id.as_deref() != plan["planId"].as_str() { - return Err(invalid(format!( + let mut error = invalid(format!( "planId is missing or stale; current planId {} state {}", text(&plan["planId"]), text(&plan["state"]) - ))); + )); + error.details = json!({"planId":plan["planId"],"state":plan["state"], + "nextAction":{"tool":"devup_project_plan","arguments":{ + "action":"status","projectRoot":input.project_root,"planId":plan["planId"]}}}); + return Err(error); } let (answers, forward) = if matches!(input.action.as_str(), "draft" | "confirm") { let mut shared = BTreeMap::new(); diff --git a/crates/devup-mcp/src/server/project_plan/tests.rs b/crates/devup-mcp/src/server/project_plan/tests.rs index a6ab26f3..673d85db 100644 --- a/crates/devup-mcp/src/server/project_plan/tests.rs +++ b/crates/devup-mcp/src/server/project_plan/tests.rs @@ -22,6 +22,7 @@ use super::*; mod feedback; mod contracts; mod inference; +mod recovery; fn input(root: &Path, action: &str) -> ProjectPlanInput { serde_json::from_value(json!({"action":action,"projectRoot":root})).unwrap() } diff --git a/crates/devup-mcp/src/server/project_plan/tests/recovery.rs b/crates/devup-mcp/src/server/project_plan/tests/recovery.rs new file mode 100644 index 00000000..d09cc73c --- /dev/null +++ b/crates/devup-mcp/src/server/project_plan/tests/recovery.rs @@ -0,0 +1,14 @@ +use super::*; + +#[tokio::test] +async fn stale_status_id_returns_current_plan_and_executable_recovery() { + let dir = TestDir::new(); + let (mut request, policy, draft) = drafted(dir.path(), false).await; + request.action = "status".into(); + request.plan_id = Some("stale".into()); + let error = run(&request, &policy).await.unwrap_err(); + assert_eq!(error.details["planId"], draft["planId"]); + assert_eq!(error.details["state"], "draft"); + let recovery: ProjectPlanInput = serde_json::from_value(error.details["nextAction"]["arguments"].clone()).unwrap(); + assert_eq!(run(&recovery, &policy).await.unwrap()["planId"], draft["planId"]); +} From 0b271c2950f6dcd77d80c0a7984e7ad49e99bd38 Mon Sep 17 00:00:00 2001 From: leader Date: Sat, 24 Oct 2026 01:15:08 +0900 Subject: [PATCH 088/149] fix(figma-search): accept omitted type-list queries and classify bad limits (13) --- .../src/server/handlers/devup_figma_search.rs | 2 +- .../src/server/text/devup-figma-search.md | 2 +- crates/devup-mcp/src/server/tools.rs | 1 + crates/devup-mcp/tests/figma_search_scope.rs | 14 +++++++++++++- 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/crates/devup-mcp/src/server/handlers/devup_figma_search.rs b/crates/devup-mcp/src/server/handlers/devup_figma_search.rs index 6bff8160..60514ec5 100644 --- a/crates/devup-mcp/src/server/handlers/devup_figma_search.rs +++ b/crates/devup-mcp/src/server/handlers/devup_figma_search.rs @@ -16,7 +16,7 @@ impl DevupServer { // an out-of-range limit is owed. if !(1..=MATCH_CEILING).contains(&input.limit) { return Err(to_mcp_error(DevupError::new( - ErrorCode::DevupFigmaResponseTooLarge, + ErrorCode::DevupInvalidInput, "The search limit must be between 1 and 100 inclusive.", false, ))); diff --git a/crates/devup-mcp/src/server/text/devup-figma-search.md b/crates/devup-mcp/src/server/text/devup-figma-search.md index f3c6bc27..614c8fb1 100644 --- a/crates/devup-mcp/src/server/text/devup-figma-search.md +++ b/crates/devup-mcp/src/server/text/devup-figma-search.md @@ -1 +1 @@ -Search Figma pages, sections, frames, and components by name to locate the target before devup_figma_export. A node-id in the URL scopes the search to that node and everything under it; a URL without one searches the whole file. url is optional while exactly one Devup Bridge plugin is attached: omitted, the whole file that plugin has open is searched, and figma-bridge://current?node-id= scopes it. The answer says which of the two it did under `scope`, and `limit` is the number of matches returned. \ No newline at end of file +Search Figma pages, sections, frames, and components by name to locate the target before devup_figma_export. With nodeTypes supplied, omit query or pass an empty query or "*" to list nodes by type. A node-id in the URL scopes the search to that node and everything under it; a URL without one searches the whole file. url is optional while exactly one Devup Bridge plugin is attached: omitted, the whole file that plugin has open is searched, and figma-bridge://current?node-id= scopes it. The answer says which of the two it did under `scope`, and `limit` is the number of matches returned. diff --git a/crates/devup-mcp/src/server/tools.rs b/crates/devup-mcp/src/server/tools.rs index a1558876..eec1374d 100644 --- a/crates/devup-mcp/src/server/tools.rs +++ b/crates/devup-mcp/src/server/tools.rs @@ -299,6 +299,7 @@ pub struct FigmaSearchInput { /// plugin is attached: omitted, the whole file that plugin has open. #[serde(default)] pub url: Option, + #[serde(default)] pub query: String, #[serde(default)] pub node_types: Vec, diff --git a/crates/devup-mcp/tests/figma_search_scope.rs b/crates/devup-mcp/tests/figma_search_scope.rs index 04fc15b1..d64188f0 100644 --- a/crates/devup-mcp/tests/figma_search_scope.rs +++ b/crates/devup-mcp/tests/figma_search_scope.rs @@ -424,9 +424,21 @@ async fn an_out_of_range_limit_is_refused_before_collecting() -> anyhow::Result< ) .await; - common::tool_error(refused?); + let error = common::tool_error(refused?); + assert!(error.contains("DEVUP_INVALID_INPUT"), "{error}"); assert_eq!(upstream.calls.load(Ordering::SeqCst), 0); client.cancel().await?; task.await??; Ok(()) } + +#[tokio::test] +async fn an_omitted_query_lists_sections_by_type() -> anyhow::Result<()> { + let (output, _) = search(json!({ + "url": "https://www.figma.com/design/FileKey123/Girok?node-id=0-1", + "nodeTypes": ["SECTION"] + })) + .await?; + assert_eq!(ids(&output, "matches"), ["4279:7810"]); + Ok(()) +} From 7a375768afa9e69db78b897c47c64a22cbe0e960 Mon Sep 17 00:00:00 2001 From: leader Date: Sat, 24 Oct 2026 01:17:04 +0900 Subject: [PATCH 089/149] fix(project-plan): preserve empty-prefix header capacity (07) --- crates/devup-mcp/src/server/project_plan/rounds.rs | 4 +--- crates/devup-mcp/src/server/project_plan/tests/recovery.rs | 7 +++++++ crates/devup-mcp/src/server/screen_plan/state.rs | 5 ++++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/crates/devup-mcp/src/server/project_plan/rounds.rs b/crates/devup-mcp/src/server/project_plan/rounds.rs index d1677f0c..c49c5878 100644 --- a/crates/devup-mcp/src/server/project_plan/rounds.rs +++ b/crates/devup-mcp/src/server/project_plan/rounds.rs @@ -51,9 +51,7 @@ fn spec_candidates(root: &Path) -> Vec { fn ask(id: &str, prompt: String, options: &[&str]) -> Value { question( id.into(), - screen_plan::header("", id) - .trim_start_matches(": ") - .to_owned(), + screen_plan::header("", id), prompt, options .iter() diff --git a/crates/devup-mcp/src/server/project_plan/tests/recovery.rs b/crates/devup-mcp/src/server/project_plan/tests/recovery.rs index d09cc73c..ca971220 100644 --- a/crates/devup-mcp/src/server/project_plan/tests/recovery.rs +++ b/crates/devup-mcp/src/server/project_plan/tests/recovery.rs @@ -1,5 +1,12 @@ use super::*; +#[test] +fn empty_prefix_header_preserves_thirty_character_capacity() { + let name = "123456789012345678901234567890"; + assert_eq!(screen_plan::header("", name), name); + assert_eq!(screen_plan::header("Page", "Home"), "Page: Home"); +} + #[tokio::test] async fn stale_status_id_returns_current_plan_and_executable_recovery() { let dir = TestDir::new(); diff --git a/crates/devup-mcp/src/server/screen_plan/state.rs b/crates/devup-mcp/src/server/screen_plan/state.rs index 577d4bce..cfbd1e33 100644 --- a/crates/devup-mcp/src/server/screen_plan/state.rs +++ b/crates/devup-mcp/src/server/screen_plan/state.rs @@ -816,6 +816,9 @@ fn truncate(value: &str, limit: usize) -> String { } pub(super) fn header(prefix: &str, name: &str) -> String { + if prefix.is_empty() { + return truncate(name, HEADER_LIMIT); + } truncate(&format!("{prefix}: {name}"), HEADER_LIMIT) } @@ -884,4 +887,4 @@ pub(super) fn question( /// The questions as they go to the question tool: no resolution table. pub(super) fn ask_shape(questions: &[Value]) -> Vec { ask_shape_with(questions, questions) -} \ No newline at end of file +} From a3c1adf2952f4424a3af3f3436b31a03ec2b335e Mon Sep 17 00:00:00 2001 From: leader Date: Sat, 24 Oct 2026 01:17:59 +0900 Subject: [PATCH 090/149] fix(codegen): resolve component export roots to their collected sets (66) --- .../server/projection/component_projection.rs | 53 +++++++++++-------- .../projection/feedback_codegen_tests.rs | 29 ++++++++++ 2 files changed, 61 insertions(+), 21 deletions(-) diff --git a/crates/devup-mcp/src/server/projection/component_projection.rs b/crates/devup-mcp/src/server/projection/component_projection.rs index d0840b39..4d66de6c 100644 --- a/crates/devup-mcp/src/server/projection/component_projection.rs +++ b/crates/devup-mcp/src/server/projection/component_projection.rs @@ -58,45 +58,56 @@ fn generate_collected_component( node_id: &str, options: &CodegenOptions, ) -> Result { - if !options.inline_instances { + let snapshot = &payload.snapshot; + let root = snapshot.nodes.get(node_id); + let master = root.filter(|node| node.node_type == "INSTANCE").and_then(|node| { + let view = node.typed_view(); + view.string("mainComponentId") + .or_else(|| view.string("componentId")) + .or_else(|| view.string("mainComponent")) + .or_else(|| view.value("mainComponent").and_then(|value| value.get("id")).and_then(Value::as_str)) + }); + let target = master.and_then(|id| snapshot.nodes.get(id)).or(root); + let target = target.and_then(|node| { + let parent = node.typed_view().string("parentId") + .and_then(|id| snapshot.nodes.get(id)) + .filter(|parent| parent.node_type == "COMPONENT_SET"); + parent.or(Some(node)) + }); + if !options.inline_instances + && !target.is_some_and(|node| matches!(node.node_type.as_str(), "COMPONENT" | "COMPONENT_SET")) + { let mut pending = vec![node_id]; let mut visited = std::collections::BTreeSet::new(); while let Some(id) = pending.pop() { if !visited.insert(id) { continue; } - let Some(node) = payload.snapshot.nodes.get(id) else { + let Some(node) = snapshot.nodes.get(id) else { continue; }; let view = node.typed_view(); if view.bool("visible") == Some(false) { continue; } - if node.node_type == "INSTANCE" { - let component_id = view - .string("mainComponentId") + if node.node_type == "INSTANCE" + && let Some(component_id) = view.string("mainComponentId") .or_else(|| view.string("componentId")) .or_else(|| view.string("mainComponent")) - .or_else(|| { - view.value("mainComponent") - .and_then(|value| value.get("id")) - .and_then(Value::as_str) - }); - if let Some(component_id) = component_id - && !payload.snapshot.nodes.contains_key(component_id) - { - return Err(DevupError::with_details( - ErrorCode::DevupFigmaNodeNotFound, - "componentTsx needs a component master outside this snapshot. Re-collect the component master, or request tsx to inline the instance.", - false, - json!({"componentId":component_id,"instanceNodeId":id}), - )); - } + .or_else(|| view.value("mainComponent").and_then(|value| value.get("id")).and_then(Value::as_str)) + && !snapshot.nodes.contains_key(component_id) + { + return Err(DevupError::with_details( + ErrorCode::DevupFigmaNodeNotFound, + "componentTsx needs a component master outside this snapshot. Re-collect the component master, or request tsx to inline the instance.", + false, + json!({"componentId":component_id,"instanceNodeId":id}), + )); } pending.extend(view.child_ids()); } } - generate_component(&payload.snapshot, node_id, options) + generate_component(snapshot, target.map_or(node_id, |node| node.id.as_str()), options) } fn component_definition_actions( diff --git a/crates/devup-mcp/src/server/projection/feedback_codegen_tests.rs b/crates/devup-mcp/src/server/projection/feedback_codegen_tests.rs index 25354d6c..f31e1699 100644 --- a/crates/devup-mcp/src/server/projection/feedback_codegen_tests.rs +++ b/crates/devup-mcp/src/server/projection/feedback_codegen_tests.rs @@ -68,6 +68,35 @@ fn feedback32_component_payload() -> CollectedPayload { data } +#[tokio::test] +async fn feedback66_component_roots_export_despite_uncaptured_nested_master() { + for root in ["set", "default", "instance"] { + let mut data = feedback32_component_payload(); + data.snapshot.nodes.insert("nested".into(), serde_json::from_value(json!({ + "id":"nested","type":"INSTANCE","fields":{"name":"Icon","mainComponentId":"outside"} + })).unwrap()); + data.snapshot.nodes.get_mut("default").unwrap().fields.insert("childrenIds".into(), json!(["label","nested"])); + data.snapshot.nodes.insert("instance".into(), serde_json::from_value(json!({ + "id":"instance","type":"INSTANCE","fields":{"name":"Button","mainComponentId":"default"} + })).unwrap()); + data.target.node_id = Some(root.into()); + data.snapshot.roots = vec![root.into()]; + let directory = std::env::temp_dir().join(format!("feedback66-{}", rand::random::())); + std::fs::create_dir_all(&directory).unwrap(); + let path = directory.join("Button.tsx"); + let mut op = operation(&["componentTsx", "sourceMap"]); + if let PendingOperation::Export { output_paths, .. } = &mut op { + output_paths.insert("componentTsx".into(), path.to_string_lossy().into_owned()); + } + let result = project(data, op).await.unwrap(); + let code = result["componentTsx"].as_str().expect("component definition"); + assert!(code.contains("export function Button"), "{root}: {result}"); + assert_eq!(std::fs::read_to_string(path).unwrap(), code); + assert!(result["failures"].as_array().unwrap().is_empty(), "{root}: {result}"); + std::fs::remove_dir_all(directory).unwrap(); + } +} + #[tokio::test] async fn feedback32_component_set_has_no_canvas_coverage_failure() { let result = project(feedback32_component_payload(), operation(&["tsx"])).await.unwrap(); From dec29476df329289ab526ba10fd6df9d9886e2f2 Mon Sep 17 00:00:00 2001 From: leader Date: Sat, 24 Oct 2026 01:18:51 +0900 Subject: [PATCH 091/149] fix(figma-search): load pages without switching and disclose candidate cuts (50) --- crates/devup-mcp-figma/src/collector/w5_tests.rs | 8 ++++++++ crates/devup-mcp-figma/src/scripts/search.js | 9 +++++---- .../src/text/collector-w5_tests_TREE-ae8d8ae7.md | 1 + .../src/server/handlers/devup_figma_search.rs | 6 +----- .../devup-mcp/src/server/mod_parts/acquisition.rs | 8 +++++++- crates/devup-mcp/tests/figma_search_scope.rs | 13 +++++++++++++ plugin/dist/code.js | 2 +- 7 files changed, 36 insertions(+), 11 deletions(-) diff --git a/crates/devup-mcp-figma/src/collector/w5_tests.rs b/crates/devup-mcp-figma/src/collector/w5_tests.rs index 3004e2ee..5a78ad1b 100644 --- a/crates/devup-mcp-figma/src/collector/w5_tests.rs +++ b/crates/devup-mcp-figma/src/collector/w5_tests.rs @@ -246,6 +246,14 @@ mod w5_tests { assert_eq!(ids, BTreeSet::from(["1:1", "2:1"])); } + #[test] + fn feedback50_page_search_loads_descendants_without_changing_the_viewer_page() { + let setup = format!("{TREE}\nlet loaded = false;\npage.loadAsync = async () => {{ loaded = true; }};\npage.findAll = predicate => {{ if (!loaded) return []; return page.children.flatMap(child => [child, ...child.findAll(predicate)]).filter(predicate); }};\nfigma.getNodeByIdAsync = async () => page;\nfigma.setCurrentPageAsync = async () => {{ throw new Error('search changed viewer page'); }};"); + let output = run_script(include_str!("../scripts/search.js"), &setup, + "__DEVUP_SEARCH__", json!({"query":"", "nodeTypes":["SECTION"], "matchKind":"normalized", "limit":5})); + assert!(output["nodes"].as_array().unwrap().iter().any(|node| node["id"] == "2:1")); + } + fn page_output(ids: &[String]) -> UpstreamResult { UpstreamResult { raw: json!({ diff --git a/crates/devup-mcp-figma/src/scripts/search.js b/crates/devup-mcp-figma/src/scripts/search.js index 0b71c5ea..e4d4209c 100644 --- a/crates/devup-mcp-figma/src/scripts/search.js +++ b/crates/devup-mcp-figma/src/scripts/search.js @@ -7,7 +7,7 @@ if (!root) throw new Error("DEVUP_NODE_NOT_FOUND"); let page = root; while (page && page.type !== "PAGE") page = page.parent; if (!page || page.type !== "PAGE") throw new Error("DEVUP_PAGE_NOT_FOUND"); -await figma.setCurrentPageAsync(page); +await page.loadAsync(); const options = "__DEVUP_SEARCH__"; const defaultTypes = ["PAGE", "SECTION", "FRAME", "COMPONENT_SET", "COMPONENT"]; @@ -56,7 +56,7 @@ function score(name) { const descendants = root === page ? page.findAll(() => true) : "findAll" in root ? root.findAll(() => true) : []; -const candidates = [root, ...descendants] +const ranked = [root, ...descendants] .filter((node) => allowedTypes.has(node.type) && typeof node.name === "string") .map((node) => ({ node, score: score(node.name) })) .filter((entry) => entry.score !== null) @@ -64,8 +64,8 @@ const candidates = [root, ...descendants] right.score - left.score || left.node.name.localeCompare(right.node.name) || left.node.id.localeCompare(right.node.id), - ) - .slice(0, options.limit); + ); +const candidates = ranked.slice(0, options.limit); const included = new Map([[page.id, page]]); // Keep the anchor and its ancestry even when nothing matches. Consumers @@ -87,6 +87,7 @@ return { type: node.type, fields: { name: node.name, + searchCandidatesTruncated: node === root && ranked.length > candidates.length, parentId: node.parent && node.parent.type !== "DOCUMENT" ? node.parent.id : null, childrenIds: "children" in node ? node.children.map((child) => child.id) : [], }, diff --git a/crates/devup-mcp-figma/src/text/collector-w5_tests_TREE-ae8d8ae7.md b/crates/devup-mcp-figma/src/text/collector-w5_tests_TREE-ae8d8ae7.md index 398bd04b..31aadc36 100644 --- a/crates/devup-mcp-figma/src/text/collector-w5_tests_TREE-ae8d8ae7.md +++ b/crates/devup-mcp-figma/src/text/collector-w5_tests_TREE-ae8d8ae7.md @@ -1,6 +1,7 @@ const doc = {id:'0:0', type:'DOCUMENT'}; const page = {id:'1:1', type:'PAGE', name:'Page', parent:doc, children:[]}; +page.loadAsync = async () => {}; function node(id, type, name, parent) { const n = {id, type, name, parent, children:[], x:0, y:0, width:390, height:844, visible:true}; n.findAll = predicate => n.children.flatMap(c => [c, ...c.findAll(predicate)]).filter(predicate); diff --git a/crates/devup-mcp/src/server/handlers/devup_figma_search.rs b/crates/devup-mcp/src/server/handlers/devup_figma_search.rs index 60514ec5..012336e9 100644 --- a/crates/devup-mcp/src/server/handlers/devup_figma_search.rs +++ b/crates/devup-mcp/src/server/handlers/devup_figma_search.rs @@ -42,11 +42,7 @@ impl DevupServer { } else { CollectionScope::File }; - let projected_limit = if scope.node_id.is_some() { - MATCH_CEILING - } else { - input.limit - }; + let projected_limit = MATCH_CEILING; let mut request = CollectionRequest::new(target, collection_scope); request.search = Some(SearchReadOptions { query: input.query.clone(), diff --git a/crates/devup-mcp/src/server/mod_parts/acquisition.rs b/crates/devup-mcp/src/server/mod_parts/acquisition.rs index 71238680..7c86630a 100644 --- a/crates/devup-mcp/src/server/mod_parts/acquisition.rs +++ b/crates/devup-mcp/src/server/mod_parts/acquisition.rs @@ -911,6 +911,11 @@ fn apply_search_scope( return response; }; let scanned = matches.len(); + let upstream_truncated = snapshot.nodes.values().any(|node| { + node.fields.get("searchCandidatesTruncated") == Some(&json!(true)) + }); + let candidates_truncated = upstream_truncated || scanned > scope.limit || scanned == MATCH_CEILING; + response["hasMore"] = json!(candidates_truncated); let node_id = match scope.node_id.as_deref() { None => { let mut kept = matches; @@ -922,6 +927,7 @@ fn apply_search_scope( "nodeId": Value::Null, "collectionScope": collection_scope, "scannedMatches": scanned, + "candidatesTruncated": candidates_truncated, "note": "The URL carried no node-id, so the whole file was searched. \ Put the node you care about in the URL to search only inside it.", }); @@ -945,7 +951,7 @@ fn apply_search_scope( "matchedInScope": found, "returned": inside.len(), "excludedOutOfScope": outside.len(), - "candidatesTruncated": found > inside.len(), + "candidatesTruncated": candidates_truncated || found > inside.len(), "note": "The URL carried a node-id, so only matches inside that node are \ returned. Remove the node-id to search elsewhere in the file.", }); diff --git a/crates/devup-mcp/tests/figma_search_scope.rs b/crates/devup-mcp/tests/figma_search_scope.rs index d64188f0..400a94e5 100644 --- a/crates/devup-mcp/tests/figma_search_scope.rs +++ b/crates/devup-mcp/tests/figma_search_scope.rs @@ -442,3 +442,16 @@ async fn an_omitted_query_lists_sections_by_type() -> anyhow::Result<()> { assert_eq!(ids(&output, "matches"), ["4279:7810"]); Ok(()) } + +#[tokio::test] +async fn a_file_search_reports_a_truncated_candidate_list() -> anyhow::Result<()> { + let (output, _) = search(json!({ + "url": "https://www.figma.com/design/FileKey123/Girok", + "query": "Loading", "limit": 1 + })) + .await?; + assert_eq!(output["count"], 1); + assert_eq!(output["scope"]["candidatesTruncated"], true, "{output}"); + assert_eq!(output["hasMore"], true); + Ok(()) +} diff --git a/plugin/dist/code.js b/plugin/dist/code.js index b4cf6639..acc5ad31 100644 --- a/plugin/dist/code.js +++ b/plugin/dist/code.js @@ -1 +1 @@ -(()=>{"use strict";let e={assets:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=e.asset;function r(e){let t=[0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,0xe49b69c1,0xefbe4786,0xfc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x6ca6351,0x14292967,0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2],r=64*Math.ceil((e.length+9)/64),n=new Uint8Array(r);n.set(e),n[e.length]=128;let i=8*e.length;for(let e=0;e<8;e+=1)n[r-1-e]=255&Math.floor(i/2**(8*e));let a=[0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19],o=(e,t)=>e>>>t|e<<32-t;for(let e=0;e>>3,n=o(r[e-2],17)^o(r[e-2],19)^r[e-2]>>>10;r[e]=r[e-16]+t+r[e-7]+n>>>0}let[i,l,s,d,f,c,u,p]=a;for(let e=0;e<64;e+=1){let n=p+(o(f,6)^o(f,11)^o(f,25))+(f&c^~f&u)+t[e]+r[e]>>>0,a=(o(i,2)^o(i,13)^o(i,22))+(i&l^i&s^l&s)>>>0;p=u,u=c,c=f,f=d+n>>>0,d=s,s=l,l=i,i=n+a>>>0}for(let[e,t]of[i,l,s,d,f,c,u,p].entries())a[e]=a[e]+t>>>0}return a.map(e=>e.toString(16).padStart(8,"0")).join("")}async function n(e){try{let t=await figma.getNodeByIdAsync(e);if(t)return t}catch(r){if(!e.startsWith("I")||!e.includes(";"))throw r;try{return await t()}catch(e){throw Error(`${r.message||r}; nested lookup fallback: ${e.message||e}`)}}if(!e.startsWith("I")||!e.includes(";"))return null;return t();async function t(){let t=await figma.getNodeByIdAsync(e.slice(1).split(";")[0]),r=t?[t]:[],n=new Set;for(;r.length;){let t=r.pop();if(t.id===e)return t;if(!n.has(t.id)){if(n.add(t.id),n.size>1e4)throw Error("nested lookup exceeds 10000 nodes");Array.isArray(t.children)&&r.push(...t.children)}}throw Error(`nested node ${e} not found in its instance`)}}async function i(e,t,r=85e3,n=()=>{}){let a,o=Date.now();try{let i=await Promise.race([e.exportAsync(t),new Promise((e,t)=>{a=setTimeout(()=>t(Error(`DEVUP_ASSET_TIMEOUT: native export exceeded ${r} ms`)),r)})]);return n("native-export",Date.now()-o),i}catch(i){if(n("native-export",Date.now()-o,String(i.message||i).slice(0,500)),"SVG_STRING"!==t.format)throw i;let r=Date.now();try{let t=function(e){let t=e=>"number"==typeof e&&Number.isFinite(e),r=(e,t)=>{throw Error(`geometry fallback unsupported: ${e.type||"unknown"} ${e.id||""}: ${t}`)},n=0,i=0;function a(e,i,a){(void 0!==a&&!Array.isArray(a)||void 0===a&&Array.isArray(i)&&i.length)&&r(e,"unresolved paint requires native export");let o=Array.isArray(a)?a.filter(e=>!1!==e.visible):[];return o.length?(Array.isArray(i)&&i.length||r(e,"paint has no rendered geometry"),o.map(a=>{("SOLID"!==a.type||a.blendMode&&"NORMAL"!==a.blendMode)&&r(e,"paint type or blending requires native export");let o=a.color;o&&[o.r,o.g,o.b].every(e=>t(e)&&e>=0&&e<=1)||r(e,"invalid solid color");let l=void 0===a.opacity?1:a.opacity;return(!t(l)||l<0||l>1)&&r(e,"invalid paint opacity"),["NONZERO","EVENODD"].map(t=>{let a=i.filter(e=>e.windingRule===t);if(!a.length)return"";for(let t of a)"string"==typeof t.data&&t.data.trim()||r(e,"empty path"),n++;return`"']/g,e=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[e])}" fill="rgb(${100*o.r}%,${100*o.g}%,${100*o.b}%)" fill-opacity="${l}" fill-rule="${"EVENODD"===t?"evenodd":"nonzero"}"/>`}).join("")}).join("")):""}let o=e.absoluteRenderBounds,l=e.absoluteTransform;(!o||![o.x,o.y,o.width,o.height].every(t)||o.width<=0||o.height<=0)&&r(e,"missing render bounds"),(!Array.isArray(l)||2!==l.length||!l.every(e=>Array.isArray(e)&&3===e.length&&e.every(t))||0!==l[0][1]||0!==l[1][0]||l[0][0]<=0||l[1][1]<=0)&&r(e,"root rotation, reflection or shear requires native export");let s=(o.x-l[0][2])/l[0][0],d=(o.y-l[1][2])/l[1][1],f=o.width/l[0][0],c=o.height/l[1][1],u=function e(n,o){if(++i>1e4&&r(n,"subtree exceeds 10000 nodes"),n.isMask&&r(n,"mask requires native export"),!1===n.visible||0===n.opacity)return"";n.clipsContent&&r(n,"clipping requires native export"),Array.isArray(n.effects)&&n.effects.some(e=>!1!==e.visible)&&r(n,"effect requires native export"),n.blendMode&&!["NORMAL","PASS_THROUGH"].includes(n.blendMode)&&r(n,"layer blending requires native export");let l=void 0===n.opacity?1:n.opacity;(!t(l)||l<0||l>1)&&r(n,"invalid layer opacity");let s="";if(!o){let e=n.relativeTransform;Array.isArray(e)&&2===e.length&&e.every(e=>Array.isArray(e)&&3===e.length&&e.every(t))||r(n,"missing relative transform"),s=` transform="matrix(${e[0][0]} ${e[1][0]} ${e[0][1]} ${e[1][1]} ${e[0][2]} ${e[1][2]})"`}let d="";if(["GROUP","FRAME","INSTANCE","COMPONENT"].includes(n.type))(Array.isArray(n.fills)&&n.fills.some(e=>!1!==e.visible)||Array.isArray(n.strokes)&&n.strokes.some(e=>!1!==e.visible))&&r(n,"container paint requires native export"),Array.isArray(n.children)||r(n,"missing children"),d=(!0===n.itemReverseZIndex?[...n.children].reverse():n.children).map(t=>e(t,!1)).join("");else{for(let e of(["VECTOR","BOOLEAN_OPERATION","RECTANGLE","ELLIPSE","POLYGON","STAR","LINE"].includes(n.type)||r(n,"node type requires native export"),Array.isArray(n.strokes)&&n.strokes.some(e=>!1!==e.visible)&&"CENTER"!==n.strokeAlign&&r(n,"non-center stroke alignment requires native export"),Array.isArray(n.dashPattern)&&n.dashPattern.length&&r(n,"dashed stroke requires native export"),[n.fillGeometry,n.strokeGeometry]))void 0!==e&&(!Array.isArray(e)||e.some(e=>!["NONZERO","EVENODD"].includes(e.windingRule)))&&r(n,"unknown winding rule");d=a(n,n.fillGeometry,n.fills)+a(n,n.strokeGeometry,n.strokes)}return`${d}`}(e,!0);return n||r(e,"no painted geometry"),`${u}`}(e);return n("geometry-fallback",Date.now()-r),t}catch(e){throw Error(`native export: ${i.message||i}; ${e.message||e}`)}}finally{clearTimeout(a)}}let a=Date.now(),o="lookup",l="unknown",s=Math.min(6e5,Math.max(1e3,Number(t.timeoutMs)||9e4)),d={method:"native",stages:[]};function f(e,t,r){d.stages.push({stage:e,elapsedMs:t}),r&&(d.nativeError=r),"geometry-fallback"===e&&(d.method="geometry")}async function c(e){let t;try{return await Promise.race([e(),new Promise((e,r)=>{t=setTimeout(()=>r(Error(`DEVUP_ASSET_TIMEOUT: ${o} exceeded ${s} ms asset budget`)),Math.max(0,s-(Date.now()-a)))})])}finally{clearTimeout(t)}}function u(e){let r=e&&"string"==typeof e.message?e.message:String(e),n="string"==typeof figma.editorType?` (editor: ${figma.editorType})`:"";return`${o} failed after ${Date.now()-a} ms${n} (node: ${t.nodeId}, type: ${l}): ${r}`.slice(0,500)}function p(e,r){return e&&d.renderInfo&&(d.renderInfo.attemptedRepresentation=d.renderInfo.representation,d.renderInfo.representation="not-exported",d.renderInfo.renderedAs="not-exported",d.renderInfo.effectsBakedIntoAsset=!1,d.renderInfo.fidelity="unverified"),{kind:"devupAssetExport",fileKey:figma.fileKey||"",version:t.version,assetId:t.assetId,nodeId:t.nodeId,field:t.field,imageHash:t.imageHash,format:t.format,scale:t.scale,renderMode:h,usageAspectRatio:t.usageAspectRatio??null,maxWidth:t.maxWidth??null,srcset:!0===t.srcset,visibleScopeNodeId:t.visibleScopeNodeId??null,status:"failed",byteLength:null,sha256:null,errorCode:e,exportDiagnostics:d,...r?{errorDetail:r}:{}}}let g="string"==typeof t.field&&t.field.startsWith("$original-image/fills/"),h=void 0===t.renderMode?g?"fill":["svg","pdf"].includes(t.format)?"node":"original-source":t.renderMode,y=["auto","original-source"].includes(h)?"auto"===h?"node":"fill":h;function m(e,r){let n=e=>"number"==typeof e&&Number.isFinite(e)&&e>0,i=Array.isArray(e.effects)?e.effects:null,a=[],o=e=>[...e].slice(0,128).join(""),l=["string"==typeof e.name?o(e.name):"layer"],s=new Set,d=e;for(let t=e.parent;t&&a.length<32&&!s.has(t);t=t.parent)s.add(t),l.length<4&&"string"==typeof t.name&&t.name.length&&!["PAGE","DOCUMENT"].includes(t.type)&&l.push(o(t.name)),a.push({type:t.type||"unknown",clipsContent:t.clipsContent??null,isMask:t.isMask??null,effects:Array.isArray(t.effects)?t.effects:null,opacity:t.opacity??null,blendMode:t.blendMode??null,visible:t.visible??null,siblingMaskPresent:Array.isArray(t.children)?t.children.some(e=>e!==d&&!0===e.isMask):null}),d=t;return{sourceKind:r?"image-fill":t.imageHash?"image-node":"vector-node",renderedAs:"fill"===y?"raw-fill":"node-render",representation:"fill"===y?"original-fill":"native-node",layerHierarchy:l.reverse(),scope:"fill"===y?"selected-paint-original-bytes":"exact-node-subtree",exportSettings:"node"===y?{contentsOnly:!0,useAbsoluteBounds:!1}:null,requestedScale:t.scale,scaleSemantics:"fill"===y?"intrinsic-original-bytes-no-resize":["png","jpg"].includes(t.format)?"native-node-SCALE-constraint":"not-applied-to-vector-document",nodeSize:n(e.width)&&n(e.height)?{width:e.width,height:e.height,aspectRatio:e.width/e.height}:null,outputSize:null,dimensionEvidence:"unknown-until-byte-inspection",aspectRatioComparison:"unknown",scaleMode:r?r.scaleMode??null:null,imageTransform:r?r.imageTransform??null:null,visibleSourceRatio:null,visibleSourceRatioEvidence:"not-computed",effects:i,extraFills:Array.isArray(e.fills)?Math.max(0,e.fills.filter(e=>!1!==e.visible).length-!!r):null,effectsBakedIntoAsset:"node"===y&&["png","jpg"].includes(t.format)&&null!==i,bakedEffects:"node"===y&&["png","jpg"].includes(t.format)&&i?i.filter(e=>!1!==e.visible):[],instanceOverride:"unknown",instanceOverrideEvidence:"no-component-paint-comparison",sourceProperties:{scaleMode:r?r.scaleMode??null:null,imageTransform:r?r.imageTransform??null:null,rotation:r?r.rotation??null:null,filters:r?r.filters??null:null,opacity:r?r.opacity??null:null,effects:i,blurEffects:i?i.filter(e=>!1!==e.visible&&String(e.type).includes("BLUR")):null,isMask:e.isMask??null,clipsContent:e.clipsContent??null,ancestors:a},unappliedProperties:"fill"===y?["scaleMode","imageTransform","rotation","filters","paintOpacity","nodeGeometry","nodeTransforms","nodeEffectsIncludingBlur","nodeAndAncestorMasks","ancestorClipsAndEffects"]:[],warnings:"fill"===y?["original-fill-properties-unapplied","aspect-ratio-unknown"]:["ancestor-canvas-appearance-unverified","aspect-ratio-unknown"],fidelity:"unverified"}}function b(e,t){let r=0;for(let n=e;n&&r++<32;n=n.parent){if("INSTANCE"===n.type&&Array.isArray(n.overrides)&&n.overrides.find(t=>t.id===e.id&&Array.isArray(t.overriddenFields)&&t.overriddenFields.includes("fills"))){t.instanceOverride=!0,t.instanceOverrideEvidence={kind:"figma-instance-overrides",instanceId:n.id,nodeId:e.id,overriddenFields:["fills"]};return}if(["PAGE","DOCUMENT"].includes(n.type))break}}try{let e;if(!["original-source","fill","node","visible","auto"].includes(h))return p("DEVUP_ASSET_RENDER_MODE_UNSUPPORTED");if(!g&&"fill"===h&&(!String(t.field).startsWith("fills/")||!["png","jpg","webp"].includes(t.format)||1!==t.scale))return p("DEVUP_ASSET_FILL_SETTINGS_UNSUPPORTED","Original fill requires fills/N, png or jpg matching its codec, and scale 1; no resizing or transcoding.");if((g||["fill","original-source"].includes(h))&&"bridge"!==t.transport)return p(g?"DEVUP_ORIGINAL_IMAGE_REQUIRES_BRIDGE":"DEVUP_ASSET_FILL_REQUIRES_BRIDGE");let x=await c(()=>n(t.nodeId));if(f("lookup",Date.now()-a),l=x&&x.type||"unknown",!x||["node","visible"].includes(h)&&"function"!=typeof x.exportAsync)return p("DEVUP_ASSET_UNSUPPORTED_BY_UPSTREAM",u(Error(x?"node has no exportAsync":"node not found")));let E=null;if(g||"string"==typeof t.field&&t.field.startsWith("fills/")){let e=Number(t.field.slice(g?22:6)),r="fills"in x&&Array.isArray(x.fills)?x.fills:[],n=Number.isInteger(e)?r[e]:null,i=n&&"IMAGE"===n.type?n.imageHash||n.imageRef:null;if(!n||i!==t.imageHash)return p("DEVUP_ASSET_SOURCE_CHANGED");E=n}else if("node"!==t.field)return p("DEVUP_ASSET_FIELD_UNSUPPORTED");if("original-source"===h&&(!E||!t.imageHash))return p("DEVUP_ORIGINAL_SOURCE_UNAVAILABLE","No IMAGE fill source exists for this selection. Choose an IMAGE fill or explicitly renderMode node; vector assets should retain SVG.");let A=x;if("visible"===h){let e=t.visibleScopeNodeId;if(!e)return p("DEVUP_VISIBLE_CONTEXT_UNAVAILABLE","Recollect the parent frame and provide visibleScopeNodeId.");let r=new Set;for(;A&&A.id!==e&&!r.has(A)&&r.size<64;)r.add(A),A=A.parent;if(!A||A.id!==e||["PAGE","DOCUMENT"].includes(A.type)||"function"!=typeof A.exportAsync)return p("DEVUP_VISIBLE_SCOPE_UNSUPPORTED","The selected capture scope is not an exportable ancestor of the requested node.")}if(d.renderInfo=m(x,E),"visible"===h){let e=d.renderInfo;e.representation="native-visible",e.renderedAs="visible-scoped-capture",e.captureNodeId=A.id,e.captureBox={width:A.width,height:A.height,aspectRatio:A.width/A.height},e.contextScope="selected-ancestor-subtree",e.captureEffects=Array.isArray(A.effects)?A.effects:null,e.requestedNodeEffects=e.effects,e.effects=e.captureEffects,e.sourceProperties.captureEffects=e.captureEffects,e.visibleContext={requestedNodeId:x.id,captureNodeId:A.id,included:["selected-scope-children","selected-scope-clipping","selected-scope-masks","selected-scope-rounding"],outsideScope:"unknown; ancestors, page backdrop and external layers are not included",canvasReferenceCompared:!1},e.warnings.push("visible-scoped-capture","outside-capture-context-unverified")}b(x,d.renderInfo);let I=null;if(E)try{let r=figma.getImageByHash(t.imageHash),n=r&&await Promise.race([r.getSizeAsync(),new Promise(t=>{e=setTimeout(()=>t(null),250)})]);I=n&&n.width>0&&n.height>0&&Number.isFinite(n.width)&&Number.isFinite(n.height)?{...n,aspectRatio:n.width/n.height}:null}catch(e){}finally{clearTimeout(e)}let S=null;if("auto"===h){let e=function(e,r,n){let i,a=[],o=(e,t)=>{e||a.push(t)},l=e=>Array.isArray(e)&&2===e.length&&e.every(e=>Array.isArray(e)&&3===e.length&&e.every(Number.isFinite))&&1===e[0][0]&&1===e[1][1]&&0===e[0][1]&&0===e[1][0]&&Number.isInteger(e[0][2])&&Number.isInteger(e[1][2]);for(let a of(o("bridge"===t.transport,"raw-requires-bridge"),o(1===t.scale&&["png","jpg"].includes(t.format),"raw-requires-unscaled-raster"),o(!!r&&!0===r.visible&&1===r.opacity&&"NORMAL"===r.blendMode,"paint-compositing-or-unknown"),o(r&&"FILL"===r.scaleMode&&Array.isArray(i=r.imageTransform)&&2===i.length&&i.every((e,t)=>Array.isArray(e)&&3===e.length&&e.every((e,r)=>e===[[1,0,0],[0,1,0]][t][r]))&&0===r.rotation,"paint-transform-or-unknown"),o(r&&r.filters&&Object.values(r.filters).every(e=>0===e),"image-filters-or-unknown"),o(Array.isArray(e.fills)&&1===e.fills.length,"multiple-fills-or-unknown"),o("RECTANGLE"===e.type&&0===e.cornerRadius&&Array.isArray(e.strokes)&&0===e.strokes.length&&(!e.children||0===e.children.length),"node-geometry-or-unknown"),o(Array.isArray(e.effects)&&0===e.effects.length&&!1===e.isMask&&(void 0===e.clipsContent||!1===e.clipsContent)&&1===e.opacity&&!0===e.visible&&"NORMAL"===e.blendMode,"node-effects-mask-compositing-or-unknown"),o(l(e.absoluteTransform)&&l(e.relativeTransform),"node-transform-or-unknown"),o(n.sourceProperties.ancestors.length>0&&n.sourceProperties.ancestors.some(e=>["PAGE","DOCUMENT"].includes(e.type)),"ancestor-chain-incomplete"),o(n.sourceProperties.ancestors.every(e=>!1===e.siblingMaskPresent),"ancestor-sibling-mask-or-unknown"),n.sourceProperties.ancestors.filter(e=>!["PAGE","DOCUMENT"].includes(e.type))))o(!1===a.clipsContent&&!1===a.isMask&&!1===a.siblingMaskPresent&&Array.isArray(a.effects)&&0===a.effects.length&&1===a.opacity&&!0===a.visible&&["NORMAL","PASS_THROUGH"].includes(a.blendMode),"ancestor-dependency-or-unknown");return{eligible:0===a.length,reasons:[...new Set(a)],evidence:"explicit-plugin-properties; intrinsic-dimensions-and-codec-required"}}(x,E,d.renderInfo);if(e.eligible)try{let r=figma.getImageByHash(t.imageHash),n=I;n&&n.width===x.width&&n.height===x.height?(S=await c(()=>r.getBytesAsync()),(137===S[0]&&80===S[1]&&78===S[2]&&71===S[3]?"png":255===S[0]&&216===S[1]&&255===S[2]?"jpg":"unknown")!==t.format&&e.reasons.push("source-codec-mismatch-or-unknown")):e.reasons.push("intrinsic-dimensions-mismatch-or-unknown")}catch(t){e.reasons.push("source-inspection-unavailable")}e.eligible=0===e.reasons.length,y=e.eligible?"fill":"node",d.renderInfo=m(x,E),b(x,d.renderInfo),d.renderInfo.autoDecision={...e,selected:y}}if(d.renderInfo.sourceSize=I,d.renderInfo.sourceSizeEvidence=I?"Figma-image-getSizeAsync":"unknown",g||"fill"===y){d.method="original-fill",o="reading the original image";let e=figma.getImageByHash(t.imageHash);if(!e)return p("DEVUP_ORIGINAL_IMAGE_NOT_FOUND");let n=S||await c(()=>e.getBytesAsync());if(0===n.length||n.length>0x5000000)return p("DEVUP_ASSET_RESPONSE_TOO_LARGE",`${n.length} bytes; one answer carries 1 to 83886080`);let i=e=>e.every((e,t)=>n[t]===e),a=i([137,80,78,71,13,10,26,10])?"image/png":i([255,216,255])?"image/jpeg":i([71,73,70,56])?"image/gif":i([82,73,70,70])&&87===n[8]&&69===n[9]&&66===n[10]&&80===n[11]?"image/webp":null;if(!a)return p("DEVUP_ORIGINAL_IMAGE_CODEC_UNSUPPORTED");if(d.renderInfo.originalMimeType=a,!g&&"fill"===h&&"webp"!==t.format&&a!==({png:"image/png",jpg:"image/jpeg"})[t.format])return p("DEVUP_ASSET_FILL_CODEC_MISMATCH",`Original fill codec is ${a}, but ${t.format} was requested; choose the matching format or renderMode node. No transcoding was performed.`);if(!g)return{...p(null),status:"exported",mimeType:a,byteLength:n.length,sha256:r(n),data:figma.base64Encode(n)};let l=await c(()=>e.getSizeAsync());return{...p(null),status:"exported",kind:"devupOriginalImage",representation:"original-image-v1",format:null,scale:null,mimeType:a,width:l.width,height:l.height,byteLength:n.length,sha256:r(n),data:figma.base64Encode(n)}}let w="webp"===t.format?"PNG":String(t.format||"").toUpperCase();if(!["PNG","JPG","SVG","PDF"].includes(w))return p("DEVUP_ASSET_FORMAT_UNSUPPORTED");let v=Number(t.scale??2);if(!Number.isFinite(v)||v<=0||v>4)return p("DEVUP_ASSET_SCALE_INVALID");let N="SVG"===w,O={format:N?"SVG_STRING":w,contentsOnly:!0,useAbsoluteBounds:!1};("PNG"===w||"JPG"===w)&&(O.constraint={type:"SCALE",value:v}),o="export";let T=Math.max(1,s-(Date.now()-a)),P=N?Math.max(1,T-Math.min(5e3,Math.floor(T/5))):T,_=Date.now(),M=await c(()=>"visible"===h?A.exportAsync(O):E?x.exportAsync(O):i(x,O,P,f));if(E&&f("native",Date.now()-_),"geometry"===d.method&&(d.renderInfo.representation="geometry-fallback",d.renderInfo.renderedAs="geometry-fallback",d.renderInfo.effectsBakedIntoAsset=!1,d.renderInfo.warnings.push("geometry-fallback-not-native-render")),Date.now()-a>=s)throw Error(`DEVUP_ASSET_TIMEOUT: export exceeded ${s} ms asset budget`);o="encode";let C=Date.now(),k=N&&"string"==typeof M?M:null,R=null===k?M instanceof Uint8Array?M:new Uint8Array(M):function(e){let t=[];for(let r=0;r=55296&&n<=56319){let t=r+1=56320&&t<=57343?(n=65536+(n-55296<<10)+(t-56320),r+=1):n=65533}else n>=56320&&n<=57343&&(n=65533);n<128?t.push(n):n<2048?t.push(192|n>>6,128|63&n):n<65536?t.push(224|n>>12,128|n>>6&63,128|63&n):t.push(240|n>>18,128|n>>12&63,128|n>>6&63,128|63&n)}return new Uint8Array(t)}(k);if(0===R.length||R.length>0x5000000)return p("DEVUP_ASSET_RESPONSE_TOO_LARGE",`${w} at scale ${v} is ${R.length} bytes; one answer carries 1 to 83886080`);let D=r(R);f("encode",Date.now()-C);let L=figma.io&&"function"==typeof figma.io.write,$="PNG"===w&&R.length>786432;if(L&&(null!==k&&R.length>12288||$))return{kind:"devupAssetExport",fileKey:figma.fileKey||"",version:t.version,assetId:t.assetId,nodeId:t.nodeId,field:t.field,imageHash:t.imageHash,format:t.format,scale:v,renderMode:h,usageAspectRatio:t.usageAspectRatio??null,maxWidth:t.maxWidth??null,srcset:!0===t.srcset,visibleScopeNodeId:t.visibleScopeNodeId??null,status:"chunked",byteLength:R.length,sha256:D,cursor:{nextOffset:0,maxChunkBytes:12288},errorCode:null,exportDiagnostics:d};return L&&figma.io.write(`devup-asset-${t.assetId.replace(/[^A-Za-z0-9_-]/g,"_")}.${w.toLowerCase()}`,R),{kind:"devupAssetExport",fileKey:figma.fileKey||"",version:t.version,assetId:t.assetId,nodeId:t.nodeId,field:t.field,imageHash:t.imageHash,format:t.format,scale:v,renderMode:h,usageAspectRatio:t.usageAspectRatio??null,maxWidth:t.maxWidth??null,srcset:!0===t.srcset,visibleScopeNodeId:t.visibleScopeNodeId??null,status:"exported",byteLength:R.length,sha256:D,mimeType:L?null===k?null:"image/svg+xml":({PNG:"image/png",JPG:"image/jpeg",SVG:"image/svg+xml",PDF:"application/pdf"})[w],...!L&&null===k?{data:figma.base64Encode(R)}:{},text:k,errorCode:null,exportDiagnostics:d}}catch(e){return p(String(e.message||e).includes("DEVUP_ASSET_TIMEOUT:")?"DEVUP_ASSET_TIMEOUT":g||["fill","original-source"].includes(h)?"DEVUP_ORIGINAL_IMAGE_READ_FAILED":"DEVUP_ASSET_EXPORT_FAILED",u(e))}},bridgeContext:async function(e){let t=figma.currentPage,r=t.selection,n=e=>({id:e.id,name:String(e.name).slice(0,256),type:e.type});return{kind:"devup-bridge-context",currentPage:n(t),selection:r.slice(0,20).map(n),selectionCount:r.length,zoom:Number.isFinite(figma.viewport?.zoom)?figma.viewport.zoom:null,observedAt:Date.now(),lastKnownSelection:globalThis.__devupLastSelection??null,lastKnownPage:globalThis.__devupLastSelectionPage??null}},explore:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=await figma.getNodeByIdAsync(e.nodeId);if(!t)throw Error("DEVUP_NODE_NOT_FOUND");let r=e.explore,n=Math.max(20,Math.min(400,Number.isInteger(r.projectionLimit)?r.projectionLimit:200)),i=Math.max(0,Math.min(500,Number.isInteger(r.textPreviewLimit)?r.textPreviewLimit:160));function a(e){return"string"==typeof e?e.slice(0,240):""}let o="PAGE"===t.type,l=t,s="SECTION"===t.type?t:null;for(;!o&&l.parent&&"PAGE"!==l.parent.type;)l=l.parent,s||"SECTION"!==l.type||(s=l);let d="PAGE"===t.type?t:l.parent;if(!d||"PAGE"!==d.type)throw Error("DEVUP_PAGE_NOT_FOUND");function f(e){let t=e.absoluteBoundingBox||{x:"number"==typeof e.x?e.x:0,y:"number"==typeof e.y?e.y:0,width:"number"==typeof e.width?e.width:0,height:"number"==typeof e.height?e.height:0};return[t.x,t.y,t.width,t.height].every(Number.isFinite)?{x:t.x,y:t.y,width:t.width,height:t.height}:null}function c(e){let t=f(e);return t&&["FRAME","COMPONENT","INSTANCE","COMPONENT_SET"].includes(e.type)&&t.width>=240&&t.width<=1800&&t.height>=300&&t.height<=2e3&&t.width/Math.max(1,t.height)>=.25&&t.width/Math.max(1,t.height)<=2.5}function u(e){let t=[],r=e;for(;r&&"DOCUMENT"!==r.type&&t.length<12;){let e=a(r.name);e&&t.push(e),r=r.parent}return t.reverse()}await figma.setCurrentPageAsync(d);let p=o?d:!c(t)&&s?s:l,g=f(p)||f(t);if(!g)throw Error("DEVUP_NODE_BOUNDS_UNAVAILABLE");let h="children"in d?d.children:[],y=h.map((e,t)=>({node:e,pageChildIndex:t,bounds:f(e)})).filter(e=>e.bounds).filter(e=>{var t;return!!o||e.node.id===p.id||e.bounds.y>=g.y-240&&e.bounds.y<=g.y+12e3&&(t=e.bounds,Math.min(t.x+t.width,g.x+g.width)-Math.max(t.x,g.x)>0)}).sort((e,t)=>e.bounds.y-t.bounds.y||e.bounds.x-t.bounds.x||e.node.id.localeCompare(t.node.id)),m="SECTION"===p.type?y.filter(e=>e.node.id===p.id):y.slice(0,n),b=new Map(m.map(e=>[e.node.id,e]));b.has(l.id)||b.set(l.id,{node:l,pageChildIndex:-1,bounds:f(l)}),b.has(p.id)||b.set(p.id,{node:p,pageChildIndex:-1,bounds:f(p)}),b.has(t.id)||b.set(t.id,{node:t,pageChildIndex:-1,bounds:f(t)});let x=o?null:t.parent;for(;x&&"PAGE"!==x.type&&(b.has(x.id)||b.set(x.id,{node:x,pageChildIndex:-1,bounds:f(x)}),x.id!==p.id);)x=x.parent;if("children"in p)for(let e of p.children.slice(0,n))b.has(e.id)||b.set(e.id,{node:e,pageChildIndex:-1,bounds:f(e)});let E="SECTION"===p.type||o,A=E&&"children"in p&&p.children.length>n;if(E&&"children"in p){let e=8*n,t=p.children.filter(e=>!c(e)).map(e=>({node:e,ancestors:[]})),r=0;for(;t.length&&r!b.has(e.id));if(b.size+t.length>n+2){A=!0;continue}for(let e of t)b.set(e.id,{node:e,pageChildIndex:-1,bounds:f(e)});continue}if("children"in e)for(let r of e.children)o&&["FRAME","COMPONENT","INSTANCE","COMPONENT_SET"].includes(e.type)||t.push({node:r,ancestors:[...i,e]})}A||=t.length>0}let I=[...b.values()].filter(e=>e.bounds).slice(0,n+2).map(({node:e,pageChildIndex:t,bounds:r})=>{let n;return{id:e.id,type:e.type,fields:{name:a(e.name),parentId:e.parent&&"DOCUMENT"!==e.parent.type?e.parent.id:null,childrenIds:[],x:r.x,y:r.y,width:r.width,height:r.height,childCount:"children"in e?e.children.length:0,textPreview:"",...o||null===(n=function(e){let t=[e],r=0,n=null;for(;t.length&&r++<400;){let e=t.shift();if("visible"in e&&!1===e.visible)continue;let r="children"in e?e.children.filter(e=>!("visible"in e)||!1!==e.visible):[];if(/header|nav|gnb/i.test(e.name||"")&&r.length>=2&&"layoutMode"in e&&"HORIZONTAL"===e.layoutMode){let t=r.map(f);t.every(Boolean)&&(n=Math.max(n||0,t.reduce((e,t)=>e+t.width,0)+(Number(e.itemSpacing)||0)*(t.length-1)))}t.push(...r.slice(0,80))}return n}(e))?{}:{navigationContentWidth:n},pageChildIndex:t>=0?t:null,visible:!("visible"in e)||!1!==e.visible,breadcrumb:u(e)},extra:{},fieldErrors:{}}}),S=I.reduce((e,t)=>{let r=t.fields;return e?{x:Math.min(e.x,r.x),y:Math.min(e.y,r.y),right:Math.max(e.right,r.x+r.width),bottom:Math.max(e.bottom,r.y+r.height)}:{x:r.x,y:r.y,right:r.x+r.width,bottom:r.y+r.height}},null)||{x:0,y:0,right:0,bottom:0},w="SECTION"!==p.type&&y.length>m.length||b.size>n+2||A,v={id:d.id,type:d.type,fields:{name:a(d.name),parentId:null,childrenIds:[],x:S.x,y:S.y,width:S.right-S.x,height:S.bottom-S.y,childCount:h.length,textPreview:"",projectionTruncated:w,visible:!0,breadcrumb:u(d),pageChildIndex:null},extra:{},fieldErrors:{}},N={fileKey:figma.fileKey||"",version:null,rootIds:[d.id],nodes:[v,...I.filter(e=>e.id!==d.id)],diagnostics:[]},O=new Set([d.id,l.id,p.id,t.id]);for(;JSON.stringify(N).length>14e3;){let e=N.nodes.length-1;for(;e>=0&&O.has(N.nodes[e].id);)e-=1;if(e<0)break;N.nodes.splice(e,1),v.fields.projectionTruncated=!0}if(JSON.stringify(N).length>14e3&&(N.nodes=N.nodes.filter(e=>O.has(e.id)).map(e=>({...e,fields:{...e.fields,name:a(e.fields.name).slice(0,80),textPreview:"",breadcrumb:e.fields.breadcrumb.slice(-4).map(e=>e.slice(0,80)),projectionTruncated:e.id===d.id||e.fields.projectionTruncated}}))),JSON.stringify(N).length>14e3)throw Error("DEVUP_EXPLORE_PROJECTION_TOO_LARGE");function T(e){let t=0;for(let r of e){let e=r.codePointAt(0);t+=e<128?1:e<2048?2:e<65536?3:4}return t}if(o){for(;T(JSON.stringify(N))>19456&&N.nodes.length>1;)N.nodes.pop(),v.fields.projectionTruncated=!0;if(T(JSON.stringify(N))>19456)throw Error("DEVUP_EXPLORE_PROJECTION_TOO_LARGE")}let P=14e3-JSON.stringify(N).length;if(o&&(P=Math.min(P,19456-T(JSON.stringify(N)))),i>0&&P>0){let e=N.nodes.filter(e=>b.get(e.id)&&e.id!==d.id),t=e.length;for(let r of e){let e=Math.floor(P/t);if(t-=1,e<=0)continue;let n="",a=0;for(let t of function(e){if(0===i)return"";let t=[],r=[e],n=0;for(;r.length&&n<80&&t.join(" ").lengthe)break;n+=t,a+=r}r.fields.textPreview=n,P-=a}}return N},fastSnapshot:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=e.rootIds;if(!Array.isArray(t)||0===t.length)throw Error("DEVUP_ROOTS_INVALID");let r=await Promise.all(t.map(e=>figma.getNodeByIdAsync(e)));if(r.some(e=>!e))throw Error("DEVUP_NODE_NOT_FOUND");if(1===r.length&&"SECTION"===r[0].type)throw Error("DEVUP_TARGET_IS_SECTION");let n=["mobile","tablet","desktop"],i=e=>{let t=String(e.name||"").trim().toLowerCase(),r=n.indexOf(t);if(r>=0)return r;let i=t.match(/^(\d+)\s*[~–]\s*(\d+)$/);if(!i)return -1;let a=Number(i[1]),o=Number(i[2]);return Number.isSafeInteger(a)&&Number.isSafeInteger(o)&&a>0&&o>=a&&o<=0xffffffff?1:-1};if(1===r.length&&i(r[0])>=0){let e=r[0].parent;if(e&&"SECTION"===e.type&&"children"in e){let t=e.children.filter(e=>e.id===r[0].id||i(e)>=0).filter(e=>!1!==e.visible);t.length>1&&(r.length=0,r.push(...t))}}if(1===t.length){let e=[],t=new Set(r.map(e=>e.id)),n=[],i=e=>{if("reactions"in e&&Array.isArray(e.reactions)){for(let r of e.reactions)if(r&&r.trigger&&"AFTER_TIMEOUT"===r.trigger.type)for(let e of r.actions||[])e&&"NODE"===e.type&&e.transition&&"SMART_ANIMATE"===e.transition.type&&"string"==typeof e.destinationId&&!t.has(e.destinationId)&&(t.add(e.destinationId),n.push(e.destinationId));if("children"in e)for(let t of e.children)i(t)}};for(let e of r)i(e);for(;n.length>0;){let t=n.shift(),r=await figma.getNodeByIdAsync(t);r&&"DOCUMENT"!==r.type&&"PAGE"!==r.type&&(e.push(r),i(r))}r.push(...e)}let a=e.nodeId,o=["absoluteBoundingBox","absoluteRenderBounds","arcData","backgroundStyleId","blendMode","bottomLeftRadius","bottomRightRadius","boundVariables","characters","clipsContent","componentProperties","componentPropertyDefinitions","componentPropertyReferences","constraints","cornerRadius","counterAxisAlignItems","dashPattern","defaultVariant","effectStyleId","effects","fillStyleId","fills","fontName","fontSize","gridColumnAnchorIndex","gridColumnCount","gridColumnGap","gridColumnSizes","gridColumnSpan","gridRowAnchorIndex","gridRowCount","gridRowGap","gridRowSizes","gridRowSpan","gridStyleId","height","inferredAutoLayout","isAsset","isMask","itemSpacing","layoutGrow","layoutMode","layoutWrap","layoutPositioning","layoutSizingHorizontal","layoutSizingVertical","letterSpacing","lineHeight","maxHeight","maxLines","maxWidth","minHeight","minWidth","name","opacity","overflowDirection","paddingBottom","paddingLeft","paddingRight","paddingTop","primaryAxisAlignItems","reactions","rotation","strokeAlign","strokeBottomWeight","strokeLeftWeight","strokeRightWeight","strokeStyleId","strokeTopWeight","strokeWeight","strokes","targetAspectRatio","textAlignHorizontal","textAlignVertical","textAutoResize","textCase","textDecoration","textStyleId","textTruncation","topLeftRadius","topRightRadius","variantProperties","visible","width","x","y"],l=["fontName","fontWeight","fontSize","textDecoration","textCase","lineHeight","letterSpacing","fills","textStyleId","fillStyleId","listOptions","indentation","hyperlink"],s=e.snapshot,d=Math.max(0,Math.floor(Number(s.offset)||0)),f=Math.max(8192,Math.floor(Number(s.maxEnvelopeBytes)||19456)),c=Math.min(s.maxEnvelopeBytes?f-1024:18e3,Math.max(4096,Math.floor(Number(s.maxPayloadBytes)||15e3))),u=new Set(["backgroundStyleId","effectStyleId","fillStyleId","gridStyleId","strokeStyleId","textStyleId"]),p=new Set(["maxWidth","maxHeight","absoluteRenderBounds"]),g=new Map([["rotation",0],["cornerRadius",0],["isAsset",!1],["isMask",!1],["clipsContent",!1],["blendMode","PASS_THROUGH"],["strokeAlign","INSIDE"],["textCase","ORIGINAL"],["textDecoration","NONE"],["textAlignHorizontal","LEFT"],["textAlignVertical","TOP"],["counterAxisAlignItems","MIN"],["primaryAxisAlignItems","MIN"],["gridColumnCount",0],["gridRowCount",0],["gridColumnGap",0],["gridRowGap",0],["gridColumnAnchorIndex",-1],["gridRowAnchorIndex",-1],["gridColumnSpan",1],["gridRowSpan",1]]),h=new Set(["start","end","characters","fontWeight","textStyleId","fillStyleId","listOptions","indentation","hyperlink"]),y=new Set(["parent","children","consumers"]);function m(e,t=!1,r=new WeakSet,n=0){let i;if(null===e||["string","number","boolean"].includes(typeof e))return e;if(void 0===e)return{$undefined:!0};if("bigint"==typeof e)return{$bigint:e.toString()};if(["function","symbol"].includes(typeof e))return{$unsupported:typeof e};if(n>12)return{$truncated:"max-depth"};if("object"==typeof e&&"parent"in e&&"string"==typeof e.id&&"string"==typeof e.type)return{$nodeId:e.id,$nodeType:e.type};if(Array.isArray(e))return e.map(e=>m(e,t,r,n+1));if(ArrayBuffer.isView(e))return{$binary:e.constructor.name,byteLength:e.byteLength};if(e instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:e.byteLength};if(r.has(e))return{$circular:!0};if(r.add(e),t){let t=new Set(Object.keys(e)),r=e;for(;r&&r!==Object.prototype;){for(let e of Object.getOwnPropertyNames(r))t.add(e);r=Object.getPrototypeOf(r)}i=[...t].sort().filter(e=>!e.startsWith("_")&&!y.has(e))}else i=Object.keys(e).sort();let a={};for(let o of i)try{let i=m(e[o],t,r,n+1);i&&"function"===i.$unsupported||(a[o]=i)}catch(e){a[o]=t?{$error:"unavailable"}:{$error:String(e&&e.message?e.message:e)}}return r.delete(e),a}let b=new Map;async function x(e,t,r){if("INSTANCE"!==e.type||"function"!=typeof e.getMainComponentAsync)return;b.has(e.id)||b.set(e.id,(async()=>{try{let t=await e.getMainComponentAsync();return{fields:{mainComponentId:t?t.id:null,mainComponentName:t?t.name:null,mainComponentSetId:t&&t.parent&&"COMPONENT_SET"===t.parent.type?t.parent.id:null}}}catch(e){return{error:String(e&&e.message?e.message:e)}}})());let n=await b.get(e.id);n.error?r.mainComponentId=n.error:Object.assign(t,n.fields)}async function E(e){let t={},r={};e.parent&&(t.parentId=e.parent.id),e.parent&&("PAGE"===e.parent.type||"SECTION"===e.parent.type||"COMPONENT_SET"===e.parent.type)&&(t.parentType=e.parent.type,"SECTION"===e.parent.type&&(t.parentName=e.parent.name));let n="children"in e?e.children.map(e=>e.id):[];for(let i of(n.length>0&&(t.childrenIds=n),o)){let n;if("componentPropertyDefinitions"!==i||"COMPONENT"!==e.type||!e.parent||"COMPONENT_SET"!==e.parent.type)try{if(!(i in e)){"overflowDirection"===i&&["FRAME","COMPONENT","INSTANCE","COMPONENT_SET"].includes(e.type)&&(t[i]=null);continue}if(n=e[i],"function"==typeof n)continue;let r=m(n);if("overflowDirection"===i&&null==n){t[i]=null;continue}(null===r?!p.has(i):Array.isArray(r)?0===r.length:"object"==typeof r?0===Object.keys(r).length:!!(""===r&&u.has(i))||g.has(i)&&g.get(i)===r)||(t[i]=r)}catch(e){r[i]=String(e&&e.message?e.message:e)}}if("TEXT"===e.type&&"function"==typeof e.getStyledTextSegments)try{let r=m(e.getStyledTextSegments(l));if(1===r.length){let e=r[0];for(let t of Object.keys(e))h.has(t)||delete e[t]}r.length>0&&(t.styledTextSegments=r)}catch(e){r.styledTextSegments=String(e&&e.message?e.message:e)}await x(e,t,r);let i={id:e.id,type:e.type,fields:t};return Object.keys(r).length>0&&(i.fieldErrors=r),i}let A=[],I=[...r],S=new Set;for(let e=0;e=A.length&&A.length>0)throw Error("DEVUP_SNAPSHOT_RANGE_INVALID");function w(e){let t=0;for(let r=0;r=55296&&n<=56319&&r+1({id:e,styleType:t})).sort((e,t)=>e.id.localeCompare(t.id));async function a(e){try{let t=await figma.variables.getVariableByIdAsync(e);return t?{kind:"variable",value:m(t,!0),collectionId:t.variableCollectionId,aliasIds:Object.values(t.valuesByMode||{}).filter(e=>e&&"VARIABLE_ALIAS"===e.type&&"string"==typeof e.id&&e.id).map(e=>e.id)}:{kind:"unresolved",value:{id:e,kind:"variable",reason:"notFoundOrUnavailable"}}}catch(t){return{kind:"unresolved",value:{id:e,kind:"variable",reason:"notFoundOrUnavailable",error:String(t&&t.message||t).slice(0,300)}}}}let o=await Promise.all([...n.map(a),...i.map(async({id:e,styleType:t})=>{try{let r=await figma.getStyleByIdAsync(e);if(!r)return{kind:"unresolved",value:{id:e,kind:"style",reason:"notFoundOrUnavailable"}};return{kind:"style",value:{...m(r,!0),styleType:t,value:m("PAINT"===t?r.paints:"EFFECT"===t?r.effects:"GRID"===t?r.layoutGrids:r,!0)}}}catch(t){return{kind:"unresolved",value:{id:e,kind:"style",reason:"notFoundOrUnavailable",error:String(t&&t.message||t).slice(0,300)}}}})]),l=new Set(n),s=o.flatMap(e=>e.aliasIds||[]);for(;s.length>0;){let e=[...new Set(s)].filter(e=>!l.has(e)).sort();for(let t of e)l.add(t);let t=await Promise.all(e.map(a));o.push(...t),s=t.flatMap(e=>e.aliasIds||[])}let d=[...new Set(o.filter(e=>"variable"===e.kind&&e.collectionId).map(e=>e.collectionId))].sort(),f=(await Promise.all(d.map(async e=>{try{let t=await figma.variables.getVariableCollectionByIdAsync(e);return t?m(t,!0):null}catch(e){return null}}))).filter(e=>null!==e),c=o.filter(e=>"variable"===e.kind).map(e=>e.value),u=o.filter(e=>"style"===e.kind).map(e=>e.value),p=o.filter(e=>"unresolved"===e.kind).map(e=>e.value);return{collections:f,variables:c,styles:u,usedRemoteVariables:c.filter(e=>!0===e.remote),usedVariableIds:n,usedStyleIds:i.map(e=>e.id),localComplete:!1,usedRemoteComplete:0===p.length,unresolved:p,$variableRefCount:n.length,$styleRefCount:i.length}}async function N(e){let t=[],r=2;for(let n=d;ne)break;t.push(i),r+=a}return{pageNodes:t,packedBytes:r}}let O=c-1024,T=null;for(let e=0;e<5;e+=1){let{pageNodes:e,packedBytes:t}=await N(O);if(0===e.length)throw Error("DEVUP_SNAPSHOT_RANGE_INVALID");let n=function(e,t){let n=Math.min(A.length,d+e.length),{$variableRefCount:i,$styleRefCount:o,...l}=t,s=[...e,{id:"__DEVUP_SNAPSHOT_CURSOR__",type:"DEVUP_INTERNAL",fields:{offset:d,nextOffset:n,complete:n>=A.length,totalNodes:A.length},extra:{},fieldErrors:{}}],f={kind:"devupFastSnapshotEnvelope",schemaVersion:1,source:{fileKey:figma.fileKey||"",rootId:a},snapshot:{fileKey:figma.fileKey||"",version:null,rootIds:r.map(e=>e.id),nodes:s,diagnostics:[]},resources:l,integrity:{nodeCount:s.length,variableRefCount:i,styleRefCount:o,utf8Bytes:0}},c=0;for(let e=0;e<8&&(c=w(JSON.stringify(f)),f.integrity.utf8Bytes!==c);e+=1)f.integrity.utf8Bytes=c;if(f.integrity.utf8Bytes!==w(JSON.stringify(f)))throw Error("DEVUP_ENVELOPE_LENGTH_UNSTABLE");return{envelope:f,bytes:c}}(e,await v(e));if(n.bytes<=f){T=n;break}if(1===e.length)throw Error("DEVUP_ENVELOPE_TOO_LARGE");O=Math.max(1,Math.min(O-1,t-(n.bytes-f)-256))}if(!T)throw Error("DEVUP_ENVELOPE_TOO_LARGE");return T.envelope},fastTheme:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=Math.max(0,Math.floor(Number(e.theme.offset)||0));function r(e,t=new WeakSet,n=0){if(null===e||["string","number","boolean"].includes(typeof e))return e;if(void 0===e)return{$undefined:!0};if("bigint"==typeof e)return{$bigint:e.toString()};if(["function","symbol"].includes(typeof e))return{$unsupported:typeof e};if(n>12)return{$truncated:"max-depth"};if("object"==typeof e&&"parent"in e&&"string"==typeof e.id&&"string"==typeof e.type)return{$nodeId:e.id,$nodeType:e.type};if(Array.isArray(e))return e.map(e=>r(e,t,n+1));if(ArrayBuffer.isView(e))return{$binary:e.constructor.name,byteLength:e.byteLength};if(e instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:e.byteLength};if(t.has(e))return{$circular:!0};t.add(e);let i={};for(let a of function(e){let t=new Set(Object.keys(e)),r=e;for(;r&&r!==Object.prototype;){for(let e of Object.getOwnPropertyNames(r))t.add(e);r=Object.getPrototypeOf(r)}return[...t].sort()}(e))if(!(a.startsWith("_")||["parent","children","consumers"].includes(a)))try{let o=r(e[a],t,n+1);o&&"function"===o.$unsupported||(i[a]=o)}catch(e){i[a]={$error:"unavailable"}}return t.delete(e),i}let n=new Set,i=new Map;for(let e of figma.root.children)await e.loadAsync();for(let e of[figma.root,...figma.root.findAll(()=>!0)])for(let t of["boundVariables","fills","strokes","effects","layoutGrids","textStyleId","fillStyleId","strokeStyleId","backgroundStyleId","effectStyleId","gridStyleId"])try{!function e(t,r="",a=new WeakSet,o=0){if(!(o>16)&&null!==t&&"object"==typeof t&&!a.has(t)){if(a.add(t),Array.isArray(t)){for(let n of t)e(n,r,a,o+1);return}for(let[l,s]of("VARIABLE_ALIAS"===t.type&&"string"==typeof t.id&&t.id&&"figma.mixed"!==t.id&&"MIXED"!==t.id&&n.add(t.id),Object.entries(t))){let t="textStyleId"===l?"TEXT":["fillStyleId","strokeStyleId","backgroundStyleId"].includes(l)?"PAINT":"effectStyleId"===l?"EFFECT":"gridStyleId"===l?"GRID":null;t&&"string"==typeof s&&s&&"figma.mixed"!==s&&"MIXED"!==s&&!i.has(s)&&i.set(s,t),e(s,l||r,a,o+1)}}}({[t]:e[t]})}catch(e){}let[a,o,l,s,d,f]=await Promise.all([figma.variables.getLocalVariableCollectionsAsync(),figma.variables.getLocalVariablesAsync(),figma.getLocalPaintStylesAsync(),figma.getLocalTextStylesAsync(),figma.getLocalEffectStylesAsync(),figma.getLocalGridStylesAsync()]),c=new Set(o.map(e=>e.id)),u=new Set([...l,...s,...d,...f].map(e=>e.id)),p=[];async function g(e){try{return await figma.variables.getVariableByIdAsync(e)||null}catch(t){return p.push({id:e,kind:"variable",reason:"notFoundOrUnavailable",error:String(t&&t.message||t).slice(0,300)}),null}}function h(e){return Object.values(e.valuesByMode||{}).filter(e=>e&&"VARIABLE_ALIAS"===e.type&&"string"==typeof e.id&&e.id).map(e=>e.id)}let y=[...i.entries()].filter(([e])=>!u.has(e)).sort(([e],[t])=>e.localeCompare(t)).map(async([e,t])=>{try{let r=await figma.getStyleByIdAsync(e);return r?{style:r,styleType:t}:null}catch(t){return p.push({id:e,kind:"style",reason:"notFoundOrUnavailable",error:String(t&&t.message||t).slice(0,300)}),null}}),m=new Set,b=[],x=[...n,...o.flatMap(h)];for(;x.length>0;){let e=[...new Set(x)].filter(e=>!c.has(e)&&!m.has(e)).sort();for(let t of e)m.add(t);let t=(await Promise.all(e.map(g))).filter(Boolean);b.push(...t),x=t.flatMap(h)}let E=(await Promise.all(y)).filter(Boolean);for(let e of m)b.some(t=>t.id===e)||p.some(t=>t.id===e)||p.push({id:e,kind:"variable",reason:"notFoundOrUnavailable"});for(let[e]of[...i.entries()].filter(([e])=>!u.has(e)))E.some(t=>t.style.id===e)||p.some(t=>t.id===e)||p.push({id:e,kind:"style",reason:"notFoundOrUnavailable"});let A=[...new Set(b.map(e=>e.variableCollectionId))].filter(e=>!a.some(t=>t.id===e)).sort(),I=(await Promise.all(A.map(async e=>{try{return await figma.variables.getVariableCollectionByIdAsync(e)}catch(e){return null}}))).filter(Boolean);function S(e,t){return{...r(e),styleType:t,value:r("PAINT"===t?e.paints:"EFFECT"===t?e.effects:"GRID"===t?e.layoutGrids:e)}}let w=[...l.map(e=>S(e,"PAINT")),...s.map(e=>S(e,"TEXT")),...d.map(e=>S(e,"EFFECT")),...f.map(e=>S(e,"GRID")),...E.map(({style:e,styleType:t})=>S(e,t))].sort((e,t)=>e.id.localeCompare(t.id)),v=[...o,...b].map(e=>r(e)).sort((e,t)=>e.id.localeCompare(t.id)),N=[...a,...I].map(e=>r(e)).sort((e,t)=>e.id.localeCompare(t.id));function O(e){let t=[];for(let r=0;r=55296&&n<=56319){let t=r+1=56320&&t<=57343?(n=65536+(n-55296<<10)+(t-56320),r+=1):n=65533}else n>=56320&&n<=57343&&(n=65533);n<128?t.push(n):n<2048?t.push(192|n>>6,128|63&n):n<65536?t.push(224|n>>12,128|n>>6&63,128|63&n):t.push(240|n>>18,128|n>>12&63,128|n>>6&63,128|63&n)}return new Uint8Array(t)}p.sort((e,t)=>e.kind.localeCompare(t.kind)||e.id.localeCompare(t.id));let T=[...N.map(e=>({kind:"collection",value:e})),...v.map(e=>({kind:"variable",value:e})),...w.map(e=>({kind:"style",value:e})),...[...n].sort().map(e=>({kind:"usedVariableId",value:e})),...[...i.keys()].sort().map(e=>({kind:"usedStyleId",value:e})),...p.map(e=>({kind:"unresolved",value:e}))];if(t>T.length)throw Error("DEVUP_SNAPSHOT_RANGE_INVALID");let P=T.map(e=>O(JSON.stringify(e.value)).length+1),_=18432,M=null;for(let e=0;e<6;e+=1){let{pageItems:e,packed:r}=function(e){let r=[],n=0;for(let i=t;i0)||!(n+P[i]>e));i+=1)r.push(T[i]),n+=P[i];return{pageItems:r,packed:n}}(_),n=function(e,r){let n=t=>e.filter(e=>e.kind===t).map(e=>e.value),i=n("collection"),a=n("variable"),o=n("style"),l=n("unresolved"),s={kind:"devupFastThemeEnvelope",schemaVersion:1,source:{fileKey:figma.fileKey||"",version:null},resources:{collections:i,variables:a,styles:o,usedRemoteVariables:[],usedVariableIds:n("usedVariableId"),usedStyleIds:n("usedStyleId"),localComplete:!0,usedRemoteComplete:0===p.length,unresolved:l},page:{offset:t,nextOffset:r,complete:r>=T.length,totalItems:T.length},integrity:{collectionCount:i.length,variableCount:a.length,styleCount:o.length,unresolvedCount:l.length,utf8Bytes:0}},d=new Uint8Array;for(let e=0;e<8&&(d=O(JSON.stringify(s)),s.integrity.utf8Bytes!==d.length);e+=1)s.integrity.utf8Bytes=d.length;if(d=O(JSON.stringify(s)),s.integrity.utf8Bytes!==d.length)throw Error("DEVUP_ENVELOPE_LENGTH_UNSTABLE");return{envelope:s,bytes:d.length}}(e,t+e.length);if(n.bytes<=19456){M=n;break}if(e.length<=1)break;_=Math.max(1,Math.min(_-1,r-(n.bytes-19456)-256))}if(null===M||M.bytes>8388608)throw Error("DEVUP_ENVELOPE_TOO_LARGE");return M.envelope},largeValue:async function(e){let t;figma.skipInvisibleInstanceChildren=!1;let r=e.largeValue;async function n(e){try{let t=await figma.getNodeByIdAsync(e);if(t)return t}catch(r){if(!e.startsWith("I")||!e.includes(";"))throw r;try{return await t()}catch(e){throw Error(`${r.message||r}; nested lookup fallback: ${e.message||e}`)}}if(!e.startsWith("I")||!e.includes(";"))return null;return t();async function t(){let t=await figma.getNodeByIdAsync(e.slice(1).split(";")[0]),r=t?[t]:[],n=new Set;for(;r.length;){let t=r.pop();if(t.id===e)return t;if(!n.has(t.id)){if(n.add(t.id),n.size>1e4)throw Error("nested lookup exceeds 10000 nodes");Array.isArray(t.children)&&r.push(...t.children)}}throw Error(`nested node ${e} not found in its instance`)}}async function i(e,t,r=85e3,n=()=>{}){let a,o=Date.now();try{let i=await Promise.race([e.exportAsync(t),new Promise((e,t)=>{a=setTimeout(()=>t(Error(`DEVUP_ASSET_TIMEOUT: native export exceeded ${r} ms`)),r)})]);return n("native-export",Date.now()-o),i}catch(i){if(n("native-export",Date.now()-o,String(i.message||i).slice(0,500)),"SVG_STRING"!==t.format)throw i;let r=Date.now();try{let t=function(e){let t=e=>"number"==typeof e&&Number.isFinite(e),r=(e,t)=>{throw Error(`geometry fallback unsupported: ${e.type||"unknown"} ${e.id||""}: ${t}`)},n=0,i=0;function a(e,i,a){(void 0!==a&&!Array.isArray(a)||void 0===a&&Array.isArray(i)&&i.length)&&r(e,"unresolved paint requires native export");let o=Array.isArray(a)?a.filter(e=>!1!==e.visible):[];return o.length?(Array.isArray(i)&&i.length||r(e,"paint has no rendered geometry"),o.map(a=>{("SOLID"!==a.type||a.blendMode&&"NORMAL"!==a.blendMode)&&r(e,"paint type or blending requires native export");let o=a.color;o&&[o.r,o.g,o.b].every(e=>t(e)&&e>=0&&e<=1)||r(e,"invalid solid color");let l=void 0===a.opacity?1:a.opacity;return(!t(l)||l<0||l>1)&&r(e,"invalid paint opacity"),["NONZERO","EVENODD"].map(t=>{let a=i.filter(e=>e.windingRule===t);if(!a.length)return"";for(let t of a)"string"==typeof t.data&&t.data.trim()||r(e,"empty path"),n++;return`"']/g,e=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[e])}" fill="rgb(${100*o.r}%,${100*o.g}%,${100*o.b}%)" fill-opacity="${l}" fill-rule="${"EVENODD"===t?"evenodd":"nonzero"}"/>`}).join("")}).join("")):""}let o=e.absoluteRenderBounds,l=e.absoluteTransform;(!o||![o.x,o.y,o.width,o.height].every(t)||o.width<=0||o.height<=0)&&r(e,"missing render bounds"),(!Array.isArray(l)||2!==l.length||!l.every(e=>Array.isArray(e)&&3===e.length&&e.every(t))||0!==l[0][1]||0!==l[1][0]||l[0][0]<=0||l[1][1]<=0)&&r(e,"root rotation, reflection or shear requires native export");let s=(o.x-l[0][2])/l[0][0],d=(o.y-l[1][2])/l[1][1],f=o.width/l[0][0],c=o.height/l[1][1],u=function e(n,o){if(++i>1e4&&r(n,"subtree exceeds 10000 nodes"),n.isMask&&r(n,"mask requires native export"),!1===n.visible||0===n.opacity)return"";n.clipsContent&&r(n,"clipping requires native export"),Array.isArray(n.effects)&&n.effects.some(e=>!1!==e.visible)&&r(n,"effect requires native export"),n.blendMode&&!["NORMAL","PASS_THROUGH"].includes(n.blendMode)&&r(n,"layer blending requires native export");let l=void 0===n.opacity?1:n.opacity;(!t(l)||l<0||l>1)&&r(n,"invalid layer opacity");let s="";if(!o){let e=n.relativeTransform;Array.isArray(e)&&2===e.length&&e.every(e=>Array.isArray(e)&&3===e.length&&e.every(t))||r(n,"missing relative transform"),s=` transform="matrix(${e[0][0]} ${e[1][0]} ${e[0][1]} ${e[1][1]} ${e[0][2]} ${e[1][2]})"`}let d="";if(["GROUP","FRAME","INSTANCE","COMPONENT"].includes(n.type))(Array.isArray(n.fills)&&n.fills.some(e=>!1!==e.visible)||Array.isArray(n.strokes)&&n.strokes.some(e=>!1!==e.visible))&&r(n,"container paint requires native export"),Array.isArray(n.children)||r(n,"missing children"),d=(!0===n.itemReverseZIndex?[...n.children].reverse():n.children).map(t=>e(t,!1)).join("");else{for(let e of(["VECTOR","BOOLEAN_OPERATION","RECTANGLE","ELLIPSE","POLYGON","STAR","LINE"].includes(n.type)||r(n,"node type requires native export"),Array.isArray(n.strokes)&&n.strokes.some(e=>!1!==e.visible)&&"CENTER"!==n.strokeAlign&&r(n,"non-center stroke alignment requires native export"),Array.isArray(n.dashPattern)&&n.dashPattern.length&&r(n,"dashed stroke requires native export"),[n.fillGeometry,n.strokeGeometry]))void 0!==e&&(!Array.isArray(e)||e.some(e=>!["NONZERO","EVENODD"].includes(e.windingRule)))&&r(n,"unknown winding rule");d=a(n,n.fillGeometry,n.fills)+a(n,n.strokeGeometry,n.strokes)}return`${d}`}(e,!0);return n||r(e,"no painted geometry"),`${u}`}(e);return n("geometry-fallback",Date.now()-r),t}catch(e){throw Error(`native export: ${i.message||i}; ${e.message||e}`)}}finally{clearTimeout(a)}}let a=await n(r.nodeId);if(!a)throw Error("DEVUP_NODE_NOT_FOUND");function o(e){let t=[];for(let r=0;r=55296&&n<=56319){let t=r+1=56320&&t<=57343?(n=65536+(n-55296<<10)+(t-56320),r+=1):n=65533}else n>=56320&&n<=57343&&(n=65533);n<128?t.push(n):n<2048?t.push(192|n>>6,128|63&n):n<65536?t.push(224|n>>12,128|n>>6&63,128|63&n):t.push(240|n>>18,128|n>>12&63,128|n>>6&63,128|63&n)}return new Uint8Array(t)}let l=null;try{if("$export:svg"===r.field){if("function"!=typeof a.exportAsync)throw Error("unsupported");let e=await i(a,{format:"SVG_STRING"},Math.max(1,(Number(r.timeoutMs)||9e4)-5e3));if("string"!=typeof e)throw Error("unsupported");l=o(e)}else if("string"==typeof r.field&&r.field.startsWith("$export:png")){if("function"!=typeof a.exportAsync)throw Error("unsupported");let e=Number(r.field.split("@")[1]||2);if(!Number.isFinite(e)||e<=0||e>4)throw Error("invalid scale");let t=r.field.split("@"),n=a;if("visible"===t[2]){let e=new Set;for(;n&&n.id!==t[3]&&!e.has(n)&&e.size<64;)e.add(n),n=n.parent;if(!n||n.id!==t[3]||["PAGE","DOCUMENT"].includes(n.type))throw Error("invalid visible scope")}else if(t.length>2)throw Error("invalid export field");let i=await n.exportAsync({format:"PNG",contentsOnly:!0,useAbsoluteBounds:!1,constraint:{type:"SCALE",value:e}});l=i instanceof Uint8Array?i:new Uint8Array(i)}else if("styledTextSegments"===r.field&&"TEXT"===a.type&&"function"==typeof a.getStyledTextSegments)t=a.getStyledTextSegments(["fontName","fontWeight","fontSize","textDecoration","textCase","lineHeight","letterSpacing","fills","textStyleId","fillStyleId","listOptions","indentation","hyperlink"]);else if(r.field in a)t=a[r.field];else throw Error("unsupported")}catch(e){return{kind:"devupLargeValueUnsupported",fileKey:figma.fileKey||"",version:r.version,nodeId:r.nodeId,field:r.field,byteLength:r.byteLength,sha256:r.sha256,errorCode:"DEVUP_FIELD_UNSUPPORTED_BY_UPSTREAM",errorDetail:`${r.field} failed: ${e.message||e}`.slice(0,500)}}let s=null===l?o(JSON.stringify(function e(t,r=new WeakSet,n=0){if(null===t||["string","number","boolean"].includes(typeof t))return t;if(void 0===t)return{$undefined:!0};if("bigint"==typeof t)return{$bigint:t.toString()};if(["function","symbol"].includes(typeof t))return{$unsupported:typeof t};if(n>12)return{$truncated:"max-depth"};if("object"==typeof t&&"parent"in t&&"string"==typeof t.id&&"string"==typeof t.type)return{$nodeId:t.id,$nodeType:t.type};if(Array.isArray(t))return t.map(t=>e(t,r,n+1));if(ArrayBuffer.isView(t))return{$binary:t.constructor.name,byteLength:t.byteLength};if(t instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:t.byteLength};if(r.has(t))return{$circular:!0};r.add(t);let i={};for(let a of Object.keys(t).sort())try{let o=e(t[a],r,n+1);o&&"function"===o.$unsupported||(i[a]=o)}catch(e){i[a]={$error:String(e&&e.message?e.message:e)}}return r.delete(t),i}(t))):l,d=function(e){let t=[0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,0xe49b69c1,0xefbe4786,0xfc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x6ca6351,0x14292967,0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2],r=e.length,n=64*Math.ceil((r+9)/64),i=new Uint8Array(n);i.set(e),i[r]=128;let a=8*r;for(let e=0;e<8;e+=1)i[n-1-e]=255&Math.floor(a/2**(8*e));let o=[0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19],l=(e,t)=>e>>>t|e<<32-t;for(let e=0;e>>3,n=l(r[e-2],17)^l(r[e-2],19)^r[e-2]>>>10;r[e]=r[e-16]+t+r[e-7]+n>>>0}let[n,a,s,d,f,c,u,p]=o;for(let e=0;e<64;e+=1){let i=p+(l(f,6)^l(f,11)^l(f,25))+(f&c^~f&u)+t[e]+r[e]>>>0,o=(l(n,2)^l(n,13)^l(n,22))+(n&a^n&s^a&s)>>>0;p=u,u=c,c=f,f=d+i>>>0,d=s,s=a,a=n,n=i+o>>>0}for(let[e,t]of[n,a,s,d,f,c,u,p].entries())o[e]=o[e]+t>>>0}return o.map(e=>e.toString(16).padStart(8,"0")).join("")}(s);if(s.length!==r.byteLength||d!==r.sha256)throw Error("DEVUP_LARGE_VALUE_CHANGED");let f=Math.max(0,Math.floor(Number(r.offset)||0)),c=Math.min(65536,Math.max(1,Math.floor(Number(r.maxChunkBytes)||8192)));if(f>=s.length)throw Error("DEVUP_LARGE_VALUE_RANGE_INVALID");let u=Math.min(s.length,f+c);return{kind:"devupLargeValueFragment",fileKey:figma.fileKey||"",version:r.version,nodeId:r.nodeId,field:r.field,offset:f,nextOffset:u,byteLength:s.length,sha256:d,dataBase64:function(e){let t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",r="";for(let n=0;n>2],r+=t[(3&i)<<4|a>>4],r+=n+1>6]:"=",r+=n+210||t.some(e=>"string"!=typeof e||e.length>128||!/^(?:[0-9]+:[0-9]+|I[0-9]+:[0-9]+(?:;[0-9]+:[0-9]+)+)$/.test(e))||new Set(t).size!==t.length)throw Error("DEVUP_INVALID_INPUT: nodeIds must be 1..10 distinct Figma IDs");let r=(e,t)=>{try{let r=e[t];return"number"==typeof r&&Number.isFinite(r)?r:null}catch{return null}},n=(e,t)=>{try{let r=e[t];return"string"==typeof r&&r.length<=32&&/^[A-Z_]+$/.test(r)?r:null}catch{return null}},i=[];for(let e of t)try{let t=await figma.getNodeByIdAsync(e);if(!t||"DOCUMENT"===t.type||"PAGE"===t.type){i.push({nodeId:e,status:"unavailable",reason:t?"not-scene-node":"not-found"});continue}let a={width:r(t,"width"),height:r(t,"height"),padding:{top:r(t,"paddingTop"),right:r(t,"paddingRight"),bottom:r(t,"paddingBottom"),left:r(t,"paddingLeft")},gap:r(t,"itemSpacing"),layoutMode:n(t,"layoutMode"),align:{primary:n(t,"primaryAxisAlignItems"),counter:n(t,"counterAxisAlignItems")}},o=null!==a.width&&null!==a.height;i.push({nodeId:e,status:o?"measured":"partial",...a})}catch{i.push({nodeId:e,status:"unavailable",reason:"read-failed"})}return{kind:"devup-measure",schemaVersion:1,coordinateSpace:"figma-logical",nodes:i,partial:i.some(e=>"measured"!==e.status),evidenceLimit:"Null means unavailable or not applicable; no screenshot dimensions, descendants, theme, assets or code were read."}},metadata:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=await figma.getNodeByIdAsync(e.nodeId);if(!t)throw Error("DEVUP_NODE_NOT_FOUND");let r=t;for(;r&&"PAGE"!==r.type;)r=r.parent;r&&"PAGE"===r.type&&await figma.setCurrentPageAsync(r);let n=[];return!function(e){let t=[{node:e,visited:!1}],r=new Map;for(;t.length>0;){let e=t[t.length-1],i=e.node,a="children"in i?i.children:[];if(!e.visited){e.visited=!0;for(let e=a.length-1;e>=0;e-=1)t.push({node:a[e],visited:!1});continue}t.pop();let o=0,l=[];for(let e of a)l.push(e.id),o+=1+(r.get(e.id)||0);r.set(i.id,o),n.push({id:i.id,type:i.type,name:i.name,childrenIds:l,descendantCount:o})}}(t),{fileKey:figma.fileKey||"",version:null,rootId:t.id,nodes:n}},pageCatalog:async function(e){let t=figma.root.children;return{fileKey:figma.fileKey||"",version:null,rootIds:t.map(e=>e.id),nodes:t.map(e=>({id:e.id,type:e.type,fields:{name:e.name,parentId:null,childrenIds:[]},extra:{},fieldErrors:{}})),diagnostics:[]}},referencePng:async function(e){let t,r=e.nodeId;globalThis.__devupReferencePngAnswers||(globalThis.__devupReferencePngAnswers=new Map);let n=globalThis.__devupReferencePngAnswers,i=0;for(let[e,t]of n)Date.now()-t.at>12e4?n.delete(e):i+=t.text.length;if(i>=0xc000000)throw Error(`DEVUP_REFERENCE_PNG_BUSY ${i} characters are held for other reads`);let a=await figma.getNodeByIdAsync(r);if(!a||"function"!=typeof a.exportAsync)throw Error(`DEVUP_NODE_NOT_FOUND ${r}`);let o=a.absoluteRenderBounds||a.absoluteBoundingBox,l=Math.max(o.width,o.height),s=l>8192?8192/l:1,d=(e,t)=>({format:"PNG",constraint:t&&s<1?{type:o.width>=o.height?"WIDTH":"HEIGHT",value:8192}:{type:"SCALE",value:s},contentsOnly:e,useAbsoluteBounds:!1,colorProfile:"SRGB"}),f=e=>(Array.isArray(e)?e:[]).filter(e=>e&&!1!==e.visible),c=(e,t)=>!!e&&!!t&&e.x!!e&&e.x+t<=o.x&&e.y+t<=o.y&&e.x+e.width-t>=o.x+o.width&&e.y+e.height-t>=o.y+o.height,p=e=>`${e.type} ${JSON.stringify(e.name)}`,g=e=>JSON.parse(JSON.stringify(e===figma.mixed?null:e)),h=e=>(void 0===e.opacity||1===e.opacity)&&(!e.blendMode||["PASS_THROUGH","NORMAL"].includes(e.blendMode)),y=[],m=a.parent;for(;m&&"PAGE"!==m.type;)y.unshift(m),m=m.parent;let b=[255,255,255];for(let e of f(m&&m.backgrounds)){if("SOLID"!==e.type)continue;let t=void 0===e.opacity?1:e.opacity;["r","g","b"].forEach((r,n)=>{b[n]=t*e.color[r]*255+(1-t)*b[n]})}let x=0,E=e=>!1!==e.visible&&!e.isMask&&!!c(e.absoluteRenderBounds,o)&&((x+=1)>5e3||"TEXT"===e.type||!!f(e.fills).length||!!f(e.strokes).length||!!f(e.effects).length||"children"in e&&e.children.some(E)),A=e=>h(e)&&u(e.absoluteBoundingBox,0)&&1e-9>Math.abs(e.absoluteTransform[0][1])&&1e-9>Math.abs(e.absoluteTransform[1][0])&&0===Math.max(0,...[e.topLeftRadius,e.topRightRadius,e.bottomRightRadius,e.bottomLeftRadius].filter(e=>"number"==typeof e))&&f(e.fills).some(e=>"NORMAL"===(e.blendMode||"NORMAL")&&(void 0===e.opacity||1===e.opacity)&&("SOLID"===e.type||e.type.startsWith("GRADIENT_")&&e.gradientStops.every(e=>1===e.color.a))),I=[...y,a],S=[],w=[],v=new Map;for(let e=0;eA(e)&&i.slice(0,t).every(h));r.forEach((e,r)=>{e===t||(e.isMask&&!1!==e.visible&&r{let t=await a.exportAsync(d(!1,!0));return{kind:"devupReferencePng",nodeId:r,mimeType:"image/png",byteLength:t.length,data:figma.base64Encode(t),backdrop:N,...e}},T=(e,t)=>{let r=[...new Set(e)];return{approximate:r.join("; "),note:`Drawn as the canvas shows this area: devup-mcp cannot yet compose what get_screenshot draws here (${r.slice(0,3).join("; ")}). Layers reaching into it may show here: ${t}; get_screenshot leaves other layers out.`}},P=e=>{let t=JSON.stringify(e).replace(/[\u0080-\uffff]/g,e=>`\\u${e.charCodeAt(0).toString(16).padStart(4,"0")}`);if(t.length>0x5a00000){if(e.compose){let{compose:r,reaching:n,...i}=e;return P({...i,...T([`the answer would be ${t.length} characters`],n)})}throw Error(`DEVUP_REFERENCE_PNG_TOO_LARGE ${t.length} characters, over the 94371840 one answer is read back up to`)}if(t.length<=6291456)return e;let i=`${r}@${Date.now()}.${Math.random().toString(36).slice(2,10)}`;return n.set(i,{text:t,at:Date.now()}),{kind:"devupReferencePngChunked",nodeId:r,id:i,length:t.length}};if(0===S.length)return P(await O({}));let _=I.some(e=>e.isMask),M=[],C=new Set,k=(e,t)=>{let r=p(e);for(let t of f(e.fills))"IMAGE"===t.type?((t.rotation||f([t.filters]).some(e=>Object.values(e).some(e=>e)))&&M.push(`${r} has an image fill with a rotation or filters`),C.add(t.imageHash)):"SOLID"===t.type||t.type.startsWith("GRADIENT_")||M.push(`${r} is painted with ${t.type}`);for(let t of f(e.effects))["LAYER_BLUR","DROP_SHADOW","INNER_SHADOW","BACKGROUND_BLUR"].includes(t.type)||M.push(`${r} has a ${t.type}`);let n=f(e.strokes);if((e=>{let t=0===f(e.strokes).length?[]:[e.strokeWeight,e.strokeTopWeight,e.strokeRightWeight,e.strokeBottomWeight,e.strokeLeftWeight].filter(e=>"number"==typeof e&&e>0);if(0===t.length)return!1;let r=Math.max(...t),n="OUTSIDE"===e.strokeAlign?0:"CENTER"===e.strokeAlign?r/2:r,i="INSIDE"===e.strokeAlign?0:"CENTER"===e.strokeAlign?r/2:r,a=e.absoluteBoundingBox;return c(a&&{x:a.x-i,y:a.y-i,width:a.width+2*i,height:a.height+2*i},o)&&!u(a,n)})(e)){let t=["FRAME","RECTANGLE","COMPONENT","COMPONENT_SET","INSTANCE","SECTION"].includes(e.type),i=n.every(e=>"SOLID"===e.type||e.type.startsWith("GRADIENT_")),a=Array.isArray(e.dashPattern)&&e.dashPattern.length>0,o="INSIDE"===e.strokeAlign||!e.strokeJoin||"MITER"===e.strokeJoin;t&&i&&!a&&o||M.push(`${r}'s stroke reaches it`)}return{transform:g(e.absoluteTransform),width:e.width,height:e.height,radii:[e.topLeftRadius,e.topRightRadius,e.bottomRightRadius,e.bottomLeftRadius].map(e=>"number"==typeof e?e:0),fills:g(f(e.fills)),opacity:void 0===e.opacity?1:e.opacity,blendMode:e.blendMode||null,clips:!!e.clipsContent,effects:g(f(e.effects)),strokes:g(n),strokeAlign:e.strokeAlign||null,strokeWeights:["strokeTopWeight","strokeRightWeight","strokeBottomWeight","strokeLeftWeight"].map(t=>"number"==typeof e[t]?e[t]:"number"==typeof e.strokeWeight?e.strokeWeight:0)}},R=async e=>{let t=await e.exportAsync(d(!0,e===a));return{data:figma.base64Encode(t),area:e.absoluteRenderBounds||e.absoluteBoundingBox,blendMode:e.blendMode||null,size:[16,20].map(e=>(t[e]<<24|t[e+1]<<16|t[e+2]<<8|t[e+3])>>>0)}},D=async e=>{let t=v.get(e);if(!t)return null;let r=await t.exportAsync({...d(!0,!1),useAbsoluteBounds:!0});return{data:figma.base64Encode(r),area:t.absoluteBoundingBox,luminance:"LUMINANCE"===t.maskType}},L=[],$={},U={};try{for(let e of y){let t=k(e,!1),r=await D(e);r&&(t.mask=r),L.push(t)}let e=e=>f(e.effects).some(e=>"BACKGROUND_BLUR"===e.type);if(w.length>0&&(e(a)||"findOne"in a&&a.findOne(e))){let r="children"in a&&a.children.some(t=>e(t)||"findOne"in t&&!!t.findOne(e));if("children"in a&&!r&&!f(a.strokes).length){for(let e of($.own=k(a,!0),$.children=[],a.children)){if(!1===e.visible||e.isMask||!e.absoluteRenderBounds)continue;let t=await R(e);$.children.push({data:t.data,area:t.area,blendMode:t.blendMode})}t=[Math.ceil(o.width*s-1e-6),Math.ceil(o.height*s-1e-6)]}else M.push(`${p(a)} blurs what lies behind it`)}else if(_)t=[Math.ceil(o.width*s-1e-6),Math.ceil(o.height*s-1e-6)];else{let e=await R(a);$.picture={data:e.data,area:e.area,blendMode:e.blendMode},t=e.size}let r=await D(a);r&&($.mask=r),t&&t[0]*t[1]>8e6&&M.push(`the area is ${t[0]}x${t[1]} pixels`);let n=globalThis.__devupNormalizeImage,i=Date.now()+2e4,l=0;for(let e of C){let t=e&&figma.getImageByHash(e),r=t&&await t.getBytesAsync();if(!r){M.push(`the image ${e} could not be read`);continue}let a=null;if("function"==typeof n){let{width:e,height:o}=await t.getSizeAsync(),l=(...e)=>e.every((e,t)=>r[t]===e),s=l(137,80,78,71)||l(255,216)||l(71,73,70),d=Math.min(1,8192/Math.max(e,o,1)),f=Math.ceil(e*d)*Math.ceil(o*d),c=5*f+65536,u=5*r.length+4*e*o*(d<1?2:1)+4*f+4*c+1048576,p=i-Date.now();if((!s||Math.max(e,o)>8192||r.length>4*e*o+1048576)&&u<=0x20000000&&p>0){let t=await n(r,8192,p,e,o);t&&(r=t,a={width:e,height:o})}}if(l+r.length>0x4000000){M.push("its ancestors' images are over 67108864 bytes");break}l+=r.length,U[e]=a?{data:figma.base64Encode(r),...a}:figma.base64Encode(r)}}catch(e){M.push(`a part could not be exported: ${e instanceof Error?e.message:String(e)}`)}let B=`${S.slice(0,5).join(", ")}${S.length>5?` and ${S.length-5} more`:""}`;return M.length>0?P(await O(T(M,B))):P(await O({reaching:B,compose:{area:o,scale:s,width:t[0],height:t[1],page:b,layers:L,node:$,images:U}}))},referencePngChunk:async function(e){let t=e.largeValue,r=globalThis.__devupReferencePngAnswers,n=r&&r.get(t.id);if(!n||"string"!=typeof n.text)throw Error(`DEVUP_REFERENCE_PNG_ANSWER_GONE ${t.id}`);let i=n.text,a=Math.max(0,Math.floor(Number(t.offset)||0)),o=Math.min(i.length,a+4194304);return o>=i.length&&r.delete(t.id),{kind:"devupReferencePngChunk",id:t.id,offset:a,nextOffset:o,complete:o>=i.length,text:i.slice(a,o)}},search:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=await figma.getNodeByIdAsync(e.nodeId);if(!t)throw Error("DEVUP_NODE_NOT_FOUND");let r=t;for(;r&&"PAGE"!==r.type;)r=r.parent;if(!r||"PAGE"!==r.type)throw Error("DEVUP_PAGE_NOT_FOUND");await figma.setCurrentPageAsync(r);let n=e.search,i=new Set((n.nodeTypes.length?n.nodeTypes:["PAGE","SECTION","FRAME","COMPONENT_SET","COMPONENT"]).map(e=>e.toUpperCase()));function a(e){return e.normalize("NFC").toLocaleLowerCase().replace(/\s/gu,"")}let o=t===r?r.findAll(()=>!0):"findAll"in t?t.findAll(()=>!0):[],l=[t,...o].filter(e=>i.has(e.type)&&"string"==typeof e.name).map(e=>({node:e,score:function(e){let t=n.query.trim();if(""===t||"*"===t)return 100;if(e===n.query)return 400;if("exact"===n.matchKind)return null;let r=a(e),i=a(n.query);if(r===i)return 300;if(r.startsWith(i))return 200;if(r.includes(i))return 100;if("fuzzy"!==n.matchKind)return null;let o=function(e,t){let r=Array.from({length:t.length+1},(e,t)=>t);for(let n=0;nnull!==e.score).sort((e,t)=>t.score-e.score||e.node.name.localeCompare(t.node.name)||e.node.id.localeCompare(t.node.id)).slice(0,n.limit),s=new Map([[r.id,r]]);for(let e of[t,...l.map(e=>e.node)]){let t=e;for(;t&&"DOCUMENT"!==t.type;)s.set(t.id,t),t=t.parent}return{fileKey:figma.fileKey||"",version:null,rootIds:[r.id],nodes:[...s.values()].map(e=>({id:e.id,type:e.type,fields:{name:e.name,parentId:e.parent&&"DOCUMENT"!==e.parent.type?e.parent.id:null,childrenIds:"children"in e?e.children.map(e=>e.id):[]},extra:{},fieldErrors:{}})),diagnostics:[]}},sectionIndex:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=await figma.getNodeByIdAsync(e.nodeId);if(!t)throw Error("DEVUP_NODE_NOT_FOUND");if("SECTION"!==t.type){let e=t.parent,r=new Set;for(;e&&"SECTION"!==e.type&&!r.has(e.id);)r.add(e.id),e=e.parent;let n=e&&"SECTION"===e.type?e.id:null,i=n&&figma.fileKey?`https://www.figma.com/design/${figma.fileKey}?node-id=${n.replace(/:/g,"-")}`:null;throw Error("DEVUP_SECTION_REQUIRED "+JSON.stringify({pluginCode:"DEVUP_SECTION_REQUIRED",stage:"section-index",nodeId:t.id,nodeType:t.type,sectionId:n,nextAction:{tool:"devup_figma_export",how:n?`The url must point to a SECTION. This capture's SECTION is ${n}; select frames with frameIds.`:"The url must point to a SECTION. This node has no ancestor SECTION; choose the intended SECTION in Figma and copy its link.",arguments:i?"FRAME"===t.type&&t.parent===e?{url:i,frameIds:[t.id]}:{url:i}:null,requiredArguments:i?[]:["url"]}}))}let r=2048;function n(e){let t=e.absoluteBoundingBox||{x:"number"==typeof e.x?e.x:0,y:"number"==typeof e.y?e.y:0,width:"number"==typeof e.width?e.width:0,height:"number"==typeof e.height?e.height:0};return[t.x,t.y,t.width,t.height].every(Number.isFinite)?{x:t.x,y:t.y,width:t.width,height:t.height}:null}function i(e,t){if("FRAME"!==e.type||!1===e.visible||!t)return!1;let r=t.width/Math.max(1,t.height);return t.width>=240&&t.width<=1800&&t.height>=300&&t.height<=2e3&&r>=.25&&r<=2.5}function a(e){let t=0;for(let r=0;r=55296&&n<=56319&&r+1[e.id,t.id])),s=[],d=0;for(let e=0;ee.id));for(let r of t.children){if(e.has(r.id))continue;let t=n(r);t&&!1!==r.visible&&s.push({node:r,box:t})}}s.sort((e,t)=>e.box.y-t.box.y||e.box.x-t.box.x||e.node.id.localeCompare(t.node.id));let f=o.length>d||s.length>100,c=s.slice(0,100),u=new Set(c.map(({node:e})=>e.id)),p=new Set(e.rootIds),g={},h=new Map([[t.id,null]]);for(let e=0;e{let r=e.parent;for(;r&&r.id!==t.id&&!u.has(r.id);)r=r.parent;return[e.id,r&&u.has(r.id)?r.id:t.id]})),m=e=>c.filter(({node:t})=>y.get(t.id)===e).map(({node:e})=>e.id),b=n(t);if(!b)throw Error("DEVUP_NODE_BOUNDS_UNAVAILABLE");let x={id:t.id,type:t.type,fields:{name:t.name,parentId:t.parent&&"DOCUMENT"!==t.parent.type?t.parent.id:null,childrenIds:m(t.id),absoluteBoundingBox:b,visible:!1!==t.visible,projectionTruncated:f,nodeScreenIds:g},extra:{},fieldErrors:{}},E=c.length,A=c.map(({node:e,box:t})=>{let o,l=function(e){let t=[e],r=0,n=0;for(let e=0;er}}(e),s=Math.floor(r/E);E-=1;let d=function(e,t){let r=[e],n="",i=0,o=0,l=!1;if(t<=0)return{text:"",state:"budget-exhausted",spent:0};for(let e=0;e=120||o+e>t)return{text:n.trimEnd(),state:o+e>t?"budget-exhausted":"truncated",spent:o};n+=r,i+=1,o+=e}}if("children"in s){let e=Math.max(0,64-r.length);s.children.length>e&&(l=!0),r.push(...s.children.slice(0,e))}}}return{text:n,state:l?"truncated":n?"available":"no-text",spent:o}}(e,s);return r-=d.spent,{id:e.id,type:e.type,fields:{name:"string"==typeof e.name?e.name:"",parentId:y.get(e.id),childrenIds:m(e.id),absoluteBoundingBox:t,visible:!1!==e.visible,breadcrumb:function(e){let t=[],r=e;for(;r&&"DOCUMENT"!==r.type;)"string"==typeof r.name&&r.name&&t.push(r.name),r=r.parent;return t.reverse()}(e),directChildCount:"children"in e?e.children.length:0,textPreview:d.text,...null===(o=function(e){let t=[e],r=0,i=null;for(;t.length&&r++<400;){let e=t.shift();if("visible"in e&&!1===e.visible)continue;let r="children"in e?e.children.filter(e=>!("visible"in e)||!1!==e.visible):[];if(/header|nav|gnb/i.test(e.name||"")&&r.length>=2&&"layoutMode"in e&&"HORIZONTAL"===e.layoutMode){let t=r.map(n);t.every(Boolean)&&(i=Math.max(i||0,t.reduce((e,t)=>e+t.width,0)+(Number(e.itemSpacing)||0)*(t.length-1)))}t.push(...r.slice(0,80))}return i}(e))?{}:{navigationContentWidth:o},textPreviewState:d.state,subtreeNodeCount:l.subtreeNodeCount,estimatedSerializedBytes:l.estimatedSerializedBytes,selectionReasons:[i(e,t)?"screen-like":"explicit-selection-only","inside-section"],estimateTruncated:l.truncated},extra:{},fieldErrors:{}}}),I={fileKey:figma.fileKey||"",version:null,rootIds:[t.id],nodes:[x,...A],diagnostics:[]},S=a(JSON.stringify(I));for(let e=A.length-1;e>=0&&S>19456;e-=1)A[e].fields.textPreview="",A[e].fields.textPreviewState="budget-exhausted",S=a(JSON.stringify(I));let w=()=>I.nodes.length-1,v=A.length;for(;S>19456&&w()>1;){let e=I.nodes.pop();x.fields.projectionTruncated=!0,x.fields.childrenIds=x.fields.childrenIds.filter(t=>t!==e.id),S=a(JSON.stringify(I))}if(S>19456)throw Error("DEVUP_SECTION_INDEX_TOO_LARGE "+JSON.stringify({pluginCode:"DEVUP_SECTION_INDEX_TOO_LARGE",stage:"section-index",responseBytes:S,maxResponseBytes:19456,discoveredCandidates:v,retainedCandidates:w()}));return I},snapshot:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=await figma.getNodeByIdAsync(e.nodeId);if(!t)throw Error("DEVUP_NODE_NOT_FOUND");let r=["absoluteBoundingBox","absoluteRenderBounds","arcData","backgroundStyleId","blendMode","bottomLeftRadius","bottomRightRadius","boundVariables","characters","clipsContent","componentProperties","componentPropertyDefinitions","componentPropertyReferences","constraints","cornerRadius","counterAxisAlignItems","dashPattern","defaultVariant","effectStyleId","effects","fillStyleId","fills","fontName","fontSize","gridColumnAnchorIndex","gridColumnCount","gridColumnGap","gridColumnSizes","gridColumnSpan","gridRowAnchorIndex","gridRowCount","gridRowGap","gridRowSizes","gridRowSpan","gridStyleId","height","inferredAutoLayout","isAsset","isMask","itemSpacing","layoutGrow","layoutMode","layoutWrap","layoutPositioning","layoutSizingHorizontal","layoutSizingVertical","letterSpacing","lineHeight","maxHeight","maxLines","maxWidth","minHeight","minWidth","name","opacity","overflowDirection","paddingBottom","paddingLeft","paddingRight","paddingTop","primaryAxisAlignItems","reactions","rotation","strokeAlign","strokeBottomWeight","strokeLeftWeight","strokeRightWeight","strokeStyleId","strokeTopWeight","strokeWeight","strokes","targetAspectRatio","textAlignHorizontal","textAlignVertical","textAutoResize","textCase","textDecoration","textStyleId","textTruncation","topLeftRadius","topRightRadius","variantProperties","visible","width","x","y"],n=new Set(r),i=["fontName","fontWeight","fontSize","textDecoration","textCase","lineHeight","letterSpacing","fills","textStyleId","fillStyleId","listOptions","indentation","hyperlink"],a=e.snapshot,o=Math.max(0,Math.floor(Number(a.offset)||0)),l=Math.min(a.maxEnvelopeBytes?Math.max(8192,Math.floor(Number(a.maxEnvelopeBytes))):16e3,Math.max(4096,Math.floor(Number(a.maxPayloadBytes)||15e3))),s=Math.min(l-1024,Math.max(512,Math.floor(Number(a.maxFieldBytes)||4096))),d=new Set(["id","type","parent","children","mainComponent","masterComponent","instances"]),f=new Set(["parentId","childrenIds","name","characters","styledTextSegments","boundVariables"]);function c(e,t,r){let n=function(e){let t=[];for(let r=0;r=55296&&n<=56319){let t=r+1=56320&&t<=57343?(n=65536+(n-55296<<10)+(t-56320),r+=1):n=65533}else n>=56320&&n<=57343&&(n=65533);n<128?t.push(n):n<2048?t.push(192|n>>6,128|63&n):n<65536?t.push(224|n>>12,128|n>>6&63,128|63&n):t.push(240|n>>18,128|n>>12&63,128|n>>6&63,128|63&n)}return new Uint8Array(t)}(JSON.stringify(r));return n.length>0x1000000?{$truncated:"max-large-value-bytes",byteLength:n.length}:{$largeValue:{nodeId:e,field:t,byteLength:n.length,sha256:function(e){let t=[0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,0xe49b69c1,0xefbe4786,0xfc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x6ca6351,0x14292967,0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2],r=64*Math.ceil((e.length+9)/64),n=new Uint8Array(r);n.set(e),n[e.length]=128;let i=8*e.length;for(let e=0;e<8;e+=1)n[r-1-e]=255&Math.floor(i/2**(8*e));let a=[0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19],o=(e,t)=>e>>>t|e<<32-t;for(let e=0;e>>3,n=o(r[e-2],17)^o(r[e-2],19)^r[e-2]>>>10;r[e]=r[e-16]+t+r[e-7]+n>>>0}let[i,l,s,d,f,c,u,p]=a;for(let e=0;e<64;e+=1){let n=p+(o(f,6)^o(f,11)^o(f,25))+(f&c^~f&u)+t[e]+r[e]>>>0,a=(o(i,2)^o(i,13)^o(i,22))+(i&l^i&s^l&s)>>>0;p=u,u=c,c=f,f=d+n>>>0,d=s,s=l,l=i,i=n+a>>>0}for(let[e,t]of[i,l,s,d,f,c,u,p].entries())a[e]=a[e]+t>>>0}return a.map(e=>e.toString(16).padStart(8,"0")).join("")}(n),cursor:{nextOffset:0,maxChunkBytes:12288}}}}function u(e){return function(e){let t=0;for(let r=0;r=55296&&n<=56319&&r+112)return{$truncated:"max-depth"};if("object"==typeof t&&"parent"in t&&"string"==typeof t.id&&"string"==typeof t.type)return{$nodeId:t.id,$nodeType:t.type};if(Array.isArray(t))return t.map(t=>e(t,r,n+1));if(ArrayBuffer.isView(t))return{$binary:t.constructor.name,byteLength:t.byteLength};if(t instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:t.byteLength};if(r.has(t))return{$circular:!0};r.add(t);let i={};for(let a of Object.keys(t).sort())try{let o=e(t[a],r,n+1);o&&"function"===o.$unsupported||(i[a]=o)}catch(e){i[a]={$error:String(e&&e.message?e.message:e)}}return r.delete(t),i}(r),a=u(i);if(a<=s)return i;let o=c(e,t,i);return o.$truncated&&(n[t]=`DEVUP_FIELD_VALUE_UNSUPPORTED:${a}>16777216`),o}let g=new Map;async function h(e,t,r){if("INSTANCE"!==e.type||"function"!=typeof e.getMainComponentAsync)return;g.has(e.id)||g.set(e.id,(async()=>{try{let t=await e.getMainComponentAsync();return{fields:{mainComponentId:t?t.id:null,mainComponentName:t?t.name:null,mainComponentSetId:t&&t.parent&&"COMPONENT_SET"===t.parent.type?t.parent.id:null}}}catch(e){return{error:String(e&&e.message?e.message:e)}}})());let n=await g.get(e.id);n.error?r.mainComponentId=n.error:Object.assign(t,n.fields)}async function y(e){let t={},a={},o={};for(let i of(t.parentId=e.parent?e.parent.id:null,e.parent&&("PAGE"===e.parent.type||"SECTION"===e.parent.type||"COMPONENT_SET"===e.parent.type)&&(t.parentType=e.parent.type,"SECTION"===e.parent.type&&(t.parentName=e.parent.name)),t.childrenIds="children"in e?e.children.map(e=>e.id):[],!("overflowDirection"in e)&&["FRAME","COMPONENT","INSTANCE","COMPONENT_SET"].includes(e.type)&&(t.overflowDirection=null),function(e){let t=new Set,n=e;for(;n&&n!==Object.prototype;){for(let e of Object.getOwnPropertyNames(n))t.add(e);n=Object.getPrototypeOf(n)}for(let n of r)try{n in e&&t.add(n)}catch(e){}return[...t].sort()}(e)))if(!(d.has(i)||i.startsWith("_")||"componentPropertyDefinitions"===i&&"COMPONENT"===e.type&&e.parent&&"COMPONENT_SET"===e.parent.type))try{let r=e[i];if("function"==typeof r)continue;let l=p(e.id,i,r,o);(n.has(i)?t:a)[i]=l}catch(e){o[i]=String(e&&e.message?e.message:e)}if("TEXT"===e.type&&"function"==typeof e.getStyledTextSegments)try{t.styledTextSegments=p(e.id,"styledTextSegments",e.getStyledTextSegments(i),o)}catch(e){o.styledTextSegments=String(e&&e.message?e.message:e)}return await h(e,t,o),{id:e.id,type:e.type,fields:t,extra:a,fieldErrors:o}}let m=[],b=[t];for(;b.length;){let e=b.shift();m.push(e),"children"in e&&b.push(...e.children)}let x=[],E=l-1024,A=2;for(let e=o;eNumber(e.protected)-Number(t.protected)||t.byteLength-e.byteLength),n)){if(r<=t)break;let n=c(e.id,i.name,e[i.sectionName][i.name]);e[i.sectionName][i.name]=n,n.$truncated&&(e.fieldErrors[i.name]=`DEVUP_FIELD_VALUE_UNSUPPORTED:${i.byteLength}>16777216`),r=u(e)}return e}(await y(m[e]),E),r=u(t)+ +!!x.length;if(x.length&&A+r>E)break;x.push(t),A+=r}let I=Math.min(m.length,o+x.length);return x.push({id:"__DEVUP_SNAPSHOT_CURSOR__",type:"DEVUP_INTERNAL",fields:{offset:o,nextOffset:I,complete:I>=m.length,totalNodes:m.length},extra:{},fieldErrors:{}}),{fileKey:figma.fileKey||"",version:null,rootIds:[t.id],nodes:x,diagnostics:[]}},usedResources:async function(e){let t=e.resources;function r(e,t=new WeakSet,n=0){if(null===e||["string","number","boolean"].includes(typeof e))return e;if(void 0===e)return{$undefined:!0};if("bigint"==typeof e)return{$bigint:e.toString()};if(["function","symbol"].includes(typeof e))return{$unsupported:typeof e};if(n>12)return{$truncated:"max-depth"};if("object"==typeof e&&"parent"in e&&"string"==typeof e.id&&"string"==typeof e.type)return{$nodeId:e.id,$nodeType:e.type};if(Array.isArray(e))return e.map(e=>r(e,t,n+1));if(ArrayBuffer.isView(e))return{$binary:e.constructor.name,byteLength:e.byteLength};if(e instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:e.byteLength};if(t.has(e))return{$circular:!0};t.add(e);let i={},a=new Set(Object.keys(e)),o=e;for(;o&&o!==Object.prototype;){for(let e of Object.getOwnPropertyNames(o))a.add(e);o=Object.getPrototypeOf(o)}for(let o of[...a].sort())if(!(o.startsWith("_")||["parent","children","consumers"].includes(o)))try{let a=r(e[o],t,n+1);a&&"function"===a.$unsupported||(i[o]=a)}catch(e){i[o]={$error:"unavailable"}}return t.delete(e),i}let n=new Map,i="ok";try{for(let e of(await figma.variables.getLocalVariablesAsync()))n.set(e.id,e)}catch(e){i="unavailable"}function a(e){return Object.values(e.valuesByMode||{}).filter(e=>e&&"VARIABLE_ALIAS"===e.type&&"string"==typeof e.id&&e.id).map(e=>e.id)}async function o(e){let t=n.get(e);if(t)return{value:r(t),collectionId:t.variableCollectionId,aliasIds:a(t)};try{let t=await figma.variables.getVariableByIdAsync(e);return t?{value:r(t),collectionId:t.variableCollectionId,aliasIds:a(t)}:{unresolved:{id:e,kind:"variable",reason:"notInFileAndLookupEmpty"}}}catch(t){return{unresolved:{id:e,kind:"variable",reason:"notInFileAndLookupThrew",error:String(t&&t.message||t).slice(0,300)}}}}let l=await Promise.all(t.variableIds.map(o)),s=new Set(t.variableIds),d=l.flatMap(e=>e.aliasIds||[]);for(;d.length>0;){let e=[...new Set(d)].filter(e=>!s.has(e)).sort();for(let t of e)s.add(t);let t=await Promise.all(e.map(o));l.push(...t),d=t.flatMap(e=>e.aliasIds||[])}let f=[...new Set(l.flatMap(e=>e.collectionId?[e.collectionId]:[]))].sort(),c=await Promise.all(f.map(async e=>{try{let t=await figma.variables.getVariableCollectionByIdAsync(e);return t?[r(t)]:[]}catch(e){return[]}})),u=await Promise.all(t.styles.map(async e=>{try{let t=await figma.getStyleByIdAsync(e.id);if(!t)return{unresolved:{id:e.id,kind:"style",reason:"notFoundOrUnavailable"}};return{value:{...r(t),styleType:e.styleType,value:r("PAINT"===e.styleType?t.paints:"EFFECT"===e.styleType?t.effects:"GRID"===e.styleType?t.layoutGrids:t)}}}catch(t){return{unresolved:{id:e.id,kind:"style",reason:"notFoundOrUnavailable",error:String(t&&t.message||t).slice(0,300)}}}}));return{collections:c.flat(),variables:l.flatMap(e=>e.value?[e.value]:[]),styles:u.flatMap(e=>e.value?[e.value]:[]),usedVariableIds:t.variableIds,usedStyleIds:t.styles.map(e=>e.id),localVariableListing:i,localVariableCount:n.size,unresolved:[...l,...u].flatMap(e=>e.unresolved?[e.unresolved]:[])}},variableCatalog:async function(e){let[t,r,n,i,a]=await Promise.all([figma.variables.getLocalVariableCollectionsAsync(),figma.getLocalPaintStylesAsync(),figma.getLocalTextStylesAsync(),figma.getLocalEffectStylesAsync(),figma.getLocalGridStylesAsync()]),o=["PAINT","TEXT","EFFECT","GRID"];return{collections:t.map(e=>(function e(t,r=new WeakSet,n=0){if(null===t||["string","number","boolean"].includes(typeof t))return t;if(void 0===t)return{$undefined:!0};if("bigint"==typeof t)return{$bigint:t.toString()};if(["function","symbol"].includes(typeof t))return{$unsupported:typeof t};if(n>12)return{$truncated:"max-depth"};if(Array.isArray(t))return t.map(t=>e(t,r,n+1));if(r.has(t))return{$circular:!0};r.add(t);let i={},a=new Set(Object.keys(t)),o=t;for(;o&&o!==Object.prototype;){for(let e of Object.getOwnPropertyNames(o))a.add(e);o=Object.getPrototypeOf(o)}for(let o of[...a].sort())if(!o.startsWith("_"))try{let a=e(t[o],r,n+1);a&&"function"===a.$unsupported||(i[o]=a)}catch(e){i[o]={$error:String(e&&e.message?e.message:e)}}return r.delete(t),i})(e)),variableIds:[...new Set(t.flatMap(e=>e.variableIds))].sort(),styles:[r,n,i,a].flatMap((e,t)=>e.map(e=>({id:e.id,styleType:o[t]}))).sort((e,t)=>e.id.localeCompare(t.id)),localComplete:!0,usedRemoteComplete:!1}},variables:async function(e){let t=e.resources;function r(e,t=new WeakSet,n=0){if(null===e||["string","number","boolean"].includes(typeof e))return e;if(void 0===e)return{$undefined:!0};if("bigint"==typeof e)return{$bigint:e.toString()};if(["function","symbol"].includes(typeof e))return{$unsupported:typeof e};if(n>12)return{$truncated:"max-depth"};if("object"==typeof e&&"parent"in e&&"string"==typeof e.id&&"string"==typeof e.type)return{$nodeId:e.id,$nodeType:e.type};if(Array.isArray(e))return e.map(e=>r(e,t,n+1));if(ArrayBuffer.isView(e))return{$binary:e.constructor.name,byteLength:e.byteLength};if(e instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:e.byteLength};if(t.has(e))return{$circular:!0};t.add(e);let i={},a=new Set(Object.keys(e)),o=e;for(;o&&o!==Object.prototype;){for(let e of Object.getOwnPropertyNames(o))a.add(e);o=Object.getPrototypeOf(o)}for(let o of[...a].sort())if(!(o.startsWith("_")||["parent","children","consumers"].includes(o)))try{let a=r(e[o],t,n+1);a&&"function"===a.$unsupported||(i[o]=a)}catch(e){i[o]={$error:String(e&&e.message?e.message:e)}}return t.delete(e),i}let[n,i]=await Promise.all([Promise.all(t.variableIds.map(e=>figma.variables.getVariableByIdAsync(e))),Promise.all(t.styles.map(e=>figma.getStyleByIdAsync(e.id)))]),a=new Map(t.styles.map(e=>[e.id,e])),o=await Promise.all(i.filter(Boolean).map(async e=>{let t=a.get(e.id),n=t.styleType;if(Number.isInteger(t.consumerStart)&&Number.isInteger(t.consumerEnd)){let i=await e.getStyleConsumersAsync();return{id:e.id,styleType:n,$consumerStart:t.consumerStart,$consumerEntries:i.slice(t.consumerStart,t.consumerEnd).map(e=>[e.node.id,e.node.type,r(e.fields)])}}let i=await e.getStyleConsumersAsync();return{...r(e),styleType:n,$consumerCount:i.length,value:r("PAINT"===n?e.paints:"EFFECT"===n?e.effects:"GRID"===n?e.layoutGrids:e)}}));return{variables:n.filter(Boolean).map(e=>r(e)),styles:o}}},t=new Map,r=0;globalThis.__devupNormalizeImage=(e,n,i,a,o)=>new Promise(l=>{r+=1;let s=`decode-${r}`,d=setTimeout(()=>{t.delete(s),l(null)},i);t.set(s,e=>{clearTimeout(d),t.delete(s),l(e)}),figma.ui.postMessage({kind:"devup-decode",id:s,bytes:e,maxEdge:n,width:a,height:o})});let n=globalThis;function i(){let e=figma.currentPage,t=e.selection;return t.length>0&&(n.__devupLastSelection=t.slice(0,20).map(e=>({id:e.id,name:e.name.slice(0,256),type:e.type})),n.__devupLastSelectionPage={id:e.id,name:e.name.slice(0,256)}),{zoom:Number.isFinite(figma.viewport?.zoom)?figma.viewport.zoom:null,observedAt:Date.now(),lastKnownSelection:n.__devupLastSelection??null,lastKnownPage:n.__devupLastSelectionPage??null,currentPage:{id:e.id,name:e.name.slice(0,256)},selection:t.slice(0,20).map(e=>({id:e.id,name:e.name.slice(0,256),type:e.type})),selectionCount:t.length}}function a(){let e={kind:"devup-context",...i()};figma.ui.postMessage(e)}async function o(t){let r=e[t.script];if(!r)return{kind:"devup-result",requestId:t.requestId,error:`DEVUP_BRIDGE_UNKNOWN_SCRIPT: ${t.script} (아는 스크립트: ${Object.keys(e).join(", ")})`};try{var n;"bridgeContext"===t.script&&i();let e=await r({nodeId:(n=t.params).nodeId??"",rootIds:n.rootIds??[],snapshot:n.snapshot??{},search:n.search??{},explore:n.explore??{},resources:n.resources??{variableIds:[],styles:[]},largeValue:n.largeValue??{},asset:n.asset??{},theme:n.theme??{offset:0}});return{kind:"devup-result",requestId:t.requestId,data:e}}catch(e){return{kind:"devup-result",requestId:t.requestId,error:e instanceof Error?e.message:String(e)}}}let l=[],s=!1,d=null,f=new Set;async function c(){if(!s){s=!0;try{for(;l.length>0;){let e=l.shift();d=e.requestId;let t=await o(e).catch(t=>({kind:"devup-result",requestId:e.requestId,error:t instanceof Error?t.message:String(t)}));d=null,f.delete(e.requestId)||figma.ui.postMessage(t)}}finally{s=!1}}}let u=null;function p(){u??(u=(async()=>{try{await figma.loadAllPagesAsync(),await Promise.all([figma.variables.getLocalVariableCollectionsAsync(),figma.variables.getLocalVariablesAsync(),figma.getLocalPaintStylesAsync(),figma.getLocalTextStylesAsync(),figma.getLocalEffectStylesAsync(),figma.getLocalGridStylesAsync()])}catch{}finally{u=null}})())}figma.showUI(__html__,{width:320,height:220}),p(),figma.on("selectionchange",a),figma.on("currentpagechange",a),figma.ui.onmessage=e=>{if("object"==typeof e&&null!==e){if("devup-ready"===e.kind){let e={kind:"devup-status",fileKey:figma.fileKey??null,fileName:figma.root.name,port:1993,...i()};figma.ui.postMessage(e);return}if("devup-job"===e.kind){l.push(e),c();return}if("devup-cancel"===e.kind){var r;let t;return void(r=e.requestId,(t=l.findIndex(e=>e.requestId===r))>=0?l.splice(t,1):d===r&&f.add(r))}if("devup-decoded"===e.kind)return void t.get(e.id)?.(e.png??null);"devup-in-view"===e.kind&&p()}}})(); \ No newline at end of file +(()=>{"use strict";let e={assets:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=e.asset;function r(e){let t=[0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,0xe49b69c1,0xefbe4786,0xfc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x6ca6351,0x14292967,0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2],r=64*Math.ceil((e.length+9)/64),n=new Uint8Array(r);n.set(e),n[e.length]=128;let i=8*e.length;for(let e=0;e<8;e+=1)n[r-1-e]=255&Math.floor(i/2**(8*e));let a=[0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19],o=(e,t)=>e>>>t|e<<32-t;for(let e=0;e>>3,n=o(r[e-2],17)^o(r[e-2],19)^r[e-2]>>>10;r[e]=r[e-16]+t+r[e-7]+n>>>0}let[i,l,s,d,f,c,u,p]=a;for(let e=0;e<64;e+=1){let n=p+(o(f,6)^o(f,11)^o(f,25))+(f&c^~f&u)+t[e]+r[e]>>>0,a=(o(i,2)^o(i,13)^o(i,22))+(i&l^i&s^l&s)>>>0;p=u,u=c,c=f,f=d+n>>>0,d=s,s=l,l=i,i=n+a>>>0}for(let[e,t]of[i,l,s,d,f,c,u,p].entries())a[e]=a[e]+t>>>0}return a.map(e=>e.toString(16).padStart(8,"0")).join("")}async function n(e){try{let t=await figma.getNodeByIdAsync(e);if(t)return t}catch(r){if(!e.startsWith("I")||!e.includes(";"))throw r;try{return await t()}catch(e){throw Error(`${r.message||r}; nested lookup fallback: ${e.message||e}`)}}if(!e.startsWith("I")||!e.includes(";"))return null;return t();async function t(){let t=await figma.getNodeByIdAsync(e.slice(1).split(";")[0]),r=t?[t]:[],n=new Set;for(;r.length;){let t=r.pop();if(t.id===e)return t;if(!n.has(t.id)){if(n.add(t.id),n.size>1e4)throw Error("nested lookup exceeds 10000 nodes");Array.isArray(t.children)&&r.push(...t.children)}}throw Error(`nested node ${e} not found in its instance`)}}async function i(e,t,r=85e3,n=()=>{}){let a,o=Date.now();try{let i=await Promise.race([e.exportAsync(t),new Promise((e,t)=>{a=setTimeout(()=>t(Error(`DEVUP_ASSET_TIMEOUT: native export exceeded ${r} ms`)),r)})]);return n("native-export",Date.now()-o),i}catch(i){if(n("native-export",Date.now()-o,String(i.message||i).slice(0,500)),"SVG_STRING"!==t.format)throw i;let r=Date.now();try{let t=function(e){let t=e=>"number"==typeof e&&Number.isFinite(e),r=(e,t)=>{throw Error(`geometry fallback unsupported: ${e.type||"unknown"} ${e.id||""}: ${t}`)},n=0,i=0;function a(e,i,a){(void 0!==a&&!Array.isArray(a)||void 0===a&&Array.isArray(i)&&i.length)&&r(e,"unresolved paint requires native export");let o=Array.isArray(a)?a.filter(e=>!1!==e.visible):[];return o.length?(Array.isArray(i)&&i.length||r(e,"paint has no rendered geometry"),o.map(a=>{("SOLID"!==a.type||a.blendMode&&"NORMAL"!==a.blendMode)&&r(e,"paint type or blending requires native export");let o=a.color;o&&[o.r,o.g,o.b].every(e=>t(e)&&e>=0&&e<=1)||r(e,"invalid solid color");let l=void 0===a.opacity?1:a.opacity;return(!t(l)||l<0||l>1)&&r(e,"invalid paint opacity"),["NONZERO","EVENODD"].map(t=>{let a=i.filter(e=>e.windingRule===t);if(!a.length)return"";for(let t of a)"string"==typeof t.data&&t.data.trim()||r(e,"empty path"),n++;return`"']/g,e=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[e])}" fill="rgb(${100*o.r}%,${100*o.g}%,${100*o.b}%)" fill-opacity="${l}" fill-rule="${"EVENODD"===t?"evenodd":"nonzero"}"/>`}).join("")}).join("")):""}let o=e.absoluteRenderBounds,l=e.absoluteTransform;(!o||![o.x,o.y,o.width,o.height].every(t)||o.width<=0||o.height<=0)&&r(e,"missing render bounds"),(!Array.isArray(l)||2!==l.length||!l.every(e=>Array.isArray(e)&&3===e.length&&e.every(t))||0!==l[0][1]||0!==l[1][0]||l[0][0]<=0||l[1][1]<=0)&&r(e,"root rotation, reflection or shear requires native export");let s=(o.x-l[0][2])/l[0][0],d=(o.y-l[1][2])/l[1][1],f=o.width/l[0][0],c=o.height/l[1][1],u=function e(n,o){if(++i>1e4&&r(n,"subtree exceeds 10000 nodes"),n.isMask&&r(n,"mask requires native export"),!1===n.visible||0===n.opacity)return"";n.clipsContent&&r(n,"clipping requires native export"),Array.isArray(n.effects)&&n.effects.some(e=>!1!==e.visible)&&r(n,"effect requires native export"),n.blendMode&&!["NORMAL","PASS_THROUGH"].includes(n.blendMode)&&r(n,"layer blending requires native export");let l=void 0===n.opacity?1:n.opacity;(!t(l)||l<0||l>1)&&r(n,"invalid layer opacity");let s="";if(!o){let e=n.relativeTransform;Array.isArray(e)&&2===e.length&&e.every(e=>Array.isArray(e)&&3===e.length&&e.every(t))||r(n,"missing relative transform"),s=` transform="matrix(${e[0][0]} ${e[1][0]} ${e[0][1]} ${e[1][1]} ${e[0][2]} ${e[1][2]})"`}let d="";if(["GROUP","FRAME","INSTANCE","COMPONENT"].includes(n.type))(Array.isArray(n.fills)&&n.fills.some(e=>!1!==e.visible)||Array.isArray(n.strokes)&&n.strokes.some(e=>!1!==e.visible))&&r(n,"container paint requires native export"),Array.isArray(n.children)||r(n,"missing children"),d=(!0===n.itemReverseZIndex?[...n.children].reverse():n.children).map(t=>e(t,!1)).join("");else{for(let e of(["VECTOR","BOOLEAN_OPERATION","RECTANGLE","ELLIPSE","POLYGON","STAR","LINE"].includes(n.type)||r(n,"node type requires native export"),Array.isArray(n.strokes)&&n.strokes.some(e=>!1!==e.visible)&&"CENTER"!==n.strokeAlign&&r(n,"non-center stroke alignment requires native export"),Array.isArray(n.dashPattern)&&n.dashPattern.length&&r(n,"dashed stroke requires native export"),[n.fillGeometry,n.strokeGeometry]))void 0!==e&&(!Array.isArray(e)||e.some(e=>!["NONZERO","EVENODD"].includes(e.windingRule)))&&r(n,"unknown winding rule");d=a(n,n.fillGeometry,n.fills)+a(n,n.strokeGeometry,n.strokes)}return`${d}`}(e,!0);return n||r(e,"no painted geometry"),`${u}`}(e);return n("geometry-fallback",Date.now()-r),t}catch(e){throw Error(`native export: ${i.message||i}; ${e.message||e}`)}}finally{clearTimeout(a)}}let a=Date.now(),o="lookup",l="unknown",s=Math.min(6e5,Math.max(1e3,Number(t.timeoutMs)||9e4)),d={method:"native",stages:[]};function f(e,t,r){d.stages.push({stage:e,elapsedMs:t}),r&&(d.nativeError=r),"geometry-fallback"===e&&(d.method="geometry")}async function c(e){let t;try{return await Promise.race([e(),new Promise((e,r)=>{t=setTimeout(()=>r(Error(`DEVUP_ASSET_TIMEOUT: ${o} exceeded ${s} ms asset budget`)),Math.max(0,s-(Date.now()-a)))})])}finally{clearTimeout(t)}}function u(e){let r=e&&"string"==typeof e.message?e.message:String(e),n="string"==typeof figma.editorType?` (editor: ${figma.editorType})`:"";return`${o} failed after ${Date.now()-a} ms${n} (node: ${t.nodeId}, type: ${l}): ${r}`.slice(0,500)}function p(e,r){return e&&d.renderInfo&&(d.renderInfo.attemptedRepresentation=d.renderInfo.representation,d.renderInfo.representation="not-exported",d.renderInfo.renderedAs="not-exported",d.renderInfo.effectsBakedIntoAsset=!1,d.renderInfo.fidelity="unverified"),{kind:"devupAssetExport",fileKey:figma.fileKey||"",version:t.version,assetId:t.assetId,nodeId:t.nodeId,field:t.field,imageHash:t.imageHash,format:t.format,scale:t.scale,renderMode:h,usageAspectRatio:t.usageAspectRatio??null,maxWidth:t.maxWidth??null,srcset:!0===t.srcset,visibleScopeNodeId:t.visibleScopeNodeId??null,status:"failed",byteLength:null,sha256:null,errorCode:e,exportDiagnostics:d,...r?{errorDetail:r}:{}}}let g="string"==typeof t.field&&t.field.startsWith("$original-image/fills/"),h=void 0===t.renderMode?g?"fill":["svg","pdf"].includes(t.format)?"node":"original-source":t.renderMode,y=["auto","original-source"].includes(h)?"auto"===h?"node":"fill":h;function m(e,r){let n=e=>"number"==typeof e&&Number.isFinite(e)&&e>0,i=Array.isArray(e.effects)?e.effects:null,a=[],o=e=>[...e].slice(0,128).join(""),l=["string"==typeof e.name?o(e.name):"layer"],s=new Set,d=e;for(let t=e.parent;t&&a.length<32&&!s.has(t);t=t.parent)s.add(t),l.length<4&&"string"==typeof t.name&&t.name.length&&!["PAGE","DOCUMENT"].includes(t.type)&&l.push(o(t.name)),a.push({type:t.type||"unknown",clipsContent:t.clipsContent??null,isMask:t.isMask??null,effects:Array.isArray(t.effects)?t.effects:null,opacity:t.opacity??null,blendMode:t.blendMode??null,visible:t.visible??null,siblingMaskPresent:Array.isArray(t.children)?t.children.some(e=>e!==d&&!0===e.isMask):null}),d=t;return{sourceKind:r?"image-fill":t.imageHash?"image-node":"vector-node",renderedAs:"fill"===y?"raw-fill":"node-render",representation:"fill"===y?"original-fill":"native-node",layerHierarchy:l.reverse(),scope:"fill"===y?"selected-paint-original-bytes":"exact-node-subtree",exportSettings:"node"===y?{contentsOnly:!0,useAbsoluteBounds:!1}:null,requestedScale:t.scale,scaleSemantics:"fill"===y?"intrinsic-original-bytes-no-resize":["png","jpg"].includes(t.format)?"native-node-SCALE-constraint":"not-applied-to-vector-document",nodeSize:n(e.width)&&n(e.height)?{width:e.width,height:e.height,aspectRatio:e.width/e.height}:null,outputSize:null,dimensionEvidence:"unknown-until-byte-inspection",aspectRatioComparison:"unknown",scaleMode:r?r.scaleMode??null:null,imageTransform:r?r.imageTransform??null:null,visibleSourceRatio:null,visibleSourceRatioEvidence:"not-computed",effects:i,extraFills:Array.isArray(e.fills)?Math.max(0,e.fills.filter(e=>!1!==e.visible).length-!!r):null,effectsBakedIntoAsset:"node"===y&&["png","jpg"].includes(t.format)&&null!==i,bakedEffects:"node"===y&&["png","jpg"].includes(t.format)&&i?i.filter(e=>!1!==e.visible):[],instanceOverride:"unknown",instanceOverrideEvidence:"no-component-paint-comparison",sourceProperties:{scaleMode:r?r.scaleMode??null:null,imageTransform:r?r.imageTransform??null:null,rotation:r?r.rotation??null:null,filters:r?r.filters??null:null,opacity:r?r.opacity??null:null,effects:i,blurEffects:i?i.filter(e=>!1!==e.visible&&String(e.type).includes("BLUR")):null,isMask:e.isMask??null,clipsContent:e.clipsContent??null,ancestors:a},unappliedProperties:"fill"===y?["scaleMode","imageTransform","rotation","filters","paintOpacity","nodeGeometry","nodeTransforms","nodeEffectsIncludingBlur","nodeAndAncestorMasks","ancestorClipsAndEffects"]:[],warnings:"fill"===y?["original-fill-properties-unapplied","aspect-ratio-unknown"]:["ancestor-canvas-appearance-unverified","aspect-ratio-unknown"],fidelity:"unverified"}}function b(e,t){let r=0;for(let n=e;n&&r++<32;n=n.parent){if("INSTANCE"===n.type&&Array.isArray(n.overrides)&&n.overrides.find(t=>t.id===e.id&&Array.isArray(t.overriddenFields)&&t.overriddenFields.includes("fills"))){t.instanceOverride=!0,t.instanceOverrideEvidence={kind:"figma-instance-overrides",instanceId:n.id,nodeId:e.id,overriddenFields:["fills"]};return}if(["PAGE","DOCUMENT"].includes(n.type))break}}try{let e;if(!["original-source","fill","node","visible","auto"].includes(h))return p("DEVUP_ASSET_RENDER_MODE_UNSUPPORTED");if(!g&&"fill"===h&&(!String(t.field).startsWith("fills/")||!["png","jpg","webp"].includes(t.format)||1!==t.scale))return p("DEVUP_ASSET_FILL_SETTINGS_UNSUPPORTED","Original fill requires fills/N, png or jpg matching its codec, and scale 1; no resizing or transcoding.");if((g||["fill","original-source"].includes(h))&&"bridge"!==t.transport)return p(g?"DEVUP_ORIGINAL_IMAGE_REQUIRES_BRIDGE":"DEVUP_ASSET_FILL_REQUIRES_BRIDGE");let x=await c(()=>n(t.nodeId));if(f("lookup",Date.now()-a),l=x&&x.type||"unknown",!x||["node","visible"].includes(h)&&"function"!=typeof x.exportAsync)return p("DEVUP_ASSET_UNSUPPORTED_BY_UPSTREAM",u(Error(x?"node has no exportAsync":"node not found")));let E=null;if(g||"string"==typeof t.field&&t.field.startsWith("fills/")){let e=Number(t.field.slice(g?22:6)),r="fills"in x&&Array.isArray(x.fills)?x.fills:[],n=Number.isInteger(e)?r[e]:null,i=n&&"IMAGE"===n.type?n.imageHash||n.imageRef:null;if(!n||i!==t.imageHash)return p("DEVUP_ASSET_SOURCE_CHANGED");E=n}else if("node"!==t.field)return p("DEVUP_ASSET_FIELD_UNSUPPORTED");if("original-source"===h&&(!E||!t.imageHash))return p("DEVUP_ORIGINAL_SOURCE_UNAVAILABLE","No IMAGE fill source exists for this selection. Choose an IMAGE fill or explicitly renderMode node; vector assets should retain SVG.");let A=x;if("visible"===h){let e=t.visibleScopeNodeId;if(!e)return p("DEVUP_VISIBLE_CONTEXT_UNAVAILABLE","Recollect the parent frame and provide visibleScopeNodeId.");let r=new Set;for(;A&&A.id!==e&&!r.has(A)&&r.size<64;)r.add(A),A=A.parent;if(!A||A.id!==e||["PAGE","DOCUMENT"].includes(A.type)||"function"!=typeof A.exportAsync)return p("DEVUP_VISIBLE_SCOPE_UNSUPPORTED","The selected capture scope is not an exportable ancestor of the requested node.")}if(d.renderInfo=m(x,E),"visible"===h){let e=d.renderInfo;e.representation="native-visible",e.renderedAs="visible-scoped-capture",e.captureNodeId=A.id,e.captureBox={width:A.width,height:A.height,aspectRatio:A.width/A.height},e.contextScope="selected-ancestor-subtree",e.captureEffects=Array.isArray(A.effects)?A.effects:null,e.requestedNodeEffects=e.effects,e.effects=e.captureEffects,e.sourceProperties.captureEffects=e.captureEffects,e.visibleContext={requestedNodeId:x.id,captureNodeId:A.id,included:["selected-scope-children","selected-scope-clipping","selected-scope-masks","selected-scope-rounding"],outsideScope:"unknown; ancestors, page backdrop and external layers are not included",canvasReferenceCompared:!1},e.warnings.push("visible-scoped-capture","outside-capture-context-unverified")}b(x,d.renderInfo);let I=null;if(E)try{let r=figma.getImageByHash(t.imageHash),n=r&&await Promise.race([r.getSizeAsync(),new Promise(t=>{e=setTimeout(()=>t(null),250)})]);I=n&&n.width>0&&n.height>0&&Number.isFinite(n.width)&&Number.isFinite(n.height)?{...n,aspectRatio:n.width/n.height}:null}catch(e){}finally{clearTimeout(e)}let S=null;if("auto"===h){let e=function(e,r,n){let i,a=[],o=(e,t)=>{e||a.push(t)},l=e=>Array.isArray(e)&&2===e.length&&e.every(e=>Array.isArray(e)&&3===e.length&&e.every(Number.isFinite))&&1===e[0][0]&&1===e[1][1]&&0===e[0][1]&&0===e[1][0]&&Number.isInteger(e[0][2])&&Number.isInteger(e[1][2]);for(let a of(o("bridge"===t.transport,"raw-requires-bridge"),o(1===t.scale&&["png","jpg"].includes(t.format),"raw-requires-unscaled-raster"),o(!!r&&!0===r.visible&&1===r.opacity&&"NORMAL"===r.blendMode,"paint-compositing-or-unknown"),o(r&&"FILL"===r.scaleMode&&Array.isArray(i=r.imageTransform)&&2===i.length&&i.every((e,t)=>Array.isArray(e)&&3===e.length&&e.every((e,r)=>e===[[1,0,0],[0,1,0]][t][r]))&&0===r.rotation,"paint-transform-or-unknown"),o(r&&r.filters&&Object.values(r.filters).every(e=>0===e),"image-filters-or-unknown"),o(Array.isArray(e.fills)&&1===e.fills.length,"multiple-fills-or-unknown"),o("RECTANGLE"===e.type&&0===e.cornerRadius&&Array.isArray(e.strokes)&&0===e.strokes.length&&(!e.children||0===e.children.length),"node-geometry-or-unknown"),o(Array.isArray(e.effects)&&0===e.effects.length&&!1===e.isMask&&(void 0===e.clipsContent||!1===e.clipsContent)&&1===e.opacity&&!0===e.visible&&"NORMAL"===e.blendMode,"node-effects-mask-compositing-or-unknown"),o(l(e.absoluteTransform)&&l(e.relativeTransform),"node-transform-or-unknown"),o(n.sourceProperties.ancestors.length>0&&n.sourceProperties.ancestors.some(e=>["PAGE","DOCUMENT"].includes(e.type)),"ancestor-chain-incomplete"),o(n.sourceProperties.ancestors.every(e=>!1===e.siblingMaskPresent),"ancestor-sibling-mask-or-unknown"),n.sourceProperties.ancestors.filter(e=>!["PAGE","DOCUMENT"].includes(e.type))))o(!1===a.clipsContent&&!1===a.isMask&&!1===a.siblingMaskPresent&&Array.isArray(a.effects)&&0===a.effects.length&&1===a.opacity&&!0===a.visible&&["NORMAL","PASS_THROUGH"].includes(a.blendMode),"ancestor-dependency-or-unknown");return{eligible:0===a.length,reasons:[...new Set(a)],evidence:"explicit-plugin-properties; intrinsic-dimensions-and-codec-required"}}(x,E,d.renderInfo);if(e.eligible)try{let r=figma.getImageByHash(t.imageHash),n=I;n&&n.width===x.width&&n.height===x.height?(S=await c(()=>r.getBytesAsync()),(137===S[0]&&80===S[1]&&78===S[2]&&71===S[3]?"png":255===S[0]&&216===S[1]&&255===S[2]?"jpg":"unknown")!==t.format&&e.reasons.push("source-codec-mismatch-or-unknown")):e.reasons.push("intrinsic-dimensions-mismatch-or-unknown")}catch(t){e.reasons.push("source-inspection-unavailable")}e.eligible=0===e.reasons.length,y=e.eligible?"fill":"node",d.renderInfo=m(x,E),b(x,d.renderInfo),d.renderInfo.autoDecision={...e,selected:y}}if(d.renderInfo.sourceSize=I,d.renderInfo.sourceSizeEvidence=I?"Figma-image-getSizeAsync":"unknown",g||"fill"===y){d.method="original-fill",o="reading the original image";let e=figma.getImageByHash(t.imageHash);if(!e)return p("DEVUP_ORIGINAL_IMAGE_NOT_FOUND");let n=S||await c(()=>e.getBytesAsync());if(0===n.length||n.length>0x5000000)return p("DEVUP_ASSET_RESPONSE_TOO_LARGE",`${n.length} bytes; one answer carries 1 to 83886080`);let i=e=>e.every((e,t)=>n[t]===e),a=i([137,80,78,71,13,10,26,10])?"image/png":i([255,216,255])?"image/jpeg":i([71,73,70,56])?"image/gif":i([82,73,70,70])&&87===n[8]&&69===n[9]&&66===n[10]&&80===n[11]?"image/webp":null;if(!a)return p("DEVUP_ORIGINAL_IMAGE_CODEC_UNSUPPORTED");if(d.renderInfo.originalMimeType=a,!g&&"fill"===h&&"webp"!==t.format&&a!==({png:"image/png",jpg:"image/jpeg"})[t.format])return p("DEVUP_ASSET_FILL_CODEC_MISMATCH",`Original fill codec is ${a}, but ${t.format} was requested; choose the matching format or renderMode node. No transcoding was performed.`);if(!g)return{...p(null),status:"exported",mimeType:a,byteLength:n.length,sha256:r(n),data:figma.base64Encode(n)};let l=await c(()=>e.getSizeAsync());return{...p(null),status:"exported",kind:"devupOriginalImage",representation:"original-image-v1",format:null,scale:null,mimeType:a,width:l.width,height:l.height,byteLength:n.length,sha256:r(n),data:figma.base64Encode(n)}}let w="webp"===t.format?"PNG":String(t.format||"").toUpperCase();if(!["PNG","JPG","SVG","PDF"].includes(w))return p("DEVUP_ASSET_FORMAT_UNSUPPORTED");let v=Number(t.scale??2);if(!Number.isFinite(v)||v<=0||v>4)return p("DEVUP_ASSET_SCALE_INVALID");let N="SVG"===w,O={format:N?"SVG_STRING":w,contentsOnly:!0,useAbsoluteBounds:!1};("PNG"===w||"JPG"===w)&&(O.constraint={type:"SCALE",value:v}),o="export";let T=Math.max(1,s-(Date.now()-a)),_=N?Math.max(1,T-Math.min(5e3,Math.floor(T/5))):T,P=Date.now(),M=await c(()=>"visible"===h?A.exportAsync(O):E?x.exportAsync(O):i(x,O,_,f));if(E&&f("native",Date.now()-P),"geometry"===d.method&&(d.renderInfo.representation="geometry-fallback",d.renderInfo.renderedAs="geometry-fallback",d.renderInfo.effectsBakedIntoAsset=!1,d.renderInfo.warnings.push("geometry-fallback-not-native-render")),Date.now()-a>=s)throw Error(`DEVUP_ASSET_TIMEOUT: export exceeded ${s} ms asset budget`);o="encode";let C=Date.now(),k=N&&"string"==typeof M?M:null,R=null===k?M instanceof Uint8Array?M:new Uint8Array(M):function(e){let t=[];for(let r=0;r=55296&&n<=56319){let t=r+1=56320&&t<=57343?(n=65536+(n-55296<<10)+(t-56320),r+=1):n=65533}else n>=56320&&n<=57343&&(n=65533);n<128?t.push(n):n<2048?t.push(192|n>>6,128|63&n):n<65536?t.push(224|n>>12,128|n>>6&63,128|63&n):t.push(240|n>>18,128|n>>12&63,128|n>>6&63,128|63&n)}return new Uint8Array(t)}(k);if(0===R.length||R.length>0x5000000)return p("DEVUP_ASSET_RESPONSE_TOO_LARGE",`${w} at scale ${v} is ${R.length} bytes; one answer carries 1 to 83886080`);let D=r(R);f("encode",Date.now()-C);let L=figma.io&&"function"==typeof figma.io.write,$="PNG"===w&&R.length>786432;if(L&&(null!==k&&R.length>12288||$))return{kind:"devupAssetExport",fileKey:figma.fileKey||"",version:t.version,assetId:t.assetId,nodeId:t.nodeId,field:t.field,imageHash:t.imageHash,format:t.format,scale:v,renderMode:h,usageAspectRatio:t.usageAspectRatio??null,maxWidth:t.maxWidth??null,srcset:!0===t.srcset,visibleScopeNodeId:t.visibleScopeNodeId??null,status:"chunked",byteLength:R.length,sha256:D,cursor:{nextOffset:0,maxChunkBytes:12288},errorCode:null,exportDiagnostics:d};return L&&figma.io.write(`devup-asset-${t.assetId.replace(/[^A-Za-z0-9_-]/g,"_")}.${w.toLowerCase()}`,R),{kind:"devupAssetExport",fileKey:figma.fileKey||"",version:t.version,assetId:t.assetId,nodeId:t.nodeId,field:t.field,imageHash:t.imageHash,format:t.format,scale:v,renderMode:h,usageAspectRatio:t.usageAspectRatio??null,maxWidth:t.maxWidth??null,srcset:!0===t.srcset,visibleScopeNodeId:t.visibleScopeNodeId??null,status:"exported",byteLength:R.length,sha256:D,mimeType:L?null===k?null:"image/svg+xml":({PNG:"image/png",JPG:"image/jpeg",SVG:"image/svg+xml",PDF:"application/pdf"})[w],...!L&&null===k?{data:figma.base64Encode(R)}:{},text:k,errorCode:null,exportDiagnostics:d}}catch(e){return p(String(e.message||e).includes("DEVUP_ASSET_TIMEOUT:")?"DEVUP_ASSET_TIMEOUT":g||["fill","original-source"].includes(h)?"DEVUP_ORIGINAL_IMAGE_READ_FAILED":"DEVUP_ASSET_EXPORT_FAILED",u(e))}},bridgeContext:async function(e){let t=figma.currentPage,r=t.selection,n=e=>({id:e.id,name:String(e.name).slice(0,256),type:e.type});return{kind:"devup-bridge-context",currentPage:n(t),selection:r.slice(0,20).map(n),selectionCount:r.length,zoom:Number.isFinite(figma.viewport?.zoom)?figma.viewport.zoom:null,observedAt:Date.now(),lastKnownSelection:globalThis.__devupLastSelection??null,lastKnownPage:globalThis.__devupLastSelectionPage??null}},explore:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=await figma.getNodeByIdAsync(e.nodeId);if(!t)throw Error("DEVUP_NODE_NOT_FOUND");let r=e.explore,n=Math.max(20,Math.min(400,Number.isInteger(r.projectionLimit)?r.projectionLimit:200)),i=Math.max(0,Math.min(500,Number.isInteger(r.textPreviewLimit)?r.textPreviewLimit:160));function a(e){return"string"==typeof e?e.slice(0,240):""}let o="PAGE"===t.type,l=t,s="SECTION"===t.type?t:null;for(;!o&&l.parent&&"PAGE"!==l.parent.type;)l=l.parent,s||"SECTION"!==l.type||(s=l);let d="PAGE"===t.type?t:l.parent;if(!d||"PAGE"!==d.type)throw Error("DEVUP_PAGE_NOT_FOUND");function f(e){let t=e.absoluteBoundingBox||{x:"number"==typeof e.x?e.x:0,y:"number"==typeof e.y?e.y:0,width:"number"==typeof e.width?e.width:0,height:"number"==typeof e.height?e.height:0};return[t.x,t.y,t.width,t.height].every(Number.isFinite)?{x:t.x,y:t.y,width:t.width,height:t.height}:null}function c(e){let t=f(e);return t&&["FRAME","COMPONENT","INSTANCE","COMPONENT_SET"].includes(e.type)&&t.width>=240&&t.width<=1800&&t.height>=300&&t.height<=2e3&&t.width/Math.max(1,t.height)>=.25&&t.width/Math.max(1,t.height)<=2.5}function u(e){let t=[],r=e;for(;r&&"DOCUMENT"!==r.type&&t.length<12;){let e=a(r.name);e&&t.push(e),r=r.parent}return t.reverse()}await figma.setCurrentPageAsync(d);let p=o?d:!c(t)&&s?s:l,g=f(p)||f(t);if(!g)throw Error("DEVUP_NODE_BOUNDS_UNAVAILABLE");let h="children"in d?d.children:[],y=h.map((e,t)=>({node:e,pageChildIndex:t,bounds:f(e)})).filter(e=>e.bounds).filter(e=>{var t;return!!o||e.node.id===p.id||e.bounds.y>=g.y-240&&e.bounds.y<=g.y+12e3&&(t=e.bounds,Math.min(t.x+t.width,g.x+g.width)-Math.max(t.x,g.x)>0)}).sort((e,t)=>e.bounds.y-t.bounds.y||e.bounds.x-t.bounds.x||e.node.id.localeCompare(t.node.id)),m="SECTION"===p.type?y.filter(e=>e.node.id===p.id):y.slice(0,n),b=new Map(m.map(e=>[e.node.id,e]));b.has(l.id)||b.set(l.id,{node:l,pageChildIndex:-1,bounds:f(l)}),b.has(p.id)||b.set(p.id,{node:p,pageChildIndex:-1,bounds:f(p)}),b.has(t.id)||b.set(t.id,{node:t,pageChildIndex:-1,bounds:f(t)});let x=o?null:t.parent;for(;x&&"PAGE"!==x.type&&(b.has(x.id)||b.set(x.id,{node:x,pageChildIndex:-1,bounds:f(x)}),x.id!==p.id);)x=x.parent;if("children"in p)for(let e of p.children.slice(0,n))b.has(e.id)||b.set(e.id,{node:e,pageChildIndex:-1,bounds:f(e)});let E="SECTION"===p.type||o,A=E&&"children"in p&&p.children.length>n;if(E&&"children"in p){let e=8*n,t=p.children.filter(e=>!c(e)).map(e=>({node:e,ancestors:[]})),r=0;for(;t.length&&r!b.has(e.id));if(b.size+t.length>n+2){A=!0;continue}for(let e of t)b.set(e.id,{node:e,pageChildIndex:-1,bounds:f(e)});continue}if("children"in e)for(let r of e.children)o&&["FRAME","COMPONENT","INSTANCE","COMPONENT_SET"].includes(e.type)||t.push({node:r,ancestors:[...i,e]})}A||=t.length>0}let I=[...b.values()].filter(e=>e.bounds).slice(0,n+2).map(({node:e,pageChildIndex:t,bounds:r})=>{let n;return{id:e.id,type:e.type,fields:{name:a(e.name),parentId:e.parent&&"DOCUMENT"!==e.parent.type?e.parent.id:null,childrenIds:[],x:r.x,y:r.y,width:r.width,height:r.height,childCount:"children"in e?e.children.length:0,textPreview:"",...o||null===(n=function(e){let t=[e],r=0,n=null;for(;t.length&&r++<400;){let e=t.shift();if("visible"in e&&!1===e.visible)continue;let r="children"in e?e.children.filter(e=>!("visible"in e)||!1!==e.visible):[];if(/header|nav|gnb/i.test(e.name||"")&&r.length>=2&&"layoutMode"in e&&"HORIZONTAL"===e.layoutMode){let t=r.map(f);t.every(Boolean)&&(n=Math.max(n||0,t.reduce((e,t)=>e+t.width,0)+(Number(e.itemSpacing)||0)*(t.length-1)))}t.push(...r.slice(0,80))}return n}(e))?{}:{navigationContentWidth:n},pageChildIndex:t>=0?t:null,visible:!("visible"in e)||!1!==e.visible,breadcrumb:u(e)},extra:{},fieldErrors:{}}}),S=I.reduce((e,t)=>{let r=t.fields;return e?{x:Math.min(e.x,r.x),y:Math.min(e.y,r.y),right:Math.max(e.right,r.x+r.width),bottom:Math.max(e.bottom,r.y+r.height)}:{x:r.x,y:r.y,right:r.x+r.width,bottom:r.y+r.height}},null)||{x:0,y:0,right:0,bottom:0},w="SECTION"!==p.type&&y.length>m.length||b.size>n+2||A,v={id:d.id,type:d.type,fields:{name:a(d.name),parentId:null,childrenIds:[],x:S.x,y:S.y,width:S.right-S.x,height:S.bottom-S.y,childCount:h.length,textPreview:"",projectionTruncated:w,visible:!0,breadcrumb:u(d),pageChildIndex:null},extra:{},fieldErrors:{}},N={fileKey:figma.fileKey||"",version:null,rootIds:[d.id],nodes:[v,...I.filter(e=>e.id!==d.id)],diagnostics:[]},O=new Set([d.id,l.id,p.id,t.id]);for(;JSON.stringify(N).length>14e3;){let e=N.nodes.length-1;for(;e>=0&&O.has(N.nodes[e].id);)e-=1;if(e<0)break;N.nodes.splice(e,1),v.fields.projectionTruncated=!0}if(JSON.stringify(N).length>14e3&&(N.nodes=N.nodes.filter(e=>O.has(e.id)).map(e=>({...e,fields:{...e.fields,name:a(e.fields.name).slice(0,80),textPreview:"",breadcrumb:e.fields.breadcrumb.slice(-4).map(e=>e.slice(0,80)),projectionTruncated:e.id===d.id||e.fields.projectionTruncated}}))),JSON.stringify(N).length>14e3)throw Error("DEVUP_EXPLORE_PROJECTION_TOO_LARGE");function T(e){let t=0;for(let r of e){let e=r.codePointAt(0);t+=e<128?1:e<2048?2:e<65536?3:4}return t}if(o){for(;T(JSON.stringify(N))>19456&&N.nodes.length>1;)N.nodes.pop(),v.fields.projectionTruncated=!0;if(T(JSON.stringify(N))>19456)throw Error("DEVUP_EXPLORE_PROJECTION_TOO_LARGE")}let _=14e3-JSON.stringify(N).length;if(o&&(_=Math.min(_,19456-T(JSON.stringify(N)))),i>0&&_>0){let e=N.nodes.filter(e=>b.get(e.id)&&e.id!==d.id),t=e.length;for(let r of e){let e=Math.floor(_/t);if(t-=1,e<=0)continue;let n="",a=0;for(let t of function(e){if(0===i)return"";let t=[],r=[e],n=0;for(;r.length&&n<80&&t.join(" ").lengthe)break;n+=t,a+=r}r.fields.textPreview=n,_-=a}}return N},fastSnapshot:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=e.rootIds;if(!Array.isArray(t)||0===t.length)throw Error("DEVUP_ROOTS_INVALID");let r=await Promise.all(t.map(e=>figma.getNodeByIdAsync(e)));if(r.some(e=>!e))throw Error("DEVUP_NODE_NOT_FOUND");if(1===r.length&&"SECTION"===r[0].type)throw Error("DEVUP_TARGET_IS_SECTION");let n=["mobile","tablet","desktop"],i=e=>{let t=String(e.name||"").trim().toLowerCase(),r=n.indexOf(t);if(r>=0)return r;let i=t.match(/^(\d+)\s*[~–]\s*(\d+)$/);if(!i)return -1;let a=Number(i[1]),o=Number(i[2]);return Number.isSafeInteger(a)&&Number.isSafeInteger(o)&&a>0&&o>=a&&o<=0xffffffff?1:-1};if(1===r.length&&i(r[0])>=0){let e=r[0].parent;if(e&&"SECTION"===e.type&&"children"in e){let t=e.children.filter(e=>e.id===r[0].id||i(e)>=0).filter(e=>!1!==e.visible);t.length>1&&(r.length=0,r.push(...t))}}if(1===t.length){let e=[],t=new Set(r.map(e=>e.id)),n=[],i=e=>{if("reactions"in e&&Array.isArray(e.reactions)){for(let r of e.reactions)if(r&&r.trigger&&"AFTER_TIMEOUT"===r.trigger.type)for(let e of r.actions||[])e&&"NODE"===e.type&&e.transition&&"SMART_ANIMATE"===e.transition.type&&"string"==typeof e.destinationId&&!t.has(e.destinationId)&&(t.add(e.destinationId),n.push(e.destinationId));if("children"in e)for(let t of e.children)i(t)}};for(let e of r)i(e);for(;n.length>0;){let t=n.shift(),r=await figma.getNodeByIdAsync(t);r&&"DOCUMENT"!==r.type&&"PAGE"!==r.type&&(e.push(r),i(r))}r.push(...e)}let a=e.nodeId,o=["absoluteBoundingBox","absoluteRenderBounds","arcData","backgroundStyleId","blendMode","bottomLeftRadius","bottomRightRadius","boundVariables","characters","clipsContent","componentProperties","componentPropertyDefinitions","componentPropertyReferences","constraints","cornerRadius","counterAxisAlignItems","dashPattern","defaultVariant","effectStyleId","effects","fillStyleId","fills","fontName","fontSize","gridColumnAnchorIndex","gridColumnCount","gridColumnGap","gridColumnSizes","gridColumnSpan","gridRowAnchorIndex","gridRowCount","gridRowGap","gridRowSizes","gridRowSpan","gridStyleId","height","inferredAutoLayout","isAsset","isMask","itemSpacing","layoutGrow","layoutMode","layoutWrap","layoutPositioning","layoutSizingHorizontal","layoutSizingVertical","letterSpacing","lineHeight","maxHeight","maxLines","maxWidth","minHeight","minWidth","name","opacity","overflowDirection","paddingBottom","paddingLeft","paddingRight","paddingTop","primaryAxisAlignItems","reactions","rotation","strokeAlign","strokeBottomWeight","strokeLeftWeight","strokeRightWeight","strokeStyleId","strokeTopWeight","strokeWeight","strokes","targetAspectRatio","textAlignHorizontal","textAlignVertical","textAutoResize","textCase","textDecoration","textStyleId","textTruncation","topLeftRadius","topRightRadius","variantProperties","visible","width","x","y"],l=["fontName","fontWeight","fontSize","textDecoration","textCase","lineHeight","letterSpacing","fills","textStyleId","fillStyleId","listOptions","indentation","hyperlink"],s=e.snapshot,d=Math.max(0,Math.floor(Number(s.offset)||0)),f=Math.max(8192,Math.floor(Number(s.maxEnvelopeBytes)||19456)),c=Math.min(s.maxEnvelopeBytes?f-1024:18e3,Math.max(4096,Math.floor(Number(s.maxPayloadBytes)||15e3))),u=new Set(["backgroundStyleId","effectStyleId","fillStyleId","gridStyleId","strokeStyleId","textStyleId"]),p=new Set(["maxWidth","maxHeight","absoluteRenderBounds"]),g=new Map([["rotation",0],["cornerRadius",0],["isAsset",!1],["isMask",!1],["clipsContent",!1],["blendMode","PASS_THROUGH"],["strokeAlign","INSIDE"],["textCase","ORIGINAL"],["textDecoration","NONE"],["textAlignHorizontal","LEFT"],["textAlignVertical","TOP"],["counterAxisAlignItems","MIN"],["primaryAxisAlignItems","MIN"],["gridColumnCount",0],["gridRowCount",0],["gridColumnGap",0],["gridRowGap",0],["gridColumnAnchorIndex",-1],["gridRowAnchorIndex",-1],["gridColumnSpan",1],["gridRowSpan",1]]),h=new Set(["start","end","characters","fontWeight","textStyleId","fillStyleId","listOptions","indentation","hyperlink"]),y=new Set(["parent","children","consumers"]);function m(e,t=!1,r=new WeakSet,n=0){let i;if(null===e||["string","number","boolean"].includes(typeof e))return e;if(void 0===e)return{$undefined:!0};if("bigint"==typeof e)return{$bigint:e.toString()};if(["function","symbol"].includes(typeof e))return{$unsupported:typeof e};if(n>12)return{$truncated:"max-depth"};if("object"==typeof e&&"parent"in e&&"string"==typeof e.id&&"string"==typeof e.type)return{$nodeId:e.id,$nodeType:e.type};if(Array.isArray(e))return e.map(e=>m(e,t,r,n+1));if(ArrayBuffer.isView(e))return{$binary:e.constructor.name,byteLength:e.byteLength};if(e instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:e.byteLength};if(r.has(e))return{$circular:!0};if(r.add(e),t){let t=new Set(Object.keys(e)),r=e;for(;r&&r!==Object.prototype;){for(let e of Object.getOwnPropertyNames(r))t.add(e);r=Object.getPrototypeOf(r)}i=[...t].sort().filter(e=>!e.startsWith("_")&&!y.has(e))}else i=Object.keys(e).sort();let a={};for(let o of i)try{let i=m(e[o],t,r,n+1);i&&"function"===i.$unsupported||(a[o]=i)}catch(e){a[o]=t?{$error:"unavailable"}:{$error:String(e&&e.message?e.message:e)}}return r.delete(e),a}let b=new Map;async function x(e,t,r){if("INSTANCE"!==e.type||"function"!=typeof e.getMainComponentAsync)return;b.has(e.id)||b.set(e.id,(async()=>{try{let t=await e.getMainComponentAsync();return{fields:{mainComponentId:t?t.id:null,mainComponentName:t?t.name:null,mainComponentSetId:t&&t.parent&&"COMPONENT_SET"===t.parent.type?t.parent.id:null}}}catch(e){return{error:String(e&&e.message?e.message:e)}}})());let n=await b.get(e.id);n.error?r.mainComponentId=n.error:Object.assign(t,n.fields)}async function E(e){let t={},r={};e.parent&&(t.parentId=e.parent.id),e.parent&&("PAGE"===e.parent.type||"SECTION"===e.parent.type||"COMPONENT_SET"===e.parent.type)&&(t.parentType=e.parent.type,"SECTION"===e.parent.type&&(t.parentName=e.parent.name));let n="children"in e?e.children.map(e=>e.id):[];for(let i of(n.length>0&&(t.childrenIds=n),o)){let n;if("componentPropertyDefinitions"!==i||"COMPONENT"!==e.type||!e.parent||"COMPONENT_SET"!==e.parent.type)try{if(!(i in e)){"overflowDirection"===i&&["FRAME","COMPONENT","INSTANCE","COMPONENT_SET"].includes(e.type)&&(t[i]=null);continue}if(n=e[i],"function"==typeof n)continue;let r=m(n);if("overflowDirection"===i&&null==n){t[i]=null;continue}(null===r?!p.has(i):Array.isArray(r)?0===r.length:"object"==typeof r?0===Object.keys(r).length:!!(""===r&&u.has(i))||g.has(i)&&g.get(i)===r)||(t[i]=r)}catch(e){r[i]=String(e&&e.message?e.message:e)}}if("TEXT"===e.type&&"function"==typeof e.getStyledTextSegments)try{let r=m(e.getStyledTextSegments(l));if(1===r.length){let e=r[0];for(let t of Object.keys(e))h.has(t)||delete e[t]}r.length>0&&(t.styledTextSegments=r)}catch(e){r.styledTextSegments=String(e&&e.message?e.message:e)}await x(e,t,r);let i={id:e.id,type:e.type,fields:t};return Object.keys(r).length>0&&(i.fieldErrors=r),i}let A=[],I=[...r],S=new Set;for(let e=0;e=A.length&&A.length>0)throw Error("DEVUP_SNAPSHOT_RANGE_INVALID");function w(e){let t=0;for(let r=0;r=55296&&n<=56319&&r+1({id:e,styleType:t})).sort((e,t)=>e.id.localeCompare(t.id));async function a(e){try{let t=await figma.variables.getVariableByIdAsync(e);return t?{kind:"variable",value:m(t,!0),collectionId:t.variableCollectionId,aliasIds:Object.values(t.valuesByMode||{}).filter(e=>e&&"VARIABLE_ALIAS"===e.type&&"string"==typeof e.id&&e.id).map(e=>e.id)}:{kind:"unresolved",value:{id:e,kind:"variable",reason:"notFoundOrUnavailable"}}}catch(t){return{kind:"unresolved",value:{id:e,kind:"variable",reason:"notFoundOrUnavailable",error:String(t&&t.message||t).slice(0,300)}}}}let o=await Promise.all([...n.map(a),...i.map(async({id:e,styleType:t})=>{try{let r=await figma.getStyleByIdAsync(e);if(!r)return{kind:"unresolved",value:{id:e,kind:"style",reason:"notFoundOrUnavailable"}};return{kind:"style",value:{...m(r,!0),styleType:t,value:m("PAINT"===t?r.paints:"EFFECT"===t?r.effects:"GRID"===t?r.layoutGrids:r,!0)}}}catch(t){return{kind:"unresolved",value:{id:e,kind:"style",reason:"notFoundOrUnavailable",error:String(t&&t.message||t).slice(0,300)}}}})]),l=new Set(n),s=o.flatMap(e=>e.aliasIds||[]);for(;s.length>0;){let e=[...new Set(s)].filter(e=>!l.has(e)).sort();for(let t of e)l.add(t);let t=await Promise.all(e.map(a));o.push(...t),s=t.flatMap(e=>e.aliasIds||[])}let d=[...new Set(o.filter(e=>"variable"===e.kind&&e.collectionId).map(e=>e.collectionId))].sort(),f=(await Promise.all(d.map(async e=>{try{let t=await figma.variables.getVariableCollectionByIdAsync(e);return t?m(t,!0):null}catch(e){return null}}))).filter(e=>null!==e),c=o.filter(e=>"variable"===e.kind).map(e=>e.value),u=o.filter(e=>"style"===e.kind).map(e=>e.value),p=o.filter(e=>"unresolved"===e.kind).map(e=>e.value);return{collections:f,variables:c,styles:u,usedRemoteVariables:c.filter(e=>!0===e.remote),usedVariableIds:n,usedStyleIds:i.map(e=>e.id),localComplete:!1,usedRemoteComplete:0===p.length,unresolved:p,$variableRefCount:n.length,$styleRefCount:i.length}}async function N(e){let t=[],r=2;for(let n=d;ne)break;t.push(i),r+=a}return{pageNodes:t,packedBytes:r}}let O=c-1024,T=null;for(let e=0;e<5;e+=1){let{pageNodes:e,packedBytes:t}=await N(O);if(0===e.length)throw Error("DEVUP_SNAPSHOT_RANGE_INVALID");let n=function(e,t){let n=Math.min(A.length,d+e.length),{$variableRefCount:i,$styleRefCount:o,...l}=t,s=[...e,{id:"__DEVUP_SNAPSHOT_CURSOR__",type:"DEVUP_INTERNAL",fields:{offset:d,nextOffset:n,complete:n>=A.length,totalNodes:A.length},extra:{},fieldErrors:{}}],f={kind:"devupFastSnapshotEnvelope",schemaVersion:1,source:{fileKey:figma.fileKey||"",rootId:a},snapshot:{fileKey:figma.fileKey||"",version:null,rootIds:r.map(e=>e.id),nodes:s,diagnostics:[]},resources:l,integrity:{nodeCount:s.length,variableRefCount:i,styleRefCount:o,utf8Bytes:0}},c=0;for(let e=0;e<8&&(c=w(JSON.stringify(f)),f.integrity.utf8Bytes!==c);e+=1)f.integrity.utf8Bytes=c;if(f.integrity.utf8Bytes!==w(JSON.stringify(f)))throw Error("DEVUP_ENVELOPE_LENGTH_UNSTABLE");return{envelope:f,bytes:c}}(e,await v(e));if(n.bytes<=f){T=n;break}if(1===e.length)throw Error("DEVUP_ENVELOPE_TOO_LARGE");O=Math.max(1,Math.min(O-1,t-(n.bytes-f)-256))}if(!T)throw Error("DEVUP_ENVELOPE_TOO_LARGE");return T.envelope},fastTheme:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=Math.max(0,Math.floor(Number(e.theme.offset)||0));function r(e,t=new WeakSet,n=0){if(null===e||["string","number","boolean"].includes(typeof e))return e;if(void 0===e)return{$undefined:!0};if("bigint"==typeof e)return{$bigint:e.toString()};if(["function","symbol"].includes(typeof e))return{$unsupported:typeof e};if(n>12)return{$truncated:"max-depth"};if("object"==typeof e&&"parent"in e&&"string"==typeof e.id&&"string"==typeof e.type)return{$nodeId:e.id,$nodeType:e.type};if(Array.isArray(e))return e.map(e=>r(e,t,n+1));if(ArrayBuffer.isView(e))return{$binary:e.constructor.name,byteLength:e.byteLength};if(e instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:e.byteLength};if(t.has(e))return{$circular:!0};t.add(e);let i={};for(let a of function(e){let t=new Set(Object.keys(e)),r=e;for(;r&&r!==Object.prototype;){for(let e of Object.getOwnPropertyNames(r))t.add(e);r=Object.getPrototypeOf(r)}return[...t].sort()}(e))if(!(a.startsWith("_")||["parent","children","consumers"].includes(a)))try{let o=r(e[a],t,n+1);o&&"function"===o.$unsupported||(i[a]=o)}catch(e){i[a]={$error:"unavailable"}}return t.delete(e),i}let n=new Set,i=new Map;for(let e of figma.root.children)await e.loadAsync();for(let e of[figma.root,...figma.root.findAll(()=>!0)])for(let t of["boundVariables","fills","strokes","effects","layoutGrids","textStyleId","fillStyleId","strokeStyleId","backgroundStyleId","effectStyleId","gridStyleId"])try{!function e(t,r="",a=new WeakSet,o=0){if(!(o>16)&&null!==t&&"object"==typeof t&&!a.has(t)){if(a.add(t),Array.isArray(t)){for(let n of t)e(n,r,a,o+1);return}for(let[l,s]of("VARIABLE_ALIAS"===t.type&&"string"==typeof t.id&&t.id&&"figma.mixed"!==t.id&&"MIXED"!==t.id&&n.add(t.id),Object.entries(t))){let t="textStyleId"===l?"TEXT":["fillStyleId","strokeStyleId","backgroundStyleId"].includes(l)?"PAINT":"effectStyleId"===l?"EFFECT":"gridStyleId"===l?"GRID":null;t&&"string"==typeof s&&s&&"figma.mixed"!==s&&"MIXED"!==s&&!i.has(s)&&i.set(s,t),e(s,l||r,a,o+1)}}}({[t]:e[t]})}catch(e){}let[a,o,l,s,d,f]=await Promise.all([figma.variables.getLocalVariableCollectionsAsync(),figma.variables.getLocalVariablesAsync(),figma.getLocalPaintStylesAsync(),figma.getLocalTextStylesAsync(),figma.getLocalEffectStylesAsync(),figma.getLocalGridStylesAsync()]),c=new Set(o.map(e=>e.id)),u=new Set([...l,...s,...d,...f].map(e=>e.id)),p=[];async function g(e){try{return await figma.variables.getVariableByIdAsync(e)||null}catch(t){return p.push({id:e,kind:"variable",reason:"notFoundOrUnavailable",error:String(t&&t.message||t).slice(0,300)}),null}}function h(e){return Object.values(e.valuesByMode||{}).filter(e=>e&&"VARIABLE_ALIAS"===e.type&&"string"==typeof e.id&&e.id).map(e=>e.id)}let y=[...i.entries()].filter(([e])=>!u.has(e)).sort(([e],[t])=>e.localeCompare(t)).map(async([e,t])=>{try{let r=await figma.getStyleByIdAsync(e);return r?{style:r,styleType:t}:null}catch(t){return p.push({id:e,kind:"style",reason:"notFoundOrUnavailable",error:String(t&&t.message||t).slice(0,300)}),null}}),m=new Set,b=[],x=[...n,...o.flatMap(h)];for(;x.length>0;){let e=[...new Set(x)].filter(e=>!c.has(e)&&!m.has(e)).sort();for(let t of e)m.add(t);let t=(await Promise.all(e.map(g))).filter(Boolean);b.push(...t),x=t.flatMap(h)}let E=(await Promise.all(y)).filter(Boolean);for(let e of m)b.some(t=>t.id===e)||p.some(t=>t.id===e)||p.push({id:e,kind:"variable",reason:"notFoundOrUnavailable"});for(let[e]of[...i.entries()].filter(([e])=>!u.has(e)))E.some(t=>t.style.id===e)||p.some(t=>t.id===e)||p.push({id:e,kind:"style",reason:"notFoundOrUnavailable"});let A=[...new Set(b.map(e=>e.variableCollectionId))].filter(e=>!a.some(t=>t.id===e)).sort(),I=(await Promise.all(A.map(async e=>{try{return await figma.variables.getVariableCollectionByIdAsync(e)}catch(e){return null}}))).filter(Boolean);function S(e,t){return{...r(e),styleType:t,value:r("PAINT"===t?e.paints:"EFFECT"===t?e.effects:"GRID"===t?e.layoutGrids:e)}}let w=[...l.map(e=>S(e,"PAINT")),...s.map(e=>S(e,"TEXT")),...d.map(e=>S(e,"EFFECT")),...f.map(e=>S(e,"GRID")),...E.map(({style:e,styleType:t})=>S(e,t))].sort((e,t)=>e.id.localeCompare(t.id)),v=[...o,...b].map(e=>r(e)).sort((e,t)=>e.id.localeCompare(t.id)),N=[...a,...I].map(e=>r(e)).sort((e,t)=>e.id.localeCompare(t.id));function O(e){let t=[];for(let r=0;r=55296&&n<=56319){let t=r+1=56320&&t<=57343?(n=65536+(n-55296<<10)+(t-56320),r+=1):n=65533}else n>=56320&&n<=57343&&(n=65533);n<128?t.push(n):n<2048?t.push(192|n>>6,128|63&n):n<65536?t.push(224|n>>12,128|n>>6&63,128|63&n):t.push(240|n>>18,128|n>>12&63,128|n>>6&63,128|63&n)}return new Uint8Array(t)}p.sort((e,t)=>e.kind.localeCompare(t.kind)||e.id.localeCompare(t.id));let T=[...N.map(e=>({kind:"collection",value:e})),...v.map(e=>({kind:"variable",value:e})),...w.map(e=>({kind:"style",value:e})),...[...n].sort().map(e=>({kind:"usedVariableId",value:e})),...[...i.keys()].sort().map(e=>({kind:"usedStyleId",value:e})),...p.map(e=>({kind:"unresolved",value:e}))];if(t>T.length)throw Error("DEVUP_SNAPSHOT_RANGE_INVALID");let _=T.map(e=>O(JSON.stringify(e.value)).length+1),P=18432,M=null;for(let e=0;e<6;e+=1){let{pageItems:e,packed:r}=function(e){let r=[],n=0;for(let i=t;i0)||!(n+_[i]>e));i+=1)r.push(T[i]),n+=_[i];return{pageItems:r,packed:n}}(P),n=function(e,r){let n=t=>e.filter(e=>e.kind===t).map(e=>e.value),i=n("collection"),a=n("variable"),o=n("style"),l=n("unresolved"),s={kind:"devupFastThemeEnvelope",schemaVersion:1,source:{fileKey:figma.fileKey||"",version:null},resources:{collections:i,variables:a,styles:o,usedRemoteVariables:[],usedVariableIds:n("usedVariableId"),usedStyleIds:n("usedStyleId"),localComplete:!0,usedRemoteComplete:0===p.length,unresolved:l},page:{offset:t,nextOffset:r,complete:r>=T.length,totalItems:T.length},integrity:{collectionCount:i.length,variableCount:a.length,styleCount:o.length,unresolvedCount:l.length,utf8Bytes:0}},d=new Uint8Array;for(let e=0;e<8&&(d=O(JSON.stringify(s)),s.integrity.utf8Bytes!==d.length);e+=1)s.integrity.utf8Bytes=d.length;if(d=O(JSON.stringify(s)),s.integrity.utf8Bytes!==d.length)throw Error("DEVUP_ENVELOPE_LENGTH_UNSTABLE");return{envelope:s,bytes:d.length}}(e,t+e.length);if(n.bytes<=19456){M=n;break}if(e.length<=1)break;P=Math.max(1,Math.min(P-1,r-(n.bytes-19456)-256))}if(null===M||M.bytes>8388608)throw Error("DEVUP_ENVELOPE_TOO_LARGE");return M.envelope},largeValue:async function(e){let t;figma.skipInvisibleInstanceChildren=!1;let r=e.largeValue;async function n(e){try{let t=await figma.getNodeByIdAsync(e);if(t)return t}catch(r){if(!e.startsWith("I")||!e.includes(";"))throw r;try{return await t()}catch(e){throw Error(`${r.message||r}; nested lookup fallback: ${e.message||e}`)}}if(!e.startsWith("I")||!e.includes(";"))return null;return t();async function t(){let t=await figma.getNodeByIdAsync(e.slice(1).split(";")[0]),r=t?[t]:[],n=new Set;for(;r.length;){let t=r.pop();if(t.id===e)return t;if(!n.has(t.id)){if(n.add(t.id),n.size>1e4)throw Error("nested lookup exceeds 10000 nodes");Array.isArray(t.children)&&r.push(...t.children)}}throw Error(`nested node ${e} not found in its instance`)}}async function i(e,t,r=85e3,n=()=>{}){let a,o=Date.now();try{let i=await Promise.race([e.exportAsync(t),new Promise((e,t)=>{a=setTimeout(()=>t(Error(`DEVUP_ASSET_TIMEOUT: native export exceeded ${r} ms`)),r)})]);return n("native-export",Date.now()-o),i}catch(i){if(n("native-export",Date.now()-o,String(i.message||i).slice(0,500)),"SVG_STRING"!==t.format)throw i;let r=Date.now();try{let t=function(e){let t=e=>"number"==typeof e&&Number.isFinite(e),r=(e,t)=>{throw Error(`geometry fallback unsupported: ${e.type||"unknown"} ${e.id||""}: ${t}`)},n=0,i=0;function a(e,i,a){(void 0!==a&&!Array.isArray(a)||void 0===a&&Array.isArray(i)&&i.length)&&r(e,"unresolved paint requires native export");let o=Array.isArray(a)?a.filter(e=>!1!==e.visible):[];return o.length?(Array.isArray(i)&&i.length||r(e,"paint has no rendered geometry"),o.map(a=>{("SOLID"!==a.type||a.blendMode&&"NORMAL"!==a.blendMode)&&r(e,"paint type or blending requires native export");let o=a.color;o&&[o.r,o.g,o.b].every(e=>t(e)&&e>=0&&e<=1)||r(e,"invalid solid color");let l=void 0===a.opacity?1:a.opacity;return(!t(l)||l<0||l>1)&&r(e,"invalid paint opacity"),["NONZERO","EVENODD"].map(t=>{let a=i.filter(e=>e.windingRule===t);if(!a.length)return"";for(let t of a)"string"==typeof t.data&&t.data.trim()||r(e,"empty path"),n++;return`"']/g,e=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[e])}" fill="rgb(${100*o.r}%,${100*o.g}%,${100*o.b}%)" fill-opacity="${l}" fill-rule="${"EVENODD"===t?"evenodd":"nonzero"}"/>`}).join("")}).join("")):""}let o=e.absoluteRenderBounds,l=e.absoluteTransform;(!o||![o.x,o.y,o.width,o.height].every(t)||o.width<=0||o.height<=0)&&r(e,"missing render bounds"),(!Array.isArray(l)||2!==l.length||!l.every(e=>Array.isArray(e)&&3===e.length&&e.every(t))||0!==l[0][1]||0!==l[1][0]||l[0][0]<=0||l[1][1]<=0)&&r(e,"root rotation, reflection or shear requires native export");let s=(o.x-l[0][2])/l[0][0],d=(o.y-l[1][2])/l[1][1],f=o.width/l[0][0],c=o.height/l[1][1],u=function e(n,o){if(++i>1e4&&r(n,"subtree exceeds 10000 nodes"),n.isMask&&r(n,"mask requires native export"),!1===n.visible||0===n.opacity)return"";n.clipsContent&&r(n,"clipping requires native export"),Array.isArray(n.effects)&&n.effects.some(e=>!1!==e.visible)&&r(n,"effect requires native export"),n.blendMode&&!["NORMAL","PASS_THROUGH"].includes(n.blendMode)&&r(n,"layer blending requires native export");let l=void 0===n.opacity?1:n.opacity;(!t(l)||l<0||l>1)&&r(n,"invalid layer opacity");let s="";if(!o){let e=n.relativeTransform;Array.isArray(e)&&2===e.length&&e.every(e=>Array.isArray(e)&&3===e.length&&e.every(t))||r(n,"missing relative transform"),s=` transform="matrix(${e[0][0]} ${e[1][0]} ${e[0][1]} ${e[1][1]} ${e[0][2]} ${e[1][2]})"`}let d="";if(["GROUP","FRAME","INSTANCE","COMPONENT"].includes(n.type))(Array.isArray(n.fills)&&n.fills.some(e=>!1!==e.visible)||Array.isArray(n.strokes)&&n.strokes.some(e=>!1!==e.visible))&&r(n,"container paint requires native export"),Array.isArray(n.children)||r(n,"missing children"),d=(!0===n.itemReverseZIndex?[...n.children].reverse():n.children).map(t=>e(t,!1)).join("");else{for(let e of(["VECTOR","BOOLEAN_OPERATION","RECTANGLE","ELLIPSE","POLYGON","STAR","LINE"].includes(n.type)||r(n,"node type requires native export"),Array.isArray(n.strokes)&&n.strokes.some(e=>!1!==e.visible)&&"CENTER"!==n.strokeAlign&&r(n,"non-center stroke alignment requires native export"),Array.isArray(n.dashPattern)&&n.dashPattern.length&&r(n,"dashed stroke requires native export"),[n.fillGeometry,n.strokeGeometry]))void 0!==e&&(!Array.isArray(e)||e.some(e=>!["NONZERO","EVENODD"].includes(e.windingRule)))&&r(n,"unknown winding rule");d=a(n,n.fillGeometry,n.fills)+a(n,n.strokeGeometry,n.strokes)}return`${d}`}(e,!0);return n||r(e,"no painted geometry"),`${u}`}(e);return n("geometry-fallback",Date.now()-r),t}catch(e){throw Error(`native export: ${i.message||i}; ${e.message||e}`)}}finally{clearTimeout(a)}}let a=await n(r.nodeId);if(!a)throw Error("DEVUP_NODE_NOT_FOUND");function o(e){let t=[];for(let r=0;r=55296&&n<=56319){let t=r+1=56320&&t<=57343?(n=65536+(n-55296<<10)+(t-56320),r+=1):n=65533}else n>=56320&&n<=57343&&(n=65533);n<128?t.push(n):n<2048?t.push(192|n>>6,128|63&n):n<65536?t.push(224|n>>12,128|n>>6&63,128|63&n):t.push(240|n>>18,128|n>>12&63,128|n>>6&63,128|63&n)}return new Uint8Array(t)}let l=null;try{if("$export:svg"===r.field){if("function"!=typeof a.exportAsync)throw Error("unsupported");let e=await i(a,{format:"SVG_STRING"},Math.max(1,(Number(r.timeoutMs)||9e4)-5e3));if("string"!=typeof e)throw Error("unsupported");l=o(e)}else if("string"==typeof r.field&&r.field.startsWith("$export:png")){if("function"!=typeof a.exportAsync)throw Error("unsupported");let e=Number(r.field.split("@")[1]||2);if(!Number.isFinite(e)||e<=0||e>4)throw Error("invalid scale");let t=r.field.split("@"),n=a;if("visible"===t[2]){let e=new Set;for(;n&&n.id!==t[3]&&!e.has(n)&&e.size<64;)e.add(n),n=n.parent;if(!n||n.id!==t[3]||["PAGE","DOCUMENT"].includes(n.type))throw Error("invalid visible scope")}else if(t.length>2)throw Error("invalid export field");let i=await n.exportAsync({format:"PNG",contentsOnly:!0,useAbsoluteBounds:!1,constraint:{type:"SCALE",value:e}});l=i instanceof Uint8Array?i:new Uint8Array(i)}else if("styledTextSegments"===r.field&&"TEXT"===a.type&&"function"==typeof a.getStyledTextSegments)t=a.getStyledTextSegments(["fontName","fontWeight","fontSize","textDecoration","textCase","lineHeight","letterSpacing","fills","textStyleId","fillStyleId","listOptions","indentation","hyperlink"]);else if(r.field in a)t=a[r.field];else throw Error("unsupported")}catch(e){return{kind:"devupLargeValueUnsupported",fileKey:figma.fileKey||"",version:r.version,nodeId:r.nodeId,field:r.field,byteLength:r.byteLength,sha256:r.sha256,errorCode:"DEVUP_FIELD_UNSUPPORTED_BY_UPSTREAM",errorDetail:`${r.field} failed: ${e.message||e}`.slice(0,500)}}let s=null===l?o(JSON.stringify(function e(t,r=new WeakSet,n=0){if(null===t||["string","number","boolean"].includes(typeof t))return t;if(void 0===t)return{$undefined:!0};if("bigint"==typeof t)return{$bigint:t.toString()};if(["function","symbol"].includes(typeof t))return{$unsupported:typeof t};if(n>12)return{$truncated:"max-depth"};if("object"==typeof t&&"parent"in t&&"string"==typeof t.id&&"string"==typeof t.type)return{$nodeId:t.id,$nodeType:t.type};if(Array.isArray(t))return t.map(t=>e(t,r,n+1));if(ArrayBuffer.isView(t))return{$binary:t.constructor.name,byteLength:t.byteLength};if(t instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:t.byteLength};if(r.has(t))return{$circular:!0};r.add(t);let i={};for(let a of Object.keys(t).sort())try{let o=e(t[a],r,n+1);o&&"function"===o.$unsupported||(i[a]=o)}catch(e){i[a]={$error:String(e&&e.message?e.message:e)}}return r.delete(t),i}(t))):l,d=function(e){let t=[0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,0xe49b69c1,0xefbe4786,0xfc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x6ca6351,0x14292967,0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2],r=e.length,n=64*Math.ceil((r+9)/64),i=new Uint8Array(n);i.set(e),i[r]=128;let a=8*r;for(let e=0;e<8;e+=1)i[n-1-e]=255&Math.floor(a/2**(8*e));let o=[0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19],l=(e,t)=>e>>>t|e<<32-t;for(let e=0;e>>3,n=l(r[e-2],17)^l(r[e-2],19)^r[e-2]>>>10;r[e]=r[e-16]+t+r[e-7]+n>>>0}let[n,a,s,d,f,c,u,p]=o;for(let e=0;e<64;e+=1){let i=p+(l(f,6)^l(f,11)^l(f,25))+(f&c^~f&u)+t[e]+r[e]>>>0,o=(l(n,2)^l(n,13)^l(n,22))+(n&a^n&s^a&s)>>>0;p=u,u=c,c=f,f=d+i>>>0,d=s,s=a,a=n,n=i+o>>>0}for(let[e,t]of[n,a,s,d,f,c,u,p].entries())o[e]=o[e]+t>>>0}return o.map(e=>e.toString(16).padStart(8,"0")).join("")}(s);if(s.length!==r.byteLength||d!==r.sha256)throw Error("DEVUP_LARGE_VALUE_CHANGED");let f=Math.max(0,Math.floor(Number(r.offset)||0)),c=Math.min(65536,Math.max(1,Math.floor(Number(r.maxChunkBytes)||8192)));if(f>=s.length)throw Error("DEVUP_LARGE_VALUE_RANGE_INVALID");let u=Math.min(s.length,f+c);return{kind:"devupLargeValueFragment",fileKey:figma.fileKey||"",version:r.version,nodeId:r.nodeId,field:r.field,offset:f,nextOffset:u,byteLength:s.length,sha256:d,dataBase64:function(e){let t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",r="";for(let n=0;n>2],r+=t[(3&i)<<4|a>>4],r+=n+1>6]:"=",r+=n+210||t.some(e=>"string"!=typeof e||e.length>128||!/^(?:[0-9]+:[0-9]+|I[0-9]+:[0-9]+(?:;[0-9]+:[0-9]+)+)$/.test(e))||new Set(t).size!==t.length)throw Error("DEVUP_INVALID_INPUT: nodeIds must be 1..10 distinct Figma IDs");let r=(e,t)=>{try{let r=e[t];return"number"==typeof r&&Number.isFinite(r)?r:null}catch{return null}},n=(e,t)=>{try{let r=e[t];return"string"==typeof r&&r.length<=32&&/^[A-Z_]+$/.test(r)?r:null}catch{return null}},i=[];for(let e of t)try{let t=await figma.getNodeByIdAsync(e);if(!t||"DOCUMENT"===t.type||"PAGE"===t.type){i.push({nodeId:e,status:"unavailable",reason:t?"not-scene-node":"not-found"});continue}let a={width:r(t,"width"),height:r(t,"height"),padding:{top:r(t,"paddingTop"),right:r(t,"paddingRight"),bottom:r(t,"paddingBottom"),left:r(t,"paddingLeft")},gap:r(t,"itemSpacing"),layoutMode:n(t,"layoutMode"),align:{primary:n(t,"primaryAxisAlignItems"),counter:n(t,"counterAxisAlignItems")}},o=null!==a.width&&null!==a.height;i.push({nodeId:e,status:o?"measured":"partial",...a})}catch{i.push({nodeId:e,status:"unavailable",reason:"read-failed"})}return{kind:"devup-measure",schemaVersion:1,coordinateSpace:"figma-logical",nodes:i,partial:i.some(e=>"measured"!==e.status),evidenceLimit:"Null means unavailable or not applicable; no screenshot dimensions, descendants, theme, assets or code were read."}},metadata:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=await figma.getNodeByIdAsync(e.nodeId);if(!t)throw Error("DEVUP_NODE_NOT_FOUND");let r=t;for(;r&&"PAGE"!==r.type;)r=r.parent;r&&"PAGE"===r.type&&await figma.setCurrentPageAsync(r);let n=[];return!function(e){let t=[{node:e,visited:!1}],r=new Map;for(;t.length>0;){let e=t[t.length-1],i=e.node,a="children"in i?i.children:[];if(!e.visited){e.visited=!0;for(let e=a.length-1;e>=0;e-=1)t.push({node:a[e],visited:!1});continue}t.pop();let o=0,l=[];for(let e of a)l.push(e.id),o+=1+(r.get(e.id)||0);r.set(i.id,o),n.push({id:i.id,type:i.type,name:i.name,childrenIds:l,descendantCount:o})}}(t),{fileKey:figma.fileKey||"",version:null,rootId:t.id,nodes:n}},pageCatalog:async function(e){let t=figma.root.children;return{fileKey:figma.fileKey||"",version:null,rootIds:t.map(e=>e.id),nodes:t.map(e=>({id:e.id,type:e.type,fields:{name:e.name,parentId:null,childrenIds:[]},extra:{},fieldErrors:{}})),diagnostics:[]}},referencePng:async function(e){let t,r=e.nodeId;globalThis.__devupReferencePngAnswers||(globalThis.__devupReferencePngAnswers=new Map);let n=globalThis.__devupReferencePngAnswers,i=0;for(let[e,t]of n)Date.now()-t.at>12e4?n.delete(e):i+=t.text.length;if(i>=0xc000000)throw Error(`DEVUP_REFERENCE_PNG_BUSY ${i} characters are held for other reads`);let a=await figma.getNodeByIdAsync(r);if(!a||"function"!=typeof a.exportAsync)throw Error(`DEVUP_NODE_NOT_FOUND ${r}`);let o=a.absoluteRenderBounds||a.absoluteBoundingBox,l=Math.max(o.width,o.height),s=l>8192?8192/l:1,d=(e,t)=>({format:"PNG",constraint:t&&s<1?{type:o.width>=o.height?"WIDTH":"HEIGHT",value:8192}:{type:"SCALE",value:s},contentsOnly:e,useAbsoluteBounds:!1,colorProfile:"SRGB"}),f=e=>(Array.isArray(e)?e:[]).filter(e=>e&&!1!==e.visible),c=(e,t)=>!!e&&!!t&&e.x!!e&&e.x+t<=o.x&&e.y+t<=o.y&&e.x+e.width-t>=o.x+o.width&&e.y+e.height-t>=o.y+o.height,p=e=>`${e.type} ${JSON.stringify(e.name)}`,g=e=>JSON.parse(JSON.stringify(e===figma.mixed?null:e)),h=e=>(void 0===e.opacity||1===e.opacity)&&(!e.blendMode||["PASS_THROUGH","NORMAL"].includes(e.blendMode)),y=[],m=a.parent;for(;m&&"PAGE"!==m.type;)y.unshift(m),m=m.parent;let b=[255,255,255];for(let e of f(m&&m.backgrounds)){if("SOLID"!==e.type)continue;let t=void 0===e.opacity?1:e.opacity;["r","g","b"].forEach((r,n)=>{b[n]=t*e.color[r]*255+(1-t)*b[n]})}let x=0,E=e=>!1!==e.visible&&!e.isMask&&!!c(e.absoluteRenderBounds,o)&&((x+=1)>5e3||"TEXT"===e.type||!!f(e.fills).length||!!f(e.strokes).length||!!f(e.effects).length||"children"in e&&e.children.some(E)),A=e=>h(e)&&u(e.absoluteBoundingBox,0)&&1e-9>Math.abs(e.absoluteTransform[0][1])&&1e-9>Math.abs(e.absoluteTransform[1][0])&&0===Math.max(0,...[e.topLeftRadius,e.topRightRadius,e.bottomRightRadius,e.bottomLeftRadius].filter(e=>"number"==typeof e))&&f(e.fills).some(e=>"NORMAL"===(e.blendMode||"NORMAL")&&(void 0===e.opacity||1===e.opacity)&&("SOLID"===e.type||e.type.startsWith("GRADIENT_")&&e.gradientStops.every(e=>1===e.color.a))),I=[...y,a],S=[],w=[],v=new Map;for(let e=0;eA(e)&&i.slice(0,t).every(h));r.forEach((e,r)=>{e===t||(e.isMask&&!1!==e.visible&&r{let t=await a.exportAsync(d(!1,!0));return{kind:"devupReferencePng",nodeId:r,mimeType:"image/png",byteLength:t.length,data:figma.base64Encode(t),backdrop:N,...e}},T=(e,t)=>{let r=[...new Set(e)];return{approximate:r.join("; "),note:`Drawn as the canvas shows this area: devup-mcp cannot yet compose what get_screenshot draws here (${r.slice(0,3).join("; ")}). Layers reaching into it may show here: ${t}; get_screenshot leaves other layers out.`}},_=e=>{let t=JSON.stringify(e).replace(/[\u0080-\uffff]/g,e=>`\\u${e.charCodeAt(0).toString(16).padStart(4,"0")}`);if(t.length>0x5a00000){if(e.compose){let{compose:r,reaching:n,...i}=e;return _({...i,...T([`the answer would be ${t.length} characters`],n)})}throw Error(`DEVUP_REFERENCE_PNG_TOO_LARGE ${t.length} characters, over the 94371840 one answer is read back up to`)}if(t.length<=6291456)return e;let i=`${r}@${Date.now()}.${Math.random().toString(36).slice(2,10)}`;return n.set(i,{text:t,at:Date.now()}),{kind:"devupReferencePngChunked",nodeId:r,id:i,length:t.length}};if(0===S.length)return _(await O({}));let P=I.some(e=>e.isMask),M=[],C=new Set,k=(e,t)=>{let r=p(e);for(let t of f(e.fills))"IMAGE"===t.type?((t.rotation||f([t.filters]).some(e=>Object.values(e).some(e=>e)))&&M.push(`${r} has an image fill with a rotation or filters`),C.add(t.imageHash)):"SOLID"===t.type||t.type.startsWith("GRADIENT_")||M.push(`${r} is painted with ${t.type}`);for(let t of f(e.effects))["LAYER_BLUR","DROP_SHADOW","INNER_SHADOW","BACKGROUND_BLUR"].includes(t.type)||M.push(`${r} has a ${t.type}`);let n=f(e.strokes);if((e=>{let t=0===f(e.strokes).length?[]:[e.strokeWeight,e.strokeTopWeight,e.strokeRightWeight,e.strokeBottomWeight,e.strokeLeftWeight].filter(e=>"number"==typeof e&&e>0);if(0===t.length)return!1;let r=Math.max(...t),n="OUTSIDE"===e.strokeAlign?0:"CENTER"===e.strokeAlign?r/2:r,i="INSIDE"===e.strokeAlign?0:"CENTER"===e.strokeAlign?r/2:r,a=e.absoluteBoundingBox;return c(a&&{x:a.x-i,y:a.y-i,width:a.width+2*i,height:a.height+2*i},o)&&!u(a,n)})(e)){let t=["FRAME","RECTANGLE","COMPONENT","COMPONENT_SET","INSTANCE","SECTION"].includes(e.type),i=n.every(e=>"SOLID"===e.type||e.type.startsWith("GRADIENT_")),a=Array.isArray(e.dashPattern)&&e.dashPattern.length>0,o="INSIDE"===e.strokeAlign||!e.strokeJoin||"MITER"===e.strokeJoin;t&&i&&!a&&o||M.push(`${r}'s stroke reaches it`)}return{transform:g(e.absoluteTransform),width:e.width,height:e.height,radii:[e.topLeftRadius,e.topRightRadius,e.bottomRightRadius,e.bottomLeftRadius].map(e=>"number"==typeof e?e:0),fills:g(f(e.fills)),opacity:void 0===e.opacity?1:e.opacity,blendMode:e.blendMode||null,clips:!!e.clipsContent,effects:g(f(e.effects)),strokes:g(n),strokeAlign:e.strokeAlign||null,strokeWeights:["strokeTopWeight","strokeRightWeight","strokeBottomWeight","strokeLeftWeight"].map(t=>"number"==typeof e[t]?e[t]:"number"==typeof e.strokeWeight?e.strokeWeight:0)}},R=async e=>{let t=await e.exportAsync(d(!0,e===a));return{data:figma.base64Encode(t),area:e.absoluteRenderBounds||e.absoluteBoundingBox,blendMode:e.blendMode||null,size:[16,20].map(e=>(t[e]<<24|t[e+1]<<16|t[e+2]<<8|t[e+3])>>>0)}},D=async e=>{let t=v.get(e);if(!t)return null;let r=await t.exportAsync({...d(!0,!1),useAbsoluteBounds:!0});return{data:figma.base64Encode(r),area:t.absoluteBoundingBox,luminance:"LUMINANCE"===t.maskType}},L=[],$={},U={};try{for(let e of y){let t=k(e,!1),r=await D(e);r&&(t.mask=r),L.push(t)}let e=e=>f(e.effects).some(e=>"BACKGROUND_BLUR"===e.type);if(w.length>0&&(e(a)||"findOne"in a&&a.findOne(e))){let r="children"in a&&a.children.some(t=>e(t)||"findOne"in t&&!!t.findOne(e));if("children"in a&&!r&&!f(a.strokes).length){for(let e of($.own=k(a,!0),$.children=[],a.children)){if(!1===e.visible||e.isMask||!e.absoluteRenderBounds)continue;let t=await R(e);$.children.push({data:t.data,area:t.area,blendMode:t.blendMode})}t=[Math.ceil(o.width*s-1e-6),Math.ceil(o.height*s-1e-6)]}else M.push(`${p(a)} blurs what lies behind it`)}else if(P)t=[Math.ceil(o.width*s-1e-6),Math.ceil(o.height*s-1e-6)];else{let e=await R(a);$.picture={data:e.data,area:e.area,blendMode:e.blendMode},t=e.size}let r=await D(a);r&&($.mask=r),t&&t[0]*t[1]>8e6&&M.push(`the area is ${t[0]}x${t[1]} pixels`);let n=globalThis.__devupNormalizeImage,i=Date.now()+2e4,l=0;for(let e of C){let t=e&&figma.getImageByHash(e),r=t&&await t.getBytesAsync();if(!r){M.push(`the image ${e} could not be read`);continue}let a=null;if("function"==typeof n){let{width:e,height:o}=await t.getSizeAsync(),l=(...e)=>e.every((e,t)=>r[t]===e),s=l(137,80,78,71)||l(255,216)||l(71,73,70),d=Math.min(1,8192/Math.max(e,o,1)),f=Math.ceil(e*d)*Math.ceil(o*d),c=5*f+65536,u=5*r.length+4*e*o*(d<1?2:1)+4*f+4*c+1048576,p=i-Date.now();if((!s||Math.max(e,o)>8192||r.length>4*e*o+1048576)&&u<=0x20000000&&p>0){let t=await n(r,8192,p,e,o);t&&(r=t,a={width:e,height:o})}}if(l+r.length>0x4000000){M.push("its ancestors' images are over 67108864 bytes");break}l+=r.length,U[e]=a?{data:figma.base64Encode(r),...a}:figma.base64Encode(r)}}catch(e){M.push(`a part could not be exported: ${e instanceof Error?e.message:String(e)}`)}let B=`${S.slice(0,5).join(", ")}${S.length>5?` and ${S.length-5} more`:""}`;return M.length>0?_(await O(T(M,B))):_(await O({reaching:B,compose:{area:o,scale:s,width:t[0],height:t[1],page:b,layers:L,node:$,images:U}}))},referencePngChunk:async function(e){let t=e.largeValue,r=globalThis.__devupReferencePngAnswers,n=r&&r.get(t.id);if(!n||"string"!=typeof n.text)throw Error(`DEVUP_REFERENCE_PNG_ANSWER_GONE ${t.id}`);let i=n.text,a=Math.max(0,Math.floor(Number(t.offset)||0)),o=Math.min(i.length,a+4194304);return o>=i.length&&r.delete(t.id),{kind:"devupReferencePngChunk",id:t.id,offset:a,nextOffset:o,complete:o>=i.length,text:i.slice(a,o)}},search:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=await figma.getNodeByIdAsync(e.nodeId);if(!t)throw Error("DEVUP_NODE_NOT_FOUND");let r=t;for(;r&&"PAGE"!==r.type;)r=r.parent;if(!r||"PAGE"!==r.type)throw Error("DEVUP_PAGE_NOT_FOUND");await r.loadAsync();let n=e.search,i=new Set((n.nodeTypes.length?n.nodeTypes:["PAGE","SECTION","FRAME","COMPONENT_SET","COMPONENT"]).map(e=>e.toUpperCase()));function a(e){return e.normalize("NFC").toLocaleLowerCase().replace(/\s/gu,"")}let o=t===r?r.findAll(()=>!0):"findAll"in t?t.findAll(()=>!0):[],l=[t,...o].filter(e=>i.has(e.type)&&"string"==typeof e.name).map(e=>({node:e,score:function(e){let t=n.query.trim();if(""===t||"*"===t)return 100;if(e===n.query)return 400;if("exact"===n.matchKind)return null;let r=a(e),i=a(n.query);if(r===i)return 300;if(r.startsWith(i))return 200;if(r.includes(i))return 100;if("fuzzy"!==n.matchKind)return null;let o=function(e,t){let r=Array.from({length:t.length+1},(e,t)=>t);for(let n=0;nnull!==e.score).sort((e,t)=>t.score-e.score||e.node.name.localeCompare(t.node.name)||e.node.id.localeCompare(t.node.id)),s=l.slice(0,n.limit),d=new Map([[r.id,r]]);for(let e of[t,...s.map(e=>e.node)]){let t=e;for(;t&&"DOCUMENT"!==t.type;)d.set(t.id,t),t=t.parent}return{fileKey:figma.fileKey||"",version:null,rootIds:[r.id],nodes:[...d.values()].map(e=>({id:e.id,type:e.type,fields:{name:e.name,searchCandidatesTruncated:e===t&&l.length>s.length,parentId:e.parent&&"DOCUMENT"!==e.parent.type?e.parent.id:null,childrenIds:"children"in e?e.children.map(e=>e.id):[]},extra:{},fieldErrors:{}})),diagnostics:[]}},sectionIndex:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=await figma.getNodeByIdAsync(e.nodeId);if(!t)throw Error("DEVUP_NODE_NOT_FOUND");if("SECTION"!==t.type){let e=t.parent,r=new Set;for(;e&&"SECTION"!==e.type&&!r.has(e.id);)r.add(e.id),e=e.parent;let n=e&&"SECTION"===e.type?e.id:null,i=n&&figma.fileKey?`https://www.figma.com/design/${figma.fileKey}?node-id=${n.replace(/:/g,"-")}`:null;throw Error("DEVUP_SECTION_REQUIRED "+JSON.stringify({pluginCode:"DEVUP_SECTION_REQUIRED",stage:"section-index",nodeId:t.id,nodeType:t.type,sectionId:n,nextAction:{tool:"devup_figma_export",how:n?`The url must point to a SECTION. This capture's SECTION is ${n}; select frames with frameIds.`:"The url must point to a SECTION. This node has no ancestor SECTION; choose the intended SECTION in Figma and copy its link.",arguments:i?"FRAME"===t.type&&t.parent===e?{url:i,frameIds:[t.id]}:{url:i}:null,requiredArguments:i?[]:["url"]}}))}let r=2048;function n(e){let t=e.absoluteBoundingBox||{x:"number"==typeof e.x?e.x:0,y:"number"==typeof e.y?e.y:0,width:"number"==typeof e.width?e.width:0,height:"number"==typeof e.height?e.height:0};return[t.x,t.y,t.width,t.height].every(Number.isFinite)?{x:t.x,y:t.y,width:t.width,height:t.height}:null}function i(e,t){if("FRAME"!==e.type||!1===e.visible||!t)return!1;let r=t.width/Math.max(1,t.height);return t.width>=240&&t.width<=1800&&t.height>=300&&t.height<=2e3&&r>=.25&&r<=2.5}function a(e){let t=0;for(let r=0;r=55296&&n<=56319&&r+1[e.id,t.id])),s=[],d=0;for(let e=0;ee.id));for(let r of t.children){if(e.has(r.id))continue;let t=n(r);t&&!1!==r.visible&&s.push({node:r,box:t})}}s.sort((e,t)=>e.box.y-t.box.y||e.box.x-t.box.x||e.node.id.localeCompare(t.node.id));let f=o.length>d||s.length>100,c=s.slice(0,100),u=new Set(c.map(({node:e})=>e.id)),p=new Set(e.rootIds),g={},h=new Map([[t.id,null]]);for(let e=0;e{let r=e.parent;for(;r&&r.id!==t.id&&!u.has(r.id);)r=r.parent;return[e.id,r&&u.has(r.id)?r.id:t.id]})),m=e=>c.filter(({node:t})=>y.get(t.id)===e).map(({node:e})=>e.id),b=n(t);if(!b)throw Error("DEVUP_NODE_BOUNDS_UNAVAILABLE");let x={id:t.id,type:t.type,fields:{name:t.name,parentId:t.parent&&"DOCUMENT"!==t.parent.type?t.parent.id:null,childrenIds:m(t.id),absoluteBoundingBox:b,visible:!1!==t.visible,projectionTruncated:f,nodeScreenIds:g},extra:{},fieldErrors:{}},E=c.length,A=c.map(({node:e,box:t})=>{let o,l=function(e){let t=[e],r=0,n=0;for(let e=0;er}}(e),s=Math.floor(r/E);E-=1;let d=function(e,t){let r=[e],n="",i=0,o=0,l=!1;if(t<=0)return{text:"",state:"budget-exhausted",spent:0};for(let e=0;e=120||o+e>t)return{text:n.trimEnd(),state:o+e>t?"budget-exhausted":"truncated",spent:o};n+=r,i+=1,o+=e}}if("children"in s){let e=Math.max(0,64-r.length);s.children.length>e&&(l=!0),r.push(...s.children.slice(0,e))}}}return{text:n,state:l?"truncated":n?"available":"no-text",spent:o}}(e,s);return r-=d.spent,{id:e.id,type:e.type,fields:{name:"string"==typeof e.name?e.name:"",parentId:y.get(e.id),childrenIds:m(e.id),absoluteBoundingBox:t,visible:!1!==e.visible,breadcrumb:function(e){let t=[],r=e;for(;r&&"DOCUMENT"!==r.type;)"string"==typeof r.name&&r.name&&t.push(r.name),r=r.parent;return t.reverse()}(e),directChildCount:"children"in e?e.children.length:0,textPreview:d.text,...null===(o=function(e){let t=[e],r=0,i=null;for(;t.length&&r++<400;){let e=t.shift();if("visible"in e&&!1===e.visible)continue;let r="children"in e?e.children.filter(e=>!("visible"in e)||!1!==e.visible):[];if(/header|nav|gnb/i.test(e.name||"")&&r.length>=2&&"layoutMode"in e&&"HORIZONTAL"===e.layoutMode){let t=r.map(n);t.every(Boolean)&&(i=Math.max(i||0,t.reduce((e,t)=>e+t.width,0)+(Number(e.itemSpacing)||0)*(t.length-1)))}t.push(...r.slice(0,80))}return i}(e))?{}:{navigationContentWidth:o},textPreviewState:d.state,subtreeNodeCount:l.subtreeNodeCount,estimatedSerializedBytes:l.estimatedSerializedBytes,selectionReasons:[i(e,t)?"screen-like":"explicit-selection-only","inside-section"],estimateTruncated:l.truncated},extra:{},fieldErrors:{}}}),I={fileKey:figma.fileKey||"",version:null,rootIds:[t.id],nodes:[x,...A],diagnostics:[]},S=a(JSON.stringify(I));for(let e=A.length-1;e>=0&&S>19456;e-=1)A[e].fields.textPreview="",A[e].fields.textPreviewState="budget-exhausted",S=a(JSON.stringify(I));let w=()=>I.nodes.length-1,v=A.length;for(;S>19456&&w()>1;){let e=I.nodes.pop();x.fields.projectionTruncated=!0,x.fields.childrenIds=x.fields.childrenIds.filter(t=>t!==e.id),S=a(JSON.stringify(I))}if(S>19456)throw Error("DEVUP_SECTION_INDEX_TOO_LARGE "+JSON.stringify({pluginCode:"DEVUP_SECTION_INDEX_TOO_LARGE",stage:"section-index",responseBytes:S,maxResponseBytes:19456,discoveredCandidates:v,retainedCandidates:w()}));return I},snapshot:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=await figma.getNodeByIdAsync(e.nodeId);if(!t)throw Error("DEVUP_NODE_NOT_FOUND");let r=["absoluteBoundingBox","absoluteRenderBounds","arcData","backgroundStyleId","blendMode","bottomLeftRadius","bottomRightRadius","boundVariables","characters","clipsContent","componentProperties","componentPropertyDefinitions","componentPropertyReferences","constraints","cornerRadius","counterAxisAlignItems","dashPattern","defaultVariant","effectStyleId","effects","fillStyleId","fills","fontName","fontSize","gridColumnAnchorIndex","gridColumnCount","gridColumnGap","gridColumnSizes","gridColumnSpan","gridRowAnchorIndex","gridRowCount","gridRowGap","gridRowSizes","gridRowSpan","gridStyleId","height","inferredAutoLayout","isAsset","isMask","itemSpacing","layoutGrow","layoutMode","layoutWrap","layoutPositioning","layoutSizingHorizontal","layoutSizingVertical","letterSpacing","lineHeight","maxHeight","maxLines","maxWidth","minHeight","minWidth","name","opacity","overflowDirection","paddingBottom","paddingLeft","paddingRight","paddingTop","primaryAxisAlignItems","reactions","rotation","strokeAlign","strokeBottomWeight","strokeLeftWeight","strokeRightWeight","strokeStyleId","strokeTopWeight","strokeWeight","strokes","targetAspectRatio","textAlignHorizontal","textAlignVertical","textAutoResize","textCase","textDecoration","textStyleId","textTruncation","topLeftRadius","topRightRadius","variantProperties","visible","width","x","y"],n=new Set(r),i=["fontName","fontWeight","fontSize","textDecoration","textCase","lineHeight","letterSpacing","fills","textStyleId","fillStyleId","listOptions","indentation","hyperlink"],a=e.snapshot,o=Math.max(0,Math.floor(Number(a.offset)||0)),l=Math.min(a.maxEnvelopeBytes?Math.max(8192,Math.floor(Number(a.maxEnvelopeBytes))):16e3,Math.max(4096,Math.floor(Number(a.maxPayloadBytes)||15e3))),s=Math.min(l-1024,Math.max(512,Math.floor(Number(a.maxFieldBytes)||4096))),d=new Set(["id","type","parent","children","mainComponent","masterComponent","instances"]),f=new Set(["parentId","childrenIds","name","characters","styledTextSegments","boundVariables"]);function c(e,t,r){let n=function(e){let t=[];for(let r=0;r=55296&&n<=56319){let t=r+1=56320&&t<=57343?(n=65536+(n-55296<<10)+(t-56320),r+=1):n=65533}else n>=56320&&n<=57343&&(n=65533);n<128?t.push(n):n<2048?t.push(192|n>>6,128|63&n):n<65536?t.push(224|n>>12,128|n>>6&63,128|63&n):t.push(240|n>>18,128|n>>12&63,128|n>>6&63,128|63&n)}return new Uint8Array(t)}(JSON.stringify(r));return n.length>0x1000000?{$truncated:"max-large-value-bytes",byteLength:n.length}:{$largeValue:{nodeId:e,field:t,byteLength:n.length,sha256:function(e){let t=[0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,0xe49b69c1,0xefbe4786,0xfc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x6ca6351,0x14292967,0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2],r=64*Math.ceil((e.length+9)/64),n=new Uint8Array(r);n.set(e),n[e.length]=128;let i=8*e.length;for(let e=0;e<8;e+=1)n[r-1-e]=255&Math.floor(i/2**(8*e));let a=[0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19],o=(e,t)=>e>>>t|e<<32-t;for(let e=0;e>>3,n=o(r[e-2],17)^o(r[e-2],19)^r[e-2]>>>10;r[e]=r[e-16]+t+r[e-7]+n>>>0}let[i,l,s,d,f,c,u,p]=a;for(let e=0;e<64;e+=1){let n=p+(o(f,6)^o(f,11)^o(f,25))+(f&c^~f&u)+t[e]+r[e]>>>0,a=(o(i,2)^o(i,13)^o(i,22))+(i&l^i&s^l&s)>>>0;p=u,u=c,c=f,f=d+n>>>0,d=s,s=l,l=i,i=n+a>>>0}for(let[e,t]of[i,l,s,d,f,c,u,p].entries())a[e]=a[e]+t>>>0}return a.map(e=>e.toString(16).padStart(8,"0")).join("")}(n),cursor:{nextOffset:0,maxChunkBytes:12288}}}}function u(e){return function(e){let t=0;for(let r=0;r=55296&&n<=56319&&r+112)return{$truncated:"max-depth"};if("object"==typeof t&&"parent"in t&&"string"==typeof t.id&&"string"==typeof t.type)return{$nodeId:t.id,$nodeType:t.type};if(Array.isArray(t))return t.map(t=>e(t,r,n+1));if(ArrayBuffer.isView(t))return{$binary:t.constructor.name,byteLength:t.byteLength};if(t instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:t.byteLength};if(r.has(t))return{$circular:!0};r.add(t);let i={};for(let a of Object.keys(t).sort())try{let o=e(t[a],r,n+1);o&&"function"===o.$unsupported||(i[a]=o)}catch(e){i[a]={$error:String(e&&e.message?e.message:e)}}return r.delete(t),i}(r),a=u(i);if(a<=s)return i;let o=c(e,t,i);return o.$truncated&&(n[t]=`DEVUP_FIELD_VALUE_UNSUPPORTED:${a}>16777216`),o}let g=new Map;async function h(e,t,r){if("INSTANCE"!==e.type||"function"!=typeof e.getMainComponentAsync)return;g.has(e.id)||g.set(e.id,(async()=>{try{let t=await e.getMainComponentAsync();return{fields:{mainComponentId:t?t.id:null,mainComponentName:t?t.name:null,mainComponentSetId:t&&t.parent&&"COMPONENT_SET"===t.parent.type?t.parent.id:null}}}catch(e){return{error:String(e&&e.message?e.message:e)}}})());let n=await g.get(e.id);n.error?r.mainComponentId=n.error:Object.assign(t,n.fields)}async function y(e){let t={},a={},o={};for(let i of(t.parentId=e.parent?e.parent.id:null,e.parent&&("PAGE"===e.parent.type||"SECTION"===e.parent.type||"COMPONENT_SET"===e.parent.type)&&(t.parentType=e.parent.type,"SECTION"===e.parent.type&&(t.parentName=e.parent.name)),t.childrenIds="children"in e?e.children.map(e=>e.id):[],!("overflowDirection"in e)&&["FRAME","COMPONENT","INSTANCE","COMPONENT_SET"].includes(e.type)&&(t.overflowDirection=null),function(e){let t=new Set,n=e;for(;n&&n!==Object.prototype;){for(let e of Object.getOwnPropertyNames(n))t.add(e);n=Object.getPrototypeOf(n)}for(let n of r)try{n in e&&t.add(n)}catch(e){}return[...t].sort()}(e)))if(!(d.has(i)||i.startsWith("_")||"componentPropertyDefinitions"===i&&"COMPONENT"===e.type&&e.parent&&"COMPONENT_SET"===e.parent.type))try{let r=e[i];if("function"==typeof r)continue;let l=p(e.id,i,r,o);(n.has(i)?t:a)[i]=l}catch(e){o[i]=String(e&&e.message?e.message:e)}if("TEXT"===e.type&&"function"==typeof e.getStyledTextSegments)try{t.styledTextSegments=p(e.id,"styledTextSegments",e.getStyledTextSegments(i),o)}catch(e){o.styledTextSegments=String(e&&e.message?e.message:e)}return await h(e,t,o),{id:e.id,type:e.type,fields:t,extra:a,fieldErrors:o}}let m=[],b=[t];for(;b.length;){let e=b.shift();m.push(e),"children"in e&&b.push(...e.children)}let x=[],E=l-1024,A=2;for(let e=o;eNumber(e.protected)-Number(t.protected)||t.byteLength-e.byteLength),n)){if(r<=t)break;let n=c(e.id,i.name,e[i.sectionName][i.name]);e[i.sectionName][i.name]=n,n.$truncated&&(e.fieldErrors[i.name]=`DEVUP_FIELD_VALUE_UNSUPPORTED:${i.byteLength}>16777216`),r=u(e)}return e}(await y(m[e]),E),r=u(t)+ +!!x.length;if(x.length&&A+r>E)break;x.push(t),A+=r}let I=Math.min(m.length,o+x.length);return x.push({id:"__DEVUP_SNAPSHOT_CURSOR__",type:"DEVUP_INTERNAL",fields:{offset:o,nextOffset:I,complete:I>=m.length,totalNodes:m.length},extra:{},fieldErrors:{}}),{fileKey:figma.fileKey||"",version:null,rootIds:[t.id],nodes:x,diagnostics:[]}},usedResources:async function(e){let t=e.resources;function r(e,t=new WeakSet,n=0){if(null===e||["string","number","boolean"].includes(typeof e))return e;if(void 0===e)return{$undefined:!0};if("bigint"==typeof e)return{$bigint:e.toString()};if(["function","symbol"].includes(typeof e))return{$unsupported:typeof e};if(n>12)return{$truncated:"max-depth"};if("object"==typeof e&&"parent"in e&&"string"==typeof e.id&&"string"==typeof e.type)return{$nodeId:e.id,$nodeType:e.type};if(Array.isArray(e))return e.map(e=>r(e,t,n+1));if(ArrayBuffer.isView(e))return{$binary:e.constructor.name,byteLength:e.byteLength};if(e instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:e.byteLength};if(t.has(e))return{$circular:!0};t.add(e);let i={},a=new Set(Object.keys(e)),o=e;for(;o&&o!==Object.prototype;){for(let e of Object.getOwnPropertyNames(o))a.add(e);o=Object.getPrototypeOf(o)}for(let o of[...a].sort())if(!(o.startsWith("_")||["parent","children","consumers"].includes(o)))try{let a=r(e[o],t,n+1);a&&"function"===a.$unsupported||(i[o]=a)}catch(e){i[o]={$error:"unavailable"}}return t.delete(e),i}let n=new Map,i="ok";try{for(let e of(await figma.variables.getLocalVariablesAsync()))n.set(e.id,e)}catch(e){i="unavailable"}function a(e){return Object.values(e.valuesByMode||{}).filter(e=>e&&"VARIABLE_ALIAS"===e.type&&"string"==typeof e.id&&e.id).map(e=>e.id)}async function o(e){let t=n.get(e);if(t)return{value:r(t),collectionId:t.variableCollectionId,aliasIds:a(t)};try{let t=await figma.variables.getVariableByIdAsync(e);return t?{value:r(t),collectionId:t.variableCollectionId,aliasIds:a(t)}:{unresolved:{id:e,kind:"variable",reason:"notInFileAndLookupEmpty"}}}catch(t){return{unresolved:{id:e,kind:"variable",reason:"notInFileAndLookupThrew",error:String(t&&t.message||t).slice(0,300)}}}}let l=await Promise.all(t.variableIds.map(o)),s=new Set(t.variableIds),d=l.flatMap(e=>e.aliasIds||[]);for(;d.length>0;){let e=[...new Set(d)].filter(e=>!s.has(e)).sort();for(let t of e)s.add(t);let t=await Promise.all(e.map(o));l.push(...t),d=t.flatMap(e=>e.aliasIds||[])}let f=[...new Set(l.flatMap(e=>e.collectionId?[e.collectionId]:[]))].sort(),c=await Promise.all(f.map(async e=>{try{let t=await figma.variables.getVariableCollectionByIdAsync(e);return t?[r(t)]:[]}catch(e){return[]}})),u=await Promise.all(t.styles.map(async e=>{try{let t=await figma.getStyleByIdAsync(e.id);if(!t)return{unresolved:{id:e.id,kind:"style",reason:"notFoundOrUnavailable"}};return{value:{...r(t),styleType:e.styleType,value:r("PAINT"===e.styleType?t.paints:"EFFECT"===e.styleType?t.effects:"GRID"===e.styleType?t.layoutGrids:t)}}}catch(t){return{unresolved:{id:e.id,kind:"style",reason:"notFoundOrUnavailable",error:String(t&&t.message||t).slice(0,300)}}}}));return{collections:c.flat(),variables:l.flatMap(e=>e.value?[e.value]:[]),styles:u.flatMap(e=>e.value?[e.value]:[]),usedVariableIds:t.variableIds,usedStyleIds:t.styles.map(e=>e.id),localVariableListing:i,localVariableCount:n.size,unresolved:[...l,...u].flatMap(e=>e.unresolved?[e.unresolved]:[])}},variableCatalog:async function(e){let[t,r,n,i,a]=await Promise.all([figma.variables.getLocalVariableCollectionsAsync(),figma.getLocalPaintStylesAsync(),figma.getLocalTextStylesAsync(),figma.getLocalEffectStylesAsync(),figma.getLocalGridStylesAsync()]),o=["PAINT","TEXT","EFFECT","GRID"];return{collections:t.map(e=>(function e(t,r=new WeakSet,n=0){if(null===t||["string","number","boolean"].includes(typeof t))return t;if(void 0===t)return{$undefined:!0};if("bigint"==typeof t)return{$bigint:t.toString()};if(["function","symbol"].includes(typeof t))return{$unsupported:typeof t};if(n>12)return{$truncated:"max-depth"};if(Array.isArray(t))return t.map(t=>e(t,r,n+1));if(r.has(t))return{$circular:!0};r.add(t);let i={},a=new Set(Object.keys(t)),o=t;for(;o&&o!==Object.prototype;){for(let e of Object.getOwnPropertyNames(o))a.add(e);o=Object.getPrototypeOf(o)}for(let o of[...a].sort())if(!o.startsWith("_"))try{let a=e(t[o],r,n+1);a&&"function"===a.$unsupported||(i[o]=a)}catch(e){i[o]={$error:String(e&&e.message?e.message:e)}}return r.delete(t),i})(e)),variableIds:[...new Set(t.flatMap(e=>e.variableIds))].sort(),styles:[r,n,i,a].flatMap((e,t)=>e.map(e=>({id:e.id,styleType:o[t]}))).sort((e,t)=>e.id.localeCompare(t.id)),localComplete:!0,usedRemoteComplete:!1}},variables:async function(e){let t=e.resources;function r(e,t=new WeakSet,n=0){if(null===e||["string","number","boolean"].includes(typeof e))return e;if(void 0===e)return{$undefined:!0};if("bigint"==typeof e)return{$bigint:e.toString()};if(["function","symbol"].includes(typeof e))return{$unsupported:typeof e};if(n>12)return{$truncated:"max-depth"};if("object"==typeof e&&"parent"in e&&"string"==typeof e.id&&"string"==typeof e.type)return{$nodeId:e.id,$nodeType:e.type};if(Array.isArray(e))return e.map(e=>r(e,t,n+1));if(ArrayBuffer.isView(e))return{$binary:e.constructor.name,byteLength:e.byteLength};if(e instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:e.byteLength};if(t.has(e))return{$circular:!0};t.add(e);let i={},a=new Set(Object.keys(e)),o=e;for(;o&&o!==Object.prototype;){for(let e of Object.getOwnPropertyNames(o))a.add(e);o=Object.getPrototypeOf(o)}for(let o of[...a].sort())if(!(o.startsWith("_")||["parent","children","consumers"].includes(o)))try{let a=r(e[o],t,n+1);a&&"function"===a.$unsupported||(i[o]=a)}catch(e){i[o]={$error:String(e&&e.message?e.message:e)}}return t.delete(e),i}let[n,i]=await Promise.all([Promise.all(t.variableIds.map(e=>figma.variables.getVariableByIdAsync(e))),Promise.all(t.styles.map(e=>figma.getStyleByIdAsync(e.id)))]),a=new Map(t.styles.map(e=>[e.id,e])),o=await Promise.all(i.filter(Boolean).map(async e=>{let t=a.get(e.id),n=t.styleType;if(Number.isInteger(t.consumerStart)&&Number.isInteger(t.consumerEnd)){let i=await e.getStyleConsumersAsync();return{id:e.id,styleType:n,$consumerStart:t.consumerStart,$consumerEntries:i.slice(t.consumerStart,t.consumerEnd).map(e=>[e.node.id,e.node.type,r(e.fields)])}}let i=await e.getStyleConsumersAsync();return{...r(e),styleType:n,$consumerCount:i.length,value:r("PAINT"===n?e.paints:"EFFECT"===n?e.effects:"GRID"===n?e.layoutGrids:e)}}));return{variables:n.filter(Boolean).map(e=>r(e)),styles:o}}},t=new Map,r=0;globalThis.__devupNormalizeImage=(e,n,i,a,o)=>new Promise(l=>{r+=1;let s=`decode-${r}`,d=setTimeout(()=>{t.delete(s),l(null)},i);t.set(s,e=>{clearTimeout(d),t.delete(s),l(e)}),figma.ui.postMessage({kind:"devup-decode",id:s,bytes:e,maxEdge:n,width:a,height:o})});let n=globalThis;function i(){let e=figma.currentPage,t=e.selection;return t.length>0&&(n.__devupLastSelection=t.slice(0,20).map(e=>({id:e.id,name:e.name.slice(0,256),type:e.type})),n.__devupLastSelectionPage={id:e.id,name:e.name.slice(0,256)}),{zoom:Number.isFinite(figma.viewport?.zoom)?figma.viewport.zoom:null,observedAt:Date.now(),lastKnownSelection:n.__devupLastSelection??null,lastKnownPage:n.__devupLastSelectionPage??null,currentPage:{id:e.id,name:e.name.slice(0,256)},selection:t.slice(0,20).map(e=>({id:e.id,name:e.name.slice(0,256),type:e.type})),selectionCount:t.length}}function a(){let e={kind:"devup-context",...i()};figma.ui.postMessage(e)}async function o(t){let r=e[t.script];if(!r)return{kind:"devup-result",requestId:t.requestId,error:`DEVUP_BRIDGE_UNKNOWN_SCRIPT: ${t.script} (아는 스크립트: ${Object.keys(e).join(", ")})`};try{var n;"bridgeContext"===t.script&&i();let e=await r({nodeId:(n=t.params).nodeId??"",rootIds:n.rootIds??[],snapshot:n.snapshot??{},search:n.search??{},explore:n.explore??{},resources:n.resources??{variableIds:[],styles:[]},largeValue:n.largeValue??{},asset:n.asset??{},theme:n.theme??{offset:0}});return{kind:"devup-result",requestId:t.requestId,data:e}}catch(e){return{kind:"devup-result",requestId:t.requestId,error:e instanceof Error?e.message:String(e)}}}let l=[],s=!1,d=null,f=new Set;async function c(){if(!s){s=!0;try{for(;l.length>0;){let e=l.shift();d=e.requestId;let t=await o(e).catch(t=>({kind:"devup-result",requestId:e.requestId,error:t instanceof Error?t.message:String(t)}));d=null,f.delete(e.requestId)||figma.ui.postMessage(t)}}finally{s=!1}}}let u=null;function p(){u??(u=(async()=>{try{await figma.loadAllPagesAsync(),await Promise.all([figma.variables.getLocalVariableCollectionsAsync(),figma.variables.getLocalVariablesAsync(),figma.getLocalPaintStylesAsync(),figma.getLocalTextStylesAsync(),figma.getLocalEffectStylesAsync(),figma.getLocalGridStylesAsync()])}catch{}finally{u=null}})())}figma.showUI(__html__,{width:320,height:220}),p(),figma.on("selectionchange",a),figma.on("currentpagechange",a),figma.ui.onmessage=e=>{if("object"==typeof e&&null!==e){if("devup-ready"===e.kind){let e={kind:"devup-status",fileKey:figma.fileKey??null,fileName:figma.root.name,port:1993,...i()};figma.ui.postMessage(e);return}if("devup-job"===e.kind){l.push(e),c();return}if("devup-cancel"===e.kind){var r;let t;return void(r=e.requestId,(t=l.findIndex(e=>e.requestId===r))>=0?l.splice(t,1):d===r&&f.add(r))}if("devup-decoded"===e.kind)return void t.get(e.id)?.(e.png??null);"devup-in-view"===e.kind&&p()}}})(); \ No newline at end of file From b2223c972e4da747cccde47614b726c9172abb4f Mon Sep 17 00:00:00 2001 From: leader Date: Sat, 24 Oct 2026 01:19:29 +0900 Subject: [PATCH 092/149] fix(project-plan): block destructive source model replacement (55) --- .../devup-mcp/src/server/project_plan/erd.rs | 21 +++++++++++++++++++ .../src/server/project_plan/tests/recovery.rs | 18 ++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/crates/devup-mcp/src/server/project_plan/erd.rs b/crates/devup-mcp/src/server/project_plan/erd.rs index 500dddc4..47088965 100644 --- a/crates/devup-mcp/src/server/project_plan/erd.rs +++ b/crates/devup-mcp/src/server/project_plan/erd.rs @@ -267,6 +267,7 @@ pub(super) fn draft(input: &ProjectPlanInput, plan: &mut Value) -> Result<(), De "vespertide export --orm seaorm" ]); plan["backendNotes"] = json!([ + "Stop if vespertide diff reports a table or column deletion; preserve existing models and review the destructive change before generating SQL or a revision.", "Before cargo check, make sure the API Cargo.toml depends on serde (derive) and serde_json; exported entities use them.", "Add `mod models;` to the API main.rs; export does not wire the generated src/models.", "Right after export, cargo clippy -- -D warnings reports never-constructed ModelEx structs until routes use them; that is expected before the route units." @@ -357,6 +358,26 @@ pub(super) fn write_models( return Err(invalid("API plan requires a complete ERD")); } validate(entities)?; + let mut existing = Vec::new(); + scan_models(&Path::new(root).join(text(&plan["modelsDir"])), &mut existing)?; + let mut removed_columns = Vec::new(); + for model in &existing { + if let Some(entity) = entities.iter().find(|entity| entity["name"] == model["name"]) { + for column in array(&model["columns"]) { + if !array(&entity["columns"]).iter().any(|candidate| candidate["name"] == column["name"]) { + removed_columns.push(json!({"table":model["name"],"column":column["name"]})); + } + } + } + } + if !removed_columns.is_empty() { + let mut error = invalid("Confirm would remove existing model columns; redraft with the existing columns preserved before confirming"); + error.details = json!({"removedColumns":removed_columns, + "decision":{"id":"model-column-removal","destructive":true,"state":"pending", + "reason":"Existing source-model columns are absent from the proposed ERD", + "recommended":"Preserve existing columns and redraft"}}); + return Err(error); + } for e in entities { let file = format!("{}/{}.json", text(&plan["modelsDir"]), text(&e["name"])); let bytes = serde_json::to_vec_pretty(e).map_err(|e| invalid(e.to_string()))?; diff --git a/crates/devup-mcp/src/server/project_plan/tests/recovery.rs b/crates/devup-mcp/src/server/project_plan/tests/recovery.rs index ca971220..904e2baa 100644 --- a/crates/devup-mcp/src/server/project_plan/tests/recovery.rs +++ b/crates/devup-mcp/src/server/project_plan/tests/recovery.rs @@ -1,5 +1,23 @@ use super::*; +#[tokio::test] +async fn confirm_refuses_to_drop_existing_columns_without_writing_models() { + let dir = TestDir::new(); + let (request, policy, draft) = drafted(dir.path(), true).await; + let mut post = array(&draft["plan"]["entities"]).iter().find(|entity| entity["name"] == "post").unwrap().clone(); + post["columns"].as_array_mut().unwrap().push(json!({"name":"category","type":"text","nullable":false})); + std::fs::create_dir_all(dir.path().join("models")).unwrap(); + let path = dir.path().join("models/post.json"); + let original = serde_json::to_vec(&post).unwrap(); + std::fs::write(&path, &original).unwrap(); + let error = run(&request, &policy).await.unwrap_err(); + assert_eq!(error.details["removedColumns"], json!([{"table":"post","column":"category"}])); + assert_eq!(error.details["decision"]["destructive"], true); + assert_eq!(std::fs::read(path).unwrap(), original); + assert!(!dir.path().join("models/user.json").exists()); + assert_eq!(read(&request.project_root).unwrap()["state"], "draft"); +} + #[test] fn empty_prefix_header_preserves_thirty_character_capacity() { let name = "123456789012345678901234567890"; From baf6690221f5db5eb2f20cc3cb442713a94ab9a2 Mon Sep 17 00:00:00 2001 From: leader Date: Sat, 24 Oct 2026 01:20:18 +0900 Subject: [PATCH 093/149] fix(figma-search): flag hidden ancestry and rank live designs first (08) --- crates/devup-mcp-figma/src/lib.rs | 4 +- crates/devup-mcp-figma/src/scripts/search.js | 12 +++++ crates/devup-mcp-figma/src/search.rs | 48 ++++++++++++++++++- .../src/server/projection/completion.rs | 3 ++ plugin/dist/code.js | 2 +- 5 files changed, 66 insertions(+), 3 deletions(-) diff --git a/crates/devup-mcp-figma/src/lib.rs b/crates/devup-mcp-figma/src/lib.rs index 2ec41fcc..7b02413f 100644 --- a/crates/devup-mcp-figma/src/lib.rs +++ b/crates/devup-mcp-figma/src/lib.rs @@ -63,7 +63,9 @@ pub use payload::{ pub use resources::{ ResourceKind, ResourceOccurrence, ResourceScope, UsedResourceRefs, collect_used_resource_refs, }; -pub use search::{SearchOptions, SearchResult, excluded_duplicate_warnings, search_snapshot}; +pub use search::{ + SearchOptions, SearchResult, excluded_duplicate_warnings, node_excluded_reason, search_snapshot, +}; pub use section::{ BatchLimits, SectionBatch, SectionCandidate, SectionIndex, SectionSummary, build_section_index, plan_batches, screen_owners, diff --git a/crates/devup-mcp-figma/src/scripts/search.js b/crates/devup-mcp-figma/src/scripts/search.js index e4d4209c..64510aac 100644 --- a/crates/devup-mcp-figma/src/scripts/search.js +++ b/crates/devup-mcp-figma/src/scripts/search.js @@ -53,6 +53,15 @@ function score(name) { return distance <= threshold ? Math.max(0, 50 - distance) : null; } +function excluded(node) { + for (let current = node; current && current.type !== "DOCUMENT"; current = current.parent) { + if (current.visible === false || current.opacity === 0) return true; + const name = (current.name || "").toLowerCase(); + if (["trash", "archive", "draft", "copy", "hidden", "휴지통", "보관", "초안", "복사", "pdf", "저장용", "export", "출력"].some(keyword => name.includes(keyword))) return true; + } + return false; +} + const descendants = root === page ? page.findAll(() => true) : "findAll" in root ? root.findAll(() => true) : []; @@ -61,6 +70,7 @@ const ranked = [root, ...descendants] .map((node) => ({ node, score: score(node.name) })) .filter((entry) => entry.score !== null) .sort((left, right) => + Number(excluded(left.node)) - Number(excluded(right.node)) || right.score - left.score || left.node.name.localeCompare(right.node.name) || left.node.id.localeCompare(right.node.id), @@ -87,6 +97,8 @@ return { type: node.type, fields: { name: node.name, + visible: node.visible !== false, + opacity: typeof node.opacity === "number" ? node.opacity : 1, searchCandidatesTruncated: node === root && ranked.length > candidates.length, parentId: node.parent && node.parent.type !== "DOCUMENT" ? node.parent.id : null, childrenIds: "children" in node ? node.children.map((child) => child.id) : [], diff --git a/crates/devup-mcp-figma/src/search.rs b/crates/devup-mcp-figma/src/search.rs index 3cb25b21..448d68c4 100644 --- a/crates/devup-mcp-figma/src/search.rs +++ b/crates/devup-mcp-figma/src/search.rs @@ -41,6 +41,7 @@ const EXCLUDED_KEYWORDS: &[&str] = &[ "archive", "draft", "copy", + "hidden", "휴지통", "보관", "초안", @@ -64,6 +65,33 @@ fn excluded_reason(breadcrumb: &[String], page_name: Option<&str>) -> Option Option { + let parents = parent_index(snapshot); + let mut current = Some(node_id); + let mut visited = std::collections::BTreeSet::new(); + while let Some(id) = current { + if !visited.insert(id) { + break; + } + let node = snapshot.nodes.get(id)?; + let name = node.typed_view().name().unwrap_or(id); + if node.fields.get("visible") == Some(&serde_json::Value::Bool(false)) + || node + .fields + .get("opacity") + .and_then(serde_json::Value::as_f64) + == Some(0.0) + { + return Some(format!("hidden ({name})")); + } + if let Some(reason) = excluded_reason(&[], Some(name)) { + return Some(reason); + } + current = parents.get(id).map(String::as_str); + } + None +} + pub fn search_snapshot( snapshot: &Snapshot, target: &FigmaTarget, @@ -122,7 +150,7 @@ pub fn search_snapshot( }; let breadcrumb = breadcrumb(snapshot, &parents, &node.id); let page_name = ancestor_page(snapshot, &parents, &node.id); - let excluded = excluded_reason(&breadcrumb, page_name.as_deref()); + let excluded = node_excluded_reason(snapshot, &node.id); Some(SearchResult { excluded, node_id: node.id.clone(), @@ -496,6 +524,24 @@ mod tests { assert!(excluded_reason(&[], Some("디자인 시스템")).is_none()); } + #[test] + fn feedback08_hidden_ancestors_are_excluded() { + let (mut snapshot, target) = typed_snapshot(); + snapshot + .nodes + .get_mut("0:1") + .unwrap() + .fields + .insert("visible".into(), json!(false)); + let results = search_snapshot(&snapshot, &target, &options("Home", &[])).unwrap(); + assert!( + results[0] + .excluded + .as_deref() + .is_some_and(|reason| reason.contains("hidden")) + ); + } + #[test] fn an_empty_query_without_node_types_is_invalid_input() { let (snapshot, target) = typed_snapshot(); diff --git a/crates/devup-mcp/src/server/projection/completion.rs b/crates/devup-mcp/src/server/projection/completion.rs index fddbe45f..6e85963d 100644 --- a/crates/devup-mcp/src/server/projection/completion.rs +++ b/crates/devup-mcp/src/server/projection/completion.rs @@ -111,6 +111,9 @@ fn attach_node_page(source: &mut Value, payload: &CollectedPayload) { let Some(start) = payload.target.node_id.as_deref() else { return; }; + if let Some(reason) = devup_mcp_figma::node_excluded_reason(&payload.snapshot, start) { + source["excluded"] = json!(reason); + } let mut current = Some(start.to_owned()); let mut visited = std::collections::BTreeSet::new(); let mut page: Option<(String, String)> = None; diff --git a/plugin/dist/code.js b/plugin/dist/code.js index acc5ad31..70fe790d 100644 --- a/plugin/dist/code.js +++ b/plugin/dist/code.js @@ -1 +1 @@ -(()=>{"use strict";let e={assets:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=e.asset;function r(e){let t=[0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,0xe49b69c1,0xefbe4786,0xfc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x6ca6351,0x14292967,0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2],r=64*Math.ceil((e.length+9)/64),n=new Uint8Array(r);n.set(e),n[e.length]=128;let i=8*e.length;for(let e=0;e<8;e+=1)n[r-1-e]=255&Math.floor(i/2**(8*e));let a=[0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19],o=(e,t)=>e>>>t|e<<32-t;for(let e=0;e>>3,n=o(r[e-2],17)^o(r[e-2],19)^r[e-2]>>>10;r[e]=r[e-16]+t+r[e-7]+n>>>0}let[i,l,s,d,f,c,u,p]=a;for(let e=0;e<64;e+=1){let n=p+(o(f,6)^o(f,11)^o(f,25))+(f&c^~f&u)+t[e]+r[e]>>>0,a=(o(i,2)^o(i,13)^o(i,22))+(i&l^i&s^l&s)>>>0;p=u,u=c,c=f,f=d+n>>>0,d=s,s=l,l=i,i=n+a>>>0}for(let[e,t]of[i,l,s,d,f,c,u,p].entries())a[e]=a[e]+t>>>0}return a.map(e=>e.toString(16).padStart(8,"0")).join("")}async function n(e){try{let t=await figma.getNodeByIdAsync(e);if(t)return t}catch(r){if(!e.startsWith("I")||!e.includes(";"))throw r;try{return await t()}catch(e){throw Error(`${r.message||r}; nested lookup fallback: ${e.message||e}`)}}if(!e.startsWith("I")||!e.includes(";"))return null;return t();async function t(){let t=await figma.getNodeByIdAsync(e.slice(1).split(";")[0]),r=t?[t]:[],n=new Set;for(;r.length;){let t=r.pop();if(t.id===e)return t;if(!n.has(t.id)){if(n.add(t.id),n.size>1e4)throw Error("nested lookup exceeds 10000 nodes");Array.isArray(t.children)&&r.push(...t.children)}}throw Error(`nested node ${e} not found in its instance`)}}async function i(e,t,r=85e3,n=()=>{}){let a,o=Date.now();try{let i=await Promise.race([e.exportAsync(t),new Promise((e,t)=>{a=setTimeout(()=>t(Error(`DEVUP_ASSET_TIMEOUT: native export exceeded ${r} ms`)),r)})]);return n("native-export",Date.now()-o),i}catch(i){if(n("native-export",Date.now()-o,String(i.message||i).slice(0,500)),"SVG_STRING"!==t.format)throw i;let r=Date.now();try{let t=function(e){let t=e=>"number"==typeof e&&Number.isFinite(e),r=(e,t)=>{throw Error(`geometry fallback unsupported: ${e.type||"unknown"} ${e.id||""}: ${t}`)},n=0,i=0;function a(e,i,a){(void 0!==a&&!Array.isArray(a)||void 0===a&&Array.isArray(i)&&i.length)&&r(e,"unresolved paint requires native export");let o=Array.isArray(a)?a.filter(e=>!1!==e.visible):[];return o.length?(Array.isArray(i)&&i.length||r(e,"paint has no rendered geometry"),o.map(a=>{("SOLID"!==a.type||a.blendMode&&"NORMAL"!==a.blendMode)&&r(e,"paint type or blending requires native export");let o=a.color;o&&[o.r,o.g,o.b].every(e=>t(e)&&e>=0&&e<=1)||r(e,"invalid solid color");let l=void 0===a.opacity?1:a.opacity;return(!t(l)||l<0||l>1)&&r(e,"invalid paint opacity"),["NONZERO","EVENODD"].map(t=>{let a=i.filter(e=>e.windingRule===t);if(!a.length)return"";for(let t of a)"string"==typeof t.data&&t.data.trim()||r(e,"empty path"),n++;return`"']/g,e=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[e])}" fill="rgb(${100*o.r}%,${100*o.g}%,${100*o.b}%)" fill-opacity="${l}" fill-rule="${"EVENODD"===t?"evenodd":"nonzero"}"/>`}).join("")}).join("")):""}let o=e.absoluteRenderBounds,l=e.absoluteTransform;(!o||![o.x,o.y,o.width,o.height].every(t)||o.width<=0||o.height<=0)&&r(e,"missing render bounds"),(!Array.isArray(l)||2!==l.length||!l.every(e=>Array.isArray(e)&&3===e.length&&e.every(t))||0!==l[0][1]||0!==l[1][0]||l[0][0]<=0||l[1][1]<=0)&&r(e,"root rotation, reflection or shear requires native export");let s=(o.x-l[0][2])/l[0][0],d=(o.y-l[1][2])/l[1][1],f=o.width/l[0][0],c=o.height/l[1][1],u=function e(n,o){if(++i>1e4&&r(n,"subtree exceeds 10000 nodes"),n.isMask&&r(n,"mask requires native export"),!1===n.visible||0===n.opacity)return"";n.clipsContent&&r(n,"clipping requires native export"),Array.isArray(n.effects)&&n.effects.some(e=>!1!==e.visible)&&r(n,"effect requires native export"),n.blendMode&&!["NORMAL","PASS_THROUGH"].includes(n.blendMode)&&r(n,"layer blending requires native export");let l=void 0===n.opacity?1:n.opacity;(!t(l)||l<0||l>1)&&r(n,"invalid layer opacity");let s="";if(!o){let e=n.relativeTransform;Array.isArray(e)&&2===e.length&&e.every(e=>Array.isArray(e)&&3===e.length&&e.every(t))||r(n,"missing relative transform"),s=` transform="matrix(${e[0][0]} ${e[1][0]} ${e[0][1]} ${e[1][1]} ${e[0][2]} ${e[1][2]})"`}let d="";if(["GROUP","FRAME","INSTANCE","COMPONENT"].includes(n.type))(Array.isArray(n.fills)&&n.fills.some(e=>!1!==e.visible)||Array.isArray(n.strokes)&&n.strokes.some(e=>!1!==e.visible))&&r(n,"container paint requires native export"),Array.isArray(n.children)||r(n,"missing children"),d=(!0===n.itemReverseZIndex?[...n.children].reverse():n.children).map(t=>e(t,!1)).join("");else{for(let e of(["VECTOR","BOOLEAN_OPERATION","RECTANGLE","ELLIPSE","POLYGON","STAR","LINE"].includes(n.type)||r(n,"node type requires native export"),Array.isArray(n.strokes)&&n.strokes.some(e=>!1!==e.visible)&&"CENTER"!==n.strokeAlign&&r(n,"non-center stroke alignment requires native export"),Array.isArray(n.dashPattern)&&n.dashPattern.length&&r(n,"dashed stroke requires native export"),[n.fillGeometry,n.strokeGeometry]))void 0!==e&&(!Array.isArray(e)||e.some(e=>!["NONZERO","EVENODD"].includes(e.windingRule)))&&r(n,"unknown winding rule");d=a(n,n.fillGeometry,n.fills)+a(n,n.strokeGeometry,n.strokes)}return`${d}`}(e,!0);return n||r(e,"no painted geometry"),`${u}`}(e);return n("geometry-fallback",Date.now()-r),t}catch(e){throw Error(`native export: ${i.message||i}; ${e.message||e}`)}}finally{clearTimeout(a)}}let a=Date.now(),o="lookup",l="unknown",s=Math.min(6e5,Math.max(1e3,Number(t.timeoutMs)||9e4)),d={method:"native",stages:[]};function f(e,t,r){d.stages.push({stage:e,elapsedMs:t}),r&&(d.nativeError=r),"geometry-fallback"===e&&(d.method="geometry")}async function c(e){let t;try{return await Promise.race([e(),new Promise((e,r)=>{t=setTimeout(()=>r(Error(`DEVUP_ASSET_TIMEOUT: ${o} exceeded ${s} ms asset budget`)),Math.max(0,s-(Date.now()-a)))})])}finally{clearTimeout(t)}}function u(e){let r=e&&"string"==typeof e.message?e.message:String(e),n="string"==typeof figma.editorType?` (editor: ${figma.editorType})`:"";return`${o} failed after ${Date.now()-a} ms${n} (node: ${t.nodeId}, type: ${l}): ${r}`.slice(0,500)}function p(e,r){return e&&d.renderInfo&&(d.renderInfo.attemptedRepresentation=d.renderInfo.representation,d.renderInfo.representation="not-exported",d.renderInfo.renderedAs="not-exported",d.renderInfo.effectsBakedIntoAsset=!1,d.renderInfo.fidelity="unverified"),{kind:"devupAssetExport",fileKey:figma.fileKey||"",version:t.version,assetId:t.assetId,nodeId:t.nodeId,field:t.field,imageHash:t.imageHash,format:t.format,scale:t.scale,renderMode:h,usageAspectRatio:t.usageAspectRatio??null,maxWidth:t.maxWidth??null,srcset:!0===t.srcset,visibleScopeNodeId:t.visibleScopeNodeId??null,status:"failed",byteLength:null,sha256:null,errorCode:e,exportDiagnostics:d,...r?{errorDetail:r}:{}}}let g="string"==typeof t.field&&t.field.startsWith("$original-image/fills/"),h=void 0===t.renderMode?g?"fill":["svg","pdf"].includes(t.format)?"node":"original-source":t.renderMode,y=["auto","original-source"].includes(h)?"auto"===h?"node":"fill":h;function m(e,r){let n=e=>"number"==typeof e&&Number.isFinite(e)&&e>0,i=Array.isArray(e.effects)?e.effects:null,a=[],o=e=>[...e].slice(0,128).join(""),l=["string"==typeof e.name?o(e.name):"layer"],s=new Set,d=e;for(let t=e.parent;t&&a.length<32&&!s.has(t);t=t.parent)s.add(t),l.length<4&&"string"==typeof t.name&&t.name.length&&!["PAGE","DOCUMENT"].includes(t.type)&&l.push(o(t.name)),a.push({type:t.type||"unknown",clipsContent:t.clipsContent??null,isMask:t.isMask??null,effects:Array.isArray(t.effects)?t.effects:null,opacity:t.opacity??null,blendMode:t.blendMode??null,visible:t.visible??null,siblingMaskPresent:Array.isArray(t.children)?t.children.some(e=>e!==d&&!0===e.isMask):null}),d=t;return{sourceKind:r?"image-fill":t.imageHash?"image-node":"vector-node",renderedAs:"fill"===y?"raw-fill":"node-render",representation:"fill"===y?"original-fill":"native-node",layerHierarchy:l.reverse(),scope:"fill"===y?"selected-paint-original-bytes":"exact-node-subtree",exportSettings:"node"===y?{contentsOnly:!0,useAbsoluteBounds:!1}:null,requestedScale:t.scale,scaleSemantics:"fill"===y?"intrinsic-original-bytes-no-resize":["png","jpg"].includes(t.format)?"native-node-SCALE-constraint":"not-applied-to-vector-document",nodeSize:n(e.width)&&n(e.height)?{width:e.width,height:e.height,aspectRatio:e.width/e.height}:null,outputSize:null,dimensionEvidence:"unknown-until-byte-inspection",aspectRatioComparison:"unknown",scaleMode:r?r.scaleMode??null:null,imageTransform:r?r.imageTransform??null:null,visibleSourceRatio:null,visibleSourceRatioEvidence:"not-computed",effects:i,extraFills:Array.isArray(e.fills)?Math.max(0,e.fills.filter(e=>!1!==e.visible).length-!!r):null,effectsBakedIntoAsset:"node"===y&&["png","jpg"].includes(t.format)&&null!==i,bakedEffects:"node"===y&&["png","jpg"].includes(t.format)&&i?i.filter(e=>!1!==e.visible):[],instanceOverride:"unknown",instanceOverrideEvidence:"no-component-paint-comparison",sourceProperties:{scaleMode:r?r.scaleMode??null:null,imageTransform:r?r.imageTransform??null:null,rotation:r?r.rotation??null:null,filters:r?r.filters??null:null,opacity:r?r.opacity??null:null,effects:i,blurEffects:i?i.filter(e=>!1!==e.visible&&String(e.type).includes("BLUR")):null,isMask:e.isMask??null,clipsContent:e.clipsContent??null,ancestors:a},unappliedProperties:"fill"===y?["scaleMode","imageTransform","rotation","filters","paintOpacity","nodeGeometry","nodeTransforms","nodeEffectsIncludingBlur","nodeAndAncestorMasks","ancestorClipsAndEffects"]:[],warnings:"fill"===y?["original-fill-properties-unapplied","aspect-ratio-unknown"]:["ancestor-canvas-appearance-unverified","aspect-ratio-unknown"],fidelity:"unverified"}}function b(e,t){let r=0;for(let n=e;n&&r++<32;n=n.parent){if("INSTANCE"===n.type&&Array.isArray(n.overrides)&&n.overrides.find(t=>t.id===e.id&&Array.isArray(t.overriddenFields)&&t.overriddenFields.includes("fills"))){t.instanceOverride=!0,t.instanceOverrideEvidence={kind:"figma-instance-overrides",instanceId:n.id,nodeId:e.id,overriddenFields:["fills"]};return}if(["PAGE","DOCUMENT"].includes(n.type))break}}try{let e;if(!["original-source","fill","node","visible","auto"].includes(h))return p("DEVUP_ASSET_RENDER_MODE_UNSUPPORTED");if(!g&&"fill"===h&&(!String(t.field).startsWith("fills/")||!["png","jpg","webp"].includes(t.format)||1!==t.scale))return p("DEVUP_ASSET_FILL_SETTINGS_UNSUPPORTED","Original fill requires fills/N, png or jpg matching its codec, and scale 1; no resizing or transcoding.");if((g||["fill","original-source"].includes(h))&&"bridge"!==t.transport)return p(g?"DEVUP_ORIGINAL_IMAGE_REQUIRES_BRIDGE":"DEVUP_ASSET_FILL_REQUIRES_BRIDGE");let x=await c(()=>n(t.nodeId));if(f("lookup",Date.now()-a),l=x&&x.type||"unknown",!x||["node","visible"].includes(h)&&"function"!=typeof x.exportAsync)return p("DEVUP_ASSET_UNSUPPORTED_BY_UPSTREAM",u(Error(x?"node has no exportAsync":"node not found")));let E=null;if(g||"string"==typeof t.field&&t.field.startsWith("fills/")){let e=Number(t.field.slice(g?22:6)),r="fills"in x&&Array.isArray(x.fills)?x.fills:[],n=Number.isInteger(e)?r[e]:null,i=n&&"IMAGE"===n.type?n.imageHash||n.imageRef:null;if(!n||i!==t.imageHash)return p("DEVUP_ASSET_SOURCE_CHANGED");E=n}else if("node"!==t.field)return p("DEVUP_ASSET_FIELD_UNSUPPORTED");if("original-source"===h&&(!E||!t.imageHash))return p("DEVUP_ORIGINAL_SOURCE_UNAVAILABLE","No IMAGE fill source exists for this selection. Choose an IMAGE fill or explicitly renderMode node; vector assets should retain SVG.");let A=x;if("visible"===h){let e=t.visibleScopeNodeId;if(!e)return p("DEVUP_VISIBLE_CONTEXT_UNAVAILABLE","Recollect the parent frame and provide visibleScopeNodeId.");let r=new Set;for(;A&&A.id!==e&&!r.has(A)&&r.size<64;)r.add(A),A=A.parent;if(!A||A.id!==e||["PAGE","DOCUMENT"].includes(A.type)||"function"!=typeof A.exportAsync)return p("DEVUP_VISIBLE_SCOPE_UNSUPPORTED","The selected capture scope is not an exportable ancestor of the requested node.")}if(d.renderInfo=m(x,E),"visible"===h){let e=d.renderInfo;e.representation="native-visible",e.renderedAs="visible-scoped-capture",e.captureNodeId=A.id,e.captureBox={width:A.width,height:A.height,aspectRatio:A.width/A.height},e.contextScope="selected-ancestor-subtree",e.captureEffects=Array.isArray(A.effects)?A.effects:null,e.requestedNodeEffects=e.effects,e.effects=e.captureEffects,e.sourceProperties.captureEffects=e.captureEffects,e.visibleContext={requestedNodeId:x.id,captureNodeId:A.id,included:["selected-scope-children","selected-scope-clipping","selected-scope-masks","selected-scope-rounding"],outsideScope:"unknown; ancestors, page backdrop and external layers are not included",canvasReferenceCompared:!1},e.warnings.push("visible-scoped-capture","outside-capture-context-unverified")}b(x,d.renderInfo);let I=null;if(E)try{let r=figma.getImageByHash(t.imageHash),n=r&&await Promise.race([r.getSizeAsync(),new Promise(t=>{e=setTimeout(()=>t(null),250)})]);I=n&&n.width>0&&n.height>0&&Number.isFinite(n.width)&&Number.isFinite(n.height)?{...n,aspectRatio:n.width/n.height}:null}catch(e){}finally{clearTimeout(e)}let S=null;if("auto"===h){let e=function(e,r,n){let i,a=[],o=(e,t)=>{e||a.push(t)},l=e=>Array.isArray(e)&&2===e.length&&e.every(e=>Array.isArray(e)&&3===e.length&&e.every(Number.isFinite))&&1===e[0][0]&&1===e[1][1]&&0===e[0][1]&&0===e[1][0]&&Number.isInteger(e[0][2])&&Number.isInteger(e[1][2]);for(let a of(o("bridge"===t.transport,"raw-requires-bridge"),o(1===t.scale&&["png","jpg"].includes(t.format),"raw-requires-unscaled-raster"),o(!!r&&!0===r.visible&&1===r.opacity&&"NORMAL"===r.blendMode,"paint-compositing-or-unknown"),o(r&&"FILL"===r.scaleMode&&Array.isArray(i=r.imageTransform)&&2===i.length&&i.every((e,t)=>Array.isArray(e)&&3===e.length&&e.every((e,r)=>e===[[1,0,0],[0,1,0]][t][r]))&&0===r.rotation,"paint-transform-or-unknown"),o(r&&r.filters&&Object.values(r.filters).every(e=>0===e),"image-filters-or-unknown"),o(Array.isArray(e.fills)&&1===e.fills.length,"multiple-fills-or-unknown"),o("RECTANGLE"===e.type&&0===e.cornerRadius&&Array.isArray(e.strokes)&&0===e.strokes.length&&(!e.children||0===e.children.length),"node-geometry-or-unknown"),o(Array.isArray(e.effects)&&0===e.effects.length&&!1===e.isMask&&(void 0===e.clipsContent||!1===e.clipsContent)&&1===e.opacity&&!0===e.visible&&"NORMAL"===e.blendMode,"node-effects-mask-compositing-or-unknown"),o(l(e.absoluteTransform)&&l(e.relativeTransform),"node-transform-or-unknown"),o(n.sourceProperties.ancestors.length>0&&n.sourceProperties.ancestors.some(e=>["PAGE","DOCUMENT"].includes(e.type)),"ancestor-chain-incomplete"),o(n.sourceProperties.ancestors.every(e=>!1===e.siblingMaskPresent),"ancestor-sibling-mask-or-unknown"),n.sourceProperties.ancestors.filter(e=>!["PAGE","DOCUMENT"].includes(e.type))))o(!1===a.clipsContent&&!1===a.isMask&&!1===a.siblingMaskPresent&&Array.isArray(a.effects)&&0===a.effects.length&&1===a.opacity&&!0===a.visible&&["NORMAL","PASS_THROUGH"].includes(a.blendMode),"ancestor-dependency-or-unknown");return{eligible:0===a.length,reasons:[...new Set(a)],evidence:"explicit-plugin-properties; intrinsic-dimensions-and-codec-required"}}(x,E,d.renderInfo);if(e.eligible)try{let r=figma.getImageByHash(t.imageHash),n=I;n&&n.width===x.width&&n.height===x.height?(S=await c(()=>r.getBytesAsync()),(137===S[0]&&80===S[1]&&78===S[2]&&71===S[3]?"png":255===S[0]&&216===S[1]&&255===S[2]?"jpg":"unknown")!==t.format&&e.reasons.push("source-codec-mismatch-or-unknown")):e.reasons.push("intrinsic-dimensions-mismatch-or-unknown")}catch(t){e.reasons.push("source-inspection-unavailable")}e.eligible=0===e.reasons.length,y=e.eligible?"fill":"node",d.renderInfo=m(x,E),b(x,d.renderInfo),d.renderInfo.autoDecision={...e,selected:y}}if(d.renderInfo.sourceSize=I,d.renderInfo.sourceSizeEvidence=I?"Figma-image-getSizeAsync":"unknown",g||"fill"===y){d.method="original-fill",o="reading the original image";let e=figma.getImageByHash(t.imageHash);if(!e)return p("DEVUP_ORIGINAL_IMAGE_NOT_FOUND");let n=S||await c(()=>e.getBytesAsync());if(0===n.length||n.length>0x5000000)return p("DEVUP_ASSET_RESPONSE_TOO_LARGE",`${n.length} bytes; one answer carries 1 to 83886080`);let i=e=>e.every((e,t)=>n[t]===e),a=i([137,80,78,71,13,10,26,10])?"image/png":i([255,216,255])?"image/jpeg":i([71,73,70,56])?"image/gif":i([82,73,70,70])&&87===n[8]&&69===n[9]&&66===n[10]&&80===n[11]?"image/webp":null;if(!a)return p("DEVUP_ORIGINAL_IMAGE_CODEC_UNSUPPORTED");if(d.renderInfo.originalMimeType=a,!g&&"fill"===h&&"webp"!==t.format&&a!==({png:"image/png",jpg:"image/jpeg"})[t.format])return p("DEVUP_ASSET_FILL_CODEC_MISMATCH",`Original fill codec is ${a}, but ${t.format} was requested; choose the matching format or renderMode node. No transcoding was performed.`);if(!g)return{...p(null),status:"exported",mimeType:a,byteLength:n.length,sha256:r(n),data:figma.base64Encode(n)};let l=await c(()=>e.getSizeAsync());return{...p(null),status:"exported",kind:"devupOriginalImage",representation:"original-image-v1",format:null,scale:null,mimeType:a,width:l.width,height:l.height,byteLength:n.length,sha256:r(n),data:figma.base64Encode(n)}}let w="webp"===t.format?"PNG":String(t.format||"").toUpperCase();if(!["PNG","JPG","SVG","PDF"].includes(w))return p("DEVUP_ASSET_FORMAT_UNSUPPORTED");let v=Number(t.scale??2);if(!Number.isFinite(v)||v<=0||v>4)return p("DEVUP_ASSET_SCALE_INVALID");let N="SVG"===w,O={format:N?"SVG_STRING":w,contentsOnly:!0,useAbsoluteBounds:!1};("PNG"===w||"JPG"===w)&&(O.constraint={type:"SCALE",value:v}),o="export";let T=Math.max(1,s-(Date.now()-a)),_=N?Math.max(1,T-Math.min(5e3,Math.floor(T/5))):T,P=Date.now(),M=await c(()=>"visible"===h?A.exportAsync(O):E?x.exportAsync(O):i(x,O,_,f));if(E&&f("native",Date.now()-P),"geometry"===d.method&&(d.renderInfo.representation="geometry-fallback",d.renderInfo.renderedAs="geometry-fallback",d.renderInfo.effectsBakedIntoAsset=!1,d.renderInfo.warnings.push("geometry-fallback-not-native-render")),Date.now()-a>=s)throw Error(`DEVUP_ASSET_TIMEOUT: export exceeded ${s} ms asset budget`);o="encode";let C=Date.now(),k=N&&"string"==typeof M?M:null,R=null===k?M instanceof Uint8Array?M:new Uint8Array(M):function(e){let t=[];for(let r=0;r=55296&&n<=56319){let t=r+1=56320&&t<=57343?(n=65536+(n-55296<<10)+(t-56320),r+=1):n=65533}else n>=56320&&n<=57343&&(n=65533);n<128?t.push(n):n<2048?t.push(192|n>>6,128|63&n):n<65536?t.push(224|n>>12,128|n>>6&63,128|63&n):t.push(240|n>>18,128|n>>12&63,128|n>>6&63,128|63&n)}return new Uint8Array(t)}(k);if(0===R.length||R.length>0x5000000)return p("DEVUP_ASSET_RESPONSE_TOO_LARGE",`${w} at scale ${v} is ${R.length} bytes; one answer carries 1 to 83886080`);let D=r(R);f("encode",Date.now()-C);let L=figma.io&&"function"==typeof figma.io.write,$="PNG"===w&&R.length>786432;if(L&&(null!==k&&R.length>12288||$))return{kind:"devupAssetExport",fileKey:figma.fileKey||"",version:t.version,assetId:t.assetId,nodeId:t.nodeId,field:t.field,imageHash:t.imageHash,format:t.format,scale:v,renderMode:h,usageAspectRatio:t.usageAspectRatio??null,maxWidth:t.maxWidth??null,srcset:!0===t.srcset,visibleScopeNodeId:t.visibleScopeNodeId??null,status:"chunked",byteLength:R.length,sha256:D,cursor:{nextOffset:0,maxChunkBytes:12288},errorCode:null,exportDiagnostics:d};return L&&figma.io.write(`devup-asset-${t.assetId.replace(/[^A-Za-z0-9_-]/g,"_")}.${w.toLowerCase()}`,R),{kind:"devupAssetExport",fileKey:figma.fileKey||"",version:t.version,assetId:t.assetId,nodeId:t.nodeId,field:t.field,imageHash:t.imageHash,format:t.format,scale:v,renderMode:h,usageAspectRatio:t.usageAspectRatio??null,maxWidth:t.maxWidth??null,srcset:!0===t.srcset,visibleScopeNodeId:t.visibleScopeNodeId??null,status:"exported",byteLength:R.length,sha256:D,mimeType:L?null===k?null:"image/svg+xml":({PNG:"image/png",JPG:"image/jpeg",SVG:"image/svg+xml",PDF:"application/pdf"})[w],...!L&&null===k?{data:figma.base64Encode(R)}:{},text:k,errorCode:null,exportDiagnostics:d}}catch(e){return p(String(e.message||e).includes("DEVUP_ASSET_TIMEOUT:")?"DEVUP_ASSET_TIMEOUT":g||["fill","original-source"].includes(h)?"DEVUP_ORIGINAL_IMAGE_READ_FAILED":"DEVUP_ASSET_EXPORT_FAILED",u(e))}},bridgeContext:async function(e){let t=figma.currentPage,r=t.selection,n=e=>({id:e.id,name:String(e.name).slice(0,256),type:e.type});return{kind:"devup-bridge-context",currentPage:n(t),selection:r.slice(0,20).map(n),selectionCount:r.length,zoom:Number.isFinite(figma.viewport?.zoom)?figma.viewport.zoom:null,observedAt:Date.now(),lastKnownSelection:globalThis.__devupLastSelection??null,lastKnownPage:globalThis.__devupLastSelectionPage??null}},explore:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=await figma.getNodeByIdAsync(e.nodeId);if(!t)throw Error("DEVUP_NODE_NOT_FOUND");let r=e.explore,n=Math.max(20,Math.min(400,Number.isInteger(r.projectionLimit)?r.projectionLimit:200)),i=Math.max(0,Math.min(500,Number.isInteger(r.textPreviewLimit)?r.textPreviewLimit:160));function a(e){return"string"==typeof e?e.slice(0,240):""}let o="PAGE"===t.type,l=t,s="SECTION"===t.type?t:null;for(;!o&&l.parent&&"PAGE"!==l.parent.type;)l=l.parent,s||"SECTION"!==l.type||(s=l);let d="PAGE"===t.type?t:l.parent;if(!d||"PAGE"!==d.type)throw Error("DEVUP_PAGE_NOT_FOUND");function f(e){let t=e.absoluteBoundingBox||{x:"number"==typeof e.x?e.x:0,y:"number"==typeof e.y?e.y:0,width:"number"==typeof e.width?e.width:0,height:"number"==typeof e.height?e.height:0};return[t.x,t.y,t.width,t.height].every(Number.isFinite)?{x:t.x,y:t.y,width:t.width,height:t.height}:null}function c(e){let t=f(e);return t&&["FRAME","COMPONENT","INSTANCE","COMPONENT_SET"].includes(e.type)&&t.width>=240&&t.width<=1800&&t.height>=300&&t.height<=2e3&&t.width/Math.max(1,t.height)>=.25&&t.width/Math.max(1,t.height)<=2.5}function u(e){let t=[],r=e;for(;r&&"DOCUMENT"!==r.type&&t.length<12;){let e=a(r.name);e&&t.push(e),r=r.parent}return t.reverse()}await figma.setCurrentPageAsync(d);let p=o?d:!c(t)&&s?s:l,g=f(p)||f(t);if(!g)throw Error("DEVUP_NODE_BOUNDS_UNAVAILABLE");let h="children"in d?d.children:[],y=h.map((e,t)=>({node:e,pageChildIndex:t,bounds:f(e)})).filter(e=>e.bounds).filter(e=>{var t;return!!o||e.node.id===p.id||e.bounds.y>=g.y-240&&e.bounds.y<=g.y+12e3&&(t=e.bounds,Math.min(t.x+t.width,g.x+g.width)-Math.max(t.x,g.x)>0)}).sort((e,t)=>e.bounds.y-t.bounds.y||e.bounds.x-t.bounds.x||e.node.id.localeCompare(t.node.id)),m="SECTION"===p.type?y.filter(e=>e.node.id===p.id):y.slice(0,n),b=new Map(m.map(e=>[e.node.id,e]));b.has(l.id)||b.set(l.id,{node:l,pageChildIndex:-1,bounds:f(l)}),b.has(p.id)||b.set(p.id,{node:p,pageChildIndex:-1,bounds:f(p)}),b.has(t.id)||b.set(t.id,{node:t,pageChildIndex:-1,bounds:f(t)});let x=o?null:t.parent;for(;x&&"PAGE"!==x.type&&(b.has(x.id)||b.set(x.id,{node:x,pageChildIndex:-1,bounds:f(x)}),x.id!==p.id);)x=x.parent;if("children"in p)for(let e of p.children.slice(0,n))b.has(e.id)||b.set(e.id,{node:e,pageChildIndex:-1,bounds:f(e)});let E="SECTION"===p.type||o,A=E&&"children"in p&&p.children.length>n;if(E&&"children"in p){let e=8*n,t=p.children.filter(e=>!c(e)).map(e=>({node:e,ancestors:[]})),r=0;for(;t.length&&r!b.has(e.id));if(b.size+t.length>n+2){A=!0;continue}for(let e of t)b.set(e.id,{node:e,pageChildIndex:-1,bounds:f(e)});continue}if("children"in e)for(let r of e.children)o&&["FRAME","COMPONENT","INSTANCE","COMPONENT_SET"].includes(e.type)||t.push({node:r,ancestors:[...i,e]})}A||=t.length>0}let I=[...b.values()].filter(e=>e.bounds).slice(0,n+2).map(({node:e,pageChildIndex:t,bounds:r})=>{let n;return{id:e.id,type:e.type,fields:{name:a(e.name),parentId:e.parent&&"DOCUMENT"!==e.parent.type?e.parent.id:null,childrenIds:[],x:r.x,y:r.y,width:r.width,height:r.height,childCount:"children"in e?e.children.length:0,textPreview:"",...o||null===(n=function(e){let t=[e],r=0,n=null;for(;t.length&&r++<400;){let e=t.shift();if("visible"in e&&!1===e.visible)continue;let r="children"in e?e.children.filter(e=>!("visible"in e)||!1!==e.visible):[];if(/header|nav|gnb/i.test(e.name||"")&&r.length>=2&&"layoutMode"in e&&"HORIZONTAL"===e.layoutMode){let t=r.map(f);t.every(Boolean)&&(n=Math.max(n||0,t.reduce((e,t)=>e+t.width,0)+(Number(e.itemSpacing)||0)*(t.length-1)))}t.push(...r.slice(0,80))}return n}(e))?{}:{navigationContentWidth:n},pageChildIndex:t>=0?t:null,visible:!("visible"in e)||!1!==e.visible,breadcrumb:u(e)},extra:{},fieldErrors:{}}}),S=I.reduce((e,t)=>{let r=t.fields;return e?{x:Math.min(e.x,r.x),y:Math.min(e.y,r.y),right:Math.max(e.right,r.x+r.width),bottom:Math.max(e.bottom,r.y+r.height)}:{x:r.x,y:r.y,right:r.x+r.width,bottom:r.y+r.height}},null)||{x:0,y:0,right:0,bottom:0},w="SECTION"!==p.type&&y.length>m.length||b.size>n+2||A,v={id:d.id,type:d.type,fields:{name:a(d.name),parentId:null,childrenIds:[],x:S.x,y:S.y,width:S.right-S.x,height:S.bottom-S.y,childCount:h.length,textPreview:"",projectionTruncated:w,visible:!0,breadcrumb:u(d),pageChildIndex:null},extra:{},fieldErrors:{}},N={fileKey:figma.fileKey||"",version:null,rootIds:[d.id],nodes:[v,...I.filter(e=>e.id!==d.id)],diagnostics:[]},O=new Set([d.id,l.id,p.id,t.id]);for(;JSON.stringify(N).length>14e3;){let e=N.nodes.length-1;for(;e>=0&&O.has(N.nodes[e].id);)e-=1;if(e<0)break;N.nodes.splice(e,1),v.fields.projectionTruncated=!0}if(JSON.stringify(N).length>14e3&&(N.nodes=N.nodes.filter(e=>O.has(e.id)).map(e=>({...e,fields:{...e.fields,name:a(e.fields.name).slice(0,80),textPreview:"",breadcrumb:e.fields.breadcrumb.slice(-4).map(e=>e.slice(0,80)),projectionTruncated:e.id===d.id||e.fields.projectionTruncated}}))),JSON.stringify(N).length>14e3)throw Error("DEVUP_EXPLORE_PROJECTION_TOO_LARGE");function T(e){let t=0;for(let r of e){let e=r.codePointAt(0);t+=e<128?1:e<2048?2:e<65536?3:4}return t}if(o){for(;T(JSON.stringify(N))>19456&&N.nodes.length>1;)N.nodes.pop(),v.fields.projectionTruncated=!0;if(T(JSON.stringify(N))>19456)throw Error("DEVUP_EXPLORE_PROJECTION_TOO_LARGE")}let _=14e3-JSON.stringify(N).length;if(o&&(_=Math.min(_,19456-T(JSON.stringify(N)))),i>0&&_>0){let e=N.nodes.filter(e=>b.get(e.id)&&e.id!==d.id),t=e.length;for(let r of e){let e=Math.floor(_/t);if(t-=1,e<=0)continue;let n="",a=0;for(let t of function(e){if(0===i)return"";let t=[],r=[e],n=0;for(;r.length&&n<80&&t.join(" ").lengthe)break;n+=t,a+=r}r.fields.textPreview=n,_-=a}}return N},fastSnapshot:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=e.rootIds;if(!Array.isArray(t)||0===t.length)throw Error("DEVUP_ROOTS_INVALID");let r=await Promise.all(t.map(e=>figma.getNodeByIdAsync(e)));if(r.some(e=>!e))throw Error("DEVUP_NODE_NOT_FOUND");if(1===r.length&&"SECTION"===r[0].type)throw Error("DEVUP_TARGET_IS_SECTION");let n=["mobile","tablet","desktop"],i=e=>{let t=String(e.name||"").trim().toLowerCase(),r=n.indexOf(t);if(r>=0)return r;let i=t.match(/^(\d+)\s*[~–]\s*(\d+)$/);if(!i)return -1;let a=Number(i[1]),o=Number(i[2]);return Number.isSafeInteger(a)&&Number.isSafeInteger(o)&&a>0&&o>=a&&o<=0xffffffff?1:-1};if(1===r.length&&i(r[0])>=0){let e=r[0].parent;if(e&&"SECTION"===e.type&&"children"in e){let t=e.children.filter(e=>e.id===r[0].id||i(e)>=0).filter(e=>!1!==e.visible);t.length>1&&(r.length=0,r.push(...t))}}if(1===t.length){let e=[],t=new Set(r.map(e=>e.id)),n=[],i=e=>{if("reactions"in e&&Array.isArray(e.reactions)){for(let r of e.reactions)if(r&&r.trigger&&"AFTER_TIMEOUT"===r.trigger.type)for(let e of r.actions||[])e&&"NODE"===e.type&&e.transition&&"SMART_ANIMATE"===e.transition.type&&"string"==typeof e.destinationId&&!t.has(e.destinationId)&&(t.add(e.destinationId),n.push(e.destinationId));if("children"in e)for(let t of e.children)i(t)}};for(let e of r)i(e);for(;n.length>0;){let t=n.shift(),r=await figma.getNodeByIdAsync(t);r&&"DOCUMENT"!==r.type&&"PAGE"!==r.type&&(e.push(r),i(r))}r.push(...e)}let a=e.nodeId,o=["absoluteBoundingBox","absoluteRenderBounds","arcData","backgroundStyleId","blendMode","bottomLeftRadius","bottomRightRadius","boundVariables","characters","clipsContent","componentProperties","componentPropertyDefinitions","componentPropertyReferences","constraints","cornerRadius","counterAxisAlignItems","dashPattern","defaultVariant","effectStyleId","effects","fillStyleId","fills","fontName","fontSize","gridColumnAnchorIndex","gridColumnCount","gridColumnGap","gridColumnSizes","gridColumnSpan","gridRowAnchorIndex","gridRowCount","gridRowGap","gridRowSizes","gridRowSpan","gridStyleId","height","inferredAutoLayout","isAsset","isMask","itemSpacing","layoutGrow","layoutMode","layoutWrap","layoutPositioning","layoutSizingHorizontal","layoutSizingVertical","letterSpacing","lineHeight","maxHeight","maxLines","maxWidth","minHeight","minWidth","name","opacity","overflowDirection","paddingBottom","paddingLeft","paddingRight","paddingTop","primaryAxisAlignItems","reactions","rotation","strokeAlign","strokeBottomWeight","strokeLeftWeight","strokeRightWeight","strokeStyleId","strokeTopWeight","strokeWeight","strokes","targetAspectRatio","textAlignHorizontal","textAlignVertical","textAutoResize","textCase","textDecoration","textStyleId","textTruncation","topLeftRadius","topRightRadius","variantProperties","visible","width","x","y"],l=["fontName","fontWeight","fontSize","textDecoration","textCase","lineHeight","letterSpacing","fills","textStyleId","fillStyleId","listOptions","indentation","hyperlink"],s=e.snapshot,d=Math.max(0,Math.floor(Number(s.offset)||0)),f=Math.max(8192,Math.floor(Number(s.maxEnvelopeBytes)||19456)),c=Math.min(s.maxEnvelopeBytes?f-1024:18e3,Math.max(4096,Math.floor(Number(s.maxPayloadBytes)||15e3))),u=new Set(["backgroundStyleId","effectStyleId","fillStyleId","gridStyleId","strokeStyleId","textStyleId"]),p=new Set(["maxWidth","maxHeight","absoluteRenderBounds"]),g=new Map([["rotation",0],["cornerRadius",0],["isAsset",!1],["isMask",!1],["clipsContent",!1],["blendMode","PASS_THROUGH"],["strokeAlign","INSIDE"],["textCase","ORIGINAL"],["textDecoration","NONE"],["textAlignHorizontal","LEFT"],["textAlignVertical","TOP"],["counterAxisAlignItems","MIN"],["primaryAxisAlignItems","MIN"],["gridColumnCount",0],["gridRowCount",0],["gridColumnGap",0],["gridRowGap",0],["gridColumnAnchorIndex",-1],["gridRowAnchorIndex",-1],["gridColumnSpan",1],["gridRowSpan",1]]),h=new Set(["start","end","characters","fontWeight","textStyleId","fillStyleId","listOptions","indentation","hyperlink"]),y=new Set(["parent","children","consumers"]);function m(e,t=!1,r=new WeakSet,n=0){let i;if(null===e||["string","number","boolean"].includes(typeof e))return e;if(void 0===e)return{$undefined:!0};if("bigint"==typeof e)return{$bigint:e.toString()};if(["function","symbol"].includes(typeof e))return{$unsupported:typeof e};if(n>12)return{$truncated:"max-depth"};if("object"==typeof e&&"parent"in e&&"string"==typeof e.id&&"string"==typeof e.type)return{$nodeId:e.id,$nodeType:e.type};if(Array.isArray(e))return e.map(e=>m(e,t,r,n+1));if(ArrayBuffer.isView(e))return{$binary:e.constructor.name,byteLength:e.byteLength};if(e instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:e.byteLength};if(r.has(e))return{$circular:!0};if(r.add(e),t){let t=new Set(Object.keys(e)),r=e;for(;r&&r!==Object.prototype;){for(let e of Object.getOwnPropertyNames(r))t.add(e);r=Object.getPrototypeOf(r)}i=[...t].sort().filter(e=>!e.startsWith("_")&&!y.has(e))}else i=Object.keys(e).sort();let a={};for(let o of i)try{let i=m(e[o],t,r,n+1);i&&"function"===i.$unsupported||(a[o]=i)}catch(e){a[o]=t?{$error:"unavailable"}:{$error:String(e&&e.message?e.message:e)}}return r.delete(e),a}let b=new Map;async function x(e,t,r){if("INSTANCE"!==e.type||"function"!=typeof e.getMainComponentAsync)return;b.has(e.id)||b.set(e.id,(async()=>{try{let t=await e.getMainComponentAsync();return{fields:{mainComponentId:t?t.id:null,mainComponentName:t?t.name:null,mainComponentSetId:t&&t.parent&&"COMPONENT_SET"===t.parent.type?t.parent.id:null}}}catch(e){return{error:String(e&&e.message?e.message:e)}}})());let n=await b.get(e.id);n.error?r.mainComponentId=n.error:Object.assign(t,n.fields)}async function E(e){let t={},r={};e.parent&&(t.parentId=e.parent.id),e.parent&&("PAGE"===e.parent.type||"SECTION"===e.parent.type||"COMPONENT_SET"===e.parent.type)&&(t.parentType=e.parent.type,"SECTION"===e.parent.type&&(t.parentName=e.parent.name));let n="children"in e?e.children.map(e=>e.id):[];for(let i of(n.length>0&&(t.childrenIds=n),o)){let n;if("componentPropertyDefinitions"!==i||"COMPONENT"!==e.type||!e.parent||"COMPONENT_SET"!==e.parent.type)try{if(!(i in e)){"overflowDirection"===i&&["FRAME","COMPONENT","INSTANCE","COMPONENT_SET"].includes(e.type)&&(t[i]=null);continue}if(n=e[i],"function"==typeof n)continue;let r=m(n);if("overflowDirection"===i&&null==n){t[i]=null;continue}(null===r?!p.has(i):Array.isArray(r)?0===r.length:"object"==typeof r?0===Object.keys(r).length:!!(""===r&&u.has(i))||g.has(i)&&g.get(i)===r)||(t[i]=r)}catch(e){r[i]=String(e&&e.message?e.message:e)}}if("TEXT"===e.type&&"function"==typeof e.getStyledTextSegments)try{let r=m(e.getStyledTextSegments(l));if(1===r.length){let e=r[0];for(let t of Object.keys(e))h.has(t)||delete e[t]}r.length>0&&(t.styledTextSegments=r)}catch(e){r.styledTextSegments=String(e&&e.message?e.message:e)}await x(e,t,r);let i={id:e.id,type:e.type,fields:t};return Object.keys(r).length>0&&(i.fieldErrors=r),i}let A=[],I=[...r],S=new Set;for(let e=0;e=A.length&&A.length>0)throw Error("DEVUP_SNAPSHOT_RANGE_INVALID");function w(e){let t=0;for(let r=0;r=55296&&n<=56319&&r+1({id:e,styleType:t})).sort((e,t)=>e.id.localeCompare(t.id));async function a(e){try{let t=await figma.variables.getVariableByIdAsync(e);return t?{kind:"variable",value:m(t,!0),collectionId:t.variableCollectionId,aliasIds:Object.values(t.valuesByMode||{}).filter(e=>e&&"VARIABLE_ALIAS"===e.type&&"string"==typeof e.id&&e.id).map(e=>e.id)}:{kind:"unresolved",value:{id:e,kind:"variable",reason:"notFoundOrUnavailable"}}}catch(t){return{kind:"unresolved",value:{id:e,kind:"variable",reason:"notFoundOrUnavailable",error:String(t&&t.message||t).slice(0,300)}}}}let o=await Promise.all([...n.map(a),...i.map(async({id:e,styleType:t})=>{try{let r=await figma.getStyleByIdAsync(e);if(!r)return{kind:"unresolved",value:{id:e,kind:"style",reason:"notFoundOrUnavailable"}};return{kind:"style",value:{...m(r,!0),styleType:t,value:m("PAINT"===t?r.paints:"EFFECT"===t?r.effects:"GRID"===t?r.layoutGrids:r,!0)}}}catch(t){return{kind:"unresolved",value:{id:e,kind:"style",reason:"notFoundOrUnavailable",error:String(t&&t.message||t).slice(0,300)}}}})]),l=new Set(n),s=o.flatMap(e=>e.aliasIds||[]);for(;s.length>0;){let e=[...new Set(s)].filter(e=>!l.has(e)).sort();for(let t of e)l.add(t);let t=await Promise.all(e.map(a));o.push(...t),s=t.flatMap(e=>e.aliasIds||[])}let d=[...new Set(o.filter(e=>"variable"===e.kind&&e.collectionId).map(e=>e.collectionId))].sort(),f=(await Promise.all(d.map(async e=>{try{let t=await figma.variables.getVariableCollectionByIdAsync(e);return t?m(t,!0):null}catch(e){return null}}))).filter(e=>null!==e),c=o.filter(e=>"variable"===e.kind).map(e=>e.value),u=o.filter(e=>"style"===e.kind).map(e=>e.value),p=o.filter(e=>"unresolved"===e.kind).map(e=>e.value);return{collections:f,variables:c,styles:u,usedRemoteVariables:c.filter(e=>!0===e.remote),usedVariableIds:n,usedStyleIds:i.map(e=>e.id),localComplete:!1,usedRemoteComplete:0===p.length,unresolved:p,$variableRefCount:n.length,$styleRefCount:i.length}}async function N(e){let t=[],r=2;for(let n=d;ne)break;t.push(i),r+=a}return{pageNodes:t,packedBytes:r}}let O=c-1024,T=null;for(let e=0;e<5;e+=1){let{pageNodes:e,packedBytes:t}=await N(O);if(0===e.length)throw Error("DEVUP_SNAPSHOT_RANGE_INVALID");let n=function(e,t){let n=Math.min(A.length,d+e.length),{$variableRefCount:i,$styleRefCount:o,...l}=t,s=[...e,{id:"__DEVUP_SNAPSHOT_CURSOR__",type:"DEVUP_INTERNAL",fields:{offset:d,nextOffset:n,complete:n>=A.length,totalNodes:A.length},extra:{},fieldErrors:{}}],f={kind:"devupFastSnapshotEnvelope",schemaVersion:1,source:{fileKey:figma.fileKey||"",rootId:a},snapshot:{fileKey:figma.fileKey||"",version:null,rootIds:r.map(e=>e.id),nodes:s,diagnostics:[]},resources:l,integrity:{nodeCount:s.length,variableRefCount:i,styleRefCount:o,utf8Bytes:0}},c=0;for(let e=0;e<8&&(c=w(JSON.stringify(f)),f.integrity.utf8Bytes!==c);e+=1)f.integrity.utf8Bytes=c;if(f.integrity.utf8Bytes!==w(JSON.stringify(f)))throw Error("DEVUP_ENVELOPE_LENGTH_UNSTABLE");return{envelope:f,bytes:c}}(e,await v(e));if(n.bytes<=f){T=n;break}if(1===e.length)throw Error("DEVUP_ENVELOPE_TOO_LARGE");O=Math.max(1,Math.min(O-1,t-(n.bytes-f)-256))}if(!T)throw Error("DEVUP_ENVELOPE_TOO_LARGE");return T.envelope},fastTheme:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=Math.max(0,Math.floor(Number(e.theme.offset)||0));function r(e,t=new WeakSet,n=0){if(null===e||["string","number","boolean"].includes(typeof e))return e;if(void 0===e)return{$undefined:!0};if("bigint"==typeof e)return{$bigint:e.toString()};if(["function","symbol"].includes(typeof e))return{$unsupported:typeof e};if(n>12)return{$truncated:"max-depth"};if("object"==typeof e&&"parent"in e&&"string"==typeof e.id&&"string"==typeof e.type)return{$nodeId:e.id,$nodeType:e.type};if(Array.isArray(e))return e.map(e=>r(e,t,n+1));if(ArrayBuffer.isView(e))return{$binary:e.constructor.name,byteLength:e.byteLength};if(e instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:e.byteLength};if(t.has(e))return{$circular:!0};t.add(e);let i={};for(let a of function(e){let t=new Set(Object.keys(e)),r=e;for(;r&&r!==Object.prototype;){for(let e of Object.getOwnPropertyNames(r))t.add(e);r=Object.getPrototypeOf(r)}return[...t].sort()}(e))if(!(a.startsWith("_")||["parent","children","consumers"].includes(a)))try{let o=r(e[a],t,n+1);o&&"function"===o.$unsupported||(i[a]=o)}catch(e){i[a]={$error:"unavailable"}}return t.delete(e),i}let n=new Set,i=new Map;for(let e of figma.root.children)await e.loadAsync();for(let e of[figma.root,...figma.root.findAll(()=>!0)])for(let t of["boundVariables","fills","strokes","effects","layoutGrids","textStyleId","fillStyleId","strokeStyleId","backgroundStyleId","effectStyleId","gridStyleId"])try{!function e(t,r="",a=new WeakSet,o=0){if(!(o>16)&&null!==t&&"object"==typeof t&&!a.has(t)){if(a.add(t),Array.isArray(t)){for(let n of t)e(n,r,a,o+1);return}for(let[l,s]of("VARIABLE_ALIAS"===t.type&&"string"==typeof t.id&&t.id&&"figma.mixed"!==t.id&&"MIXED"!==t.id&&n.add(t.id),Object.entries(t))){let t="textStyleId"===l?"TEXT":["fillStyleId","strokeStyleId","backgroundStyleId"].includes(l)?"PAINT":"effectStyleId"===l?"EFFECT":"gridStyleId"===l?"GRID":null;t&&"string"==typeof s&&s&&"figma.mixed"!==s&&"MIXED"!==s&&!i.has(s)&&i.set(s,t),e(s,l||r,a,o+1)}}}({[t]:e[t]})}catch(e){}let[a,o,l,s,d,f]=await Promise.all([figma.variables.getLocalVariableCollectionsAsync(),figma.variables.getLocalVariablesAsync(),figma.getLocalPaintStylesAsync(),figma.getLocalTextStylesAsync(),figma.getLocalEffectStylesAsync(),figma.getLocalGridStylesAsync()]),c=new Set(o.map(e=>e.id)),u=new Set([...l,...s,...d,...f].map(e=>e.id)),p=[];async function g(e){try{return await figma.variables.getVariableByIdAsync(e)||null}catch(t){return p.push({id:e,kind:"variable",reason:"notFoundOrUnavailable",error:String(t&&t.message||t).slice(0,300)}),null}}function h(e){return Object.values(e.valuesByMode||{}).filter(e=>e&&"VARIABLE_ALIAS"===e.type&&"string"==typeof e.id&&e.id).map(e=>e.id)}let y=[...i.entries()].filter(([e])=>!u.has(e)).sort(([e],[t])=>e.localeCompare(t)).map(async([e,t])=>{try{let r=await figma.getStyleByIdAsync(e);return r?{style:r,styleType:t}:null}catch(t){return p.push({id:e,kind:"style",reason:"notFoundOrUnavailable",error:String(t&&t.message||t).slice(0,300)}),null}}),m=new Set,b=[],x=[...n,...o.flatMap(h)];for(;x.length>0;){let e=[...new Set(x)].filter(e=>!c.has(e)&&!m.has(e)).sort();for(let t of e)m.add(t);let t=(await Promise.all(e.map(g))).filter(Boolean);b.push(...t),x=t.flatMap(h)}let E=(await Promise.all(y)).filter(Boolean);for(let e of m)b.some(t=>t.id===e)||p.some(t=>t.id===e)||p.push({id:e,kind:"variable",reason:"notFoundOrUnavailable"});for(let[e]of[...i.entries()].filter(([e])=>!u.has(e)))E.some(t=>t.style.id===e)||p.some(t=>t.id===e)||p.push({id:e,kind:"style",reason:"notFoundOrUnavailable"});let A=[...new Set(b.map(e=>e.variableCollectionId))].filter(e=>!a.some(t=>t.id===e)).sort(),I=(await Promise.all(A.map(async e=>{try{return await figma.variables.getVariableCollectionByIdAsync(e)}catch(e){return null}}))).filter(Boolean);function S(e,t){return{...r(e),styleType:t,value:r("PAINT"===t?e.paints:"EFFECT"===t?e.effects:"GRID"===t?e.layoutGrids:e)}}let w=[...l.map(e=>S(e,"PAINT")),...s.map(e=>S(e,"TEXT")),...d.map(e=>S(e,"EFFECT")),...f.map(e=>S(e,"GRID")),...E.map(({style:e,styleType:t})=>S(e,t))].sort((e,t)=>e.id.localeCompare(t.id)),v=[...o,...b].map(e=>r(e)).sort((e,t)=>e.id.localeCompare(t.id)),N=[...a,...I].map(e=>r(e)).sort((e,t)=>e.id.localeCompare(t.id));function O(e){let t=[];for(let r=0;r=55296&&n<=56319){let t=r+1=56320&&t<=57343?(n=65536+(n-55296<<10)+(t-56320),r+=1):n=65533}else n>=56320&&n<=57343&&(n=65533);n<128?t.push(n):n<2048?t.push(192|n>>6,128|63&n):n<65536?t.push(224|n>>12,128|n>>6&63,128|63&n):t.push(240|n>>18,128|n>>12&63,128|n>>6&63,128|63&n)}return new Uint8Array(t)}p.sort((e,t)=>e.kind.localeCompare(t.kind)||e.id.localeCompare(t.id));let T=[...N.map(e=>({kind:"collection",value:e})),...v.map(e=>({kind:"variable",value:e})),...w.map(e=>({kind:"style",value:e})),...[...n].sort().map(e=>({kind:"usedVariableId",value:e})),...[...i.keys()].sort().map(e=>({kind:"usedStyleId",value:e})),...p.map(e=>({kind:"unresolved",value:e}))];if(t>T.length)throw Error("DEVUP_SNAPSHOT_RANGE_INVALID");let _=T.map(e=>O(JSON.stringify(e.value)).length+1),P=18432,M=null;for(let e=0;e<6;e+=1){let{pageItems:e,packed:r}=function(e){let r=[],n=0;for(let i=t;i0)||!(n+_[i]>e));i+=1)r.push(T[i]),n+=_[i];return{pageItems:r,packed:n}}(P),n=function(e,r){let n=t=>e.filter(e=>e.kind===t).map(e=>e.value),i=n("collection"),a=n("variable"),o=n("style"),l=n("unresolved"),s={kind:"devupFastThemeEnvelope",schemaVersion:1,source:{fileKey:figma.fileKey||"",version:null},resources:{collections:i,variables:a,styles:o,usedRemoteVariables:[],usedVariableIds:n("usedVariableId"),usedStyleIds:n("usedStyleId"),localComplete:!0,usedRemoteComplete:0===p.length,unresolved:l},page:{offset:t,nextOffset:r,complete:r>=T.length,totalItems:T.length},integrity:{collectionCount:i.length,variableCount:a.length,styleCount:o.length,unresolvedCount:l.length,utf8Bytes:0}},d=new Uint8Array;for(let e=0;e<8&&(d=O(JSON.stringify(s)),s.integrity.utf8Bytes!==d.length);e+=1)s.integrity.utf8Bytes=d.length;if(d=O(JSON.stringify(s)),s.integrity.utf8Bytes!==d.length)throw Error("DEVUP_ENVELOPE_LENGTH_UNSTABLE");return{envelope:s,bytes:d.length}}(e,t+e.length);if(n.bytes<=19456){M=n;break}if(e.length<=1)break;P=Math.max(1,Math.min(P-1,r-(n.bytes-19456)-256))}if(null===M||M.bytes>8388608)throw Error("DEVUP_ENVELOPE_TOO_LARGE");return M.envelope},largeValue:async function(e){let t;figma.skipInvisibleInstanceChildren=!1;let r=e.largeValue;async function n(e){try{let t=await figma.getNodeByIdAsync(e);if(t)return t}catch(r){if(!e.startsWith("I")||!e.includes(";"))throw r;try{return await t()}catch(e){throw Error(`${r.message||r}; nested lookup fallback: ${e.message||e}`)}}if(!e.startsWith("I")||!e.includes(";"))return null;return t();async function t(){let t=await figma.getNodeByIdAsync(e.slice(1).split(";")[0]),r=t?[t]:[],n=new Set;for(;r.length;){let t=r.pop();if(t.id===e)return t;if(!n.has(t.id)){if(n.add(t.id),n.size>1e4)throw Error("nested lookup exceeds 10000 nodes");Array.isArray(t.children)&&r.push(...t.children)}}throw Error(`nested node ${e} not found in its instance`)}}async function i(e,t,r=85e3,n=()=>{}){let a,o=Date.now();try{let i=await Promise.race([e.exportAsync(t),new Promise((e,t)=>{a=setTimeout(()=>t(Error(`DEVUP_ASSET_TIMEOUT: native export exceeded ${r} ms`)),r)})]);return n("native-export",Date.now()-o),i}catch(i){if(n("native-export",Date.now()-o,String(i.message||i).slice(0,500)),"SVG_STRING"!==t.format)throw i;let r=Date.now();try{let t=function(e){let t=e=>"number"==typeof e&&Number.isFinite(e),r=(e,t)=>{throw Error(`geometry fallback unsupported: ${e.type||"unknown"} ${e.id||""}: ${t}`)},n=0,i=0;function a(e,i,a){(void 0!==a&&!Array.isArray(a)||void 0===a&&Array.isArray(i)&&i.length)&&r(e,"unresolved paint requires native export");let o=Array.isArray(a)?a.filter(e=>!1!==e.visible):[];return o.length?(Array.isArray(i)&&i.length||r(e,"paint has no rendered geometry"),o.map(a=>{("SOLID"!==a.type||a.blendMode&&"NORMAL"!==a.blendMode)&&r(e,"paint type or blending requires native export");let o=a.color;o&&[o.r,o.g,o.b].every(e=>t(e)&&e>=0&&e<=1)||r(e,"invalid solid color");let l=void 0===a.opacity?1:a.opacity;return(!t(l)||l<0||l>1)&&r(e,"invalid paint opacity"),["NONZERO","EVENODD"].map(t=>{let a=i.filter(e=>e.windingRule===t);if(!a.length)return"";for(let t of a)"string"==typeof t.data&&t.data.trim()||r(e,"empty path"),n++;return`"']/g,e=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[e])}" fill="rgb(${100*o.r}%,${100*o.g}%,${100*o.b}%)" fill-opacity="${l}" fill-rule="${"EVENODD"===t?"evenodd":"nonzero"}"/>`}).join("")}).join("")):""}let o=e.absoluteRenderBounds,l=e.absoluteTransform;(!o||![o.x,o.y,o.width,o.height].every(t)||o.width<=0||o.height<=0)&&r(e,"missing render bounds"),(!Array.isArray(l)||2!==l.length||!l.every(e=>Array.isArray(e)&&3===e.length&&e.every(t))||0!==l[0][1]||0!==l[1][0]||l[0][0]<=0||l[1][1]<=0)&&r(e,"root rotation, reflection or shear requires native export");let s=(o.x-l[0][2])/l[0][0],d=(o.y-l[1][2])/l[1][1],f=o.width/l[0][0],c=o.height/l[1][1],u=function e(n,o){if(++i>1e4&&r(n,"subtree exceeds 10000 nodes"),n.isMask&&r(n,"mask requires native export"),!1===n.visible||0===n.opacity)return"";n.clipsContent&&r(n,"clipping requires native export"),Array.isArray(n.effects)&&n.effects.some(e=>!1!==e.visible)&&r(n,"effect requires native export"),n.blendMode&&!["NORMAL","PASS_THROUGH"].includes(n.blendMode)&&r(n,"layer blending requires native export");let l=void 0===n.opacity?1:n.opacity;(!t(l)||l<0||l>1)&&r(n,"invalid layer opacity");let s="";if(!o){let e=n.relativeTransform;Array.isArray(e)&&2===e.length&&e.every(e=>Array.isArray(e)&&3===e.length&&e.every(t))||r(n,"missing relative transform"),s=` transform="matrix(${e[0][0]} ${e[1][0]} ${e[0][1]} ${e[1][1]} ${e[0][2]} ${e[1][2]})"`}let d="";if(["GROUP","FRAME","INSTANCE","COMPONENT"].includes(n.type))(Array.isArray(n.fills)&&n.fills.some(e=>!1!==e.visible)||Array.isArray(n.strokes)&&n.strokes.some(e=>!1!==e.visible))&&r(n,"container paint requires native export"),Array.isArray(n.children)||r(n,"missing children"),d=(!0===n.itemReverseZIndex?[...n.children].reverse():n.children).map(t=>e(t,!1)).join("");else{for(let e of(["VECTOR","BOOLEAN_OPERATION","RECTANGLE","ELLIPSE","POLYGON","STAR","LINE"].includes(n.type)||r(n,"node type requires native export"),Array.isArray(n.strokes)&&n.strokes.some(e=>!1!==e.visible)&&"CENTER"!==n.strokeAlign&&r(n,"non-center stroke alignment requires native export"),Array.isArray(n.dashPattern)&&n.dashPattern.length&&r(n,"dashed stroke requires native export"),[n.fillGeometry,n.strokeGeometry]))void 0!==e&&(!Array.isArray(e)||e.some(e=>!["NONZERO","EVENODD"].includes(e.windingRule)))&&r(n,"unknown winding rule");d=a(n,n.fillGeometry,n.fills)+a(n,n.strokeGeometry,n.strokes)}return`${d}`}(e,!0);return n||r(e,"no painted geometry"),`${u}`}(e);return n("geometry-fallback",Date.now()-r),t}catch(e){throw Error(`native export: ${i.message||i}; ${e.message||e}`)}}finally{clearTimeout(a)}}let a=await n(r.nodeId);if(!a)throw Error("DEVUP_NODE_NOT_FOUND");function o(e){let t=[];for(let r=0;r=55296&&n<=56319){let t=r+1=56320&&t<=57343?(n=65536+(n-55296<<10)+(t-56320),r+=1):n=65533}else n>=56320&&n<=57343&&(n=65533);n<128?t.push(n):n<2048?t.push(192|n>>6,128|63&n):n<65536?t.push(224|n>>12,128|n>>6&63,128|63&n):t.push(240|n>>18,128|n>>12&63,128|n>>6&63,128|63&n)}return new Uint8Array(t)}let l=null;try{if("$export:svg"===r.field){if("function"!=typeof a.exportAsync)throw Error("unsupported");let e=await i(a,{format:"SVG_STRING"},Math.max(1,(Number(r.timeoutMs)||9e4)-5e3));if("string"!=typeof e)throw Error("unsupported");l=o(e)}else if("string"==typeof r.field&&r.field.startsWith("$export:png")){if("function"!=typeof a.exportAsync)throw Error("unsupported");let e=Number(r.field.split("@")[1]||2);if(!Number.isFinite(e)||e<=0||e>4)throw Error("invalid scale");let t=r.field.split("@"),n=a;if("visible"===t[2]){let e=new Set;for(;n&&n.id!==t[3]&&!e.has(n)&&e.size<64;)e.add(n),n=n.parent;if(!n||n.id!==t[3]||["PAGE","DOCUMENT"].includes(n.type))throw Error("invalid visible scope")}else if(t.length>2)throw Error("invalid export field");let i=await n.exportAsync({format:"PNG",contentsOnly:!0,useAbsoluteBounds:!1,constraint:{type:"SCALE",value:e}});l=i instanceof Uint8Array?i:new Uint8Array(i)}else if("styledTextSegments"===r.field&&"TEXT"===a.type&&"function"==typeof a.getStyledTextSegments)t=a.getStyledTextSegments(["fontName","fontWeight","fontSize","textDecoration","textCase","lineHeight","letterSpacing","fills","textStyleId","fillStyleId","listOptions","indentation","hyperlink"]);else if(r.field in a)t=a[r.field];else throw Error("unsupported")}catch(e){return{kind:"devupLargeValueUnsupported",fileKey:figma.fileKey||"",version:r.version,nodeId:r.nodeId,field:r.field,byteLength:r.byteLength,sha256:r.sha256,errorCode:"DEVUP_FIELD_UNSUPPORTED_BY_UPSTREAM",errorDetail:`${r.field} failed: ${e.message||e}`.slice(0,500)}}let s=null===l?o(JSON.stringify(function e(t,r=new WeakSet,n=0){if(null===t||["string","number","boolean"].includes(typeof t))return t;if(void 0===t)return{$undefined:!0};if("bigint"==typeof t)return{$bigint:t.toString()};if(["function","symbol"].includes(typeof t))return{$unsupported:typeof t};if(n>12)return{$truncated:"max-depth"};if("object"==typeof t&&"parent"in t&&"string"==typeof t.id&&"string"==typeof t.type)return{$nodeId:t.id,$nodeType:t.type};if(Array.isArray(t))return t.map(t=>e(t,r,n+1));if(ArrayBuffer.isView(t))return{$binary:t.constructor.name,byteLength:t.byteLength};if(t instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:t.byteLength};if(r.has(t))return{$circular:!0};r.add(t);let i={};for(let a of Object.keys(t).sort())try{let o=e(t[a],r,n+1);o&&"function"===o.$unsupported||(i[a]=o)}catch(e){i[a]={$error:String(e&&e.message?e.message:e)}}return r.delete(t),i}(t))):l,d=function(e){let t=[0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,0xe49b69c1,0xefbe4786,0xfc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x6ca6351,0x14292967,0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2],r=e.length,n=64*Math.ceil((r+9)/64),i=new Uint8Array(n);i.set(e),i[r]=128;let a=8*r;for(let e=0;e<8;e+=1)i[n-1-e]=255&Math.floor(a/2**(8*e));let o=[0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19],l=(e,t)=>e>>>t|e<<32-t;for(let e=0;e>>3,n=l(r[e-2],17)^l(r[e-2],19)^r[e-2]>>>10;r[e]=r[e-16]+t+r[e-7]+n>>>0}let[n,a,s,d,f,c,u,p]=o;for(let e=0;e<64;e+=1){let i=p+(l(f,6)^l(f,11)^l(f,25))+(f&c^~f&u)+t[e]+r[e]>>>0,o=(l(n,2)^l(n,13)^l(n,22))+(n&a^n&s^a&s)>>>0;p=u,u=c,c=f,f=d+i>>>0,d=s,s=a,a=n,n=i+o>>>0}for(let[e,t]of[n,a,s,d,f,c,u,p].entries())o[e]=o[e]+t>>>0}return o.map(e=>e.toString(16).padStart(8,"0")).join("")}(s);if(s.length!==r.byteLength||d!==r.sha256)throw Error("DEVUP_LARGE_VALUE_CHANGED");let f=Math.max(0,Math.floor(Number(r.offset)||0)),c=Math.min(65536,Math.max(1,Math.floor(Number(r.maxChunkBytes)||8192)));if(f>=s.length)throw Error("DEVUP_LARGE_VALUE_RANGE_INVALID");let u=Math.min(s.length,f+c);return{kind:"devupLargeValueFragment",fileKey:figma.fileKey||"",version:r.version,nodeId:r.nodeId,field:r.field,offset:f,nextOffset:u,byteLength:s.length,sha256:d,dataBase64:function(e){let t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",r="";for(let n=0;n>2],r+=t[(3&i)<<4|a>>4],r+=n+1>6]:"=",r+=n+210||t.some(e=>"string"!=typeof e||e.length>128||!/^(?:[0-9]+:[0-9]+|I[0-9]+:[0-9]+(?:;[0-9]+:[0-9]+)+)$/.test(e))||new Set(t).size!==t.length)throw Error("DEVUP_INVALID_INPUT: nodeIds must be 1..10 distinct Figma IDs");let r=(e,t)=>{try{let r=e[t];return"number"==typeof r&&Number.isFinite(r)?r:null}catch{return null}},n=(e,t)=>{try{let r=e[t];return"string"==typeof r&&r.length<=32&&/^[A-Z_]+$/.test(r)?r:null}catch{return null}},i=[];for(let e of t)try{let t=await figma.getNodeByIdAsync(e);if(!t||"DOCUMENT"===t.type||"PAGE"===t.type){i.push({nodeId:e,status:"unavailable",reason:t?"not-scene-node":"not-found"});continue}let a={width:r(t,"width"),height:r(t,"height"),padding:{top:r(t,"paddingTop"),right:r(t,"paddingRight"),bottom:r(t,"paddingBottom"),left:r(t,"paddingLeft")},gap:r(t,"itemSpacing"),layoutMode:n(t,"layoutMode"),align:{primary:n(t,"primaryAxisAlignItems"),counter:n(t,"counterAxisAlignItems")}},o=null!==a.width&&null!==a.height;i.push({nodeId:e,status:o?"measured":"partial",...a})}catch{i.push({nodeId:e,status:"unavailable",reason:"read-failed"})}return{kind:"devup-measure",schemaVersion:1,coordinateSpace:"figma-logical",nodes:i,partial:i.some(e=>"measured"!==e.status),evidenceLimit:"Null means unavailable or not applicable; no screenshot dimensions, descendants, theme, assets or code were read."}},metadata:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=await figma.getNodeByIdAsync(e.nodeId);if(!t)throw Error("DEVUP_NODE_NOT_FOUND");let r=t;for(;r&&"PAGE"!==r.type;)r=r.parent;r&&"PAGE"===r.type&&await figma.setCurrentPageAsync(r);let n=[];return!function(e){let t=[{node:e,visited:!1}],r=new Map;for(;t.length>0;){let e=t[t.length-1],i=e.node,a="children"in i?i.children:[];if(!e.visited){e.visited=!0;for(let e=a.length-1;e>=0;e-=1)t.push({node:a[e],visited:!1});continue}t.pop();let o=0,l=[];for(let e of a)l.push(e.id),o+=1+(r.get(e.id)||0);r.set(i.id,o),n.push({id:i.id,type:i.type,name:i.name,childrenIds:l,descendantCount:o})}}(t),{fileKey:figma.fileKey||"",version:null,rootId:t.id,nodes:n}},pageCatalog:async function(e){let t=figma.root.children;return{fileKey:figma.fileKey||"",version:null,rootIds:t.map(e=>e.id),nodes:t.map(e=>({id:e.id,type:e.type,fields:{name:e.name,parentId:null,childrenIds:[]},extra:{},fieldErrors:{}})),diagnostics:[]}},referencePng:async function(e){let t,r=e.nodeId;globalThis.__devupReferencePngAnswers||(globalThis.__devupReferencePngAnswers=new Map);let n=globalThis.__devupReferencePngAnswers,i=0;for(let[e,t]of n)Date.now()-t.at>12e4?n.delete(e):i+=t.text.length;if(i>=0xc000000)throw Error(`DEVUP_REFERENCE_PNG_BUSY ${i} characters are held for other reads`);let a=await figma.getNodeByIdAsync(r);if(!a||"function"!=typeof a.exportAsync)throw Error(`DEVUP_NODE_NOT_FOUND ${r}`);let o=a.absoluteRenderBounds||a.absoluteBoundingBox,l=Math.max(o.width,o.height),s=l>8192?8192/l:1,d=(e,t)=>({format:"PNG",constraint:t&&s<1?{type:o.width>=o.height?"WIDTH":"HEIGHT",value:8192}:{type:"SCALE",value:s},contentsOnly:e,useAbsoluteBounds:!1,colorProfile:"SRGB"}),f=e=>(Array.isArray(e)?e:[]).filter(e=>e&&!1!==e.visible),c=(e,t)=>!!e&&!!t&&e.x!!e&&e.x+t<=o.x&&e.y+t<=o.y&&e.x+e.width-t>=o.x+o.width&&e.y+e.height-t>=o.y+o.height,p=e=>`${e.type} ${JSON.stringify(e.name)}`,g=e=>JSON.parse(JSON.stringify(e===figma.mixed?null:e)),h=e=>(void 0===e.opacity||1===e.opacity)&&(!e.blendMode||["PASS_THROUGH","NORMAL"].includes(e.blendMode)),y=[],m=a.parent;for(;m&&"PAGE"!==m.type;)y.unshift(m),m=m.parent;let b=[255,255,255];for(let e of f(m&&m.backgrounds)){if("SOLID"!==e.type)continue;let t=void 0===e.opacity?1:e.opacity;["r","g","b"].forEach((r,n)=>{b[n]=t*e.color[r]*255+(1-t)*b[n]})}let x=0,E=e=>!1!==e.visible&&!e.isMask&&!!c(e.absoluteRenderBounds,o)&&((x+=1)>5e3||"TEXT"===e.type||!!f(e.fills).length||!!f(e.strokes).length||!!f(e.effects).length||"children"in e&&e.children.some(E)),A=e=>h(e)&&u(e.absoluteBoundingBox,0)&&1e-9>Math.abs(e.absoluteTransform[0][1])&&1e-9>Math.abs(e.absoluteTransform[1][0])&&0===Math.max(0,...[e.topLeftRadius,e.topRightRadius,e.bottomRightRadius,e.bottomLeftRadius].filter(e=>"number"==typeof e))&&f(e.fills).some(e=>"NORMAL"===(e.blendMode||"NORMAL")&&(void 0===e.opacity||1===e.opacity)&&("SOLID"===e.type||e.type.startsWith("GRADIENT_")&&e.gradientStops.every(e=>1===e.color.a))),I=[...y,a],S=[],w=[],v=new Map;for(let e=0;eA(e)&&i.slice(0,t).every(h));r.forEach((e,r)=>{e===t||(e.isMask&&!1!==e.visible&&r{let t=await a.exportAsync(d(!1,!0));return{kind:"devupReferencePng",nodeId:r,mimeType:"image/png",byteLength:t.length,data:figma.base64Encode(t),backdrop:N,...e}},T=(e,t)=>{let r=[...new Set(e)];return{approximate:r.join("; "),note:`Drawn as the canvas shows this area: devup-mcp cannot yet compose what get_screenshot draws here (${r.slice(0,3).join("; ")}). Layers reaching into it may show here: ${t}; get_screenshot leaves other layers out.`}},_=e=>{let t=JSON.stringify(e).replace(/[\u0080-\uffff]/g,e=>`\\u${e.charCodeAt(0).toString(16).padStart(4,"0")}`);if(t.length>0x5a00000){if(e.compose){let{compose:r,reaching:n,...i}=e;return _({...i,...T([`the answer would be ${t.length} characters`],n)})}throw Error(`DEVUP_REFERENCE_PNG_TOO_LARGE ${t.length} characters, over the 94371840 one answer is read back up to`)}if(t.length<=6291456)return e;let i=`${r}@${Date.now()}.${Math.random().toString(36).slice(2,10)}`;return n.set(i,{text:t,at:Date.now()}),{kind:"devupReferencePngChunked",nodeId:r,id:i,length:t.length}};if(0===S.length)return _(await O({}));let P=I.some(e=>e.isMask),M=[],C=new Set,k=(e,t)=>{let r=p(e);for(let t of f(e.fills))"IMAGE"===t.type?((t.rotation||f([t.filters]).some(e=>Object.values(e).some(e=>e)))&&M.push(`${r} has an image fill with a rotation or filters`),C.add(t.imageHash)):"SOLID"===t.type||t.type.startsWith("GRADIENT_")||M.push(`${r} is painted with ${t.type}`);for(let t of f(e.effects))["LAYER_BLUR","DROP_SHADOW","INNER_SHADOW","BACKGROUND_BLUR"].includes(t.type)||M.push(`${r} has a ${t.type}`);let n=f(e.strokes);if((e=>{let t=0===f(e.strokes).length?[]:[e.strokeWeight,e.strokeTopWeight,e.strokeRightWeight,e.strokeBottomWeight,e.strokeLeftWeight].filter(e=>"number"==typeof e&&e>0);if(0===t.length)return!1;let r=Math.max(...t),n="OUTSIDE"===e.strokeAlign?0:"CENTER"===e.strokeAlign?r/2:r,i="INSIDE"===e.strokeAlign?0:"CENTER"===e.strokeAlign?r/2:r,a=e.absoluteBoundingBox;return c(a&&{x:a.x-i,y:a.y-i,width:a.width+2*i,height:a.height+2*i},o)&&!u(a,n)})(e)){let t=["FRAME","RECTANGLE","COMPONENT","COMPONENT_SET","INSTANCE","SECTION"].includes(e.type),i=n.every(e=>"SOLID"===e.type||e.type.startsWith("GRADIENT_")),a=Array.isArray(e.dashPattern)&&e.dashPattern.length>0,o="INSIDE"===e.strokeAlign||!e.strokeJoin||"MITER"===e.strokeJoin;t&&i&&!a&&o||M.push(`${r}'s stroke reaches it`)}return{transform:g(e.absoluteTransform),width:e.width,height:e.height,radii:[e.topLeftRadius,e.topRightRadius,e.bottomRightRadius,e.bottomLeftRadius].map(e=>"number"==typeof e?e:0),fills:g(f(e.fills)),opacity:void 0===e.opacity?1:e.opacity,blendMode:e.blendMode||null,clips:!!e.clipsContent,effects:g(f(e.effects)),strokes:g(n),strokeAlign:e.strokeAlign||null,strokeWeights:["strokeTopWeight","strokeRightWeight","strokeBottomWeight","strokeLeftWeight"].map(t=>"number"==typeof e[t]?e[t]:"number"==typeof e.strokeWeight?e.strokeWeight:0)}},R=async e=>{let t=await e.exportAsync(d(!0,e===a));return{data:figma.base64Encode(t),area:e.absoluteRenderBounds||e.absoluteBoundingBox,blendMode:e.blendMode||null,size:[16,20].map(e=>(t[e]<<24|t[e+1]<<16|t[e+2]<<8|t[e+3])>>>0)}},D=async e=>{let t=v.get(e);if(!t)return null;let r=await t.exportAsync({...d(!0,!1),useAbsoluteBounds:!0});return{data:figma.base64Encode(r),area:t.absoluteBoundingBox,luminance:"LUMINANCE"===t.maskType}},L=[],$={},U={};try{for(let e of y){let t=k(e,!1),r=await D(e);r&&(t.mask=r),L.push(t)}let e=e=>f(e.effects).some(e=>"BACKGROUND_BLUR"===e.type);if(w.length>0&&(e(a)||"findOne"in a&&a.findOne(e))){let r="children"in a&&a.children.some(t=>e(t)||"findOne"in t&&!!t.findOne(e));if("children"in a&&!r&&!f(a.strokes).length){for(let e of($.own=k(a,!0),$.children=[],a.children)){if(!1===e.visible||e.isMask||!e.absoluteRenderBounds)continue;let t=await R(e);$.children.push({data:t.data,area:t.area,blendMode:t.blendMode})}t=[Math.ceil(o.width*s-1e-6),Math.ceil(o.height*s-1e-6)]}else M.push(`${p(a)} blurs what lies behind it`)}else if(P)t=[Math.ceil(o.width*s-1e-6),Math.ceil(o.height*s-1e-6)];else{let e=await R(a);$.picture={data:e.data,area:e.area,blendMode:e.blendMode},t=e.size}let r=await D(a);r&&($.mask=r),t&&t[0]*t[1]>8e6&&M.push(`the area is ${t[0]}x${t[1]} pixels`);let n=globalThis.__devupNormalizeImage,i=Date.now()+2e4,l=0;for(let e of C){let t=e&&figma.getImageByHash(e),r=t&&await t.getBytesAsync();if(!r){M.push(`the image ${e} could not be read`);continue}let a=null;if("function"==typeof n){let{width:e,height:o}=await t.getSizeAsync(),l=(...e)=>e.every((e,t)=>r[t]===e),s=l(137,80,78,71)||l(255,216)||l(71,73,70),d=Math.min(1,8192/Math.max(e,o,1)),f=Math.ceil(e*d)*Math.ceil(o*d),c=5*f+65536,u=5*r.length+4*e*o*(d<1?2:1)+4*f+4*c+1048576,p=i-Date.now();if((!s||Math.max(e,o)>8192||r.length>4*e*o+1048576)&&u<=0x20000000&&p>0){let t=await n(r,8192,p,e,o);t&&(r=t,a={width:e,height:o})}}if(l+r.length>0x4000000){M.push("its ancestors' images are over 67108864 bytes");break}l+=r.length,U[e]=a?{data:figma.base64Encode(r),...a}:figma.base64Encode(r)}}catch(e){M.push(`a part could not be exported: ${e instanceof Error?e.message:String(e)}`)}let B=`${S.slice(0,5).join(", ")}${S.length>5?` and ${S.length-5} more`:""}`;return M.length>0?_(await O(T(M,B))):_(await O({reaching:B,compose:{area:o,scale:s,width:t[0],height:t[1],page:b,layers:L,node:$,images:U}}))},referencePngChunk:async function(e){let t=e.largeValue,r=globalThis.__devupReferencePngAnswers,n=r&&r.get(t.id);if(!n||"string"!=typeof n.text)throw Error(`DEVUP_REFERENCE_PNG_ANSWER_GONE ${t.id}`);let i=n.text,a=Math.max(0,Math.floor(Number(t.offset)||0)),o=Math.min(i.length,a+4194304);return o>=i.length&&r.delete(t.id),{kind:"devupReferencePngChunk",id:t.id,offset:a,nextOffset:o,complete:o>=i.length,text:i.slice(a,o)}},search:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=await figma.getNodeByIdAsync(e.nodeId);if(!t)throw Error("DEVUP_NODE_NOT_FOUND");let r=t;for(;r&&"PAGE"!==r.type;)r=r.parent;if(!r||"PAGE"!==r.type)throw Error("DEVUP_PAGE_NOT_FOUND");await r.loadAsync();let n=e.search,i=new Set((n.nodeTypes.length?n.nodeTypes:["PAGE","SECTION","FRAME","COMPONENT_SET","COMPONENT"]).map(e=>e.toUpperCase()));function a(e){return e.normalize("NFC").toLocaleLowerCase().replace(/\s/gu,"")}let o=t===r?r.findAll(()=>!0):"findAll"in t?t.findAll(()=>!0):[],l=[t,...o].filter(e=>i.has(e.type)&&"string"==typeof e.name).map(e=>({node:e,score:function(e){let t=n.query.trim();if(""===t||"*"===t)return 100;if(e===n.query)return 400;if("exact"===n.matchKind)return null;let r=a(e),i=a(n.query);if(r===i)return 300;if(r.startsWith(i))return 200;if(r.includes(i))return 100;if("fuzzy"!==n.matchKind)return null;let o=function(e,t){let r=Array.from({length:t.length+1},(e,t)=>t);for(let n=0;nnull!==e.score).sort((e,t)=>t.score-e.score||e.node.name.localeCompare(t.node.name)||e.node.id.localeCompare(t.node.id)),s=l.slice(0,n.limit),d=new Map([[r.id,r]]);for(let e of[t,...s.map(e=>e.node)]){let t=e;for(;t&&"DOCUMENT"!==t.type;)d.set(t.id,t),t=t.parent}return{fileKey:figma.fileKey||"",version:null,rootIds:[r.id],nodes:[...d.values()].map(e=>({id:e.id,type:e.type,fields:{name:e.name,searchCandidatesTruncated:e===t&&l.length>s.length,parentId:e.parent&&"DOCUMENT"!==e.parent.type?e.parent.id:null,childrenIds:"children"in e?e.children.map(e=>e.id):[]},extra:{},fieldErrors:{}})),diagnostics:[]}},sectionIndex:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=await figma.getNodeByIdAsync(e.nodeId);if(!t)throw Error("DEVUP_NODE_NOT_FOUND");if("SECTION"!==t.type){let e=t.parent,r=new Set;for(;e&&"SECTION"!==e.type&&!r.has(e.id);)r.add(e.id),e=e.parent;let n=e&&"SECTION"===e.type?e.id:null,i=n&&figma.fileKey?`https://www.figma.com/design/${figma.fileKey}?node-id=${n.replace(/:/g,"-")}`:null;throw Error("DEVUP_SECTION_REQUIRED "+JSON.stringify({pluginCode:"DEVUP_SECTION_REQUIRED",stage:"section-index",nodeId:t.id,nodeType:t.type,sectionId:n,nextAction:{tool:"devup_figma_export",how:n?`The url must point to a SECTION. This capture's SECTION is ${n}; select frames with frameIds.`:"The url must point to a SECTION. This node has no ancestor SECTION; choose the intended SECTION in Figma and copy its link.",arguments:i?"FRAME"===t.type&&t.parent===e?{url:i,frameIds:[t.id]}:{url:i}:null,requiredArguments:i?[]:["url"]}}))}let r=2048;function n(e){let t=e.absoluteBoundingBox||{x:"number"==typeof e.x?e.x:0,y:"number"==typeof e.y?e.y:0,width:"number"==typeof e.width?e.width:0,height:"number"==typeof e.height?e.height:0};return[t.x,t.y,t.width,t.height].every(Number.isFinite)?{x:t.x,y:t.y,width:t.width,height:t.height}:null}function i(e,t){if("FRAME"!==e.type||!1===e.visible||!t)return!1;let r=t.width/Math.max(1,t.height);return t.width>=240&&t.width<=1800&&t.height>=300&&t.height<=2e3&&r>=.25&&r<=2.5}function a(e){let t=0;for(let r=0;r=55296&&n<=56319&&r+1[e.id,t.id])),s=[],d=0;for(let e=0;ee.id));for(let r of t.children){if(e.has(r.id))continue;let t=n(r);t&&!1!==r.visible&&s.push({node:r,box:t})}}s.sort((e,t)=>e.box.y-t.box.y||e.box.x-t.box.x||e.node.id.localeCompare(t.node.id));let f=o.length>d||s.length>100,c=s.slice(0,100),u=new Set(c.map(({node:e})=>e.id)),p=new Set(e.rootIds),g={},h=new Map([[t.id,null]]);for(let e=0;e{let r=e.parent;for(;r&&r.id!==t.id&&!u.has(r.id);)r=r.parent;return[e.id,r&&u.has(r.id)?r.id:t.id]})),m=e=>c.filter(({node:t})=>y.get(t.id)===e).map(({node:e})=>e.id),b=n(t);if(!b)throw Error("DEVUP_NODE_BOUNDS_UNAVAILABLE");let x={id:t.id,type:t.type,fields:{name:t.name,parentId:t.parent&&"DOCUMENT"!==t.parent.type?t.parent.id:null,childrenIds:m(t.id),absoluteBoundingBox:b,visible:!1!==t.visible,projectionTruncated:f,nodeScreenIds:g},extra:{},fieldErrors:{}},E=c.length,A=c.map(({node:e,box:t})=>{let o,l=function(e){let t=[e],r=0,n=0;for(let e=0;er}}(e),s=Math.floor(r/E);E-=1;let d=function(e,t){let r=[e],n="",i=0,o=0,l=!1;if(t<=0)return{text:"",state:"budget-exhausted",spent:0};for(let e=0;e=120||o+e>t)return{text:n.trimEnd(),state:o+e>t?"budget-exhausted":"truncated",spent:o};n+=r,i+=1,o+=e}}if("children"in s){let e=Math.max(0,64-r.length);s.children.length>e&&(l=!0),r.push(...s.children.slice(0,e))}}}return{text:n,state:l?"truncated":n?"available":"no-text",spent:o}}(e,s);return r-=d.spent,{id:e.id,type:e.type,fields:{name:"string"==typeof e.name?e.name:"",parentId:y.get(e.id),childrenIds:m(e.id),absoluteBoundingBox:t,visible:!1!==e.visible,breadcrumb:function(e){let t=[],r=e;for(;r&&"DOCUMENT"!==r.type;)"string"==typeof r.name&&r.name&&t.push(r.name),r=r.parent;return t.reverse()}(e),directChildCount:"children"in e?e.children.length:0,textPreview:d.text,...null===(o=function(e){let t=[e],r=0,i=null;for(;t.length&&r++<400;){let e=t.shift();if("visible"in e&&!1===e.visible)continue;let r="children"in e?e.children.filter(e=>!("visible"in e)||!1!==e.visible):[];if(/header|nav|gnb/i.test(e.name||"")&&r.length>=2&&"layoutMode"in e&&"HORIZONTAL"===e.layoutMode){let t=r.map(n);t.every(Boolean)&&(i=Math.max(i||0,t.reduce((e,t)=>e+t.width,0)+(Number(e.itemSpacing)||0)*(t.length-1)))}t.push(...r.slice(0,80))}return i}(e))?{}:{navigationContentWidth:o},textPreviewState:d.state,subtreeNodeCount:l.subtreeNodeCount,estimatedSerializedBytes:l.estimatedSerializedBytes,selectionReasons:[i(e,t)?"screen-like":"explicit-selection-only","inside-section"],estimateTruncated:l.truncated},extra:{},fieldErrors:{}}}),I={fileKey:figma.fileKey||"",version:null,rootIds:[t.id],nodes:[x,...A],diagnostics:[]},S=a(JSON.stringify(I));for(let e=A.length-1;e>=0&&S>19456;e-=1)A[e].fields.textPreview="",A[e].fields.textPreviewState="budget-exhausted",S=a(JSON.stringify(I));let w=()=>I.nodes.length-1,v=A.length;for(;S>19456&&w()>1;){let e=I.nodes.pop();x.fields.projectionTruncated=!0,x.fields.childrenIds=x.fields.childrenIds.filter(t=>t!==e.id),S=a(JSON.stringify(I))}if(S>19456)throw Error("DEVUP_SECTION_INDEX_TOO_LARGE "+JSON.stringify({pluginCode:"DEVUP_SECTION_INDEX_TOO_LARGE",stage:"section-index",responseBytes:S,maxResponseBytes:19456,discoveredCandidates:v,retainedCandidates:w()}));return I},snapshot:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=await figma.getNodeByIdAsync(e.nodeId);if(!t)throw Error("DEVUP_NODE_NOT_FOUND");let r=["absoluteBoundingBox","absoluteRenderBounds","arcData","backgroundStyleId","blendMode","bottomLeftRadius","bottomRightRadius","boundVariables","characters","clipsContent","componentProperties","componentPropertyDefinitions","componentPropertyReferences","constraints","cornerRadius","counterAxisAlignItems","dashPattern","defaultVariant","effectStyleId","effects","fillStyleId","fills","fontName","fontSize","gridColumnAnchorIndex","gridColumnCount","gridColumnGap","gridColumnSizes","gridColumnSpan","gridRowAnchorIndex","gridRowCount","gridRowGap","gridRowSizes","gridRowSpan","gridStyleId","height","inferredAutoLayout","isAsset","isMask","itemSpacing","layoutGrow","layoutMode","layoutWrap","layoutPositioning","layoutSizingHorizontal","layoutSizingVertical","letterSpacing","lineHeight","maxHeight","maxLines","maxWidth","minHeight","minWidth","name","opacity","overflowDirection","paddingBottom","paddingLeft","paddingRight","paddingTop","primaryAxisAlignItems","reactions","rotation","strokeAlign","strokeBottomWeight","strokeLeftWeight","strokeRightWeight","strokeStyleId","strokeTopWeight","strokeWeight","strokes","targetAspectRatio","textAlignHorizontal","textAlignVertical","textAutoResize","textCase","textDecoration","textStyleId","textTruncation","topLeftRadius","topRightRadius","variantProperties","visible","width","x","y"],n=new Set(r),i=["fontName","fontWeight","fontSize","textDecoration","textCase","lineHeight","letterSpacing","fills","textStyleId","fillStyleId","listOptions","indentation","hyperlink"],a=e.snapshot,o=Math.max(0,Math.floor(Number(a.offset)||0)),l=Math.min(a.maxEnvelopeBytes?Math.max(8192,Math.floor(Number(a.maxEnvelopeBytes))):16e3,Math.max(4096,Math.floor(Number(a.maxPayloadBytes)||15e3))),s=Math.min(l-1024,Math.max(512,Math.floor(Number(a.maxFieldBytes)||4096))),d=new Set(["id","type","parent","children","mainComponent","masterComponent","instances"]),f=new Set(["parentId","childrenIds","name","characters","styledTextSegments","boundVariables"]);function c(e,t,r){let n=function(e){let t=[];for(let r=0;r=55296&&n<=56319){let t=r+1=56320&&t<=57343?(n=65536+(n-55296<<10)+(t-56320),r+=1):n=65533}else n>=56320&&n<=57343&&(n=65533);n<128?t.push(n):n<2048?t.push(192|n>>6,128|63&n):n<65536?t.push(224|n>>12,128|n>>6&63,128|63&n):t.push(240|n>>18,128|n>>12&63,128|n>>6&63,128|63&n)}return new Uint8Array(t)}(JSON.stringify(r));return n.length>0x1000000?{$truncated:"max-large-value-bytes",byteLength:n.length}:{$largeValue:{nodeId:e,field:t,byteLength:n.length,sha256:function(e){let t=[0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,0xe49b69c1,0xefbe4786,0xfc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x6ca6351,0x14292967,0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2],r=64*Math.ceil((e.length+9)/64),n=new Uint8Array(r);n.set(e),n[e.length]=128;let i=8*e.length;for(let e=0;e<8;e+=1)n[r-1-e]=255&Math.floor(i/2**(8*e));let a=[0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19],o=(e,t)=>e>>>t|e<<32-t;for(let e=0;e>>3,n=o(r[e-2],17)^o(r[e-2],19)^r[e-2]>>>10;r[e]=r[e-16]+t+r[e-7]+n>>>0}let[i,l,s,d,f,c,u,p]=a;for(let e=0;e<64;e+=1){let n=p+(o(f,6)^o(f,11)^o(f,25))+(f&c^~f&u)+t[e]+r[e]>>>0,a=(o(i,2)^o(i,13)^o(i,22))+(i&l^i&s^l&s)>>>0;p=u,u=c,c=f,f=d+n>>>0,d=s,s=l,l=i,i=n+a>>>0}for(let[e,t]of[i,l,s,d,f,c,u,p].entries())a[e]=a[e]+t>>>0}return a.map(e=>e.toString(16).padStart(8,"0")).join("")}(n),cursor:{nextOffset:0,maxChunkBytes:12288}}}}function u(e){return function(e){let t=0;for(let r=0;r=55296&&n<=56319&&r+112)return{$truncated:"max-depth"};if("object"==typeof t&&"parent"in t&&"string"==typeof t.id&&"string"==typeof t.type)return{$nodeId:t.id,$nodeType:t.type};if(Array.isArray(t))return t.map(t=>e(t,r,n+1));if(ArrayBuffer.isView(t))return{$binary:t.constructor.name,byteLength:t.byteLength};if(t instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:t.byteLength};if(r.has(t))return{$circular:!0};r.add(t);let i={};for(let a of Object.keys(t).sort())try{let o=e(t[a],r,n+1);o&&"function"===o.$unsupported||(i[a]=o)}catch(e){i[a]={$error:String(e&&e.message?e.message:e)}}return r.delete(t),i}(r),a=u(i);if(a<=s)return i;let o=c(e,t,i);return o.$truncated&&(n[t]=`DEVUP_FIELD_VALUE_UNSUPPORTED:${a}>16777216`),o}let g=new Map;async function h(e,t,r){if("INSTANCE"!==e.type||"function"!=typeof e.getMainComponentAsync)return;g.has(e.id)||g.set(e.id,(async()=>{try{let t=await e.getMainComponentAsync();return{fields:{mainComponentId:t?t.id:null,mainComponentName:t?t.name:null,mainComponentSetId:t&&t.parent&&"COMPONENT_SET"===t.parent.type?t.parent.id:null}}}catch(e){return{error:String(e&&e.message?e.message:e)}}})());let n=await g.get(e.id);n.error?r.mainComponentId=n.error:Object.assign(t,n.fields)}async function y(e){let t={},a={},o={};for(let i of(t.parentId=e.parent?e.parent.id:null,e.parent&&("PAGE"===e.parent.type||"SECTION"===e.parent.type||"COMPONENT_SET"===e.parent.type)&&(t.parentType=e.parent.type,"SECTION"===e.parent.type&&(t.parentName=e.parent.name)),t.childrenIds="children"in e?e.children.map(e=>e.id):[],!("overflowDirection"in e)&&["FRAME","COMPONENT","INSTANCE","COMPONENT_SET"].includes(e.type)&&(t.overflowDirection=null),function(e){let t=new Set,n=e;for(;n&&n!==Object.prototype;){for(let e of Object.getOwnPropertyNames(n))t.add(e);n=Object.getPrototypeOf(n)}for(let n of r)try{n in e&&t.add(n)}catch(e){}return[...t].sort()}(e)))if(!(d.has(i)||i.startsWith("_")||"componentPropertyDefinitions"===i&&"COMPONENT"===e.type&&e.parent&&"COMPONENT_SET"===e.parent.type))try{let r=e[i];if("function"==typeof r)continue;let l=p(e.id,i,r,o);(n.has(i)?t:a)[i]=l}catch(e){o[i]=String(e&&e.message?e.message:e)}if("TEXT"===e.type&&"function"==typeof e.getStyledTextSegments)try{t.styledTextSegments=p(e.id,"styledTextSegments",e.getStyledTextSegments(i),o)}catch(e){o.styledTextSegments=String(e&&e.message?e.message:e)}return await h(e,t,o),{id:e.id,type:e.type,fields:t,extra:a,fieldErrors:o}}let m=[],b=[t];for(;b.length;){let e=b.shift();m.push(e),"children"in e&&b.push(...e.children)}let x=[],E=l-1024,A=2;for(let e=o;eNumber(e.protected)-Number(t.protected)||t.byteLength-e.byteLength),n)){if(r<=t)break;let n=c(e.id,i.name,e[i.sectionName][i.name]);e[i.sectionName][i.name]=n,n.$truncated&&(e.fieldErrors[i.name]=`DEVUP_FIELD_VALUE_UNSUPPORTED:${i.byteLength}>16777216`),r=u(e)}return e}(await y(m[e]),E),r=u(t)+ +!!x.length;if(x.length&&A+r>E)break;x.push(t),A+=r}let I=Math.min(m.length,o+x.length);return x.push({id:"__DEVUP_SNAPSHOT_CURSOR__",type:"DEVUP_INTERNAL",fields:{offset:o,nextOffset:I,complete:I>=m.length,totalNodes:m.length},extra:{},fieldErrors:{}}),{fileKey:figma.fileKey||"",version:null,rootIds:[t.id],nodes:x,diagnostics:[]}},usedResources:async function(e){let t=e.resources;function r(e,t=new WeakSet,n=0){if(null===e||["string","number","boolean"].includes(typeof e))return e;if(void 0===e)return{$undefined:!0};if("bigint"==typeof e)return{$bigint:e.toString()};if(["function","symbol"].includes(typeof e))return{$unsupported:typeof e};if(n>12)return{$truncated:"max-depth"};if("object"==typeof e&&"parent"in e&&"string"==typeof e.id&&"string"==typeof e.type)return{$nodeId:e.id,$nodeType:e.type};if(Array.isArray(e))return e.map(e=>r(e,t,n+1));if(ArrayBuffer.isView(e))return{$binary:e.constructor.name,byteLength:e.byteLength};if(e instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:e.byteLength};if(t.has(e))return{$circular:!0};t.add(e);let i={},a=new Set(Object.keys(e)),o=e;for(;o&&o!==Object.prototype;){for(let e of Object.getOwnPropertyNames(o))a.add(e);o=Object.getPrototypeOf(o)}for(let o of[...a].sort())if(!(o.startsWith("_")||["parent","children","consumers"].includes(o)))try{let a=r(e[o],t,n+1);a&&"function"===a.$unsupported||(i[o]=a)}catch(e){i[o]={$error:"unavailable"}}return t.delete(e),i}let n=new Map,i="ok";try{for(let e of(await figma.variables.getLocalVariablesAsync()))n.set(e.id,e)}catch(e){i="unavailable"}function a(e){return Object.values(e.valuesByMode||{}).filter(e=>e&&"VARIABLE_ALIAS"===e.type&&"string"==typeof e.id&&e.id).map(e=>e.id)}async function o(e){let t=n.get(e);if(t)return{value:r(t),collectionId:t.variableCollectionId,aliasIds:a(t)};try{let t=await figma.variables.getVariableByIdAsync(e);return t?{value:r(t),collectionId:t.variableCollectionId,aliasIds:a(t)}:{unresolved:{id:e,kind:"variable",reason:"notInFileAndLookupEmpty"}}}catch(t){return{unresolved:{id:e,kind:"variable",reason:"notInFileAndLookupThrew",error:String(t&&t.message||t).slice(0,300)}}}}let l=await Promise.all(t.variableIds.map(o)),s=new Set(t.variableIds),d=l.flatMap(e=>e.aliasIds||[]);for(;d.length>0;){let e=[...new Set(d)].filter(e=>!s.has(e)).sort();for(let t of e)s.add(t);let t=await Promise.all(e.map(o));l.push(...t),d=t.flatMap(e=>e.aliasIds||[])}let f=[...new Set(l.flatMap(e=>e.collectionId?[e.collectionId]:[]))].sort(),c=await Promise.all(f.map(async e=>{try{let t=await figma.variables.getVariableCollectionByIdAsync(e);return t?[r(t)]:[]}catch(e){return[]}})),u=await Promise.all(t.styles.map(async e=>{try{let t=await figma.getStyleByIdAsync(e.id);if(!t)return{unresolved:{id:e.id,kind:"style",reason:"notFoundOrUnavailable"}};return{value:{...r(t),styleType:e.styleType,value:r("PAINT"===e.styleType?t.paints:"EFFECT"===e.styleType?t.effects:"GRID"===e.styleType?t.layoutGrids:t)}}}catch(t){return{unresolved:{id:e.id,kind:"style",reason:"notFoundOrUnavailable",error:String(t&&t.message||t).slice(0,300)}}}}));return{collections:c.flat(),variables:l.flatMap(e=>e.value?[e.value]:[]),styles:u.flatMap(e=>e.value?[e.value]:[]),usedVariableIds:t.variableIds,usedStyleIds:t.styles.map(e=>e.id),localVariableListing:i,localVariableCount:n.size,unresolved:[...l,...u].flatMap(e=>e.unresolved?[e.unresolved]:[])}},variableCatalog:async function(e){let[t,r,n,i,a]=await Promise.all([figma.variables.getLocalVariableCollectionsAsync(),figma.getLocalPaintStylesAsync(),figma.getLocalTextStylesAsync(),figma.getLocalEffectStylesAsync(),figma.getLocalGridStylesAsync()]),o=["PAINT","TEXT","EFFECT","GRID"];return{collections:t.map(e=>(function e(t,r=new WeakSet,n=0){if(null===t||["string","number","boolean"].includes(typeof t))return t;if(void 0===t)return{$undefined:!0};if("bigint"==typeof t)return{$bigint:t.toString()};if(["function","symbol"].includes(typeof t))return{$unsupported:typeof t};if(n>12)return{$truncated:"max-depth"};if(Array.isArray(t))return t.map(t=>e(t,r,n+1));if(r.has(t))return{$circular:!0};r.add(t);let i={},a=new Set(Object.keys(t)),o=t;for(;o&&o!==Object.prototype;){for(let e of Object.getOwnPropertyNames(o))a.add(e);o=Object.getPrototypeOf(o)}for(let o of[...a].sort())if(!o.startsWith("_"))try{let a=e(t[o],r,n+1);a&&"function"===a.$unsupported||(i[o]=a)}catch(e){i[o]={$error:String(e&&e.message?e.message:e)}}return r.delete(t),i})(e)),variableIds:[...new Set(t.flatMap(e=>e.variableIds))].sort(),styles:[r,n,i,a].flatMap((e,t)=>e.map(e=>({id:e.id,styleType:o[t]}))).sort((e,t)=>e.id.localeCompare(t.id)),localComplete:!0,usedRemoteComplete:!1}},variables:async function(e){let t=e.resources;function r(e,t=new WeakSet,n=0){if(null===e||["string","number","boolean"].includes(typeof e))return e;if(void 0===e)return{$undefined:!0};if("bigint"==typeof e)return{$bigint:e.toString()};if(["function","symbol"].includes(typeof e))return{$unsupported:typeof e};if(n>12)return{$truncated:"max-depth"};if("object"==typeof e&&"parent"in e&&"string"==typeof e.id&&"string"==typeof e.type)return{$nodeId:e.id,$nodeType:e.type};if(Array.isArray(e))return e.map(e=>r(e,t,n+1));if(ArrayBuffer.isView(e))return{$binary:e.constructor.name,byteLength:e.byteLength};if(e instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:e.byteLength};if(t.has(e))return{$circular:!0};t.add(e);let i={},a=new Set(Object.keys(e)),o=e;for(;o&&o!==Object.prototype;){for(let e of Object.getOwnPropertyNames(o))a.add(e);o=Object.getPrototypeOf(o)}for(let o of[...a].sort())if(!(o.startsWith("_")||["parent","children","consumers"].includes(o)))try{let a=r(e[o],t,n+1);a&&"function"===a.$unsupported||(i[o]=a)}catch(e){i[o]={$error:String(e&&e.message?e.message:e)}}return t.delete(e),i}let[n,i]=await Promise.all([Promise.all(t.variableIds.map(e=>figma.variables.getVariableByIdAsync(e))),Promise.all(t.styles.map(e=>figma.getStyleByIdAsync(e.id)))]),a=new Map(t.styles.map(e=>[e.id,e])),o=await Promise.all(i.filter(Boolean).map(async e=>{let t=a.get(e.id),n=t.styleType;if(Number.isInteger(t.consumerStart)&&Number.isInteger(t.consumerEnd)){let i=await e.getStyleConsumersAsync();return{id:e.id,styleType:n,$consumerStart:t.consumerStart,$consumerEntries:i.slice(t.consumerStart,t.consumerEnd).map(e=>[e.node.id,e.node.type,r(e.fields)])}}let i=await e.getStyleConsumersAsync();return{...r(e),styleType:n,$consumerCount:i.length,value:r("PAINT"===n?e.paints:"EFFECT"===n?e.effects:"GRID"===n?e.layoutGrids:e)}}));return{variables:n.filter(Boolean).map(e=>r(e)),styles:o}}},t=new Map,r=0;globalThis.__devupNormalizeImage=(e,n,i,a,o)=>new Promise(l=>{r+=1;let s=`decode-${r}`,d=setTimeout(()=>{t.delete(s),l(null)},i);t.set(s,e=>{clearTimeout(d),t.delete(s),l(e)}),figma.ui.postMessage({kind:"devup-decode",id:s,bytes:e,maxEdge:n,width:a,height:o})});let n=globalThis;function i(){let e=figma.currentPage,t=e.selection;return t.length>0&&(n.__devupLastSelection=t.slice(0,20).map(e=>({id:e.id,name:e.name.slice(0,256),type:e.type})),n.__devupLastSelectionPage={id:e.id,name:e.name.slice(0,256)}),{zoom:Number.isFinite(figma.viewport?.zoom)?figma.viewport.zoom:null,observedAt:Date.now(),lastKnownSelection:n.__devupLastSelection??null,lastKnownPage:n.__devupLastSelectionPage??null,currentPage:{id:e.id,name:e.name.slice(0,256)},selection:t.slice(0,20).map(e=>({id:e.id,name:e.name.slice(0,256),type:e.type})),selectionCount:t.length}}function a(){let e={kind:"devup-context",...i()};figma.ui.postMessage(e)}async function o(t){let r=e[t.script];if(!r)return{kind:"devup-result",requestId:t.requestId,error:`DEVUP_BRIDGE_UNKNOWN_SCRIPT: ${t.script} (아는 스크립트: ${Object.keys(e).join(", ")})`};try{var n;"bridgeContext"===t.script&&i();let e=await r({nodeId:(n=t.params).nodeId??"",rootIds:n.rootIds??[],snapshot:n.snapshot??{},search:n.search??{},explore:n.explore??{},resources:n.resources??{variableIds:[],styles:[]},largeValue:n.largeValue??{},asset:n.asset??{},theme:n.theme??{offset:0}});return{kind:"devup-result",requestId:t.requestId,data:e}}catch(e){return{kind:"devup-result",requestId:t.requestId,error:e instanceof Error?e.message:String(e)}}}let l=[],s=!1,d=null,f=new Set;async function c(){if(!s){s=!0;try{for(;l.length>0;){let e=l.shift();d=e.requestId;let t=await o(e).catch(t=>({kind:"devup-result",requestId:e.requestId,error:t instanceof Error?t.message:String(t)}));d=null,f.delete(e.requestId)||figma.ui.postMessage(t)}}finally{s=!1}}}let u=null;function p(){u??(u=(async()=>{try{await figma.loadAllPagesAsync(),await Promise.all([figma.variables.getLocalVariableCollectionsAsync(),figma.variables.getLocalVariablesAsync(),figma.getLocalPaintStylesAsync(),figma.getLocalTextStylesAsync(),figma.getLocalEffectStylesAsync(),figma.getLocalGridStylesAsync()])}catch{}finally{u=null}})())}figma.showUI(__html__,{width:320,height:220}),p(),figma.on("selectionchange",a),figma.on("currentpagechange",a),figma.ui.onmessage=e=>{if("object"==typeof e&&null!==e){if("devup-ready"===e.kind){let e={kind:"devup-status",fileKey:figma.fileKey??null,fileName:figma.root.name,port:1993,...i()};figma.ui.postMessage(e);return}if("devup-job"===e.kind){l.push(e),c();return}if("devup-cancel"===e.kind){var r;let t;return void(r=e.requestId,(t=l.findIndex(e=>e.requestId===r))>=0?l.splice(t,1):d===r&&f.add(r))}if("devup-decoded"===e.kind)return void t.get(e.id)?.(e.png??null);"devup-in-view"===e.kind&&p()}}})(); \ No newline at end of file +(()=>{"use strict";let e={assets:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=e.asset;function r(e){let t=[0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,0xe49b69c1,0xefbe4786,0xfc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x6ca6351,0x14292967,0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2],r=64*Math.ceil((e.length+9)/64),n=new Uint8Array(r);n.set(e),n[e.length]=128;let i=8*e.length;for(let e=0;e<8;e+=1)n[r-1-e]=255&Math.floor(i/2**(8*e));let a=[0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19],o=(e,t)=>e>>>t|e<<32-t;for(let e=0;e>>3,n=o(r[e-2],17)^o(r[e-2],19)^r[e-2]>>>10;r[e]=r[e-16]+t+r[e-7]+n>>>0}let[i,l,s,d,f,c,u,p]=a;for(let e=0;e<64;e+=1){let n=p+(o(f,6)^o(f,11)^o(f,25))+(f&c^~f&u)+t[e]+r[e]>>>0,a=(o(i,2)^o(i,13)^o(i,22))+(i&l^i&s^l&s)>>>0;p=u,u=c,c=f,f=d+n>>>0,d=s,s=l,l=i,i=n+a>>>0}for(let[e,t]of[i,l,s,d,f,c,u,p].entries())a[e]=a[e]+t>>>0}return a.map(e=>e.toString(16).padStart(8,"0")).join("")}async function n(e){try{let t=await figma.getNodeByIdAsync(e);if(t)return t}catch(r){if(!e.startsWith("I")||!e.includes(";"))throw r;try{return await t()}catch(e){throw Error(`${r.message||r}; nested lookup fallback: ${e.message||e}`)}}if(!e.startsWith("I")||!e.includes(";"))return null;return t();async function t(){let t=await figma.getNodeByIdAsync(e.slice(1).split(";")[0]),r=t?[t]:[],n=new Set;for(;r.length;){let t=r.pop();if(t.id===e)return t;if(!n.has(t.id)){if(n.add(t.id),n.size>1e4)throw Error("nested lookup exceeds 10000 nodes");Array.isArray(t.children)&&r.push(...t.children)}}throw Error(`nested node ${e} not found in its instance`)}}async function i(e,t,r=85e3,n=()=>{}){let a,o=Date.now();try{let i=await Promise.race([e.exportAsync(t),new Promise((e,t)=>{a=setTimeout(()=>t(Error(`DEVUP_ASSET_TIMEOUT: native export exceeded ${r} ms`)),r)})]);return n("native-export",Date.now()-o),i}catch(i){if(n("native-export",Date.now()-o,String(i.message||i).slice(0,500)),"SVG_STRING"!==t.format)throw i;let r=Date.now();try{let t=function(e){let t=e=>"number"==typeof e&&Number.isFinite(e),r=(e,t)=>{throw Error(`geometry fallback unsupported: ${e.type||"unknown"} ${e.id||""}: ${t}`)},n=0,i=0;function a(e,i,a){(void 0!==a&&!Array.isArray(a)||void 0===a&&Array.isArray(i)&&i.length)&&r(e,"unresolved paint requires native export");let o=Array.isArray(a)?a.filter(e=>!1!==e.visible):[];return o.length?(Array.isArray(i)&&i.length||r(e,"paint has no rendered geometry"),o.map(a=>{("SOLID"!==a.type||a.blendMode&&"NORMAL"!==a.blendMode)&&r(e,"paint type or blending requires native export");let o=a.color;o&&[o.r,o.g,o.b].every(e=>t(e)&&e>=0&&e<=1)||r(e,"invalid solid color");let l=void 0===a.opacity?1:a.opacity;return(!t(l)||l<0||l>1)&&r(e,"invalid paint opacity"),["NONZERO","EVENODD"].map(t=>{let a=i.filter(e=>e.windingRule===t);if(!a.length)return"";for(let t of a)"string"==typeof t.data&&t.data.trim()||r(e,"empty path"),n++;return`"']/g,e=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[e])}" fill="rgb(${100*o.r}%,${100*o.g}%,${100*o.b}%)" fill-opacity="${l}" fill-rule="${"EVENODD"===t?"evenodd":"nonzero"}"/>`}).join("")}).join("")):""}let o=e.absoluteRenderBounds,l=e.absoluteTransform;(!o||![o.x,o.y,o.width,o.height].every(t)||o.width<=0||o.height<=0)&&r(e,"missing render bounds"),(!Array.isArray(l)||2!==l.length||!l.every(e=>Array.isArray(e)&&3===e.length&&e.every(t))||0!==l[0][1]||0!==l[1][0]||l[0][0]<=0||l[1][1]<=0)&&r(e,"root rotation, reflection or shear requires native export");let s=(o.x-l[0][2])/l[0][0],d=(o.y-l[1][2])/l[1][1],f=o.width/l[0][0],c=o.height/l[1][1],u=function e(n,o){if(++i>1e4&&r(n,"subtree exceeds 10000 nodes"),n.isMask&&r(n,"mask requires native export"),!1===n.visible||0===n.opacity)return"";n.clipsContent&&r(n,"clipping requires native export"),Array.isArray(n.effects)&&n.effects.some(e=>!1!==e.visible)&&r(n,"effect requires native export"),n.blendMode&&!["NORMAL","PASS_THROUGH"].includes(n.blendMode)&&r(n,"layer blending requires native export");let l=void 0===n.opacity?1:n.opacity;(!t(l)||l<0||l>1)&&r(n,"invalid layer opacity");let s="";if(!o){let e=n.relativeTransform;Array.isArray(e)&&2===e.length&&e.every(e=>Array.isArray(e)&&3===e.length&&e.every(t))||r(n,"missing relative transform"),s=` transform="matrix(${e[0][0]} ${e[1][0]} ${e[0][1]} ${e[1][1]} ${e[0][2]} ${e[1][2]})"`}let d="";if(["GROUP","FRAME","INSTANCE","COMPONENT"].includes(n.type))(Array.isArray(n.fills)&&n.fills.some(e=>!1!==e.visible)||Array.isArray(n.strokes)&&n.strokes.some(e=>!1!==e.visible))&&r(n,"container paint requires native export"),Array.isArray(n.children)||r(n,"missing children"),d=(!0===n.itemReverseZIndex?[...n.children].reverse():n.children).map(t=>e(t,!1)).join("");else{for(let e of(["VECTOR","BOOLEAN_OPERATION","RECTANGLE","ELLIPSE","POLYGON","STAR","LINE"].includes(n.type)||r(n,"node type requires native export"),Array.isArray(n.strokes)&&n.strokes.some(e=>!1!==e.visible)&&"CENTER"!==n.strokeAlign&&r(n,"non-center stroke alignment requires native export"),Array.isArray(n.dashPattern)&&n.dashPattern.length&&r(n,"dashed stroke requires native export"),[n.fillGeometry,n.strokeGeometry]))void 0!==e&&(!Array.isArray(e)||e.some(e=>!["NONZERO","EVENODD"].includes(e.windingRule)))&&r(n,"unknown winding rule");d=a(n,n.fillGeometry,n.fills)+a(n,n.strokeGeometry,n.strokes)}return`${d}`}(e,!0);return n||r(e,"no painted geometry"),`${u}`}(e);return n("geometry-fallback",Date.now()-r),t}catch(e){throw Error(`native export: ${i.message||i}; ${e.message||e}`)}}finally{clearTimeout(a)}}let a=Date.now(),o="lookup",l="unknown",s=Math.min(6e5,Math.max(1e3,Number(t.timeoutMs)||9e4)),d={method:"native",stages:[]};function f(e,t,r){d.stages.push({stage:e,elapsedMs:t}),r&&(d.nativeError=r),"geometry-fallback"===e&&(d.method="geometry")}async function c(e){let t;try{return await Promise.race([e(),new Promise((e,r)=>{t=setTimeout(()=>r(Error(`DEVUP_ASSET_TIMEOUT: ${o} exceeded ${s} ms asset budget`)),Math.max(0,s-(Date.now()-a)))})])}finally{clearTimeout(t)}}function u(e){let r=e&&"string"==typeof e.message?e.message:String(e),n="string"==typeof figma.editorType?` (editor: ${figma.editorType})`:"";return`${o} failed after ${Date.now()-a} ms${n} (node: ${t.nodeId}, type: ${l}): ${r}`.slice(0,500)}function p(e,r){return e&&d.renderInfo&&(d.renderInfo.attemptedRepresentation=d.renderInfo.representation,d.renderInfo.representation="not-exported",d.renderInfo.renderedAs="not-exported",d.renderInfo.effectsBakedIntoAsset=!1,d.renderInfo.fidelity="unverified"),{kind:"devupAssetExport",fileKey:figma.fileKey||"",version:t.version,assetId:t.assetId,nodeId:t.nodeId,field:t.field,imageHash:t.imageHash,format:t.format,scale:t.scale,renderMode:h,usageAspectRatio:t.usageAspectRatio??null,maxWidth:t.maxWidth??null,srcset:!0===t.srcset,visibleScopeNodeId:t.visibleScopeNodeId??null,status:"failed",byteLength:null,sha256:null,errorCode:e,exportDiagnostics:d,...r?{errorDetail:r}:{}}}let g="string"==typeof t.field&&t.field.startsWith("$original-image/fills/"),h=void 0===t.renderMode?g?"fill":["svg","pdf"].includes(t.format)?"node":"original-source":t.renderMode,y=["auto","original-source"].includes(h)?"auto"===h?"node":"fill":h;function m(e,r){let n=e=>"number"==typeof e&&Number.isFinite(e)&&e>0,i=Array.isArray(e.effects)?e.effects:null,a=[],o=e=>[...e].slice(0,128).join(""),l=["string"==typeof e.name?o(e.name):"layer"],s=new Set,d=e;for(let t=e.parent;t&&a.length<32&&!s.has(t);t=t.parent)s.add(t),l.length<4&&"string"==typeof t.name&&t.name.length&&!["PAGE","DOCUMENT"].includes(t.type)&&l.push(o(t.name)),a.push({type:t.type||"unknown",clipsContent:t.clipsContent??null,isMask:t.isMask??null,effects:Array.isArray(t.effects)?t.effects:null,opacity:t.opacity??null,blendMode:t.blendMode??null,visible:t.visible??null,siblingMaskPresent:Array.isArray(t.children)?t.children.some(e=>e!==d&&!0===e.isMask):null}),d=t;return{sourceKind:r?"image-fill":t.imageHash?"image-node":"vector-node",renderedAs:"fill"===y?"raw-fill":"node-render",representation:"fill"===y?"original-fill":"native-node",layerHierarchy:l.reverse(),scope:"fill"===y?"selected-paint-original-bytes":"exact-node-subtree",exportSettings:"node"===y?{contentsOnly:!0,useAbsoluteBounds:!1}:null,requestedScale:t.scale,scaleSemantics:"fill"===y?"intrinsic-original-bytes-no-resize":["png","jpg"].includes(t.format)?"native-node-SCALE-constraint":"not-applied-to-vector-document",nodeSize:n(e.width)&&n(e.height)?{width:e.width,height:e.height,aspectRatio:e.width/e.height}:null,outputSize:null,dimensionEvidence:"unknown-until-byte-inspection",aspectRatioComparison:"unknown",scaleMode:r?r.scaleMode??null:null,imageTransform:r?r.imageTransform??null:null,visibleSourceRatio:null,visibleSourceRatioEvidence:"not-computed",effects:i,extraFills:Array.isArray(e.fills)?Math.max(0,e.fills.filter(e=>!1!==e.visible).length-!!r):null,effectsBakedIntoAsset:"node"===y&&["png","jpg"].includes(t.format)&&null!==i,bakedEffects:"node"===y&&["png","jpg"].includes(t.format)&&i?i.filter(e=>!1!==e.visible):[],instanceOverride:"unknown",instanceOverrideEvidence:"no-component-paint-comparison",sourceProperties:{scaleMode:r?r.scaleMode??null:null,imageTransform:r?r.imageTransform??null:null,rotation:r?r.rotation??null:null,filters:r?r.filters??null:null,opacity:r?r.opacity??null:null,effects:i,blurEffects:i?i.filter(e=>!1!==e.visible&&String(e.type).includes("BLUR")):null,isMask:e.isMask??null,clipsContent:e.clipsContent??null,ancestors:a},unappliedProperties:"fill"===y?["scaleMode","imageTransform","rotation","filters","paintOpacity","nodeGeometry","nodeTransforms","nodeEffectsIncludingBlur","nodeAndAncestorMasks","ancestorClipsAndEffects"]:[],warnings:"fill"===y?["original-fill-properties-unapplied","aspect-ratio-unknown"]:["ancestor-canvas-appearance-unverified","aspect-ratio-unknown"],fidelity:"unverified"}}function b(e,t){let r=0;for(let n=e;n&&r++<32;n=n.parent){if("INSTANCE"===n.type&&Array.isArray(n.overrides)&&n.overrides.find(t=>t.id===e.id&&Array.isArray(t.overriddenFields)&&t.overriddenFields.includes("fills"))){t.instanceOverride=!0,t.instanceOverrideEvidence={kind:"figma-instance-overrides",instanceId:n.id,nodeId:e.id,overriddenFields:["fills"]};return}if(["PAGE","DOCUMENT"].includes(n.type))break}}try{let e;if(!["original-source","fill","node","visible","auto"].includes(h))return p("DEVUP_ASSET_RENDER_MODE_UNSUPPORTED");if(!g&&"fill"===h&&(!String(t.field).startsWith("fills/")||!["png","jpg","webp"].includes(t.format)||1!==t.scale))return p("DEVUP_ASSET_FILL_SETTINGS_UNSUPPORTED","Original fill requires fills/N, png or jpg matching its codec, and scale 1; no resizing or transcoding.");if((g||["fill","original-source"].includes(h))&&"bridge"!==t.transport)return p(g?"DEVUP_ORIGINAL_IMAGE_REQUIRES_BRIDGE":"DEVUP_ASSET_FILL_REQUIRES_BRIDGE");let x=await c(()=>n(t.nodeId));if(f("lookup",Date.now()-a),l=x&&x.type||"unknown",!x||["node","visible"].includes(h)&&"function"!=typeof x.exportAsync)return p("DEVUP_ASSET_UNSUPPORTED_BY_UPSTREAM",u(Error(x?"node has no exportAsync":"node not found")));let E=null;if(g||"string"==typeof t.field&&t.field.startsWith("fills/")){let e=Number(t.field.slice(g?22:6)),r="fills"in x&&Array.isArray(x.fills)?x.fills:[],n=Number.isInteger(e)?r[e]:null,i=n&&"IMAGE"===n.type?n.imageHash||n.imageRef:null;if(!n||i!==t.imageHash)return p("DEVUP_ASSET_SOURCE_CHANGED");E=n}else if("node"!==t.field)return p("DEVUP_ASSET_FIELD_UNSUPPORTED");if("original-source"===h&&(!E||!t.imageHash))return p("DEVUP_ORIGINAL_SOURCE_UNAVAILABLE","No IMAGE fill source exists for this selection. Choose an IMAGE fill or explicitly renderMode node; vector assets should retain SVG.");let A=x;if("visible"===h){let e=t.visibleScopeNodeId;if(!e)return p("DEVUP_VISIBLE_CONTEXT_UNAVAILABLE","Recollect the parent frame and provide visibleScopeNodeId.");let r=new Set;for(;A&&A.id!==e&&!r.has(A)&&r.size<64;)r.add(A),A=A.parent;if(!A||A.id!==e||["PAGE","DOCUMENT"].includes(A.type)||"function"!=typeof A.exportAsync)return p("DEVUP_VISIBLE_SCOPE_UNSUPPORTED","The selected capture scope is not an exportable ancestor of the requested node.")}if(d.renderInfo=m(x,E),"visible"===h){let e=d.renderInfo;e.representation="native-visible",e.renderedAs="visible-scoped-capture",e.captureNodeId=A.id,e.captureBox={width:A.width,height:A.height,aspectRatio:A.width/A.height},e.contextScope="selected-ancestor-subtree",e.captureEffects=Array.isArray(A.effects)?A.effects:null,e.requestedNodeEffects=e.effects,e.effects=e.captureEffects,e.sourceProperties.captureEffects=e.captureEffects,e.visibleContext={requestedNodeId:x.id,captureNodeId:A.id,included:["selected-scope-children","selected-scope-clipping","selected-scope-masks","selected-scope-rounding"],outsideScope:"unknown; ancestors, page backdrop and external layers are not included",canvasReferenceCompared:!1},e.warnings.push("visible-scoped-capture","outside-capture-context-unverified")}b(x,d.renderInfo);let I=null;if(E)try{let r=figma.getImageByHash(t.imageHash),n=r&&await Promise.race([r.getSizeAsync(),new Promise(t=>{e=setTimeout(()=>t(null),250)})]);I=n&&n.width>0&&n.height>0&&Number.isFinite(n.width)&&Number.isFinite(n.height)?{...n,aspectRatio:n.width/n.height}:null}catch(e){}finally{clearTimeout(e)}let S=null;if("auto"===h){let e=function(e,r,n){let i,a=[],o=(e,t)=>{e||a.push(t)},l=e=>Array.isArray(e)&&2===e.length&&e.every(e=>Array.isArray(e)&&3===e.length&&e.every(Number.isFinite))&&1===e[0][0]&&1===e[1][1]&&0===e[0][1]&&0===e[1][0]&&Number.isInteger(e[0][2])&&Number.isInteger(e[1][2]);for(let a of(o("bridge"===t.transport,"raw-requires-bridge"),o(1===t.scale&&["png","jpg"].includes(t.format),"raw-requires-unscaled-raster"),o(!!r&&!0===r.visible&&1===r.opacity&&"NORMAL"===r.blendMode,"paint-compositing-or-unknown"),o(r&&"FILL"===r.scaleMode&&Array.isArray(i=r.imageTransform)&&2===i.length&&i.every((e,t)=>Array.isArray(e)&&3===e.length&&e.every((e,r)=>e===[[1,0,0],[0,1,0]][t][r]))&&0===r.rotation,"paint-transform-or-unknown"),o(r&&r.filters&&Object.values(r.filters).every(e=>0===e),"image-filters-or-unknown"),o(Array.isArray(e.fills)&&1===e.fills.length,"multiple-fills-or-unknown"),o("RECTANGLE"===e.type&&0===e.cornerRadius&&Array.isArray(e.strokes)&&0===e.strokes.length&&(!e.children||0===e.children.length),"node-geometry-or-unknown"),o(Array.isArray(e.effects)&&0===e.effects.length&&!1===e.isMask&&(void 0===e.clipsContent||!1===e.clipsContent)&&1===e.opacity&&!0===e.visible&&"NORMAL"===e.blendMode,"node-effects-mask-compositing-or-unknown"),o(l(e.absoluteTransform)&&l(e.relativeTransform),"node-transform-or-unknown"),o(n.sourceProperties.ancestors.length>0&&n.sourceProperties.ancestors.some(e=>["PAGE","DOCUMENT"].includes(e.type)),"ancestor-chain-incomplete"),o(n.sourceProperties.ancestors.every(e=>!1===e.siblingMaskPresent),"ancestor-sibling-mask-or-unknown"),n.sourceProperties.ancestors.filter(e=>!["PAGE","DOCUMENT"].includes(e.type))))o(!1===a.clipsContent&&!1===a.isMask&&!1===a.siblingMaskPresent&&Array.isArray(a.effects)&&0===a.effects.length&&1===a.opacity&&!0===a.visible&&["NORMAL","PASS_THROUGH"].includes(a.blendMode),"ancestor-dependency-or-unknown");return{eligible:0===a.length,reasons:[...new Set(a)],evidence:"explicit-plugin-properties; intrinsic-dimensions-and-codec-required"}}(x,E,d.renderInfo);if(e.eligible)try{let r=figma.getImageByHash(t.imageHash),n=I;n&&n.width===x.width&&n.height===x.height?(S=await c(()=>r.getBytesAsync()),(137===S[0]&&80===S[1]&&78===S[2]&&71===S[3]?"png":255===S[0]&&216===S[1]&&255===S[2]?"jpg":"unknown")!==t.format&&e.reasons.push("source-codec-mismatch-or-unknown")):e.reasons.push("intrinsic-dimensions-mismatch-or-unknown")}catch(t){e.reasons.push("source-inspection-unavailable")}e.eligible=0===e.reasons.length,y=e.eligible?"fill":"node",d.renderInfo=m(x,E),b(x,d.renderInfo),d.renderInfo.autoDecision={...e,selected:y}}if(d.renderInfo.sourceSize=I,d.renderInfo.sourceSizeEvidence=I?"Figma-image-getSizeAsync":"unknown",g||"fill"===y){d.method="original-fill",o="reading the original image";let e=figma.getImageByHash(t.imageHash);if(!e)return p("DEVUP_ORIGINAL_IMAGE_NOT_FOUND");let n=S||await c(()=>e.getBytesAsync());if(0===n.length||n.length>0x5000000)return p("DEVUP_ASSET_RESPONSE_TOO_LARGE",`${n.length} bytes; one answer carries 1 to 83886080`);let i=e=>e.every((e,t)=>n[t]===e),a=i([137,80,78,71,13,10,26,10])?"image/png":i([255,216,255])?"image/jpeg":i([71,73,70,56])?"image/gif":i([82,73,70,70])&&87===n[8]&&69===n[9]&&66===n[10]&&80===n[11]?"image/webp":null;if(!a)return p("DEVUP_ORIGINAL_IMAGE_CODEC_UNSUPPORTED");if(d.renderInfo.originalMimeType=a,!g&&"fill"===h&&"webp"!==t.format&&a!==({png:"image/png",jpg:"image/jpeg"})[t.format])return p("DEVUP_ASSET_FILL_CODEC_MISMATCH",`Original fill codec is ${a}, but ${t.format} was requested; choose the matching format or renderMode node. No transcoding was performed.`);if(!g)return{...p(null),status:"exported",mimeType:a,byteLength:n.length,sha256:r(n),data:figma.base64Encode(n)};let l=await c(()=>e.getSizeAsync());return{...p(null),status:"exported",kind:"devupOriginalImage",representation:"original-image-v1",format:null,scale:null,mimeType:a,width:l.width,height:l.height,byteLength:n.length,sha256:r(n),data:figma.base64Encode(n)}}let w="webp"===t.format?"PNG":String(t.format||"").toUpperCase();if(!["PNG","JPG","SVG","PDF"].includes(w))return p("DEVUP_ASSET_FORMAT_UNSUPPORTED");let v=Number(t.scale??2);if(!Number.isFinite(v)||v<=0||v>4)return p("DEVUP_ASSET_SCALE_INVALID");let N="SVG"===w,O={format:N?"SVG_STRING":w,contentsOnly:!0,useAbsoluteBounds:!1};("PNG"===w||"JPG"===w)&&(O.constraint={type:"SCALE",value:v}),o="export";let T=Math.max(1,s-(Date.now()-a)),_=N?Math.max(1,T-Math.min(5e3,Math.floor(T/5))):T,P=Date.now(),M=await c(()=>"visible"===h?A.exportAsync(O):E?x.exportAsync(O):i(x,O,_,f));if(E&&f("native",Date.now()-P),"geometry"===d.method&&(d.renderInfo.representation="geometry-fallback",d.renderInfo.renderedAs="geometry-fallback",d.renderInfo.effectsBakedIntoAsset=!1,d.renderInfo.warnings.push("geometry-fallback-not-native-render")),Date.now()-a>=s)throw Error(`DEVUP_ASSET_TIMEOUT: export exceeded ${s} ms asset budget`);o="encode";let C=Date.now(),k=N&&"string"==typeof M?M:null,R=null===k?M instanceof Uint8Array?M:new Uint8Array(M):function(e){let t=[];for(let r=0;r=55296&&n<=56319){let t=r+1=56320&&t<=57343?(n=65536+(n-55296<<10)+(t-56320),r+=1):n=65533}else n>=56320&&n<=57343&&(n=65533);n<128?t.push(n):n<2048?t.push(192|n>>6,128|63&n):n<65536?t.push(224|n>>12,128|n>>6&63,128|63&n):t.push(240|n>>18,128|n>>12&63,128|n>>6&63,128|63&n)}return new Uint8Array(t)}(k);if(0===R.length||R.length>0x5000000)return p("DEVUP_ASSET_RESPONSE_TOO_LARGE",`${w} at scale ${v} is ${R.length} bytes; one answer carries 1 to 83886080`);let D=r(R);f("encode",Date.now()-C);let L=figma.io&&"function"==typeof figma.io.write,$="PNG"===w&&R.length>786432;if(L&&(null!==k&&R.length>12288||$))return{kind:"devupAssetExport",fileKey:figma.fileKey||"",version:t.version,assetId:t.assetId,nodeId:t.nodeId,field:t.field,imageHash:t.imageHash,format:t.format,scale:v,renderMode:h,usageAspectRatio:t.usageAspectRatio??null,maxWidth:t.maxWidth??null,srcset:!0===t.srcset,visibleScopeNodeId:t.visibleScopeNodeId??null,status:"chunked",byteLength:R.length,sha256:D,cursor:{nextOffset:0,maxChunkBytes:12288},errorCode:null,exportDiagnostics:d};return L&&figma.io.write(`devup-asset-${t.assetId.replace(/[^A-Za-z0-9_-]/g,"_")}.${w.toLowerCase()}`,R),{kind:"devupAssetExport",fileKey:figma.fileKey||"",version:t.version,assetId:t.assetId,nodeId:t.nodeId,field:t.field,imageHash:t.imageHash,format:t.format,scale:v,renderMode:h,usageAspectRatio:t.usageAspectRatio??null,maxWidth:t.maxWidth??null,srcset:!0===t.srcset,visibleScopeNodeId:t.visibleScopeNodeId??null,status:"exported",byteLength:R.length,sha256:D,mimeType:L?null===k?null:"image/svg+xml":({PNG:"image/png",JPG:"image/jpeg",SVG:"image/svg+xml",PDF:"application/pdf"})[w],...!L&&null===k?{data:figma.base64Encode(R)}:{},text:k,errorCode:null,exportDiagnostics:d}}catch(e){return p(String(e.message||e).includes("DEVUP_ASSET_TIMEOUT:")?"DEVUP_ASSET_TIMEOUT":g||["fill","original-source"].includes(h)?"DEVUP_ORIGINAL_IMAGE_READ_FAILED":"DEVUP_ASSET_EXPORT_FAILED",u(e))}},bridgeContext:async function(e){let t=figma.currentPage,r=t.selection,n=e=>({id:e.id,name:String(e.name).slice(0,256),type:e.type});return{kind:"devup-bridge-context",currentPage:n(t),selection:r.slice(0,20).map(n),selectionCount:r.length,zoom:Number.isFinite(figma.viewport?.zoom)?figma.viewport.zoom:null,observedAt:Date.now(),lastKnownSelection:globalThis.__devupLastSelection??null,lastKnownPage:globalThis.__devupLastSelectionPage??null}},explore:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=await figma.getNodeByIdAsync(e.nodeId);if(!t)throw Error("DEVUP_NODE_NOT_FOUND");let r=e.explore,n=Math.max(20,Math.min(400,Number.isInteger(r.projectionLimit)?r.projectionLimit:200)),i=Math.max(0,Math.min(500,Number.isInteger(r.textPreviewLimit)?r.textPreviewLimit:160));function a(e){return"string"==typeof e?e.slice(0,240):""}let o="PAGE"===t.type,l=t,s="SECTION"===t.type?t:null;for(;!o&&l.parent&&"PAGE"!==l.parent.type;)l=l.parent,s||"SECTION"!==l.type||(s=l);let d="PAGE"===t.type?t:l.parent;if(!d||"PAGE"!==d.type)throw Error("DEVUP_PAGE_NOT_FOUND");function f(e){let t=e.absoluteBoundingBox||{x:"number"==typeof e.x?e.x:0,y:"number"==typeof e.y?e.y:0,width:"number"==typeof e.width?e.width:0,height:"number"==typeof e.height?e.height:0};return[t.x,t.y,t.width,t.height].every(Number.isFinite)?{x:t.x,y:t.y,width:t.width,height:t.height}:null}function c(e){let t=f(e);return t&&["FRAME","COMPONENT","INSTANCE","COMPONENT_SET"].includes(e.type)&&t.width>=240&&t.width<=1800&&t.height>=300&&t.height<=2e3&&t.width/Math.max(1,t.height)>=.25&&t.width/Math.max(1,t.height)<=2.5}function u(e){let t=[],r=e;for(;r&&"DOCUMENT"!==r.type&&t.length<12;){let e=a(r.name);e&&t.push(e),r=r.parent}return t.reverse()}await figma.setCurrentPageAsync(d);let p=o?d:!c(t)&&s?s:l,g=f(p)||f(t);if(!g)throw Error("DEVUP_NODE_BOUNDS_UNAVAILABLE");let h="children"in d?d.children:[],y=h.map((e,t)=>({node:e,pageChildIndex:t,bounds:f(e)})).filter(e=>e.bounds).filter(e=>{var t;return!!o||e.node.id===p.id||e.bounds.y>=g.y-240&&e.bounds.y<=g.y+12e3&&(t=e.bounds,Math.min(t.x+t.width,g.x+g.width)-Math.max(t.x,g.x)>0)}).sort((e,t)=>e.bounds.y-t.bounds.y||e.bounds.x-t.bounds.x||e.node.id.localeCompare(t.node.id)),m="SECTION"===p.type?y.filter(e=>e.node.id===p.id):y.slice(0,n),b=new Map(m.map(e=>[e.node.id,e]));b.has(l.id)||b.set(l.id,{node:l,pageChildIndex:-1,bounds:f(l)}),b.has(p.id)||b.set(p.id,{node:p,pageChildIndex:-1,bounds:f(p)}),b.has(t.id)||b.set(t.id,{node:t,pageChildIndex:-1,bounds:f(t)});let x=o?null:t.parent;for(;x&&"PAGE"!==x.type&&(b.has(x.id)||b.set(x.id,{node:x,pageChildIndex:-1,bounds:f(x)}),x.id!==p.id);)x=x.parent;if("children"in p)for(let e of p.children.slice(0,n))b.has(e.id)||b.set(e.id,{node:e,pageChildIndex:-1,bounds:f(e)});let E="SECTION"===p.type||o,A=E&&"children"in p&&p.children.length>n;if(E&&"children"in p){let e=8*n,t=p.children.filter(e=>!c(e)).map(e=>({node:e,ancestors:[]})),r=0;for(;t.length&&r!b.has(e.id));if(b.size+t.length>n+2){A=!0;continue}for(let e of t)b.set(e.id,{node:e,pageChildIndex:-1,bounds:f(e)});continue}if("children"in e)for(let r of e.children)o&&["FRAME","COMPONENT","INSTANCE","COMPONENT_SET"].includes(e.type)||t.push({node:r,ancestors:[...i,e]})}A||=t.length>0}let I=[...b.values()].filter(e=>e.bounds).slice(0,n+2).map(({node:e,pageChildIndex:t,bounds:r})=>{let n;return{id:e.id,type:e.type,fields:{name:a(e.name),parentId:e.parent&&"DOCUMENT"!==e.parent.type?e.parent.id:null,childrenIds:[],x:r.x,y:r.y,width:r.width,height:r.height,childCount:"children"in e?e.children.length:0,textPreview:"",...o||null===(n=function(e){let t=[e],r=0,n=null;for(;t.length&&r++<400;){let e=t.shift();if("visible"in e&&!1===e.visible)continue;let r="children"in e?e.children.filter(e=>!("visible"in e)||!1!==e.visible):[];if(/header|nav|gnb/i.test(e.name||"")&&r.length>=2&&"layoutMode"in e&&"HORIZONTAL"===e.layoutMode){let t=r.map(f);t.every(Boolean)&&(n=Math.max(n||0,t.reduce((e,t)=>e+t.width,0)+(Number(e.itemSpacing)||0)*(t.length-1)))}t.push(...r.slice(0,80))}return n}(e))?{}:{navigationContentWidth:n},pageChildIndex:t>=0?t:null,visible:!("visible"in e)||!1!==e.visible,breadcrumb:u(e)},extra:{},fieldErrors:{}}}),S=I.reduce((e,t)=>{let r=t.fields;return e?{x:Math.min(e.x,r.x),y:Math.min(e.y,r.y),right:Math.max(e.right,r.x+r.width),bottom:Math.max(e.bottom,r.y+r.height)}:{x:r.x,y:r.y,right:r.x+r.width,bottom:r.y+r.height}},null)||{x:0,y:0,right:0,bottom:0},w="SECTION"!==p.type&&y.length>m.length||b.size>n+2||A,v={id:d.id,type:d.type,fields:{name:a(d.name),parentId:null,childrenIds:[],x:S.x,y:S.y,width:S.right-S.x,height:S.bottom-S.y,childCount:h.length,textPreview:"",projectionTruncated:w,visible:!0,breadcrumb:u(d),pageChildIndex:null},extra:{},fieldErrors:{}},N={fileKey:figma.fileKey||"",version:null,rootIds:[d.id],nodes:[v,...I.filter(e=>e.id!==d.id)],diagnostics:[]},O=new Set([d.id,l.id,p.id,t.id]);for(;JSON.stringify(N).length>14e3;){let e=N.nodes.length-1;for(;e>=0&&O.has(N.nodes[e].id);)e-=1;if(e<0)break;N.nodes.splice(e,1),v.fields.projectionTruncated=!0}if(JSON.stringify(N).length>14e3&&(N.nodes=N.nodes.filter(e=>O.has(e.id)).map(e=>({...e,fields:{...e.fields,name:a(e.fields.name).slice(0,80),textPreview:"",breadcrumb:e.fields.breadcrumb.slice(-4).map(e=>e.slice(0,80)),projectionTruncated:e.id===d.id||e.fields.projectionTruncated}}))),JSON.stringify(N).length>14e3)throw Error("DEVUP_EXPLORE_PROJECTION_TOO_LARGE");function T(e){let t=0;for(let r of e){let e=r.codePointAt(0);t+=e<128?1:e<2048?2:e<65536?3:4}return t}if(o){for(;T(JSON.stringify(N))>19456&&N.nodes.length>1;)N.nodes.pop(),v.fields.projectionTruncated=!0;if(T(JSON.stringify(N))>19456)throw Error("DEVUP_EXPLORE_PROJECTION_TOO_LARGE")}let _=14e3-JSON.stringify(N).length;if(o&&(_=Math.min(_,19456-T(JSON.stringify(N)))),i>0&&_>0){let e=N.nodes.filter(e=>b.get(e.id)&&e.id!==d.id),t=e.length;for(let r of e){let e=Math.floor(_/t);if(t-=1,e<=0)continue;let n="",a=0;for(let t of function(e){if(0===i)return"";let t=[],r=[e],n=0;for(;r.length&&n<80&&t.join(" ").lengthe)break;n+=t,a+=r}r.fields.textPreview=n,_-=a}}return N},fastSnapshot:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=e.rootIds;if(!Array.isArray(t)||0===t.length)throw Error("DEVUP_ROOTS_INVALID");let r=await Promise.all(t.map(e=>figma.getNodeByIdAsync(e)));if(r.some(e=>!e))throw Error("DEVUP_NODE_NOT_FOUND");if(1===r.length&&"SECTION"===r[0].type)throw Error("DEVUP_TARGET_IS_SECTION");let n=["mobile","tablet","desktop"],i=e=>{let t=String(e.name||"").trim().toLowerCase(),r=n.indexOf(t);if(r>=0)return r;let i=t.match(/^(\d+)\s*[~–]\s*(\d+)$/);if(!i)return -1;let a=Number(i[1]),o=Number(i[2]);return Number.isSafeInteger(a)&&Number.isSafeInteger(o)&&a>0&&o>=a&&o<=0xffffffff?1:-1};if(1===r.length&&i(r[0])>=0){let e=r[0].parent;if(e&&"SECTION"===e.type&&"children"in e){let t=e.children.filter(e=>e.id===r[0].id||i(e)>=0).filter(e=>!1!==e.visible);t.length>1&&(r.length=0,r.push(...t))}}if(1===t.length){let e=[],t=new Set(r.map(e=>e.id)),n=[],i=e=>{if("reactions"in e&&Array.isArray(e.reactions)){for(let r of e.reactions)if(r&&r.trigger&&"AFTER_TIMEOUT"===r.trigger.type)for(let e of r.actions||[])e&&"NODE"===e.type&&e.transition&&"SMART_ANIMATE"===e.transition.type&&"string"==typeof e.destinationId&&!t.has(e.destinationId)&&(t.add(e.destinationId),n.push(e.destinationId));if("children"in e)for(let t of e.children)i(t)}};for(let e of r)i(e);for(;n.length>0;){let t=n.shift(),r=await figma.getNodeByIdAsync(t);r&&"DOCUMENT"!==r.type&&"PAGE"!==r.type&&(e.push(r),i(r))}r.push(...e)}let a=e.nodeId,o=["absoluteBoundingBox","absoluteRenderBounds","arcData","backgroundStyleId","blendMode","bottomLeftRadius","bottomRightRadius","boundVariables","characters","clipsContent","componentProperties","componentPropertyDefinitions","componentPropertyReferences","constraints","cornerRadius","counterAxisAlignItems","dashPattern","defaultVariant","effectStyleId","effects","fillStyleId","fills","fontName","fontSize","gridColumnAnchorIndex","gridColumnCount","gridColumnGap","gridColumnSizes","gridColumnSpan","gridRowAnchorIndex","gridRowCount","gridRowGap","gridRowSizes","gridRowSpan","gridStyleId","height","inferredAutoLayout","isAsset","isMask","itemSpacing","layoutGrow","layoutMode","layoutWrap","layoutPositioning","layoutSizingHorizontal","layoutSizingVertical","letterSpacing","lineHeight","maxHeight","maxLines","maxWidth","minHeight","minWidth","name","opacity","overflowDirection","paddingBottom","paddingLeft","paddingRight","paddingTop","primaryAxisAlignItems","reactions","rotation","strokeAlign","strokeBottomWeight","strokeLeftWeight","strokeRightWeight","strokeStyleId","strokeTopWeight","strokeWeight","strokes","targetAspectRatio","textAlignHorizontal","textAlignVertical","textAutoResize","textCase","textDecoration","textStyleId","textTruncation","topLeftRadius","topRightRadius","variantProperties","visible","width","x","y"],l=["fontName","fontWeight","fontSize","textDecoration","textCase","lineHeight","letterSpacing","fills","textStyleId","fillStyleId","listOptions","indentation","hyperlink"],s=e.snapshot,d=Math.max(0,Math.floor(Number(s.offset)||0)),f=Math.max(8192,Math.floor(Number(s.maxEnvelopeBytes)||19456)),c=Math.min(s.maxEnvelopeBytes?f-1024:18e3,Math.max(4096,Math.floor(Number(s.maxPayloadBytes)||15e3))),u=new Set(["backgroundStyleId","effectStyleId","fillStyleId","gridStyleId","strokeStyleId","textStyleId"]),p=new Set(["maxWidth","maxHeight","absoluteRenderBounds"]),g=new Map([["rotation",0],["cornerRadius",0],["isAsset",!1],["isMask",!1],["clipsContent",!1],["blendMode","PASS_THROUGH"],["strokeAlign","INSIDE"],["textCase","ORIGINAL"],["textDecoration","NONE"],["textAlignHorizontal","LEFT"],["textAlignVertical","TOP"],["counterAxisAlignItems","MIN"],["primaryAxisAlignItems","MIN"],["gridColumnCount",0],["gridRowCount",0],["gridColumnGap",0],["gridRowGap",0],["gridColumnAnchorIndex",-1],["gridRowAnchorIndex",-1],["gridColumnSpan",1],["gridRowSpan",1]]),h=new Set(["start","end","characters","fontWeight","textStyleId","fillStyleId","listOptions","indentation","hyperlink"]),y=new Set(["parent","children","consumers"]);function m(e,t=!1,r=new WeakSet,n=0){let i;if(null===e||["string","number","boolean"].includes(typeof e))return e;if(void 0===e)return{$undefined:!0};if("bigint"==typeof e)return{$bigint:e.toString()};if(["function","symbol"].includes(typeof e))return{$unsupported:typeof e};if(n>12)return{$truncated:"max-depth"};if("object"==typeof e&&"parent"in e&&"string"==typeof e.id&&"string"==typeof e.type)return{$nodeId:e.id,$nodeType:e.type};if(Array.isArray(e))return e.map(e=>m(e,t,r,n+1));if(ArrayBuffer.isView(e))return{$binary:e.constructor.name,byteLength:e.byteLength};if(e instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:e.byteLength};if(r.has(e))return{$circular:!0};if(r.add(e),t){let t=new Set(Object.keys(e)),r=e;for(;r&&r!==Object.prototype;){for(let e of Object.getOwnPropertyNames(r))t.add(e);r=Object.getPrototypeOf(r)}i=[...t].sort().filter(e=>!e.startsWith("_")&&!y.has(e))}else i=Object.keys(e).sort();let a={};for(let o of i)try{let i=m(e[o],t,r,n+1);i&&"function"===i.$unsupported||(a[o]=i)}catch(e){a[o]=t?{$error:"unavailable"}:{$error:String(e&&e.message?e.message:e)}}return r.delete(e),a}let b=new Map;async function x(e,t,r){if("INSTANCE"!==e.type||"function"!=typeof e.getMainComponentAsync)return;b.has(e.id)||b.set(e.id,(async()=>{try{let t=await e.getMainComponentAsync();return{fields:{mainComponentId:t?t.id:null,mainComponentName:t?t.name:null,mainComponentSetId:t&&t.parent&&"COMPONENT_SET"===t.parent.type?t.parent.id:null}}}catch(e){return{error:String(e&&e.message?e.message:e)}}})());let n=await b.get(e.id);n.error?r.mainComponentId=n.error:Object.assign(t,n.fields)}async function E(e){let t={},r={};e.parent&&(t.parentId=e.parent.id),e.parent&&("PAGE"===e.parent.type||"SECTION"===e.parent.type||"COMPONENT_SET"===e.parent.type)&&(t.parentType=e.parent.type,"SECTION"===e.parent.type&&(t.parentName=e.parent.name));let n="children"in e?e.children.map(e=>e.id):[];for(let i of(n.length>0&&(t.childrenIds=n),o)){let n;if("componentPropertyDefinitions"!==i||"COMPONENT"!==e.type||!e.parent||"COMPONENT_SET"!==e.parent.type)try{if(!(i in e)){"overflowDirection"===i&&["FRAME","COMPONENT","INSTANCE","COMPONENT_SET"].includes(e.type)&&(t[i]=null);continue}if(n=e[i],"function"==typeof n)continue;let r=m(n);if("overflowDirection"===i&&null==n){t[i]=null;continue}(null===r?!p.has(i):Array.isArray(r)?0===r.length:"object"==typeof r?0===Object.keys(r).length:!!(""===r&&u.has(i))||g.has(i)&&g.get(i)===r)||(t[i]=r)}catch(e){r[i]=String(e&&e.message?e.message:e)}}if("TEXT"===e.type&&"function"==typeof e.getStyledTextSegments)try{let r=m(e.getStyledTextSegments(l));if(1===r.length){let e=r[0];for(let t of Object.keys(e))h.has(t)||delete e[t]}r.length>0&&(t.styledTextSegments=r)}catch(e){r.styledTextSegments=String(e&&e.message?e.message:e)}await x(e,t,r);let i={id:e.id,type:e.type,fields:t};return Object.keys(r).length>0&&(i.fieldErrors=r),i}let A=[],I=[...r],S=new Set;for(let e=0;e=A.length&&A.length>0)throw Error("DEVUP_SNAPSHOT_RANGE_INVALID");function w(e){let t=0;for(let r=0;r=55296&&n<=56319&&r+1({id:e,styleType:t})).sort((e,t)=>e.id.localeCompare(t.id));async function a(e){try{let t=await figma.variables.getVariableByIdAsync(e);return t?{kind:"variable",value:m(t,!0),collectionId:t.variableCollectionId,aliasIds:Object.values(t.valuesByMode||{}).filter(e=>e&&"VARIABLE_ALIAS"===e.type&&"string"==typeof e.id&&e.id).map(e=>e.id)}:{kind:"unresolved",value:{id:e,kind:"variable",reason:"notFoundOrUnavailable"}}}catch(t){return{kind:"unresolved",value:{id:e,kind:"variable",reason:"notFoundOrUnavailable",error:String(t&&t.message||t).slice(0,300)}}}}let o=await Promise.all([...n.map(a),...i.map(async({id:e,styleType:t})=>{try{let r=await figma.getStyleByIdAsync(e);if(!r)return{kind:"unresolved",value:{id:e,kind:"style",reason:"notFoundOrUnavailable"}};return{kind:"style",value:{...m(r,!0),styleType:t,value:m("PAINT"===t?r.paints:"EFFECT"===t?r.effects:"GRID"===t?r.layoutGrids:r,!0)}}}catch(t){return{kind:"unresolved",value:{id:e,kind:"style",reason:"notFoundOrUnavailable",error:String(t&&t.message||t).slice(0,300)}}}})]),l=new Set(n),s=o.flatMap(e=>e.aliasIds||[]);for(;s.length>0;){let e=[...new Set(s)].filter(e=>!l.has(e)).sort();for(let t of e)l.add(t);let t=await Promise.all(e.map(a));o.push(...t),s=t.flatMap(e=>e.aliasIds||[])}let d=[...new Set(o.filter(e=>"variable"===e.kind&&e.collectionId).map(e=>e.collectionId))].sort(),f=(await Promise.all(d.map(async e=>{try{let t=await figma.variables.getVariableCollectionByIdAsync(e);return t?m(t,!0):null}catch(e){return null}}))).filter(e=>null!==e),c=o.filter(e=>"variable"===e.kind).map(e=>e.value),u=o.filter(e=>"style"===e.kind).map(e=>e.value),p=o.filter(e=>"unresolved"===e.kind).map(e=>e.value);return{collections:f,variables:c,styles:u,usedRemoteVariables:c.filter(e=>!0===e.remote),usedVariableIds:n,usedStyleIds:i.map(e=>e.id),localComplete:!1,usedRemoteComplete:0===p.length,unresolved:p,$variableRefCount:n.length,$styleRefCount:i.length}}async function N(e){let t=[],r=2;for(let n=d;ne)break;t.push(i),r+=a}return{pageNodes:t,packedBytes:r}}let O=c-1024,T=null;for(let e=0;e<5;e+=1){let{pageNodes:e,packedBytes:t}=await N(O);if(0===e.length)throw Error("DEVUP_SNAPSHOT_RANGE_INVALID");let n=function(e,t){let n=Math.min(A.length,d+e.length),{$variableRefCount:i,$styleRefCount:o,...l}=t,s=[...e,{id:"__DEVUP_SNAPSHOT_CURSOR__",type:"DEVUP_INTERNAL",fields:{offset:d,nextOffset:n,complete:n>=A.length,totalNodes:A.length},extra:{},fieldErrors:{}}],f={kind:"devupFastSnapshotEnvelope",schemaVersion:1,source:{fileKey:figma.fileKey||"",rootId:a},snapshot:{fileKey:figma.fileKey||"",version:null,rootIds:r.map(e=>e.id),nodes:s,diagnostics:[]},resources:l,integrity:{nodeCount:s.length,variableRefCount:i,styleRefCount:o,utf8Bytes:0}},c=0;for(let e=0;e<8&&(c=w(JSON.stringify(f)),f.integrity.utf8Bytes!==c);e+=1)f.integrity.utf8Bytes=c;if(f.integrity.utf8Bytes!==w(JSON.stringify(f)))throw Error("DEVUP_ENVELOPE_LENGTH_UNSTABLE");return{envelope:f,bytes:c}}(e,await v(e));if(n.bytes<=f){T=n;break}if(1===e.length)throw Error("DEVUP_ENVELOPE_TOO_LARGE");O=Math.max(1,Math.min(O-1,t-(n.bytes-f)-256))}if(!T)throw Error("DEVUP_ENVELOPE_TOO_LARGE");return T.envelope},fastTheme:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=Math.max(0,Math.floor(Number(e.theme.offset)||0));function r(e,t=new WeakSet,n=0){if(null===e||["string","number","boolean"].includes(typeof e))return e;if(void 0===e)return{$undefined:!0};if("bigint"==typeof e)return{$bigint:e.toString()};if(["function","symbol"].includes(typeof e))return{$unsupported:typeof e};if(n>12)return{$truncated:"max-depth"};if("object"==typeof e&&"parent"in e&&"string"==typeof e.id&&"string"==typeof e.type)return{$nodeId:e.id,$nodeType:e.type};if(Array.isArray(e))return e.map(e=>r(e,t,n+1));if(ArrayBuffer.isView(e))return{$binary:e.constructor.name,byteLength:e.byteLength};if(e instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:e.byteLength};if(t.has(e))return{$circular:!0};t.add(e);let i={};for(let a of function(e){let t=new Set(Object.keys(e)),r=e;for(;r&&r!==Object.prototype;){for(let e of Object.getOwnPropertyNames(r))t.add(e);r=Object.getPrototypeOf(r)}return[...t].sort()}(e))if(!(a.startsWith("_")||["parent","children","consumers"].includes(a)))try{let o=r(e[a],t,n+1);o&&"function"===o.$unsupported||(i[a]=o)}catch(e){i[a]={$error:"unavailable"}}return t.delete(e),i}let n=new Set,i=new Map;for(let e of figma.root.children)await e.loadAsync();for(let e of[figma.root,...figma.root.findAll(()=>!0)])for(let t of["boundVariables","fills","strokes","effects","layoutGrids","textStyleId","fillStyleId","strokeStyleId","backgroundStyleId","effectStyleId","gridStyleId"])try{!function e(t,r="",a=new WeakSet,o=0){if(!(o>16)&&null!==t&&"object"==typeof t&&!a.has(t)){if(a.add(t),Array.isArray(t)){for(let n of t)e(n,r,a,o+1);return}for(let[l,s]of("VARIABLE_ALIAS"===t.type&&"string"==typeof t.id&&t.id&&"figma.mixed"!==t.id&&"MIXED"!==t.id&&n.add(t.id),Object.entries(t))){let t="textStyleId"===l?"TEXT":["fillStyleId","strokeStyleId","backgroundStyleId"].includes(l)?"PAINT":"effectStyleId"===l?"EFFECT":"gridStyleId"===l?"GRID":null;t&&"string"==typeof s&&s&&"figma.mixed"!==s&&"MIXED"!==s&&!i.has(s)&&i.set(s,t),e(s,l||r,a,o+1)}}}({[t]:e[t]})}catch(e){}let[a,o,l,s,d,f]=await Promise.all([figma.variables.getLocalVariableCollectionsAsync(),figma.variables.getLocalVariablesAsync(),figma.getLocalPaintStylesAsync(),figma.getLocalTextStylesAsync(),figma.getLocalEffectStylesAsync(),figma.getLocalGridStylesAsync()]),c=new Set(o.map(e=>e.id)),u=new Set([...l,...s,...d,...f].map(e=>e.id)),p=[];async function g(e){try{return await figma.variables.getVariableByIdAsync(e)||null}catch(t){return p.push({id:e,kind:"variable",reason:"notFoundOrUnavailable",error:String(t&&t.message||t).slice(0,300)}),null}}function h(e){return Object.values(e.valuesByMode||{}).filter(e=>e&&"VARIABLE_ALIAS"===e.type&&"string"==typeof e.id&&e.id).map(e=>e.id)}let y=[...i.entries()].filter(([e])=>!u.has(e)).sort(([e],[t])=>e.localeCompare(t)).map(async([e,t])=>{try{let r=await figma.getStyleByIdAsync(e);return r?{style:r,styleType:t}:null}catch(t){return p.push({id:e,kind:"style",reason:"notFoundOrUnavailable",error:String(t&&t.message||t).slice(0,300)}),null}}),m=new Set,b=[],x=[...n,...o.flatMap(h)];for(;x.length>0;){let e=[...new Set(x)].filter(e=>!c.has(e)&&!m.has(e)).sort();for(let t of e)m.add(t);let t=(await Promise.all(e.map(g))).filter(Boolean);b.push(...t),x=t.flatMap(h)}let E=(await Promise.all(y)).filter(Boolean);for(let e of m)b.some(t=>t.id===e)||p.some(t=>t.id===e)||p.push({id:e,kind:"variable",reason:"notFoundOrUnavailable"});for(let[e]of[...i.entries()].filter(([e])=>!u.has(e)))E.some(t=>t.style.id===e)||p.some(t=>t.id===e)||p.push({id:e,kind:"style",reason:"notFoundOrUnavailable"});let A=[...new Set(b.map(e=>e.variableCollectionId))].filter(e=>!a.some(t=>t.id===e)).sort(),I=(await Promise.all(A.map(async e=>{try{return await figma.variables.getVariableCollectionByIdAsync(e)}catch(e){return null}}))).filter(Boolean);function S(e,t){return{...r(e),styleType:t,value:r("PAINT"===t?e.paints:"EFFECT"===t?e.effects:"GRID"===t?e.layoutGrids:e)}}let w=[...l.map(e=>S(e,"PAINT")),...s.map(e=>S(e,"TEXT")),...d.map(e=>S(e,"EFFECT")),...f.map(e=>S(e,"GRID")),...E.map(({style:e,styleType:t})=>S(e,t))].sort((e,t)=>e.id.localeCompare(t.id)),v=[...o,...b].map(e=>r(e)).sort((e,t)=>e.id.localeCompare(t.id)),N=[...a,...I].map(e=>r(e)).sort((e,t)=>e.id.localeCompare(t.id));function O(e){let t=[];for(let r=0;r=55296&&n<=56319){let t=r+1=56320&&t<=57343?(n=65536+(n-55296<<10)+(t-56320),r+=1):n=65533}else n>=56320&&n<=57343&&(n=65533);n<128?t.push(n):n<2048?t.push(192|n>>6,128|63&n):n<65536?t.push(224|n>>12,128|n>>6&63,128|63&n):t.push(240|n>>18,128|n>>12&63,128|n>>6&63,128|63&n)}return new Uint8Array(t)}p.sort((e,t)=>e.kind.localeCompare(t.kind)||e.id.localeCompare(t.id));let T=[...N.map(e=>({kind:"collection",value:e})),...v.map(e=>({kind:"variable",value:e})),...w.map(e=>({kind:"style",value:e})),...[...n].sort().map(e=>({kind:"usedVariableId",value:e})),...[...i.keys()].sort().map(e=>({kind:"usedStyleId",value:e})),...p.map(e=>({kind:"unresolved",value:e}))];if(t>T.length)throw Error("DEVUP_SNAPSHOT_RANGE_INVALID");let _=T.map(e=>O(JSON.stringify(e.value)).length+1),P=18432,M=null;for(let e=0;e<6;e+=1){let{pageItems:e,packed:r}=function(e){let r=[],n=0;for(let i=t;i0)||!(n+_[i]>e));i+=1)r.push(T[i]),n+=_[i];return{pageItems:r,packed:n}}(P),n=function(e,r){let n=t=>e.filter(e=>e.kind===t).map(e=>e.value),i=n("collection"),a=n("variable"),o=n("style"),l=n("unresolved"),s={kind:"devupFastThemeEnvelope",schemaVersion:1,source:{fileKey:figma.fileKey||"",version:null},resources:{collections:i,variables:a,styles:o,usedRemoteVariables:[],usedVariableIds:n("usedVariableId"),usedStyleIds:n("usedStyleId"),localComplete:!0,usedRemoteComplete:0===p.length,unresolved:l},page:{offset:t,nextOffset:r,complete:r>=T.length,totalItems:T.length},integrity:{collectionCount:i.length,variableCount:a.length,styleCount:o.length,unresolvedCount:l.length,utf8Bytes:0}},d=new Uint8Array;for(let e=0;e<8&&(d=O(JSON.stringify(s)),s.integrity.utf8Bytes!==d.length);e+=1)s.integrity.utf8Bytes=d.length;if(d=O(JSON.stringify(s)),s.integrity.utf8Bytes!==d.length)throw Error("DEVUP_ENVELOPE_LENGTH_UNSTABLE");return{envelope:s,bytes:d.length}}(e,t+e.length);if(n.bytes<=19456){M=n;break}if(e.length<=1)break;P=Math.max(1,Math.min(P-1,r-(n.bytes-19456)-256))}if(null===M||M.bytes>8388608)throw Error("DEVUP_ENVELOPE_TOO_LARGE");return M.envelope},largeValue:async function(e){let t;figma.skipInvisibleInstanceChildren=!1;let r=e.largeValue;async function n(e){try{let t=await figma.getNodeByIdAsync(e);if(t)return t}catch(r){if(!e.startsWith("I")||!e.includes(";"))throw r;try{return await t()}catch(e){throw Error(`${r.message||r}; nested lookup fallback: ${e.message||e}`)}}if(!e.startsWith("I")||!e.includes(";"))return null;return t();async function t(){let t=await figma.getNodeByIdAsync(e.slice(1).split(";")[0]),r=t?[t]:[],n=new Set;for(;r.length;){let t=r.pop();if(t.id===e)return t;if(!n.has(t.id)){if(n.add(t.id),n.size>1e4)throw Error("nested lookup exceeds 10000 nodes");Array.isArray(t.children)&&r.push(...t.children)}}throw Error(`nested node ${e} not found in its instance`)}}async function i(e,t,r=85e3,n=()=>{}){let a,o=Date.now();try{let i=await Promise.race([e.exportAsync(t),new Promise((e,t)=>{a=setTimeout(()=>t(Error(`DEVUP_ASSET_TIMEOUT: native export exceeded ${r} ms`)),r)})]);return n("native-export",Date.now()-o),i}catch(i){if(n("native-export",Date.now()-o,String(i.message||i).slice(0,500)),"SVG_STRING"!==t.format)throw i;let r=Date.now();try{let t=function(e){let t=e=>"number"==typeof e&&Number.isFinite(e),r=(e,t)=>{throw Error(`geometry fallback unsupported: ${e.type||"unknown"} ${e.id||""}: ${t}`)},n=0,i=0;function a(e,i,a){(void 0!==a&&!Array.isArray(a)||void 0===a&&Array.isArray(i)&&i.length)&&r(e,"unresolved paint requires native export");let o=Array.isArray(a)?a.filter(e=>!1!==e.visible):[];return o.length?(Array.isArray(i)&&i.length||r(e,"paint has no rendered geometry"),o.map(a=>{("SOLID"!==a.type||a.blendMode&&"NORMAL"!==a.blendMode)&&r(e,"paint type or blending requires native export");let o=a.color;o&&[o.r,o.g,o.b].every(e=>t(e)&&e>=0&&e<=1)||r(e,"invalid solid color");let l=void 0===a.opacity?1:a.opacity;return(!t(l)||l<0||l>1)&&r(e,"invalid paint opacity"),["NONZERO","EVENODD"].map(t=>{let a=i.filter(e=>e.windingRule===t);if(!a.length)return"";for(let t of a)"string"==typeof t.data&&t.data.trim()||r(e,"empty path"),n++;return`"']/g,e=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[e])}" fill="rgb(${100*o.r}%,${100*o.g}%,${100*o.b}%)" fill-opacity="${l}" fill-rule="${"EVENODD"===t?"evenodd":"nonzero"}"/>`}).join("")}).join("")):""}let o=e.absoluteRenderBounds,l=e.absoluteTransform;(!o||![o.x,o.y,o.width,o.height].every(t)||o.width<=0||o.height<=0)&&r(e,"missing render bounds"),(!Array.isArray(l)||2!==l.length||!l.every(e=>Array.isArray(e)&&3===e.length&&e.every(t))||0!==l[0][1]||0!==l[1][0]||l[0][0]<=0||l[1][1]<=0)&&r(e,"root rotation, reflection or shear requires native export");let s=(o.x-l[0][2])/l[0][0],d=(o.y-l[1][2])/l[1][1],f=o.width/l[0][0],c=o.height/l[1][1],u=function e(n,o){if(++i>1e4&&r(n,"subtree exceeds 10000 nodes"),n.isMask&&r(n,"mask requires native export"),!1===n.visible||0===n.opacity)return"";n.clipsContent&&r(n,"clipping requires native export"),Array.isArray(n.effects)&&n.effects.some(e=>!1!==e.visible)&&r(n,"effect requires native export"),n.blendMode&&!["NORMAL","PASS_THROUGH"].includes(n.blendMode)&&r(n,"layer blending requires native export");let l=void 0===n.opacity?1:n.opacity;(!t(l)||l<0||l>1)&&r(n,"invalid layer opacity");let s="";if(!o){let e=n.relativeTransform;Array.isArray(e)&&2===e.length&&e.every(e=>Array.isArray(e)&&3===e.length&&e.every(t))||r(n,"missing relative transform"),s=` transform="matrix(${e[0][0]} ${e[1][0]} ${e[0][1]} ${e[1][1]} ${e[0][2]} ${e[1][2]})"`}let d="";if(["GROUP","FRAME","INSTANCE","COMPONENT"].includes(n.type))(Array.isArray(n.fills)&&n.fills.some(e=>!1!==e.visible)||Array.isArray(n.strokes)&&n.strokes.some(e=>!1!==e.visible))&&r(n,"container paint requires native export"),Array.isArray(n.children)||r(n,"missing children"),d=(!0===n.itemReverseZIndex?[...n.children].reverse():n.children).map(t=>e(t,!1)).join("");else{for(let e of(["VECTOR","BOOLEAN_OPERATION","RECTANGLE","ELLIPSE","POLYGON","STAR","LINE"].includes(n.type)||r(n,"node type requires native export"),Array.isArray(n.strokes)&&n.strokes.some(e=>!1!==e.visible)&&"CENTER"!==n.strokeAlign&&r(n,"non-center stroke alignment requires native export"),Array.isArray(n.dashPattern)&&n.dashPattern.length&&r(n,"dashed stroke requires native export"),[n.fillGeometry,n.strokeGeometry]))void 0!==e&&(!Array.isArray(e)||e.some(e=>!["NONZERO","EVENODD"].includes(e.windingRule)))&&r(n,"unknown winding rule");d=a(n,n.fillGeometry,n.fills)+a(n,n.strokeGeometry,n.strokes)}return`${d}`}(e,!0);return n||r(e,"no painted geometry"),`${u}`}(e);return n("geometry-fallback",Date.now()-r),t}catch(e){throw Error(`native export: ${i.message||i}; ${e.message||e}`)}}finally{clearTimeout(a)}}let a=await n(r.nodeId);if(!a)throw Error("DEVUP_NODE_NOT_FOUND");function o(e){let t=[];for(let r=0;r=55296&&n<=56319){let t=r+1=56320&&t<=57343?(n=65536+(n-55296<<10)+(t-56320),r+=1):n=65533}else n>=56320&&n<=57343&&(n=65533);n<128?t.push(n):n<2048?t.push(192|n>>6,128|63&n):n<65536?t.push(224|n>>12,128|n>>6&63,128|63&n):t.push(240|n>>18,128|n>>12&63,128|n>>6&63,128|63&n)}return new Uint8Array(t)}let l=null;try{if("$export:svg"===r.field){if("function"!=typeof a.exportAsync)throw Error("unsupported");let e=await i(a,{format:"SVG_STRING"},Math.max(1,(Number(r.timeoutMs)||9e4)-5e3));if("string"!=typeof e)throw Error("unsupported");l=o(e)}else if("string"==typeof r.field&&r.field.startsWith("$export:png")){if("function"!=typeof a.exportAsync)throw Error("unsupported");let e=Number(r.field.split("@")[1]||2);if(!Number.isFinite(e)||e<=0||e>4)throw Error("invalid scale");let t=r.field.split("@"),n=a;if("visible"===t[2]){let e=new Set;for(;n&&n.id!==t[3]&&!e.has(n)&&e.size<64;)e.add(n),n=n.parent;if(!n||n.id!==t[3]||["PAGE","DOCUMENT"].includes(n.type))throw Error("invalid visible scope")}else if(t.length>2)throw Error("invalid export field");let i=await n.exportAsync({format:"PNG",contentsOnly:!0,useAbsoluteBounds:!1,constraint:{type:"SCALE",value:e}});l=i instanceof Uint8Array?i:new Uint8Array(i)}else if("styledTextSegments"===r.field&&"TEXT"===a.type&&"function"==typeof a.getStyledTextSegments)t=a.getStyledTextSegments(["fontName","fontWeight","fontSize","textDecoration","textCase","lineHeight","letterSpacing","fills","textStyleId","fillStyleId","listOptions","indentation","hyperlink"]);else if(r.field in a)t=a[r.field];else throw Error("unsupported")}catch(e){return{kind:"devupLargeValueUnsupported",fileKey:figma.fileKey||"",version:r.version,nodeId:r.nodeId,field:r.field,byteLength:r.byteLength,sha256:r.sha256,errorCode:"DEVUP_FIELD_UNSUPPORTED_BY_UPSTREAM",errorDetail:`${r.field} failed: ${e.message||e}`.slice(0,500)}}let s=null===l?o(JSON.stringify(function e(t,r=new WeakSet,n=0){if(null===t||["string","number","boolean"].includes(typeof t))return t;if(void 0===t)return{$undefined:!0};if("bigint"==typeof t)return{$bigint:t.toString()};if(["function","symbol"].includes(typeof t))return{$unsupported:typeof t};if(n>12)return{$truncated:"max-depth"};if("object"==typeof t&&"parent"in t&&"string"==typeof t.id&&"string"==typeof t.type)return{$nodeId:t.id,$nodeType:t.type};if(Array.isArray(t))return t.map(t=>e(t,r,n+1));if(ArrayBuffer.isView(t))return{$binary:t.constructor.name,byteLength:t.byteLength};if(t instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:t.byteLength};if(r.has(t))return{$circular:!0};r.add(t);let i={};for(let a of Object.keys(t).sort())try{let o=e(t[a],r,n+1);o&&"function"===o.$unsupported||(i[a]=o)}catch(e){i[a]={$error:String(e&&e.message?e.message:e)}}return r.delete(t),i}(t))):l,d=function(e){let t=[0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,0xe49b69c1,0xefbe4786,0xfc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x6ca6351,0x14292967,0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2],r=e.length,n=64*Math.ceil((r+9)/64),i=new Uint8Array(n);i.set(e),i[r]=128;let a=8*r;for(let e=0;e<8;e+=1)i[n-1-e]=255&Math.floor(a/2**(8*e));let o=[0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19],l=(e,t)=>e>>>t|e<<32-t;for(let e=0;e>>3,n=l(r[e-2],17)^l(r[e-2],19)^r[e-2]>>>10;r[e]=r[e-16]+t+r[e-7]+n>>>0}let[n,a,s,d,f,c,u,p]=o;for(let e=0;e<64;e+=1){let i=p+(l(f,6)^l(f,11)^l(f,25))+(f&c^~f&u)+t[e]+r[e]>>>0,o=(l(n,2)^l(n,13)^l(n,22))+(n&a^n&s^a&s)>>>0;p=u,u=c,c=f,f=d+i>>>0,d=s,s=a,a=n,n=i+o>>>0}for(let[e,t]of[n,a,s,d,f,c,u,p].entries())o[e]=o[e]+t>>>0}return o.map(e=>e.toString(16).padStart(8,"0")).join("")}(s);if(s.length!==r.byteLength||d!==r.sha256)throw Error("DEVUP_LARGE_VALUE_CHANGED");let f=Math.max(0,Math.floor(Number(r.offset)||0)),c=Math.min(65536,Math.max(1,Math.floor(Number(r.maxChunkBytes)||8192)));if(f>=s.length)throw Error("DEVUP_LARGE_VALUE_RANGE_INVALID");let u=Math.min(s.length,f+c);return{kind:"devupLargeValueFragment",fileKey:figma.fileKey||"",version:r.version,nodeId:r.nodeId,field:r.field,offset:f,nextOffset:u,byteLength:s.length,sha256:d,dataBase64:function(e){let t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",r="";for(let n=0;n>2],r+=t[(3&i)<<4|a>>4],r+=n+1>6]:"=",r+=n+210||t.some(e=>"string"!=typeof e||e.length>128||!/^(?:[0-9]+:[0-9]+|I[0-9]+:[0-9]+(?:;[0-9]+:[0-9]+)+)$/.test(e))||new Set(t).size!==t.length)throw Error("DEVUP_INVALID_INPUT: nodeIds must be 1..10 distinct Figma IDs");let r=(e,t)=>{try{let r=e[t];return"number"==typeof r&&Number.isFinite(r)?r:null}catch{return null}},n=(e,t)=>{try{let r=e[t];return"string"==typeof r&&r.length<=32&&/^[A-Z_]+$/.test(r)?r:null}catch{return null}},i=[];for(let e of t)try{let t=await figma.getNodeByIdAsync(e);if(!t||"DOCUMENT"===t.type||"PAGE"===t.type){i.push({nodeId:e,status:"unavailable",reason:t?"not-scene-node":"not-found"});continue}let a={width:r(t,"width"),height:r(t,"height"),padding:{top:r(t,"paddingTop"),right:r(t,"paddingRight"),bottom:r(t,"paddingBottom"),left:r(t,"paddingLeft")},gap:r(t,"itemSpacing"),layoutMode:n(t,"layoutMode"),align:{primary:n(t,"primaryAxisAlignItems"),counter:n(t,"counterAxisAlignItems")}},o=null!==a.width&&null!==a.height;i.push({nodeId:e,status:o?"measured":"partial",...a})}catch{i.push({nodeId:e,status:"unavailable",reason:"read-failed"})}return{kind:"devup-measure",schemaVersion:1,coordinateSpace:"figma-logical",nodes:i,partial:i.some(e=>"measured"!==e.status),evidenceLimit:"Null means unavailable or not applicable; no screenshot dimensions, descendants, theme, assets or code were read."}},metadata:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=await figma.getNodeByIdAsync(e.nodeId);if(!t)throw Error("DEVUP_NODE_NOT_FOUND");let r=t;for(;r&&"PAGE"!==r.type;)r=r.parent;r&&"PAGE"===r.type&&await figma.setCurrentPageAsync(r);let n=[];return!function(e){let t=[{node:e,visited:!1}],r=new Map;for(;t.length>0;){let e=t[t.length-1],i=e.node,a="children"in i?i.children:[];if(!e.visited){e.visited=!0;for(let e=a.length-1;e>=0;e-=1)t.push({node:a[e],visited:!1});continue}t.pop();let o=0,l=[];for(let e of a)l.push(e.id),o+=1+(r.get(e.id)||0);r.set(i.id,o),n.push({id:i.id,type:i.type,name:i.name,childrenIds:l,descendantCount:o})}}(t),{fileKey:figma.fileKey||"",version:null,rootId:t.id,nodes:n}},pageCatalog:async function(e){let t=figma.root.children;return{fileKey:figma.fileKey||"",version:null,rootIds:t.map(e=>e.id),nodes:t.map(e=>({id:e.id,type:e.type,fields:{name:e.name,parentId:null,childrenIds:[]},extra:{},fieldErrors:{}})),diagnostics:[]}},referencePng:async function(e){let t,r=e.nodeId;globalThis.__devupReferencePngAnswers||(globalThis.__devupReferencePngAnswers=new Map);let n=globalThis.__devupReferencePngAnswers,i=0;for(let[e,t]of n)Date.now()-t.at>12e4?n.delete(e):i+=t.text.length;if(i>=0xc000000)throw Error(`DEVUP_REFERENCE_PNG_BUSY ${i} characters are held for other reads`);let a=await figma.getNodeByIdAsync(r);if(!a||"function"!=typeof a.exportAsync)throw Error(`DEVUP_NODE_NOT_FOUND ${r}`);let o=a.absoluteRenderBounds||a.absoluteBoundingBox,l=Math.max(o.width,o.height),s=l>8192?8192/l:1,d=(e,t)=>({format:"PNG",constraint:t&&s<1?{type:o.width>=o.height?"WIDTH":"HEIGHT",value:8192}:{type:"SCALE",value:s},contentsOnly:e,useAbsoluteBounds:!1,colorProfile:"SRGB"}),f=e=>(Array.isArray(e)?e:[]).filter(e=>e&&!1!==e.visible),c=(e,t)=>!!e&&!!t&&e.x!!e&&e.x+t<=o.x&&e.y+t<=o.y&&e.x+e.width-t>=o.x+o.width&&e.y+e.height-t>=o.y+o.height,p=e=>`${e.type} ${JSON.stringify(e.name)}`,g=e=>JSON.parse(JSON.stringify(e===figma.mixed?null:e)),h=e=>(void 0===e.opacity||1===e.opacity)&&(!e.blendMode||["PASS_THROUGH","NORMAL"].includes(e.blendMode)),y=[],m=a.parent;for(;m&&"PAGE"!==m.type;)y.unshift(m),m=m.parent;let b=[255,255,255];for(let e of f(m&&m.backgrounds)){if("SOLID"!==e.type)continue;let t=void 0===e.opacity?1:e.opacity;["r","g","b"].forEach((r,n)=>{b[n]=t*e.color[r]*255+(1-t)*b[n]})}let x=0,E=e=>!1!==e.visible&&!e.isMask&&!!c(e.absoluteRenderBounds,o)&&((x+=1)>5e3||"TEXT"===e.type||!!f(e.fills).length||!!f(e.strokes).length||!!f(e.effects).length||"children"in e&&e.children.some(E)),A=e=>h(e)&&u(e.absoluteBoundingBox,0)&&1e-9>Math.abs(e.absoluteTransform[0][1])&&1e-9>Math.abs(e.absoluteTransform[1][0])&&0===Math.max(0,...[e.topLeftRadius,e.topRightRadius,e.bottomRightRadius,e.bottomLeftRadius].filter(e=>"number"==typeof e))&&f(e.fills).some(e=>"NORMAL"===(e.blendMode||"NORMAL")&&(void 0===e.opacity||1===e.opacity)&&("SOLID"===e.type||e.type.startsWith("GRADIENT_")&&e.gradientStops.every(e=>1===e.color.a))),I=[...y,a],S=[],w=[],v=new Map;for(let e=0;eA(e)&&i.slice(0,t).every(h));r.forEach((e,r)=>{e===t||(e.isMask&&!1!==e.visible&&r{let t=await a.exportAsync(d(!1,!0));return{kind:"devupReferencePng",nodeId:r,mimeType:"image/png",byteLength:t.length,data:figma.base64Encode(t),backdrop:N,...e}},T=(e,t)=>{let r=[...new Set(e)];return{approximate:r.join("; "),note:`Drawn as the canvas shows this area: devup-mcp cannot yet compose what get_screenshot draws here (${r.slice(0,3).join("; ")}). Layers reaching into it may show here: ${t}; get_screenshot leaves other layers out.`}},_=e=>{let t=JSON.stringify(e).replace(/[\u0080-\uffff]/g,e=>`\\u${e.charCodeAt(0).toString(16).padStart(4,"0")}`);if(t.length>0x5a00000){if(e.compose){let{compose:r,reaching:n,...i}=e;return _({...i,...T([`the answer would be ${t.length} characters`],n)})}throw Error(`DEVUP_REFERENCE_PNG_TOO_LARGE ${t.length} characters, over the 94371840 one answer is read back up to`)}if(t.length<=6291456)return e;let i=`${r}@${Date.now()}.${Math.random().toString(36).slice(2,10)}`;return n.set(i,{text:t,at:Date.now()}),{kind:"devupReferencePngChunked",nodeId:r,id:i,length:t.length}};if(0===S.length)return _(await O({}));let P=I.some(e=>e.isMask),M=[],C=new Set,k=(e,t)=>{let r=p(e);for(let t of f(e.fills))"IMAGE"===t.type?((t.rotation||f([t.filters]).some(e=>Object.values(e).some(e=>e)))&&M.push(`${r} has an image fill with a rotation or filters`),C.add(t.imageHash)):"SOLID"===t.type||t.type.startsWith("GRADIENT_")||M.push(`${r} is painted with ${t.type}`);for(let t of f(e.effects))["LAYER_BLUR","DROP_SHADOW","INNER_SHADOW","BACKGROUND_BLUR"].includes(t.type)||M.push(`${r} has a ${t.type}`);let n=f(e.strokes);if((e=>{let t=0===f(e.strokes).length?[]:[e.strokeWeight,e.strokeTopWeight,e.strokeRightWeight,e.strokeBottomWeight,e.strokeLeftWeight].filter(e=>"number"==typeof e&&e>0);if(0===t.length)return!1;let r=Math.max(...t),n="OUTSIDE"===e.strokeAlign?0:"CENTER"===e.strokeAlign?r/2:r,i="INSIDE"===e.strokeAlign?0:"CENTER"===e.strokeAlign?r/2:r,a=e.absoluteBoundingBox;return c(a&&{x:a.x-i,y:a.y-i,width:a.width+2*i,height:a.height+2*i},o)&&!u(a,n)})(e)){let t=["FRAME","RECTANGLE","COMPONENT","COMPONENT_SET","INSTANCE","SECTION"].includes(e.type),i=n.every(e=>"SOLID"===e.type||e.type.startsWith("GRADIENT_")),a=Array.isArray(e.dashPattern)&&e.dashPattern.length>0,o="INSIDE"===e.strokeAlign||!e.strokeJoin||"MITER"===e.strokeJoin;t&&i&&!a&&o||M.push(`${r}'s stroke reaches it`)}return{transform:g(e.absoluteTransform),width:e.width,height:e.height,radii:[e.topLeftRadius,e.topRightRadius,e.bottomRightRadius,e.bottomLeftRadius].map(e=>"number"==typeof e?e:0),fills:g(f(e.fills)),opacity:void 0===e.opacity?1:e.opacity,blendMode:e.blendMode||null,clips:!!e.clipsContent,effects:g(f(e.effects)),strokes:g(n),strokeAlign:e.strokeAlign||null,strokeWeights:["strokeTopWeight","strokeRightWeight","strokeBottomWeight","strokeLeftWeight"].map(t=>"number"==typeof e[t]?e[t]:"number"==typeof e.strokeWeight?e.strokeWeight:0)}},R=async e=>{let t=await e.exportAsync(d(!0,e===a));return{data:figma.base64Encode(t),area:e.absoluteRenderBounds||e.absoluteBoundingBox,blendMode:e.blendMode||null,size:[16,20].map(e=>(t[e]<<24|t[e+1]<<16|t[e+2]<<8|t[e+3])>>>0)}},D=async e=>{let t=v.get(e);if(!t)return null;let r=await t.exportAsync({...d(!0,!1),useAbsoluteBounds:!0});return{data:figma.base64Encode(r),area:t.absoluteBoundingBox,luminance:"LUMINANCE"===t.maskType}},L=[],$={},U={};try{for(let e of y){let t=k(e,!1),r=await D(e);r&&(t.mask=r),L.push(t)}let e=e=>f(e.effects).some(e=>"BACKGROUND_BLUR"===e.type);if(w.length>0&&(e(a)||"findOne"in a&&a.findOne(e))){let r="children"in a&&a.children.some(t=>e(t)||"findOne"in t&&!!t.findOne(e));if("children"in a&&!r&&!f(a.strokes).length){for(let e of($.own=k(a,!0),$.children=[],a.children)){if(!1===e.visible||e.isMask||!e.absoluteRenderBounds)continue;let t=await R(e);$.children.push({data:t.data,area:t.area,blendMode:t.blendMode})}t=[Math.ceil(o.width*s-1e-6),Math.ceil(o.height*s-1e-6)]}else M.push(`${p(a)} blurs what lies behind it`)}else if(P)t=[Math.ceil(o.width*s-1e-6),Math.ceil(o.height*s-1e-6)];else{let e=await R(a);$.picture={data:e.data,area:e.area,blendMode:e.blendMode},t=e.size}let r=await D(a);r&&($.mask=r),t&&t[0]*t[1]>8e6&&M.push(`the area is ${t[0]}x${t[1]} pixels`);let n=globalThis.__devupNormalizeImage,i=Date.now()+2e4,l=0;for(let e of C){let t=e&&figma.getImageByHash(e),r=t&&await t.getBytesAsync();if(!r){M.push(`the image ${e} could not be read`);continue}let a=null;if("function"==typeof n){let{width:e,height:o}=await t.getSizeAsync(),l=(...e)=>e.every((e,t)=>r[t]===e),s=l(137,80,78,71)||l(255,216)||l(71,73,70),d=Math.min(1,8192/Math.max(e,o,1)),f=Math.ceil(e*d)*Math.ceil(o*d),c=5*f+65536,u=5*r.length+4*e*o*(d<1?2:1)+4*f+4*c+1048576,p=i-Date.now();if((!s||Math.max(e,o)>8192||r.length>4*e*o+1048576)&&u<=0x20000000&&p>0){let t=await n(r,8192,p,e,o);t&&(r=t,a={width:e,height:o})}}if(l+r.length>0x4000000){M.push("its ancestors' images are over 67108864 bytes");break}l+=r.length,U[e]=a?{data:figma.base64Encode(r),...a}:figma.base64Encode(r)}}catch(e){M.push(`a part could not be exported: ${e instanceof Error?e.message:String(e)}`)}let B=`${S.slice(0,5).join(", ")}${S.length>5?` and ${S.length-5} more`:""}`;return M.length>0?_(await O(T(M,B))):_(await O({reaching:B,compose:{area:o,scale:s,width:t[0],height:t[1],page:b,layers:L,node:$,images:U}}))},referencePngChunk:async function(e){let t=e.largeValue,r=globalThis.__devupReferencePngAnswers,n=r&&r.get(t.id);if(!n||"string"!=typeof n.text)throw Error(`DEVUP_REFERENCE_PNG_ANSWER_GONE ${t.id}`);let i=n.text,a=Math.max(0,Math.floor(Number(t.offset)||0)),o=Math.min(i.length,a+4194304);return o>=i.length&&r.delete(t.id),{kind:"devupReferencePngChunk",id:t.id,offset:a,nextOffset:o,complete:o>=i.length,text:i.slice(a,o)}},search:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=await figma.getNodeByIdAsync(e.nodeId);if(!t)throw Error("DEVUP_NODE_NOT_FOUND");let r=t;for(;r&&"PAGE"!==r.type;)r=r.parent;if(!r||"PAGE"!==r.type)throw Error("DEVUP_PAGE_NOT_FOUND");await r.loadAsync();let n=e.search,i=new Set((n.nodeTypes.length?n.nodeTypes:["PAGE","SECTION","FRAME","COMPONENT_SET","COMPONENT"]).map(e=>e.toUpperCase()));function a(e){return e.normalize("NFC").toLocaleLowerCase().replace(/\s/gu,"")}function o(e){for(let t=e;t&&"DOCUMENT"!==t.type;t=t.parent){if(!1===t.visible||0===t.opacity)return!0;let e=(t.name||"").toLowerCase();if(["trash","archive","draft","copy","hidden","휴지통","보관","초안","복사","pdf","저장용","export","출력"].some(t=>e.includes(t)))return!0}return!1}let l=t===r?r.findAll(()=>!0):"findAll"in t?t.findAll(()=>!0):[],s=[t,...l].filter(e=>i.has(e.type)&&"string"==typeof e.name).map(e=>({node:e,score:function(e){let t=n.query.trim();if(""===t||"*"===t)return 100;if(e===n.query)return 400;if("exact"===n.matchKind)return null;let r=a(e),i=a(n.query);if(r===i)return 300;if(r.startsWith(i))return 200;if(r.includes(i))return 100;if("fuzzy"!==n.matchKind)return null;let o=function(e,t){let r=Array.from({length:t.length+1},(e,t)=>t);for(let n=0;nnull!==e.score).sort((e,t)=>Number(o(e.node))-Number(o(t.node))||t.score-e.score||e.node.name.localeCompare(t.node.name)||e.node.id.localeCompare(t.node.id)),d=s.slice(0,n.limit),f=new Map([[r.id,r]]);for(let e of[t,...d.map(e=>e.node)]){let t=e;for(;t&&"DOCUMENT"!==t.type;)f.set(t.id,t),t=t.parent}return{fileKey:figma.fileKey||"",version:null,rootIds:[r.id],nodes:[...f.values()].map(e=>({id:e.id,type:e.type,fields:{name:e.name,visible:!1!==e.visible,opacity:"number"==typeof e.opacity?e.opacity:1,searchCandidatesTruncated:e===t&&s.length>d.length,parentId:e.parent&&"DOCUMENT"!==e.parent.type?e.parent.id:null,childrenIds:"children"in e?e.children.map(e=>e.id):[]},extra:{},fieldErrors:{}})),diagnostics:[]}},sectionIndex:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=await figma.getNodeByIdAsync(e.nodeId);if(!t)throw Error("DEVUP_NODE_NOT_FOUND");if("SECTION"!==t.type){let e=t.parent,r=new Set;for(;e&&"SECTION"!==e.type&&!r.has(e.id);)r.add(e.id),e=e.parent;let n=e&&"SECTION"===e.type?e.id:null,i=n&&figma.fileKey?`https://www.figma.com/design/${figma.fileKey}?node-id=${n.replace(/:/g,"-")}`:null;throw Error("DEVUP_SECTION_REQUIRED "+JSON.stringify({pluginCode:"DEVUP_SECTION_REQUIRED",stage:"section-index",nodeId:t.id,nodeType:t.type,sectionId:n,nextAction:{tool:"devup_figma_export",how:n?`The url must point to a SECTION. This capture's SECTION is ${n}; select frames with frameIds.`:"The url must point to a SECTION. This node has no ancestor SECTION; choose the intended SECTION in Figma and copy its link.",arguments:i?"FRAME"===t.type&&t.parent===e?{url:i,frameIds:[t.id]}:{url:i}:null,requiredArguments:i?[]:["url"]}}))}let r=2048;function n(e){let t=e.absoluteBoundingBox||{x:"number"==typeof e.x?e.x:0,y:"number"==typeof e.y?e.y:0,width:"number"==typeof e.width?e.width:0,height:"number"==typeof e.height?e.height:0};return[t.x,t.y,t.width,t.height].every(Number.isFinite)?{x:t.x,y:t.y,width:t.width,height:t.height}:null}function i(e,t){if("FRAME"!==e.type||!1===e.visible||!t)return!1;let r=t.width/Math.max(1,t.height);return t.width>=240&&t.width<=1800&&t.height>=300&&t.height<=2e3&&r>=.25&&r<=2.5}function a(e){let t=0;for(let r=0;r=55296&&n<=56319&&r+1[e.id,t.id])),s=[],d=0;for(let e=0;ee.id));for(let r of t.children){if(e.has(r.id))continue;let t=n(r);t&&!1!==r.visible&&s.push({node:r,box:t})}}s.sort((e,t)=>e.box.y-t.box.y||e.box.x-t.box.x||e.node.id.localeCompare(t.node.id));let f=o.length>d||s.length>100,c=s.slice(0,100),u=new Set(c.map(({node:e})=>e.id)),p=new Set(e.rootIds),g={},h=new Map([[t.id,null]]);for(let e=0;e{let r=e.parent;for(;r&&r.id!==t.id&&!u.has(r.id);)r=r.parent;return[e.id,r&&u.has(r.id)?r.id:t.id]})),m=e=>c.filter(({node:t})=>y.get(t.id)===e).map(({node:e})=>e.id),b=n(t);if(!b)throw Error("DEVUP_NODE_BOUNDS_UNAVAILABLE");let x={id:t.id,type:t.type,fields:{name:t.name,parentId:t.parent&&"DOCUMENT"!==t.parent.type?t.parent.id:null,childrenIds:m(t.id),absoluteBoundingBox:b,visible:!1!==t.visible,projectionTruncated:f,nodeScreenIds:g},extra:{},fieldErrors:{}},E=c.length,A=c.map(({node:e,box:t})=>{let o,l=function(e){let t=[e],r=0,n=0;for(let e=0;er}}(e),s=Math.floor(r/E);E-=1;let d=function(e,t){let r=[e],n="",i=0,o=0,l=!1;if(t<=0)return{text:"",state:"budget-exhausted",spent:0};for(let e=0;e=120||o+e>t)return{text:n.trimEnd(),state:o+e>t?"budget-exhausted":"truncated",spent:o};n+=r,i+=1,o+=e}}if("children"in s){let e=Math.max(0,64-r.length);s.children.length>e&&(l=!0),r.push(...s.children.slice(0,e))}}}return{text:n,state:l?"truncated":n?"available":"no-text",spent:o}}(e,s);return r-=d.spent,{id:e.id,type:e.type,fields:{name:"string"==typeof e.name?e.name:"",parentId:y.get(e.id),childrenIds:m(e.id),absoluteBoundingBox:t,visible:!1!==e.visible,breadcrumb:function(e){let t=[],r=e;for(;r&&"DOCUMENT"!==r.type;)"string"==typeof r.name&&r.name&&t.push(r.name),r=r.parent;return t.reverse()}(e),directChildCount:"children"in e?e.children.length:0,textPreview:d.text,...null===(o=function(e){let t=[e],r=0,i=null;for(;t.length&&r++<400;){let e=t.shift();if("visible"in e&&!1===e.visible)continue;let r="children"in e?e.children.filter(e=>!("visible"in e)||!1!==e.visible):[];if(/header|nav|gnb/i.test(e.name||"")&&r.length>=2&&"layoutMode"in e&&"HORIZONTAL"===e.layoutMode){let t=r.map(n);t.every(Boolean)&&(i=Math.max(i||0,t.reduce((e,t)=>e+t.width,0)+(Number(e.itemSpacing)||0)*(t.length-1)))}t.push(...r.slice(0,80))}return i}(e))?{}:{navigationContentWidth:o},textPreviewState:d.state,subtreeNodeCount:l.subtreeNodeCount,estimatedSerializedBytes:l.estimatedSerializedBytes,selectionReasons:[i(e,t)?"screen-like":"explicit-selection-only","inside-section"],estimateTruncated:l.truncated},extra:{},fieldErrors:{}}}),I={fileKey:figma.fileKey||"",version:null,rootIds:[t.id],nodes:[x,...A],diagnostics:[]},S=a(JSON.stringify(I));for(let e=A.length-1;e>=0&&S>19456;e-=1)A[e].fields.textPreview="",A[e].fields.textPreviewState="budget-exhausted",S=a(JSON.stringify(I));let w=()=>I.nodes.length-1,v=A.length;for(;S>19456&&w()>1;){let e=I.nodes.pop();x.fields.projectionTruncated=!0,x.fields.childrenIds=x.fields.childrenIds.filter(t=>t!==e.id),S=a(JSON.stringify(I))}if(S>19456)throw Error("DEVUP_SECTION_INDEX_TOO_LARGE "+JSON.stringify({pluginCode:"DEVUP_SECTION_INDEX_TOO_LARGE",stage:"section-index",responseBytes:S,maxResponseBytes:19456,discoveredCandidates:v,retainedCandidates:w()}));return I},snapshot:async function(e){figma.skipInvisibleInstanceChildren=!1;let t=await figma.getNodeByIdAsync(e.nodeId);if(!t)throw Error("DEVUP_NODE_NOT_FOUND");let r=["absoluteBoundingBox","absoluteRenderBounds","arcData","backgroundStyleId","blendMode","bottomLeftRadius","bottomRightRadius","boundVariables","characters","clipsContent","componentProperties","componentPropertyDefinitions","componentPropertyReferences","constraints","cornerRadius","counterAxisAlignItems","dashPattern","defaultVariant","effectStyleId","effects","fillStyleId","fills","fontName","fontSize","gridColumnAnchorIndex","gridColumnCount","gridColumnGap","gridColumnSizes","gridColumnSpan","gridRowAnchorIndex","gridRowCount","gridRowGap","gridRowSizes","gridRowSpan","gridStyleId","height","inferredAutoLayout","isAsset","isMask","itemSpacing","layoutGrow","layoutMode","layoutWrap","layoutPositioning","layoutSizingHorizontal","layoutSizingVertical","letterSpacing","lineHeight","maxHeight","maxLines","maxWidth","minHeight","minWidth","name","opacity","overflowDirection","paddingBottom","paddingLeft","paddingRight","paddingTop","primaryAxisAlignItems","reactions","rotation","strokeAlign","strokeBottomWeight","strokeLeftWeight","strokeRightWeight","strokeStyleId","strokeTopWeight","strokeWeight","strokes","targetAspectRatio","textAlignHorizontal","textAlignVertical","textAutoResize","textCase","textDecoration","textStyleId","textTruncation","topLeftRadius","topRightRadius","variantProperties","visible","width","x","y"],n=new Set(r),i=["fontName","fontWeight","fontSize","textDecoration","textCase","lineHeight","letterSpacing","fills","textStyleId","fillStyleId","listOptions","indentation","hyperlink"],a=e.snapshot,o=Math.max(0,Math.floor(Number(a.offset)||0)),l=Math.min(a.maxEnvelopeBytes?Math.max(8192,Math.floor(Number(a.maxEnvelopeBytes))):16e3,Math.max(4096,Math.floor(Number(a.maxPayloadBytes)||15e3))),s=Math.min(l-1024,Math.max(512,Math.floor(Number(a.maxFieldBytes)||4096))),d=new Set(["id","type","parent","children","mainComponent","masterComponent","instances"]),f=new Set(["parentId","childrenIds","name","characters","styledTextSegments","boundVariables"]);function c(e,t,r){let n=function(e){let t=[];for(let r=0;r=55296&&n<=56319){let t=r+1=56320&&t<=57343?(n=65536+(n-55296<<10)+(t-56320),r+=1):n=65533}else n>=56320&&n<=57343&&(n=65533);n<128?t.push(n):n<2048?t.push(192|n>>6,128|63&n):n<65536?t.push(224|n>>12,128|n>>6&63,128|63&n):t.push(240|n>>18,128|n>>12&63,128|n>>6&63,128|63&n)}return new Uint8Array(t)}(JSON.stringify(r));return n.length>0x1000000?{$truncated:"max-large-value-bytes",byteLength:n.length}:{$largeValue:{nodeId:e,field:t,byteLength:n.length,sha256:function(e){let t=[0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,0xe49b69c1,0xefbe4786,0xfc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x6ca6351,0x14292967,0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2],r=64*Math.ceil((e.length+9)/64),n=new Uint8Array(r);n.set(e),n[e.length]=128;let i=8*e.length;for(let e=0;e<8;e+=1)n[r-1-e]=255&Math.floor(i/2**(8*e));let a=[0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19],o=(e,t)=>e>>>t|e<<32-t;for(let e=0;e>>3,n=o(r[e-2],17)^o(r[e-2],19)^r[e-2]>>>10;r[e]=r[e-16]+t+r[e-7]+n>>>0}let[i,l,s,d,f,c,u,p]=a;for(let e=0;e<64;e+=1){let n=p+(o(f,6)^o(f,11)^o(f,25))+(f&c^~f&u)+t[e]+r[e]>>>0,a=(o(i,2)^o(i,13)^o(i,22))+(i&l^i&s^l&s)>>>0;p=u,u=c,c=f,f=d+n>>>0,d=s,s=l,l=i,i=n+a>>>0}for(let[e,t]of[i,l,s,d,f,c,u,p].entries())a[e]=a[e]+t>>>0}return a.map(e=>e.toString(16).padStart(8,"0")).join("")}(n),cursor:{nextOffset:0,maxChunkBytes:12288}}}}function u(e){return function(e){let t=0;for(let r=0;r=55296&&n<=56319&&r+112)return{$truncated:"max-depth"};if("object"==typeof t&&"parent"in t&&"string"==typeof t.id&&"string"==typeof t.type)return{$nodeId:t.id,$nodeType:t.type};if(Array.isArray(t))return t.map(t=>e(t,r,n+1));if(ArrayBuffer.isView(t))return{$binary:t.constructor.name,byteLength:t.byteLength};if(t instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:t.byteLength};if(r.has(t))return{$circular:!0};r.add(t);let i={};for(let a of Object.keys(t).sort())try{let o=e(t[a],r,n+1);o&&"function"===o.$unsupported||(i[a]=o)}catch(e){i[a]={$error:String(e&&e.message?e.message:e)}}return r.delete(t),i}(r),a=u(i);if(a<=s)return i;let o=c(e,t,i);return o.$truncated&&(n[t]=`DEVUP_FIELD_VALUE_UNSUPPORTED:${a}>16777216`),o}let g=new Map;async function h(e,t,r){if("INSTANCE"!==e.type||"function"!=typeof e.getMainComponentAsync)return;g.has(e.id)||g.set(e.id,(async()=>{try{let t=await e.getMainComponentAsync();return{fields:{mainComponentId:t?t.id:null,mainComponentName:t?t.name:null,mainComponentSetId:t&&t.parent&&"COMPONENT_SET"===t.parent.type?t.parent.id:null}}}catch(e){return{error:String(e&&e.message?e.message:e)}}})());let n=await g.get(e.id);n.error?r.mainComponentId=n.error:Object.assign(t,n.fields)}async function y(e){let t={},a={},o={};for(let i of(t.parentId=e.parent?e.parent.id:null,e.parent&&("PAGE"===e.parent.type||"SECTION"===e.parent.type||"COMPONENT_SET"===e.parent.type)&&(t.parentType=e.parent.type,"SECTION"===e.parent.type&&(t.parentName=e.parent.name)),t.childrenIds="children"in e?e.children.map(e=>e.id):[],!("overflowDirection"in e)&&["FRAME","COMPONENT","INSTANCE","COMPONENT_SET"].includes(e.type)&&(t.overflowDirection=null),function(e){let t=new Set,n=e;for(;n&&n!==Object.prototype;){for(let e of Object.getOwnPropertyNames(n))t.add(e);n=Object.getPrototypeOf(n)}for(let n of r)try{n in e&&t.add(n)}catch(e){}return[...t].sort()}(e)))if(!(d.has(i)||i.startsWith("_")||"componentPropertyDefinitions"===i&&"COMPONENT"===e.type&&e.parent&&"COMPONENT_SET"===e.parent.type))try{let r=e[i];if("function"==typeof r)continue;let l=p(e.id,i,r,o);(n.has(i)?t:a)[i]=l}catch(e){o[i]=String(e&&e.message?e.message:e)}if("TEXT"===e.type&&"function"==typeof e.getStyledTextSegments)try{t.styledTextSegments=p(e.id,"styledTextSegments",e.getStyledTextSegments(i),o)}catch(e){o.styledTextSegments=String(e&&e.message?e.message:e)}return await h(e,t,o),{id:e.id,type:e.type,fields:t,extra:a,fieldErrors:o}}let m=[],b=[t];for(;b.length;){let e=b.shift();m.push(e),"children"in e&&b.push(...e.children)}let x=[],E=l-1024,A=2;for(let e=o;eNumber(e.protected)-Number(t.protected)||t.byteLength-e.byteLength),n)){if(r<=t)break;let n=c(e.id,i.name,e[i.sectionName][i.name]);e[i.sectionName][i.name]=n,n.$truncated&&(e.fieldErrors[i.name]=`DEVUP_FIELD_VALUE_UNSUPPORTED:${i.byteLength}>16777216`),r=u(e)}return e}(await y(m[e]),E),r=u(t)+ +!!x.length;if(x.length&&A+r>E)break;x.push(t),A+=r}let I=Math.min(m.length,o+x.length);return x.push({id:"__DEVUP_SNAPSHOT_CURSOR__",type:"DEVUP_INTERNAL",fields:{offset:o,nextOffset:I,complete:I>=m.length,totalNodes:m.length},extra:{},fieldErrors:{}}),{fileKey:figma.fileKey||"",version:null,rootIds:[t.id],nodes:x,diagnostics:[]}},usedResources:async function(e){let t=e.resources;function r(e,t=new WeakSet,n=0){if(null===e||["string","number","boolean"].includes(typeof e))return e;if(void 0===e)return{$undefined:!0};if("bigint"==typeof e)return{$bigint:e.toString()};if(["function","symbol"].includes(typeof e))return{$unsupported:typeof e};if(n>12)return{$truncated:"max-depth"};if("object"==typeof e&&"parent"in e&&"string"==typeof e.id&&"string"==typeof e.type)return{$nodeId:e.id,$nodeType:e.type};if(Array.isArray(e))return e.map(e=>r(e,t,n+1));if(ArrayBuffer.isView(e))return{$binary:e.constructor.name,byteLength:e.byteLength};if(e instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:e.byteLength};if(t.has(e))return{$circular:!0};t.add(e);let i={},a=new Set(Object.keys(e)),o=e;for(;o&&o!==Object.prototype;){for(let e of Object.getOwnPropertyNames(o))a.add(e);o=Object.getPrototypeOf(o)}for(let o of[...a].sort())if(!(o.startsWith("_")||["parent","children","consumers"].includes(o)))try{let a=r(e[o],t,n+1);a&&"function"===a.$unsupported||(i[o]=a)}catch(e){i[o]={$error:"unavailable"}}return t.delete(e),i}let n=new Map,i="ok";try{for(let e of(await figma.variables.getLocalVariablesAsync()))n.set(e.id,e)}catch(e){i="unavailable"}function a(e){return Object.values(e.valuesByMode||{}).filter(e=>e&&"VARIABLE_ALIAS"===e.type&&"string"==typeof e.id&&e.id).map(e=>e.id)}async function o(e){let t=n.get(e);if(t)return{value:r(t),collectionId:t.variableCollectionId,aliasIds:a(t)};try{let t=await figma.variables.getVariableByIdAsync(e);return t?{value:r(t),collectionId:t.variableCollectionId,aliasIds:a(t)}:{unresolved:{id:e,kind:"variable",reason:"notInFileAndLookupEmpty"}}}catch(t){return{unresolved:{id:e,kind:"variable",reason:"notInFileAndLookupThrew",error:String(t&&t.message||t).slice(0,300)}}}}let l=await Promise.all(t.variableIds.map(o)),s=new Set(t.variableIds),d=l.flatMap(e=>e.aliasIds||[]);for(;d.length>0;){let e=[...new Set(d)].filter(e=>!s.has(e)).sort();for(let t of e)s.add(t);let t=await Promise.all(e.map(o));l.push(...t),d=t.flatMap(e=>e.aliasIds||[])}let f=[...new Set(l.flatMap(e=>e.collectionId?[e.collectionId]:[]))].sort(),c=await Promise.all(f.map(async e=>{try{let t=await figma.variables.getVariableCollectionByIdAsync(e);return t?[r(t)]:[]}catch(e){return[]}})),u=await Promise.all(t.styles.map(async e=>{try{let t=await figma.getStyleByIdAsync(e.id);if(!t)return{unresolved:{id:e.id,kind:"style",reason:"notFoundOrUnavailable"}};return{value:{...r(t),styleType:e.styleType,value:r("PAINT"===e.styleType?t.paints:"EFFECT"===e.styleType?t.effects:"GRID"===e.styleType?t.layoutGrids:t)}}}catch(t){return{unresolved:{id:e.id,kind:"style",reason:"notFoundOrUnavailable",error:String(t&&t.message||t).slice(0,300)}}}}));return{collections:c.flat(),variables:l.flatMap(e=>e.value?[e.value]:[]),styles:u.flatMap(e=>e.value?[e.value]:[]),usedVariableIds:t.variableIds,usedStyleIds:t.styles.map(e=>e.id),localVariableListing:i,localVariableCount:n.size,unresolved:[...l,...u].flatMap(e=>e.unresolved?[e.unresolved]:[])}},variableCatalog:async function(e){let[t,r,n,i,a]=await Promise.all([figma.variables.getLocalVariableCollectionsAsync(),figma.getLocalPaintStylesAsync(),figma.getLocalTextStylesAsync(),figma.getLocalEffectStylesAsync(),figma.getLocalGridStylesAsync()]),o=["PAINT","TEXT","EFFECT","GRID"];return{collections:t.map(e=>(function e(t,r=new WeakSet,n=0){if(null===t||["string","number","boolean"].includes(typeof t))return t;if(void 0===t)return{$undefined:!0};if("bigint"==typeof t)return{$bigint:t.toString()};if(["function","symbol"].includes(typeof t))return{$unsupported:typeof t};if(n>12)return{$truncated:"max-depth"};if(Array.isArray(t))return t.map(t=>e(t,r,n+1));if(r.has(t))return{$circular:!0};r.add(t);let i={},a=new Set(Object.keys(t)),o=t;for(;o&&o!==Object.prototype;){for(let e of Object.getOwnPropertyNames(o))a.add(e);o=Object.getPrototypeOf(o)}for(let o of[...a].sort())if(!o.startsWith("_"))try{let a=e(t[o],r,n+1);a&&"function"===a.$unsupported||(i[o]=a)}catch(e){i[o]={$error:String(e&&e.message?e.message:e)}}return r.delete(t),i})(e)),variableIds:[...new Set(t.flatMap(e=>e.variableIds))].sort(),styles:[r,n,i,a].flatMap((e,t)=>e.map(e=>({id:e.id,styleType:o[t]}))).sort((e,t)=>e.id.localeCompare(t.id)),localComplete:!0,usedRemoteComplete:!1}},variables:async function(e){let t=e.resources;function r(e,t=new WeakSet,n=0){if(null===e||["string","number","boolean"].includes(typeof e))return e;if(void 0===e)return{$undefined:!0};if("bigint"==typeof e)return{$bigint:e.toString()};if(["function","symbol"].includes(typeof e))return{$unsupported:typeof e};if(n>12)return{$truncated:"max-depth"};if("object"==typeof e&&"parent"in e&&"string"==typeof e.id&&"string"==typeof e.type)return{$nodeId:e.id,$nodeType:e.type};if(Array.isArray(e))return e.map(e=>r(e,t,n+1));if(ArrayBuffer.isView(e))return{$binary:e.constructor.name,byteLength:e.byteLength};if(e instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:e.byteLength};if(t.has(e))return{$circular:!0};t.add(e);let i={},a=new Set(Object.keys(e)),o=e;for(;o&&o!==Object.prototype;){for(let e of Object.getOwnPropertyNames(o))a.add(e);o=Object.getPrototypeOf(o)}for(let o of[...a].sort())if(!(o.startsWith("_")||["parent","children","consumers"].includes(o)))try{let a=r(e[o],t,n+1);a&&"function"===a.$unsupported||(i[o]=a)}catch(e){i[o]={$error:String(e&&e.message?e.message:e)}}return t.delete(e),i}let[n,i]=await Promise.all([Promise.all(t.variableIds.map(e=>figma.variables.getVariableByIdAsync(e))),Promise.all(t.styles.map(e=>figma.getStyleByIdAsync(e.id)))]),a=new Map(t.styles.map(e=>[e.id,e])),o=await Promise.all(i.filter(Boolean).map(async e=>{let t=a.get(e.id),n=t.styleType;if(Number.isInteger(t.consumerStart)&&Number.isInteger(t.consumerEnd)){let i=await e.getStyleConsumersAsync();return{id:e.id,styleType:n,$consumerStart:t.consumerStart,$consumerEntries:i.slice(t.consumerStart,t.consumerEnd).map(e=>[e.node.id,e.node.type,r(e.fields)])}}let i=await e.getStyleConsumersAsync();return{...r(e),styleType:n,$consumerCount:i.length,value:r("PAINT"===n?e.paints:"EFFECT"===n?e.effects:"GRID"===n?e.layoutGrids:e)}}));return{variables:n.filter(Boolean).map(e=>r(e)),styles:o}}},t=new Map,r=0;globalThis.__devupNormalizeImage=(e,n,i,a,o)=>new Promise(l=>{r+=1;let s=`decode-${r}`,d=setTimeout(()=>{t.delete(s),l(null)},i);t.set(s,e=>{clearTimeout(d),t.delete(s),l(e)}),figma.ui.postMessage({kind:"devup-decode",id:s,bytes:e,maxEdge:n,width:a,height:o})});let n=globalThis;function i(){let e=figma.currentPage,t=e.selection;return t.length>0&&(n.__devupLastSelection=t.slice(0,20).map(e=>({id:e.id,name:e.name.slice(0,256),type:e.type})),n.__devupLastSelectionPage={id:e.id,name:e.name.slice(0,256)}),{zoom:Number.isFinite(figma.viewport?.zoom)?figma.viewport.zoom:null,observedAt:Date.now(),lastKnownSelection:n.__devupLastSelection??null,lastKnownPage:n.__devupLastSelectionPage??null,currentPage:{id:e.id,name:e.name.slice(0,256)},selection:t.slice(0,20).map(e=>({id:e.id,name:e.name.slice(0,256),type:e.type})),selectionCount:t.length}}function a(){let e={kind:"devup-context",...i()};figma.ui.postMessage(e)}async function o(t){let r=e[t.script];if(!r)return{kind:"devup-result",requestId:t.requestId,error:`DEVUP_BRIDGE_UNKNOWN_SCRIPT: ${t.script} (아는 스크립트: ${Object.keys(e).join(", ")})`};try{var n;"bridgeContext"===t.script&&i();let e=await r({nodeId:(n=t.params).nodeId??"",rootIds:n.rootIds??[],snapshot:n.snapshot??{},search:n.search??{},explore:n.explore??{},resources:n.resources??{variableIds:[],styles:[]},largeValue:n.largeValue??{},asset:n.asset??{},theme:n.theme??{offset:0}});return{kind:"devup-result",requestId:t.requestId,data:e}}catch(e){return{kind:"devup-result",requestId:t.requestId,error:e instanceof Error?e.message:String(e)}}}let l=[],s=!1,d=null,f=new Set;async function c(){if(!s){s=!0;try{for(;l.length>0;){let e=l.shift();d=e.requestId;let t=await o(e).catch(t=>({kind:"devup-result",requestId:e.requestId,error:t instanceof Error?t.message:String(t)}));d=null,f.delete(e.requestId)||figma.ui.postMessage(t)}}finally{s=!1}}}let u=null;function p(){u??(u=(async()=>{try{await figma.loadAllPagesAsync(),await Promise.all([figma.variables.getLocalVariableCollectionsAsync(),figma.variables.getLocalVariablesAsync(),figma.getLocalPaintStylesAsync(),figma.getLocalTextStylesAsync(),figma.getLocalEffectStylesAsync(),figma.getLocalGridStylesAsync()])}catch{}finally{u=null}})())}figma.showUI(__html__,{width:320,height:220}),p(),figma.on("selectionchange",a),figma.on("currentpagechange",a),figma.ui.onmessage=e=>{if("object"==typeof e&&null!==e){if("devup-ready"===e.kind){let e={kind:"devup-status",fileKey:figma.fileKey??null,fileName:figma.root.name,port:1993,...i()};figma.ui.postMessage(e);return}if("devup-job"===e.kind){l.push(e),c();return}if("devup-cancel"===e.kind){var r;let t;return void(r=e.requestId,(t=l.findIndex(e=>e.requestId===r))>=0?l.splice(t,1):d===r&&f.add(r))}if("devup-decoded"===e.kind)return void t.get(e.id)?.(e.png??null);"devup-in-view"===e.kind&&p()}}})(); \ No newline at end of file From bad82241e29e6b79d1787cc3f7deb23ea8c17bf2 Mon Sep 17 00:00:00 2001 From: leader Date: Sat, 24 Oct 2026 01:28:41 +0900 Subject: [PATCH 094/149] fix(codegen): require explicit figmaNodeIds export opt-in (64) --- .../src/server/handlers/devup_figma_export.rs | 3 +++ .../server/projection/component_projection.rs | 4 +--- .../projection/feedback_codegen_tests.rs | 24 +++++++++++++++++++ .../project_node_and_theme_outputs.rs | 18 ++++++-------- .../server/projection/project_operation.rs | 6 ++--- .../projection/project_responsive_output.rs | 4 +--- .../src/server/projection/theme_export.rs | 1 + .../devup-mcp/src/server/screen_plan/state.rs | 15 +----------- crates/devup-mcp/src/server/tools.rs | 3 +++ .../devup-mcp/tests/export_project_theme.rs | 6 ++--- 10 files changed, 45 insertions(+), 39 deletions(-) diff --git a/crates/devup-mcp/src/server/handlers/devup_figma_export.rs b/crates/devup-mcp/src/server/handlers/devup_figma_export.rs index 35f0ac00..275aa8a8 100644 --- a/crates/devup-mcp/src/server/handlers/devup_figma_export.rs +++ b/crates/devup-mcp/src/server/handlers/devup_figma_export.rs @@ -403,6 +403,7 @@ impl DevupServer { theme_options: projection::theme_export::ThemeExportOptions { mode: input.devup_json_mode, project_root: input.project_root.clone(), + figma_node_ids: input.figma_node_ids, }, strict: input.strict, output_paths: input.output_paths, @@ -452,6 +453,7 @@ impl DevupServer { theme_options: projection::theme_export::ThemeExportOptions { mode: input.devup_json_mode, project_root: input.project_root.clone(), + figma_node_ids: input.figma_node_ids, }, strict: input.strict, output_paths: input.output_paths, @@ -566,6 +568,7 @@ impl DevupServer { theme_options: projection::theme_export::ThemeExportOptions { mode: input.devup_json_mode, project_root: input.project_root.clone(), + figma_node_ids: input.figma_node_ids, }, strict: input.strict, output_paths: input.output_paths, diff --git a/crates/devup-mcp/src/server/projection/component_projection.rs b/crates/devup-mcp/src/server/projection/component_projection.rs index 4d66de6c..2c5f86f3 100644 --- a/crates/devup-mcp/src/server/projection/component_projection.rs +++ b/crates/devup-mcp/src/server/projection/component_projection.rs @@ -1,8 +1,6 @@ /// Marks the page root, its top-level children (the sections), their /// children and every asset with `data-figma-node-id`, so the runtime audit captures a section -/// or an asset by the node it came from. Only for exports into a project -/// (`projectRoot`); `.devup/screen-plan.config.json` `figmaNodeIds: false` -/// turns it off. +/// or an asset by the node it came from. Only when `figmaNodeIds` is true. fn annotate_node_ids( output: &mut devup_mcp_devup_ui::codegen::CodegenOutput, snapshot: &devup_mcp_figma::Snapshot, diff --git a/crates/devup-mcp/src/server/projection/feedback_codegen_tests.rs b/crates/devup-mcp/src/server/projection/feedback_codegen_tests.rs index f31e1699..6fc6f61e 100644 --- a/crates/devup-mcp/src/server/projection/feedback_codegen_tests.rs +++ b/crates/devup-mcp/src/server/projection/feedback_codegen_tests.rs @@ -108,6 +108,30 @@ async fn feedback32_component_set_has_no_canvas_coverage_failure() { ), "{result}"); } +#[tokio::test] +async fn feedback64_node_identity_requires_explicit_opt_in() { + for enabled in [false, true] { + let mut op = operation(&["tsx", "componentTsx", "sourceMap"]); + if let PendingOperation::Export { theme_options, .. } = &mut op { + theme_options.figma_node_ids = enabled; + } + let result = project(payload(), op).await.unwrap(); + for output in ["tsx", "componentTsx"] { + let code = result[output].as_str().unwrap(); + assert_eq!(code.contains("data-figma-node-id"), enabled, "{output}: {code}"); + } + assert!(result["sourceMap"].is_object(), "{result}"); + let mut op = operation(&["responsiveTsx"]); + if let PendingOperation::Export { theme_options, frame_ids, .. } = &mut op { + theme_options.figma_node_ids = enabled; + *frame_ids = vec!["1:1".into(), "1:2".into()]; + } + let result = project(responsive_payload(), op).await.unwrap(); + let code = result["responsiveTsx"].as_str().unwrap(); + assert_eq!(code.contains("data-figma-node-id"), enabled, "{code}"); + } +} + #[tokio::test] async fn feedback32_screen_reference_offers_component_export() { let mut data = feedback32_component_payload(); diff --git a/crates/devup-mcp/src/server/projection/project_node_and_theme_outputs.rs b/crates/devup-mcp/src/server/projection/project_node_and_theme_outputs.rs index 9d3bfe19..9c0fea20 100644 --- a/crates/devup-mcp/src/server/projection/project_node_and_theme_outputs.rs +++ b/crates/devup-mcp/src/server/projection/project_node_and_theme_outputs.rs @@ -102,9 +102,7 @@ pub(super) fn project_node_and_theme_outputs( } .with_payload_tokens(payload), )?; - if let Some(root) = theme_options.project_root.as_deref() - && super::super::screen_plan::node_ids_enabled(root) - { + if theme_options.figma_node_ids { let annotated = annotate_node_ids(&mut output, &payload.snapshot, node_id); result.insert("figmaNodeIds".to_owned(), annotated); } @@ -113,10 +111,7 @@ pub(super) fn project_node_and_theme_outputs( node_id, &mut output, true, - theme_options - .project_root - .as_deref() - .is_some_and(super::super::screen_plan::node_ids_enabled), + theme_options.figma_node_ids, ); scope_output(&mut output, "tsx"); result.entry("outputResults").or_insert_with(|| json!({}))["tsx"] = output_result(&output); @@ -172,6 +167,10 @@ pub(super) fn project_node_and_theme_outputs( ); match output { Ok(mut output) => { + if theme_options.figma_node_ids { + let annotated = annotate_node_ids(&mut output, &payload.snapshot, node_id); + result.insert("figmaNodeIds".to_owned(), annotated); + } let definitions = component_definition_actions(payload, &output.tsx, &result); if !definitions.is_empty() { result.insert("componentDefinitions".to_owned(), json!(definitions)); @@ -181,10 +180,7 @@ pub(super) fn project_node_and_theme_outputs( node_id, &mut output, false, - theme_options - .project_root - .as_deref() - .is_some_and(super::super::screen_plan::node_ids_enabled), + theme_options.figma_node_ids, ); scope_output(&mut output, "componentTsx"); result.entry("outputResults").or_insert_with(|| json!({}))["componentTsx"] = diff --git a/crates/devup-mcp/src/server/projection/project_operation.rs b/crates/devup-mcp/src/server/projection/project_operation.rs index f1cc9fa6..c9a429f9 100644 --- a/crates/devup-mcp/src/server/projection/project_operation.rs +++ b/crates/devup-mcp/src/server/projection/project_operation.rs @@ -271,6 +271,7 @@ async fn project_operation( "rootLayout":match root_layout { devup_mcp_devup_ui::codegen::RootLayout::Standalone => "standalone", devup_mcp_devup_ui::codegen::RootLayout::Embedded => "embedded" }, "assetNamesPerNode":asset_names_per_node,"scope":scope,"strict":strict, "devupJsonMode":if theme_options.mode.is_empty() {"full"} else {&theme_options.mode},"projectRoot":theme_options.project_root, + "figmaNodeIds":theme_options.figma_node_ids, "outputPaths":output_paths,"frameIds":frame_ids,"allScreens":all_screens, "debug":outputs.iter().any(|o| matches!(o.as_str(), "rawSnapshot" | "rawPayload")), "delivery":"resource", @@ -548,10 +549,7 @@ async fn project_operation( section_tsx_projected, ) = project_section_frames::project_section_frames( project_section_frames::SectionFrames { - annotate_node_ids: theme_options - .project_root - .as_deref() - .is_some_and(super::screen_plan::node_ids_enabled), + annotate_node_ids: theme_options.figma_node_ids, failures, design_fingerprints: &design_fingerprints, payload, diff --git a/crates/devup-mcp/src/server/projection/project_responsive_output.rs b/crates/devup-mcp/src/server/projection/project_responsive_output.rs index 3f75dc8e..ef759bed 100644 --- a/crates/devup-mcp/src/server/projection/project_responsive_output.rs +++ b/crates/devup-mcp/src/server/projection/project_responsive_output.rs @@ -115,9 +115,7 @@ pub(super) fn project_responsive_output(input: ResponsiveOutput<'_>) -> Responsi "Two frames occupy the same breakpoint slot. Request tsx separately, or select one frame per breakpoint slot.", )); } - if let Some(root) = theme_options.project_root.as_deref() - && super::super::screen_plan::node_ids_enabled(root) - { + if theme_options.figma_node_ids { let written = devup_mcp_devup_ui::codegen::node_ids::annotate_merged(&mut merged); let ids = result.entry("figmaNodeIds").or_insert_with( || json!({"attribute": devup_mcp_devup_ui::codegen::node_ids::NODE_ID_ATTRIBUTE}), diff --git a/crates/devup-mcp/src/server/projection/theme_export.rs b/crates/devup-mcp/src/server/projection/theme_export.rs index c44f909f..983f72fb 100644 --- a/crates/devup-mcp/src/server/projection/theme_export.rs +++ b/crates/devup-mcp/src/server/projection/theme_export.rs @@ -5,6 +5,7 @@ use devup_mcp_devup_ui::theme::{ThemeOutput, TokenUsage}; pub struct ThemeExportOptions { pub mode: String, pub project_root: Option, + pub figma_node_ids: bool, } pub fn validate( diff --git a/crates/devup-mcp/src/server/screen_plan/state.rs b/crates/devup-mcp/src/server/screen_plan/state.rs index 577d4bce..cbcb1724 100644 --- a/crates/devup-mcp/src/server/screen_plan/state.rs +++ b/crates/devup-mcp/src/server/screen_plan/state.rs @@ -355,19 +355,6 @@ pub(super) struct PlanConfig { pub visual_thresholds: Option, } -/// Whether exports into this project mark their elements with -/// data-figma-node-id: on unless the project config says false. An -/// unreadable config leaves it on; draft reports the config error. -pub(super) fn node_ids_enabled(project_root: &str) -> bool { - !matches!( - read_config(project_root), - Ok(Some(PlanConfig { - figma_node_ids: Some(false), - .. - })) - ) -} - fn read_config(project_root: &str) -> Result, DevupError> { let path = Path::new(project_root).join(CONFIG_FILE); let Ok(bytes) = std::fs::read(&path) else { @@ -884,4 +871,4 @@ pub(super) fn question( /// The questions as they go to the question tool: no resolution table. pub(super) fn ask_shape(questions: &[Value]) -> Vec { ask_shape_with(questions, questions) -} \ No newline at end of file +} diff --git a/crates/devup-mcp/src/server/tools.rs b/crates/devup-mcp/src/server/tools.rs index a1558876..ffb36944 100644 --- a/crates/devup-mcp/src/server/tools.rs +++ b/crates/devup-mcp/src/server/tools.rs @@ -96,6 +96,9 @@ pub struct FigmaExportInput { pub component_name: Option, #[serde(default)] pub include_diagnostics: bool, + /// Opt in to data-figma-node-id attributes for runtime design audits. + #[serde(default)] + pub figma_node_ids: bool, #[serde(default = "default_scope")] #[schemars(extend("enum" = super::validation::COLLECTION_SCOPES))] pub scope: String, diff --git a/crates/devup-mcp/tests/export_project_theme.rs b/crates/devup-mcp/tests/export_project_theme.rs index 2529a603..732bc12f 100644 --- a/crates/devup-mcp/tests/export_project_theme.rs +++ b/crates/devup-mcp/tests/export_project_theme.rs @@ -224,9 +224,7 @@ async fn an_export_confirms_a_project_that_already_has_the_token() -> anyhow::Re Ok(()) } -/// An export into a project marks the elements the runtime audit finds by -/// Figma node; the project config turns it off, and an export with no -/// project never writes it, so plugin-parity output stays byte-equal. +/// Node identities are explicitly opted into, independently of project config. #[tokio::test] async fn an_export_into_a_project_marks_its_elements_with_the_figma_node() -> anyhow::Result<()> { let root = std::env::temp_dir().join(format!("devup-node-ids-{}", std::process::id())); @@ -234,7 +232,7 @@ async fn an_export_into_a_project_marks_its_elements_with_the_figma_node() -> an fs::write(root.join("package.json"), r#"{"name":"fixture"}"#)?; let marked = r#"data-figma-node-id="3879:35481""#; let result = - export(json!({"url": URL, "outputs": ["tsx"], "projectRoot": root.to_string_lossy()})) + export(json!({"url": URL, "outputs": ["tsx"], "projectRoot": root.to_string_lossy(), "figmaNodeIds": true})) .await?; assert!( result["tsx"].as_str().unwrap_or_default().contains(marked), From db5825a2c7c3dca589400595daac29baa9dc1bd5 Mon Sep 17 00:00:00 2001 From: leader Date: Sat, 24 Oct 2026 01:33:05 +0900 Subject: [PATCH 095/149] fix(project-plan): respect operation evidence and API model paths (54) --- .../devup-mcp/src/server/project_plan/erd.rs | 6 +-- .../devup-mcp/src/server/project_plan/mod.rs | 7 ++- .../src/server/project_plan/tests.rs | 4 ++ .../server/project_plan/tests/contracts.rs | 6 ++- .../src/server/project_plan/tests/recovery.rs | 46 +++++++++++++++++++ .../src/server/project_plan/wiring/draft.rs | 11 +++-- 6 files changed, 70 insertions(+), 10 deletions(-) diff --git a/crates/devup-mcp/src/server/project_plan/erd.rs b/crates/devup-mcp/src/server/project_plan/erd.rs index 47088965..92ed55f4 100644 --- a/crates/devup-mcp/src/server/project_plan/erd.rs +++ b/crates/devup-mcp/src/server/project_plan/erd.rs @@ -181,9 +181,9 @@ pub(super) fn draft(input: &ProjectPlanInput, plan: &mut Value) -> Result<(), De )); } plan["modelsDir"] = json!(dir); - if plan["modelsWorkingDirectory"].is_null() { - plan["modelsWorkingDirectory"] = json!("."); - } + plan["modelsWorkingDirectory"] = json!(Path::new(&dir).parent() + .filter(|parent| !parent.as_os_str().is_empty()) + .map_or_else(|| ".".to_owned(), |parent| parent.to_string_lossy().replace('\\', "/"))); let mut entities = if !input.entities.is_empty() { input.entities.clone() } else { diff --git a/crates/devup-mcp/src/server/project_plan/mod.rs b/crates/devup-mcp/src/server/project_plan/mod.rs index f9a56f2f..80e22959 100644 --- a/crates/devup-mcp/src/server/project_plan/mod.rs +++ b/crates/devup-mcp/src/server/project_plan/mod.rs @@ -125,6 +125,8 @@ pub struct Decision { pub recommended: String, #[serde(default)] pub destructive: bool, + pub state: Option, + pub kind: Option, } #[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] @@ -218,7 +220,10 @@ fn record_decisions( let list = plan["decisions"].as_array_mut().unwrap(); list.retain(|v| v["id"] != d.id); let mut value = json!(d); - value["state"] = json!(state); + if d.state.as_deref().is_some_and(|state| !["pending", "confirmed"].contains(&state)) { + return Err(invalid("Decision state must be pending or confirmed")); + } + value["state"] = json!(d.state.as_deref().unwrap_or(state)); list.push(value); } Ok(()) diff --git a/crates/devup-mcp/src/server/project_plan/tests.rs b/crates/devup-mcp/src/server/project_plan/tests.rs index 673d85db..026eeeb6 100644 --- a/crates/devup-mcp/src/server/project_plan/tests.rs +++ b/crates/devup-mcp/src/server/project_plan/tests.rs @@ -242,6 +242,8 @@ async fn feature_split_and_decisions_and_state_gates() { reason: "Destructive cleanup".into(), recommended: "Keep data".into(), destructive: true, + state: None, + kind: None, }); run(&i, &policy).await.unwrap(); i.action = "assume".into(); @@ -265,6 +267,8 @@ fn decision(id: &str) -> Decision { reason: "Needs the user".into(), recommended: "Keep".into(), destructive: false, + state: None, + kind: None, } } #[tokio::test] diff --git a/crates/devup-mcp/src/server/project_plan/tests/contracts.rs b/crates/devup-mcp/src/server/project_plan/tests/contracts.rs index 191b872b..d97d312f 100644 --- a/crates/devup-mcp/src/server/project_plan/tests/contracts.rs +++ b/crates/devup-mcp/src/server/project_plan/tests/contracts.rs @@ -36,12 +36,14 @@ fn list_contracts_draft_queries_only_from_relevant_evidence() { let mut plain = plan.clone(); plain["features"][0]["scope"] = json!("List"); wiring::draft(&mut plain); - assert_eq!(plain["endpointContracts"][0]["queryParams"], json!([])); + let plain_queries = array(&plain["endpointContracts"][0]["queryParams"]); + assert!(plain_queries.iter().any(|query| query["name"] == "state")); + assert!(!plain_queries.iter().any(|query| query["name"] == "page" || query["name"] == "sort" || query["name"] == "q")); } #[test] fn write_contracts_include_validation_and_delete_has_no_response_fields() { - let mut plan = plan("Create edit CMS", json!([ + let mut plan = plan("Create edit delete CMS", json!([ {"name":"email","type":{"kind":"varchar","length":120},"nullable":false}, {"name":"phone","type":"text","nullable":true}, {"name":"website_link","type":"text","nullable":true}, diff --git a/crates/devup-mcp/src/server/project_plan/tests/recovery.rs b/crates/devup-mcp/src/server/project_plan/tests/recovery.rs index 904e2baa..3cdad8f3 100644 --- a/crates/devup-mcp/src/server/project_plan/tests/recovery.rs +++ b/crates/devup-mcp/src/server/project_plan/tests/recovery.rs @@ -1,5 +1,51 @@ use super::*; +#[test] +fn list_counts_supply_pagination_and_enum_filters() { + let mut plan = json!({"facts":{"parts":"API"},"entities":[{"name":"post","columns":[{"name":"category","type":{"kind":"enum","values":["a","b"]}}]}], + "features":[{"id":"posts","scope":"post 목록10","screens":[],"acceptance":[],"entities":["post"]}]}); + wiring::draft(&mut plan); + let queries = array(&plan["endpointContracts"][0]["queryParams"]); + for name in ["page", "pageSize", "category"] { + assert!(queries.iter().any(|query| query["name"] == name), "{name}"); + } +} + +#[test] +fn negated_korean_writes_do_not_become_operations() { + let mut plan = json!({"facts":{"parts":"API"},"entities":[{"name":"post","columns":[]}], + "features":[{"id":"posts","scope":"post 목록","screens":[],"acceptance":["등록·수정 불가", "미등록"],"entities":["post"]}]}); + wiring::draft(&mut plan); + assert_eq!(plan["dataMappings"][0]["requiredOperations"], json!(["list"])); +} + +#[tokio::test] +async fn confirm_accepts_explicit_confirmed_decision_state() { + let dir = TestDir::new(); + let (mut request, policy, _) = drafted(dir.path(), false).await; + request.decisions = vec![serde_json::from_value(json!({"id":"scope","reason":"Approved scope","recommended":"Keep scope","state":"confirmed","kind":"scope-exclusion"})).unwrap()]; + let result = run(&request, &policy).await.unwrap(); + assert_eq!(result["plan"]["decisions"][0]["state"], "confirmed"); + assert_eq!(result["plan"]["decisions"][0]["kind"], "scope-exclusion"); +} + +#[test] +fn admin_label_does_not_imply_delete_operation() { + let mut plan = json!({"facts":{"parts":"API"},"entities":[{"name":"post","columns":[]}], + "features":[{"id":"posts","scope":"admin post list","screens":[],"acceptance":[],"entities":["post"]}]}); + wiring::draft(&mut plan); + assert!(!array(&plan["dataMappings"][0]["requiredOperations"]).contains(&json!("delete"))); +} + +#[tokio::test] +async fn explicit_nested_models_dir_selects_api_working_directory() { + let dir = TestDir::new(); + let (mut request, policy, _) = drafted(dir.path(), true).await; + request.models_dir = Some("apis/api/models".into()); + let result = run(&request, &policy).await.unwrap(); + assert_eq!(result["plan"]["modelsWorkingDirectory"], "apis/api"); +} + #[tokio::test] async fn confirm_refuses_to_drop_existing_columns_without_writing_models() { let dir = TestDir::new(); diff --git a/crates/devup-mcp/src/server/project_plan/wiring/draft.rs b/crates/devup-mcp/src/server/project_plan/wiring/draft.rs index 8fe9985a..978b0a83 100644 --- a/crates/devup-mcp/src/server/project_plan/wiring/draft.rs +++ b/crates/devup-mcp/src/server/project_plan/wiring/draft.rs @@ -64,7 +64,8 @@ fn rules(column: &Value) -> Value { fn queries(entity: &Value, evidence: &str) -> Vec { let mut queries = vec![]; - if contains(evidence, &["pagination", "페이지", "page number"]) { + if contains(evidence, &["pagination", "페이지", "page number"]) + || evidence.split("목록").skip(1).any(|suffix| suffix.starts_with(|character: char| character.is_ascii_digit())) { queries.extend([json!({"name":"page","type":"integer"}), json!({"name":"pageSize","type":"integer","aliases":["size"]})]); } if contains(evidence, &["sort", "정렬"]) { @@ -75,7 +76,8 @@ fn queries(entity: &Value, evidence: &str) -> Vec { queries.push(json!({"name":"sort","type":"string","allowedKeys":keys.iter().map(|key| key["name"].clone()).collect::>(),"columns":keys})); } if contains(evidence, &["search", "검색"]) { queries.push(json!({"name":"q","type":"string"})); } - if contains(evidence, &["filter", "category", "필터", "카테고리", "탭"]) { + if contains(evidence, &["filter", "category", "필터", "카테고리", "탭"]) + || array(&entity["columns"]).iter().any(|column| column["type"]["kind"] == "enum") { for column in array(&entity["columns"]) { let name = text(&column["name"]); if column["type"]["kind"] == "enum" || name.ends_with("_id") || contains(name, &["category", "카테고리"]) { @@ -92,15 +94,16 @@ pub(in crate::server::project_plan) fn draft(plan: &mut Value) { for feature in array(&plan["features"]) { let screens = units::feature_screens(plan, feature); let evidence = format!("{} {} {} {}", feature["scope"], feature["screens"], feature["acceptance"], json!(screens)).to_lowercase(); + let operation_text = evidence.replace("등록·수정 불가", "").replace("미등록", ""); let mut driven = BTreeMap::<&str, Vec>::new(); for (op, words) in [ ("list", vec!["list", "table", "search", "filter", "pagination", "목록", "검색", "표"]), ("get", vec!["detail", "상세"]), ("create", vec!["form", "create", "signup", "input", "textarea", "폼", "등록", "문의"]), ("update", vec!["edit", "update", "수정"]), - ("delete", vec!["delete", "remove", "삭제", "admin", "cms", "관리"]), + ("delete", vec!["delete", "remove", "삭제"]), ] { - for word in words { if evidence.contains(word) { driven.entry(op).or_default().push(format!("keyword: {word}")); } } + for word in words { if operation_text.contains(word) { driven.entry(op).or_default().push(format!("keyword: {word}")); } } } let (mut counts, mut labels) = (BTreeMap::new(), vec![]); for screen in &screens { From 63d901d75dcbc782452530a7a159c4bbb8e693fa Mon Sep 17 00:00:00 2001 From: leader Date: Sat, 24 Oct 2026 01:37:38 +0900 Subject: [PATCH 096/149] fix(project-plan): report column type violations and disk drift (51) --- .../devup-mcp/src/server/project_plan/erd.rs | 28 +++++++++++++++++++ .../devup-mcp/src/server/project_plan/mod.rs | 4 +++ .../src/server/project_plan/tests/recovery.rs | 20 +++++++++++++ 3 files changed, 52 insertions(+) diff --git a/crates/devup-mcp/src/server/project_plan/erd.rs b/crates/devup-mcp/src/server/project_plan/erd.rs index 92ed55f4..7273e51a 100644 --- a/crates/devup-mcp/src/server/project_plan/erd.rs +++ b/crates/devup-mcp/src/server/project_plan/erd.rs @@ -275,6 +275,23 @@ pub(super) fn draft(input: &ProjectPlanInput, plan: &mut Value) -> Result<(), De Ok(()) } pub(super) fn validate(entities: &[Value]) -> Result<(), DevupError> { + let mut violations = Vec::new(); + for (entity_index, entity) in entities.iter().enumerate() { + for (column_index, column) in array(&entity["columns"]).iter().enumerate() { + if unsupported_type(&column["type"]).is_some() { + violations.push(json!({"path":format!("/entities/{entity_index}/columns/{column_index}/type"), + "column":format!("{}.{}",text(&entity["name"]),text(&column["name"])), + "got":column["type"],"expected":{"simple":SIMPLE_TYPES,"complexKinds":COMPLEX_KINDS}, + "fix":if column["type"] == "jsonb" {"Use json"} else {"Use a supported Vespertide type"}})); + } + } + } + if !violations.is_empty() { + let mut error = invalid(format!("Unsupported Vespertide column types: {}; jsonb must use json", + violations.iter().map(|violation| text(&violation["column"])).collect::>().join(", "))); + error.details = json!({"violations":violations}); + return Err(error); + } let mut names = std::collections::BTreeSet::new(); for e in entities { let name = text(&e["name"]); @@ -344,6 +361,17 @@ pub(super) fn validate(entities: &[Value]) -> Result<(), DevupError> { } Ok(()) } +pub(super) fn disk_report(plan: &Value, root: &str) -> Value { + let mut existing = Vec::new(); + if let Err(error) = scan_models(&Path::new(root).join(text(&plan["modelsDir"])), &mut existing) { + return json!({"error":error.message}); + } + let violations = validate(&existing).err().map(|error| error.details).unwrap_or(Value::Null); + let drift = existing.iter().filter(|model| { + !array(&plan["entities"]).iter().any(|entity| entity["name"] == model["name"] && entity["columns"] == model["columns"]) + }).map(|model| model["name"].clone()).collect::>(); + json!({"typeValidation":violations,"entityDrift":drift}) +} pub(super) fn write_models( tx: &mut OutputTransaction, policy: &OutputPolicy, diff --git a/crates/devup-mcp/src/server/project_plan/mod.rs b/crates/devup-mcp/src/server/project_plan/mod.rs index 80e22959..ee118b9b 100644 --- a/crates/devup-mcp/src/server/project_plan/mod.rs +++ b/crates/devup-mcp/src/server/project_plan/mod.rs @@ -406,7 +406,11 @@ fn extras(plan: &Value, root: &str) -> Value { } fn response(plan: Value, root: &str, full: bool) -> Value { let extra = extras(&plan, root); + let models = erd::api(&plan).then(|| erd::disk_report(&plan, root)); let mut out = base_response(plan, root, full); + if let Some(models) = models { + out["modelDiagnostics"] = models; + } if let (Some(o), Some(e)) = (out.as_object_mut(), extra.as_object()) { o.extend(e.clone()); } diff --git a/crates/devup-mcp/src/server/project_plan/tests/recovery.rs b/crates/devup-mcp/src/server/project_plan/tests/recovery.rs index 3cdad8f3..68adda11 100644 --- a/crates/devup-mcp/src/server/project_plan/tests/recovery.rs +++ b/crates/devup-mcp/src/server/project_plan/tests/recovery.rs @@ -1,5 +1,25 @@ use super::*; +#[test] +fn existing_model_diagnostics_report_type_errors_and_entity_drift() { + let dir = TestDir::new(); + std::fs::create_dir_all(dir.path().join("models")).unwrap(); + std::fs::write(dir.path().join("models/post.json"), json!({"name":"post","columns":[{"name":"body","type":"jsonb","nullable":false}]}).to_string()).unwrap(); + let report = erd::disk_report(&json!({"modelsDir":"models","entities":[]}), &dir.path().to_string_lossy()); + assert_eq!(report["entityDrift"], json!(["post"])); + assert_eq!(report["typeValidation"]["violations"][0]["got"], "jsonb"); +} + +#[test] +fn invalid_types_report_every_column_path() { + let error = erd::validate(&[json!({"name":"post","columns":[ + {"name":"first","type":"invalid_one","nullable":false}, + {"name":"second","type":"invalid_two","nullable":false} + ]})]).unwrap_err(); + assert_eq!(array(&error.details["violations"]).len(), 2); + assert_eq!(error.details["violations"][1]["path"], "/entities/0/columns/1/type"); +} + #[test] fn list_counts_supply_pagination_and_enum_filters() { let mut plan = json!({"facts":{"parts":"API"},"entities":[{"name":"post","columns":[{"name":"category","type":{"kind":"enum","values":["a","b"]}}]}], From daead12dc457ade08112e932d693a60387051a07 Mon Sep 17 00:00:00 2001 From: leader Date: Sat, 24 Oct 2026 01:39:09 +0900 Subject: [PATCH 097/149] fix(codegen): resolve instance references from component identity (65) --- .../src/codegen/component/render_node.rs | 28 +++++++++++++++++-- .../src/codegen/variant/variant_projection.rs | 2 +- .../tests/manifest_covers_readers.rs | 3 ++ .../projection/feedback_codegen_tests.rs | 19 +++++++++++++ 4 files changed, 48 insertions(+), 4 deletions(-) diff --git a/crates/devup-mcp-devup-ui/src/codegen/component/render_node.rs b/crates/devup-mcp-devup-ui/src/codegen/component/render_node.rs index b33c7146..3cbc7485 100644 --- a/crates/devup-mcp-devup-ui/src/codegen/component/render_node.rs +++ b/crates/devup-mcp-devup-ui/src/codegen/component/render_node.rs @@ -69,7 +69,18 @@ fn render_node( .and_then(|references| references.get("visible")) .and_then(serde_json::Value::as_str) .map(component_property_name); - let component = snapshot + let master = view.string("mainComponentId") + .or_else(|| view.string("componentId")) + .and_then(|id| snapshot.nodes.get(id)); + let set = view.string("mainComponentSetId") + .or_else(|| master.and_then(|node| node.typed_view().string("parentId"))) + .and_then(|id| snapshot.nodes.get(id)) + .filter(|node| node.node_type == "COMPONENT_SET"); + let component = set + .and_then(|node| node.typed_view().name()) + .map(normalize_component_name) + .or_else(|| master.and_then(|node| node.typed_view().name()).map(normalize_component_name)) + .unwrap_or_else(|| snapshot .nodes .values() .filter(|candidate| candidate.typed_view().node_type() == "COMPONENT") @@ -80,8 +91,19 @@ fn render_node( }) .max_by_key(|name| name.len()) .map(legacy_component_name) - .unwrap_or_else(|| legacy_component_name(view.name().unwrap_or("Component"))); - let content = main_property.unwrap_or_else(|| format!("<{component} />")); + .unwrap_or_else(|| legacy_component_name(view.name().unwrap_or("Component")))); + let mut properties = super::variant::instance_variant_props(&view); + for (name, value) in view.value("variantProperties").and_then(serde_json::Value::as_object).into_iter().flatten() { + if !["effect", "viewport"].iter().any(|reserved| name.eq_ignore_ascii_case(reserved)) + && let Some(value) = value.as_str() + { + properties.entry(super::variant::instance_property_name(name)).or_insert_with(|| value.to_owned()); + } + } + let attributes = properties.iter().map(|(name, value)| { + format!(" {name}={{{}}}", serde_json::to_string(value).unwrap()) + }).collect::(); + let content = main_property.unwrap_or_else(|| format!("<{component}{attributes} />")); let expression = if let Some(visible) = visible_property { format!("{{{visible} && {content}}}") } else if references.is_some_and(|references| references.contains_key("mainComponent")) { diff --git a/crates/devup-mcp-devup-ui/src/codegen/variant/variant_projection.rs b/crates/devup-mcp-devup-ui/src/codegen/variant/variant_projection.rs index cb64be7c..6aace47a 100644 --- a/crates/devup-mcp-devup-ui/src/codegen/variant/variant_projection.rs +++ b/crates/devup-mcp-devup-ui/src/codegen/variant/variant_projection.rs @@ -380,7 +380,7 @@ pub(super) const POSITION_PROPS: [&str; 7] = [ /// is `Property1` and not `property1`; folding it turned `leftIcon` into /// `lefticon`. And only a trailing `#:` is stripped, since /// that is Figma's own suffix rather than a character a name may not contain. -fn instance_property_name(raw: &str) -> String { +pub(super) fn instance_property_name(raw: &str) -> String { let stripped = raw .rsplit_once('#') .filter(|(_, suffix)| { diff --git a/crates/devup-mcp-figma/tests/manifest_covers_readers.rs b/crates/devup-mcp-figma/tests/manifest_covers_readers.rs index 8e9f02c3..40f16715 100644 --- a/crates/devup-mcp-figma/tests/manifest_covers_readers.rs +++ b/crates/devup-mcp-figma/tests/manifest_covers_readers.rs @@ -18,6 +18,9 @@ const NOT_NODE_FIELDS: &[&str] = &[ "parentType", "childrenIds", "styledTextSegments", + "mainComponentId", // Written by component_identity_helpers.js using getMainComponentAsync. + "mainComponentSetId", // Written by component_identity_helpers.js from the main component parent. + "componentId", // REST snapshot identity alias; not a synchronous plugin node property. "nodeScreenIds", // Computed by section_index.js from visited ancestry, not a Figma property. "navigationContentWidth", // Computed from visible child widths by explore.js/section_index.js; not a Figma property. // Envelope, pagination and probe records, not nodes. diff --git a/crates/devup-mcp/src/server/projection/feedback_codegen_tests.rs b/crates/devup-mcp/src/server/projection/feedback_codegen_tests.rs index 6fc6f61e..cdd8eb29 100644 --- a/crates/devup-mcp/src/server/projection/feedback_codegen_tests.rs +++ b/crates/devup-mcp/src/server/projection/feedback_codegen_tests.rs @@ -151,3 +151,22 @@ async fn feedback32_screen_reference_offers_component_export() { assert_eq!(action["arguments"]["outputs"], json!(["componentTsx"])); assert_eq!(action["arguments"]["artifactId"], result["cache"]["artifactId"]); } + +#[tokio::test] +async fn feedback65_component_reference_uses_master_set_and_variant_props() { + let mut data = feedback32_component_payload(); + data.target.node_id = Some("screen".into()); + data.snapshot.roots = vec!["screen".into()]; + for (id, node) in [ + ("screen", json!({"id":"screen","type":"FRAME","fields":{"name":"Screen","childrenIds":["instance"]}})), + ("instance", json!({"id":"instance","type":"INSTANCE","fields":{"name":"Unrelated layer name","mainComponentId":"default","variantProperties":{"size":"sm","effect":"default"}}})), + ] { + data.snapshot.nodes.insert(id.into(), serde_json::from_value(node).unwrap()); + } + let result = project(data, operation(&["componentTsx"])).await.unwrap(); + let code = result["componentTsx"].as_str().unwrap(); + assert!(code.contains("import { Button } from \"@/components/Button\""), "{code}"); + assert!(code.contains("'); + await page.evaluate(() => document.body.remove()); + assert.deepEqual(await observeClippedContent(page), { clipped: [] }); + await observeCompleteness(page, {}, 800, { css: 'html' }); + await observeNativeAppearance(page.locator('html')); + } finally { + await browser.close(); + } +}); + +test('a measurement exception is inconclusive and later checks still run', async () => { + const server = await serve(); + const baseUrl = `http://127.0.0.1:${server.address().port}`; + const out = await mkdtemp(join(tmpdir(), 'devup-check-error-')); + try { + const plan = join(out, 'plan.json'); + await writeFile(plan, JSON.stringify({ schemaVersion: 1, kind: 'devup-runtime-audit-plan', planId: 'check-error', plan: { + baseUrl, allowedOrigins: [baseUrl], viewports: [{ width: 800, height: 600 }], + screens: [{ id: 'home', routePath: '/', checks: [ + { kind: 'text-align', subject: { css: '[' }, edge: 'left' }, + { kind: 'page-health' }, + ] }], + } })); + const report = await runPlan({ plan, out, maxMinutes: 1 }); + assert.equal(report.status, 'completed'); + assert.deepEqual(report.runs[0].screens[0].checks.map((check) => check.status), ['inconclusive', 'pass']); + assert.match(report.runs[0].screens[0].checks[0].reason, /home\/800\/check 0/); + } finally { + server.close(); + await rm(out, { recursive: true, force: true }); + } +}); + function serve() { const server = createServer(async (request, response) => { const name = new URL(request.url, 'http://local').pathname.slice(1) || 'before.html'; From cc86a2918f4f45902d421ba10ea824b962b186dc Mon Sep 17 00:00:00 2001 From: leader Date: Sat, 24 Oct 2026 02:16:02 +0900 Subject: [PATCH 113/149] fix(screen-plan): find ancestor plans for app audit requests (58) --- crates/devup-mcp/src/server/screen_plan.rs | 1 + .../src/server/screen_plan/actions.rs | 6 ++--- .../src/server/screen_plan/lookup.rs | 24 +++++++++++++++++++ .../screen_plan_tests/feedback_remaining.rs | 16 +++++++++++++ 4 files changed, 43 insertions(+), 4 deletions(-) create mode 100644 crates/devup-mcp/src/server/screen_plan/lookup.rs diff --git a/crates/devup-mcp/src/server/screen_plan.rs b/crates/devup-mcp/src/server/screen_plan.rs index c3562fae..e84b1d74 100644 --- a/crates/devup-mcp/src/server/screen_plan.rs +++ b/crates/devup-mcp/src/server/screen_plan.rs @@ -37,3 +37,4 @@ include!("screen_plan/status.rs"); include!("screen_plan/draft_plan_with.rs"); include!("screen_plan/plan_route_group.rs"); include!("screen_plan/state_variants.rs"); +include!("screen_plan/lookup.rs"); diff --git a/crates/devup-mcp/src/server/screen_plan/actions.rs b/crates/devup-mcp/src/server/screen_plan/actions.rs index e64e5bc2..560a821e 100644 --- a/crates/devup-mcp/src/server/screen_plan/actions.rs +++ b/crates/devup-mcp/src/server/screen_plan/actions.rs @@ -590,10 +590,8 @@ pub(super) async fn audit_plan( policy: &OutputPolicy, artifacts: &ArtifactStore, ) -> Result { - let root = require_root(input)?; - let mut plan = read_plan(root)?.ok_or_else(|| { - invalid("No screen plan exists in this project; run devup_screen_plan action draft first.") - })?; + let (plan_root, mut plan) = audit_plan_location(input)?; + let root = plan_root.to_str().ok_or_else(|| invalid("The plan root is not UTF-8."))?; if plan["status"] != "confirmed" { return Err(invalid( "auditPlan needs the confirmed plan: answer the questions and confirm first.", diff --git a/crates/devup-mcp/src/server/screen_plan/lookup.rs b/crates/devup-mcp/src/server/screen_plan/lookup.rs new file mode 100644 index 00000000..a54fe57f --- /dev/null +++ b/crates/devup-mcp/src/server/screen_plan/lookup.rs @@ -0,0 +1,24 @@ +fn audit_plan_location(input: &ScreenPlanInput) -> Result<(PathBuf, Value), DevupError> { + let requested = dunce::canonicalize(require_root(input)?) + .map_err(|error| invalid(format!("Cannot resolve projectRoot: {error}")))?; + let mut candidates = Vec::new(); + for root in requested.ancestors() { + let Some(plan) = read_plan(&root.to_string_lossy())? else { continue; }; + let matches_id = input.plan_id.as_deref().is_none_or(|id| plan["planId"] == id); + let matches_app = root == requested || array(&plan["routes"]).iter().any(|route| { + route["app"].as_str().is_some_and(|app| { + dunce::canonicalize(root.join(app)).ok().as_ref() == Some(&requested) + }) + }); + if matches_id && (input.plan_id.is_some() || matches_app) { + return Ok((root.to_path_buf(), plan)); + } + candidates.push(json!({"projectRoot":root,"planId":plan["planId"],"status":plan["status"]})); + } + Err(DevupError::with_details( + ErrorCode::DevupInvalidInput, + if candidates.is_empty() { "No screen plan exists in this project; run devup_screen_plan action draft first." } + else { "No ancestor screen plan matches the requested planId or app; use one of the existing plan roots." }, + false, json!({"existingPlans":candidates}), + )) +} diff --git a/crates/devup-mcp/src/server/screen_plan_tests/feedback_remaining.rs b/crates/devup-mcp/src/server/screen_plan_tests/feedback_remaining.rs index a0ba4720..b1430c70 100644 --- a/crates/devup-mcp/src/server/screen_plan_tests/feedback_remaining.rs +++ b/crates/devup-mcp/src/server/screen_plan_tests/feedback_remaining.rs @@ -48,3 +48,19 @@ fn r56_metadata_uses_the_planned_app_not_the_first_root_route() { assert_eq!(metadata["state"], "filled"); assert_eq!(metadata["detail"], "apps/front/src/app/page.tsx"); } + +#[tokio::test] +async fn r58_audit_plan_finds_the_confirmed_monorepo_plan_from_front() { + let fixture = Fixture::new(); + let mut plan = confirmed_plan(); + plan["routes"][0]["app"] = json!("apps/front"); + write_plan_file(&fixture, &plan); + fixture.write("apps/front/package.json", "{}"); + let mut request = input(&fixture, "auditPlan"); + request.project_root = Some(fixture.0.join("apps/front").to_string_lossy().into_owned()); + request.plan_id = Some(text(&plan["planId"]).to_owned()); + let result = audit_plan(&request, &fixture.policy(), &ArtifactStore::default()).await.unwrap(); + assert!(!array(&result["plans"]).is_empty()); + assert!(!fixture.0.join("apps/front/.devup/screen-plan.json").exists()); + assert!(text(&result["plans"][0]["planPath"]).starts_with(fixture.root())); +} From 8732d61e1758d50fdd54e931ade5ec8e3697d882 Mon Sep 17 00:00:00 2001 From: leader Date: Sat, 24 Oct 2026 02:16:26 +0900 Subject: [PATCH 114/149] fix(skills): stop requiring audit plugin in app build config (64) --- README.md | 4 ++-- .../src/server/commands/opencode-devup.md | 5 ++++- .../skills/devup-implementation-plan/SKILL.md | 21 ++++++++----------- .../src/server/skills/devup/SKILL.md | 9 +++++--- .../devup-mcp/src/server/skills/manifest.json | 8 +++---- .../src/server/text/devup-screen-plan.md | 2 +- .../src/server/text/guide-GUIDE-c682b0c5.md | 5 ++++- .../projection-annotate_node_ids-b03e6d56.md | 2 +- 8 files changed, 31 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index ca8d1700..c6caf0ed 100644 --- a/README.md +++ b/README.md @@ -1078,7 +1078,7 @@ snapshot에 없는 목적지(legacy 경로, 다중 루트 요청)는 조용히 규칙은 셋입니다: **모든 화면·섹션·에셋을 그려진 모든 폭에서 시각으로 검증한다**, **기다리느라 멈추지 않고 작업 순서를 바꾼다**, **완료 = 실패 0, 아니면 증거와 함께 unresolved로 남긴다**. - `devup_screen_plan` `action: "auditPlan"`(확정 뒤)은 확정된 URL마다 runtime audit 계획을 `.devup/runtime-audit//plan.json`에 쓰고 실행기를 `.devup/runtime-audit/runner/`에 한 번 씁니다. `artifacts`에 그 계획 프레임들의 `devup_figma_export` artifactId를 모두 넘깁니다. 폭은 그려진 폭(뷰포트 높이 = 프레임 높이) + 1440/1100/858/390입니다. 모든 폭에서 page health(페이지 오류·콘솔 오류·"This page couldn't load"/"Application error" 텍스트 없음), 가로 넘침, 잘린 텍스트, 내비 잘림을 보고, 그려진 폭에서는 페이지 전체를 referencePng와, 프레임의 최상위 자식(섹션)과 에셋을 각자의 native-node 렌더와 비교하고, 섹션·카드·에셋의 크기와 **위치**(`bounds` 검사; 카드 순서가 틀리면 실패)를 봅니다. 링크·버튼 전체의 hover/focus와 다음 URL로의 view-transition 검사도 들어가며, view-transition은 코드가 맨 뒤로 보내고 실행기가 끝나면 원래 URL로 돌아옵니다. 빠진 referencePng·렌더·프레임은 `missingReferences`에 만드는 export 호출과 함께 나옵니다 — 조용히 건너뛰지 않습니다. -- 섹션·카드·에셋은 `data-figma-node-id`로 찾습니다. `projectRoot`가 있는 export는 페이지 루트, 섹션, 그 자식, 에셋에 이 속성을 씁니다(projectRoot 없는 export는 플러그인과 같은 출력 그대로). 끄려면 설정 `figmaNodeIds: false`, 프로덕션에서 빼려면 Next의 `compiler.reactRemoveProperties: { properties: ["^data-figma-node-id$"] }`. +- Figma 노드 속성은 기본으로 끕니다. export 스키마가 지원할 때만 `figmaNodeIds: true`로 켜고, 속성이 없는 검사를 통과로 간주하지 않습니다. - `status`에 `auditPlanPath`·`auditReportPath`를 주면 모든 실패를 분류합니다: `runtime-error`, `overflow`, `layout-position`, `layout-size`, `asset-mismatch`(멀티컬러 SVG에 mask, 잘못된 파일, crop/objectFit 순으로 원인 제시), `section-visual`, `page-visual`, `interaction`, `view-transition` — 각각 Figma 노드, 코드 파일, 구체적인 수정 힌트를 담습니다. 반환된 `visualComparisons` 호출(`threshold`·`diffPath` 포함)을 돌리고 결과를 `visualResults`로 넘깁니다. 안티앨리어싱은 실패가 아닙니다: 바뀐 픽셀 비율 허용치가 페이지·섹션 3%, 에셋 5%이고, 설정 `visualThresholds`(`page`, `section`, `asset`, 노드별 `nodes`)로 조정합니다. - 같은 finding은 새 실행마다 한 번만 시도로 셉니다. `maxFixAttempts`(설정, 기본 3)번 고치고도 실패하면 스크린샷·diff 경로와 함께 `unresolved`로 기록하고 작업 큐에서 뺍니다. `workQueue`는 URL마다 build → audit → fix → compare 순이고 막힌 일은 뒤로 갑니다. - 확정 뒤 생긴 결정은 `action: "defer"`(`decision: {id, about, question, options, recommended, destructive}`)로 남기고 계속 일합니다. 마지막에 `assume`(`about`: 결정 id)으로 추천안을 택하고, 파괴적인 결정은 만들지 않은 채 보고서에 남습니다. 실행기를 설치할 수 없으면 `toolingUnavailable`로 그 URL의 audit를 inconclusive로 두고 계속합니다. @@ -1097,7 +1097,7 @@ Figma 프레임은 정적으로 변환되므로, 그려진 폭에서 크기·위 동적 라우트(`[slug]`, `[id]`, catch-all)는 `notAudited`로 넘기지 않고 실제 URL로 검사합니다: 라우트의 `generateStaticParams`(리터럴, 또는 읽는 로컬 데이터 파일) → 이미 검사한 페이지의 `a[href]`(실행기가 페이지마다 모으고 다음 `auditPlan`이 사용) → 매핑된 Figma 상세 프레임의 제목으로 만든 샘플(가정으로 기록) 순서이며, 10개 이하면 전부 검사합니다. 셋 다 없을 때만 이유와 함께 `notAudited`입니다. -`data-figma-node-id(s)`는 프로덕션에 남지 않습니다. vinext와 devup-ui Vite 플러그인에는 속성을 지우는 옵션이 없어서, `auditPlan`이 각 앱의 vite.config 옆에 작은 Vite 플러그인(`vite-plugin-strip-figma-node-ids.mjs`)을 둡니다. `enforce: 'pre'`, `apply`가 `vite build`일 때만이며 `DEVUP_AUDIT=1`이면 남깁니다. Rolldown은 transform 훅 뒤에 TSX/JSX를 변환하므로 이 단계에서는 속성이 그대로 JSX 텍스트입니다. `plugins: [stripFigmaNodeIds(), vinext()]`로 연결하고, audit는 dev 서버나 `DEVUP_AUDIT=1 bun run build`로 돌리며, 제출 체크리스트는 빌드 출력에서 `grep -rl data-figma-node-id dist`가 아무것도 찾지 않아야 통과합니다. `next` CLI에서는 `compiler.reactRemoveProperties`를 씁니다. +DevupUI가 audit 속성을 기본으로 제거하며 `DEVUP_AUDIT=1`이면 유지합니다. audit를 위해 앱의 빌드 설정을 바꾸거나 별도 strip 플러그인을 등록하지 않습니다. 일반 프로덕션 빌드 산출물에 `data-figma-node-id(s)`가 없는지 확인합니다. 이전 서버가 별도 플러그인 파일을 반환해도 앱 설정에 연결하지 않습니다. 최신 스택이 기본입니다: Next App Router를 vinext(Vite)로 실행하고, bun workspaces(`apps/*`), devup-ui, oxlint(eslint-plugin-devup), 최신 버전. `vinext` 의존성·`vinext` 스크립트·vinext를 import하는 `vite.config`가 있으면 `next` 의존성이 없어도 App Router 앱으로 보고, `devup_project_context`의 ui/routes가 앱별 `frameworks`를 알려 줍니다. 구현 전에는 묻지 않고 저장소 루트에서 `bunx @dependency-check-updates/cli -d -u --rm`(-d: 루트 아래 모든 manifest, -u: 복구 가능한 일괄 적용, --rm: 각 manifest 옆의 lockfile·설치 디렉터리 삭제 — Cargo.lock·target/ 포함) → `bun install` → build·lint·typecheck를 하고, 라이브러리 API는 기억이 아니라 현재 문서를 봅니다. 결과는 `assume`의 `dependencyUpdate`로 남기고 `status`에 dependencies 줄로 나옵니다. 앱이 없으면 `dev-five-git/service-template`에서 시작하고(이름·구성만 질문), `next` CLI 프로젝트에는 "Migrate to vinext (Recommended) / Keep next"를 한 번 묻습니다. 설정 파일의 `framework`·`updateDependencies: false`로 미리 답할 수 있습니다. diff --git a/crates/devup-mcp/src/server/commands/opencode-devup.md b/crates/devup-mcp/src/server/commands/opencode-devup.md index 23060a25..7e7c72ec 100644 --- a/crates/devup-mcp/src/server/commands/opencode-devup.md +++ b/crates/devup-mcp/src/server/commands/opencode-devup.md @@ -146,7 +146,10 @@ For Figma work, continue with the following branch after project intake: 12. Done = zero fails at the drawn widths and no health or overflow fail at any width, or a filed converter bug, or listed as unresolved with evidence. Also every URL implemented, no data-figma-node-id in the - production build (strip plugin in vite.config, grep the output), `bun run build`, `bun run lint`, `bunx oxlint + production build (DevupUI strips attributes by default; `DEVUP_AUDIT=1` + keeps them). Do not change app build config for auditing or register a + strip plugin. Node attributes are off by default; `figmaNodeIds: true` + is opt-in only where supported by the export schema. `bun run build`, `bun run lint`, `bunx oxlint --deny-warnings`, `bun tsc --noEmit` in each changed app and `bun test` pass (`bun run test` adds cargo tarpaulin, which does not work on Windows), devup_ui_validate has no errors, site completeness filled. diff --git a/crates/devup-mcp/src/server/skills/devup-implementation-plan/SKILL.md b/crates/devup-mcp/src/server/skills/devup-implementation-plan/SKILL.md index ab1294b9..c71f8c75 100644 --- a/crates/devup-mcp/src/server/skills/devup-implementation-plan/SKILL.md +++ b/crates/devup-mcp/src/server/skills/devup-implementation-plan/SKILL.md @@ -462,11 +462,10 @@ its computer-use setup yourself. Verify status and a small read before export. test` also runs `cargo tarpaulin` afterwards, which does not work on Windows - there, run `bun test` and `cargo test`; - `devup_ui_validate` reports no errors; - - the production build carries no audit ids: each app's vite.config uses - the `vite-plugin-strip-figma-node-ids.mjs` auditPlan put beside it - (`plugins: [stripFigmaNodeIds(), vinext()]`), and after `bun run build` - `grep -rl data-figma-node-id dist` finds nothing. The audit itself runs - against `bun run dev` or an audit build, `DEVUP_AUDIT=1 bun run build`; + - the production build carries no audit ids: DevupUI strips them by default. + Do not change app build config for auditing or register a strip plugin. + `DEVUP_AUDIT=1` keeps opted-in attributes for audit builds; inspect the + ordinary production output to verify they are absent; - the site completeness block is filled. 12. **Finish with the final report.** End your reply with status's `finalReport`: the URL x width table (health, geometry, drift / converter @@ -549,13 +548,11 @@ evidence and replaces the question it would otherwise take: - `framework` (`vinext` | `next`) answers the migrate-to-vinext question, `updateDependencies: false` skips the dependency step for a project that forbids it, and `viewTransitions: false` opts out of view transitions. -- `figmaNodeIds: false` stops exports from writing `data-figma-node-id(s)` - on the page, section, card and asset elements. The audit finds them by - it, so keep it on. Production builds drop them through the Vite plugin - auditPlan writes beside vite.config (`vite build` strips them unless - `DEVUP_AUDIT=1`); on the `next` CLI use `compiler.reactRemoveProperties: - process.env.DEVUP_AUDIT === '1' ? false : { properties: - ['^data-figma-node-ids?$'] }`. +- Figma node attributes are off by default. Where the exporter supports it, + `figmaNodeIds: true` explicitly opts in; do not send unsupported parameters + to an older server. DevupUI strips these attributes by default and + `DEVUP_AUDIT=1` keeps them. Do not change app build config for auditing. + Missing audit anchors are unverified, not a reason to claim checks passed. - `maxFixAttempts` (default 3) is how many fix-and-rerun rounds a finding gets before it is unresolved. - Geometry has no tolerance to configure; it is exact. diff --git a/crates/devup-mcp/src/server/skills/devup/SKILL.md b/crates/devup-mcp/src/server/skills/devup/SKILL.md index aaacb455..d83a5047 100644 --- a/crates/devup-mcp/src/server/skills/devup/SKILL.md +++ b/crates/devup-mcp/src/server/skills/devup/SKILL.md @@ -195,9 +195,12 @@ the full rules; these are the steps. Every check and comparison passes at the drawn widths and health and overflow pass at every required width. Report filed converter bugs and unresolved findings with evidence as incomplete. Also: every - URL implemented, the production build free of `data-figma-node-id` (the - strip plugin in vite.config; `grep -rl data-figma-node-id dist` finds -nothing). Run `bun run lint:fix` first, then `bunx oxlint --deny-warnings` at the root. + URL implemented, the production build free of `data-figma-node-id`. +DevupUI strips audit attributes by default; `DEVUP_AUDIT=1` keeps them. +Figma node attributes are off by default; opt in with `figmaNodeIds: true` +only where the export schema supports it. Do not change app build config for auditing +or register a strip plugin. Inspect the ordinary production output. +Run `bun run lint:fix` first, then `bunx oxlint --deny-warnings` at the root. Never fix JSX prop order, import sorting or formatting errors by hand. Fix only non-fixable leftovers by hand (type errors, logic and decisions the fixer cannot make). **Formatter ownership:** this template has no standalone prettier config. diff --git a/crates/devup-mcp/src/server/skills/manifest.json b/crates/devup-mcp/src/server/skills/manifest.json index 97558eaa..0134f4e5 100644 --- a/crates/devup-mcp/src/server/skills/manifest.json +++ b/crates/devup-mcp/src/server/skills/manifest.json @@ -89,8 +89,8 @@ "documents": [ { "path": "SKILL.md", - "bytes": 40122, - "sha256": "929b60809198e0714fa0f276ce2f64518d6bc18f8252ad165a26f3aa3f6a82f4" + "bytes": 39945, + "sha256": "c9b48f89651c7eb307df46d8f06d80c2bb6fb1014604f410a1bb657507b75797" } ], "sourceUrl": "https://github.com/dev-five-git/devup-mcp/blob/HEAD/crates/devup-mcp/src/server/skills/devup-implementation-plan", @@ -107,8 +107,8 @@ "documents": [ { "path": "SKILL.md", - "bytes": 19955, - "sha256": "a2571ef07adf6420cbcd634c12161137b5ca68adb57e524e58f3c17ac8c240a5" + "bytes": 20166, + "sha256": "1177f579689acc7c790e5e4c3f72f723e27815f485ee91804b6b2a4bf5620b60" } ], "sourceUrl": "https://github.com/dev-five-git/devup-mcp/blob/HEAD/crates/devup-mcp/src/server/skills/devup", diff --git a/crates/devup-mcp/src/server/text/devup-screen-plan.md b/crates/devup-mcp/src/server/text/devup-screen-plan.md index 8e1b1c0f..3ca5d28d 100644 --- a/crates/devup-mcp/src/server/text/devup-screen-plan.md +++ b/crates/devup-mcp/src/server/text/devup-screen-plan.md @@ -1,5 +1,5 @@ Plan which Figma screen becomes which URL before any page is written, confirm it up front, then report it URL by width (action: draft | confirm | status | assume | auditPlan | defer). draft reads the linked Section or frame (or the Bridge selection, or `screens` from an earlier devup_figma_explore) and proposes a route only from evidence: an existing page built from the frame, a nav link label bound to an existing href, a navigationTargets entry, or .devup/screen-plan.config.json (modalKeywords, routeAliases, widthGrouping). A route inferred from a layer name is routeEvidence name-match and is put to the user; with no evidence it is UNVERIFIED and asked. Only frames under the link are candidates, never the rest of the file; frames in trash, archive, draft, copy or hidden areas are excluded and listed (excludeKeywords/includeKeywords in the config). Frames sharing a name in one section are asked about per group; the same name in different sections, a screen split across sections, a frame outside any section and duplicates each get an explicit question with a "the Figma structure is wrong" option whose frames are reported, never built. It writes .devup/screen-plan.json and .md and returns `proposal` (and `proposalMarkdown`): every open decision as one table row with the recommended answer and the alternatives. Do not interview the user one decision at a time: show the table and ask ONE question with your question tool - accept it, or name the rows to change - in the language the user first wrote in (translate for display only; answers stay keyed by question id with the original labels or custom text). confirm with `acceptRecommended: true` fills every unanswered applicable question with its recommended option, so send only the changed rows in `answers`; without it, confirm refuses and names the missing ids. `questions`/`questionRounds` remain for reviewing individual rows. A confirm that puts two screens on one URL writes nothing and returns `details.conflict` (both screens and the questions placing them) and `details.retryAnswers` with a distinct suggested URL to retry with. After confirm, never ask the user: take the recommended or most conservative option and record it with assume. status compares the confirmed plan with the routes on disk and an optional devup_runtime_audit plan/report: per URL, implemented yes/no and per-width pass, fail with reason, or not-run - a width without an audit item is never pass - plus every assumption. draft also lists the stack defaults - Next App Router on vinext, a new project from dev-five-git/service-template, and the dependency update `bunx @dependency-check-updates/cli -d -u --rm` before implementing - and asks once whether a next-CLI app migrates to vinext; with several workspace apps it picks the app by meaning (the public app unless the frames clearly say admin, per screen in a mixed file) and asks only when no app looks like the public site; then the site defaults it fills in without asking (an interaction recipe per element role, restrained; one shared transition; reduced motion; view transitions always on (React's ViewTransition; skipped only when the project config opts out), verified by a runtime-audit view-transition check; metadata, lang, alt text, not-found) and asks only what needs a file or a fact: favicon and share image (from existing files or Figma logo/og nodes, never generated), site name and URL when the project does not say, analytics when the project shows it, and scroll reveal; `extraQuestions` adds the agent's own questions to the same rounds so nothing is asked after confirm; .devup/screen-plan.config.json may pre-answer them (favicon, ogImage, siteName, siteUrl, motion). status adds site completeness: favicon, metadata per URL, share image, hover/focus coverage and reduced motion as filled, asked-and-answered or missing. auditPlan writes one devup_runtime_audit plan per confirmed URL from the export artifacts: page health, overflow and clipped text at every width; the page, each section and each asset against its Figma render (devup_visual_compare calls returned by status) and the size and position of each section, card and asset at the drawn widths, exactly (half a pixel of rounding, no tolerance); merged pages found per width through data-figma-node-ids; dynamic routes at concrete URLs from generateStaticParams, the site's links or the detail frame; hover and focus; a view-transition check placed last; a Vite plugin beside each app's vite.config that strips the ids from production builds unless DEVUP_AUDIT=1; missing referencePngs and renders are listed with the export call that makes them. status then classifies every failure (runtime-error, overflow, code-drift or converter-bug for geometry - compared with the export record each export writes to .devup/exports, a converter bug written as a repro to .devup/converter-bugs and never retried - layout-position, layout-size, asset-mismatch, section-visual, page-visual, interaction, view-transition) with its Figma node, code file and fix hint, folds visualResults in, marks a finding unresolved with its evidence after maxFixAttempts (config, default 3), and returns a workQueue per URL and the finalReport. defer records a decision met after confirm as pending with its recommended option so the work never waits; assume takes it at the end, and a destructive one stays unbuilt. Done = zero fails, or listed as unresolved with evidence. The devup-implementation-plan skill (devup_skills) carries the whole workflow; /devup starts it. -Before reporting done, run the runtime audit for every confirmed URL at every required width (default 1440/1100/858/390), read the report, fix failures and rerun until every cell passes. Missing cells are not-run and block completion. At drawn widths require rendered completeness and page, section and asset visual comparisons. Check sequence-order and section-layout at every width. +Audit build policy: do not change app build config for auditing or register a strip plugin, including any legacy auditPlan suggestion to do so. DevupUI strips audit attributes by default; DEVUP_AUDIT=1 keeps them. Figma node attributes are off by default; figmaNodeIds true is opt-in only where supported by the export schema. Missing anchors leave coverage unverified. Before reporting done, run the runtime audit for every confirmed URL at every required width (default 1440/1100/858/390), read the report, fix failures and rerun until every cell passes. Missing cells are not-run and block completion. At drawn widths require rendered completeness and page, section and asset visual comparisons. Check sequence-order and section-layout at every width. Ask known site, structure and URL decisions up front in one runtime question call. Keep table approval separate from individual decisions. New user-owned decisions after confirm still use the question tool: headers up to 30 characters, two to four options, recommended first and labelled "(Recommended)". Never ask for numbers in chat. Record the chosen answer, decision id, affected model/endpoint/file and rationale through existing defer/assume actions; do not invent an ask action. Re-confirm source model changes before dependent work. Without a question tool, report the limitation and leave the decision unresolved. Workers escalate to the supervisor; continue independent work while answers are pending. The saved plan, draft, confirm and status expose frameInventory (every listed frame, including excluded, out-of-scope and site assets) and linkLabelCheck. Incomplete exploration is refused. A unique link-label match follows its frame's confirmed route; explicit prototype destinations take priority and ambiguous labels stay unresolved. A found frame without a bound route is still unresolved. Search frameInventory and run devup_figma_search before claiming a screen does not exist; an unlisted name is unverified. Never bind an unresolved label to a guessed route. diff --git a/crates/devup-mcp/src/server/text/guide-GUIDE-c682b0c5.md b/crates/devup-mcp/src/server/text/guide-GUIDE-c682b0c5.md index 4f412641..bd2f7e09 100644 --- a/crates/devup-mcp/src/server/text/guide-GUIDE-c682b0c5.md +++ b/crates/devup-mcp/src/server/text/guide-GUIDE-c682b0c5.md @@ -304,7 +304,10 @@ recorded in `.devup/exports/`, or `converter-bug` when it does - reported with a repro in `.devup/converter-bugs/`, never retried or tuned silently. Merged pages are found per width through `data-figma-node-ids`, dynamic routes at concrete URLs, and production builds strip the ids unless -`DEVUP_AUDIT=1`. `status` classifies each failure (runtime-error, +`DEVUP_AUDIT=1`. DevupUI owns stripping; do not change app build config +for auditing or register a strip plugin. Node attributes are off by default; +`figmaNodeIds: true` is opt-in only where the export schema supports it. +Missing anchors leave coverage unverified. `status` classifies each failure (runtime-error, overflow, layout-position, layout-size, asset-mismatch, section-visual, page-visual, interaction, view-transition) with its node, file and fix hint. Never stop to wait; reorder the work: a decision after confirm goes to diff --git a/crates/devup-mcp/src/server/text/projection-annotate_node_ids-b03e6d56.md b/crates/devup-mcp/src/server/text/projection-annotate_node_ids-b03e6d56.md index 64e6a0da..63b2f912 100644 --- a/crates/devup-mcp/src/server/text/projection-annotate_node_ids-b03e6d56.md +++ b/crates/devup-mcp/src/server/text/projection-annotate_node_ids-b03e6d56.md @@ -1 +1 @@ -Inert data attributes the runtime audit locates sections and assets by. Keep them; to drop them from a production build use Next's compiler.reactRemoveProperties {properties: ["^data-figma-node-id$"]}, or set figmaNodeIds false in .devup/screen-plan.config.json and re-export. \ No newline at end of file +Optional inert Figma node attributes, off by default in the opt-in export workflow. Use figmaNodeIds true only when the export schema supports it. DevupUI strips audit attributes by default; DEVUP_AUDIT=1 keeps them. Do not change app build config for auditing or register a strip plugin. Missing anchors leave coverage unverified. From be4ffbb6dfe5395c43033059a9bc42d245a722e3 Mon Sep 17 00:00:00 2001 From: leader Date: Sat, 24 Oct 2026 02:17:29 +0900 Subject: [PATCH 115/149] fix(screen-plan): refresh plans independently of runner collisions (59) --- .../src/server/runtime_audit/write_plan.rs | 8 +++++--- .../screen_plan_tests/feedback_remaining.rs | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/crates/devup-mcp/src/server/runtime_audit/write_plan.rs b/crates/devup-mcp/src/server/runtime_audit/write_plan.rs index 4f6b0220..a0c916a5 100644 --- a/crates/devup-mcp/src/server/runtime_audit/write_plan.rs +++ b/crates/devup-mcp/src/server/runtime_audit/write_plan.rs @@ -54,7 +54,8 @@ fn write_plan( let mut template_files = Vec::new(); let mut transaction = OutputTransaction::new(); if let Some(directory) = template_path { - transaction.refuse_existing(); + let mut template_transaction = OutputTransaction::new(); + template_transaction.refuse_existing(); for (name, bytes) in [ ("resource-checks.mjs", include_bytes!("../../../../../harness/runtime-audit/resource-checks.mjs").as_slice()), ( @@ -82,8 +83,9 @@ fn write_plan( let requested = Path::new(directory).join(name); let target = policy.resolve(&requested.to_string_lossy())?; template_files.push(json!({"path": target.display_path().display().to_string(), "sha256": sha256(bytes)})); - transaction.stage(name, target, bytes)?; + template_transaction.stage(name, target, bytes)?; } + template_transaction.commit()?; } if let Some(path) = output_path { let target = policy.resolve(path)?; @@ -991,4 +993,4 @@ fn finding( "fixHint": fix_hint(class, kind, measured), "evidence": {"screenshot": screenshot, "measured": measured}, }) -} \ No newline at end of file +} diff --git a/crates/devup-mcp/src/server/screen_plan_tests/feedback_remaining.rs b/crates/devup-mcp/src/server/screen_plan_tests/feedback_remaining.rs index b1430c70..c1b94ea8 100644 --- a/crates/devup-mcp/src/server/screen_plan_tests/feedback_remaining.rs +++ b/crates/devup-mcp/src/server/screen_plan_tests/feedback_remaining.rs @@ -64,3 +64,21 @@ async fn r58_audit_plan_finds_the_confirmed_monorepo_plan_from_front() { assert!(!fixture.0.join("apps/front/.devup/screen-plan.json").exists()); assert!(text(&result["plans"][0]["planPath"]).starts_with(fixture.root())); } + +#[tokio::test] +async fn r59_audit_plan_refreshes_its_output_when_the_runner_needs_recreation() { + let fixture = Fixture::new(); + write_plan_file(&fixture, &confirmed_plan()); + let request = input(&fixture, "auditPlan"); + let store = ArtifactStore::default(); + let first = audit_plan(&request, &fixture.policy(), &store).await.unwrap(); + let path = text(&first["plans"][0]["planPath"]); + let report = Path::new(path).parent().unwrap().join("report/run.log"); + std::fs::create_dir_all(report.parent().unwrap()).unwrap(); + std::fs::write(&report, "keep this report").unwrap(); + std::fs::remove_dir_all(fixture.0.join(".devup/runtime-audit/runner")).unwrap(); + let refreshed = audit_plan(&request, &fixture.policy(), &store).await.unwrap(); + assert_eq!(refreshed["plans"][0]["planPath"], first["plans"][0]["planPath"]); + assert_eq!(std::fs::read_to_string(report).unwrap(), "keep this report"); + assert!(fixture.0.join(".devup/runtime-audit/runner/run.mjs").is_file()); +} From 42b275a643353425170af4d2b273ab3d530e50d6 Mon Sep 17 00:00:00 2001 From: leader Date: Sat, 24 Oct 2026 02:21:47 +0900 Subject: [PATCH 116/149] fix(skills): extract components before assembling pages (65) --- .../src/server/commands/opencode-devup.md | 10 +++++++++- .../skills/devup-implementation-plan/SKILL.md | 19 ++++++++++++++++++- .../src/server/skills/devup/SKILL.md | 11 ++++++++++- .../devup-mcp/src/server/skills/manifest.json | 8 ++++---- .../src/server/text/devup-project-plan.md | 2 ++ .../src/server/text/devup-screen-plan.md | 2 ++ .../src/server/text/guide-GUIDE-c682b0c5.md | 9 +++++++++ .../text/guide-INSTRUCTIONS-49bd7ec6.md | 3 ++- 8 files changed, 56 insertions(+), 8 deletions(-) diff --git a/crates/devup-mcp/src/server/commands/opencode-devup.md b/crates/devup-mcp/src/server/commands/opencode-devup.md index 7e7c72ec..149bebf8 100644 --- a/crates/devup-mcp/src/server/commands/opencode-devup.md +++ b/crates/devup-mcp/src/server/commands/opencode-devup.md @@ -119,7 +119,15 @@ For Figma work, continue with the following branch after project intake: to the new APIs, pin only when no fix exists, and record it with `devup_screen_plan` assume (dependencyUpdate). Check current docs before using any library API. -8. Implement URL by URL: `devup_figma_export` with +8. Extract components before pages: the first implementation unit after confirm + exports used COMPONENT_SET/COMPONENT nodes with `outputs: ["componentTsx"]` + to components/common or components/layout. Record mainComponent id -> generated + file and artifact identity in `.devup/component-map.json`. Assemble pages by + screen-plan section (a direct child of the screen frame root), starting from + generated TSX and mapped imports. Never hand-write TSX from Figma nodes. + Audit through the component/section file mapping and explicit rendered targets, + not DOM Figma attributes. Report unsupported exports/overrides as limitations. + Implement URL by URL: `devup_figma_export` with projectRoot and frameIds, plus the plan's defaults - each element's role recipe (hover, pressed, focus ring, current page, expanded, disabled, loading; restrained), one shared transition, reduced motion, view diff --git a/crates/devup-mcp/src/server/skills/devup-implementation-plan/SKILL.md b/crates/devup-mcp/src/server/skills/devup-implementation-plan/SKILL.md index c71f8c75..3626ea1a 100644 --- a/crates/devup-mcp/src/server/skills/devup-implementation-plan/SKILL.md +++ b/crates/devup-mcp/src/server/skills/devup-implementation-plan/SKILL.md @@ -341,7 +341,24 @@ its computer-use setup yourself. Verify status and a small read before export. Record the outcome with `devup_screen_plan` `assume` (`dependencyUpdate: { bumped, pins }`); status shows it as the dependencies line. Skip only when `.devup/screen-plan.config.json` says `updateDependencies: false`. -6. **Implement URL by URL.** For each confirmed route, call +6. **Extract components before pages.** The first implementation unit after + confirm is component extraction, after the dependency prerequisite above. + Export every used `COMPONENT_SET`/`COMPONENT` with `outputs: ["componentTsx"]` + into `components/common` or `components/layout`, one component per file. + Record each `mainComponent` id -> generated file in `.devup/component-map.json`, + retaining its artifact identity and variant/instance bindings. Reuse these + files instead of duplicating instance bodies or inventing a name-based mapping. + Assemble each confirmed page by screen-plan section: a section is a direct + child of the screen frame root, not the enclosing Figma SECTION. Start from + generated section TSX and wire imports to the recorded component files. + Never hand-write TSX from Figma nodes or reconstruct it from screenshots. + If this server cannot export the required section or preserve an instance + override, record the limitation; do not pretend an unsupported export exists. + Audit through the component/section file mapping and explicit rendered + targets, not DOM Figma attributes. Retain node -> file -> rendered-target + evidence; missing mapping coverage remains unverified, never a pass. + + **Implement URL by URL.** For each confirmed route, call `devup_figma_export` with `projectRoot` and that screen's `frameIds`. With `pageScaffold`, omit `route`: the export takes it from the plan and refuses a different one. Add the plan's `defaults` (states, transition, reduced diff --git a/crates/devup-mcp/src/server/skills/devup/SKILL.md b/crates/devup-mcp/src/server/skills/devup/SKILL.md index d83a5047..a3c97bb0 100644 --- a/crates/devup-mcp/src/server/skills/devup/SKILL.md +++ b/crates/devup-mcp/src/server/skills/devup/SKILL.md @@ -152,7 +152,16 @@ the full rules; these are the steps. Record the outcome with `devup_screen_plan` `assume` (`dependencyUpdate: { bumped, pins }`); status shows it as the dependencies line. Skip only when `.devup/screen-plan.config.json` says `updateDependencies: false`. -8. **Implement URL by URL.** `devup_figma_export` with +8. **Extract components before pages.** The first implementation unit after + confirm exports used `COMPONENT_SET`/`COMPONENT` nodes with + `outputs: ["componentTsx"]` to `components/common` or `components/layout`. + Record each `mainComponent` id -> generated file and artifact identity in + `.devup/component-map.json`. Assemble pages by screen-plan section (a direct + child of the screen frame root), starting from generated TSX and mapped imports. + Never hand-write TSX from Figma nodes. Audit through the component/section + file mapping and explicit rendered targets, not DOM Figma attributes. + Unsupported section exports or instance overrides remain reported limitations. + **Implement URL by URL.** `devup_figma_export` with `projectRoot` and each screen's `frameIds`; `pageScaffold` takes its route from the plan. Add the plan's `defaults` as you go: each interactive element's role recipe (hover, pressed, focus ring, current page, expanded, diff --git a/crates/devup-mcp/src/server/skills/manifest.json b/crates/devup-mcp/src/server/skills/manifest.json index 0134f4e5..1fec446f 100644 --- a/crates/devup-mcp/src/server/skills/manifest.json +++ b/crates/devup-mcp/src/server/skills/manifest.json @@ -89,8 +89,8 @@ "documents": [ { "path": "SKILL.md", - "bytes": 39945, - "sha256": "c9b48f89651c7eb307df46d8f06d80c2bb6fb1014604f410a1bb657507b75797" + "bytes": 41192, + "sha256": "75acf47ca65662fbad0cbbc7713593e0e532f57d3ce2807f143137270af0598d" } ], "sourceUrl": "https://github.com/dev-five-git/devup-mcp/blob/HEAD/crates/devup-mcp/src/server/skills/devup-implementation-plan", @@ -107,8 +107,8 @@ "documents": [ { "path": "SKILL.md", - "bytes": 20166, - "sha256": "1177f579689acc7c790e5e4c3f72f723e27815f485ee91804b6b2a4bf5620b60" + "bytes": 20857, + "sha256": "b97af364c5883d289837db47faf1e26bdb342261b1352e643062c4f949d75abb" } ], "sourceUrl": "https://github.com/dev-five-git/devup-mcp/blob/HEAD/crates/devup-mcp/src/server/skills/devup", diff --git a/crates/devup-mcp/src/server/text/devup-project-plan.md b/crates/devup-mcp/src/server/text/devup-project-plan.md index 6861861b..9c88578a 100644 --- a/crates/devup-mcp/src/server/text/devup-project-plan.md +++ b/crates/devup-mcp/src/server/text/devup-project-plan.md @@ -1,5 +1,7 @@ Project intake and supervisor: intake | draft | confirm | status | assume | defer | split. +Figma execution: the first implementation unit after confirm extracts used COMPONENT_SET/COMPONENT nodes with outputs [componentTsx] into components/common or components/layout. Record mainComponent id -> generated file plus artifact identity in .devup/component-map.json. Assemble pages by screen-plan section (a direct child of the screen frame root) from generated TSX and mapped imports. Never hand-write TSX from Figma nodes. Audit through component/section file mappings and explicit rendered targets, not DOM Figma attributes. Unsupported section exports or instance overrides remain reported limitations, not invented capabilities. + Read Jira/epic children, Figma and devup_project_context all scopes first; pass source-labelled evidence. intake asks work type, optional sources, goal and parts through the runtime question tool. draft records feature-specific rounds and ERD, merges shared question topics, and reports added/changed/kept/removed records in changeSummary. Shared site facts are forwarded to open screen-plan questions; explicit answers take precedence. confirm requires all answers and the Figma screen plan when applicable, writes vespertide models/*.json transactionally, and returns executable recovery for missing answers. split creates a dependency DAG and .devup/units/.md briefs: supervisor-owned shared/table foundations, parallel domain workers, then supervisor integration. Features sharing screen section/menu or entities form one worker owning API, client and pages together; static domains only own front work. Repeated screen parts become sharedParts in stage 0. Each table has one model owner. Briefs prioritize answers and decisions over original scope and filter routes by owned screens (URL or nodeId). Facts keyed .exports, .assets and .imports take precedence, including explicit empty lists; protectedFiles is preserved when supplied. When absent, drafts infer exports from owned routes and detected src/app or apps/*/src/app directories, assets from owned screen/export facts, and imports from sharedParts using components/layout for Header/Footer/Nav and components/common otherwise. Inferred entries carry inferred:true and evidence. Multiple apps require route/app evidence; unresolved app ownership is not guessed. Workers escalate to supervisor by default (unitQuestions: supervisor|worker). diff --git a/crates/devup-mcp/src/server/text/devup-screen-plan.md b/crates/devup-mcp/src/server/text/devup-screen-plan.md index 3ca5d28d..b9feadb4 100644 --- a/crates/devup-mcp/src/server/text/devup-screen-plan.md +++ b/crates/devup-mcp/src/server/text/devup-screen-plan.md @@ -3,4 +3,6 @@ Audit build policy: do not change app build config for auditing or register a st Ask known site, structure and URL decisions up front in one runtime question call. Keep table approval separate from individual decisions. New user-owned decisions after confirm still use the question tool: headers up to 30 characters, two to four options, recommended first and labelled "(Recommended)". Never ask for numbers in chat. Record the chosen answer, decision id, affected model/endpoint/file and rationale through existing defer/assume actions; do not invent an ask action. Re-confirm source model changes before dependent work. Without a question tool, report the limitation and leave the decision unresolved. Workers escalate to the supervisor; continue independent work while answers are pending. The saved plan, draft, confirm and status expose frameInventory (every listed frame, including excluded, out-of-scope and site assets) and linkLabelCheck. Incomplete exploration is refused. A unique link-label match follows its frame's confirmed route; explicit prototype destinations take priority and ambiguous labels stay unresolved. A found frame without a bound route is still unresolved. Search frameInventory and run devup_figma_search before claiming a screen does not exist; an unlisted name is unverified. Never bind an unresolved label to a guessed route. +After confirm, extract used COMPONENT_SET/COMPONENT nodes first with outputs [componentTsx] into components/common or components/layout. Record mainComponent id -> generated file and artifact identity in .devup/component-map.json. Assemble each page from generated TSX by screen-plan section (a direct child of the screen frame root). Never hand-write TSX from Figma nodes. Audit through component/section file mappings and explicit rendered targets, not DOM Figma attributes; missing mapping support stays unverified. Report unsupported section exports or overrides rather than inventing them. + Connected URLs: run `links-resolve` at every width. Each same-origin anchor contributes source URL, accessible label, href, resolved pathname and width to `linkTable` (also Markdown in `finalReport`). GET each distinct target once, including concrete dynamic hrefs, and reject HTTP failures, error pages and `unplanned-target` URLs absent from the confirmed plan and existing routes. Any failing link row blocks done. diff --git a/crates/devup-mcp/src/server/text/guide-GUIDE-c682b0c5.md b/crates/devup-mcp/src/server/text/guide-GUIDE-c682b0c5.md index bd2f7e09..64a00740 100644 --- a/crates/devup-mcp/src/server/text/guide-GUIDE-c682b0c5.md +++ b/crates/devup-mcp/src/server/text/guide-GUIDE-c682b0c5.md @@ -292,6 +292,15 @@ on the `next` CLI is asked once whether to migrate. ## 16. Visual verification of everything, full autonomy +After confirm, the first implementation unit extracts used COMPONENT_SET/COMPONENT +nodes with `outputs: ["componentTsx"]` into `components/common` or +`components/layout`. Record `mainComponent` id -> generated file and artifact +identity in `.devup/component-map.json`. Assemble pages from generated TSX by +screen-plan section (a direct child of the screen frame root). Never hand-write +TSX from Figma nodes. Audit through component/section file mappings and explicit +rendered targets, not DOM Figma attributes. Unsupported section exports or +instance overrides remain reported limitations; missing mapping coverage is unverified. + Visually verify every screen, section and asset at every drawn width. `devup_screen_plan` `auditPlan` writes one runtime-audit plan per URL: page health, overflow and clipped text at every width; the page, each section and diff --git a/crates/devup-mcp/src/server/text/guide-INSTRUCTIONS-49bd7ec6.md b/crates/devup-mcp/src/server/text/guide-INSTRUCTIONS-49bd7ec6.md index 571634ca..575025e5 100644 --- a/crates/devup-mcp/src/server/text/guide-INSTRUCTIONS-49bd7ec6.md +++ b/crates/devup-mcp/src/server/text/guide-INSTRUCTIONS-49bd7ec6.md @@ -2,4 +2,5 @@ Build identity: use server.commit/buildId, not version; reconnect if stale. serv 1. devup-mcp is the primary Figma-to-code source. Connection: bridge -> direct; call devup_figma_auth status first, one Bridge needs no login or url. 2b. /devup starts devup_project_plan intake for any project/feature/fix/refactor. Read Jira/spec/Figma and devup_project_context first; ask every round up front, draft ERD, confirm, then split work units. devup_screen_plan handles the Figma branch. Load devup_skills and devup-implementation-plan; after confirm never wait: defer and continue, assume at the end. 3. For Figma implementation use devup_figma_export tsx, the deliverable. TSX links/events need wiring: resolve interactiveCandidates, devup_ui_validate, report wired/tested counts. deliverable.isFinal describes export readiness. Audit every width and exact layout. Backend completion needs devup_stack_diff zero drift. -4. Request only outputs you read. Before your second call read devup://guide/usage for sizing, verification, batching, assets and delivery. \ No newline at end of file +4. Request only outputs you read. Before your second call read devup://guide/usage for sizing, verification, batching, assets and delivery. +5. After confirm, extract COMPONENT_SET/COMPONENT first with componentTsx into components/common|layout; record mainComponent id -> file in .devup/component-map.json. Assemble generated sections (frame-root children); audit via file mappings, not DOM Figma attributes. Never hand-write TSX from Figma nodes. Report unsupported exports. From 53df39deb378243bf7b586394a947f607ff33861 Mon Sep 17 00:00:00 2001 From: leader Date: Sat, 24 Oct 2026 02:23:31 +0900 Subject: [PATCH 117/149] fix(screen-plan): gate audit checks on source anchors and references (63) --- .../server/audit_plan/planning_and_capture.rs | 57 +++++++++++++++++++ .../devup-mcp/src/server/audit_plan_tests.rs | 22 +++++++ .../runtime_audit/inputs_and_capture.rs | 8 ++- .../src/server/runtime_audit/write_plan.rs | 4 +- harness/runtime-audit/checks.mjs | 1 + harness/runtime-audit/run.mjs | 4 ++ harness/runtime-audit/test/browser.test.mjs | 4 +- 7 files changed, 96 insertions(+), 4 deletions(-) diff --git a/crates/devup-mcp/src/server/audit_plan/planning_and_capture.rs b/crates/devup-mcp/src/server/audit_plan/planning_and_capture.rs index a302f1b4..dbec2d3c 100644 --- a/crates/devup-mcp/src/server/audit_plan/planning_and_capture.rs +++ b/crates/devup-mcp/src/server/audit_plan/planning_and_capture.rs @@ -327,6 +327,7 @@ pub(super) fn url_plan( let mut nav_checked = false; for (width, frame_id, _) in &target.frames { let Some((artifact_id, payload)) = sources.frame(frame_id) else { + checks.push(check(json!({"kind": "page-health", "label": "design reference unavailable", "widths": [width], "unavailableReason": "Design comparison inconclusive: frame artifact missing or expired; export the frame again."}))); missing.push(json!({"kind": "frame", "nodeId": frame_id, "width": width, "why": "No artifact passed holds this frame; section, asset and geometry checks for this width are missing.", "call": {"tool": "devup_figma_export", "arguments": {"url": format!(""), "outputs": ["tsx", "referencePng"], "projectRoot": ""}}})); @@ -335,6 +336,7 @@ pub(super) fn url_plan( }; let reference = sources.reference_png(frame_id); if reference.is_none() { + checks.push(check(json!({"kind": "page-health", "label": "design reference unavailable", "widths": [width], "unavailableReason": "Design comparison inconclusive: referencePng missing or expired; export the frame again."}))); missing.push(json!({"kind": "referencePng", "nodeId": frame_id, "width": width, "why": format!("No referencePng for the frame drawn at {width}px, so its page comparison cannot run."), "call": {"tool": "devup_figma_export", "arguments": {"url": format!(""), "outputs": ["referencePng"]}}})); @@ -634,6 +636,60 @@ fn capture( ) } +fn gate_source_anchors(built: &mut UrlPlan, app: &Path) { + fn scan(path: &Path, text: &mut String) { + let Ok(entries) = std::fs::read_dir(path) else { return }; + for entry in entries.flatten() { + let path = entry.path(); + if entry.file_type().is_ok_and(|kind| kind.is_symlink()) { + continue; + } + if path.is_dir() { + scan(&path, text); + } else if matches!(path.extension().and_then(|ext| ext.to_str()), Some("tsx" | "jsx" | "ts" | "js")) + && let Ok(source) = std::fs::read_to_string(path) { + text.push_str(&source); + text.push('\n'); + } + } + } + let mut source = String::new(); + scan(&app.join("src"), &mut source); + if !app.join("src").exists() { + scan(&app.join("app"), &mut source); + scan(&app.join("components"), &mut source); + } + let mut anchors = BTreeSet::new(); + for tail in source.split("data-figma-node-id").skip(1) { + let (plural, tail) = tail.strip_prefix('s').map_or((false, tail), |tail| (true, tail)); + let Some(tail) = tail.trim_start().strip_prefix('=') else { continue }; + let tail = tail.trim_start(); + let Some(quote @ ('\'' | '"')) = tail.chars().next() else { continue }; + if let Some(value) = tail[1..].split(quote).next() { + for value in value.split_whitespace() { + let id = if plural { value.split_once(':').map_or(value, |(_, id)| id) } else { value }; + anchors.insert(id.to_owned()); + } + } + } + for check in &mut built.plan.screens[0].checks { + let anchored = [&check.subject, &check.container].into_iter().flatten() + .filter_map(|locator| locator.css.as_deref()) + .any(|css| css.contains("data-figma-node-id")); + let selectors = [&check.subject, &check.container].into_iter().flatten().filter_map(|locator| locator.css.as_deref()).collect::>(); + let missing = selectors.iter().any(|css| { + css.split('"').enumerate().filter(|(index, _)| index % 2 == 1) + .all(|(_, id)| !anchors.contains(id) && !id.split_once(':').is_some_and(|(_, node)| anchors.contains(node))) + }); + let completeness_missing = check.completeness.as_ref().is_some_and(|complete| complete.sections.iter().chain(&complete.images).any(|id| !anchors.contains(id))); + if (anchored && missing) || completeness_missing { + check.unavailable_reason = Some("not-run: source anchors absent; data-figma-node-id is off by default. Export with figmaNodeIds:true and preserve anchors, or use explicit role/testId locators.".into()); + check.subject = None; + check.container = None; + } + } +} + pub(super) fn slug(url: &str) -> String { let trimmed = url.trim_matches('/'); if trimmed.is_empty() { @@ -783,6 +839,7 @@ pub(super) async fn write( }; let base = base_urls.get(app).map_or(base_url, String::as_str); let mut built = url_plan(&target, base, &sources, thresholds)?; + gate_source_anchors(&mut built, &Path::new(root).join(app)); if let Some(node) = route["state"]["nodeId"].as_str() { built.notes.push(format!("State {} ({node}) on {url}: configure the runtime screen's steps to reach this state before running; no interaction is inferred from the frame name.", route["state"]["name"].as_str().unwrap_or_default())); for check in &mut built.plan.screens[0].checks { diff --git a/crates/devup-mcp/src/server/audit_plan_tests.rs b/crates/devup-mcp/src/server/audit_plan_tests.rs index 28c83ff4..bbce6231 100644 --- a/crates/devup-mcp/src/server/audit_plan_tests.rs +++ b/crates/devup-mcp/src/server/audit_plan_tests.rs @@ -1,5 +1,27 @@ use super::*; +#[test] +fn absent_source_anchors_leave_design_checks_not_run() { + let project = Project::new("missing-anchors"); + project.write("src/app/page.tsx", "export default function Page() { return
}"); + let mut built = url_plan(&home(), "http://localhost:3000", &sources(), &VisualThresholds::default()).unwrap(); + gate_source_anchors(&mut built, Path::new(project.root())); + let checks = &built.plan.screens[0].checks; + assert!(checks.iter().any(|check| check.kind == "bounds" && check.unavailable_reason.as_deref().is_some_and(|reason| reason.contains("anchors absent")))); + assert!(checks.iter().filter(|check| check.unavailable_reason.is_some()).all(|check| check.subject.is_none() && check.container.is_none())); + assert!(checks.iter().any(|check| check.kind == "page-health" && check.unavailable_reason.is_none())); +} + +#[test] +fn partial_source_anchors_do_not_enable_unmatched_nodes() { + let project = Project::new("partial-anchors"); + project.write("src/app/page.tsx", "
"); + let mut built = url_plan(&home(), "http://localhost:3000", &sources(), &VisualThresholds::default()).unwrap(); + gate_source_anchors(&mut built, Path::new(project.root())); + assert!(built.plan.screens[0].checks.iter().any(|check| check.kind == "bounds" && check.node_id.as_deref() == Some("10:12") && check.unavailable_reason.is_none())); + assert!(built.plan.screens[0].checks.iter().filter(|check| check.node_id.as_deref() == Some("10:131")).all(|check| check.unavailable_reason.is_some())); +} + fn payload(target: &str, nodes: Value, assets: Value, reference: bool) -> CollectedPayload { let mut value = json!({ "target": {"fileKey": "audit", "nodeId": target}, diff --git a/crates/devup-mcp/src/server/runtime_audit/inputs_and_capture.rs b/crates/devup-mcp/src/server/runtime_audit/inputs_and_capture.rs index 67cde63a..463d11f7 100644 --- a/crates/devup-mcp/src/server/runtime_audit/inputs_and_capture.rs +++ b/crates/devup-mcp/src/server/runtime_audit/inputs_and_capture.rs @@ -181,6 +181,9 @@ pub struct ExpectInput { #[serde(rename_all = "camelCase", deny_unknown_fields)] #[schemars(extend("additionalProperties" = json!({"not": {}})))] pub struct CheckInput { + /// Planning prerequisite missing: do not execute this check or count it as passed. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub unavailable_reason: Option, /// no-horizontal-overflow: the page is no wider than the viewport. /// contained: subject's box lies inside container's. /// aligned: subject keeps offsetPx from container's edge (left/right) or @@ -608,6 +611,9 @@ pub(crate) fn validate_plan(plan: &PlanInput) -> Result<(), DevupError> { } fn validate_check(check: &CheckInput) -> Result<(), DevupError> { + if let Some(reason) = &check.unavailable_reason { + return bounded_text("unavailableReason", reason); + } let needs = |subject: bool, container: bool| -> Result<(), DevupError> { if subject != check.subject.is_some() || container != check.container.is_some() { return Err(invalid(format!( @@ -993,4 +999,4 @@ pub(crate) fn order_checks(input: &mut PlanInput) { .checks .sort_by_key(|check| check.kind == "view-transition"); } -} \ No newline at end of file +} diff --git a/crates/devup-mcp/src/server/runtime_audit/write_plan.rs b/crates/devup-mcp/src/server/runtime_audit/write_plan.rs index 755cb340..2c7d4003 100644 --- a/crates/devup-mcp/src/server/runtime_audit/write_plan.rs +++ b/crates/devup-mcp/src/server/runtime_audit/write_plan.rs @@ -348,7 +348,7 @@ fn ingest_references( && reported.is_some_and(|s| s["status"] == "completed"), ) }); - let status = match dimension + let status = if let Some(reason) = &check.unavailable_reason { if reason.starts_with("not-run:") { "not-run" } else { "inconclusive" } } else { match dimension .as_ref() .map(|row| &row["status"]) .or_else(|| item.map(|item| &item["status"])) @@ -361,7 +361,7 @@ fn ingest_references( Some("not-run") => "not-run", Some(_) => "inconclusive", None => "not-run", - }; + }}; *counts.entry(status).or_default() += 1; if status == "fail" && failures.len() < MAX_LISTED { failures.push(json!({ diff --git a/harness/runtime-audit/checks.mjs b/harness/runtime-audit/checks.mjs index 53080271..efaeef88 100644 --- a/harness/runtime-audit/checks.mjs +++ b/harness/runtime-audit/checks.mjs @@ -453,6 +453,7 @@ export function summarize(runs) { for (const run of runs) { for (const screen of run.screens) { for (const item of screen.checks) { + if (item.status === 'not-run') summary.skipped += 1; if (item.status in summary) summary[item.status] += 1; } } diff --git a/harness/runtime-audit/run.mjs b/harness/runtime-audit/run.mjs index 3af4f22d..ae5b90dd 100644 --- a/harness/runtime-audit/run.mjs +++ b/harness/runtime-audit/run.mjs @@ -807,6 +807,10 @@ async function runViewport(browser, plan, viewport, out, deadline, references, r if (check.kind === 'view-transition') continue; if (Array.isArray(check.widths) && check.widths.length && !check.widths.includes(viewport.width)) continue; const item = { index, kind: check.kind, label: check.label }; + if (check.unavailableReason) { + record.checks.push({ ...item, status: check.unavailableReason.startsWith('not-run:') ? 'not-run' : 'inconclusive', reason: check.unavailableReason, measured: {} }); + continue; + } if (check.kind === 'links-resolve') continue; try { if (check.kind === 'page-health') { diff --git a/harness/runtime-audit/test/browser.test.mjs b/harness/runtime-audit/test/browser.test.mjs index 2bcee37e..f1183e97 100644 --- a/harness/runtime-audit/test/browser.test.mjs +++ b/harness/runtime-audit/test/browser.test.mjs @@ -40,11 +40,13 @@ test('a measurement exception is inconclusive and later checks still run', async screens: [{ id: 'home', routePath: '/', checks: [ { kind: 'text-align', subject: { css: '[' }, edge: 'left' }, { kind: 'page-health' }, + { kind: 'bounds', unavailableReason: 'not-run: source anchors absent' }, + { kind: 'page-health', unavailableReason: 'Design comparison inconclusive: reference expired' }, ] }], } })); const report = await runPlan({ plan, out, maxMinutes: 1 }); assert.equal(report.status, 'completed'); - assert.deepEqual(report.runs[0].screens[0].checks.map((check) => check.status), ['inconclusive', 'pass']); + assert.deepEqual(report.runs[0].screens[0].checks.map((check) => check.status), ['inconclusive', 'pass', 'not-run', 'inconclusive']); assert.match(report.runs[0].screens[0].checks[0].reason, /home\/800\/check 0/); } finally { server.close(); From e74d9cd06a75040606838ceb4cd607ad43c16c96 Mon Sep 17 00:00:00 2001 From: leader Date: Sat, 24 Oct 2026 02:27:00 +0900 Subject: [PATCH 118/149] fix(skills): preserve screen-plan question guidance (42) --- .../src/server/text/screen_plan-ASK_INSTRUCTION-0285b268.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/devup-mcp/src/server/text/screen_plan-ASK_INSTRUCTION-0285b268.md b/crates/devup-mcp/src/server/text/screen_plan-ASK_INSTRUCTION-0285b268.md index 0d743267..2b590757 100644 --- a/crates/devup-mcp/src/server/text/screen_plan-ASK_INSTRUCTION-0285b268.md +++ b/crates/devup-mcp/src/server/text/screen_plan-ASK_INSTRUCTION-0285b268.md @@ -1 +1 @@ -Ask known decisions together before implementing. Show the `proposal` table and use the runtime question tool, never numbered chat questions. Keep table approval and individual decisions as separate entries in the same call: headers up to 30 characters, two to four options, recommended first and labelled "(Recommended)". Ask in the user's language; translate for display only, keeping question ids and original answer labels (or custom routes) for confirm. After approval, confirm with `acceptRecommended: true` and only changed rows in `answers`. New user-owned decisions after confirm still require the question tool. Record the chosen answer, id, affected model/endpoint/file and rationale with existing defer/assume actions; do not invent an ask action. Update source models and re-confirm ERD changes before dependent work. Without a question tool, report the limitation and leave the decision unresolved. Workers escalate to the supervisor; continue independent work while answers are pending. Ordinary implementation choices use assume. +Ask known decisions together before implementing. Show the `proposal` table and use the runtime question tool, never numbered chat questions. Keep table approval and individual decisions as separate entries in the same call: headers up to 30 characters, two to four options, recommended first and labelled "(Recommended)". Ask in the user's language; translate for display only, keeping question ids and original answer labels (or custom routes) for confirm. After approval, confirm with `acceptRecommended: true` and only changed rows in `answers`. After confirm never ask numbered chat questions; new user-owned decisions still require the question tool. Record the chosen answer, id, affected model/endpoint/file and rationale with existing defer/assume actions; do not invent an ask action. Update source models and re-confirm ERD changes before dependent work. Without a question tool, report the limitation and leave the decision unresolved. Workers escalate to the supervisor; continue independent work while answers are pending. Ordinary implementation choices use assume. From c92c6e9a8ece7786cb8c6683739ed7108832c201 Mon Sep 17 00:00:00 2001 From: leader Date: Sat, 24 Oct 2026 02:27:12 +0900 Subject: [PATCH 119/149] fix(skills): recognize bridge setup as an own skill in install test (48) --- crates/devup-mcp/tests/skills_install.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/crates/devup-mcp/tests/skills_install.rs b/crates/devup-mcp/tests/skills_install.rs index a4fe1831..65e99ad1 100644 --- a/crates/devup-mcp/tests/skills_install.rs +++ b/crates/devup-mcp/tests/skills_install.rs @@ -138,8 +138,13 @@ async fn a_bare_workspace_reports_the_gap_and_one_call_closes_it() -> anyhow::Re for entry in written { assert_eq!(entry["source"], "embedded"); // Skills authored in this repository have no upstream to fetch from. - if !["devfive-frontend", "devup-implementation-plan", "devup"] - .contains(&entry["name"].as_str().unwrap()) + if ![ + "devfive-frontend", + "devup-implementation-plan", + "devup", + "figma-bridge-setup", + ] + .contains(&entry["name"].as_str().unwrap()) { assert!( entry["reason"] From 9f926f912f327fd9c5150f6686821de3fa6134d3 Mon Sep 17 00:00:00 2001 From: leader Date: Sat, 24 Oct 2026 02:27:51 +0900 Subject: [PATCH 120/149] fix(screen-plan): validate plan before writing runner files (59) --- .../src/server/runtime_audit/write_plan.rs | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/crates/devup-mcp/src/server/runtime_audit/write_plan.rs b/crates/devup-mcp/src/server/runtime_audit/write_plan.rs index a0c916a5..e43a653f 100644 --- a/crates/devup-mcp/src/server/runtime_audit/write_plan.rs +++ b/crates/devup-mcp/src/server/runtime_audit/write_plan.rs @@ -53,6 +53,19 @@ fn write_plan( let mut written = None; let mut template_files = Vec::new(); let mut transaction = OutputTransaction::new(); + if let Some(path) = output_path { + let target = policy.resolve(path)?; + let display = target.display_path().display().to_string(); + let bytes = serde_json::to_vec_pretty(&document) + .map_err(|error| invalid(format!("Cannot serialize the plan: {error}")))?; + if bytes.len() > MAX_PLAN_BYTES { + return Err(invalid( + "The bound plan exceeds the ingest size limit; split the plan.", + )); + } + transaction.stage("plan", target, &bytes)?; + written = Some(display); + } if let Some(directory) = template_path { let mut template_transaction = OutputTransaction::new(); template_transaction.refuse_existing(); @@ -87,19 +100,6 @@ fn write_plan( } template_transaction.commit()?; } - if let Some(path) = output_path { - let target = policy.resolve(path)?; - let display = target.display_path().display().to_string(); - let bytes = serde_json::to_vec_pretty(&document) - .map_err(|error| invalid(format!("Cannot serialize the plan: {error}")))?; - if bytes.len() > MAX_PLAN_BYTES { - return Err(invalid( - "The bound plan exceeds the ingest size limit; split the plan.", - )); - } - transaction.stage("plan", target, &bytes)?; - written = Some(display); - } transaction.commit()?; let mut result = document; result["writtenTo"] = json!(written); From 53440e9202eb373f635ca0c2109ca2dd254eb754 Mon Sep 17 00:00:00 2001 From: leader Date: Sat, 24 Oct 2026 02:45:09 +0900 Subject: [PATCH 121/149] fix(runtime-audit): update emitted module integration contract (61) --- crates/devup-mcp/tests/runtime_audit_tool.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/devup-mcp/tests/runtime_audit_tool.rs b/crates/devup-mcp/tests/runtime_audit_tool.rs index ba96024d..b95f8c5e 100644 --- a/crates/devup-mcp/tests/runtime_audit_tool.rs +++ b/crates/devup-mcp/tests/runtime_audit_tool.rs @@ -176,7 +176,7 @@ async fn consumer_template_is_allowlisted_and_missing_acquisitions_never_pass() assert!(!failed, "{planned}"); assert_eq!(planned["template"]["executed"], false); assert_eq!(planned["dimensionReferences"][0]["status"], "unverified"); - assert_eq!(planned["template"]["files"].as_array().unwrap().len(), 6); + assert_eq!(planned["template"]["files"].as_array().unwrap().len(), 7); let runner = std::fs::read_to_string(root.join("consumer-audit/run.mjs"))?; assert!(runner.contains("runPlan")); let manifest: Value = From 64597fea3fa64d807ca2d6243b0a8530be51cafd Mon Sep 17 00:00:00 2001 From: leader Date: Sat, 24 Oct 2026 02:45:22 +0900 Subject: [PATCH 122/149] fix(screen-plan): return exact app dev server start commands (60) --- crates/devup-mcp/src/server/audit_plan.rs | 1 + .../server/audit_plan/planning_and_capture.rs | 60 +--------------- .../src/server/audit_plan/prerequisites.rs | 68 +++++++++++++++++++ .../devup-mcp/src/server/audit_plan_tests.rs | 12 ++++ .../runtime_audit/inputs_and_capture.rs | 3 - ...is_vinext_with_view_transitions_enabled.rs | 2 +- 6 files changed, 83 insertions(+), 63 deletions(-) create mode 100644 crates/devup-mcp/src/server/audit_plan/prerequisites.rs diff --git a/crates/devup-mcp/src/server/audit_plan.rs b/crates/devup-mcp/src/server/audit_plan.rs index 8705e28e..04f980bf 100644 --- a/crates/devup-mcp/src/server/audit_plan.rs +++ b/crates/devup-mcp/src/server/audit_plan.rs @@ -11,3 +11,4 @@ // Source sections share this module to preserve privacy and test names. include!("audit_plan/planning_and_capture.rs"); include!("audit_plan/static_params.rs"); +include!("audit_plan/prerequisites.rs"); diff --git a/crates/devup-mcp/src/server/audit_plan/planning_and_capture.rs b/crates/devup-mcp/src/server/audit_plan/planning_and_capture.rs index dbec2d3c..9532de9c 100644 --- a/crates/devup-mcp/src/server/audit_plan/planning_and_capture.rs +++ b/crates/devup-mcp/src/server/audit_plan/planning_and_capture.rs @@ -636,60 +636,6 @@ fn capture( ) } -fn gate_source_anchors(built: &mut UrlPlan, app: &Path) { - fn scan(path: &Path, text: &mut String) { - let Ok(entries) = std::fs::read_dir(path) else { return }; - for entry in entries.flatten() { - let path = entry.path(); - if entry.file_type().is_ok_and(|kind| kind.is_symlink()) { - continue; - } - if path.is_dir() { - scan(&path, text); - } else if matches!(path.extension().and_then(|ext| ext.to_str()), Some("tsx" | "jsx" | "ts" | "js")) - && let Ok(source) = std::fs::read_to_string(path) { - text.push_str(&source); - text.push('\n'); - } - } - } - let mut source = String::new(); - scan(&app.join("src"), &mut source); - if !app.join("src").exists() { - scan(&app.join("app"), &mut source); - scan(&app.join("components"), &mut source); - } - let mut anchors = BTreeSet::new(); - for tail in source.split("data-figma-node-id").skip(1) { - let (plural, tail) = tail.strip_prefix('s').map_or((false, tail), |tail| (true, tail)); - let Some(tail) = tail.trim_start().strip_prefix('=') else { continue }; - let tail = tail.trim_start(); - let Some(quote @ ('\'' | '"')) = tail.chars().next() else { continue }; - if let Some(value) = tail[1..].split(quote).next() { - for value in value.split_whitespace() { - let id = if plural { value.split_once(':').map_or(value, |(_, id)| id) } else { value }; - anchors.insert(id.to_owned()); - } - } - } - for check in &mut built.plan.screens[0].checks { - let anchored = [&check.subject, &check.container].into_iter().flatten() - .filter_map(|locator| locator.css.as_deref()) - .any(|css| css.contains("data-figma-node-id")); - let selectors = [&check.subject, &check.container].into_iter().flatten().filter_map(|locator| locator.css.as_deref()).collect::>(); - let missing = selectors.iter().any(|css| { - css.split('"').enumerate().filter(|(index, _)| index % 2 == 1) - .all(|(_, id)| !anchors.contains(id) && !id.split_once(':').is_some_and(|(_, node)| anchors.contains(node))) - }); - let completeness_missing = check.completeness.as_ref().is_some_and(|complete| complete.sections.iter().chain(&complete.images).any(|id| !anchors.contains(id))); - if (anchored && missing) || completeness_missing { - check.unavailable_reason = Some("not-run: source anchors absent; data-figma-node-id is off by default. Export with figmaNodeIds:true and preserve anchors, or use explicit role/testId locators.".into()); - check.subject = None; - check.container = None; - } - } -} - pub(super) fn slug(url: &str) -> String { let trimmed = url.trim_matches('/'); if trimmed.is_empty() { @@ -707,18 +653,15 @@ pub(super) fn slug(url: &str) -> String { .collect() } -/// What auditPlan was asked for. pub(super) struct AuditRequest<'a> { pub root: &'a str, pub base_url: &'a str, pub base_urls: &'a BTreeMap, - /// Only these URLs; empty means every confirmed URL. pub only: &'a [String], pub artifact_ids: &'a [String], pub thresholds: &'a VisualThresholds, } -/// Writes every URL's plan, the runner once, and returns what to run. pub(super) async fn write( plan: &Value, request: &AuditRequest<'_>, @@ -740,8 +683,6 @@ pub(super) async fn write( .filter(|route| route["implementation"] == "page" && route["url"].is_string()) .collect::>(); let records = super::export_record::records(Path::new(root)); - // Every route as the concrete URLs it is audited at: a dynamic route at - // the URLs its params, the site's links or its Figma frame give. let mut concrete = Vec::<(usize, String, Option)>::new(); let mut not_audited = Vec::new(); for (index, route) in routes.iter().enumerate() { @@ -888,6 +829,7 @@ pub(super) async fn write( "notes": built.notes, "dynamic": dynamic, "run": ["node", "run.mjs", "--plan", plan_path.display().to_string(), "--out", report_dir.display().to_string()], + "startServer": dev_server_command(Path::new(root), app, base), })); } let missing = written diff --git a/crates/devup-mcp/src/server/audit_plan/prerequisites.rs b/crates/devup-mcp/src/server/audit_plan/prerequisites.rs new file mode 100644 index 00000000..e4f18d33 --- /dev/null +++ b/crates/devup-mcp/src/server/audit_plan/prerequisites.rs @@ -0,0 +1,68 @@ +fn gate_source_anchors(built: &mut UrlPlan, app: &Path) { + fn scan(path: &Path, text: &mut String) { + let Ok(entries) = std::fs::read_dir(path) else { return }; + for entry in entries.flatten() { + let path = entry.path(); + if entry.file_type().is_ok_and(|kind| kind.is_symlink()) { continue; } + if path.is_dir() { scan(&path, text); } + else if matches!(path.extension().and_then(|ext| ext.to_str()), Some("tsx" | "jsx" | "ts" | "js")) + && let Ok(source) = std::fs::read_to_string(path) { + text.push_str(&source); + text.push('\n'); + } + } + } + let mut source = String::new(); + scan(&app.join("src"), &mut source); + if !app.join("src").exists() { + scan(&app.join("app"), &mut source); + scan(&app.join("components"), &mut source); + } + let mut anchors = BTreeSet::new(); + for tail in source.split("data-figma-node-id").skip(1) { + let (plural, tail) = tail.strip_prefix('s').map_or((false, tail), |tail| (true, tail)); + let Some(tail) = tail.trim_start().strip_prefix('=') else { continue }; + let tail = tail.trim_start(); + let Some(quote @ ('\'' | '"')) = tail.chars().next() else { continue }; + if let Some(value) = tail[1..].split(quote).next() { + for value in value.split_whitespace() { + let id = if plural { value.split_once(':').map_or(value, |(_, id)| id) } else { value }; + anchors.insert(id.to_owned()); + } + } + } + for check in &mut built.plan.screens[0].checks { + let anchored = [&check.subject, &check.container].into_iter().flatten() + .filter_map(|locator| locator.css.as_deref()).any(|css| css.contains("data-figma-node-id")); + let missing = [&check.subject, &check.container].into_iter().flatten().filter_map(|locator| locator.css.as_deref()).any(|css| { + css.split('"').enumerate().filter(|(index, _)| index % 2 == 1) + .all(|(_, id)| !anchors.contains(id) && !id.split_once(':').is_some_and(|(_, node)| anchors.contains(node))) + }); + let completeness_missing = check.completeness.as_ref().is_some_and(|complete| complete.sections.iter().chain(&complete.images).any(|id| !anchors.contains(id))); + if (anchored && missing) || completeness_missing { + check.unavailable_reason = Some("not-run: source anchors absent; data-figma-node-id is off by default. Export with figmaNodeIds:true and preserve anchors, or use explicit role/testId locators.".into()); + check.subject = None; + check.container = None; + } + } +} + +fn dev_server_command(root: &Path, app: &str, base_url: &str) -> Value { + let cwd = root.join(app); + let package = std::fs::read(cwd.join("package.json")).ok().and_then(|bytes| serde_json::from_slice::(&bytes).ok()); + let Some(script) = package.as_ref().and_then(|package| package["scripts"]["dev"].as_str()) else { + return json!({"available": false, "cwd": cwd, "reason": "The route app has no package.json dev script; no start command was guessed."}); + }; + let manager = package.as_ref().and_then(|package| package["packageManager"].as_str()) + .and_then(|value| value.split('@').next()).filter(|manager| matches!(*manager, "bun" | "npm" | "pnpm" | "yarn")) + .unwrap_or_else(|| { + if ["bun.lock", "bun.lockb"].iter().any(|name| cwd.join(name).exists() || root.join(name).exists()) { "bun" } + else if cwd.join("pnpm-lock.yaml").exists() || root.join("pnpm-lock.yaml").exists() { "pnpm" } + else if cwd.join("yarn.lock").exists() || root.join("yarn.lock").exists() { "yarn" } + else { "npm" } + }); + json!({"available": true, "command": [manager, "run", "dev"], "cwd": cwd, "script": script, + "windowsCommand": format!("cmd.exe /d /s /c \"{manager} run dev\""), + "readyUrl": base_url, "readyTimeoutMs": 120000, "reuseExisting": true, + "instructions": "Run command in cwd only when readyUrl is unreachable; wait for HTTP 200 before the runner. Stop only the process tree you started after the audit."}) +} diff --git a/crates/devup-mcp/src/server/audit_plan_tests.rs b/crates/devup-mcp/src/server/audit_plan_tests.rs index bbce6231..2fd09232 100644 --- a/crates/devup-mcp/src/server/audit_plan_tests.rs +++ b/crates/devup-mcp/src/server/audit_plan_tests.rs @@ -1,5 +1,17 @@ use super::*; +#[test] +fn dev_server_command_uses_the_route_app_and_its_package_manager() { + let project = Project::new("dev-command"); + project.write("bun.lock", ""); + project.write("apps/front/package.json", r#"{"scripts":{"dev":"vinext dev"}}"#); + let command = dev_server_command(Path::new(project.root()), "apps/front", "http://localhost:3000"); + assert_eq!(command["command"], json!(["bun", "run", "dev"])); + assert!(command["cwd"].as_str().unwrap().ends_with("apps\\front") || command["cwd"].as_str().unwrap().ends_with("apps/front")); + assert_eq!(command["script"], "vinext dev"); + assert!(command["windowsCommand"].as_str().unwrap().contains("cmd.exe /d /s /c")); +} + #[test] fn absent_source_anchors_leave_design_checks_not_run() { let project = Project::new("missing-anchors"); diff --git a/crates/devup-mcp/src/server/runtime_audit/inputs_and_capture.rs b/crates/devup-mcp/src/server/runtime_audit/inputs_and_capture.rs index 463d11f7..79edf4c3 100644 --- a/crates/devup-mcp/src/server/runtime_audit/inputs_and_capture.rs +++ b/crates/devup-mcp/src/server/runtime_audit/inputs_and_capture.rs @@ -25,7 +25,6 @@ const MAX_REPORT_BYTES: usize = 8 * 1024 * 1024; const MAX_SCREENSHOT_BYTES: usize = 16 * 1024 * 1024; const MAX_SCREENSHOTS: usize = 400; const MAX_LISTED: usize = 50; -// Keep complete per-width rows in the response without unbounded duplication. const MAX_DIMENSION_ROWS: usize = 100; #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] @@ -275,10 +274,8 @@ pub struct CheckInput { /// was drawn. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub widths: Vec, - /// The Figma node this check is about, so a failure names it. #[serde(default, skip_serializing_if = "Option::is_none")] pub node_id: Option, - /// capture: what the element screenshot is compared with. #[serde(default, skip_serializing_if = "Option::is_none")] pub reference: Option, /// rendered-completeness: what the frame drawn at this width says must be diff --git a/crates/devup-mcp/src/server/screen_plan_tests/a_service_template_project_is_vinext_with_view_transitions_enabled.rs b/crates/devup-mcp/src/server/screen_plan_tests/a_service_template_project_is_vinext_with_view_transitions_enabled.rs index 70f7785b..7dc6cb0c 100644 --- a/crates/devup-mcp/src/server/screen_plan_tests/a_service_template_project_is_vinext_with_view_transitions_enabled.rs +++ b/crates/devup-mcp/src/server/screen_plan_tests/a_service_template_project_is_vinext_with_view_transitions_enabled.rs @@ -481,7 +481,7 @@ async fn audit_plan_writes_a_plan_per_url_and_reports_what_it_cannot_compare() { ); let home = &plans[0]; assert_eq!(home["checks"]["view-transition"], 1); - assert_eq!(home["checks"]["page-health"], 1); + assert_eq!(home["checks"]["page-health"], 3); let written = read_plan(fixture.root()).unwrap().unwrap(); assert_eq!(array(&written["audit"]["plans"]).len(), 2); assert!(text(&written["audit"]["plans"][0]["reportPath"]).ends_with("home/report/report.json")); From 691052a07dde50190d45d3c295cc7ce56cd1650a Mon Sep 17 00:00:00 2001 From: leader Date: Sat, 24 Oct 2026 02:54:37 +0900 Subject: [PATCH 123/149] chore: add changepack for feedback batch 2-66 --- .changepacks/changepack_log_vh0-IGr9a1CbP3nVaXSf2.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changepacks/changepack_log_vh0-IGr9a1CbP3nVaXSf2.json diff --git a/.changepacks/changepack_log_vh0-IGr9a1CbP3nVaXSf2.json b/.changepacks/changepack_log_vh0-IGr9a1CbP3nVaXSf2.json new file mode 100644 index 00000000..942808ae --- /dev/null +++ b/.changepacks/changepack_log_vh0-IGr9a1CbP3nVaXSf2.json @@ -0,0 +1,7 @@ +{ + "changes": { + "Cargo.toml": "Patch" + }, + "note": "feedback batch 2-66: componentTsx exports component sets/variants/instances, data-figma-node-id opt-in (figmaNodeIds), search lists by type across pages, bridge referencePng frameIds, project-plan never drops on-disk columns and recovers existing plans, screen-plan condensed proposals and monorepo auditPlan lookup, runtime-audit emits all runner modules and isolates probe errors, figma-bridge-setup skill and component-first workflow", + "date": "2026-10-23T17:54:27.176404300Z" +} \ No newline at end of file From 3b3a794f7cb13aeceba52e4200dcff3a4484955c Mon Sep 17 00:00:00 2001 From: leader Date: Sat, 24 Oct 2026 09:34:50 +0900 Subject: [PATCH 124/149] fix(codegen): componentTsx for real component sets - quote non-identifier lookup keys (button set produced invalid TSX) - treat an aborted OXC parse as a validation error (parserAborted) - expose unbound TEXT nodes in a component set as string props with the Figma copy as default - drop set-canvas placement from variant roots, keep a containing block for absolute descendants - emit cond ? value : undefined instead of cond && value --- .../changepack_log_eRTXLDvy0yMJ3YwboDGWu.json | 7 + .../src/codegen/variant/drawn_when.rs | 24 +- .../src/codegen/variant/text_properties.rs | 57 +- .../src/codegen/variant/variant_projection.rs | 7 +- crates/devup-mcp-devup-ui/src/validation.rs | 4 +- .../tests/chondrogen_component_sets.rs | 138 + .../tests/fixtures/chondrogen_button_set.json | 12500 ++++++++++++++++ .../chondrogen_section_title_set.json | 684 + crates/devup-mcp-devup-ui/tests/validation.rs | 11 + .../tests/variant_nesting.rs | 2 +- fixtures/devup-figma-plugin/manifest.json | 4 +- .../upstream-codegen-249-6b8a42981d.snap | 8 +- .../upstream-codegen-250-935ccdb80f.snap | 7 +- 13 files changed, 13433 insertions(+), 20 deletions(-) create mode 100644 .changepacks/changepack_log_eRTXLDvy0yMJ3YwboDGWu.json create mode 100644 crates/devup-mcp-devup-ui/tests/chondrogen_component_sets.rs create mode 100644 crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_button_set.json create mode 100644 crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_section_title_set.json diff --git a/.changepacks/changepack_log_eRTXLDvy0yMJ3YwboDGWu.json b/.changepacks/changepack_log_eRTXLDvy0yMJ3YwboDGWu.json new file mode 100644 index 00000000..cf3ff864 --- /dev/null +++ b/.changepacks/changepack_log_eRTXLDvy0yMJ3YwboDGWu.json @@ -0,0 +1,7 @@ +{ + "changes": { + "Cargo.toml": "Patch" + }, + "note": "componentTsx: fix button set panic, expose component text as props, drop set-canvas placement, emit undefined for absent variant values", + "date": "2026-10-24T00:21:08.585604600Z" +} \ No newline at end of file diff --git a/crates/devup-mcp-devup-ui/src/codegen/variant/drawn_when.rs b/crates/devup-mcp-devup-ui/src/codegen/variant/drawn_when.rs index 783506d4..dcb81964 100644 --- a/crates/devup-mcp-devup-ui/src/codegen/variant/drawn_when.rs +++ b/crates/devup-mcp-devup-ui/src/codegen/variant/drawn_when.rs @@ -604,7 +604,10 @@ fn render_component( .collect::>() .join("\n"); for name in text_properties.keys() { - fields.push_str(&format!("\n {name}?: string")); + if !fields.is_empty() { + fields.push('\n'); + } + fields.push_str(&format!(" {name}?: string")); } format!("export interface {name}Props {{\n{fields}\n}}\n\n") }; @@ -739,9 +742,6 @@ fn needs_const(prop: &str) -> bool { fn render_expression_attr(prop: &str, expression: &Expression) -> String { match expression { Expression::Literal(value) => format!("=\"{value}\""), - Expression::Conditional(name, option, value) => { - format!("={{{name} === '{option}' && \"{value}\"}}") - } Expression::Variant(name, _) if needs_const(prop) => { let rendered = render_expression_value(expression, 0); let object = rendered @@ -777,14 +777,14 @@ fn indent_attribute_expression(expression: &str, depth: usize) -> String { fn render_expression_value(expression: &Expression, depth: usize) -> String { match expression { - Expression::Literal(value) => format!("\"{value}\""), + Expression::Literal(value) => serde_json::to_string(value).unwrap_or_default(), Expression::Responsive(slots) => format!( "[{}]", slots .iter() .map(|slot| slot .as_ref() - .map_or("null".to_owned(), |value| format!("\"{value}\""))) + .map_or("null".to_owned(), |value| serde_json::to_string(value).unwrap_or_default())) .collect::>() .join(", ") ), @@ -794,6 +794,14 @@ fn render_expression_value(expression: &Expression, depth: usize) -> String { let lines = values .iter() .map(|(key, value)| { + let key = if key.chars().enumerate().all(|(index, character)| { + character.is_ascii_alphabetic() || character == '_' || character == '$' + || (index > 0 && character.is_ascii_digit()) + }) && !key.is_empty() { + key.clone() + } else { + serde_json::to_string(key).unwrap_or_default() + }; format!( "{child_indent}{key}: {}", render_expression_value(value, depth + 1) @@ -804,7 +812,9 @@ fn render_expression_value(expression: &Expression, depth: usize) -> String { format!("{{\n{lines}\n{indent}}}[{prop}]") } Expression::Conditional(prop, option, value) => { - format!("{prop} === '{option}' && \"{value}\"") + let option = serde_json::to_string(option).unwrap_or_default(); + let value = serde_json::to_string(value).unwrap_or_default(); + format!("{prop} === {option} ? {value} : undefined") } } } diff --git a/crates/devup-mcp-devup-ui/src/codegen/variant/text_properties.rs b/crates/devup-mcp-devup-ui/src/codegen/variant/text_properties.rs index 02e4dd71..f238af0d 100644 --- a/crates/devup-mcp-devup-ui/src/codegen/variant/text_properties.rs +++ b/crates/devup-mcp-devup-ui/src/codegen/variant/text_properties.rs @@ -14,6 +14,10 @@ fn bind_text_properties( (instance_property_name(name.split('#').next().unwrap_or(name)), definition["defaultValue"].to_string()), )) .collect::>(); + let mut defaults = properties.values().cloned().collect::>(); + let mut used = definitions(set).into_iter().map(|definition| definition.name) + .chain(defaults.keys().cloned()).collect::>(); + let mut fallback_index = 0; let mut pending = vec![tree]; while let Some(node) = pending.pop() { if let Some((name, _)) = snapshot.nodes.get(&node.node_id) @@ -23,8 +27,57 @@ fn bind_text_properties( .and_then(|reference| properties.get(reference)) { node.content = Some(format!("{{{name}}}")); + } else if let Some(raw) = snapshot.nodes.get(&node.node_id) + && raw.node_type == "TEXT" + { + let layer = raw.typed_view().name().unwrap_or_default(); + let base = if layer.is_ascii() && layer.chars().any(|character| character.is_ascii_alphabetic()) { + let mut parts = layer.split(|character: char| !character.is_ascii_alphanumeric()) + .filter(|part| !part.is_empty()); + let mut name = parts.next().unwrap_or_default().to_owned(); + if name.chars().all(|character| !character.is_ascii_lowercase()) { + name.make_ascii_lowercase(); + } else if let Some(first) = name.get_mut(..1) { + first.make_ascii_lowercase(); + } + for part in parts { + let mut part = part.to_ascii_lowercase(); + if let Some(first) = part.get_mut(..1) { + first.make_ascii_uppercase(); + } + name.push_str(&part); + } + if name.starts_with(|character: char| character.is_ascii_digit()) { + name.insert(0, '_'); + } + name + } else { + fallback_index += 1; + format!("text{fallback_index}") + }; + let mut name = base.clone(); + let mut suffix = 2; + while used.contains(&name) || matches!(name.as_str(), + "break" | "case" | "catch" | "class" | "const" | "continue" | "debugger" | + "default" | "delete" | "do" | "else" | "enum" | "export" | "extends" | + "false" | "finally" | "for" | "function" | "if" | "import" | "in" | + "instanceof" | "new" | "null" | "return" | "super" | "switch" | "this" | + "throw" | "true" | "try" | "typeof" | "var" | "void" | "while" | "with" | + "let" | "yield" | "await" | "implements" | "interface" | "package" | + "private" | "protected" | "public" | "static" | "eval" | "arguments") { + name = format!("{base}{suffix}"); + suffix += 1; + } + used.insert(name.clone()); + let characters = raw.typed_view().string("characters").unwrap_or_default(); + let default = match node.content.as_deref() { + Some(content) if content.starts_with("{{") && content.ends_with('}') => content[1..content.len()-1].to_owned(), + _ => serde_json::to_string(characters).unwrap_or_default(), + }; + defaults.insert(name.clone(), default); + node.content = Some(format!("{{{name}}}")); } - pending.extend(node.children.iter_mut()); + pending.extend(node.children.iter_mut().rev()); } - properties.into_values().collect() + defaults } diff --git a/crates/devup-mcp-devup-ui/src/codegen/variant/variant_projection.rs b/crates/devup-mcp-devup-ui/src/codegen/variant/variant_projection.rs index 6aace47a..0af0d72a 100644 --- a/crates/devup-mcp-devup-ui/src/codegen/variant/variant_projection.rs +++ b/crates/devup-mcp-devup-ui/src/codegen/variant/variant_projection.rs @@ -124,9 +124,14 @@ pub(super) fn generate_variant_component_set( Some((node, values)) }) .map(|(node, values)| { + let mut tree = project_tree(snapshot, node, options, true)?; + tree.props.retain(|name, _| !POSITION_PROPS.contains(&name.as_str())); + if layout::holds_positioned_children(snapshot, node) { + tree.props.insert("pos".into(), "relative".into()); + } Ok(Record { values, - tree: project_tree(snapshot, node, options, true)?, + tree, node_id: node.id.clone(), }) }) diff --git a/crates/devup-mcp-devup-ui/src/validation.rs b/crates/devup-mcp-devup-ui/src/validation.rs index a1606c0c..0203ef14 100644 --- a/crates/devup-mcp-devup-ui/src/validation.rs +++ b/crates/devup-mcp-devup-ui/src/validation.rs @@ -18,7 +18,7 @@ pub fn validate_tsx(source: &str) -> Result { // `ParserReturn::diagnostics` (oxc_parser 0.148, was `errors` on 0.96) derefs to // `Vec`, so this keeps the original "any diagnostic fails validation" // behavior regardless of severity. - if parsed.diagnostics.is_empty() { + if !parsed.panicked && parsed.diagnostics.is_empty() { return Ok(TsxValidation { byte_len: source.len(), statement_count: parsed.program.body.len(), @@ -53,7 +53,7 @@ pub fn validate_tsx(source: &str) -> Result { false, json!({ "errorCount": errors.len(), - "panicked": parsed.panicked, + "parserAborted": parsed.panicked, "errors": errors }), )) diff --git a/crates/devup-mcp-devup-ui/tests/chondrogen_component_sets.rs b/crates/devup-mcp-devup-ui/tests/chondrogen_component_sets.rs new file mode 100644 index 00000000..9a5ef7b5 --- /dev/null +++ b/crates/devup-mcp-devup-ui/tests/chondrogen_component_sets.rs @@ -0,0 +1,138 @@ +//! Real component sets from the chondrogen Figma file. Each one exported with +//! `componentTsx` came back unusable: the `button` set panicked in codegen, and +//! `SectionTitle` carried the sample copy, the set-canvas position of its +//! variants and `cond && "value"` attributes that evaluate to `false`. +use devup_mcp_devup_ui::codegen::{CodegenOptions, generate_component}; +use devup_mcp_figma::Snapshot; + +fn load(raw: &str) -> Snapshot { + serde_json::from_str(raw).unwrap() +} + +fn button() -> Snapshot { + load(include_str!("fixtures/chondrogen_button_set.json")) +} + +fn section_title() -> Snapshot { + load(include_str!("fixtures/chondrogen_section_title_set.json")) +} + +#[test] +fn button_set_generates_without_panicking() { + let snapshot = button(); + let root = snapshot.roots[0].clone(); + let output = std::panic::catch_unwind(|| { + generate_component(&snapshot, &root, &CodegenOptions::default()) + }) + .expect("component codegen must not panic"); + let output = output.expect("button set must produce componentTsx"); + println!("{}", output.tsx); + assert!(output.tsx.contains("export function"), "{}", output.tsx); +} + +#[test] +fn section_title_texts_are_props_not_sample_copy() { + let snapshot = section_title(); + let root = snapshot.roots[0].clone(); + let output = generate_component(&snapshot, &root, &CodegenOptions::default()).unwrap(); + println!("{}", output.tsx); + let body = output.tsx.split("return (").nth(1).unwrap(); + assert!(!body.contains("CEO MESSEAGE"), "{}", output.tsx); + assert!(!body.contains("관절재생으로 새로운 삶을"), "{}", output.tsx); + assert!(body.contains("{ceoMesseage}"), "{}", output.tsx); + assert!(body.contains("{text1}"), "{}", output.tsx); + assert!(output.tsx.contains("ceoMesseage = \"CEO MESSEAGE\"")); + assert!(output.tsx.contains("text1 = \"관절재생으로 새로운 삶을\"")); +} + +#[test] +fn section_title_variant_roots_drop_set_canvas_placement() { + let snapshot = section_title(); + let root = snapshot.roots[0].clone(); + let output = generate_component(&snapshot, &root, &CodegenOptions::default()).unwrap(); + assert!(!output.tsx.contains("pos=\"absolute\""), "{}", output.tsx); + assert!(!output.tsx.contains("top={"), "{}", output.tsx); + assert!(!output.tsx.contains("left=\"20px\""), "{}", output.tsx); +} + +#[test] +fn conditional_variant_values_never_evaluate_to_false() { + for snapshot in [section_title(), button()] { + let root = snapshot.roots[0].clone(); + let Ok(output) = std::panic::catch_unwind(|| { + generate_component(&snapshot, &root, &CodegenOptions::default()) + }) else { + continue; + }; + let Ok(output) = output else { continue }; + assert!(!output.tsx.contains(" && \""), "{}", output.tsx); + assert!(!output.tsx.contains(" && '"), "{}", output.tsx); + } +} + +#[test] +fn named_text_props_are_unique_identifiers_with_escaped_defaults() { + for (first, second, expected) in [ + ("Title label", "Title-label", ["titleLabel", "titleLabel2"]), + ("align", "class", ["align2", "class2"]), + ("", "제목", ["text1", "text2"]), + ] { + let mut snapshot = section_title(); + for node in snapshot + .nodes + .values_mut() + .filter(|node| node.node_type == "TEXT") + { + let name = if node.fields["characters"] == "CEO MESSEAGE" { + first + } else { + second + }; + node.fields.insert("name".into(), serde_json::json!(name)); + node.fields.insert( + "characters".into(), + serde_json::json!("A \"quote\"\n{body}"), + ); + } + let output = + generate_component(&snapshot, &snapshot.roots[0], &CodegenOptions::default()).unwrap(); + for name in expected { + assert!( + output.tsx.contains(&format!("{name}?: string")), + "{}", + output.tsx + ); + assert!( + output.tsx.contains(&format!("{{{name}}}")), + "{}", + output.tsx + ); + } + assert!( + output.tsx.contains(r#"= "A \"quote\"\n{body}""#), + "{}", + output.tsx + ); + } +} + +#[test] +fn variant_root_remains_a_containing_block_for_absolute_descendants() { + let mut snapshot = section_title(); + for node in snapshot + .nodes + .values_mut() + .filter(|node| node.node_type == "TEXT") + { + node.fields + .insert("layoutPositioning".into(), serde_json::json!("ABSOLUTE")); + node.fields.insert("x".into(), serde_json::json!(3)); + node.fields.insert("y".into(), serde_json::json!(7)); + } + let output = + generate_component(&snapshot, &snapshot.roots[0], &CodegenOptions::default()).unwrap(); + let root = output.tsx.split(" - 자세히 알아보기 → + {text} ) diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-250-935ccdb80f.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-250-935ccdb80f.snap index b4205053..619278a2 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-250-935ccdb80f.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-250-935ccdb80f.snap @@ -6,9 +6,10 @@ expression: actual "export interface ButtonProps { size: 'Md' | 'Sm' variant: 'primary' | 'white' + text1?: string } -export function Button({ size, variant }: ButtonProps) { +export function Button({ size, variant, text1 = "참가 신청하기" }: ButtonProps) { return (
- 참가 신청하기 + {text1}
) From cee9d4f9dcd0b5d7707a77577f99b1b0dcc6e878 Mon Sep 17 00:00:00 2001 From: leader Date: Sat, 24 Oct 2026 10:00:29 +0900 Subject: [PATCH 125/149] fix(codegen): trace unrepresented variant children, dedupe declared text prop names - account for diagnosed variant-child fallback nodes as ignored trace entries so componentTsx no longer fails the exactly-once check (ApplicationsCard, RnDLinkRow, TechStep) - give non-ASCII declared TEXT properties unique identifiers instead of overwriting defaults --- .../changepack_log_eRTXLDvy0yMJ3YwboDGWu.json | 4 +- .../codegen/component/component_variants.rs | 15 +- .../src/codegen/variant/text_properties.rs | 23 +- .../tests/chondrogen_component_text_traces.rs | 91 + .../chondrogen_applications_card_set.json | 2120 +++++++++++++++++ .../fixtures/chondrogen_rnd_link_row_set.json | 1910 +++++++++++++++ .../fixtures/chondrogen_tech_step_set.json | 1448 +++++++++++ 7 files changed, 5600 insertions(+), 11 deletions(-) create mode 100644 crates/devup-mcp-devup-ui/tests/chondrogen_component_text_traces.rs create mode 100644 crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_applications_card_set.json create mode 100644 crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_rnd_link_row_set.json create mode 100644 crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_tech_step_set.json diff --git a/.changepacks/changepack_log_eRTXLDvy0yMJ3YwboDGWu.json b/.changepacks/changepack_log_eRTXLDvy0yMJ3YwboDGWu.json index cf3ff864..755e0270 100644 --- a/.changepacks/changepack_log_eRTXLDvy0yMJ3YwboDGWu.json +++ b/.changepacks/changepack_log_eRTXLDvy0yMJ3YwboDGWu.json @@ -2,6 +2,6 @@ "changes": { "Cargo.toml": "Patch" }, - "note": "componentTsx: fix button set panic, expose component text as props, drop set-canvas placement, emit undefined for absent variant values", + "note": "componentTsx: fix button set panic, expose component text as unique props even when declared names normalize alike, drop set-canvas placement, emit undefined for absent variant values, account explicitly for unrepresented variant children without claiming emitted property provenance", "date": "2026-10-24T00:21:08.585604600Z" -} \ No newline at end of file +} diff --git a/crates/devup-mcp-devup-ui/src/codegen/component/component_variants.rs b/crates/devup-mcp-devup-ui/src/codegen/component/component_variants.rs index ed506132..126ea2d5 100644 --- a/crates/devup-mcp-devup-ui/src/codegen/component/component_variants.rs +++ b/crates/devup-mcp-devup-ui/src/codegen/component/component_variants.rs @@ -269,6 +269,19 @@ fn finalize_codegen_output( .extend(unresolved_token_bindings(snapshot, root_id, options)); output.projection_trace = build_projection_trace(snapshot, root_id, &output.tsx, &output.source_map); + for diagnostic in &output.diagnostics { + if diagnostic.code == "DEVUP_CODEGEN_VARIANT_CHILD_FALLBACK" + && let Some(node_id) = &diagnostic.node_id + && !output.projection_trace.entries.iter().any(|entry| &entry.node_id == node_id) + { + output.projection_trace.entries.push(crate::provenance::ProjectionTraceEntry { + node_id: node_id.clone(), + disposition: crate::provenance::ProjectionDisposition::Ignored, + reason: "unrepresented-variant-child".into(), + generated_range: None, + }); + } + } output.fidelity_report = validate_fidelity(snapshot, root_id, &output)?; output .diagnostics @@ -555,4 +568,4 @@ pub(super) enum PropValue { String(String), } -pub(super) type Prop = (String, PropValue); \ No newline at end of file +pub(super) type Prop = (String, PropValue); diff --git a/crates/devup-mcp-devup-ui/src/codegen/variant/text_properties.rs b/crates/devup-mcp-devup-ui/src/codegen/variant/text_properties.rs index f238af0d..2910d52e 100644 --- a/crates/devup-mcp-devup-ui/src/codegen/variant/text_properties.rs +++ b/crates/devup-mcp-devup-ui/src/codegen/variant/text_properties.rs @@ -3,20 +3,27 @@ fn bind_text_properties( set: &RawNode, tree: &mut Tree, ) -> BTreeMap { - let properties = set.typed_view() + let mut used = definitions(set).into_iter().map(|definition| definition.name) + .collect::>(); + let mut properties = BTreeMap::new(); + for (name, definition) in set.typed_view() .value("componentPropertyDefinitions") .and_then(Value::as_object) .into_iter() .flatten() .filter(|(_, definition)| definition["type"] == "TEXT") - .map(|(name, definition)| ( - name.clone(), - (instance_property_name(name.split('#').next().unwrap_or(name)), definition["defaultValue"].to_string()), - )) - .collect::>(); + { + let base = instance_property_name(name.split('#').next().unwrap_or(name)); + let mut property = base.clone(); + let mut suffix = 2; + while used.contains(&property) { + property = format!("{base}{suffix}"); + suffix += 1; + } + used.insert(property.clone()); + properties.insert(name.clone(), (property, definition["defaultValue"].to_string())); + } let mut defaults = properties.values().cloned().collect::>(); - let mut used = definitions(set).into_iter().map(|definition| definition.name) - .chain(defaults.keys().cloned()).collect::>(); let mut fallback_index = 0; let mut pending = vec![tree]; while let Some(node) = pending.pop() { diff --git a/crates/devup-mcp-devup-ui/tests/chondrogen_component_text_traces.rs b/crates/devup-mcp-devup-ui/tests/chondrogen_component_text_traces.rs new file mode 100644 index 00000000..a837361a --- /dev/null +++ b/crates/devup-mcp-devup-ui/tests/chondrogen_component_text_traces.rs @@ -0,0 +1,91 @@ +use devup_mcp_devup_ui::codegen::{CodegenOptions, generate_component}; +use devup_mcp_devup_ui::provenance::ProjectionDisposition; +use devup_mcp_figma::Snapshot; + +fn assert_component_text_traces(raw: &str, excluded: &[&str]) { + let snapshot: Snapshot = serde_json::from_str(raw).unwrap(); + let output = generate_component(&snapshot, &snapshot.roots[0], &CodegenOptions::default()) + .expect("componentTsx must account for every source node"); + println!("{}\n{}", snapshot.roots[0], output.tsx); + + let (signature, body) = output.tsx.split_once("return (").unwrap(); + for node in snapshot + .nodes + .values() + .filter(|node| node.node_type == "TEXT") + { + assert_eq!( + output + .projection_trace + .entries + .iter() + .filter(|entry| entry.node_id == node.id) + .count(), + 1, + "TEXT {} must be traced exactly once", + node.id + ); + let characters = node.typed_view().string("characters").unwrap(); + let default = serde_json::to_string(characters).unwrap(); + assert!( + signature.contains(&format!("= {default}")), + "TEXT {} needs a prop default: {}", + node.id, + output.tsx + ); + assert!( + !body.contains(characters), + "TEXT {} sample copy must not be a JSX child: {}", + node.id, + output.tsx + ); + } + for node_id in excluded { + let entry = output + .projection_trace + .entries + .iter() + .find(|entry| entry.node_id == *node_id) + .unwrap(); + assert_eq!(entry.disposition, ProjectionDisposition::Ignored); + assert_eq!(entry.reason, "unrepresented-variant-child"); + assert!(entry.generated_range.is_none()); + assert!( + !output + .source_map + .entries + .iter() + .any(|entry| entry.node_id.as_deref() == Some(node_id)) + ); + assert!(output.diagnostics.iter().any(|diagnostic| { + diagnostic.node_id.as_deref() == Some(node_id) + && diagnostic.code == "DEVUP_CODEGEN_VARIANT_CHILD_FALLBACK" + && diagnostic.fidelity_impact() == devup_mcp_figma::FidelityImpact::Lossy + })); + } + assert!(!output.fidelity_report.strict_compatible()); +} + +#[test] +fn applications_card_component_text_traces() { + assert_component_text_traces( + include_str!("fixtures/chondrogen_applications_card_set.json"), + &["81:12384"], + ); +} + +#[test] +fn rnd_link_row_component_text_traces() { + assert_component_text_traces( + include_str!("fixtures/chondrogen_rnd_link_row_set.json"), + &["51:5764"], + ); +} + +#[test] +fn tech_step_component_text_traces() { + assert_component_text_traces( + include_str!("fixtures/chondrogen_tech_step_set.json"), + &["51:5505", "51:5506"], + ); +} diff --git a/crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_applications_card_set.json b/crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_applications_card_set.json new file mode 100644 index 00000000..e85241d2 --- /dev/null +++ b/crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_applications_card_set.json @@ -0,0 +1,2120 @@ +{ + "fileKey": "K2kUnxGgvooAJwCQiVTb7g", + "version": null, + "roots": [ + "81:12380" + ], + "nodes": { + "81:12375": { + "id": "81:12375", + "type": "TEXT", + "fields": { + "parentId": "81:12379", + "absoluteBoundingBox": { + "height": 34, + "width": 32, + "x": 8822, + "y": 10251 + }, + "absoluteRenderBounds": { + "height": 14.919921875, + "width": 18.9443359375, + "x": 8822.8798828125, + "y": 10260.2998046875 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:345", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:950", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "01", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:345", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.7323716878890991, + "g": 0.34177345037460327, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "ExtraBold" + }, + "fontSize": 20, + "height": 34, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 139.9999976158142 + }, + "maxHeight": null, + "maxWidth": null, + "name": "01", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAlignVertical": "CENTER", + "textAutoResize": "NONE", + "textStyleId": "S:0b5158f29530255590daed174bb87c3a635c314a,", + "textTruncation": "DISABLED", + "visible": true, + "width": 32, + "x": 0, + "y": 32, + "styledTextSegments": [ + { + "characters": "01", + "end": 2, + "fillStyleId": "", + "fontWeight": 800, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:0b5158f29530255590daed174bb87c3a635c314a," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "81:12376": { + "id": "81:12376", + "type": "FRAME", + "fields": { + "parentId": "81:12379", + "childrenIds": [ + "81:12377", + "81:12378" + ], + "absoluteBoundingBox": { + "height": 64, + "width": 586, + "x": 8874, + "y": 10251 + }, + "absoluteRenderBounds": { + "height": 64, + "width": 586, + "x": 8874, + "y": 10251 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "height": 64, + "inferredAutoLayout": { + "counterAxisAlignItems": "MIN", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 6, + "layoutAlign": "INHERIT", + "layoutGrow": 1, + "layoutMode": "VERTICAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 6, + "layoutGrow": 1, + "layoutMode": "VERTICAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "text", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 586, + "x": 52, + "y": 32 + }, + "extra": {}, + "fieldErrors": {} + }, + "81:12377": { + "id": "81:12377", + "type": "TEXT", + "fields": { + "parentId": "81:12376", + "absoluteBoundingBox": { + "height": 34, + "width": 586, + "x": 8874, + "y": 10251 + }, + "absoluteRenderBounds": { + "height": 21.671875, + "width": 212.6923828125, + "x": 8874.599609375, + "y": 10257.416015625 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:345", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:946", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "신약 후보물질 스크리닝", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:345", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.7323716878890991, + "g": 0.34177345037460327, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "ExtraBold" + }, + "fontSize": 24, + "height": 34, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 139.9999976158142 + }, + "maxHeight": null, + "maxWidth": null, + "name": "신약 후보물질 스크리닝", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "HEIGHT", + "textStyleId": "S:695d59044b4e4f7eea6a65d6dd08296043af8a66,", + "textTruncation": "DISABLED", + "visible": true, + "width": 586, + "x": 0, + "y": 0, + "styledTextSegments": [ + { + "characters": "신약 후보물질 스크리닝", + "end": 12, + "fillStyleId": "", + "fontWeight": 800, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:695d59044b4e4f7eea6a65d6dd08296043af8a66," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "81:12378": { + "id": "81:12378", + "type": "TEXT", + "fields": { + "parentId": "81:12376", + "absoluteBoundingBox": { + "height": 24, + "width": 586, + "x": 8874, + "y": 10291 + }, + "absoluteRenderBounds": { + "height": 14.7197265625, + "width": 506.4794921875, + "x": 8874.84765625, + "y": 10296.0244140625 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:348", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:955", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "염증 · 연골 분해 지표(IL-6, PGE2, MMP-13 등)로 후보물질의 효능을 평가합니다.", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:348", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.3176470696926117, + "g": 0.250980406999588, + "r": 0.21568627655506137 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Regular" + }, + "fontSize": 16, + "height": 24, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "염증 · 연골 분해 지표(IL-6, PGE2, MM", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "HEIGHT", + "textStyleId": "S:e4c7b7dacbd4e73f6c967cfc38e87b24df7d8775,", + "textTruncation": "DISABLED", + "visible": true, + "width": 586, + "x": 0, + "y": 40, + "styledTextSegments": [ + { + "characters": "염증 · 연골 분해 지표(IL-6, PGE2, MMP-13 등)로 후보물질의 효능을 평가합니다.", + "end": 53, + "fillStyleId": "", + "fontWeight": 400, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:e4c7b7dacbd4e73f6c967cfc38e87b24df7d8775," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "81:12379": { + "id": "81:12379", + "type": "COMPONENT", + "fields": { + "parentId": "81:12380", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "81:12375", + "81:12376" + ], + "absoluteBoundingBox": { + "height": 129, + "width": 638, + "x": 8822, + "y": 10219 + }, + "absoluteRenderBounds": { + "height": 129, + "width": 638, + "x": 8822, + "y": 10219 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "boundVariables": { + "strokes": [ + { + "id": "VariableID:9:351", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "height": 129, + "inferredAutoLayout": { + "counterAxisAlignItems": "MIN", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 20, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 32, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 32, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 20, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "effect=selected, viewport=Desktop", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 32, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 32, + "strokeBottomWeight": 1, + "strokeLeftWeight": 0, + "strokeRightWeight": 0, + "strokeTopWeight": 0, + "strokeWeight": { + "$unsupported": "symbol" + }, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:351", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.9215686321258544, + "g": 0.9058823585510254, + "r": 0.8980392217636108 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "effect": "selected", + "viewport": "Desktop" + }, + "visible": true, + "width": 638, + "x": 21, + "y": 21 + }, + "extra": {}, + "fieldErrors": {} + }, + "81:12380": { + "id": "81:12380", + "type": "COMPONENT_SET", + "fields": { + "parentId": "19:963", + "parentType": "SECTION", + "parentName": "Components", + "childrenIds": [ + "81:12379", + "81:12381", + "81:12431", + "81:12426" + ], + "absoluteBoundingBox": { + "height": 572, + "width": 680, + "x": 8801, + "y": 10198 + }, + "absoluteRenderBounds": { + "height": 572, + "width": 680, + "x": 8801, + "y": 10198 + }, + "bottomLeftRadius": 5, + "bottomRightRadius": 5, + "clipsContent": true, + "componentPropertyDefinitions": { + "effect": { + "defaultValue": "selected", + "type": "VARIANT", + "variantOptions": [ + "selected", + "default" + ] + }, + "viewport": { + "defaultValue": "Desktop", + "type": "VARIANT", + "variantOptions": [ + "Desktop", + "Mobile" + ] + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 5, + "dashPattern": [ + 10, + 5 + ], + "defaultVariant": { + "$nodeId": "81:12379", + "$nodeType": "COMPONENT" + }, + "height": 572, + "inferredAutoLayout": { + "counterAxisAlignItems": "MIN", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 20, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutPositioning": "AUTO", + "paddingBottom": 20, + "paddingLeft": 20, + "paddingRight": 20, + "paddingTop": 20, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 20, + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "ApplicationsCard", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 20, + "paddingLeft": 20, + "paddingRight": 20, + "paddingTop": 20, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 0.27843138575553894, + "r": 0.5921568870544434 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "topLeftRadius": 5, + "topRightRadius": 5, + "visible": true, + "width": 680, + "x": 131, + "y": 9160 + }, + "extra": {}, + "fieldErrors": {} + }, + "81:12381": { + "id": "81:12381", + "type": "COMPONENT", + "fields": { + "parentId": "81:12380", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "81:12382", + "81:12383" + ], + "absoluteBoundingBox": { + "height": 129, + "width": 638, + "x": 8822, + "y": 10368 + }, + "absoluteRenderBounds": { + "height": 129, + "width": 638, + "x": 8822, + "y": 10368 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "boundVariables": { + "strokes": [ + { + "id": "VariableID:9:351", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "height": 129, + "inferredAutoLayout": { + "counterAxisAlignItems": "MIN", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 20, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 32, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 32, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 20, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "effect=default, viewport=Desktop", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 32, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 32, + "strokeBottomWeight": 1, + "strokeLeftWeight": 0, + "strokeRightWeight": 0, + "strokeTopWeight": 0, + "strokeWeight": { + "$unsupported": "symbol" + }, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:351", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.9215686321258544, + "g": 0.9058823585510254, + "r": 0.8980392217636108 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "effect": "default", + "viewport": "Desktop" + }, + "visible": true, + "width": 638, + "x": 21, + "y": 170 + }, + "extra": {}, + "fieldErrors": {} + }, + "81:12382": { + "id": "81:12382", + "type": "TEXT", + "fields": { + "parentId": "81:12381", + "absoluteBoundingBox": { + "height": 34, + "width": 32, + "x": 8822, + "y": 10400 + }, + "absoluteRenderBounds": { + "height": 14.919921875, + "width": 18.9443359375, + "x": 8822.8798828125, + "y": 10409.2998046875 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:359", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:950", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "01", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:359", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.6682692170143127, + "g": 0.6104382276535034, + "r": 0.5815227627754211 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "ExtraBold" + }, + "fontSize": 20, + "height": 34, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 139.9999976158142 + }, + "maxHeight": null, + "maxWidth": null, + "name": "01", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAlignVertical": "CENTER", + "textAutoResize": "NONE", + "textStyleId": "S:0b5158f29530255590daed174bb87c3a635c314a,", + "textTruncation": "DISABLED", + "visible": true, + "width": 32, + "x": 0, + "y": 32, + "styledTextSegments": [ + { + "characters": "01", + "end": 2, + "fillStyleId": "", + "fontWeight": 800, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:0b5158f29530255590daed174bb87c3a635c314a," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "81:12383": { + "id": "81:12383", + "type": "FRAME", + "fields": { + "parentId": "81:12381", + "childrenIds": [ + "81:12384", + "81:12385" + ], + "absoluteBoundingBox": { + "height": 64, + "width": 586, + "x": 8874, + "y": 10400 + }, + "absoluteRenderBounds": { + "height": 64, + "width": 586, + "x": 8874, + "y": 10400 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "height": 64, + "inferredAutoLayout": { + "counterAxisAlignItems": "MIN", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 6, + "layoutAlign": "INHERIT", + "layoutGrow": 1, + "layoutMode": "VERTICAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 6, + "layoutGrow": 1, + "layoutMode": "VERTICAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "text", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 586, + "x": 52, + "y": 32 + }, + "extra": {}, + "fieldErrors": {} + }, + "81:12384": { + "id": "81:12384", + "type": "TEXT", + "fields": { + "parentId": "81:12383", + "absoluteBoundingBox": { + "height": 34, + "width": 586, + "x": 8874, + "y": 10400 + }, + "absoluteRenderBounds": { + "height": 21.671875, + "width": 212.6923828125, + "x": 8874.599609375, + "y": 10406.416015625 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:946", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "신약 후보물질 스크리닝", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.1529411822557449, + "g": 0.08627451211214066, + "r": 0.06666667014360428 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "ExtraBold" + }, + "fontSize": 24, + "height": 34, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 139.9999976158142 + }, + "maxHeight": null, + "maxWidth": null, + "name": "신약 후보물질 스크리닝", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "HEIGHT", + "textStyleId": "S:695d59044b4e4f7eea6a65d6dd08296043af8a66,", + "textTruncation": "DISABLED", + "visible": true, + "width": 586, + "x": 0, + "y": 0, + "styledTextSegments": [ + { + "characters": "신약 후보물질 스크리닝", + "end": 12, + "fillStyleId": "", + "fontWeight": 800, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:695d59044b4e4f7eea6a65d6dd08296043af8a66," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "81:12385": { + "id": "81:12385", + "type": "TEXT", + "fields": { + "parentId": "81:12383", + "absoluteBoundingBox": { + "height": 24, + "width": 586, + "x": 8874, + "y": 10440 + }, + "absoluteRenderBounds": { + "height": 14.7197265625, + "width": 506.4794921875, + "x": 8874.84765625, + "y": 10445.0244140625 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:348", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:955", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "염증 · 연골 분해 지표(IL-6, PGE2, MMP-13 등)로 후보물질의 효능을 평가합니다.", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:348", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.3176470696926117, + "g": 0.250980406999588, + "r": 0.21568627655506137 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Regular" + }, + "fontSize": 16, + "height": 24, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "염증 · 연골 분해 지표(IL-6, PGE2, MM", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "HEIGHT", + "textStyleId": "S:e4c7b7dacbd4e73f6c967cfc38e87b24df7d8775,", + "textTruncation": "DISABLED", + "visible": true, + "width": 586, + "x": 0, + "y": 40, + "styledTextSegments": [ + { + "characters": "염증 · 연골 분해 지표(IL-6, PGE2, MMP-13 등)로 후보물질의 효능을 평가합니다.", + "end": 53, + "fillStyleId": "", + "fontWeight": 400, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:e4c7b7dacbd4e73f6c967cfc38e87b24df7d8775," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "81:12426": { + "id": "81:12426", + "type": "COMPONENT", + "fields": { + "parentId": "81:12380", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "81:12427", + "81:12428" + ], + "absoluteBoundingBox": { + "height": 106, + "width": 638, + "x": 8822, + "y": 10643 + }, + "absoluteRenderBounds": { + "height": 106, + "width": 638, + "x": 8822, + "y": 10643 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "boundVariables": { + "strokes": [ + { + "id": "VariableID:9:351", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "height": 106, + "inferredAutoLayout": { + "counterAxisAlignItems": "MIN", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 8, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 24, + "paddingLeft": 20, + "paddingRight": 20, + "paddingTop": 24, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 8, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "effect=default, viewport=Mobile", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 24, + "paddingLeft": 20, + "paddingRight": 20, + "paddingTop": 24, + "strokeBottomWeight": 1, + "strokeLeftWeight": 0, + "strokeRightWeight": 0, + "strokeTopWeight": 0, + "strokeWeight": { + "$unsupported": "symbol" + }, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:351", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.9215686321258544, + "g": 0.9058823585510254, + "r": 0.8980392217636108 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "effect": "default", + "viewport": "Mobile" + }, + "visible": true, + "width": 638, + "x": 21, + "y": 445 + }, + "extra": {}, + "fieldErrors": {} + }, + "81:12427": { + "id": "81:12427", + "type": "TEXT", + "fields": { + "parentId": "81:12426", + "absoluteBoundingBox": { + "height": 25, + "width": 32, + "x": 8842, + "y": 10667 + }, + "absoluteRenderBounds": { + "height": 13.4287109375, + "width": 17.0498046875, + "x": 8842.7919921875, + "y": 10672.76953125 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:359", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:950", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "01", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:359", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.6682692170143127, + "g": 0.6104382276535034, + "r": 0.5815227627754211 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "ExtraBold" + }, + "fontSize": 18, + "height": 25, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 139.9999976158142 + }, + "maxHeight": null, + "maxWidth": null, + "name": "01", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "HEIGHT", + "textStyleId": "S:0b5158f29530255590daed174bb87c3a635c314a,", + "textTruncation": "DISABLED", + "visible": true, + "width": 32, + "x": 20, + "y": 24, + "styledTextSegments": [ + { + "characters": "01", + "end": 2, + "fillStyleId": "", + "fontWeight": 800, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:0b5158f29530255590daed174bb87c3a635c314a," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "81:12428": { + "id": "81:12428", + "type": "FRAME", + "fields": { + "parentId": "81:12426", + "childrenIds": [ + "81:12429", + "81:12430" + ], + "absoluteBoundingBox": { + "height": 57, + "width": 558, + "x": 8882, + "y": 10667 + }, + "absoluteRenderBounds": { + "height": 57, + "width": 558, + "x": 8882, + "y": 10667 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "height": 57, + "inferredAutoLayout": { + "counterAxisAlignItems": "MIN", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 6, + "layoutAlign": "INHERIT", + "layoutGrow": 1, + "layoutMode": "VERTICAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 6, + "layoutGrow": 1, + "layoutMode": "VERTICAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "text", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 558, + "x": 60, + "y": 24 + }, + "extra": {}, + "fieldErrors": {} + }, + "81:12429": { + "id": "81:12429", + "type": "TEXT", + "fields": { + "parentId": "81:12428", + "absoluteBoundingBox": { + "height": 28, + "width": 558, + "x": 8882, + "y": 10667 + }, + "absoluteRenderBounds": { + "height": 18.060546875, + "width": 177.2431640625, + "x": 8882.5, + "y": 10671.6796875 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:946", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "신약 후보물질 스크리닝", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.1529411822557449, + "g": 0.08627451211214066, + "r": 0.06666667014360428 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "ExtraBold" + }, + "fontSize": 20, + "height": 28, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 139.9999976158142 + }, + "maxHeight": null, + "maxWidth": null, + "name": "신약 후보물질 스크리닝", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "HEIGHT", + "textStyleId": "S:695d59044b4e4f7eea6a65d6dd08296043af8a66,", + "textTruncation": "DISABLED", + "visible": true, + "width": 558, + "x": 0, + "y": 0, + "styledTextSegments": [ + { + "characters": "신약 후보물질 스크리닝", + "end": 12, + "fillStyleId": "", + "fontWeight": 800, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:695d59044b4e4f7eea6a65d6dd08296043af8a66," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "81:12430": { + "id": "81:12430", + "type": "TEXT", + "fields": { + "parentId": "81:12428", + "absoluteBoundingBox": { + "height": 23, + "width": 558, + "x": 8882, + "y": 10701 + }, + "absoluteRenderBounds": { + "height": 13.7998046875, + "width": 474.82421875, + "x": 8882.794921875, + "y": 10705.8349609375 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:348", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:955", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "염증 · 연골 분해 지표(IL-6, PGE2, MMP-13 등)로 후보물질의 효능을 평가합니다.", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:348", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.3176470696926117, + "g": 0.250980406999588, + "r": 0.21568627655506137 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Regular" + }, + "fontSize": 15, + "height": 23, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "염증 · 연골 분해 지표(IL-6, PGE2, MM", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "HEIGHT", + "textStyleId": "S:e4c7b7dacbd4e73f6c967cfc38e87b24df7d8775,", + "textTruncation": "DISABLED", + "visible": true, + "width": 558, + "x": 0, + "y": 34, + "styledTextSegments": [ + { + "characters": "염증 · 연골 분해 지표(IL-6, PGE2, MMP-13 등)로 후보물질의 효능을 평가합니다.", + "end": 53, + "fillStyleId": "", + "fontWeight": 400, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:e4c7b7dacbd4e73f6c967cfc38e87b24df7d8775," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "81:12431": { + "id": "81:12431", + "type": "COMPONENT", + "fields": { + "parentId": "81:12380", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "81:12432", + "81:12433" + ], + "absoluteBoundingBox": { + "height": 106, + "width": 638, + "x": 8822, + "y": 10517 + }, + "absoluteRenderBounds": { + "height": 106, + "width": 638, + "x": 8822, + "y": 10517 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "boundVariables": { + "strokes": [ + { + "id": "VariableID:9:351", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "height": 106, + "inferredAutoLayout": { + "counterAxisAlignItems": "MIN", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 8, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 24, + "paddingLeft": 20, + "paddingRight": 20, + "paddingTop": 24, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 8, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "effect=selected, viewport=Mobile", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 24, + "paddingLeft": 20, + "paddingRight": 20, + "paddingTop": 24, + "strokeBottomWeight": 1, + "strokeLeftWeight": 0, + "strokeRightWeight": 0, + "strokeTopWeight": 0, + "strokeWeight": { + "$unsupported": "symbol" + }, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:351", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.9215686321258544, + "g": 0.9058823585510254, + "r": 0.8980392217636108 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "effect": "selected", + "viewport": "Mobile" + }, + "visible": true, + "width": 638, + "x": 21, + "y": 319 + }, + "extra": {}, + "fieldErrors": {} + }, + "81:12432": { + "id": "81:12432", + "type": "TEXT", + "fields": { + "parentId": "81:12431", + "absoluteBoundingBox": { + "height": 25, + "width": 32, + "x": 8842, + "y": 10541 + }, + "absoluteRenderBounds": { + "height": 13.4287109375, + "width": 17.0498046875, + "x": 8842.7919921875, + "y": 10546.76953125 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:345", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:950", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "01", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:345", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.7323716878890991, + "g": 0.34177345037460327, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "ExtraBold" + }, + "fontSize": 18, + "height": 25, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 139.9999976158142 + }, + "maxHeight": null, + "maxWidth": null, + "name": "01", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "HEIGHT", + "textStyleId": "S:0b5158f29530255590daed174bb87c3a635c314a,", + "textTruncation": "DISABLED", + "visible": true, + "width": 32, + "x": 20, + "y": 24, + "styledTextSegments": [ + { + "characters": "01", + "end": 2, + "fillStyleId": "", + "fontWeight": 800, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:0b5158f29530255590daed174bb87c3a635c314a," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "81:12433": { + "id": "81:12433", + "type": "FRAME", + "fields": { + "parentId": "81:12431", + "childrenIds": [ + "81:12434", + "81:12435" + ], + "absoluteBoundingBox": { + "height": 57, + "width": 558, + "x": 8882, + "y": 10541 + }, + "absoluteRenderBounds": { + "height": 57, + "width": 558, + "x": 8882, + "y": 10541 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "height": 57, + "inferredAutoLayout": { + "counterAxisAlignItems": "MIN", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 6, + "layoutAlign": "INHERIT", + "layoutGrow": 1, + "layoutMode": "VERTICAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 6, + "layoutGrow": 1, + "layoutMode": "VERTICAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "text", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 558, + "x": 60, + "y": 24 + }, + "extra": {}, + "fieldErrors": {} + }, + "81:12434": { + "id": "81:12434", + "type": "TEXT", + "fields": { + "parentId": "81:12433", + "absoluteBoundingBox": { + "height": 28, + "width": 558, + "x": 8882, + "y": 10541 + }, + "absoluteRenderBounds": { + "height": 18.060546875, + "width": 177.2431640625, + "x": 8882.5, + "y": 10545.6796875 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:345", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:946", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "신약 후보물질 스크리닝", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:345", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.7323716878890991, + "g": 0.34177345037460327, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "ExtraBold" + }, + "fontSize": 20, + "height": 28, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 139.9999976158142 + }, + "maxHeight": null, + "maxWidth": null, + "name": "신약 후보물질 스크리닝", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "HEIGHT", + "textStyleId": "S:695d59044b4e4f7eea6a65d6dd08296043af8a66,", + "textTruncation": "DISABLED", + "visible": true, + "width": 558, + "x": 0, + "y": 0, + "styledTextSegments": [ + { + "characters": "신약 후보물질 스크리닝", + "end": 12, + "fillStyleId": "", + "fontWeight": 800, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:695d59044b4e4f7eea6a65d6dd08296043af8a66," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "81:12435": { + "id": "81:12435", + "type": "TEXT", + "fields": { + "parentId": "81:12433", + "absoluteBoundingBox": { + "height": 23, + "width": 558, + "x": 8882, + "y": 10575 + }, + "absoluteRenderBounds": { + "height": 13.7998046875, + "width": 474.82421875, + "x": 8882.794921875, + "y": 10579.8349609375 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:348", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:955", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "염증 · 연골 분해 지표(IL-6, PGE2, MMP-13 등)로 후보물질의 효능을 평가합니다.", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:348", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.3176470696926117, + "g": 0.250980406999588, + "r": 0.21568627655506137 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Regular" + }, + "fontSize": 15, + "height": 23, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "염증 · 연골 분해 지표(IL-6, PGE2, MM", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "HEIGHT", + "textStyleId": "S:e4c7b7dacbd4e73f6c967cfc38e87b24df7d8775,", + "textTruncation": "DISABLED", + "visible": true, + "width": 558, + "x": 0, + "y": 34, + "styledTextSegments": [ + { + "characters": "염증 · 연골 분해 지표(IL-6, PGE2, MMP-13 등)로 후보물질의 효능을 평가합니다.", + "end": 53, + "fillStyleId": "", + "fontWeight": 400, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:e4c7b7dacbd4e73f6c967cfc38e87b24df7d8775," + } + ] + }, + "extra": {}, + "fieldErrors": {} + } + }, + "diagnostics": [] +} \ No newline at end of file diff --git a/crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_rnd_link_row_set.json b/crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_rnd_link_row_set.json new file mode 100644 index 00000000..6e08b6b7 --- /dev/null +++ b/crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_rnd_link_row_set.json @@ -0,0 +1,1910 @@ +{ + "fileKey": "K2kUnxGgvooAJwCQiVTb7g", + "version": null, + "roots": [ + "51:5754" + ], + "nodes": { + "51:5754": { + "id": "51:5754", + "type": "COMPONENT_SET", + "fields": { + "parentId": "19:963", + "parentType": "SECTION", + "parentName": "Components", + "childrenIds": [ + "51:5755", + "51:5761" + ], + "absoluteBoundingBox": { + "height": 350, + "width": 808, + "x": 9458, + "y": 4559 + }, + "absoluteRenderBounds": { + "height": 350, + "width": 808, + "x": 9458, + "y": 4559 + }, + "bottomLeftRadius": 5, + "bottomRightRadius": 5, + "boundVariables": { + "fills": [ + { + "id": "VariableID:11:19", + "type": "VARIABLE_ALIAS" + } + ] + }, + "clipsContent": true, + "componentPropertyDefinitions": { + "effect": { + "defaultValue": "default", + "type": "VARIANT", + "variantOptions": [ + "default", + "hover" + ] + }, + "번호#47:79": { + "defaultValue": "01", + "type": "TEXT" + }, + "설명#47:85": { + "defaultValue": "세포치료제와 3차원 공배양 모델, 두 축의 연구", + "type": "TEXT" + }, + "제목#47:82": { + "defaultValue": "연구개발 분야", + "type": "TEXT" + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 5, + "dashPattern": [ + 10, + 5 + ], + "defaultVariant": { + "$nodeId": "51:5755", + "$nodeType": "COMPONENT" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:11:19", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.21960784494876864, + "g": 0.10196078568696976, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 350, + "inferredAutoLayout": { + "counterAxisAlignItems": "MIN", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 24, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutPositioning": "AUTO", + "paddingBottom": 24, + "paddingLeft": 24, + "paddingRight": 24, + "paddingTop": 24, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 24, + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "RnDLinkRow", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 24, + "paddingLeft": 24, + "paddingRight": 24, + "paddingTop": 24, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 5, + "topRightRadius": 5, + "visible": true, + "width": 808, + "x": 788, + "y": 3521 + }, + "extra": {}, + "fieldErrors": {} + }, + "51:5755": { + "id": "51:5755", + "type": "COMPONENT", + "fields": { + "parentId": "51:5754", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "51:5800", + "51:5757", + "51:5807" + ], + "absoluteBoundingBox": { + "height": 139, + "width": 760, + "x": 9482, + "y": 4583 + }, + "absoluteRenderBounds": { + "height": 139, + "width": 760, + "x": 9482, + "y": 4583 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "height": 139, + "inferredAutoLayout": { + "counterAxisAlignItems": "MIN", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 32, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 36, + "paddingLeft": 24, + "paddingRight": 24, + "paddingTop": 36, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 32, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "effect=default", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 36, + "paddingLeft": 24, + "paddingRight": 24, + "paddingTop": 36, + "strokeBottomWeight": 0, + "strokeLeftWeight": 0, + "strokeRightWeight": 0, + "strokeTopWeight": 1, + "strokeWeight": { + "$unsupported": "symbol" + }, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 0.4000000059604645, + "type": "SOLID", + "visible": true + } + ], + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "effect": "default" + }, + "visible": true, + "width": 760, + "x": 24, + "y": 24 + }, + "extra": {}, + "fieldErrors": {} + }, + "51:5756": { + "id": "51:5756", + "type": "TEXT", + "fields": { + "parentId": "51:5800", + "absoluteBoundingBox": { + "height": 20, + "width": 40, + "x": 9506, + "y": 4632 + }, + "absoluteRenderBounds": { + "height": 11.25, + "width": 15.8681640625, + "x": 9506.599609375, + "y": 4635.94482421875 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:11:27", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "01", + "componentPropertyReferences": { + "characters": "번호#47:79" + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:11:27", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.8392156958580017, + "g": 0.8156862854957581, + "r": 0.40392157435417175 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Heavy" + }, + "fontSize": 15, + "height": 20, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": 4 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 132.00000524520874 + }, + "maxHeight": null, + "maxWidth": null, + "name": "number", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "HEIGHT", + "textStyleId": "S:18adf49c362ab9c0a968104533e0a31e663f3222,", + "textTruncation": "DISABLED", + "visible": true, + "width": 40, + "x": 0, + "y": 12, + "styledTextSegments": [ + { + "characters": "01", + "end": 2, + "fillStyleId": "", + "fontWeight": 900, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:18adf49c362ab9c0a968104533e0a31e663f3222," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "51:5757": { + "id": "51:5757", + "type": "FRAME", + "fields": { + "parentId": "51:5755", + "childrenIds": [ + "51:5758", + "51:5759" + ], + "absoluteBoundingBox": { + "height": 66, + "width": 560, + "x": 9578, + "y": 4620 + }, + "absoluteRenderBounds": { + "height": 66, + "width": 560, + "x": 9578, + "y": 4620 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "height": 66, + "inferredAutoLayout": { + "counterAxisAlignItems": "MIN", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 8, + "layoutAlign": "INHERIT", + "layoutGrow": 1, + "layoutMode": "VERTICAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 8, + "layoutGrow": 1, + "layoutMode": "VERTICAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "text", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 560, + "x": 96, + "y": 37 + }, + "extra": {}, + "fieldErrors": {} + }, + "51:5758": { + "id": "51:5758", + "type": "TEXT", + "fields": { + "parentId": "51:5757", + "absoluteBoundingBox": { + "height": 34, + "width": 560, + "x": 9578, + "y": 4620 + }, + "absoluteRenderBounds": { + "height": 21.64794921875, + "width": 127.166015625, + "x": 9578.9599609375, + "y": 4626.43994140625 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:356", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:946", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "연구개발 분야", + "componentPropertyReferences": { + "characters": "제목#47:82" + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:356", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "ExtraBold" + }, + "fontSize": 24, + "height": 34, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 139.9999976158142 + }, + "maxHeight": null, + "maxWidth": null, + "name": "title", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "HEIGHT", + "textStyleId": "S:695d59044b4e4f7eea6a65d6dd08296043af8a66,", + "textTruncation": "DISABLED", + "visible": true, + "width": 560, + "x": 0, + "y": 0, + "styledTextSegments": [ + { + "characters": "연구개발 분야", + "end": 7, + "fillStyleId": "", + "fontWeight": 800, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:695d59044b4e4f7eea6a65d6dd08296043af8a66," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "51:5759": { + "id": "51:5759", + "type": "TEXT", + "fields": { + "parentId": "51:5757", + "absoluteBoundingBox": { + "height": 24, + "width": 560, + "x": 9578, + "y": 4662 + }, + "absoluteRenderBounds": { + "height": 14.65625, + "width": 278.2529296875, + "x": 9578.48046875, + "y": 4667.087890625 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:356", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:955", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "세포치료제와 3차원 공배양 모델, 두 축의 연구", + "componentPropertyReferences": { + "characters": "설명#47:85" + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:356", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Regular" + }, + "fontSize": 16, + "height": 24, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "desc", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "HEIGHT", + "textStyleId": "S:e4c7b7dacbd4e73f6c967cfc38e87b24df7d8775,", + "textTruncation": "DISABLED", + "visible": true, + "width": 560, + "x": 0, + "y": 42, + "styledTextSegments": [ + { + "characters": "세포치료제와 3차원 공배양 모델, 두 축의 연구", + "end": 26, + "fillStyleId": "", + "fontWeight": 400, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:e4c7b7dacbd4e73f6c967cfc38e87b24df7d8775," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "51:5760": { + "id": "51:5760", + "type": "INSTANCE", + "fields": { + "parentId": "51:5807", + "childrenIds": [ + "I51:5760;30:720" + ], + "absoluteBoundingBox": { + "height": 48, + "width": 48, + "x": 10170, + "y": 4629 + }, + "absoluteRenderBounds": { + "height": 48, + "width": 48, + "x": 10170, + "y": 4629 + }, + "bottomLeftRadius": 100, + "bottomRightRadius": 100, + "componentProperties": { + "effect": { + "boundVariables": {}, + "type": "VARIANT", + "value": "default" + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 100, + "counterAxisAlignItems": "CENTER", + "effects": [ + { + "blurType": "NORMAL", + "boundVariables": {}, + "radius": 4, + "type": "BACKGROUND_BLUR", + "visible": true + } + ], + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 0.07999999821186066, + "type": "SOLID", + "visible": true + } + ], + "height": 48, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 10, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 12, + "paddingLeft": 12, + "paddingRight": 12, + "paddingTop": 12, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 10, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "arrow", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 12, + "paddingLeft": 12, + "paddingRight": 12, + "paddingTop": 12, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "targetAspectRatio": { + "x": 44, + "y": 44 + }, + "topLeftRadius": 100, + "topRightRadius": 100, + "variantProperties": { + "effect": "default" + }, + "visible": true, + "width": 48, + "x": 0, + "y": 9, + "mainComponentId": "30:723", + "mainComponentName": "effect=default", + "mainComponentSetId": "30:724" + }, + "extra": {}, + "fieldErrors": {} + }, + "51:5761": { + "id": "51:5761", + "type": "COMPONENT", + "fields": { + "parentId": "51:5754", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "51:5762", + "51:5763", + "51:5820" + ], + "absoluteBoundingBox": { + "height": 139, + "width": 760, + "x": 9482, + "y": 4746 + }, + "absoluteRenderBounds": { + "height": 139, + "width": 760, + "x": 9482, + "y": 4746 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 0.05999999865889549, + "type": "SOLID", + "visible": true + } + ], + "height": 139, + "inferredAutoLayout": { + "counterAxisAlignItems": "MIN", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 32, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 36, + "paddingLeft": 24, + "paddingRight": 24, + "paddingTop": 36, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 32, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "effect=hover", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 36, + "paddingLeft": 24, + "paddingRight": 24, + "paddingTop": 36, + "strokeBottomWeight": 0, + "strokeLeftWeight": 0, + "strokeRightWeight": 0, + "strokeTopWeight": 1, + "strokeWeight": { + "$unsupported": "symbol" + }, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 0.4000000059604645, + "type": "SOLID", + "visible": true + } + ], + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "effect": "hover" + }, + "visible": true, + "width": 760, + "x": 24, + "y": 187 + }, + "extra": {}, + "fieldErrors": {} + }, + "51:5762": { + "id": "51:5762", + "type": "TEXT", + "fields": { + "parentId": "51:5761", + "absoluteBoundingBox": { + "height": 20, + "width": 40, + "x": 9506, + "y": 4783 + }, + "absoluteRenderBounds": { + "height": 11.25, + "width": 15.8681640625, + "x": 9506.599609375, + "y": 4786.94482421875 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:11:27", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "01", + "componentPropertyReferences": { + "characters": "번호#47:79" + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:11:27", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.8392156958580017, + "g": 0.8156862854957581, + "r": 0.40392157435417175 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Heavy" + }, + "fontSize": 15, + "height": 20, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": 4 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 132.00000524520874 + }, + "maxHeight": null, + "maxWidth": null, + "name": "number", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "HEIGHT", + "textStyleId": "S:18adf49c362ab9c0a968104533e0a31e663f3222,", + "textTruncation": "DISABLED", + "visible": true, + "width": 40, + "x": 24, + "y": 37, + "styledTextSegments": [ + { + "characters": "01", + "end": 2, + "fillStyleId": "", + "fontWeight": 900, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:18adf49c362ab9c0a968104533e0a31e663f3222," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "51:5763": { + "id": "51:5763", + "type": "FRAME", + "fields": { + "parentId": "51:5761", + "childrenIds": [ + "51:5764", + "51:5765" + ], + "absoluteBoundingBox": { + "height": 66, + "width": 560, + "x": 9578, + "y": 4783 + }, + "absoluteRenderBounds": { + "height": 66, + "width": 560, + "x": 9578, + "y": 4783 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "height": 66, + "inferredAutoLayout": { + "counterAxisAlignItems": "MIN", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 8, + "layoutAlign": "INHERIT", + "layoutGrow": 1, + "layoutMode": "VERTICAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 8, + "layoutGrow": 1, + "layoutMode": "VERTICAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "text", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 560, + "x": 96, + "y": 37 + }, + "extra": {}, + "fieldErrors": {} + }, + "51:5764": { + "id": "51:5764", + "type": "TEXT", + "fields": { + "parentId": "51:5763", + "absoluteBoundingBox": { + "height": 34, + "width": 560, + "x": 9578, + "y": 4783 + }, + "absoluteRenderBounds": { + "height": 21.64794921875, + "width": 127.166015625, + "x": 9578.9599609375, + "y": 4789.43994140625 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:11:27", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:946", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "연구개발 분야", + "componentPropertyReferences": { + "characters": "제목#47:82" + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:11:27", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.8392156958580017, + "g": 0.8156862854957581, + "r": 0.40392157435417175 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "ExtraBold" + }, + "fontSize": 24, + "height": 34, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 139.9999976158142 + }, + "maxHeight": null, + "maxWidth": null, + "name": "title", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "HEIGHT", + "textStyleId": "S:695d59044b4e4f7eea6a65d6dd08296043af8a66,", + "textTruncation": "DISABLED", + "visible": true, + "width": 560, + "x": 0, + "y": 0, + "styledTextSegments": [ + { + "characters": "연구개발 분야", + "end": 7, + "fillStyleId": "", + "fontWeight": 800, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:695d59044b4e4f7eea6a65d6dd08296043af8a66," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "51:5765": { + "id": "51:5765", + "type": "TEXT", + "fields": { + "parentId": "51:5763", + "absoluteBoundingBox": { + "height": 24, + "width": 560, + "x": 9578, + "y": 4825 + }, + "absoluteRenderBounds": { + "height": 14.65625, + "width": 278.2529296875, + "x": 9578.48046875, + "y": 4830.087890625 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:356", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:955", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "세포치료제와 3차원 공배양 모델, 두 축의 연구", + "componentPropertyReferences": { + "characters": "설명#47:85" + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:356", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Regular" + }, + "fontSize": 16, + "height": 24, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "desc", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "HEIGHT", + "textStyleId": "S:e4c7b7dacbd4e73f6c967cfc38e87b24df7d8775,", + "textTruncation": "DISABLED", + "visible": true, + "width": 560, + "x": 0, + "y": 42, + "styledTextSegments": [ + { + "characters": "세포치료제와 3차원 공배양 모델, 두 축의 연구", + "end": 26, + "fillStyleId": "", + "fontWeight": 400, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:e4c7b7dacbd4e73f6c967cfc38e87b24df7d8775," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "51:5800": { + "id": "51:5800", + "type": "FRAME", + "fields": { + "parentId": "51:5755", + "childrenIds": [ + "51:5756" + ], + "absoluteBoundingBox": { + "height": 44, + "width": 40, + "x": 9506, + "y": 4620 + }, + "absoluteRenderBounds": { + "height": 44, + "width": 40, + "x": 9506, + "y": 4620 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "height": 44, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 10, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 12, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 12, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 10, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "Frame 549", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 12, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 12, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 40, + "x": 24, + "y": 37 + }, + "extra": {}, + "fieldErrors": {} + }, + "51:5807": { + "id": "51:5807", + "type": "FRAME", + "fields": { + "parentId": "51:5755", + "childrenIds": [ + "51:5760" + ], + "absoluteBoundingBox": { + "height": 66, + "width": 48, + "x": 10170, + "y": 4620 + }, + "absoluteRenderBounds": { + "height": 66, + "width": 48, + "x": 10170, + "y": 4620 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "height": 66, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 10, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 10, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "FILL", + "maxHeight": null, + "maxWidth": null, + "name": "Frame 550", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 48, + "x": 688, + "y": 37 + }, + "extra": {}, + "fieldErrors": {} + }, + "51:5820": { + "id": "51:5820", + "type": "FRAME", + "fields": { + "parentId": "51:5761", + "childrenIds": [ + "51:5821" + ], + "absoluteBoundingBox": { + "height": 66, + "width": 48, + "x": 10170, + "y": 4783 + }, + "absoluteRenderBounds": { + "height": 66, + "width": 48, + "x": 10170, + "y": 4783 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "height": 66, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 10, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 10, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "FILL", + "maxHeight": null, + "maxWidth": null, + "name": "Frame 550", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 48, + "x": 688, + "y": 37 + }, + "extra": {}, + "fieldErrors": {} + }, + "51:5821": { + "id": "51:5821", + "type": "INSTANCE", + "fields": { + "parentId": "51:5820", + "childrenIds": [ + "I51:5821;30:726" + ], + "absoluteBoundingBox": { + "height": 48, + "width": 48, + "x": 10170, + "y": 4792 + }, + "absoluteRenderBounds": { + "height": 48, + "width": 48, + "x": 10170, + "y": 4792 + }, + "bottomLeftRadius": 100, + "bottomRightRadius": 100, + "componentProperties": { + "effect": { + "boundVariables": {}, + "type": "VARIANT", + "value": "hover" + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 100, + "counterAxisAlignItems": "CENTER", + "effects": [ + { + "blurType": "NORMAL", + "boundVariables": {}, + "radius": 4, + "type": "BACKGROUND_BLUR", + "visible": true + } + ], + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 0.20000000298023224, + "type": "SOLID", + "visible": true + } + ], + "height": 48, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 10, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 12, + "paddingLeft": 12, + "paddingRight": 12, + "paddingTop": 12, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 10, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "arrow", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 12, + "paddingLeft": 12, + "paddingRight": 12, + "paddingTop": 12, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "targetAspectRatio": { + "x": 44, + "y": 44 + }, + "topLeftRadius": 100, + "topRightRadius": 100, + "variantProperties": { + "effect": "hover" + }, + "visible": true, + "width": 48, + "x": 0, + "y": 9, + "mainComponentId": "30:725", + "mainComponentName": "effect=hover", + "mainComponentSetId": "30:724" + }, + "extra": {}, + "fieldErrors": {} + }, + "I51:5760;30:720": { + "id": "I51:5760;30:720", + "type": "INSTANCE", + "fields": { + "parentId": "51:5760", + "childrenIds": [ + "I51:5760;30:720;23:1618" + ], + "absoluteBoundingBox": { + "height": 20, + "width": 20, + "x": 10184, + "y": 4643 + }, + "absoluteRenderBounds": { + "height": 20, + "width": 20, + "x": 10184, + "y": 4643 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "componentProperties": { + "속성 1": { + "boundVariables": {}, + "type": "VARIANT", + "value": "Right" + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": false + } + ], + "height": 20, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 3.333333730697632, + "paddingLeft": 7.5, + "paddingRight": 5.833333492279053, + "paddingTop": 3.3333332538604736, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "isAsset": true, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "icon", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 0.125, + "strokeLeftWeight": 0.125, + "strokeRightWeight": 0.125, + "strokeTopWeight": 0.125, + "strokeWeight": 0.125, + "targetAspectRatio": { + "x": 32, + "y": 32 + }, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "속성 1": "Right" + }, + "visible": true, + "width": 20, + "x": 14, + "y": 14, + "mainComponentId": "23:1619", + "mainComponentName": "속성 1=Right", + "mainComponentSetId": "20:1312" + }, + "extra": {}, + "fieldErrors": {} + }, + "I51:5760;30:720;23:1618": { + "id": "I51:5760;30:720;23:1618", + "type": "VECTOR", + "fields": { + "parentId": "I51:5760;30:720", + "absoluteBoundingBox": { + "height": 13.333333015441896, + "width": 6.666666507720947, + "x": 10191.5, + "y": 4646.33349609375 + }, + "absoluteRenderBounds": { + "height": 15.33349609375, + "width": 8.6669921875, + "x": 10190.5, + "y": 4645.33349609375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "height": 13.333333015441896, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "CENTER", + "strokeWeight": 2, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "visible": true, + "width": 6.666666507720947, + "x": 7.5, + "y": 3.3333332538604736 + }, + "extra": {}, + "fieldErrors": {} + }, + "I51:5821;30:726": { + "id": "I51:5821;30:726", + "type": "INSTANCE", + "fields": { + "parentId": "51:5821", + "childrenIds": [ + "I51:5821;30:726;23:1618" + ], + "absoluteBoundingBox": { + "height": 20, + "width": 20, + "x": 10184, + "y": 4806 + }, + "absoluteRenderBounds": { + "height": 20, + "width": 20, + "x": 10184, + "y": 4806 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "componentProperties": { + "속성 1": { + "boundVariables": {}, + "type": "VARIANT", + "value": "Right" + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": false + } + ], + "height": 20, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 3.333333730697632, + "paddingLeft": 7.5, + "paddingRight": 5.833333492279053, + "paddingTop": 3.3333332538604736, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "isAsset": true, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "icon", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 0.125, + "strokeLeftWeight": 0.125, + "strokeRightWeight": 0.125, + "strokeTopWeight": 0.125, + "strokeWeight": 0.125, + "targetAspectRatio": { + "x": 32, + "y": 32 + }, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "속성 1": "Right" + }, + "visible": true, + "width": 20, + "x": 14, + "y": 14, + "mainComponentId": "23:1619", + "mainComponentName": "속성 1=Right", + "mainComponentSetId": "20:1312" + }, + "extra": {}, + "fieldErrors": {} + }, + "I51:5821;30:726;23:1618": { + "id": "I51:5821;30:726;23:1618", + "type": "VECTOR", + "fields": { + "parentId": "I51:5821;30:726", + "absoluteBoundingBox": { + "height": 13.333333015441896, + "width": 6.666666507720947, + "x": 10191.5, + "y": 4809.33349609375 + }, + "absoluteRenderBounds": { + "height": 15.33349609375, + "width": 8.6669921875, + "x": 10190.5, + "y": 4808.33349609375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "height": 13.333333015441896, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "CENTER", + "strokeWeight": 2, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "visible": true, + "width": 6.666666507720947, + "x": 7.5, + "y": 3.3333332538604736 + }, + "extra": {}, + "fieldErrors": {} + } + }, + "diagnostics": [] +} \ No newline at end of file diff --git a/crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_tech_step_set.json b/crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_tech_step_set.json new file mode 100644 index 00000000..93d7b370 --- /dev/null +++ b/crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_tech_step_set.json @@ -0,0 +1,1448 @@ +{ + "fileKey": "K2kUnxGgvooAJwCQiVTb7g", + "version": null, + "roots": [ + "51:5495" + ], + "nodes": { + "51:5495": { + "id": "51:5495", + "type": "COMPONENT_SET", + "fields": { + "parentId": "19:963", + "parentType": "SECTION", + "parentName": "Components", + "childrenIds": [ + "51:5496", + "51:5503" + ], + "absoluteBoundingBox": { + "height": 364, + "width": 528, + "x": 10145, + "y": 1877 + }, + "absoluteRenderBounds": { + "height": 364, + "width": 528, + "x": 10145, + "y": 1877 + }, + "bottomLeftRadius": 5, + "bottomRightRadius": 5, + "clipsContent": true, + "componentPropertyDefinitions": { + "state": { + "defaultValue": "active", + "type": "VARIANT", + "variantOptions": [ + "active", + "default" + ] + }, + "번호#47:70": { + "defaultValue": "01", + "type": "TEXT" + }, + "설명#47:76": { + "defaultValue": "세포를 작은 공 모양으로 뭉쳐 관절 속 환경과 비슷하게 만들고, 오래 머물 수 있도록 설계했습니다.", + "type": "TEXT" + }, + "제목#47:73": { + "defaultValue": "스페로이드 형태", + "type": "TEXT" + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 5, + "dashPattern": [ + 10, + 5 + ], + "defaultVariant": { + "$nodeId": "51:5496", + "$nodeType": "COMPONENT" + }, + "height": 364, + "inferredAutoLayout": { + "counterAxisAlignItems": "MIN", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 24, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutPositioning": "AUTO", + "paddingBottom": 24, + "paddingLeft": 24, + "paddingRight": 24, + "paddingTop": 24, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 24, + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "TechStep", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 24, + "paddingLeft": 24, + "paddingRight": 24, + "paddingTop": 24, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 5, + "topRightRadius": 5, + "visible": true, + "width": 528, + "x": 1475, + "y": 839 + }, + "extra": {}, + "fieldErrors": {} + }, + "51:5496": { + "id": "51:5496", + "type": "COMPONENT", + "fields": { + "parentId": "51:5495", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "51:5497", + "51:5500", + "51:5618" + ], + "absoluteBoundingBox": { + "height": 193, + "width": 480, + "x": 10169, + "y": 1901 + }, + "absoluteRenderBounds": { + "height": 193, + "width": 480, + "x": 10169, + "y": 1901 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "boundVariables": { + "paddingBottom": { + "id": "VariableID:18:942", + "type": "VARIABLE_ALIAS" + }, + "paddingTop": { + "id": "VariableID:18:942", + "type": "VARIABLE_ALIAS" + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "height": 193, + "inferredAutoLayout": { + "counterAxisAlignItems": "MIN", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 16, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutPositioning": "AUTO", + "paddingBottom": 32, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 32, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 16, + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "state=active", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 32, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 32, + "strokeBottomWeight": 0, + "strokeLeftWeight": 0, + "strokeRightWeight": 0, + "strokeTopWeight": 1, + "strokeWeight": { + "$unsupported": "symbol" + }, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.3176470696926117, + "g": 0.250980406999588, + "r": 0.21568627655506137 + }, + "opacity": 0.23999999463558197, + "type": "SOLID", + "visible": true + } + ], + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "state": "active" + }, + "visible": true, + "width": 480, + "x": 24, + "y": 24 + }, + "extra": {}, + "fieldErrors": {} + }, + "51:5497": { + "id": "51:5497", + "type": "FRAME", + "fields": { + "parentId": "51:5496", + "childrenIds": [ + "51:5498", + "51:5499" + ], + "absoluteBoundingBox": { + "height": 34, + "width": 480, + "x": 10169, + "y": 1934 + }, + "absoluteRenderBounds": { + "height": 34, + "width": 480, + "x": 10169, + "y": 1934 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "height": 34, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 16, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 16, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "head", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 480, + "x": 0, + "y": 33 + }, + "extra": {}, + "fieldErrors": {} + }, + "51:5498": { + "id": "51:5498", + "type": "TEXT", + "fields": { + "parentId": "51:5497", + "absoluteBoundingBox": { + "height": 20, + "width": 18, + "x": 10169, + "y": 1941 + }, + "absoluteRenderBounds": { + "height": 11.25, + "width": 15.8681640625, + "x": 10169.599609375, + "y": 1944.9449462890625 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:345", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "01", + "componentPropertyReferences": { + "characters": "번호#47:70" + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:345", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.7323716878890991, + "g": 0.34177345037460327, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Heavy" + }, + "fontSize": 15, + "height": 20, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": 4 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 132.00000524520874 + }, + "maxHeight": null, + "maxWidth": null, + "name": "number", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:18adf49c362ab9c0a968104533e0a31e663f3222,", + "textTruncation": "DISABLED", + "visible": true, + "width": 18, + "x": 0, + "y": 7, + "styledTextSegments": [ + { + "characters": "01", + "end": 2, + "fillStyleId": "", + "fontWeight": 900, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:18adf49c362ab9c0a968104533e0a31e663f3222," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "51:5499": { + "id": "51:5499", + "type": "TEXT", + "fields": { + "parentId": "51:5497", + "absoluteBoundingBox": { + "height": 34, + "width": 446, + "x": 10203, + "y": 1934 + }, + "absoluteRenderBounds": { + "height": 21.8160400390625, + "width": 146.5830078125, + "x": 10203.7197265625, + "y": 1940.27197265625 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:345", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:946", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "스페로이드 형태", + "componentPropertyReferences": { + "characters": "제목#47:73" + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:345", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.7323716878890991, + "g": 0.34177345037460327, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "ExtraBold" + }, + "fontSize": 24, + "height": 34, + "layoutGrow": 1, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 139.9999976158142 + }, + "maxHeight": null, + "maxWidth": null, + "name": "title", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "HEIGHT", + "textStyleId": "S:695d59044b4e4f7eea6a65d6dd08296043af8a66,", + "textTruncation": "DISABLED", + "visible": true, + "width": 446, + "x": 34, + "y": 0, + "styledTextSegments": [ + { + "characters": "스페로이드 형태", + "end": 8, + "fillStyleId": "", + "fontWeight": 800, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:695d59044b4e4f7eea6a65d6dd08296043af8a66," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "51:5500": { + "id": "51:5500", + "type": "TEXT", + "fields": { + "parentId": "51:5496", + "absoluteBoundingBox": { + "height": 58, + "width": 480, + "x": 10169, + "y": 1984 + }, + "absoluteRenderBounds": { + "height": 44.840087890625, + "width": 474.3935546875, + "x": 10169.5400390625, + "y": 1990.491943359375 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:348", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:951", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "세포를 작은 공 모양으로 뭉쳐 관절 속 환경과 비슷하게 만들고, 오래 머물 수 있도록 설계했습니다.", + "componentPropertyReferences": { + "characters": "설명#47:76" + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:348", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.3176470696926117, + "g": 0.250980406999588, + "r": 0.21568627655506137 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Regular" + }, + "fontSize": 18, + "height": 58, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 160.0000023841858 + }, + "maxHeight": null, + "maxWidth": null, + "name": "desc", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "HEIGHT", + "textStyleId": "S:cfcbc71ecd9536404e6fe4dfeb971ce6261df4c3,", + "textTruncation": "DISABLED", + "visible": true, + "width": 480, + "x": 0, + "y": 83, + "styledTextSegments": [ + { + "characters": "세포를 작은 공 모양으로 뭉쳐 관절 속 환경과 비슷하게 만들고, 오래 머물 수 있도록 설계했습니다.", + "end": 55, + "fillStyleId": "", + "fontWeight": 400, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:cfcbc71ecd9536404e6fe4dfeb971ce6261df4c3," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "51:5503": { + "id": "51:5503", + "type": "COMPONENT", + "fields": { + "parentId": "51:5495", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "51:5504", + "51:5507", + "51:5508" + ], + "absoluteBoundingBox": { + "height": 99, + "width": 480, + "x": 10169, + "y": 2118 + }, + "absoluteRenderBounds": { + "height": 99, + "width": 480, + "x": 10169, + "y": 2118 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "boundVariables": { + "paddingBottom": { + "id": "VariableID:18:942", + "type": "VARIABLE_ALIAS" + }, + "paddingTop": { + "id": "VariableID:18:942", + "type": "VARIABLE_ALIAS" + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "height": 99, + "inferredAutoLayout": { + "counterAxisAlignItems": "MIN", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 16, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutPositioning": "AUTO", + "paddingBottom": 32, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 32, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 16, + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "state=default", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 32, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 32, + "strokeBottomWeight": 0, + "strokeLeftWeight": 0, + "strokeRightWeight": 0, + "strokeTopWeight": 1, + "strokeWeight": { + "$unsupported": "symbol" + }, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.3176470696926117, + "g": 0.250980406999588, + "r": 0.21568627655506137 + }, + "opacity": 0.23999999463558197, + "type": "SOLID", + "visible": true + } + ], + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "state": "default" + }, + "visible": true, + "width": 480, + "x": 24, + "y": 241 + }, + "extra": {}, + "fieldErrors": {} + }, + "51:5504": { + "id": "51:5504", + "type": "FRAME", + "fields": { + "parentId": "51:5503", + "childrenIds": [ + "51:5505", + "51:5506" + ], + "absoluteBoundingBox": { + "height": 34, + "width": 480, + "x": 10169, + "y": 2151 + }, + "absoluteRenderBounds": { + "height": 34, + "width": 480, + "x": 10169, + "y": 2151 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "height": 34, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 16, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 16, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "head", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 480, + "x": 0, + "y": 33 + }, + "extra": {}, + "fieldErrors": {} + }, + "51:5505": { + "id": "51:5505", + "type": "TEXT", + "fields": { + "parentId": "51:5504", + "absoluteBoundingBox": { + "height": 20, + "width": 18, + "x": 10169, + "y": 2158 + }, + "absoluteRenderBounds": { + "height": 11.25, + "width": 15.8681640625, + "x": 10169.599609375, + "y": 2161.945068359375 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:359", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "01", + "componentPropertyReferences": { + "characters": "번호#47:70" + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:359", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.6682692170143127, + "g": 0.6104382276535034, + "r": 0.5815227627754211 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Heavy" + }, + "fontSize": 15, + "height": 20, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": 4 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 132.00000524520874 + }, + "maxHeight": null, + "maxWidth": null, + "name": "number", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:18adf49c362ab9c0a968104533e0a31e663f3222,", + "textTruncation": "DISABLED", + "visible": true, + "width": 18, + "x": 0, + "y": 7, + "styledTextSegments": [ + { + "characters": "01", + "end": 2, + "fillStyleId": "", + "fontWeight": 900, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:18adf49c362ab9c0a968104533e0a31e663f3222," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "51:5506": { + "id": "51:5506", + "type": "TEXT", + "fields": { + "parentId": "51:5504", + "absoluteBoundingBox": { + "height": 34, + "width": 446, + "x": 10203, + "y": 2151 + }, + "absoluteRenderBounds": { + "height": 21.81591796875, + "width": 146.5830078125, + "x": 10203.7197265625, + "y": 2157.27197265625 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:348", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:946", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "스페로이드 형태", + "componentPropertyReferences": { + "characters": "제목#47:73" + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:348", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.3176470696926117, + "g": 0.250980406999588, + "r": 0.21568627655506137 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "ExtraBold" + }, + "fontSize": 24, + "height": 34, + "layoutGrow": 1, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 139.9999976158142 + }, + "maxHeight": null, + "maxWidth": null, + "name": "title", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "HEIGHT", + "textStyleId": "S:695d59044b4e4f7eea6a65d6dd08296043af8a66,", + "textTruncation": "DISABLED", + "visible": true, + "width": 446, + "x": 34, + "y": 0, + "styledTextSegments": [ + { + "characters": "스페로이드 형태", + "end": 8, + "fillStyleId": "", + "fontWeight": 800, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:695d59044b4e4f7eea6a65d6dd08296043af8a66," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "51:5507": { + "id": "51:5507", + "type": "TEXT", + "fields": { + "parentId": "51:5503", + "absoluteBoundingBox": { + "height": 58, + "width": 480, + "x": 10169, + "y": 2197 + }, + "absoluteRenderBounds": null, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:356", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:951", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "세포를 작은 공 모양으로 뭉쳐 관절 속 환경과 비슷하게 만들고, 오래 머물 수 있도록 설계했습니다.", + "componentPropertyReferences": { + "characters": "설명#47:76" + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:356", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Regular" + }, + "fontSize": 18, + "height": 58, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 160.0000023841858 + }, + "maxHeight": null, + "maxWidth": null, + "name": "desc", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "HEIGHT", + "textStyleId": "S:cfcbc71ecd9536404e6fe4dfeb971ce6261df4c3,", + "textTruncation": "DISABLED", + "visible": false, + "width": 480, + "x": 0, + "y": 79, + "styledTextSegments": [ + { + "characters": "세포를 작은 공 모양으로 뭉쳐 관절 속 환경과 비슷하게 만들고, 오래 머물 수 있도록 설계했습니다.", + "end": 55, + "fillStyleId": "", + "fontWeight": 400, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:cfcbc71ecd9536404e6fe4dfeb971ce6261df4c3," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "51:5508": { + "id": "51:5508", + "type": "FRAME", + "fields": { + "parentId": "51:5503", + "childrenIds": [ + "51:5509" + ], + "absoluteBoundingBox": { + "height": 2, + "width": 480, + "x": 10169, + "y": 2197 + }, + "absoluteRenderBounds": null, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:356", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:356", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 2, + "inferredAutoLayout": { + "counterAxisAlignItems": "MIN", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "progress", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": false, + "width": 480, + "x": 0, + "y": 79 + }, + "extra": {}, + "fieldErrors": {} + }, + "51:5509": { + "id": "51:5509", + "type": "RECTANGLE", + "fields": { + "parentId": "51:5508", + "absoluteBoundingBox": { + "height": 2, + "width": 160, + "x": 10169, + "y": 2197 + }, + "absoluteRenderBounds": null, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "boundVariables": { + "fills": [ + { + "id": "VariableID:11:30", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:11:30", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.7259615659713745, + "g": 0.6905941367149353, + "r": 0.018614115193486217 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 2, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FILL", + "maxHeight": null, + "maxWidth": null, + "name": "bar", + "opacity": 1, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 160, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "51:5618": { + "id": "51:5618", + "type": "FRAME", + "fields": { + "parentId": "51:5496", + "childrenIds": [ + "51:5619" + ], + "absoluteBoundingBox": { + "height": 4, + "width": 480, + "x": 10169, + "y": 2058 + }, + "absoluteRenderBounds": { + "height": 4, + "width": 480, + "x": 10169, + "y": 2058 + }, + "bottomLeftRadius": 1000, + "bottomRightRadius": 1000, + "boundVariables": { + "fills": [ + { + "id": "VariableID:11:3", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 1000, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:11:3", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.9839743375778198, + "g": 0.9189276099205016, + "r": 0.8625540137290955 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 4, + "inferredAutoLayout": { + "counterAxisAlignItems": "MIN", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "progress", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 1000, + "topRightRadius": 1000, + "visible": true, + "width": 480, + "x": 0, + "y": 157 + }, + "extra": {}, + "fieldErrors": {} + }, + "51:5619": { + "id": "51:5619", + "type": "RECTANGLE", + "fields": { + "parentId": "51:5618", + "absoluteBoundingBox": { + "height": 4, + "width": 160, + "x": 10169, + "y": 2058 + }, + "absoluteRenderBounds": { + "height": 4, + "width": 160, + "x": 10169, + "y": 2058 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "boundVariables": { + "fills": [ + { + "id": "VariableID:11:10", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:11:10", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.7843137383460999, + "g": 0.4745098054409027, + "r": 0.20000000298023224 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 4, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FILL", + "maxHeight": null, + "maxWidth": null, + "name": "bar", + "opacity": 1, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 160, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + } + }, + "diagnostics": [] +} \ No newline at end of file From ad02a4b8a9861620ad40afeeee8e385a4c91832c Mon Sep 17 00:00:00 2001 From: leader Date: Sat, 24 Oct 2026 10:53:37 +0900 Subject: [PATCH 126/149] fix(codegen): emit type-correct componentTsx for responsive scalar props, partial variant maps, boolean props and state selectors - keep typography and Image src scalar across responsive variants - add undefined branches for variant lookups missing union keys - declare and destructure referenced boolean props with collision-free names - map selected/hoevr states to valid selectors; keep src out of pseudo objects --- .../changepack_log_eRTXLDvy0yMJ3YwboDGWu.json | 2 +- .../src/codegen/image/project_tree.rs | 1 + .../devup-mcp-devup-ui/src/codegen/variant.rs | 1 + .../src/codegen/variant/drawn_when.rs | 42 +- .../src/codegen/variant/scalar_properties.rs | 74 + .../src/codegen/variant/text_properties.rs | 3 +- .../src/codegen/variant/variant_projection.rs | 38 +- .../tests/chondrogen_component_typings.rs | 75 + .../fixtures/chondrogen_board_row_set.json | 5732 +++++++++++++++++ .../fixtures/chondrogen_cta_banner_set.json | 1991 ++++++ .../fixtures/chondrogen_icon_button_set.json | 1386 ++++ .../chondrogen_list_type_button_set.json | 1178 ++++ .../fixtures/chondrogen_page_title_set.json | 5557 ++++++++++++++++ .../fixtures/chondrogen_profile_card_set.json | 2208 +++++++ 14 files changed, 18270 insertions(+), 18 deletions(-) create mode 100644 crates/devup-mcp-devup-ui/src/codegen/variant/scalar_properties.rs create mode 100644 crates/devup-mcp-devup-ui/tests/chondrogen_component_typings.rs create mode 100644 crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_board_row_set.json create mode 100644 crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_cta_banner_set.json create mode 100644 crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_icon_button_set.json create mode 100644 crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_list_type_button_set.json create mode 100644 crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_page_title_set.json create mode 100644 crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_profile_card_set.json diff --git a/.changepacks/changepack_log_eRTXLDvy0yMJ3YwboDGWu.json b/.changepacks/changepack_log_eRTXLDvy0yMJ3YwboDGWu.json index 755e0270..b1326be0 100644 --- a/.changepacks/changepack_log_eRTXLDvy0yMJ3YwboDGWu.json +++ b/.changepacks/changepack_log_eRTXLDvy0yMJ3YwboDGWu.json @@ -2,6 +2,6 @@ "changes": { "Cargo.toml": "Patch" }, - "note": "componentTsx: fix button set panic, expose component text as unique props even when declared names normalize alike, drop set-canvas placement, emit undefined for absent variant values, account explicitly for unrepresented variant children without claiming emitted property provenance", + "note": "componentTsx: fix button set panic, expose component text as unique props even when declared names normalize alike, drop set-canvas placement, emit undefined for absent variant values, preserve scalar typography and image sources across responsive variants, declare referenced boolean props, emit valid interaction selectors, and account explicitly for unrepresented variant children without claiming emitted property provenance", "date": "2026-10-24T00:21:08.585604600Z" } diff --git a/crates/devup-mcp-devup-ui/src/codegen/image/project_tree.rs b/crates/devup-mcp-devup-ui/src/codegen/image/project_tree.rs index 9f5d4c9c..cd65ad44 100644 --- a/crates/devup-mcp-devup-ui/src/codegen/image/project_tree.rs +++ b/crates/devup-mcp-devup-ui/src/codegen/image/project_tree.rs @@ -27,6 +27,7 @@ pub(super) fn project_tree( drawn_when: Vec::new(), leading_comment: None, slots: Vec::new(), + scalar_sources: Vec::new(), }; let visible = node.typed_view().value("fills").and_then(Value::as_array); if tree.component == "Image" { diff --git a/crates/devup-mcp-devup-ui/src/codegen/variant.rs b/crates/devup-mcp-devup-ui/src/codegen/variant.rs index d7b30a67..9c3d26ea 100644 --- a/crates/devup-mcp-devup-ui/src/codegen/variant.rs +++ b/crates/devup-mcp-devup-ui/src/codegen/variant.rs @@ -2,3 +2,4 @@ include!("variant/variant_projection.rs"); include!("variant/drawn_when.rs"); include!("variant/text_properties.rs"); +include!("variant/scalar_properties.rs"); diff --git a/crates/devup-mcp-devup-ui/src/codegen/variant/drawn_when.rs b/crates/devup-mcp-devup-ui/src/codegen/variant/drawn_when.rs index dcb81964..37d4bfdd 100644 --- a/crates/devup-mcp-devup-ui/src/codegen/variant/drawn_when.rs +++ b/crates/devup-mcp-devup-ui/src/codegen/variant/drawn_when.rs @@ -104,6 +104,7 @@ fn merge_child_props( Some(&child_path), &child.props, ); + child.scalar_sources = scalar_sources(records, definitions, dimensions, viewport, effect_value, &child_path); merge_child_props( child, records, @@ -126,7 +127,7 @@ fn changed_effect_props( ) -> BTreeMap { let effect_name = definitions .iter() - .find(|definition| definition.name.eq_ignore_ascii_case("effect")) + .find(|definition| definition.name.eq_ignore_ascii_case("effect") && !dimensions.iter().any(|dimension| dimension.name == definition.name)) .map(|definition| definition.name.clone()); let keys = records .iter() @@ -183,7 +184,7 @@ fn expression_for_prop( ) -> Option { let effect_name = definitions .iter() - .find(|definition| definition.name.eq_ignore_ascii_case("effect")) + .find(|definition| definition.name.eq_ignore_ascii_case("effect") && !dimensions.iter().any(|dimension| dimension.name == definition.name)) .map(|definition| definition.name.as_str()); let dependencies = dimensions .iter() @@ -395,13 +396,13 @@ fn nested_expression( ), ) }) - .filter_map(|(option, value)| Some((option, value?))) + .map(|(option, value)| (option, value.unwrap_or(Expression::Undefined))) .collect::>() }; if rest.is_empty() { let values = branch(first, Vec::new()); - if values.is_empty() { + if values.iter().all(|(_, value)| matches!(value, Expression::Undefined)) { return None; } // Every option that was drawn saying the same thing is not a choice at @@ -426,12 +427,13 @@ fn nested_expression( } // One option carrying the only value reads better as the condition it // is than as a map with a single entry and a hole where the rest were. - if values.len() == 1 - && let Expression::Literal(value) = &values[0].1 + let present = values.iter().filter(|(_, value)| !matches!(value, Expression::Undefined)).collect::>(); + if present.len() == 1 + && let Expression::Literal(value) = &present[0].1 { return Some(Expression::Conditional( first.name.clone(), - values[0].0.clone(), + present[0].0.clone(), value.clone(), )); } @@ -616,7 +618,7 @@ fn render_component( } else { let props = dimensions .iter() - .map(|definition| definition.name.clone()) + .map(|definition| if definition.boolean { format!("{} = {}", definition.name, definition.default) } else { definition.name.clone() }) .chain(text_properties.iter().map(|(name, default)| format!("{name} = {default}"))) .collect::>() .join(", "); @@ -632,6 +634,17 @@ fn render_tree( transition: Option<&(String, String)>, depth: usize, ) -> String { + if !tree.scalar_sources.is_empty() { + let rendered = tree.scalar_sources.iter().map(|(source, display)| { + let mut image = tree.clone(); + image.scalar_sources.clear(); + image.props.insert("src".into(), source.clone()); + image.props.insert("display".into(), display.clone()); + render_tree(&image, selectors, transition, depth) + }).collect::>().join("\n"); + let indent = " ".repeat(depth); + return format!("{indent}<>\n{rendered}\n{indent}"); + } let indent = " ".repeat(depth); let child_indent = " ".repeat(depth + 1); let mut rendered_props = Vec::new(); @@ -647,9 +660,14 @@ fn render_tree( }) .collect::>() .join(",\n"); - rendered_props.push(format!( - "{child_indent}{selector}={{{{\n{values}\n{child_indent}}}}}" - )); + if selector.starts_with('&') { + let selector = serde_json::to_string(selector).unwrap_or_default(); + rendered_props.push(format!("{child_indent}selectors={{{{ {selector}: {{\n{values}\n{child_indent}}} }}}}")); + } else { + rendered_props.push(format!( + "{child_indent}{selector}={{{{\n{values}\n{child_indent}}}}}" + )); + } } for (name, expression) in &tree.props { rendered_props.push(format!( @@ -741,6 +759,7 @@ fn needs_const(prop: &str) -> bool { fn render_expression_attr(prop: &str, expression: &Expression) -> String { match expression { + Expression::Undefined => "={undefined}".into(), Expression::Literal(value) => format!("=\"{value}\""), Expression::Variant(name, _) if needs_const(prop) => { let rendered = render_expression_value(expression, 0); @@ -777,6 +796,7 @@ fn indent_attribute_expression(expression: &str, depth: usize) -> String { fn render_expression_value(expression: &Expression, depth: usize) -> String { match expression { + Expression::Undefined => "undefined".into(), Expression::Literal(value) => serde_json::to_string(value).unwrap_or_default(), Expression::Responsive(slots) => format!( "[{}]", diff --git a/crates/devup-mcp-devup-ui/src/codegen/variant/scalar_properties.rs b/crates/devup-mcp-devup-ui/src/codegen/variant/scalar_properties.rs new file mode 100644 index 00000000..5f607686 --- /dev/null +++ b/crates/devup-mcp-devup-ui/src/codegen/variant/scalar_properties.rs @@ -0,0 +1,74 @@ +fn bind_referenced_properties(tree: &mut Tree, dimensions: &mut Vec) { + if let Some(name) = &tree.visible_when + && !dimensions.iter().any(|definition| definition.name == *name) + { + dimensions.push(Definition { + name: name.clone(), + source: name.clone(), + default: "true".into(), + options: Vec::new(), + boolean: true, + }); + } + for child in &mut tree.children { + bind_referenced_properties(child, dimensions); + } +} + +fn scalar_sources( + records: &[Record], + definitions: &[Definition], + dimensions: &[Definition], + viewport: Option<&Definition>, + effect: &str, + path: &NodePath, +) -> Vec<(String, String)> { + if viewport.is_none() { return Vec::new(); } + let sources = records.iter().filter_map(|record| { + let tree = tree_at(&record.tree, path)?; + (tree.component == "Image").then(|| tree.props.get("src").cloned()).flatten() + }).collect::>(); + if sources.len() < 2 { + return Vec::new(); + } + sources.into_iter().filter_map(|source| { + let visibility = records.iter().cloned().map(|mut record| { + fn set(tree: &mut Tree, source: &str) { + if tree.component == "Image" { + let display = if tree.props.get("src").is_some_and(|value| value == source) { "inline" } else { "none" }; + tree.props.insert("src".into(), display.into()); + } + for child in &mut tree.children { set(child, source); } + } + set(&mut record.tree, &source); + record + }).collect::>(); + let display = expression_for_prop(&visibility, definitions, dimensions, viewport, effect, None, path, "src")?; + Some((render_expression_attr("src", &Expression::Literal(source)), render_expression_attr("display", &display))) + }).collect() +} + +fn effect_selector(value: &str) -> Option<&str> { + match value.to_ascii_lowercase().as_str() { + "default" => Some("&"), + "hover" | "hoevr" => Some("_hover"), + "active" => Some("_active"), + "focus" => Some("_focus"), + "disabled" => Some("_disabled"), + "selected" => Some("&[aria-selected=\"true\"]"), + _ => None, + } +} + +fn typography_attribute(expression: &Expression) -> (String, String) { + if let Expression::Responsive(slots) = expression { + let values = slots.iter().flatten().collect::>(); + if values.len() == 1 + && let Some(value) = values.first() { + return ("typography".into(), render_expression_attr("typography", &Expression::Literal((*value).clone()))); + } + let slots = slots.iter().map(|slot| slot.as_ref().map_or("null".into(), |token| format!("{{ typography: {} }}", serde_json::to_string(token).unwrap_or_default()))).collect::>().join(", "); + return ("selectors".into(), format!("={{{{ \"&\": [{slots}] }}}}")); + } + ("typography".into(), render_expression_attr("typography", expression)) +} diff --git a/crates/devup-mcp-devup-ui/src/codegen/variant/text_properties.rs b/crates/devup-mcp-devup-ui/src/codegen/variant/text_properties.rs index 2910d52e..ad4c65f1 100644 --- a/crates/devup-mcp-devup-ui/src/codegen/variant/text_properties.rs +++ b/crates/devup-mcp-devup-ui/src/codegen/variant/text_properties.rs @@ -2,8 +2,9 @@ fn bind_text_properties( snapshot: &Snapshot, set: &RawNode, tree: &mut Tree, + dimensions: &[Definition], ) -> BTreeMap { - let mut used = definitions(set).into_iter().map(|definition| definition.name) + let mut used = dimensions.iter().map(|definition| definition.name.clone()) .collect::>(); let mut properties = BTreeMap::new(); for (name, definition) in set.typed_view() diff --git a/crates/devup-mcp-devup-ui/src/codegen/variant/variant_projection.rs b/crates/devup-mcp-devup-ui/src/codegen/variant/variant_projection.rs index 0af0d72a..920b132b 100644 --- a/crates/devup-mcp-devup-ui/src/codegen/variant/variant_projection.rs +++ b/crates/devup-mcp-devup-ui/src/codegen/variant/variant_projection.rs @@ -37,6 +37,7 @@ pub(super) struct Tree { /// In a responsive merge, the node each slot's width drew this element /// from and that node's text; empty everywhere else. pub slots: Vec)>>, + pub scalar_sources: Vec<(String, String)>, } #[derive(Clone, Debug)] @@ -62,6 +63,7 @@ struct Definition { #[derive(Clone, Debug)] enum Expression { + Undefined, Literal(String), /// A devup-ui responsive array, written out slot by slot, narrowest first. /// `None` is the literal `null`, which is not "no value" but "whatever the @@ -104,7 +106,7 @@ pub(super) fn generate_variant_component_set( if !merge_all && !definitions.iter().any(|definition| matches!(definition.name.to_ascii_lowercase().as_str(), "effect" | "viewport")) { return Ok(None); } - let records = set + let mut records = set .typed_view() .child_ids() .filter_map(|id| snapshot.nodes.get(id)) @@ -139,6 +141,18 @@ pub(super) fn generate_variant_component_set( if records.is_empty() { return Ok(None); } + if records.iter().any(|record| record.tree.component == "Image") + && records.iter().all(|record| is_asset_leaf(&record.tree)) + { + for record in &mut records { + if let Some(mask) = record.tree.props.remove("maskImage") { + let source = mask.trim_start_matches("url(").trim_end_matches(')').to_owned(); + record.tree.component = "Image".into(); + record.tree.props.retain(|name, _| !matches!(name.as_str(), "bg" | "maskPos" | "maskRepeat" | "maskSize")); + record.tree.props.insert("src".into(), source); + } + } + } // A boolean property is not part of a variant's identity — it never appears // in `variantProperties` — so filtering records by it matches nothing. let default_filters = definitions @@ -155,7 +169,7 @@ pub(super) fn generate_variant_component_set( false, ) })?; - let dimensions = definitions + let mut dimensions = definitions .iter() .filter(|definition| { !matches!( @@ -167,7 +181,12 @@ pub(super) fn generate_variant_component_set( .collect::>(); let effect = definitions .iter() - .find(|definition| definition.name.eq_ignore_ascii_case("effect")); + .find(|definition| definition.name.eq_ignore_ascii_case("effect") && !records.iter().any(|record| record.tree.props.contains_key("src")) && definition.options.iter().all(|value| value.eq_ignore_ascii_case(&definition.default) || effect_selector(value).is_some())); + if effect.is_none() + && let Some(definition) = definitions.iter().find(|definition| definition.name.eq_ignore_ascii_case("effect")) + { + dimensions.push(definition.clone()); + } let viewport = definitions .iter() .find(|definition| definition.name.eq_ignore_ascii_case("viewport")); @@ -191,6 +210,7 @@ pub(super) fn generate_variant_component_set( None, &default.tree.props, ); + root.scalar_sources = scalar_sources(&records, &definitions, &dimensions, viewport, effect.map_or("default", |definition| definition.default.as_str()), &[]); merge_child_props( &mut root, &records, @@ -200,6 +220,7 @@ pub(super) fn generate_variant_component_set( effect.map_or("default", |definition| definition.default.as_str()), &[], ); + bind_referenced_properties(&mut root, &mut dimensions); let mut selectors = BTreeMap::new(); let mut transition_props = BTreeSet::new(); @@ -218,12 +239,14 @@ pub(super) fn generate_variant_component_set( ); if !changed.is_empty() { transition_props.extend(changed.keys().cloned()); - selectors.insert(format!("_{effect_value}"), changed); + if let Some(selector) = effect_selector(effect_value) { + selectors.insert(selector.to_owned(), changed); + } } } } let transition = transition(snapshot, &default.node_id, &transition_props); - let text_properties = bind_text_properties(snapshot, set, &mut root); + let text_properties = bind_text_properties(snapshot, set, &mut root, &dimensions); let component_name = legacy_component_name(set.typed_view().name().unwrap_or("Component")); let code = mark_node( &set.id, @@ -503,6 +526,7 @@ fn project_tree_inner( drawn_when: Vec::new(), leading_comment: None, slots: Vec::new(), + scalar_sources: Vec::new(), }); } let asset = super::image::asset_kind(snapshot, node, &options.asset_policies); @@ -646,6 +670,7 @@ fn project_tree_inner( drawn_when: Vec::new(), leading_comment: None, slots: Vec::new(), + scalar_sources: Vec::new(), }; Ok(super::image::project_tree(tree, snapshot, node, options)) } @@ -819,6 +844,9 @@ fn merged_props( path.unwrap_or_default(), &prop, )?; + if prop == "typography" { + return Some(typography_attribute(&expression)); + } Some((prop.clone(), render_expression_attr(&prop, &expression))) }) .collect() diff --git a/crates/devup-mcp-devup-ui/tests/chondrogen_component_typings.rs b/crates/devup-mcp-devup-ui/tests/chondrogen_component_typings.rs new file mode 100644 index 00000000..755c0b17 --- /dev/null +++ b/crates/devup-mcp-devup-ui/tests/chondrogen_component_typings.rs @@ -0,0 +1,75 @@ +use devup_mcp_devup_ui::codegen::{CodegenOptions, generate_component}; +use devup_mcp_figma::Snapshot; + +fn generate(raw: &str) -> String { + let snapshot: Snapshot = serde_json::from_str(raw).unwrap(); + let mut options = CodegenOptions { + asset_names_per_node: true, + ..CodegenOptions::default() + }; + for node in snapshot + .nodes + .values() + .filter(|node| node.node_type == "TEXT") + { + if let Some(style) = node + .typed_view() + .string("textStyleId") + .filter(|id| !id.is_empty()) + { + options + .text_style_tokens + .insert(style.into(), "bodySm".into()); + } + } + generate_component(&snapshot, &snapshot.roots[0], &options) + .unwrap() + .tsx +} + +#[test] +fn responsive_typography_remains_a_scalar_token() { + let tsx = generate(include_str!("fixtures/chondrogen_board_row_set.json")); + assert!(!tsx.contains("typography={["), "{tsx}"); + assert!(tsx.contains("typography=\"bodySm\""), "{tsx}"); +} + +#[test] +fn responsive_sources_remain_scalar_images() { + let tsx = generate(include_str!("fixtures/chondrogen_cta_banner_set.json")); + assert!(!tsx.contains("src={["), "{tsx}"); + assert!( + tsx.contains("caret-2.svg") && tsx.contains("caret.svg"), + "{tsx}" + ); +} + +#[test] +fn absent_responsive_variant_values_have_explicit_undefined_branches() { + let tsx = generate(include_str!("fixtures/chondrogen_page_title_set.json")); + assert!(tsx.contains("White: undefined"), "{tsx}"); +} + +#[test] +fn referenced_boolean_properties_are_declared_with_text_properties() { + let tsx = generate(include_str!("fixtures/chondrogen_page_title_set.json")); + assert!(tsx.contains("depth2?: boolean"), "{tsx}"); + assert!(tsx.contains("depth3?: boolean"), "{tsx}"); + assert!(tsx.contains("deapth2?: string"), "{tsx}"); +} + +#[test] +fn selected_effect_uses_a_valid_selector() { + let tsx = generate(include_str!( + "fixtures/chondrogen_list_type_button_set.json" + )); + assert!(!tsx.contains("_selected="), "{tsx}"); + assert!(tsx.contains("aria-selected"), "{tsx}"); +} + +#[test] +fn icon_effects_never_emit_attributes_inside_style_selectors() { + let tsx = generate(include_str!("fixtures/chondrogen_icon_button_set.json")); + assert!(!tsx.contains("_hoevr="), "{tsx}"); + assert!(!tsx.contains("\"src\":"), "{tsx}"); +} diff --git a/crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_board_row_set.json b/crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_board_row_set.json new file mode 100644 index 00000000..c84f0007 --- /dev/null +++ b/crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_board_row_set.json @@ -0,0 +1,5732 @@ +{ + "fileKey": "K2kUnxGgvooAJwCQiVTb7g", + "version": null, + "roots": [ + "73:5583" + ], + "nodes": { + "73:5583": { + "id": "73:5583", + "type": "COMPONENT_SET", + "fields": { + "parentId": "19:963", + "parentType": "SECTION", + "parentName": "Components", + "childrenIds": [ + "73:5584", + "73:5591", + "73:6001", + "73:6022", + "73:7420", + "73:7427" + ], + "absoluteBoundingBox": { + "height": 666, + "width": 1328, + "x": 8801, + "y": 8606 + }, + "absoluteRenderBounds": { + "height": 666, + "width": 1328, + "x": 8801, + "y": 8606 + }, + "bottomLeftRadius": 5, + "bottomRightRadius": 5, + "clipsContent": true, + "componentPropertyDefinitions": { + "effect": { + "defaultValue": "default", + "type": "VARIANT", + "variantOptions": [ + "default", + "hover" + ] + }, + "pinned": { + "defaultValue": "false", + "type": "VARIANT", + "variantOptions": [ + "false", + "true" + ] + }, + "viewport": { + "defaultValue": "Desktop", + "type": "VARIANT", + "variantOptions": [ + "Desktop", + "Mobile" + ] + }, + "등록일#47:103": { + "defaultValue": "2026. 09. 01", + "type": "TEXT" + }, + "번호#47:97": { + "defaultValue": "11", + "type": "TEXT" + }, + "제목#47:100": { + "defaultValue": "공지 제목이 들어갑니다", + "type": "TEXT" + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 5, + "dashPattern": [ + 10, + 5 + ], + "defaultVariant": { + "$nodeId": "73:5584", + "$nodeType": "COMPONENT" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 666, + "inferredAutoLayout": { + "counterAxisAlignItems": "MIN", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 16, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutPositioning": "AUTO", + "paddingBottom": 24, + "paddingLeft": 24, + "paddingRight": 24, + "paddingTop": 24, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 16, + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "BoardRow", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 24, + "paddingLeft": 24, + "paddingRight": 24, + "paddingTop": 24, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 5, + "topRightRadius": 5, + "visible": true, + "width": 1328, + "x": 131, + "y": 7568 + }, + "extra": {}, + "fieldErrors": {} + }, + "73:5584": { + "id": "73:5584", + "type": "COMPONENT", + "fields": { + "parentId": "73:5583", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "73:5585", + "73:5587", + "73:5589" + ], + "absoluteBoundingBox": { + "height": 74, + "width": 1280, + "x": 8825, + "y": 8630 + }, + "absoluteRenderBounds": { + "height": 74, + "width": 1280, + "x": 8825, + "y": 8630 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "boundVariables": { + "strokes": [ + { + "id": "VariableID:9:351", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "height": 74, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 0, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 22, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 22, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "pinned=false, effect=default, viewport=Desktop", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 22, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 22, + "strokeBottomWeight": 1, + "strokeLeftWeight": 0, + "strokeRightWeight": 0, + "strokeTopWeight": 0, + "strokeWeight": { + "$unsupported": "symbol" + }, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:351", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.9215686321258544, + "g": 0.9058823585510254, + "r": 0.8980392217636108 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "effect": "default", + "pinned": "false", + "viewport": "Desktop" + }, + "visible": true, + "width": 1280, + "x": 24, + "y": 24 + }, + "extra": {}, + "fieldErrors": {} + }, + "73:5585": { + "id": "73:5585", + "type": "FRAME", + "fields": { + "parentId": "73:5584", + "childrenIds": [ + "73:5586" + ], + "absoluteBoundingBox": { + "height": 23, + "width": 96, + "x": 8825, + "y": 8655 + }, + "absoluteRenderBounds": { + "height": 23, + "width": 96, + "x": 8825, + "y": 8655 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "height": 23, + "inferredAutoLayout": { + "counterAxisAlignItems": "MIN", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 0, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "num", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 96, + "x": 0, + "y": 25 + }, + "extra": {}, + "fieldErrors": {} + }, + "73:5586": { + "id": "73:5586", + "type": "TEXT", + "fields": { + "parentId": "73:5585", + "absoluteBoundingBox": { + "height": 23, + "width": 12, + "x": 8867, + "y": 8655 + }, + "absoluteRenderBounds": { + "height": 10.8447265625, + "width": 9.7451171875, + "x": 8867.6904296875, + "y": 8661.169921875 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:13:153", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:40:430", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "11", + "componentPropertyReferences": { + "characters": "번호#47:97" + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:13:153", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.5160255432128906, + "g": 0.4530186951160431, + "r": 0.4333290457725525 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Medium" + }, + "fontSize": 15, + "height": 23, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "number", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:a2319c6931a342fd831baf32b252756b15de72a6,", + "textTruncation": "DISABLED", + "visible": true, + "width": 12, + "x": 42, + "y": 0, + "styledTextSegments": [ + { + "characters": "11", + "end": 2, + "fillStyleId": "", + "fontWeight": 500, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:a2319c6931a342fd831baf32b252756b15de72a6," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "73:5587": { + "id": "73:5587", + "type": "FRAME", + "fields": { + "parentId": "73:5584", + "childrenIds": [ + "73:5588" + ], + "absoluteBoundingBox": { + "height": 29, + "width": 1024, + "x": 8921, + "y": 8652 + }, + "absoluteRenderBounds": { + "height": 29, + "width": 1024, + "x": 8921, + "y": 8652 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "height": 29, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 10, + "layoutAlign": "INHERIT", + "layoutGrow": 1, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 10, + "layoutGrow": 1, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "title wrap", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 1024, + "x": 96, + "y": 22 + }, + "extra": {}, + "fieldErrors": {} + }, + "73:5588": { + "id": "73:5588", + "type": "TEXT", + "fields": { + "parentId": "73:5587", + "absoluteBoundingBox": { + "height": 29, + "width": 1024, + "x": 8921, + "y": 8652 + }, + "absoluteRenderBounds": { + "height": 15.9658203125, + "width": 160.7421875, + "x": 8921.666015625, + "y": 8658.4560546875 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:951", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "공지 제목이 들어갑니다", + "componentPropertyReferences": { + "characters": "제목#47:100" + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.1529411822557449, + "g": 0.08627451211214066, + "r": 0.06666667014360428 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "SemiBold" + }, + "fontSize": 18, + "height": 29, + "layoutGrow": 1, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 160.0000023841858 + }, + "maxHeight": null, + "maxWidth": null, + "name": "title", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "HEIGHT", + "textStyleId": "S:39037a8c63dc0865f4023ee82afa65f8f6888f1a,", + "textTruncation": "DISABLED", + "visible": true, + "width": 1024, + "x": 0, + "y": 0, + "styledTextSegments": [ + { + "characters": "공지 제목이 들어갑니다", + "end": 12, + "fillStyleId": "", + "fontWeight": 600, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:39037a8c63dc0865f4023ee82afa65f8f6888f1a," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "73:5589": { + "id": "73:5589", + "type": "FRAME", + "fields": { + "parentId": "73:5584", + "childrenIds": [ + "73:5590" + ], + "absoluteBoundingBox": { + "height": 21, + "width": 160, + "x": 9945, + "y": 8656 + }, + "absoluteRenderBounds": { + "height": 21, + "width": 160, + "x": 9945, + "y": 8656 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "height": 21, + "inferredAutoLayout": { + "counterAxisAlignItems": "MIN", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 0, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "date wrap", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 160, + "x": 1120, + "y": 26 + }, + "extra": {}, + "fieldErrors": {} + }, + "73:5590": { + "id": "73:5590", + "type": "TEXT", + "fields": { + "parentId": "73:5589", + "absoluteBoundingBox": { + "height": 21, + "width": 75, + "x": 9987.5, + "y": 8656 + }, + "absoluteRenderBounds": { + "height": 10.416015625, + "width": 71.953125, + "x": 9988.2841796875, + "y": 8661.765625 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:359", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:956", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "2026. 09. 01", + "componentPropertyReferences": { + "characters": "등록일#47:103" + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:359", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.6682692170143127, + "g": 0.6104382276535034, + "r": 0.5815227627754211 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Regular" + }, + "fontSize": 14, + "height": 21, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "date", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:b24dbe253b56eab7caafdd82199d1cfb1188d02f,", + "textTruncation": "DISABLED", + "visible": true, + "width": 75, + "x": 42.5, + "y": 0, + "styledTextSegments": [ + { + "characters": "2026. 09. 01", + "end": 12, + "fillStyleId": "", + "fontWeight": 400, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:b24dbe253b56eab7caafdd82199d1cfb1188d02f," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "73:5591": { + "id": "73:5591", + "type": "COMPONENT", + "fields": { + "parentId": "73:5583", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "73:5592", + "73:5594", + "73:5598" + ], + "absoluteBoundingBox": { + "height": 74, + "width": 1280, + "x": 8825, + "y": 8720 + }, + "absoluteRenderBounds": { + "height": 74, + "width": 1280, + "x": 8825, + "y": 8720 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:349", + "type": "VARIABLE_ALIAS" + } + ], + "strokes": [ + { + "id": "VariableID:9:351", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:349", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.9843137264251708, + "g": 0.9803921580314636, + "r": 0.9764705896377563 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 74, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 0, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 22, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 22, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "pinned=true, effect=default, viewport=Desktop", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 22, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 22, + "strokeBottomWeight": 1, + "strokeLeftWeight": 0, + "strokeRightWeight": 0, + "strokeTopWeight": 0, + "strokeWeight": { + "$unsupported": "symbol" + }, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:351", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.9215686321258544, + "g": 0.9058823585510254, + "r": 0.8980392217636108 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "effect": "default", + "pinned": "true", + "viewport": "Desktop" + }, + "visible": true, + "width": 1280, + "x": 24, + "y": 114 + }, + "extra": {}, + "fieldErrors": {} + }, + "73:5592": { + "id": "73:5592", + "type": "FRAME", + "fields": { + "parentId": "73:5591", + "childrenIds": [ + "73:5593" + ], + "absoluteBoundingBox": { + "height": 23, + "width": 96, + "x": 8825, + "y": 8745 + }, + "absoluteRenderBounds": { + "height": 23, + "width": 96, + "x": 8825, + "y": 8745 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "height": 23, + "inferredAutoLayout": { + "counterAxisAlignItems": "MIN", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 0, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "num", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 96, + "x": 0, + "y": 25 + }, + "extra": {}, + "fieldErrors": {} + }, + "73:5593": { + "id": "73:5593", + "type": "TEXT", + "fields": { + "parentId": "73:5592", + "absoluteBoundingBox": { + "height": 23, + "width": 12, + "x": 8867, + "y": 8745 + }, + "absoluteRenderBounds": { + "height": 10.8447265625, + "width": 9.7451171875, + "x": 8867.6904296875, + "y": 8751.169921875 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:13:153", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:40:430", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "11", + "componentPropertyReferences": { + "characters": "번호#47:97" + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:13:153", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.5160255432128906, + "g": 0.4530186951160431, + "r": 0.4333290457725525 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Medium" + }, + "fontSize": 15, + "height": 23, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "number", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:a2319c6931a342fd831baf32b252756b15de72a6,", + "textTruncation": "DISABLED", + "visible": true, + "width": 12, + "x": 42, + "y": 0, + "styledTextSegments": [ + { + "characters": "11", + "end": 2, + "fillStyleId": "", + "fontWeight": 500, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:a2319c6931a342fd831baf32b252756b15de72a6," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "73:5594": { + "id": "73:5594", + "type": "FRAME", + "fields": { + "parentId": "73:5591", + "childrenIds": [ + "73:5596", + "73:5597" + ], + "absoluteBoundingBox": { + "height": 29, + "width": 1024, + "x": 8921, + "y": 8742 + }, + "absoluteRenderBounds": { + "height": 29, + "width": 1024, + "x": 8921, + "y": 8742 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "height": 29, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 10, + "layoutAlign": "INHERIT", + "layoutGrow": 1, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 10, + "layoutGrow": 1, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "title wrap", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 1024, + "x": 96, + "y": 22 + }, + "extra": {}, + "fieldErrors": {} + }, + "73:5596": { + "id": "73:5596", + "type": "INSTANCE", + "fields": { + "parentId": "73:5594", + "childrenIds": [ + "I73:5596;73:6826", + "I73:5596;32:362" + ], + "absoluteBoundingBox": { + "height": 28, + "width": 59, + "x": 8921, + "y": 8742.5 + }, + "absoluteRenderBounds": { + "height": 28, + "width": 59, + "x": 8921, + "y": 8742.5 + }, + "bottomLeftRadius": 100, + "bottomRightRadius": 100, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:345", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 100, + "counterAxisAlignItems": "CENTER", + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:345", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.7323716878890991, + "g": 0.34177345037460327, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 28, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 6, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 4, + "paddingLeft": 8, + "paddingRight": 10, + "paddingTop": 4, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 6, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "pin tag", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 4, + "paddingLeft": 8, + "paddingRight": 10, + "paddingTop": 4, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 100, + "topRightRadius": 100, + "visible": true, + "width": 59, + "x": 0, + "y": 0.5, + "mainComponentId": "32:368", + "mainComponentName": "noticeTag", + "mainComponentSetId": null + }, + "extra": {}, + "fieldErrors": {} + }, + "73:5597": { + "id": "73:5597", + "type": "TEXT", + "fields": { + "parentId": "73:5594", + "absoluteBoundingBox": { + "height": 29, + "width": 955, + "x": 8990, + "y": 8742 + }, + "absoluteRenderBounds": { + "height": 16.2548828125, + "width": 160.9228515625, + "x": 8990.576171875, + "y": 8748.3115234375 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:951", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "공지 제목이 들어갑니다", + "componentPropertyReferences": { + "characters": "제목#47:100" + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.1529411822557449, + "g": 0.08627451211214066, + "r": 0.06666667014360428 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "ExtraBold" + }, + "fontSize": 18, + "height": 29, + "layoutGrow": 1, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 160.0000023841858 + }, + "maxHeight": null, + "maxWidth": null, + "name": "title", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "HEIGHT", + "textStyleId": "S:7394fa3e4417d5710756b2f56d83e6105d173d06,", + "textTruncation": "DISABLED", + "visible": true, + "width": 955, + "x": 69, + "y": 0, + "styledTextSegments": [ + { + "characters": "공지 제목이 들어갑니다", + "end": 12, + "fillStyleId": "", + "fontWeight": 800, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:7394fa3e4417d5710756b2f56d83e6105d173d06," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "73:5598": { + "id": "73:5598", + "type": "FRAME", + "fields": { + "parentId": "73:5591", + "childrenIds": [ + "73:5599" + ], + "absoluteBoundingBox": { + "height": 21, + "width": 160, + "x": 9945, + "y": 8746 + }, + "absoluteRenderBounds": { + "height": 21, + "width": 160, + "x": 9945, + "y": 8746 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "height": 21, + "inferredAutoLayout": { + "counterAxisAlignItems": "MIN", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 0, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "date wrap", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 160, + "x": 1120, + "y": 26 + }, + "extra": {}, + "fieldErrors": {} + }, + "73:5599": { + "id": "73:5599", + "type": "TEXT", + "fields": { + "parentId": "73:5598", + "absoluteBoundingBox": { + "height": 21, + "width": 75, + "x": 9987.5, + "y": 8746 + }, + "absoluteRenderBounds": { + "height": 10.416015625, + "width": 71.953125, + "x": 9988.2841796875, + "y": 8751.765625 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:359", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:956", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "2026. 09. 01", + "componentPropertyReferences": { + "characters": "등록일#47:103" + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:359", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.6682692170143127, + "g": 0.6104382276535034, + "r": 0.5815227627754211 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Regular" + }, + "fontSize": 14, + "height": 21, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "date", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:b24dbe253b56eab7caafdd82199d1cfb1188d02f,", + "textTruncation": "DISABLED", + "visible": true, + "width": 75, + "x": 42.5, + "y": 0, + "styledTextSegments": [ + { + "characters": "2026. 09. 01", + "end": 12, + "fillStyleId": "", + "fontWeight": 400, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:b24dbe253b56eab7caafdd82199d1cfb1188d02f," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "73:6001": { + "id": "73:6001", + "type": "COMPONENT", + "fields": { + "parentId": "73:5583", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "73:6002", + "73:6013", + "73:6008" + ], + "absoluteBoundingBox": { + "height": 74, + "width": 1280, + "x": 8825, + "y": 8810 + }, + "absoluteRenderBounds": { + "height": 74, + "width": 1280, + "x": 8825, + "y": 8810 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:349", + "type": "VARIABLE_ALIAS" + } + ], + "strokes": [ + { + "id": "VariableID:9:351", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:349", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.9843137264251708, + "g": 0.9803921580314636, + "r": 0.9764705896377563 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 74, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 0, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 22, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 22, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "pinned=false, effect=hover, viewport=Desktop", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 22, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 22, + "strokeBottomWeight": 1, + "strokeLeftWeight": 0, + "strokeRightWeight": 0, + "strokeTopWeight": 0, + "strokeWeight": { + "$unsupported": "symbol" + }, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:351", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.9215686321258544, + "g": 0.9058823585510254, + "r": 0.8980392217636108 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "effect": "hover", + "pinned": "false", + "viewport": "Desktop" + }, + "visible": true, + "width": 1280, + "x": 24, + "y": 204 + }, + "extra": {}, + "fieldErrors": {} + }, + "73:6002": { + "id": "73:6002", + "type": "FRAME", + "fields": { + "parentId": "73:6001", + "childrenIds": [ + "73:6003" + ], + "absoluteBoundingBox": { + "height": 23, + "width": 96, + "x": 8825, + "y": 8835 + }, + "absoluteRenderBounds": { + "height": 23, + "width": 96, + "x": 8825, + "y": 8835 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "height": 23, + "inferredAutoLayout": { + "counterAxisAlignItems": "MIN", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 0, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "num", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 96, + "x": 0, + "y": 25 + }, + "extra": {}, + "fieldErrors": {} + }, + "73:6003": { + "id": "73:6003", + "type": "TEXT", + "fields": { + "parentId": "73:6002", + "absoluteBoundingBox": { + "height": 23, + "width": 12, + "x": 8867, + "y": 8835 + }, + "absoluteRenderBounds": { + "height": 10.8447265625, + "width": 9.7451171875, + "x": 8867.6904296875, + "y": 8841.169921875 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:13:153", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:40:430", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "11", + "componentPropertyReferences": { + "characters": "번호#47:97" + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:13:153", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.5160255432128906, + "g": 0.4530186951160431, + "r": 0.4333290457725525 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Medium" + }, + "fontSize": 15, + "height": 23, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "number", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:a2319c6931a342fd831baf32b252756b15de72a6,", + "textTruncation": "DISABLED", + "visible": true, + "width": 12, + "x": 42, + "y": 0, + "styledTextSegments": [ + { + "characters": "11", + "end": 2, + "fillStyleId": "", + "fontWeight": 500, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:a2319c6931a342fd831baf32b252756b15de72a6," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "73:6008": { + "id": "73:6008", + "type": "FRAME", + "fields": { + "parentId": "73:6001", + "childrenIds": [ + "73:6009" + ], + "absoluteBoundingBox": { + "height": 21, + "width": 160, + "x": 9945, + "y": 8836 + }, + "absoluteRenderBounds": { + "height": 21, + "width": 160, + "x": 9945, + "y": 8836 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "height": 21, + "inferredAutoLayout": { + "counterAxisAlignItems": "MIN", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 0, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "date wrap", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 160, + "x": 1120, + "y": 26 + }, + "extra": {}, + "fieldErrors": {} + }, + "73:6009": { + "id": "73:6009", + "type": "TEXT", + "fields": { + "parentId": "73:6008", + "absoluteBoundingBox": { + "height": 21, + "width": 75, + "x": 9987.5, + "y": 8836 + }, + "absoluteRenderBounds": { + "height": 10.416015625, + "width": 71.953125, + "x": 9988.2841796875, + "y": 8841.765625 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:359", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:956", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "2026. 09. 01", + "componentPropertyReferences": { + "characters": "등록일#47:103" + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:359", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.6682692170143127, + "g": 0.6104382276535034, + "r": 0.5815227627754211 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Regular" + }, + "fontSize": 14, + "height": 21, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "date", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:b24dbe253b56eab7caafdd82199d1cfb1188d02f,", + "textTruncation": "DISABLED", + "visible": true, + "width": 75, + "x": 42.5, + "y": 0, + "styledTextSegments": [ + { + "characters": "2026. 09. 01", + "end": 12, + "fillStyleId": "", + "fontWeight": 400, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:b24dbe253b56eab7caafdd82199d1cfb1188d02f," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "73:6013": { + "id": "73:6013", + "type": "TEXT", + "fields": { + "parentId": "73:6001", + "absoluteBoundingBox": { + "height": 29, + "width": 1024, + "x": 8921, + "y": 8832 + }, + "absoluteRenderBounds": { + "height": 15.9658203125, + "width": 160.7421875, + "x": 8921.666015625, + "y": 8838.4560546875 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:951", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "공지 제목이 들어갑니다", + "componentPropertyReferences": { + "characters": "제목#47:100" + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.1529411822557449, + "g": 0.08627451211214066, + "r": 0.06666667014360428 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "SemiBold" + }, + "fontSize": 18, + "height": 29, + "layoutGrow": 1, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 160.0000023841858 + }, + "maxHeight": null, + "maxWidth": null, + "name": "title", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "HEIGHT", + "textStyleId": "S:39037a8c63dc0865f4023ee82afa65f8f6888f1a,", + "textTruncation": "DISABLED", + "visible": true, + "width": 1024, + "x": 96, + "y": 22, + "styledTextSegments": [ + { + "characters": "공지 제목이 들어갑니다", + "end": 12, + "fillStyleId": "", + "fontWeight": 600, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:39037a8c63dc0865f4023ee82afa65f8f6888f1a," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "73:6022": { + "id": "73:6022", + "type": "COMPONENT", + "fields": { + "parentId": "73:5583", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "73:6023", + "73:6025", + "73:6028" + ], + "absoluteBoundingBox": { + "height": 74, + "width": 1280, + "x": 8825, + "y": 8900 + }, + "absoluteRenderBounds": { + "height": 74, + "width": 1280, + "x": 8825, + "y": 8900 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "boundVariables": { + "fills": [ + { + "id": "VariableID:11:2", + "type": "VARIABLE_ALIAS" + } + ], + "strokes": [ + { + "id": "VariableID:9:351", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:11:2", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 1, + "g": 0.9715099334716796, + "r": 0.9487178325653076 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 74, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 0, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 22, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 22, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "pinned=true, effect=hover, viewport=Desktop", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 22, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 22, + "strokeBottomWeight": 1, + "strokeLeftWeight": 0, + "strokeRightWeight": 0, + "strokeTopWeight": 0, + "strokeWeight": { + "$unsupported": "symbol" + }, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:351", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.9215686321258544, + "g": 0.9058823585510254, + "r": 0.8980392217636108 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "effect": "hover", + "pinned": "true", + "viewport": "Desktop" + }, + "visible": true, + "width": 1280, + "x": 24, + "y": 294 + }, + "extra": {}, + "fieldErrors": {} + }, + "73:6023": { + "id": "73:6023", + "type": "FRAME", + "fields": { + "parentId": "73:6022", + "childrenIds": [ + "73:6024" + ], + "absoluteBoundingBox": { + "height": 23, + "width": 96, + "x": 8825, + "y": 8925 + }, + "absoluteRenderBounds": { + "height": 23, + "width": 96, + "x": 8825, + "y": 8925 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "height": 23, + "inferredAutoLayout": { + "counterAxisAlignItems": "MIN", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 0, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "num", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 96, + "x": 0, + "y": 25 + }, + "extra": {}, + "fieldErrors": {} + }, + "73:6024": { + "id": "73:6024", + "type": "TEXT", + "fields": { + "parentId": "73:6023", + "absoluteBoundingBox": { + "height": 23, + "width": 12, + "x": 8867, + "y": 8925 + }, + "absoluteRenderBounds": { + "height": 10.8447265625, + "width": 9.7451171875, + "x": 8867.6904296875, + "y": 8931.169921875 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:13:153", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:40:430", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "11", + "componentPropertyReferences": { + "characters": "번호#47:97" + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:13:153", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.5160255432128906, + "g": 0.4530186951160431, + "r": 0.4333290457725525 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Medium" + }, + "fontSize": 15, + "height": 23, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "number", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:a2319c6931a342fd831baf32b252756b15de72a6,", + "textTruncation": "DISABLED", + "visible": true, + "width": 12, + "x": 42, + "y": 0, + "styledTextSegments": [ + { + "characters": "11", + "end": 2, + "fillStyleId": "", + "fontWeight": 500, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:a2319c6931a342fd831baf32b252756b15de72a6," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "73:6025": { + "id": "73:6025", + "type": "FRAME", + "fields": { + "parentId": "73:6022", + "childrenIds": [ + "73:6026", + "73:6027" + ], + "absoluteBoundingBox": { + "height": 29, + "width": 1024, + "x": 8921, + "y": 8922 + }, + "absoluteRenderBounds": { + "height": 29, + "width": 1024, + "x": 8921, + "y": 8922 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "height": 29, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 10, + "layoutAlign": "INHERIT", + "layoutGrow": 1, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 10, + "layoutGrow": 1, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "title wrap", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 1024, + "x": 96, + "y": 22 + }, + "extra": {}, + "fieldErrors": {} + }, + "73:6026": { + "id": "73:6026", + "type": "INSTANCE", + "fields": { + "parentId": "73:6025", + "childrenIds": [ + "I73:6026;73:6826", + "I73:6026;32:362" + ], + "absoluteBoundingBox": { + "height": 28, + "width": 59, + "x": 8921, + "y": 8922.5 + }, + "absoluteRenderBounds": { + "height": 28, + "width": 59, + "x": 8921, + "y": 8922.5 + }, + "bottomLeftRadius": 100, + "bottomRightRadius": 100, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:345", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 100, + "counterAxisAlignItems": "CENTER", + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:345", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.7323716878890991, + "g": 0.34177345037460327, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 28, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 6, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 4, + "paddingLeft": 8, + "paddingRight": 10, + "paddingTop": 4, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 6, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "pin tag", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 4, + "paddingLeft": 8, + "paddingRight": 10, + "paddingTop": 4, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 100, + "topRightRadius": 100, + "visible": true, + "width": 59, + "x": 0, + "y": 0.5, + "mainComponentId": "32:368", + "mainComponentName": "noticeTag", + "mainComponentSetId": null + }, + "extra": {}, + "fieldErrors": {} + }, + "73:6027": { + "id": "73:6027", + "type": "TEXT", + "fields": { + "parentId": "73:6025", + "absoluteBoundingBox": { + "height": 29, + "width": 955, + "x": 8990, + "y": 8922 + }, + "absoluteRenderBounds": { + "height": 16.2548828125, + "width": 160.9228515625, + "x": 8990.576171875, + "y": 8928.3115234375 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:951", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "공지 제목이 들어갑니다", + "componentPropertyReferences": { + "characters": "제목#47:100" + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.1529411822557449, + "g": 0.08627451211214066, + "r": 0.06666667014360428 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "ExtraBold" + }, + "fontSize": 18, + "height": 29, + "layoutGrow": 1, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 160.0000023841858 + }, + "maxHeight": null, + "maxWidth": null, + "name": "title", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "HEIGHT", + "textStyleId": "S:7394fa3e4417d5710756b2f56d83e6105d173d06,", + "textTruncation": "DISABLED", + "visible": true, + "width": 955, + "x": 69, + "y": 0, + "styledTextSegments": [ + { + "characters": "공지 제목이 들어갑니다", + "end": 12, + "fillStyleId": "", + "fontWeight": 800, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:7394fa3e4417d5710756b2f56d83e6105d173d06," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "73:6028": { + "id": "73:6028", + "type": "FRAME", + "fields": { + "parentId": "73:6022", + "childrenIds": [ + "73:6029" + ], + "absoluteBoundingBox": { + "height": 21, + "width": 160, + "x": 9945, + "y": 8926 + }, + "absoluteRenderBounds": { + "height": 21, + "width": 160, + "x": 9945, + "y": 8926 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "height": 21, + "inferredAutoLayout": { + "counterAxisAlignItems": "MIN", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 0, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "date wrap", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 160, + "x": 1120, + "y": 26 + }, + "extra": {}, + "fieldErrors": {} + }, + "73:6029": { + "id": "73:6029", + "type": "TEXT", + "fields": { + "parentId": "73:6028", + "absoluteBoundingBox": { + "height": 21, + "width": 75, + "x": 9987.5, + "y": 8926 + }, + "absoluteRenderBounds": { + "height": 10.416015625, + "width": 71.953125, + "x": 9988.2841796875, + "y": 8931.765625 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:359", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:956", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "2026. 09. 01", + "componentPropertyReferences": { + "characters": "등록일#47:103" + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:359", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.6682692170143127, + "g": 0.6104382276535034, + "r": 0.5815227627754211 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Regular" + }, + "fontSize": 14, + "height": 21, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "date", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:b24dbe253b56eab7caafdd82199d1cfb1188d02f,", + "textTruncation": "DISABLED", + "visible": true, + "width": 75, + "x": 42.5, + "y": 0, + "styledTextSegments": [ + { + "characters": "2026. 09. 01", + "end": 12, + "fillStyleId": "", + "fontWeight": 400, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:b24dbe253b56eab7caafdd82199d1cfb1188d02f," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "73:7420": { + "id": "73:7420", + "type": "COMPONENT", + "fields": { + "parentId": "73:5583", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "73:7423", + "73:7425" + ], + "absoluteBoundingBox": { + "height": 103, + "width": 1280, + "x": 8825, + "y": 8990 + }, + "absoluteRenderBounds": { + "height": 103, + "width": 1280, + "x": 8825, + "y": 8990 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "boundVariables": { + "strokes": [ + { + "id": "VariableID:9:351", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "height": 103, + "inferredAutoLayout": { + "counterAxisAlignItems": "MIN", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 8, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutPositioning": "AUTO", + "paddingBottom": 24, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 24, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 8, + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "pinned=false, effect=default, viewport=Mobile", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 24, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 24, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 0, + "strokeRightWeight": 0, + "strokeTopWeight": 0, + "strokeWeight": { + "$unsupported": "symbol" + }, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:351", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.9215686321258544, + "g": 0.9058823585510254, + "r": 0.8980392217636108 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "effect": "default", + "pinned": "false", + "viewport": "Mobile" + }, + "visible": true, + "width": 1280, + "x": 24, + "y": 384 + }, + "extra": {}, + "fieldErrors": {} + }, + "73:7423": { + "id": "73:7423", + "type": "FRAME", + "fields": { + "parentId": "73:7420", + "childrenIds": [ + "73:7424" + ], + "absoluteBoundingBox": { + "height": 26, + "width": 1280, + "x": 8825, + "y": 9014 + }, + "absoluteRenderBounds": { + "height": 26, + "width": 1280, + "x": 8825, + "y": 9014 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "height": 26, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 10, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 10, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "title wrap", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 1280, + "x": 0, + "y": 24 + }, + "extra": {}, + "fieldErrors": {} + }, + "73:7424": { + "id": "73:7424", + "type": "TEXT", + "fields": { + "parentId": "73:7423", + "absoluteBoundingBox": { + "height": 26, + "width": 1280, + "x": 8825, + "y": 9014 + }, + "absoluteRenderBounds": { + "height": 14.19140625, + "width": 142.8828125, + "x": 8825.591796875, + "y": 9020.072265625 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:951", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "공지 제목이 들어갑니다", + "componentPropertyReferences": { + "characters": "제목#47:100" + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.1529411822557449, + "g": 0.08627451211214066, + "r": 0.06666667014360428 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "SemiBold" + }, + "fontSize": 16, + "height": 26, + "layoutGrow": 1, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 160.0000023841858 + }, + "maxHeight": null, + "maxWidth": null, + "name": "title", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "HEIGHT", + "textStyleId": "S:39037a8c63dc0865f4023ee82afa65f8f6888f1a,", + "textTruncation": "DISABLED", + "visible": true, + "width": 1280, + "x": 0, + "y": 0, + "styledTextSegments": [ + { + "characters": "공지 제목이 들어갑니다", + "end": 12, + "fillStyleId": "", + "fontWeight": 600, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:39037a8c63dc0865f4023ee82afa65f8f6888f1a," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "73:7425": { + "id": "73:7425", + "type": "FRAME", + "fields": { + "parentId": "73:7420", + "childrenIds": [ + "73:7426" + ], + "absoluteBoundingBox": { + "height": 20, + "width": 1280, + "x": 8825, + "y": 9048 + }, + "absoluteRenderBounds": { + "height": 20, + "width": 1280, + "x": 8825, + "y": 9048 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "height": 20, + "inferredAutoLayout": { + "counterAxisAlignItems": "MIN", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 0, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "date wrap", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 1280, + "x": 0, + "y": 58 + }, + "extra": {}, + "fieldErrors": {} + }, + "73:7426": { + "id": "73:7426", + "type": "TEXT", + "fields": { + "parentId": "73:7425", + "absoluteBoundingBox": { + "height": 20, + "width": 69, + "x": 8825, + "y": 9048 + }, + "absoluteRenderBounds": { + "height": 9.671875, + "width": 66.814453125, + "x": 8825.7275390625, + "y": 9053.4970703125 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:359", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:956", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "2026. 09. 01", + "componentPropertyReferences": { + "characters": "등록일#47:103" + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:359", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.6682692170143127, + "g": 0.6104382276535034, + "r": 0.5815227627754211 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Regular" + }, + "fontSize": 13, + "height": 20, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "date", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:b24dbe253b56eab7caafdd82199d1cfb1188d02f,", + "textTruncation": "DISABLED", + "visible": true, + "width": 69, + "x": 0, + "y": 0, + "styledTextSegments": [ + { + "characters": "2026. 09. 01", + "end": 12, + "fillStyleId": "", + "fontWeight": 400, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:b24dbe253b56eab7caafdd82199d1cfb1188d02f," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "73:7427": { + "id": "73:7427", + "type": "COMPONENT", + "fields": { + "parentId": "73:5583", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "73:7430", + "73:7433" + ], + "absoluteBoundingBox": { + "height": 139, + "width": 1280, + "x": 8825, + "y": 9109 + }, + "absoluteRenderBounds": { + "height": 139, + "width": 1280, + "x": 8825, + "y": 9109 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:349", + "type": "VARIABLE_ALIAS" + } + ], + "strokes": [ + { + "id": "VariableID:9:351", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:349", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.9843137264251708, + "g": 0.9803921580314636, + "r": 0.9764705896377563 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 139, + "inferredAutoLayout": { + "counterAxisAlignItems": "MIN", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 8, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutPositioning": "AUTO", + "paddingBottom": 24, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 24, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 8, + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "pinned=true, effect=default, viewport=Mobile", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 24, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 24, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 0, + "strokeRightWeight": 0, + "strokeTopWeight": 0, + "strokeWeight": { + "$unsupported": "symbol" + }, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:351", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.9215686321258544, + "g": 0.9058823585510254, + "r": 0.8980392217636108 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "effect": "default", + "pinned": "true", + "viewport": "Mobile" + }, + "visible": true, + "width": 1280, + "x": 24, + "y": 503 + }, + "extra": {}, + "fieldErrors": {} + }, + "73:7430": { + "id": "73:7430", + "type": "FRAME", + "fields": { + "parentId": "73:7427", + "childrenIds": [ + "73:7431", + "73:7432" + ], + "absoluteBoundingBox": { + "height": 62, + "width": 1280, + "x": 8825, + "y": 9133 + }, + "absoluteRenderBounds": { + "height": 62, + "width": 1280, + "x": 8825, + "y": 9133 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "height": 62, + "inferredAutoLayout": { + "counterAxisAlignItems": "MIN", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 8, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 8, + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "title wrap", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 1280, + "x": 0, + "y": 24 + }, + "extra": {}, + "fieldErrors": {} + }, + "73:7431": { + "id": "73:7431", + "type": "INSTANCE", + "fields": { + "parentId": "73:7430", + "childrenIds": [ + "I73:7431;73:6826", + "I73:7431;32:362" + ], + "absoluteBoundingBox": { + "height": 28, + "width": 59, + "x": 8825, + "y": 9133 + }, + "absoluteRenderBounds": { + "height": 28, + "width": 59, + "x": 8825, + "y": 9133 + }, + "bottomLeftRadius": 100, + "bottomRightRadius": 100, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:345", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 100, + "counterAxisAlignItems": "CENTER", + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:345", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.7323716878890991, + "g": 0.34177345037460327, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 28, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 6, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 4, + "paddingLeft": 8, + "paddingRight": 10, + "paddingTop": 4, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 6, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "pin tag", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 4, + "paddingLeft": 8, + "paddingRight": 10, + "paddingTop": 4, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 100, + "topRightRadius": 100, + "visible": true, + "width": 59, + "x": 0, + "y": 0, + "mainComponentId": "32:368", + "mainComponentName": "noticeTag", + "mainComponentSetId": null + }, + "extra": {}, + "fieldErrors": {} + }, + "73:7432": { + "id": "73:7432", + "type": "TEXT", + "fields": { + "parentId": "73:7430", + "absoluteBoundingBox": { + "height": 26, + "width": 1280, + "x": 8825, + "y": 9169 + }, + "absoluteRenderBounds": { + "height": 14.447265625, + "width": 143.04296875, + "x": 8825.51171875, + "y": 9174.9443359375 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:951", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "공지 제목이 들어갑니다", + "componentPropertyReferences": { + "characters": "제목#47:100" + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.1529411822557449, + "g": 0.08627451211214066, + "r": 0.06666667014360428 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "ExtraBold" + }, + "fontSize": 16, + "height": 26, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 160.0000023841858 + }, + "maxHeight": null, + "maxWidth": null, + "name": "title", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "HEIGHT", + "textStyleId": "S:7394fa3e4417d5710756b2f56d83e6105d173d06,", + "textTruncation": "DISABLED", + "visible": true, + "width": 1280, + "x": 0, + "y": 36, + "styledTextSegments": [ + { + "characters": "공지 제목이 들어갑니다", + "end": 12, + "fillStyleId": "", + "fontWeight": 800, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:7394fa3e4417d5710756b2f56d83e6105d173d06," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "73:7433": { + "id": "73:7433", + "type": "FRAME", + "fields": { + "parentId": "73:7427", + "childrenIds": [ + "73:7434" + ], + "absoluteBoundingBox": { + "height": 20, + "width": 1280, + "x": 8825, + "y": 9203 + }, + "absoluteRenderBounds": { + "height": 20, + "width": 1280, + "x": 8825, + "y": 9203 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "height": 20, + "inferredAutoLayout": { + "counterAxisAlignItems": "MIN", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 0, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "date wrap", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 1280, + "x": 0, + "y": 94 + }, + "extra": {}, + "fieldErrors": {} + }, + "73:7434": { + "id": "73:7434", + "type": "TEXT", + "fields": { + "parentId": "73:7433", + "absoluteBoundingBox": { + "height": 20, + "width": 69, + "x": 8825, + "y": 9203 + }, + "absoluteRenderBounds": { + "height": 9.671875, + "width": 66.814453125, + "x": 8825.7275390625, + "y": 9208.4970703125 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:359", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:956", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "2026. 09. 01", + "componentPropertyReferences": { + "characters": "등록일#47:103" + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:359", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.6682692170143127, + "g": 0.6104382276535034, + "r": 0.5815227627754211 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Regular" + }, + "fontSize": 13, + "height": 20, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "date", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:b24dbe253b56eab7caafdd82199d1cfb1188d02f,", + "textTruncation": "DISABLED", + "visible": true, + "width": 69, + "x": 0, + "y": 0, + "styledTextSegments": [ + { + "characters": "2026. 09. 01", + "end": 12, + "fillStyleId": "", + "fontWeight": 400, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:b24dbe253b56eab7caafdd82199d1cfb1188d02f," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "I73:5596;32:362": { + "id": "I73:5596;32:362", + "type": "TEXT", + "fields": { + "parentId": "73:5596", + "absoluteBoundingBox": { + "height": 20, + "width": 23, + "x": 8947, + "y": 8746.5 + }, + "absoluteRenderBounds": { + "height": 11.21875, + "width": 21.697265625, + "x": 8947.3896484375, + "y": 8751.3994140625 + }, + "characters": "중요", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "ExtraBold" + }, + "fontSize": 13, + "height": 20, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "중요", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:6ff02d889b5b4f9b91a3306f15c43ad1d19ee259,", + "textTruncation": "DISABLED", + "visible": true, + "width": 23, + "x": 26, + "y": 4, + "styledTextSegments": [ + { + "characters": "중요", + "end": 2, + "fillStyleId": "", + "fontWeight": 800, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:6ff02d889b5b4f9b91a3306f15c43ad1d19ee259," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "I73:5596;73:6826": { + "id": "I73:5596;73:6826", + "type": "INSTANCE", + "fields": { + "parentId": "73:5596", + "childrenIds": [ + "I73:5596;73:6826;73:6818" + ], + "absoluteBoundingBox": { + "height": 12, + "width": 12, + "x": 8929, + "y": 8750.5 + }, + "absoluteRenderBounds": { + "height": 12, + "width": 12, + "x": 8929, + "y": 8750.5 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": false + } + ], + "height": 12, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0.4997005462646485, + "paddingLeft": 0.5, + "paddingRight": 0.4995241165161133, + "paddingTop": 0.5, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "isAsset": true, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "megaphone", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "targetAspectRatio": { + "x": 512, + "y": 512 + }, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 12, + "x": 8, + "y": 8, + "mainComponentId": "73:6825", + "mainComponentName": "megaphone", + "mainComponentSetId": null + }, + "extra": {}, + "fieldErrors": {} + }, + "I73:5596;73:6826;73:6818": { + "id": "I73:5596;73:6826;73:6818", + "type": "GROUP", + "fields": { + "parentId": "I73:5596;73:6826", + "childrenIds": [ + "I73:5596;73:6826;73:6819", + "I73:5596;73:6826;73:6821", + "I73:5596;73:6826;73:6823" + ], + "absoluteBoundingBox": { + "height": 11.000299453735352, + "width": 11.000475883483888, + "x": 8929.5, + "y": 8751 + }, + "absoluteRenderBounds": { + "height": 11.000299453735352, + "width": 11.000475883483888, + "x": 8929.5, + "y": 8751 + }, + "height": 11.000299453735352, + "inferredAutoLayout": { + "counterAxisAlignItems": "MIN", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "FIXED" + }, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Group", + "opacity": 1, + "visible": true, + "width": 11.000475883483888, + "x": 0.5, + "y": 0.5 + }, + "extra": {}, + "fieldErrors": {} + }, + "I73:5596;73:6826;73:6819": { + "id": "I73:5596;73:6826;73:6819", + "type": "GROUP", + "fields": { + "parentId": "I73:5596;73:6826;73:6818", + "childrenIds": [ + "I73:5596;73:6826;73:6820" + ], + "absoluteBoundingBox": { + "height": 7.182965278625488, + "width": 8.283529281616211, + "x": 8929.5, + "y": 8751.6591796875 + }, + "absoluteRenderBounds": { + "height": 7.182965278625488, + "width": 8.283529281616211, + "x": 8929.5, + "y": 8751.6591796875 + }, + "height": 7.182965278625488, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Group", + "opacity": 1, + "visible": true, + "width": 8.283529281616211, + "x": 0.5, + "y": 1.1593017578125 + }, + "extra": {}, + "fieldErrors": {} + }, + "I73:5596;73:6826;73:6820": { + "id": "I73:5596;73:6826;73:6820", + "type": "VECTOR", + "fields": { + "parentId": "I73:5596;73:6826;73:6819", + "absoluteBoundingBox": { + "height": 7.182965278625488, + "width": 8.283529281616211, + "x": 8929.5, + "y": 8751.6591796875 + }, + "absoluteRenderBounds": { + "height": 7.1826171875, + "width": 8.283203125, + "x": 8929.5, + "y": 8751.6591796875 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 7.182965278625488, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeWeight": 15.058823585510254, + "visible": true, + "width": 8.283529281616211, + "x": 0.5, + "y": 1.1593017578125 + }, + "extra": {}, + "fieldErrors": {} + }, + "I73:5596;73:6826;73:6821": { + "id": "I73:5596;73:6826;73:6821", + "type": "GROUP", + "fields": { + "parentId": "I73:5596;73:6826;73:6818", + "childrenIds": [ + "I73:5596;73:6826;73:6822" + ], + "absoluteBoundingBox": { + "height": 4.511041164398193, + "width": 3.78228759765625, + "x": 8930.4052734375, + "y": 8757.4892578125 + }, + "absoluteRenderBounds": { + "height": 4.511041164398193, + "width": 3.78228759765625, + "x": 8930.4052734375, + "y": 8757.4892578125 + }, + "height": 4.511041164398193, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Group", + "opacity": 1, + "visible": true, + "width": 3.78228759765625, + "x": 1.4054877758026123, + "y": 6.9888916015625 + }, + "extra": {}, + "fieldErrors": {} + }, + "I73:5596;73:6826;73:6822": { + "id": "I73:5596;73:6826;73:6822", + "type": "VECTOR", + "fields": { + "parentId": "I73:5596;73:6826;73:6821", + "absoluteBoundingBox": { + "height": 4.511041164398193, + "width": 3.78228759765625, + "x": 8930.4052734375, + "y": 8757.4892578125 + }, + "absoluteRenderBounds": { + "height": 4.5107421875, + "width": 3.7822265625, + "x": 8930.4052734375, + "y": 8757.4892578125 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 4.511041164398193, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeWeight": 15.058823585510254, + "visible": true, + "width": 3.78228759765625, + "x": 1.4054877758026123, + "y": 6.9888916015625 + }, + "extra": {}, + "fieldErrors": {} + }, + "I73:5596;73:6826;73:6823": { + "id": "I73:5596;73:6826;73:6823", + "type": "GROUP", + "fields": { + "parentId": "I73:5596;73:6826;73:6818", + "childrenIds": [ + "I73:5596;73:6826;73:6824" + ], + "absoluteBoundingBox": { + "height": 8.501578330993652, + "width": 2.0121943950653076, + "x": 8938.48828125, + "y": 8751 + }, + "absoluteRenderBounds": { + "height": 8.501953125, + "width": 2.0121943950653076, + "x": 8938.48828125, + "y": 8751 + }, + "height": 8.501578330993652, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Group", + "opacity": 1, + "visible": true, + "width": 2.0121943950653076, + "x": 9.487805366516112, + "y": 0.5 + }, + "extra": {}, + "fieldErrors": {} + }, + "I73:5596;73:6826;73:6824": { + "id": "I73:5596;73:6826;73:6824", + "type": "VECTOR", + "fields": { + "parentId": "I73:5596;73:6826;73:6823", + "absoluteBoundingBox": { + "height": 8.501578330993652, + "width": 2.0121943950653076, + "x": 8938.48828125, + "y": 8751 + }, + "absoluteRenderBounds": { + "height": 8.501953125, + "width": 2.01171875, + "x": 8938.48828125, + "y": 8751 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 8.501578330993652, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeWeight": 15.058823585510254, + "visible": true, + "width": 2.0121943950653076, + "x": 9.487805366516112, + "y": 0.5 + }, + "extra": {}, + "fieldErrors": {} + }, + "I73:6026;32:362": { + "id": "I73:6026;32:362", + "type": "TEXT", + "fields": { + "parentId": "73:6026", + "absoluteBoundingBox": { + "height": 20, + "width": 23, + "x": 8947, + "y": 8926.5 + }, + "absoluteRenderBounds": { + "height": 11.21875, + "width": 21.697265625, + "x": 8947.3896484375, + "y": 8931.3994140625 + }, + "characters": "중요", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "ExtraBold" + }, + "fontSize": 13, + "height": 20, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "중요", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:6ff02d889b5b4f9b91a3306f15c43ad1d19ee259,", + "textTruncation": "DISABLED", + "visible": true, + "width": 23, + "x": 26, + "y": 4, + "styledTextSegments": [ + { + "characters": "중요", + "end": 2, + "fillStyleId": "", + "fontWeight": 800, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:6ff02d889b5b4f9b91a3306f15c43ad1d19ee259," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "I73:6026;73:6826": { + "id": "I73:6026;73:6826", + "type": "INSTANCE", + "fields": { + "parentId": "73:6026", + "childrenIds": [ + "I73:6026;73:6826;73:6818" + ], + "absoluteBoundingBox": { + "height": 12, + "width": 12, + "x": 8929, + "y": 8930.5 + }, + "absoluteRenderBounds": { + "height": 12, + "width": 12, + "x": 8929, + "y": 8930.5 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": false + } + ], + "height": 12, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0.4997005462646485, + "paddingLeft": 0.5, + "paddingRight": 0.4995241165161133, + "paddingTop": 0.5, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "isAsset": true, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "megaphone", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "targetAspectRatio": { + "x": 512, + "y": 512 + }, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 12, + "x": 8, + "y": 8, + "mainComponentId": "73:6825", + "mainComponentName": "megaphone", + "mainComponentSetId": null + }, + "extra": {}, + "fieldErrors": {} + }, + "I73:6026;73:6826;73:6818": { + "id": "I73:6026;73:6826;73:6818", + "type": "GROUP", + "fields": { + "parentId": "I73:6026;73:6826", + "childrenIds": [ + "I73:6026;73:6826;73:6819", + "I73:6026;73:6826;73:6821", + "I73:6026;73:6826;73:6823" + ], + "absoluteBoundingBox": { + "height": 11.000299453735352, + "width": 11.000475883483888, + "x": 8929.5, + "y": 8931 + }, + "absoluteRenderBounds": { + "height": 11.000299453735352, + "width": 11.000475883483888, + "x": 8929.5, + "y": 8931 + }, + "height": 11.000299453735352, + "inferredAutoLayout": { + "counterAxisAlignItems": "MIN", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "FIXED" + }, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Group", + "opacity": 1, + "visible": true, + "width": 11.000475883483888, + "x": 0.5, + "y": 0.5 + }, + "extra": {}, + "fieldErrors": {} + }, + "I73:6026;73:6826;73:6819": { + "id": "I73:6026;73:6826;73:6819", + "type": "GROUP", + "fields": { + "parentId": "I73:6026;73:6826;73:6818", + "childrenIds": [ + "I73:6026;73:6826;73:6820" + ], + "absoluteBoundingBox": { + "height": 7.182965278625488, + "width": 8.283529281616211, + "x": 8929.5, + "y": 8931.6591796875 + }, + "absoluteRenderBounds": { + "height": 7.182965278625488, + "width": 8.283529281616211, + "x": 8929.5, + "y": 8931.6591796875 + }, + "height": 7.182965278625488, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Group", + "opacity": 1, + "visible": true, + "width": 8.283529281616211, + "x": 0.5, + "y": 1.1593017578125 + }, + "extra": {}, + "fieldErrors": {} + }, + "I73:6026;73:6826;73:6820": { + "id": "I73:6026;73:6826;73:6820", + "type": "VECTOR", + "fields": { + "parentId": "I73:6026;73:6826;73:6819", + "absoluteBoundingBox": { + "height": 7.182965278625488, + "width": 8.283529281616211, + "x": 8929.5, + "y": 8931.6591796875 + }, + "absoluteRenderBounds": { + "height": 7.1826171875, + "width": 8.283203125, + "x": 8929.5, + "y": 8931.6591796875 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 7.182965278625488, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeWeight": 15.058823585510254, + "visible": true, + "width": 8.283529281616211, + "x": 0.5, + "y": 1.1593017578125 + }, + "extra": {}, + "fieldErrors": {} + }, + "I73:6026;73:6826;73:6821": { + "id": "I73:6026;73:6826;73:6821", + "type": "GROUP", + "fields": { + "parentId": "I73:6026;73:6826;73:6818", + "childrenIds": [ + "I73:6026;73:6826;73:6822" + ], + "absoluteBoundingBox": { + "height": 4.511041164398193, + "width": 3.78228759765625, + "x": 8930.4052734375, + "y": 8937.4892578125 + }, + "absoluteRenderBounds": { + "height": 4.511041164398193, + "width": 3.78228759765625, + "x": 8930.4052734375, + "y": 8937.4892578125 + }, + "height": 4.511041164398193, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Group", + "opacity": 1, + "visible": true, + "width": 3.78228759765625, + "x": 1.4054877758026123, + "y": 6.9888916015625 + }, + "extra": {}, + "fieldErrors": {} + }, + "I73:6026;73:6826;73:6822": { + "id": "I73:6026;73:6826;73:6822", + "type": "VECTOR", + "fields": { + "parentId": "I73:6026;73:6826;73:6821", + "absoluteBoundingBox": { + "height": 4.511041164398193, + "width": 3.78228759765625, + "x": 8930.4052734375, + "y": 8937.4892578125 + }, + "absoluteRenderBounds": { + "height": 4.5107421875, + "width": 3.7822265625, + "x": 8930.4052734375, + "y": 8937.4892578125 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 4.511041164398193, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeWeight": 15.058823585510254, + "visible": true, + "width": 3.78228759765625, + "x": 1.4054877758026123, + "y": 6.9888916015625 + }, + "extra": {}, + "fieldErrors": {} + }, + "I73:6026;73:6826;73:6823": { + "id": "I73:6026;73:6826;73:6823", + "type": "GROUP", + "fields": { + "parentId": "I73:6026;73:6826;73:6818", + "childrenIds": [ + "I73:6026;73:6826;73:6824" + ], + "absoluteBoundingBox": { + "height": 8.501578330993652, + "width": 2.0121943950653076, + "x": 8938.48828125, + "y": 8931 + }, + "absoluteRenderBounds": { + "height": 8.501953125, + "width": 2.0121943950653076, + "x": 8938.48828125, + "y": 8931 + }, + "height": 8.501578330993652, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Group", + "opacity": 1, + "visible": true, + "width": 2.0121943950653076, + "x": 9.487805366516112, + "y": 0.5 + }, + "extra": {}, + "fieldErrors": {} + }, + "I73:6026;73:6826;73:6824": { + "id": "I73:6026;73:6826;73:6824", + "type": "VECTOR", + "fields": { + "parentId": "I73:6026;73:6826;73:6823", + "absoluteBoundingBox": { + "height": 8.501578330993652, + "width": 2.0121943950653076, + "x": 8938.48828125, + "y": 8931 + }, + "absoluteRenderBounds": { + "height": 8.501953125, + "width": 2.01171875, + "x": 8938.48828125, + "y": 8931 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 8.501578330993652, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeWeight": 15.058823585510254, + "visible": true, + "width": 2.0121943950653076, + "x": 9.487805366516112, + "y": 0.5 + }, + "extra": {}, + "fieldErrors": {} + }, + "I73:7431;32:362": { + "id": "I73:7431;32:362", + "type": "TEXT", + "fields": { + "parentId": "73:7431", + "absoluteBoundingBox": { + "height": 20, + "width": 23, + "x": 8851, + "y": 9137 + }, + "absoluteRenderBounds": { + "height": 11.21875, + "width": 21.697265625, + "x": 8851.3896484375, + "y": 9141.8994140625 + }, + "characters": "중요", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "ExtraBold" + }, + "fontSize": 13, + "height": 20, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "중요", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:6ff02d889b5b4f9b91a3306f15c43ad1d19ee259,", + "textTruncation": "DISABLED", + "visible": true, + "width": 23, + "x": 26, + "y": 4, + "styledTextSegments": [ + { + "characters": "중요", + "end": 2, + "fillStyleId": "", + "fontWeight": 800, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:6ff02d889b5b4f9b91a3306f15c43ad1d19ee259," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "I73:7431;73:6826": { + "id": "I73:7431;73:6826", + "type": "INSTANCE", + "fields": { + "parentId": "73:7431", + "childrenIds": [ + "I73:7431;73:6826;73:6818" + ], + "absoluteBoundingBox": { + "height": 12, + "width": 12, + "x": 8833, + "y": 9141 + }, + "absoluteRenderBounds": { + "height": 12, + "width": 12, + "x": 8833, + "y": 9141 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": false + } + ], + "height": 12, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0.4997005462646485, + "paddingLeft": 0.5, + "paddingRight": 0.4995241165161133, + "paddingTop": 0.5, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "isAsset": true, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "megaphone", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "targetAspectRatio": { + "x": 512, + "y": 512 + }, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 12, + "x": 8, + "y": 8, + "mainComponentId": "73:6825", + "mainComponentName": "megaphone", + "mainComponentSetId": null + }, + "extra": {}, + "fieldErrors": {} + }, + "I73:7431;73:6826;73:6818": { + "id": "I73:7431;73:6826;73:6818", + "type": "GROUP", + "fields": { + "parentId": "I73:7431;73:6826", + "childrenIds": [ + "I73:7431;73:6826;73:6819", + "I73:7431;73:6826;73:6821", + "I73:7431;73:6826;73:6823" + ], + "absoluteBoundingBox": { + "height": 11.000299453735352, + "width": 11.000475883483888, + "x": 8833.5, + "y": 9141.5 + }, + "absoluteRenderBounds": { + "height": 11.000299453735352, + "width": 11.000475883483888, + "x": 8833.5, + "y": 9141.5 + }, + "height": 11.000299453735352, + "inferredAutoLayout": { + "counterAxisAlignItems": "MIN", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "FIXED" + }, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Group", + "opacity": 1, + "visible": true, + "width": 11.000475883483888, + "x": 0.5, + "y": 0.5 + }, + "extra": {}, + "fieldErrors": {} + }, + "I73:7431;73:6826;73:6819": { + "id": "I73:7431;73:6826;73:6819", + "type": "GROUP", + "fields": { + "parentId": "I73:7431;73:6826;73:6818", + "childrenIds": [ + "I73:7431;73:6826;73:6820" + ], + "absoluteBoundingBox": { + "height": 7.182965278625488, + "width": 8.283529281616211, + "x": 8833.5, + "y": 9142.1591796875 + }, + "absoluteRenderBounds": { + "height": 7.182965278625488, + "width": 8.283529281616211, + "x": 8833.5, + "y": 9142.1591796875 + }, + "height": 7.182965278625488, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Group", + "opacity": 1, + "visible": true, + "width": 8.283529281616211, + "x": 0.5, + "y": 1.1593017578125 + }, + "extra": {}, + "fieldErrors": {} + }, + "I73:7431;73:6826;73:6820": { + "id": "I73:7431;73:6826;73:6820", + "type": "VECTOR", + "fields": { + "parentId": "I73:7431;73:6826;73:6819", + "absoluteBoundingBox": { + "height": 7.182965278625488, + "width": 8.283529281616211, + "x": 8833.5, + "y": 9142.1591796875 + }, + "absoluteRenderBounds": { + "height": 7.1826171875, + "width": 8.283203125, + "x": 8833.5, + "y": 9142.1591796875 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 7.182965278625488, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeWeight": 15.058823585510254, + "visible": true, + "width": 8.283529281616211, + "x": 0.5, + "y": 1.1593017578125 + }, + "extra": {}, + "fieldErrors": {} + }, + "I73:7431;73:6826;73:6821": { + "id": "I73:7431;73:6826;73:6821", + "type": "GROUP", + "fields": { + "parentId": "I73:7431;73:6826;73:6818", + "childrenIds": [ + "I73:7431;73:6826;73:6822" + ], + "absoluteBoundingBox": { + "height": 4.511041164398193, + "width": 3.78228759765625, + "x": 8834.4052734375, + "y": 9147.9892578125 + }, + "absoluteRenderBounds": { + "height": 4.511041164398193, + "width": 3.78228759765625, + "x": 8834.4052734375, + "y": 9147.9892578125 + }, + "height": 4.511041164398193, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Group", + "opacity": 1, + "visible": true, + "width": 3.78228759765625, + "x": 1.4054877758026123, + "y": 6.9888916015625 + }, + "extra": {}, + "fieldErrors": {} + }, + "I73:7431;73:6826;73:6822": { + "id": "I73:7431;73:6826;73:6822", + "type": "VECTOR", + "fields": { + "parentId": "I73:7431;73:6826;73:6821", + "absoluteBoundingBox": { + "height": 4.511041164398193, + "width": 3.78228759765625, + "x": 8834.4052734375, + "y": 9147.9892578125 + }, + "absoluteRenderBounds": { + "height": 4.5107421875, + "width": 3.7822265625, + "x": 8834.4052734375, + "y": 9147.9892578125 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 4.511041164398193, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeWeight": 15.058823585510254, + "visible": true, + "width": 3.78228759765625, + "x": 1.4054877758026123, + "y": 6.9888916015625 + }, + "extra": {}, + "fieldErrors": {} + }, + "I73:7431;73:6826;73:6823": { + "id": "I73:7431;73:6826;73:6823", + "type": "GROUP", + "fields": { + "parentId": "I73:7431;73:6826;73:6818", + "childrenIds": [ + "I73:7431;73:6826;73:6824" + ], + "absoluteBoundingBox": { + "height": 8.501578330993652, + "width": 2.0121943950653076, + "x": 8842.48828125, + "y": 9141.5 + }, + "absoluteRenderBounds": { + "height": 8.501953125, + "width": 2.0121943950653076, + "x": 8842.48828125, + "y": 9141.5 + }, + "height": 8.501578330993652, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Group", + "opacity": 1, + "visible": true, + "width": 2.0121943950653076, + "x": 9.487805366516112, + "y": 0.5 + }, + "extra": {}, + "fieldErrors": {} + }, + "I73:7431;73:6826;73:6824": { + "id": "I73:7431;73:6826;73:6824", + "type": "VECTOR", + "fields": { + "parentId": "I73:7431;73:6826;73:6823", + "absoluteBoundingBox": { + "height": 8.501578330993652, + "width": 2.0121943950653076, + "x": 8842.48828125, + "y": 9141.5 + }, + "absoluteRenderBounds": { + "height": 8.501953125, + "width": 2.01171875, + "x": 8842.48828125, + "y": 9141.5 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 8.501578330993652, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeWeight": 15.058823585510254, + "visible": true, + "width": 2.0121943950653076, + "x": 9.487805366516112, + "y": 0.5 + }, + "extra": {}, + "fieldErrors": {} + } + }, + "diagnostics": [] +} diff --git a/crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_cta_banner_set.json b/crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_cta_banner_set.json new file mode 100644 index 00000000..26036899 --- /dev/null +++ b/crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_cta_banner_set.json @@ -0,0 +1,1991 @@ +{ + "fileKey": "K2kUnxGgvooAJwCQiVTb7g", + "version": null, + "roots": [ + "73:11106" + ], + "nodes": { + "73:11070": { + "id": "73:11070", + "type": "FRAME", + "fields": { + "parentId": "73:11078", + "childrenIds": [ + "73:11071", + "73:11072", + "73:11073" + ], + "absoluteBoundingBox": { + "height": 115, + "width": 1023, + "x": 8885, + "y": 9511 + }, + "absoluteRenderBounds": { + "height": 115, + "width": 1023, + "x": 8885, + "y": 9511 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "height": 115, + "inferredAutoLayout": { + "counterAxisAlignItems": "MIN", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 12, + "layoutAlign": "INHERIT", + "layoutGrow": 1, + "layoutMode": "VERTICAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 12, + "layoutGrow": 1, + "layoutMode": "VERTICAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "text", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 1023, + "x": 64, + "y": 56 + }, + "extra": {}, + "fieldErrors": {} + }, + "73:11071": { + "id": "73:11071", + "type": "TEXT", + "fields": { + "parentId": "73:11070", + "absoluteBoundingBox": { + "height": 20, + "width": 79, + "x": 8885, + "y": 9511 + }, + "absoluteRenderBounds": { + "height": 11.625, + "width": 76.8564453125, + "x": 8885.900390625, + "y": 9514.7802734375 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:11:27", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "CONTACT", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:11:27", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.8392156958580017, + "g": 0.8156862854957581, + "r": 0.40392157435417175 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Heavy" + }, + "fontSize": 15, + "height": 20, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": 4 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 132.00000524520874 + }, + "maxHeight": null, + "maxWidth": null, + "name": "CONTACT", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:18adf49c362ab9c0a968104533e0a31e663f3222,", + "textTruncation": "DISABLED", + "visible": true, + "width": 79, + "x": 0, + "y": 0, + "styledTextSegments": [ + { + "characters": "CONTACT", + "end": 7, + "fillStyleId": "", + "fontWeight": 900, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:18adf49c362ab9c0a968104533e0a31e663f3222," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "73:11072": { + "id": "73:11072", + "type": "TEXT", + "fields": { + "parentId": "73:11070", + "absoluteBoundingBox": { + "height": 42, + "width": 1023, + "x": 8885, + "y": 9543 + }, + "absoluteRenderBounds": { + "height": 28.896484375, + "width": 333.8935546875, + "x": 8886.0244140625, + "y": 9549.8876953125 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:356", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:942", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "콘드로스페로이드 관련 문의", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:356", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "ExtraBold" + }, + "fontSize": 32, + "height": 42, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -3 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 132.00000524520874 + }, + "maxHeight": null, + "maxWidth": null, + "name": "콘드로스페로이드 관련 문의", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "HEIGHT", + "textStyleId": "S:8f74b830ee8360510db15b6145b2c30e5a240ce2,", + "textTruncation": "DISABLED", + "visible": true, + "width": 1023, + "x": 0, + "y": 32, + "styledTextSegments": [ + { + "characters": "콘드로스페로이드 관련 문의", + "end": 14, + "fillStyleId": "", + "fontWeight": 800, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:8f74b830ee8360510db15b6145b2c30e5a240ce2," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "73:11073": { + "id": "73:11073", + "type": "TEXT", + "fields": { + "parentId": "73:11070", + "absoluteBoundingBox": { + "height": 29, + "width": 1023, + "x": 8885, + "y": 9597 + }, + "absoluteRenderBounds": { + "height": 15.9111328125, + "width": 540.9013671875, + "x": 8885.73828125, + "y": 9603.4384765625 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:356", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:951", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "공동연구 · 투자 · 임상 관련 문의를 남겨 주시면 담당자가 확인 후 연락드립니다.", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:356", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Regular" + }, + "fontSize": 18, + "height": 29, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 160.0000023841858 + }, + "maxHeight": null, + "maxWidth": null, + "name": "공동연구 · 투자 · 임상 관련 문의를 남겨 주시면", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "HEIGHT", + "textStyleId": "S:cfcbc71ecd9536404e6fe4dfeb971ce6261df4c3,", + "textTruncation": "DISABLED", + "visible": true, + "width": 1023, + "x": 0, + "y": 86, + "styledTextSegments": [ + { + "characters": "공동연구 · 투자 · 임상 관련 문의를 남겨 주시면 담당자가 확인 후 연락드립니다.", + "end": 46, + "fillStyleId": "", + "fontWeight": 400, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:cfcbc71ecd9536404e6fe4dfeb971ce6261df4c3," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "73:11074": { + "id": "73:11074", + "type": "INSTANCE", + "fields": { + "parentId": "73:11078", + "childrenIds": [ + "I73:11074;24:1866", + "I73:11074;24:1867" + ], + "absoluteBoundingBox": { + "height": 50, + "width": 129, + "x": 9908, + "y": 9576 + }, + "absoluteRenderBounds": { + "height": 50, + "width": 129, + "x": 9908, + "y": 9576 + }, + "bottomLeftRadius": 12, + "bottomRightRadius": 12, + "componentProperties": { + "effect": { + "boundVariables": {}, + "type": "VARIANT", + "value": "default" + }, + "rightIcon#23:2": { + "type": "BOOLEAN", + "value": true + }, + "size": { + "boundVariables": {}, + "type": "VARIANT", + "value": "md" + }, + "type": { + "boundVariables": {}, + "type": "VARIANT", + "value": "white-line" + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 12, + "counterAxisAlignItems": "CENTER", + "height": 50, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 8, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 12, + "paddingLeft": 24, + "paddingRight": 24, + "paddingTop": 12, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 8, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "button", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 12, + "paddingLeft": 24, + "paddingRight": 24, + "paddingTop": 12, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "topLeftRadius": 12, + "topRightRadius": 12, + "variantProperties": { + "effect": "default", + "size": "md", + "type": "white-line" + }, + "visible": true, + "width": 129, + "x": 1087, + "y": 121, + "mainComponentId": "24:1865", + "mainComponentName": "effect=default, type=white-line, size=md", + "mainComponentSetId": "23:1613" + }, + "extra": {}, + "fieldErrors": {} + }, + "73:11078": { + "id": "73:11078", + "type": "COMPONENT", + "fields": { + "parentId": "73:11106", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "73:11070", + "73:11074" + ], + "absoluteBoundingBox": { + "height": 227, + "width": 1280, + "x": 8821, + "y": 9455 + }, + "absoluteRenderBounds": { + "height": 227, + "width": 1280, + "x": 8821, + "y": 9455 + }, + "bottomLeftRadius": 24, + "bottomRightRadius": 24, + "clipsContent": true, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 24, + "counterAxisAlignItems": "MAX", + "fills": [ + { + "blendMode": "NORMAL", + "filters": { + "contrast": 0, + "exposure": 0, + "highlights": 0, + "saturation": 0, + "shadows": 0, + "temperature": 0, + "tint": 0 + }, + "imageHash": "3dff48ef546574de374768d70081b41b8d205ec4", + "imageTransform": [ + [ + 1, + 0, + 0 + ], + [ + 0, + 1, + 0 + ] + ], + "opacity": 1, + "rotation": 0, + "scaleMode": "FILL", + "scalingFactor": 1, + "type": "IMAGE", + "visible": true + }, + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 0.949999988079071, + "b": 0.21960784494876864, + "g": 0.10196078568696976, + "r": 0 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 0.5, + "b": 0.21960784494876864, + "g": 0.10196078568696976, + "r": 0 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 1, + 0, + 0 + ], + [ + 0, + 1, + 0 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 227, + "inferredAutoLayout": { + "counterAxisAlignItems": "MAX", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 24, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 56, + "paddingLeft": 64, + "paddingRight": 64, + "paddingTop": 56, + "primaryAxisAlignItems": "SPACE_BETWEEN", + "primaryAxisSizingMode": "FIXED" + }, + "isAsset": true, + "itemSpacing": 24, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": 1280, + "name": "viewport=Desktop", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 56, + "paddingLeft": 64, + "paddingRight": 64, + "paddingTop": 56, + "primaryAxisAlignItems": "SPACE_BETWEEN", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 24, + "topRightRadius": 24, + "variantProperties": { + "viewport": "Desktop" + }, + "visible": true, + "width": 1280, + "x": 20, + "y": 20 + }, + "extra": {}, + "fieldErrors": {} + }, + "73:11106": { + "id": "73:11106", + "type": "COMPONENT_SET", + "fields": { + "parentId": "19:963", + "parentType": "SECTION", + "parentName": "Components", + "childrenIds": [ + "73:11078", + "73:11107" + ], + "absoluteBoundingBox": { + "height": 600, + "width": 1320, + "x": 8801, + "y": 9435 + }, + "absoluteRenderBounds": { + "height": 600, + "width": 1320, + "x": 8801, + "y": 9435 + }, + "bottomLeftRadius": 5, + "bottomRightRadius": 5, + "clipsContent": true, + "componentPropertyDefinitions": { + "viewport": { + "defaultValue": "Desktop", + "type": "VARIANT", + "variantOptions": [ + "Desktop", + "Mobile" + ] + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 5, + "dashPattern": [ + 10, + 5 + ], + "defaultVariant": { + "$nodeId": "73:11078", + "$nodeType": "COMPONENT" + }, + "height": 600, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "CTABanner", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 0.27843138575553894, + "r": 0.5921568870544434 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "topLeftRadius": 5, + "topRightRadius": 5, + "visible": true, + "width": 1320, + "x": 131, + "y": 8397 + }, + "extra": {}, + "fieldErrors": {} + }, + "73:11107": { + "id": "73:11107", + "type": "COMPONENT", + "fields": { + "parentId": "73:11106", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "73:11108", + "73:11112" + ], + "absoluteBoundingBox": { + "height": 313, + "width": 360, + "x": 8821, + "y": 9702 + }, + "absoluteRenderBounds": { + "height": 313, + "width": 360, + "x": 8821, + "y": 9702 + }, + "bottomLeftRadius": 20, + "bottomRightRadius": 20, + "clipsContent": true, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 20, + "fills": [ + { + "blendMode": "NORMAL", + "filters": { + "contrast": 0, + "exposure": 0, + "highlights": 0, + "saturation": 0, + "shadows": 0, + "temperature": 0, + "tint": 0 + }, + "imageHash": "3dff48ef546574de374768d70081b41b8d205ec4", + "imageTransform": [ + [ + 1, + 0, + 0 + ], + [ + 0, + 1, + 0 + ] + ], + "opacity": 1, + "rotation": 0, + "scaleMode": "FILL", + "scalingFactor": 1, + "type": "IMAGE", + "visible": true + }, + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 0.949999988079071, + "b": 0.21960784494876864, + "g": 0.10196078568696976, + "r": 0 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 0.5, + "b": 0.21960784494876864, + "g": 0.10196078568696976, + "r": 0 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 1, + 0, + 0 + ], + [ + 0, + 1, + 0 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 313, + "inferredAutoLayout": { + "counterAxisAlignItems": "MIN", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 40, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutPositioning": "AUTO", + "paddingBottom": 48, + "paddingLeft": 32, + "paddingRight": 32, + "paddingTop": 48, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "AUTO" + }, + "isAsset": true, + "itemSpacing": 40, + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": 1280, + "name": "viewport=Mobile", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 48, + "paddingLeft": 32, + "paddingRight": 32, + "paddingTop": 48, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 20, + "topRightRadius": 20, + "variantProperties": { + "viewport": "Mobile" + }, + "visible": true, + "width": 360, + "x": 20, + "y": 267 + }, + "extra": {}, + "fieldErrors": {} + }, + "73:11108": { + "id": "73:11108", + "type": "FRAME", + "fields": { + "parentId": "73:11107", + "childrenIds": [ + "73:11109", + "73:11110", + "73:11111" + ], + "absoluteBoundingBox": { + "height": 128, + "width": 296, + "x": 8853, + "y": 9750 + }, + "absoluteRenderBounds": { + "height": 128, + "width": 296, + "x": 8853, + "y": 9750 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "height": 128, + "inferredAutoLayout": { + "counterAxisAlignItems": "MIN", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 12, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 12, + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "text", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 296, + "x": 32, + "y": 48 + }, + "extra": {}, + "fieldErrors": {} + }, + "73:11109": { + "id": "73:11109", + "type": "TEXT", + "fields": { + "parentId": "73:11108", + "absoluteBoundingBox": { + "height": 20, + "width": 79, + "x": 8853, + "y": 9750 + }, + "absoluteRenderBounds": { + "height": 11.625, + "width": 76.8564453125, + "x": 8853.900390625, + "y": 9753.7802734375 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:11:27", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "CONTACT", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:11:27", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.8392156958580017, + "g": 0.8156862854957581, + "r": 0.40392157435417175 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Heavy" + }, + "fontSize": 15, + "height": 20, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": 4 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 132.00000524520874 + }, + "maxHeight": null, + "maxWidth": null, + "name": "CONTACT", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:18adf49c362ab9c0a968104533e0a31e663f3222,", + "textTruncation": "DISABLED", + "visible": true, + "width": 79, + "x": 0, + "y": 0, + "styledTextSegments": [ + { + "characters": "CONTACT", + "end": 7, + "fillStyleId": "", + "fontWeight": 900, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:18adf49c362ab9c0a968104533e0a31e663f3222," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "73:11110": { + "id": "73:11110", + "type": "TEXT", + "fields": { + "parentId": "73:11108", + "absoluteBoundingBox": { + "height": 32, + "width": 296, + "x": 8853, + "y": 9782 + }, + "absoluteRenderBounds": { + "height": 21.671875, + "width": 250.4208984375, + "x": 8853.767578125, + "y": 9787.416015625 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:356", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:942", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "콘드로스페로이드 관련 문의", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:356", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "ExtraBold" + }, + "fontSize": 24, + "height": 32, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -3 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 132.00000524520874 + }, + "maxHeight": null, + "maxWidth": null, + "name": "콘드로스페로이드 관련 문의", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "HEIGHT", + "textStyleId": "S:8f74b830ee8360510db15b6145b2c30e5a240ce2,", + "textTruncation": "DISABLED", + "visible": true, + "width": 296, + "x": 0, + "y": 32, + "styledTextSegments": [ + { + "characters": "콘드로스페로이드 관련 문의", + "end": 14, + "fillStyleId": "", + "fontWeight": 800, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:8f74b830ee8360510db15b6145b2c30e5a240ce2," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "73:11111": { + "id": "73:11111", + "type": "TEXT", + "fields": { + "parentId": "73:11108", + "absoluteBoundingBox": { + "height": 52, + "width": 296, + "x": 8853, + "y": 9826 + }, + "absoluteRenderBounds": { + "height": 40.03125, + "width": 285.90234375, + "x": 8853.65625, + "y": 9832.15234375 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:356", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:951", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "공동연구 · 투자 · 임상 관련 문의를 남겨 주시면 담당자가 확인 후 연락드립니다.", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:356", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Regular" + }, + "fontSize": 16, + "height": 52, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 160.0000023841858 + }, + "maxHeight": null, + "maxWidth": null, + "name": "공동연구 · 투자 · 임상 관련 문의를 남겨 주시면", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "HEIGHT", + "textStyleId": "S:cfcbc71ecd9536404e6fe4dfeb971ce6261df4c3,", + "textTruncation": "DISABLED", + "visible": true, + "width": 296, + "x": 0, + "y": 76, + "styledTextSegments": [ + { + "characters": "공동연구 · 투자 · 임상 관련 문의를 남겨 주시면 담당자가 확인 후 연락드립니다.", + "end": 46, + "fillStyleId": "", + "fontWeight": 400, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:cfcbc71ecd9536404e6fe4dfeb971ce6261df4c3," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "73:11112": { + "id": "73:11112", + "type": "INSTANCE", + "fields": { + "parentId": "73:11107", + "childrenIds": [ + "I73:11112;24:1866", + "I73:11112;24:1867" + ], + "absoluteBoundingBox": { + "height": 49, + "width": 296, + "x": 8853, + "y": 9918 + }, + "absoluteRenderBounds": { + "height": 49, + "width": 296, + "x": 8853, + "y": 9918 + }, + "bottomLeftRadius": 12, + "bottomRightRadius": 12, + "componentProperties": { + "effect": { + "boundVariables": {}, + "type": "VARIANT", + "value": "default" + }, + "rightIcon#23:2": { + "type": "BOOLEAN", + "value": true + }, + "size": { + "boundVariables": {}, + "type": "VARIANT", + "value": "md" + }, + "type": { + "boundVariables": {}, + "type": "VARIANT", + "value": "white-line" + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 12, + "counterAxisAlignItems": "CENTER", + "height": 49, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 8, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 12, + "paddingLeft": 24, + "paddingRight": 24, + "paddingTop": 12, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 8, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "button", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 12, + "paddingLeft": 24, + "paddingRight": 24, + "paddingTop": 12, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "topLeftRadius": 12, + "topRightRadius": 12, + "variantProperties": { + "effect": "default", + "size": "md", + "type": "white-line" + }, + "visible": true, + "width": 296, + "x": 32, + "y": 216, + "mainComponentId": "24:1865", + "mainComponentName": "effect=default, type=white-line, size=md", + "mainComponentSetId": "23:1613" + }, + "extra": {}, + "fieldErrors": {} + }, + "I73:11074;24:1866": { + "id": "I73:11074;24:1866", + "type": "TEXT", + "fields": { + "parentId": "73:11074", + "absoluteBoundingBox": { + "height": 24, + "width": 55, + "x": 9933, + "y": 9589 + }, + "absoluteRenderBounds": { + "height": 14.287109375, + "width": 52.78515625, + "x": 9933.5283203125, + "y": 9594.0244140625 + }, + "boundVariables": { + "fontSize": [ + { + "id": "VariableID:18:955", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "문의하기", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Bold" + }, + "fontSize": 16, + "height": 24, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "버튼명", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:cbc02b7831f02c3b00e92d3114823bdda982464e,", + "textTruncation": "DISABLED", + "visible": true, + "width": 55, + "x": 25, + "y": 13, + "styledTextSegments": [ + { + "characters": "문의하기", + "end": 4, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:cbc02b7831f02c3b00e92d3114823bdda982464e," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "I73:11074;24:1867": { + "id": "I73:11074;24:1867", + "type": "INSTANCE", + "fields": { + "parentId": "73:11074", + "childrenIds": [ + "I73:11074;24:1867;40:451" + ], + "absoluteBoundingBox": { + "height": 16, + "width": 16, + "x": 9996, + "y": 9593 + }, + "absoluteRenderBounds": { + "height": 16, + "width": 16, + "x": 9996, + "y": 9593 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "componentProperties": { + "속성 1": { + "boundVariables": {}, + "type": "VARIANT", + "value": "CaretRight" + } + }, + "componentPropertyReferences": { + "visible": "rightIcon#23:2" + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": false + } + ], + "height": 16, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 2.499721050262451, + "paddingLeft": 5.499722003936768, + "paddingRight": 4.499607086181641, + "paddingTop": 2.4997220039367676, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "isAsset": true, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Caret", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "targetAspectRatio": { + "x": 24, + "y": 24 + }, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "속성 1": "CaretRight" + }, + "visible": true, + "width": 16, + "x": 88, + "y": 17, + "mainComponentId": "40:455", + "mainComponentName": "속성 1=CaretRight", + "mainComponentSetId": "40:459" + }, + "extra": {}, + "fieldErrors": {} + }, + "I73:11074;24:1867;40:451": { + "id": "I73:11074;24:1867;40:451", + "type": "VECTOR", + "fields": { + "parentId": "I73:11074;24:1867", + "absoluteBoundingBox": { + "height": 11.00055694580078, + "width": 6.000670909881592, + "x": 10001.5, + "y": 9595.5 + }, + "absoluteRenderBounds": { + "height": 11.0009765625, + "width": 6.0009765625, + "x": 10001.5, + "y": 9595.5 + }, + "boundVariables": { + "strokes": [ + { + "id": "VariableID:9:359", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0, + "g": 0, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 11.00055694580078, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeWeight": 1.600000023841858, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:359", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.6682692170143127, + "g": 0.6104382276535034, + "r": 0.5815227627754211 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "visible": true, + "width": 6.000670909881592, + "x": 5.499722003936768, + "y": 2.4997220039367676 + }, + "extra": {}, + "fieldErrors": {} + }, + "I73:11112;24:1866": { + "id": "I73:11112;24:1866", + "type": "TEXT", + "fields": { + "parentId": "73:11112", + "absoluteBoundingBox": { + "height": 23, + "width": 52, + "x": 8963, + "y": 9931 + }, + "absoluteRenderBounds": { + "height": 13.3955078125, + "width": 49.486328125, + "x": 8963.4951171875, + "y": 9935.8349609375 + }, + "boundVariables": { + "fontSize": [ + { + "id": "VariableID:18:955", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "문의하기", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Bold" + }, + "fontSize": 15, + "height": 23, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "버튼명", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:cbc02b7831f02c3b00e92d3114823bdda982464e,", + "textTruncation": "DISABLED", + "visible": true, + "width": 52, + "x": 110, + "y": 13, + "styledTextSegments": [ + { + "characters": "문의하기", + "end": 4, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:cbc02b7831f02c3b00e92d3114823bdda982464e," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "I73:11112;24:1867": { + "id": "I73:11112;24:1867", + "type": "INSTANCE", + "fields": { + "parentId": "73:11112", + "childrenIds": [ + "I73:11112;24:1867;40:451" + ], + "absoluteBoundingBox": { + "height": 16, + "width": 16, + "x": 9023, + "y": 9934.5 + }, + "absoluteRenderBounds": { + "height": 16, + "width": 16, + "x": 9023, + "y": 9934.5 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "componentProperties": { + "속성 1": { + "boundVariables": {}, + "type": "VARIANT", + "value": "CaretRight" + } + }, + "componentPropertyReferences": { + "visible": "rightIcon#23:2" + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": false + } + ], + "height": 16, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 2.499721050262451, + "paddingLeft": 5.499722003936768, + "paddingRight": 4.499607086181641, + "paddingTop": 2.4997220039367676, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "isAsset": true, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Caret", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "targetAspectRatio": { + "x": 24, + "y": 24 + }, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "속성 1": "CaretRight" + }, + "visible": true, + "width": 16, + "x": 170, + "y": 16.5, + "mainComponentId": "40:455", + "mainComponentName": "속성 1=CaretRight", + "mainComponentSetId": "40:459" + }, + "extra": {}, + "fieldErrors": {} + }, + "I73:11112;24:1867;40:451": { + "id": "I73:11112;24:1867;40:451", + "type": "VECTOR", + "fields": { + "parentId": "I73:11112;24:1867", + "absoluteBoundingBox": { + "height": 11.00055694580078, + "width": 6.000670909881592, + "x": 9028.5, + "y": 9937 + }, + "absoluteRenderBounds": { + "height": 11.0009765625, + "width": 6.0009765625, + "x": 9028.5, + "y": 9937 + }, + "boundVariables": { + "strokes": [ + { + "id": "VariableID:9:359", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0, + "g": 0, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 11.00055694580078, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeWeight": 1.600000023841858, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:359", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.6682692170143127, + "g": 0.6104382276535034, + "r": 0.5815227627754211 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "visible": true, + "width": 6.000670909881592, + "x": 5.499722003936768, + "y": 2.4997220039367676 + }, + "extra": {}, + "fieldErrors": {} + } + }, + "diagnostics": [] +} diff --git a/crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_icon_button_set.json b/crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_icon_button_set.json new file mode 100644 index 00000000..d7b8463d --- /dev/null +++ b/crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_icon_button_set.json @@ -0,0 +1,1386 @@ +{ + "fileKey": "K2kUnxGgvooAJwCQiVTb7g", + "version": null, + "roots": [ + "20:1349" + ], + "nodes": { + "20:1321": { + "id": "20:1321", + "type": "TEXT", + "fields": { + "parentId": "20:1348", + "absoluteBoundingBox": { + "height": 17, + "width": 37, + "x": 9024, + "y": 1123 + }, + "absoluteRenderBounds": null, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:44:5420", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "KOR", + "componentPropertyReferences": { + "visible": "text#20:1" + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.1529411822557449, + "g": 0.08627451211214066, + "r": 0.06666667014360428 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Bold" + }, + "fontSize": 17, + "height": 17, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 100 + }, + "maxHeight": null, + "maxWidth": null, + "name": "KOR", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:ab9a4b96cc300838fb4b14185fdd981a07ac2475,", + "textTruncation": "DISABLED", + "visible": false, + "width": 37, + "x": 44, + "y": 12, + "styledTextSegments": [ + { + "characters": "KOR", + "end": 3, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:ab9a4b96cc300838fb4b14185fdd981a07ac2475," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "20:1333": { + "id": "20:1333", + "type": "INSTANCE", + "fields": { + "parentId": "20:1348", + "childrenIds": [ + "I20:1333;20:1304", + "I20:1333;20:1305" + ], + "absoluteBoundingBox": { + "height": 24, + "width": 24, + "x": 8992, + "y": 1123 + }, + "absoluteRenderBounds": { + "height": 24, + "width": 24, + "x": 8992, + "y": 1123 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "componentProperties": { + "속성 1": { + "boundVariables": {}, + "type": "VARIANT", + "value": "search" + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": false + } + ], + "height": 24, + "isAsset": true, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "icon", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 0.125, + "strokeLeftWeight": 0.125, + "strokeRightWeight": 0.125, + "strokeTopWeight": 0.125, + "strokeWeight": 0.125, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "속성 1": "search" + }, + "visible": true, + "width": 24, + "x": 12, + "y": 12, + "mainComponentId": "20:1310", + "mainComponentName": "속성 1=search", + "mainComponentSetId": "20:1312" + }, + "extra": {}, + "fieldErrors": {} + }, + "20:1348": { + "id": "20:1348", + "type": "COMPONENT", + "fields": { + "parentId": "20:1349", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "20:1333", + "20:1321" + ], + "absoluteBoundingBox": { + "height": 48, + "width": 48, + "x": 8980, + "y": 1111 + }, + "absoluteRenderBounds": { + "height": 48, + "width": 48, + "x": 8980, + "y": 1111 + }, + "bottomLeftRadius": 100, + "bottomRightRadius": 100, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 100, + "counterAxisAlignItems": "CENTER", + "height": 48, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 8, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 12, + "paddingLeft": 12, + "paddingRight": 12, + "paddingTop": 12, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 8, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "effect=default", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 12, + "paddingLeft": 12, + "paddingRight": 12, + "paddingTop": 12, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 100, + "topRightRadius": 100, + "variantProperties": { + "effect": "default" + }, + "visible": true, + "width": 48, + "x": 20, + "y": 20 + }, + "extra": {}, + "fieldErrors": {} + }, + "20:1349": { + "id": "20:1349", + "type": "COMPONENT_SET", + "fields": { + "parentId": "19:963", + "parentType": "SECTION", + "parentName": "Components", + "childrenIds": [ + "20:1348", + "20:1350", + "20:1355" + ], + "absoluteBoundingBox": { + "height": 91, + "width": 196, + "x": 8960, + "y": 1091 + }, + "absoluteRenderBounds": { + "height": 91, + "width": 196, + "x": 8960, + "y": 1091 + }, + "bottomLeftRadius": 5, + "bottomRightRadius": 5, + "clipsContent": true, + "componentPropertyDefinitions": { + "effect": { + "defaultValue": "default", + "type": "VARIANT", + "variantOptions": [ + "default", + "hoevr", + "active" + ] + }, + "text#20:1": { + "defaultValue": false, + "type": "BOOLEAN" + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 5, + "dashPattern": [ + 10, + 5 + ], + "defaultVariant": { + "$nodeId": "20:1348", + "$nodeType": "COMPONENT" + }, + "height": 91, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "iconButton", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 0.27843138575553894, + "r": 0.5921568870544434 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "topLeftRadius": 5, + "topRightRadius": 5, + "visible": true, + "width": 196, + "x": 290, + "y": 53 + }, + "extra": {}, + "fieldErrors": {} + }, + "20:1350": { + "id": "20:1350", + "type": "COMPONENT", + "fields": { + "parentId": "20:1349", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "20:1351", + "20:1352" + ], + "absoluteBoundingBox": { + "height": 48, + "width": 48, + "x": 9034, + "y": 1111 + }, + "absoluteRenderBounds": { + "height": 48, + "width": 48, + "x": 9034, + "y": 1111 + }, + "bottomLeftRadius": 100, + "bottomRightRadius": 100, + "boundVariables": { + "fills": [ + { + "id": "VariableID:24:1672", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 100, + "counterAxisAlignItems": "CENTER", + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:24:1672", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.3176470696926117, + "g": 0.250980406999588, + "r": 0.21568627655506137 + }, + "opacity": 0.07999999821186066, + "type": "SOLID", + "visible": true + } + ], + "height": 48, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 8, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 12, + "paddingLeft": 12, + "paddingRight": 12, + "paddingTop": 12, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 8, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "effect=hoevr", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 12, + "paddingLeft": 12, + "paddingRight": 12, + "paddingTop": 12, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 100, + "topRightRadius": 100, + "variantProperties": { + "effect": "hoevr" + }, + "visible": true, + "width": 48, + "x": 74, + "y": 20 + }, + "extra": {}, + "fieldErrors": {} + }, + "20:1351": { + "id": "20:1351", + "type": "INSTANCE", + "fields": { + "parentId": "20:1350", + "childrenIds": [ + "I20:1351;20:1304", + "I20:1351;20:1305" + ], + "absoluteBoundingBox": { + "height": 24, + "width": 24, + "x": 9046, + "y": 1123 + }, + "absoluteRenderBounds": { + "height": 24, + "width": 24, + "x": 9046, + "y": 1123 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "componentProperties": { + "속성 1": { + "boundVariables": {}, + "type": "VARIANT", + "value": "search" + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": false + } + ], + "height": 24, + "isAsset": true, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "icon", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 0.125, + "strokeLeftWeight": 0.125, + "strokeRightWeight": 0.125, + "strokeTopWeight": 0.125, + "strokeWeight": 0.125, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "속성 1": "search" + }, + "visible": true, + "width": 24, + "x": 12, + "y": 12, + "mainComponentId": "20:1310", + "mainComponentName": "속성 1=search", + "mainComponentSetId": "20:1312" + }, + "extra": {}, + "fieldErrors": {} + }, + "20:1352": { + "id": "20:1352", + "type": "TEXT", + "fields": { + "parentId": "20:1350", + "absoluteBoundingBox": { + "height": 17, + "width": 37, + "x": 9078, + "y": 1123 + }, + "absoluteRenderBounds": null, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:44:5420", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "KOR", + "componentPropertyReferences": { + "visible": "text#20:1" + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.1529411822557449, + "g": 0.08627451211214066, + "r": 0.06666667014360428 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Bold" + }, + "fontSize": 17, + "height": 17, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 100 + }, + "maxHeight": null, + "maxWidth": null, + "name": "KOR", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:ab9a4b96cc300838fb4b14185fdd981a07ac2475,", + "textTruncation": "DISABLED", + "visible": false, + "width": 37, + "x": 44, + "y": 12, + "styledTextSegments": [ + { + "characters": "KOR", + "end": 3, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:ab9a4b96cc300838fb4b14185fdd981a07ac2475," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "20:1355": { + "id": "20:1355", + "type": "COMPONENT", + "fields": { + "parentId": "20:1349", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "20:1356", + "20:1357" + ], + "absoluteBoundingBox": { + "height": 48, + "width": 48, + "x": 9088, + "y": 1111 + }, + "absoluteRenderBounds": { + "height": 48, + "width": 48, + "x": 9088, + "y": 1111 + }, + "bottomLeftRadius": 100, + "bottomRightRadius": 100, + "boundVariables": { + "fills": [ + { + "id": "VariableID:24:1680", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 100, + "counterAxisAlignItems": "CENTER", + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:24:1680", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.3176470696926117, + "g": 0.250980406999588, + "r": 0.21568627655506137 + }, + "opacity": 0.1599999964237213, + "type": "SOLID", + "visible": true + } + ], + "height": 48, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 8, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 12, + "paddingLeft": 12, + "paddingRight": 12, + "paddingTop": 12, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 8, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "effect=active", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 12, + "paddingLeft": 12, + "paddingRight": 12, + "paddingTop": 12, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 100, + "topRightRadius": 100, + "variantProperties": { + "effect": "active" + }, + "visible": true, + "width": 48, + "x": 128, + "y": 20 + }, + "extra": {}, + "fieldErrors": {} + }, + "20:1356": { + "id": "20:1356", + "type": "INSTANCE", + "fields": { + "parentId": "20:1355", + "childrenIds": [ + "I20:1356;20:1304", + "I20:1356;20:1305" + ], + "absoluteBoundingBox": { + "height": 24, + "width": 24, + "x": 9100, + "y": 1123 + }, + "absoluteRenderBounds": { + "height": 24, + "width": 24, + "x": 9100, + "y": 1123 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "componentProperties": { + "속성 1": { + "boundVariables": {}, + "type": "VARIANT", + "value": "search" + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": false + } + ], + "height": 24, + "isAsset": true, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "icon", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 0.125, + "strokeLeftWeight": 0.125, + "strokeRightWeight": 0.125, + "strokeTopWeight": 0.125, + "strokeWeight": 0.125, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "속성 1": "search" + }, + "visible": true, + "width": 24, + "x": 12, + "y": 12, + "mainComponentId": "20:1310", + "mainComponentName": "속성 1=search", + "mainComponentSetId": "20:1312" + }, + "extra": {}, + "fieldErrors": {} + }, + "20:1357": { + "id": "20:1357", + "type": "TEXT", + "fields": { + "parentId": "20:1355", + "absoluteBoundingBox": { + "height": 17, + "width": 37, + "x": 9132, + "y": 1123 + }, + "absoluteRenderBounds": null, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:345", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:44:5420", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "KOR", + "componentPropertyReferences": { + "visible": "text#20:1" + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:345", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.7323716878890991, + "g": 0.34177345037460327, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Bold" + }, + "fontSize": 17, + "height": 17, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 100 + }, + "maxHeight": null, + "maxWidth": null, + "name": "KOR", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:ab9a4b96cc300838fb4b14185fdd981a07ac2475,", + "textTruncation": "DISABLED", + "visible": false, + "width": 37, + "x": 44, + "y": 12, + "styledTextSegments": [ + { + "characters": "KOR", + "end": 3, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:ab9a4b96cc300838fb4b14185fdd981a07ac2475," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "I20:1333;20:1304": { + "id": "I20:1333;20:1304", + "type": "VECTOR", + "fields": { + "parentId": "20:1333", + "absoluteBoundingBox": { + "height": 18, + "width": 18, + "x": 8994, + "y": 1125 + }, + "absoluteRenderBounds": { + "height": 18, + "width": 18, + "x": 8994, + "y": 1125 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.1529411822557449, + "g": 0.08627451211214066, + "r": 0.06666667014360428 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 18, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector (Stroke)", + "opacity": 1, + "strokeAlign": "CENTER", + "strokeWeight": 2, + "visible": true, + "width": 18, + "x": 2, + "y": 2 + }, + "extra": {}, + "fieldErrors": {} + }, + "I20:1333;20:1305": { + "id": "I20:1333;20:1305", + "type": "VECTOR", + "fields": { + "parentId": "20:1333", + "absoluteBoundingBox": { + "height": 5.99984884262085, + "width": 5.99984884262085, + "x": 9008, + "y": 1139.0001220703125 + }, + "absoluteRenderBounds": { + "height": 5.9998779296875, + "width": 6, + "x": 9008, + "y": 1139.0001220703125 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.1529411822557449, + "g": 0.08627451211214066, + "r": 0.06666667014360428 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 5.99984884262085, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector (Stroke)", + "opacity": 1, + "strokeAlign": "CENTER", + "strokeWeight": 2, + "visible": true, + "width": 5.99984884262085, + "x": 16.000076293945312, + "y": 16.000076293945312 + }, + "extra": {}, + "fieldErrors": {} + }, + "I20:1351;20:1304": { + "id": "I20:1351;20:1304", + "type": "VECTOR", + "fields": { + "parentId": "20:1351", + "absoluteBoundingBox": { + "height": 18, + "width": 18, + "x": 9048, + "y": 1125 + }, + "absoluteRenderBounds": { + "height": 18, + "width": 18, + "x": 9048, + "y": 1125 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.1529411822557449, + "g": 0.08627451211214066, + "r": 0.06666667014360428 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 18, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector (Stroke)", + "opacity": 1, + "strokeAlign": "CENTER", + "strokeWeight": 2, + "visible": true, + "width": 18, + "x": 2, + "y": 2 + }, + "extra": {}, + "fieldErrors": {} + }, + "I20:1351;20:1305": { + "id": "I20:1351;20:1305", + "type": "VECTOR", + "fields": { + "parentId": "20:1351", + "absoluteBoundingBox": { + "height": 5.99984884262085, + "width": 5.99984884262085, + "x": 9062, + "y": 1139.0001220703125 + }, + "absoluteRenderBounds": { + "height": 5.9998779296875, + "width": 6, + "x": 9062, + "y": 1139.0001220703125 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.1529411822557449, + "g": 0.08627451211214066, + "r": 0.06666667014360428 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 5.99984884262085, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector (Stroke)", + "opacity": 1, + "strokeAlign": "CENTER", + "strokeWeight": 2, + "visible": true, + "width": 5.99984884262085, + "x": 16.000076293945312, + "y": 16.000076293945312 + }, + "extra": {}, + "fieldErrors": {} + }, + "I20:1356;20:1304": { + "id": "I20:1356;20:1304", + "type": "VECTOR", + "fields": { + "parentId": "20:1356", + "absoluteBoundingBox": { + "height": 18, + "width": 18, + "x": 9102, + "y": 1125 + }, + "absoluteRenderBounds": { + "height": 18, + "width": 18, + "x": 9102, + "y": 1125 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.1529411822557449, + "g": 0.08627451211214066, + "r": 0.06666667014360428 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 18, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector (Stroke)", + "opacity": 1, + "strokeAlign": "CENTER", + "strokeWeight": 2, + "visible": true, + "width": 18, + "x": 2, + "y": 2 + }, + "extra": {}, + "fieldErrors": {} + }, + "I20:1356;20:1305": { + "id": "I20:1356;20:1305", + "type": "VECTOR", + "fields": { + "parentId": "20:1356", + "absoluteBoundingBox": { + "height": 5.99984884262085, + "width": 5.99984884262085, + "x": 9116, + "y": 1139.0001220703125 + }, + "absoluteRenderBounds": { + "height": 5.9998779296875, + "width": 6, + "x": 9116, + "y": 1139.0001220703125 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.1529411822557449, + "g": 0.08627451211214066, + "r": 0.06666667014360428 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 5.99984884262085, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector (Stroke)", + "opacity": 1, + "strokeAlign": "CENTER", + "strokeWeight": 2, + "visible": true, + "width": 5.99984884262085, + "x": 16.000076293945312, + "y": 16.000076293945312 + }, + "extra": {}, + "fieldErrors": {} + } + }, + "diagnostics": [] +} diff --git a/crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_list_type_button_set.json b/crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_list_type_button_set.json new file mode 100644 index 00000000..193daa9b --- /dev/null +++ b/crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_list_type_button_set.json @@ -0,0 +1,1178 @@ +{ + "fileKey": "K2kUnxGgvooAJwCQiVTb7g", + "version": null, + "roots": [ + "73:6628" + ], + "nodes": { + "73:6628": { + "id": "73:6628", + "type": "COMPONENT_SET", + "fields": { + "parentId": "19:963", + "parentType": "SECTION", + "parentName": "Components", + "childrenIds": [ + "73:6629", + "73:6657", + "73:6633" + ], + "absoluteBoundingBox": { + "height": 80, + "width": 346, + "x": 8801, + "y": 8409 + }, + "absoluteRenderBounds": { + "height": 80, + "width": 346, + "x": 8801, + "y": 8409 + }, + "bottomLeftRadius": 5, + "bottomRightRadius": 5, + "clipsContent": true, + "componentPropertyDefinitions": { + "effect": { + "defaultValue": "default", + "type": "VARIANT", + "variantOptions": [ + "default", + "selected", + "hover" + ] + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 5, + "dashPattern": [ + 10, + 5 + ], + "defaultVariant": { + "$nodeId": "73:6629", + "$nodeType": "COMPONENT" + }, + "height": 80, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "ListTypeButton", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 0.27843138575553894, + "r": 0.5921568870544434 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "topLeftRadius": 5, + "topRightRadius": 5, + "visible": true, + "width": 346, + "x": 131, + "y": 7371 + }, + "extra": {}, + "fieldErrors": {} + }, + "73:6629": { + "id": "73:6629", + "type": "COMPONENT", + "fields": { + "parentId": "73:6628", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "73:6630", + "73:6631" + ], + "absoluteBoundingBox": { + "height": 40, + "width": 96, + "x": 8821, + "y": 8429 + }, + "absoluteRenderBounds": { + "height": 40, + "width": 96, + "x": 8821, + "y": 8429 + }, + "bottomLeftRadius": 8, + "bottomRightRadius": 8, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 8, + "counterAxisAlignItems": "CENTER", + "height": 40, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 6, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 8, + "paddingLeft": 12, + "paddingRight": 16, + "paddingTop": 8, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 6, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "effect=default", + "opacity": 0.699999988079071, + "overflowDirection": "NONE", + "paddingBottom": 8, + "paddingLeft": 12, + "paddingRight": 16, + "paddingTop": 8, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 8, + "topRightRadius": 8, + "variantProperties": { + "effect": "default" + }, + "visible": true, + "width": 96, + "x": 20, + "y": 20 + }, + "extra": {}, + "fieldErrors": {} + }, + "73:6630": { + "id": "73:6630", + "type": "INSTANCE", + "fields": { + "parentId": "73:6629", + "childrenIds": [ + "I73:6630;73:6543" + ], + "absoluteBoundingBox": { + "height": 20, + "width": 20, + "x": 8833, + "y": 8439 + }, + "absoluteRenderBounds": { + "height": 20, + "width": 20, + "x": 8833, + "y": 8439 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": false + } + ], + "height": 20, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 4.375, + "paddingLeft": 2.5, + "paddingRight": 2.5, + "paddingTop": 4.375, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "isAsset": true, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "ListDashes", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "targetAspectRatio": { + "x": 24, + "y": 24 + }, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 20, + "x": 12, + "y": 10, + "mainComponentId": "73:6546", + "mainComponentName": "ListDashes", + "mainComponentSetId": null + }, + "extra": {}, + "fieldErrors": {} + }, + "73:6631": { + "id": "73:6631", + "type": "TEXT", + "fields": { + "parentId": "73:6629", + "absoluteBoundingBox": { + "height": 24, + "width": 42, + "x": 8859, + "y": 8437 + }, + "absoluteRenderBounds": { + "height": 14.3681640625, + "width": 39.216796875, + "x": 8859.5439453125, + "y": 8441.927734375 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:348", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:955", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "목록형", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:348", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.3176470696926117, + "g": 0.250980406999588, + "r": 0.21568627655506137 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Bold" + }, + "fontSize": 16, + "height": 24, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "최신순", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:cbc02b7831f02c3b00e92d3114823bdda982464e,", + "textTruncation": "DISABLED", + "visible": true, + "width": 42, + "x": 38, + "y": 8, + "styledTextSegments": [ + { + "characters": "목록형", + "end": 3, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:cbc02b7831f02c3b00e92d3114823bdda982464e," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "73:6633": { + "id": "73:6633", + "type": "COMPONENT", + "fields": { + "parentId": "73:6628", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "73:6634", + "73:6635" + ], + "absoluteBoundingBox": { + "height": 40, + "width": 96, + "x": 8927, + "y": 8429 + }, + "absoluteRenderBounds": { + "height": 40, + "width": 96, + "x": 8927, + "y": 8429 + }, + "bottomLeftRadius": 8, + "bottomRightRadius": 8, + "boundVariables": { + "fills": [ + { + "id": "VariableID:11:2", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 8, + "counterAxisAlignItems": "CENTER", + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:11:2", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 1, + "g": 0.9715099334716796, + "r": 0.9487178325653076 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 40, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 6, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 8, + "paddingLeft": 12, + "paddingRight": 16, + "paddingTop": 8, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 6, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "effect=selected", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 8, + "paddingLeft": 12, + "paddingRight": 16, + "paddingTop": 8, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 8, + "topRightRadius": 8, + "variantProperties": { + "effect": "selected" + }, + "visible": true, + "width": 96, + "x": 126, + "y": 20 + }, + "extra": {}, + "fieldErrors": {} + }, + "73:6634": { + "id": "73:6634", + "type": "INSTANCE", + "fields": { + "parentId": "73:6633", + "childrenIds": [ + "I73:6634;73:6543" + ], + "absoluteBoundingBox": { + "height": 20, + "width": 20, + "x": 8939, + "y": 8439 + }, + "absoluteRenderBounds": { + "height": 20, + "width": 20, + "x": 8939, + "y": 8439 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": false + } + ], + "height": 20, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 4.375, + "paddingLeft": 2.5, + "paddingRight": 2.5, + "paddingTop": 4.375, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "isAsset": true, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "ListDashes", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "targetAspectRatio": { + "x": 24, + "y": 24 + }, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 20, + "x": 12, + "y": 10, + "mainComponentId": "73:6546", + "mainComponentName": "ListDashes", + "mainComponentSetId": null + }, + "extra": {}, + "fieldErrors": {} + }, + "73:6635": { + "id": "73:6635", + "type": "TEXT", + "fields": { + "parentId": "73:6633", + "absoluteBoundingBox": { + "height": 24, + "width": 42, + "x": 8965, + "y": 8437 + }, + "absoluteRenderBounds": { + "height": 14.3681640625, + "width": 39.216796875, + "x": 8965.5439453125, + "y": 8441.927734375 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:345", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:955", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "목록형", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:345", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.7323716878890991, + "g": 0.34177345037460327, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Bold" + }, + "fontSize": 16, + "height": 24, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "최신순", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:cbc02b7831f02c3b00e92d3114823bdda982464e,", + "textTruncation": "DISABLED", + "visible": true, + "width": 42, + "x": 38, + "y": 8, + "styledTextSegments": [ + { + "characters": "목록형", + "end": 3, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:cbc02b7831f02c3b00e92d3114823bdda982464e," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "73:6657": { + "id": "73:6657", + "type": "COMPONENT", + "fields": { + "parentId": "73:6628", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "73:6658", + "73:6659" + ], + "absoluteBoundingBox": { + "height": 40, + "width": 96, + "x": 9033, + "y": 8429 + }, + "absoluteRenderBounds": { + "height": 40, + "width": 96, + "x": 9033, + "y": 8429 + }, + "bottomLeftRadius": 8, + "bottomRightRadius": 8, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 8, + "counterAxisAlignItems": "CENTER", + "height": 40, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 6, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 8, + "paddingLeft": 12, + "paddingRight": 16, + "paddingTop": 8, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 6, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "effect=hover", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 8, + "paddingLeft": 12, + "paddingRight": 16, + "paddingTop": 8, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 8, + "topRightRadius": 8, + "variantProperties": { + "effect": "hover" + }, + "visible": true, + "width": 96, + "x": 232, + "y": 20 + }, + "extra": {}, + "fieldErrors": {} + }, + "73:6658": { + "id": "73:6658", + "type": "INSTANCE", + "fields": { + "parentId": "73:6657", + "childrenIds": [ + "I73:6658;73:6543" + ], + "absoluteBoundingBox": { + "height": 20, + "width": 20, + "x": 9045, + "y": 8439 + }, + "absoluteRenderBounds": { + "height": 20, + "width": 20, + "x": 9045, + "y": 8439 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": false + } + ], + "height": 20, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 4.375, + "paddingLeft": 2.5, + "paddingRight": 2.5, + "paddingTop": 4.375, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "isAsset": true, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "ListDashes", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "targetAspectRatio": { + "x": 24, + "y": 24 + }, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 20, + "x": 12, + "y": 10, + "mainComponentId": "73:6546", + "mainComponentName": "ListDashes", + "mainComponentSetId": null + }, + "extra": {}, + "fieldErrors": {} + }, + "73:6659": { + "id": "73:6659", + "type": "TEXT", + "fields": { + "parentId": "73:6657", + "absoluteBoundingBox": { + "height": 24, + "width": 42, + "x": 9071, + "y": 8437 + }, + "absoluteRenderBounds": { + "height": 14.3681640625, + "width": 39.216796875, + "x": 9071.5439453125, + "y": 8441.927734375 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:348", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:955", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "목록형", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:348", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.3176470696926117, + "g": 0.250980406999588, + "r": 0.21568627655506137 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Bold" + }, + "fontSize": 16, + "height": 24, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "최신순", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:cbc02b7831f02c3b00e92d3114823bdda982464e,", + "textTruncation": "DISABLED", + "visible": true, + "width": 42, + "x": 38, + "y": 8, + "styledTextSegments": [ + { + "characters": "목록형", + "end": 3, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:cbc02b7831f02c3b00e92d3114823bdda982464e," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "I73:6630;73:6543": { + "id": "I73:6630;73:6543", + "type": "VECTOR", + "fields": { + "parentId": "73:6630", + "absoluteBoundingBox": { + "height": 11.25, + "width": 15, + "x": 8835.5, + "y": 8443.375 + }, + "absoluteRenderBounds": { + "height": 11.25, + "width": 15, + "x": 8835.5, + "y": 8443.375 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:359", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:359", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.6682692170143127, + "g": 0.6104382276535034, + "r": 0.5815227627754211 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 11.25, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeWeight": 0.09375, + "visible": true, + "width": 15, + "x": 2.5, + "y": 4.375 + }, + "extra": {}, + "fieldErrors": {} + }, + "I73:6634;73:6543": { + "id": "I73:6634;73:6543", + "type": "VECTOR", + "fields": { + "parentId": "73:6634", + "absoluteBoundingBox": { + "height": 11.25, + "width": 15, + "x": 8941.5, + "y": 8443.375 + }, + "absoluteRenderBounds": { + "height": 11.25, + "width": 15, + "x": 8941.5, + "y": 8443.375 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:345", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:345", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.7323716878890991, + "g": 0.34177345037460327, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 11.25, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeWeight": 0.09375, + "visible": true, + "width": 15, + "x": 2.5, + "y": 4.375 + }, + "extra": {}, + "fieldErrors": {} + }, + "I73:6658;73:6543": { + "id": "I73:6658;73:6543", + "type": "VECTOR", + "fields": { + "parentId": "73:6658", + "absoluteBoundingBox": { + "height": 11.25, + "width": 15, + "x": 9047.5, + "y": 8443.375 + }, + "absoluteRenderBounds": { + "height": 11.25, + "width": 15, + "x": 9047.5, + "y": 8443.375 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:359", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:359", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.6682692170143127, + "g": 0.6104382276535034, + "r": 0.5815227627754211 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 11.25, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeWeight": 0.09375, + "visible": true, + "width": 15, + "x": 2.5, + "y": 4.375 + }, + "extra": {}, + "fieldErrors": {} + } + }, + "diagnostics": [] +} diff --git a/crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_page_title_set.json b/crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_page_title_set.json new file mode 100644 index 00000000..21ce158a --- /dev/null +++ b/crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_page_title_set.json @@ -0,0 +1,5557 @@ +{ + "fileKey": "K2kUnxGgvooAJwCQiVTb7g", + "version": null, + "roots": [ + "43:1158" + ], + "nodes": { + "40:544": { + "id": "40:544", + "type": "FRAME", + "fields": { + "parentId": "40:558", + "childrenIds": [ + "40:545", + "40:546" + ], + "absoluteBoundingBox": { + "height": 108, + "width": 1280, + "x": 10646, + "y": 6231 + }, + "absoluteRenderBounds": { + "height": 108, + "width": 1280, + "x": 10646, + "y": 6231 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "height": 108, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 24, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 24, + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": 1280, + "name": "Frame 479", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 1280, + "x": 100, + "y": 150 + }, + "extra": {}, + "fieldErrors": {} + }, + "40:545": { + "id": "40:545", + "type": "TEXT", + "fields": { + "parentId": "40:544", + "absoluteBoundingBox": { + "height": 63, + "width": 1280, + "x": 10646, + "y": 6231 + }, + "absoluteRenderBounds": { + "height": 43.34375, + "width": 119.234375, + "x": 11227.1982421875, + "y": 6240.83203125 + }, + "boundVariables": { + "fontSize": [ + { + "id": "VariableID:23:1559", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "타이틀", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "ExtraBold" + }, + "fontSize": 48, + "height": 63, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 132.00000524520874 + }, + "maxHeight": null, + "maxWidth": null, + "name": "타이틀", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAlignHorizontal": "CENTER", + "textAutoResize": "HEIGHT", + "textStyleId": "S:82867982bb8f7eff7b35bc2fef1bc1a06ee8a0c8,", + "textTruncation": "DISABLED", + "visible": true, + "width": 1280, + "x": 0, + "y": 0, + "styledTextSegments": [ + { + "characters": "타이틀", + "end": 3, + "fillStyleId": "", + "fontWeight": 800, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:82867982bb8f7eff7b35bc2fef1bc1a06ee8a0c8," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "40:546": { + "id": "40:546", + "type": "INSTANCE", + "fields": { + "parentId": "40:544", + "childrenIds": [ + "I40:546;40:503", + "I40:546;40:504", + "I40:546;40:505", + "I40:546;40:506", + "I40:546;40:507", + "I40:546;40:508", + "I40:546;40:509" + ], + "absoluteBoundingBox": { + "height": 21, + "width": 287, + "x": 11142.5, + "y": 6318 + }, + "absoluteRenderBounds": { + "height": 21, + "width": 287, + "x": 11142.5, + "y": 6318 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "componentProperties": { + "depth2#44:8": { + "type": "BOOLEAN", + "value": true + }, + "depth3#44:5": { + "type": "BOOLEAN", + "value": true + }, + "type": { + "boundVariables": {}, + "type": "VARIANT", + "value": "white" + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "height": 21, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 12, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 12, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "Breadcurmb", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "type": "white" + }, + "visible": true, + "width": 287, + "x": 496.5, + "y": 87, + "mainComponentId": "40:515", + "mainComponentName": "type=white", + "mainComponentSetId": "40:530" + }, + "extra": {}, + "fieldErrors": {} + }, + "40:558": { + "id": "40:558", + "type": "COMPONENT", + "fields": { + "parentId": "43:1158", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "40:544" + ], + "absoluteBoundingBox": { + "height": 360, + "width": 1480, + "x": 10546, + "y": 6081 + }, + "absoluteRenderBounds": { + "height": 360, + "width": 1480, + "x": 10546, + "y": 6081 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "boundVariables": { + "paddingLeft": { + "id": "VariableID:30:628", + "type": "VARIABLE_ALIAS" + }, + "paddingRight": { + "id": "VariableID:30:628", + "type": "VARIABLE_ALIAS" + }, + "paddingTop": { + "id": "VariableID:30:632", + "type": "VARIABLE_ALIAS" + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0, + "g": 0, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + }, + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0, + "g": 0, + "r": 0 + }, + "opacity": 0.6000000238418579, + "type": "SOLID", + "visible": true + } + ], + "height": 360, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 32, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutPositioning": "AUTO", + "paddingBottom": 32, + "paddingLeft": 40, + "paddingRight": 40, + "paddingTop": 80, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 32, + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "viewport=Desktop, type=Image", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 32, + "paddingLeft": 40, + "paddingRight": 40, + "paddingTop": 80, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "type": "Image", + "viewport": "Desktop" + }, + "visible": true, + "width": 1480, + "x": 21, + "y": 21 + }, + "extra": {}, + "fieldErrors": {} + }, + "43:1158": { + "id": "43:1158", + "type": "COMPONENT_SET", + "fields": { + "parentId": "19:963", + "parentType": "SECTION", + "parentName": "Components", + "childrenIds": [ + "40:558", + "43:1159", + "44:4929", + "44:4933" + ], + "absoluteBoundingBox": { + "height": 1260, + "width": 1522, + "x": 10525, + "y": 6060 + }, + "absoluteRenderBounds": { + "height": 1260, + "width": 1522, + "x": 10525, + "y": 6060 + }, + "bottomLeftRadius": 5, + "bottomRightRadius": 5, + "clipsContent": true, + "componentPropertyDefinitions": { + "Slot#43:2": { + "description": null, + "preferredValues": [], + "type": "SLOT" + }, + "type": { + "defaultValue": "Image", + "type": "VARIANT", + "variantOptions": [ + "Image", + "White" + ] + }, + "viewport": { + "defaultValue": "Desktop", + "type": "VARIANT", + "variantOptions": [ + "Desktop", + "Mobile" + ] + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 5, + "dashPattern": [ + 10, + 5 + ], + "defaultVariant": { + "$nodeId": "40:558", + "$nodeType": "COMPONENT" + }, + "height": 1260, + "inferredAutoLayout": { + "counterAxisAlignItems": "MIN", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 20, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutPositioning": "AUTO", + "paddingBottom": 20, + "paddingLeft": 20, + "paddingRight": 20, + "paddingTop": 20, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 20, + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "PageTitle", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 20, + "paddingLeft": 20, + "paddingRight": 20, + "paddingTop": 20, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 0.27843138575553894, + "r": 0.5921568870544434 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "topLeftRadius": 5, + "topRightRadius": 5, + "visible": true, + "width": 1522, + "x": 1855, + "y": 5022 + }, + "extra": {}, + "fieldErrors": {} + }, + "43:1159": { + "id": "43:1159", + "type": "COMPONENT", + "fields": { + "parentId": "43:1158", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "43:1160" + ], + "absoluteBoundingBox": { + "height": 280, + "width": 360, + "x": 10546, + "y": 6461 + }, + "absoluteRenderBounds": { + "height": 280, + "width": 360, + "x": 10546, + "y": 6461 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "boundVariables": { + "paddingLeft": { + "id": "VariableID:30:628", + "type": "VARIABLE_ALIAS" + }, + "paddingRight": { + "id": "VariableID:30:628", + "type": "VARIABLE_ALIAS" + }, + "paddingTop": { + "id": "VariableID:30:632", + "type": "VARIABLE_ALIAS" + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0, + "g": 0, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + }, + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0, + "g": 0, + "r": 0 + }, + "opacity": 0.6000000238418579, + "type": "SOLID", + "visible": true + } + ], + "height": 280, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 32, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutPositioning": "AUTO", + "paddingBottom": 16, + "paddingLeft": 20, + "paddingRight": 20, + "paddingTop": 40, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 32, + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "viewport=Mobile, type=Image", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 16, + "paddingLeft": 20, + "paddingRight": 20, + "paddingTop": 40, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "type": "Image", + "viewport": "Mobile" + }, + "visible": true, + "width": 360, + "x": 21, + "y": 401 + }, + "extra": {}, + "fieldErrors": {} + }, + "43:1160": { + "id": "43:1160", + "type": "FRAME", + "fields": { + "parentId": "43:1159", + "childrenIds": [ + "43:1161", + "43:1162" + ], + "absoluteBoundingBox": { + "height": 78, + "width": 320, + "x": 10566, + "y": 6574 + }, + "absoluteRenderBounds": { + "height": 78, + "width": 320, + "x": 10566, + "y": 6574 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "height": 78, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 16, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 16, + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": 1280, + "name": "Frame 479", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 320, + "x": 20, + "y": 113 + }, + "extra": {}, + "fieldErrors": {} + }, + "43:1161": { + "id": "43:1161", + "type": "TEXT", + "fields": { + "parentId": "43:1160", + "absoluteBoundingBox": { + "height": 42, + "width": 320, + "x": 10566, + "y": 6574 + }, + "absoluteRenderBounds": { + "height": 28.89599609375, + "width": 79.4892578125, + "x": 10686.798828125, + "y": 6580.88818359375 + }, + "boundVariables": { + "fontSize": [ + { + "id": "VariableID:23:1559", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "타이틀", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "ExtraBold" + }, + "fontSize": 32, + "height": 42, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 132.00000524520874 + }, + "maxHeight": null, + "maxWidth": null, + "name": "타이틀", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAlignHorizontal": "CENTER", + "textAutoResize": "HEIGHT", + "textStyleId": "S:82867982bb8f7eff7b35bc2fef1bc1a06ee8a0c8,", + "textTruncation": "DISABLED", + "visible": true, + "width": 320, + "x": 0, + "y": 0, + "styledTextSegments": [ + { + "characters": "타이틀", + "end": 3, + "fillStyleId": "", + "fontWeight": 800, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:82867982bb8f7eff7b35bc2fef1bc1a06ee8a0c8," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "43:1162": { + "id": "43:1162", + "type": "INSTANCE", + "fields": { + "parentId": "43:1160", + "childrenIds": [ + "I43:1162;40:503", + "I43:1162;40:504", + "I43:1162;40:505", + "I43:1162;40:506", + "I43:1162;40:507", + "I43:1162;40:508", + "I43:1162;40:509" + ], + "absoluteBoundingBox": { + "height": 20, + "width": 276, + "x": 10588, + "y": 6632 + }, + "absoluteRenderBounds": { + "height": 20, + "width": 276, + "x": 10588, + "y": 6632 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "componentProperties": { + "depth2#44:8": { + "type": "BOOLEAN", + "value": true + }, + "depth3#44:5": { + "type": "BOOLEAN", + "value": true + }, + "type": { + "boundVariables": {}, + "type": "VARIANT", + "value": "white" + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "height": 20, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 12, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 12, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "Breadcurmb", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "type": "white" + }, + "visible": true, + "width": 276, + "x": 22, + "y": 58, + "mainComponentId": "40:515", + "mainComponentName": "type=white", + "mainComponentSetId": "40:530" + }, + "extra": {}, + "fieldErrors": {} + }, + "44:4929": { + "id": "44:4929", + "type": "COMPONENT", + "fields": { + "parentId": "43:1158", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "44:4930" + ], + "absoluteBoundingBox": { + "height": 316, + "width": 1480, + "x": 10546, + "y": 6761 + }, + "absoluteRenderBounds": { + "height": 316, + "width": 1480, + "x": 10546, + "y": 6761 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:350", + "type": "VARIABLE_ALIAS" + } + ], + "paddingLeft": { + "id": "VariableID:30:628", + "type": "VARIABLE_ALIAS" + }, + "paddingRight": { + "id": "VariableID:30:628", + "type": "VARIABLE_ALIAS" + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:350", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 316, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 32, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutPositioning": "AUTO", + "paddingBottom": 64, + "paddingLeft": 40, + "paddingRight": 40, + "paddingTop": 144, + "primaryAxisAlignItems": "MAX", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 32, + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "viewport=Desktop, type=White", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 64, + "paddingLeft": 40, + "paddingRight": 40, + "paddingTop": 144, + "primaryAxisAlignItems": "MAX", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "type": "White", + "viewport": "Desktop" + }, + "visible": true, + "width": 1480, + "x": 21, + "y": 701 + }, + "extra": {}, + "fieldErrors": {} + }, + "44:4930": { + "id": "44:4930", + "type": "FRAME", + "fields": { + "parentId": "44:4929", + "childrenIds": [ + "44:4931", + "44:4932" + ], + "absoluteBoundingBox": { + "height": 108, + "width": 1280, + "x": 10646, + "y": 6905 + }, + "absoluteRenderBounds": { + "height": 108, + "width": 1280, + "x": 10646, + "y": 6905 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "height": 108, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 24, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 24, + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": 1280, + "name": "Frame 479", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 1280, + "x": 100, + "y": 144 + }, + "extra": {}, + "fieldErrors": {} + }, + "44:4931": { + "id": "44:4931", + "type": "TEXT", + "fields": { + "parentId": "44:4930", + "absoluteBoundingBox": { + "height": 63, + "width": 1280, + "x": 10646, + "y": 6905 + }, + "absoluteRenderBounds": { + "height": 43.34375, + "width": 119.234375, + "x": 11227.1982421875, + "y": 6914.83203125 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:23:1559", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "타이틀", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.1529411822557449, + "g": 0.08627451211214066, + "r": 0.06666667014360428 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "ExtraBold" + }, + "fontSize": 48, + "height": 63, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 132.00000524520874 + }, + "maxHeight": null, + "maxWidth": null, + "name": "타이틀", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAlignHorizontal": "CENTER", + "textAutoResize": "HEIGHT", + "textStyleId": "S:82867982bb8f7eff7b35bc2fef1bc1a06ee8a0c8,", + "textTruncation": "DISABLED", + "visible": true, + "width": 1280, + "x": 0, + "y": 0, + "styledTextSegments": [ + { + "characters": "타이틀", + "end": 3, + "fillStyleId": "", + "fontWeight": 800, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:82867982bb8f7eff7b35bc2fef1bc1a06ee8a0c8," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "44:4932": { + "id": "44:4932", + "type": "INSTANCE", + "fields": { + "parentId": "44:4930", + "childrenIds": [ + "I44:4932;40:532", + "I44:4932;40:533", + "I44:4932;40:534", + "I44:4932;40:535", + "I44:4932;40:536", + "I44:4932;40:537", + "I44:4932;40:538" + ], + "absoluteBoundingBox": { + "height": 21, + "width": 287, + "x": 11142.5, + "y": 6992 + }, + "absoluteRenderBounds": { + "height": 21, + "width": 287, + "x": 11142.5, + "y": 6992 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "componentProperties": { + "depth2#44:8": { + "type": "BOOLEAN", + "value": true + }, + "depth3#44:5": { + "type": "BOOLEAN", + "value": true + }, + "type": { + "boundVariables": {}, + "type": "VARIANT", + "value": "color" + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "height": 21, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 12, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 12, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "Breadcurmb", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "type": "color" + }, + "visible": true, + "width": 287, + "x": 496.5, + "y": 87, + "mainComponentId": "40:531", + "mainComponentName": "type=color", + "mainComponentSetId": "40:530" + }, + "extra": {}, + "fieldErrors": {} + }, + "44:4933": { + "id": "44:4933", + "type": "COMPONENT", + "fields": { + "parentId": "43:1158", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "44:4934" + ], + "absoluteBoundingBox": { + "height": 202, + "width": 360, + "x": 10546, + "y": 7097 + }, + "absoluteRenderBounds": { + "height": 202, + "width": 360, + "x": 10546, + "y": 7097 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:350", + "type": "VARIABLE_ALIAS" + } + ], + "paddingLeft": { + "id": "VariableID:30:628", + "type": "VARIABLE_ALIAS" + }, + "paddingRight": { + "id": "VariableID:30:628", + "type": "VARIABLE_ALIAS" + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:350", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 202, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 32, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutPositioning": "AUTO", + "paddingBottom": 32, + "paddingLeft": 20, + "paddingRight": 20, + "paddingTop": 92, + "primaryAxisAlignItems": "MAX", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 32, + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "viewport=Mobile, type=White", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 32, + "paddingLeft": 20, + "paddingRight": 20, + "paddingTop": 92, + "primaryAxisAlignItems": "MAX", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "type": "White", + "viewport": "Mobile" + }, + "visible": true, + "width": 360, + "x": 21, + "y": 1037 + }, + "extra": {}, + "fieldErrors": {} + }, + "44:4934": { + "id": "44:4934", + "type": "FRAME", + "fields": { + "parentId": "44:4933", + "childrenIds": [ + "44:4935", + "44:4936" + ], + "absoluteBoundingBox": { + "height": 78, + "width": 320, + "x": 10566, + "y": 7189 + }, + "absoluteRenderBounds": { + "height": 78, + "width": 320, + "x": 10566, + "y": 7189 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "height": 78, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 16, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 16, + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": 1280, + "name": "Frame 479", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 320, + "x": 20, + "y": 92 + }, + "extra": {}, + "fieldErrors": {} + }, + "44:4935": { + "id": "44:4935", + "type": "TEXT", + "fields": { + "parentId": "44:4934", + "absoluteBoundingBox": { + "height": 42, + "width": 320, + "x": 10566, + "y": 7189 + }, + "absoluteRenderBounds": { + "height": 28.89599609375, + "width": 79.4892578125, + "x": 10686.798828125, + "y": 7195.88818359375 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:23:1559", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "타이틀", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.1529411822557449, + "g": 0.08627451211214066, + "r": 0.06666667014360428 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "ExtraBold" + }, + "fontSize": 32, + "height": 42, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 132.00000524520874 + }, + "maxHeight": null, + "maxWidth": null, + "name": "타이틀", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAlignHorizontal": "CENTER", + "textAutoResize": "HEIGHT", + "textStyleId": "S:82867982bb8f7eff7b35bc2fef1bc1a06ee8a0c8,", + "textTruncation": "DISABLED", + "visible": true, + "width": 320, + "x": 0, + "y": 0, + "styledTextSegments": [ + { + "characters": "타이틀", + "end": 3, + "fillStyleId": "", + "fontWeight": 800, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:82867982bb8f7eff7b35bc2fef1bc1a06ee8a0c8," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "44:4936": { + "id": "44:4936", + "type": "INSTANCE", + "fields": { + "parentId": "44:4934", + "childrenIds": [ + "I44:4936;40:532", + "I44:4936;40:533", + "I44:4936;40:534", + "I44:4936;40:535", + "I44:4936;40:536", + "I44:4936;40:537", + "I44:4936;40:538" + ], + "absoluteBoundingBox": { + "height": 20, + "width": 276, + "x": 10588, + "y": 7247 + }, + "absoluteRenderBounds": { + "height": 20, + "width": 276, + "x": 10588, + "y": 7247 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "componentProperties": { + "depth2#44:8": { + "type": "BOOLEAN", + "value": true + }, + "depth3#44:5": { + "type": "BOOLEAN", + "value": true + }, + "type": { + "boundVariables": {}, + "type": "VARIANT", + "value": "color" + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "height": 20, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 12, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 12, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "Breadcurmb", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "type": "color" + }, + "visible": true, + "width": 276, + "x": 22, + "y": 58, + "mainComponentId": "40:531", + "mainComponentName": "type=color", + "mainComponentSetId": "40:530" + }, + "extra": {}, + "fieldErrors": {} + }, + "I40:546;40:503": { + "id": "I40:546;40:503", + "type": "INSTANCE", + "fields": { + "parentId": "40:546", + "childrenIds": [ + "I40:546;40:503;40:453" + ], + "absoluteBoundingBox": { + "height": 20, + "width": 20, + "x": 11142.5, + "y": 6318.5 + }, + "absoluteRenderBounds": { + "height": 20, + "width": 20, + "x": 11142.5, + "y": 6318.5 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": false + } + ], + "height": 20, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 2.499999523162842, + "paddingLeft": 2.5, + "paddingRight": 2.5, + "paddingTop": 1.8755793571472168, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "isAsset": true, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "House", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "targetAspectRatio": { + "x": 24, + "y": 24 + }, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 20, + "x": 0, + "y": 0.5, + "mainComponentId": "40:454", + "mainComponentName": "House", + "mainComponentSetId": null + }, + "extra": {}, + "fieldErrors": {} + }, + "I40:546;40:503;40:453": { + "id": "I40:546;40:503;40:453", + "type": "VECTOR", + "fields": { + "parentId": "I40:546;40:503", + "absoluteBoundingBox": { + "height": 15.62442111968994, + "width": 15, + "x": 11145, + "y": 6320.37548828125 + }, + "absoluteRenderBounds": { + "height": 15.62451171875, + "width": 15, + "x": 11145, + "y": 6320.37548828125 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 15.62442111968994, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeWeight": 0.09375, + "visible": true, + "width": 15, + "x": 2.5, + "y": 1.8755793571472168 + }, + "extra": {}, + "fieldErrors": {} + }, + "I40:546;40:504": { + "id": "I40:546;40:504", + "type": "INSTANCE", + "fields": { + "parentId": "40:546", + "childrenIds": [ + "I40:546;40:504;40:476" + ], + "absoluteBoundingBox": { + "height": 12, + "width": 12, + "x": 11174.5, + "y": 6322.5 + }, + "absoluteRenderBounds": { + "height": 12, + "width": 12, + "x": 11174.5, + "y": 6322.5 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "componentProperties": { + "속성 1": { + "boundVariables": {}, + "type": "VARIANT", + "value": "CaretRight" + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": false + } + ], + "height": 12, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 1.874704122543335, + "paddingLeft": 4.125, + "paddingRight": 3.3747053146362305, + "paddingTop": 1.8747045993804932, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "isAsset": true, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Caret-fill", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "targetAspectRatio": { + "x": 24, + "y": 24 + }, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "속성 1": "CaretRight" + }, + "visible": true, + "width": 12, + "x": 32, + "y": 4.5, + "mainComponentId": "40:479", + "mainComponentName": "속성 1=CaretRight", + "mainComponentSetId": "40:483" + }, + "extra": {}, + "fieldErrors": {} + }, + "I40:546;40:504;40:476": { + "id": "I40:546;40:504;40:476", + "type": "VECTOR", + "fields": { + "parentId": "I40:546;40:504", + "absoluteBoundingBox": { + "height": 8.250591278076172, + "width": 4.5002946853637695, + "x": 11178.625, + "y": 6324.37451171875 + }, + "absoluteRenderBounds": { + "height": 8.25048828125, + "width": 4.5, + "x": 11178.625, + "y": 6324.37451171875 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 0.75, + "type": "SOLID", + "visible": true + } + ], + "height": 8.250591278076172, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeWeight": 0.09375, + "visible": true, + "width": 4.5002946853637695, + "x": 4.125, + "y": 1.8747045993804932 + }, + "extra": {}, + "fieldErrors": {} + }, + "I40:546;40:505": { + "id": "I40:546;40:505", + "type": "TEXT", + "fields": { + "parentId": "40:546", + "absoluteBoundingBox": { + "height": 21, + "width": 51, + "x": 11198.5, + "y": 6318 + }, + "absoluteRenderBounds": { + "height": 13.30029296875, + "width": 48.4375, + "x": 11199.6337890625, + "y": 6323.77978515625 + }, + "boundVariables": { + "fontSize": [ + { + "id": "VariableID:18:956", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "Deapth1", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Bold" + }, + "fontSize": 14, + "height": 21, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "Deapth1", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:44614ba166a8fa9b3003396a689f04927b273a5d,", + "textTruncation": "DISABLED", + "visible": true, + "width": 51, + "x": 56, + "y": 0, + "styledTextSegments": [ + { + "characters": "Deapth1", + "end": 7, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:44614ba166a8fa9b3003396a689f04927b273a5d," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "I40:546;40:506": { + "id": "I40:546;40:506", + "type": "INSTANCE", + "fields": { + "parentId": "40:546", + "childrenIds": [ + "I40:546;40:506;40:476" + ], + "absoluteBoundingBox": { + "height": 12, + "width": 12, + "x": 11261.5, + "y": 6322.5 + }, + "absoluteRenderBounds": { + "height": 12, + "width": 12, + "x": 11261.5, + "y": 6322.5 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "componentProperties": { + "속성 1": { + "boundVariables": {}, + "type": "VARIANT", + "value": "CaretRight" + } + }, + "componentPropertyReferences": { + "visible": "depth2#44:8" + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": false + } + ], + "height": 12, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 1.874704122543335, + "paddingLeft": 4.125, + "paddingRight": 3.3747053146362305, + "paddingTop": 1.8747045993804932, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "isAsset": true, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Caret-fill", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "targetAspectRatio": { + "x": 24, + "y": 24 + }, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "속성 1": "CaretRight" + }, + "visible": true, + "width": 12, + "x": 119, + "y": 4.5, + "mainComponentId": "40:479", + "mainComponentName": "속성 1=CaretRight", + "mainComponentSetId": "40:483" + }, + "extra": {}, + "fieldErrors": {} + }, + "I40:546;40:506;40:476": { + "id": "I40:546;40:506;40:476", + "type": "VECTOR", + "fields": { + "parentId": "I40:546;40:506", + "absoluteBoundingBox": { + "height": 8.250591278076172, + "width": 4.5002946853637695, + "x": 11265.625, + "y": 6324.37451171875 + }, + "absoluteRenderBounds": { + "height": 8.25048828125, + "width": 4.5, + "x": 11265.625, + "y": 6324.37451171875 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 0.75, + "type": "SOLID", + "visible": true + } + ], + "height": 8.250591278076172, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeWeight": 0.09375, + "visible": true, + "width": 4.5002946853637695, + "x": 4.125, + "y": 1.8747045993804932 + }, + "extra": {}, + "fieldErrors": {} + }, + "I40:546;40:507": { + "id": "I40:546;40:507", + "type": "TEXT", + "fields": { + "parentId": "40:546", + "absoluteBoundingBox": { + "height": 21, + "width": 54, + "x": 11285.5, + "y": 6318 + }, + "absoluteRenderBounds": { + "height": 13.328125, + "width": 51.3359375, + "x": 11286.6337890625, + "y": 6323.751953125 + }, + "boundVariables": { + "fontSize": [ + { + "id": "VariableID:18:956", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "Deapth2", + "componentPropertyReferences": { + "visible": "depth2#44:8" + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Bold" + }, + "fontSize": 14, + "height": 21, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "Deapth2", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:44614ba166a8fa9b3003396a689f04927b273a5d,", + "textTruncation": "DISABLED", + "visible": true, + "width": 54, + "x": 143, + "y": 0, + "styledTextSegments": [ + { + "characters": "Deapth2", + "end": 7, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:44614ba166a8fa9b3003396a689f04927b273a5d," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "I40:546;40:508": { + "id": "I40:546;40:508", + "type": "INSTANCE", + "fields": { + "parentId": "40:546", + "childrenIds": [ + "I40:546;40:508;40:476" + ], + "absoluteBoundingBox": { + "height": 12, + "width": 12, + "x": 11351.5, + "y": 6322.5 + }, + "absoluteRenderBounds": { + "height": 12, + "width": 12, + "x": 11351.5, + "y": 6322.5 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "componentProperties": { + "속성 1": { + "boundVariables": {}, + "type": "VARIANT", + "value": "CaretRight" + } + }, + "componentPropertyReferences": { + "visible": "depth3#44:5" + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": false + } + ], + "height": 12, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 1.874704122543335, + "paddingLeft": 4.125, + "paddingRight": 3.3747053146362305, + "paddingTop": 1.8747045993804932, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "isAsset": true, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Caret-fill", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "targetAspectRatio": { + "x": 24, + "y": 24 + }, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "속성 1": "CaretRight" + }, + "visible": true, + "width": 12, + "x": 209, + "y": 4.5, + "mainComponentId": "40:479", + "mainComponentName": "속성 1=CaretRight", + "mainComponentSetId": "40:483" + }, + "extra": {}, + "fieldErrors": {} + }, + "I40:546;40:508;40:476": { + "id": "I40:546;40:508;40:476", + "type": "VECTOR", + "fields": { + "parentId": "I40:546;40:508", + "absoluteBoundingBox": { + "height": 8.250591278076172, + "width": 4.5002946853637695, + "x": 11355.625, + "y": 6324.37451171875 + }, + "absoluteRenderBounds": { + "height": 8.25048828125, + "width": 4.5, + "x": 11355.625, + "y": 6324.37451171875 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 0.75, + "type": "SOLID", + "visible": true + } + ], + "height": 8.250591278076172, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeWeight": 0.09375, + "visible": true, + "width": 4.5002946853637695, + "x": 4.125, + "y": 1.8747045993804932 + }, + "extra": {}, + "fieldErrors": {} + }, + "I40:546;40:509": { + "id": "I40:546;40:509", + "type": "TEXT", + "fields": { + "parentId": "40:546", + "absoluteBoundingBox": { + "height": 21, + "width": 54, + "x": 11375.5, + "y": 6318 + }, + "absoluteRenderBounds": { + "height": 13.31396484375, + "width": 51.5458984375, + "x": 11376.6337890625, + "y": 6323.76611328125 + }, + "boundVariables": { + "fontSize": [ + { + "id": "VariableID:18:956", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "Deapth3", + "componentPropertyReferences": { + "visible": "depth3#44:5" + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Bold" + }, + "fontSize": 14, + "height": 21, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "Deapth3", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:44614ba166a8fa9b3003396a689f04927b273a5d,", + "textTruncation": "DISABLED", + "visible": true, + "width": 54, + "x": 233, + "y": 0, + "styledTextSegments": [ + { + "characters": "Deapth3", + "end": 7, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:44614ba166a8fa9b3003396a689f04927b273a5d," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1162;40:503": { + "id": "I43:1162;40:503", + "type": "INSTANCE", + "fields": { + "parentId": "43:1162", + "childrenIds": [ + "I43:1162;40:503;40:453" + ], + "absoluteBoundingBox": { + "height": 20, + "width": 20, + "x": 10588, + "y": 6632 + }, + "absoluteRenderBounds": { + "height": 20, + "width": 20, + "x": 10588, + "y": 6632 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": false + } + ], + "height": 20, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 2.499999523162842, + "paddingLeft": 2.5, + "paddingRight": 2.5, + "paddingTop": 1.8755793571472168, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "isAsset": true, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "House", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "targetAspectRatio": { + "x": 24, + "y": 24 + }, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 20, + "x": 0, + "y": 0, + "mainComponentId": "40:454", + "mainComponentName": "House", + "mainComponentSetId": null + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1162;40:503;40:453": { + "id": "I43:1162;40:503;40:453", + "type": "VECTOR", + "fields": { + "parentId": "I43:1162;40:503", + "absoluteBoundingBox": { + "height": 15.62442111968994, + "width": 15, + "x": 10590.5, + "y": 6633.87548828125 + }, + "absoluteRenderBounds": { + "height": 15.62451171875, + "width": 15, + "x": 10590.5, + "y": 6633.87548828125 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 15.62442111968994, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeWeight": 0.09375, + "visible": true, + "width": 15, + "x": 2.5, + "y": 1.8755793571472168 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1162;40:504": { + "id": "I43:1162;40:504", + "type": "INSTANCE", + "fields": { + "parentId": "43:1162", + "childrenIds": [ + "I43:1162;40:504;40:476" + ], + "absoluteBoundingBox": { + "height": 12, + "width": 12, + "x": 10620, + "y": 6636 + }, + "absoluteRenderBounds": { + "height": 12, + "width": 12, + "x": 10620, + "y": 6636 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "componentProperties": { + "속성 1": { + "boundVariables": {}, + "type": "VARIANT", + "value": "CaretRight" + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": false + } + ], + "height": 12, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 1.874704122543335, + "paddingLeft": 4.125, + "paddingRight": 3.3747053146362305, + "paddingTop": 1.8747045993804932, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "isAsset": true, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Caret-fill", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "targetAspectRatio": { + "x": 24, + "y": 24 + }, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "속성 1": "CaretRight" + }, + "visible": true, + "width": 12, + "x": 32, + "y": 4, + "mainComponentId": "40:479", + "mainComponentName": "속성 1=CaretRight", + "mainComponentSetId": "40:483" + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1162;40:504;40:476": { + "id": "I43:1162;40:504;40:476", + "type": "VECTOR", + "fields": { + "parentId": "I43:1162;40:504", + "absoluteBoundingBox": { + "height": 8.250591278076172, + "width": 4.5002946853637695, + "x": 10624.125, + "y": 6637.87451171875 + }, + "absoluteRenderBounds": { + "height": 8.25048828125, + "width": 4.5, + "x": 10624.125, + "y": 6637.87451171875 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 0.75, + "type": "SOLID", + "visible": true + } + ], + "height": 8.250591278076172, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeWeight": 0.09375, + "visible": true, + "width": 4.5002946853637695, + "x": 4.125, + "y": 1.8747045993804932 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1162;40:505": { + "id": "I43:1162;40:505", + "type": "TEXT", + "fields": { + "parentId": "43:1162", + "absoluteBoundingBox": { + "height": 20, + "width": 48, + "x": 10644, + "y": 6632 + }, + "absoluteRenderBounds": { + "height": 12.35009765625, + "width": 44.9775390625, + "x": 10645.052734375, + "y": 6637.509765625 + }, + "boundVariables": { + "fontSize": [ + { + "id": "VariableID:18:956", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "Deapth1", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Bold" + }, + "fontSize": 13, + "height": 20, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "Deapth1", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:44614ba166a8fa9b3003396a689f04927b273a5d,", + "textTruncation": "DISABLED", + "visible": true, + "width": 48, + "x": 56, + "y": 0, + "styledTextSegments": [ + { + "characters": "Deapth1", + "end": 7, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:44614ba166a8fa9b3003396a689f04927b273a5d," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1162;40:506": { + "id": "I43:1162;40:506", + "type": "INSTANCE", + "fields": { + "parentId": "43:1162", + "childrenIds": [ + "I43:1162;40:506;40:476" + ], + "absoluteBoundingBox": { + "height": 12, + "width": 12, + "x": 10704, + "y": 6636 + }, + "absoluteRenderBounds": { + "height": 12, + "width": 12, + "x": 10704, + "y": 6636 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "componentProperties": { + "속성 1": { + "boundVariables": {}, + "type": "VARIANT", + "value": "CaretRight" + } + }, + "componentPropertyReferences": { + "visible": "depth2#44:8" + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": false + } + ], + "height": 12, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 1.874704122543335, + "paddingLeft": 4.125, + "paddingRight": 3.3747053146362305, + "paddingTop": 1.8747045993804932, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "isAsset": true, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Caret-fill", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "targetAspectRatio": { + "x": 24, + "y": 24 + }, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "속성 1": "CaretRight" + }, + "visible": true, + "width": 12, + "x": 116, + "y": 4, + "mainComponentId": "40:479", + "mainComponentName": "속성 1=CaretRight", + "mainComponentSetId": "40:483" + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1162;40:506;40:476": { + "id": "I43:1162;40:506;40:476", + "type": "VECTOR", + "fields": { + "parentId": "I43:1162;40:506", + "absoluteBoundingBox": { + "height": 8.250591278076172, + "width": 4.5002946853637695, + "x": 10708.125, + "y": 6637.87451171875 + }, + "absoluteRenderBounds": { + "height": 8.25048828125, + "width": 4.5, + "x": 10708.125, + "y": 6637.87451171875 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 0.75, + "type": "SOLID", + "visible": true + } + ], + "height": 8.250591278076172, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeWeight": 0.09375, + "visible": true, + "width": 4.5002946853637695, + "x": 4.125, + "y": 1.8747045993804932 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1162;40:507": { + "id": "I43:1162;40:507", + "type": "TEXT", + "fields": { + "parentId": "43:1162", + "absoluteBoundingBox": { + "height": 20, + "width": 50, + "x": 10728, + "y": 6632 + }, + "absoluteRenderBounds": { + "height": 12.3759765625, + "width": 47.6689453125, + "x": 10729.052734375, + "y": 6637.48388671875 + }, + "boundVariables": { + "fontSize": [ + { + "id": "VariableID:18:956", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "Deapth2", + "componentPropertyReferences": { + "visible": "depth2#44:8" + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Bold" + }, + "fontSize": 13, + "height": 20, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "Deapth2", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:44614ba166a8fa9b3003396a689f04927b273a5d,", + "textTruncation": "DISABLED", + "visible": true, + "width": 50, + "x": 140, + "y": 0, + "styledTextSegments": [ + { + "characters": "Deapth2", + "end": 7, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:44614ba166a8fa9b3003396a689f04927b273a5d," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1162;40:508": { + "id": "I43:1162;40:508", + "type": "INSTANCE", + "fields": { + "parentId": "43:1162", + "childrenIds": [ + "I43:1162;40:508;40:476" + ], + "absoluteBoundingBox": { + "height": 12, + "width": 12, + "x": 10790, + "y": 6636 + }, + "absoluteRenderBounds": { + "height": 12, + "width": 12, + "x": 10790, + "y": 6636 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "componentProperties": { + "속성 1": { + "boundVariables": {}, + "type": "VARIANT", + "value": "CaretRight" + } + }, + "componentPropertyReferences": { + "visible": "depth3#44:5" + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": false + } + ], + "height": 12, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 1.874704122543335, + "paddingLeft": 4.125, + "paddingRight": 3.3747053146362305, + "paddingTop": 1.8747045993804932, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "isAsset": true, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Caret-fill", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "targetAspectRatio": { + "x": 24, + "y": 24 + }, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "속성 1": "CaretRight" + }, + "visible": true, + "width": 12, + "x": 202, + "y": 4, + "mainComponentId": "40:479", + "mainComponentName": "속성 1=CaretRight", + "mainComponentSetId": "40:483" + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1162;40:508;40:476": { + "id": "I43:1162;40:508;40:476", + "type": "VECTOR", + "fields": { + "parentId": "I43:1162;40:508", + "absoluteBoundingBox": { + "height": 8.250591278076172, + "width": 4.5002946853637695, + "x": 10794.125, + "y": 6637.87451171875 + }, + "absoluteRenderBounds": { + "height": 8.25048828125, + "width": 4.5, + "x": 10794.125, + "y": 6637.87451171875 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 0.75, + "type": "SOLID", + "visible": true + } + ], + "height": 8.250591278076172, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeWeight": 0.09375, + "visible": true, + "width": 4.5002946853637695, + "x": 4.125, + "y": 1.8747045993804932 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1162;40:509": { + "id": "I43:1162;40:509", + "type": "TEXT", + "fields": { + "parentId": "43:1162", + "absoluteBoundingBox": { + "height": 20, + "width": 50, + "x": 10814, + "y": 6632 + }, + "absoluteRenderBounds": { + "height": 12.36279296875, + "width": 47.8642578125, + "x": 10815.052734375, + "y": 6637.4970703125 + }, + "boundVariables": { + "fontSize": [ + { + "id": "VariableID:18:956", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "Deapth3", + "componentPropertyReferences": { + "visible": "depth3#44:5" + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Bold" + }, + "fontSize": 13, + "height": 20, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "Deapth3", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:44614ba166a8fa9b3003396a689f04927b273a5d,", + "textTruncation": "DISABLED", + "visible": true, + "width": 50, + "x": 226, + "y": 0, + "styledTextSegments": [ + { + "characters": "Deapth3", + "end": 7, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:44614ba166a8fa9b3003396a689f04927b273a5d," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:4932;40:532": { + "id": "I44:4932;40:532", + "type": "INSTANCE", + "fields": { + "parentId": "44:4932", + "childrenIds": [ + "I44:4932;40:532;40:453" + ], + "absoluteBoundingBox": { + "height": 20, + "width": 20, + "x": 11142.5, + "y": 6992.5 + }, + "absoluteRenderBounds": { + "height": 20, + "width": 20, + "x": 11142.5, + "y": 6992.5 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": false + } + ], + "height": 20, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 2.499999523162842, + "paddingLeft": 2.5, + "paddingRight": 2.5, + "paddingTop": 1.8755793571472168, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "isAsset": true, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "House", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "targetAspectRatio": { + "x": 24, + "y": 24 + }, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 20, + "x": 0, + "y": 0.5, + "mainComponentId": "40:454", + "mainComponentName": "House", + "mainComponentSetId": null + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:4932;40:532;40:453": { + "id": "I44:4932;40:532;40:453", + "type": "VECTOR", + "fields": { + "parentId": "I44:4932;40:532", + "absoluteBoundingBox": { + "height": 15.62442111968994, + "width": 15, + "x": 11145, + "y": 6994.37548828125 + }, + "absoluteRenderBounds": { + "height": 15.62451171875, + "width": 15, + "x": 11145, + "y": 6994.37548828125 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:345", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:345", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.7323716878890991, + "g": 0.34177345037460327, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 15.62442111968994, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeWeight": 0.09375, + "visible": true, + "width": 15, + "x": 2.5, + "y": 1.8755793571472168 + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:4932;40:533": { + "id": "I44:4932;40:533", + "type": "INSTANCE", + "fields": { + "parentId": "44:4932", + "childrenIds": [ + "I44:4932;40:533;40:476" + ], + "absoluteBoundingBox": { + "height": 12, + "width": 12, + "x": 11174.5, + "y": 6996.5 + }, + "absoluteRenderBounds": { + "height": 12, + "width": 12, + "x": 11174.5, + "y": 6996.5 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "componentProperties": { + "속성 1": { + "boundVariables": {}, + "type": "VARIANT", + "value": "CaretRight" + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": false + } + ], + "height": 12, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 1.874704122543335, + "paddingLeft": 4.125, + "paddingRight": 3.3747053146362305, + "paddingTop": 1.8747045993804932, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "isAsset": true, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Caret-fill", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "targetAspectRatio": { + "x": 24, + "y": 24 + }, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "속성 1": "CaretRight" + }, + "visible": true, + "width": 12, + "x": 32, + "y": 4.5, + "mainComponentId": "40:479", + "mainComponentName": "속성 1=CaretRight", + "mainComponentSetId": "40:483" + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:4932;40:533;40:476": { + "id": "I44:4932;40:533;40:476", + "type": "VECTOR", + "fields": { + "parentId": "I44:4932;40:533", + "absoluteBoundingBox": { + "height": 8.250591278076172, + "width": 4.5002946853637695, + "x": 11178.625, + "y": 6998.37451171875 + }, + "absoluteRenderBounds": { + "height": 8.25048828125, + "width": 4.5, + "x": 11178.625, + "y": 6998.37451171875 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:359", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:359", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.6682692170143127, + "g": 0.6104382276535034, + "r": 0.5815227627754211 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 8.250591278076172, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeWeight": 0.09375, + "visible": true, + "width": 4.5002946853637695, + "x": 4.125, + "y": 1.8747045993804932 + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:4932;40:534": { + "id": "I44:4932;40:534", + "type": "TEXT", + "fields": { + "parentId": "44:4932", + "absoluteBoundingBox": { + "height": 21, + "width": 51, + "x": 11198.5, + "y": 6992 + }, + "absoluteRenderBounds": { + "height": 13.30029296875, + "width": 48.4375, + "x": 11199.6337890625, + "y": 6997.77978515625 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:348", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:956", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "Deapth1", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:348", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.3176470696926117, + "g": 0.250980406999588, + "r": 0.21568627655506137 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Bold" + }, + "fontSize": 14, + "height": 21, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "Deapth1", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:44614ba166a8fa9b3003396a689f04927b273a5d,", + "textTruncation": "DISABLED", + "visible": true, + "width": 51, + "x": 56, + "y": 0, + "styledTextSegments": [ + { + "characters": "Deapth1", + "end": 7, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:44614ba166a8fa9b3003396a689f04927b273a5d," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:4932;40:535": { + "id": "I44:4932;40:535", + "type": "INSTANCE", + "fields": { + "parentId": "44:4932", + "childrenIds": [ + "I44:4932;40:535;40:476" + ], + "absoluteBoundingBox": { + "height": 12, + "width": 12, + "x": 11261.5, + "y": 6996.5 + }, + "absoluteRenderBounds": { + "height": 12, + "width": 12, + "x": 11261.5, + "y": 6996.5 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "componentProperties": { + "속성 1": { + "boundVariables": {}, + "type": "VARIANT", + "value": "CaretRight" + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": false + } + ], + "height": 12, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 1.874704122543335, + "paddingLeft": 4.125, + "paddingRight": 3.3747053146362305, + "paddingTop": 1.8747045993804932, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "isAsset": true, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Caret-fill", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "targetAspectRatio": { + "x": 24, + "y": 24 + }, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "속성 1": "CaretRight" + }, + "visible": true, + "width": 12, + "x": 119, + "y": 4.5, + "mainComponentId": "40:479", + "mainComponentName": "속성 1=CaretRight", + "mainComponentSetId": "40:483" + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:4932;40:535;40:476": { + "id": "I44:4932;40:535;40:476", + "type": "VECTOR", + "fields": { + "parentId": "I44:4932;40:535", + "absoluteBoundingBox": { + "height": 8.250591278076172, + "width": 4.5002946853637695, + "x": 11265.625, + "y": 6998.37451171875 + }, + "absoluteRenderBounds": { + "height": 8.25048828125, + "width": 4.5, + "x": 11265.625, + "y": 6998.37451171875 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:359", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:359", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.6682692170143127, + "g": 0.6104382276535034, + "r": 0.5815227627754211 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 8.250591278076172, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeWeight": 0.09375, + "visible": true, + "width": 4.5002946853637695, + "x": 4.125, + "y": 1.8747045993804932 + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:4932;40:536": { + "id": "I44:4932;40:536", + "type": "TEXT", + "fields": { + "parentId": "44:4932", + "absoluteBoundingBox": { + "height": 21, + "width": 54, + "x": 11285.5, + "y": 6992 + }, + "absoluteRenderBounds": { + "height": 13.328125, + "width": 51.3359375, + "x": 11286.6337890625, + "y": 6997.751953125 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:348", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:956", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "Deapth2", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:348", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.3176470696926117, + "g": 0.250980406999588, + "r": 0.21568627655506137 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Bold" + }, + "fontSize": 14, + "height": 21, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "Deapth2", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:44614ba166a8fa9b3003396a689f04927b273a5d,", + "textTruncation": "DISABLED", + "visible": true, + "width": 54, + "x": 143, + "y": 0, + "styledTextSegments": [ + { + "characters": "Deapth2", + "end": 7, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:44614ba166a8fa9b3003396a689f04927b273a5d," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:4932;40:537": { + "id": "I44:4932;40:537", + "type": "INSTANCE", + "fields": { + "parentId": "44:4932", + "childrenIds": [ + "I44:4932;40:537;40:476" + ], + "absoluteBoundingBox": { + "height": 12, + "width": 12, + "x": 11351.5, + "y": 6996.5 + }, + "absoluteRenderBounds": { + "height": 12, + "width": 12, + "x": 11351.5, + "y": 6996.5 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "componentProperties": { + "속성 1": { + "boundVariables": {}, + "type": "VARIANT", + "value": "CaretRight" + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": false + } + ], + "height": 12, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 1.874704122543335, + "paddingLeft": 4.125, + "paddingRight": 3.3747053146362305, + "paddingTop": 1.8747045993804932, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "isAsset": true, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Caret-fill", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "targetAspectRatio": { + "x": 24, + "y": 24 + }, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "속성 1": "CaretRight" + }, + "visible": true, + "width": 12, + "x": 209, + "y": 4.5, + "mainComponentId": "40:479", + "mainComponentName": "속성 1=CaretRight", + "mainComponentSetId": "40:483" + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:4932;40:537;40:476": { + "id": "I44:4932;40:537;40:476", + "type": "VECTOR", + "fields": { + "parentId": "I44:4932;40:537", + "absoluteBoundingBox": { + "height": 8.250591278076172, + "width": 4.5002946853637695, + "x": 11355.625, + "y": 6998.37451171875 + }, + "absoluteRenderBounds": { + "height": 8.25048828125, + "width": 4.5, + "x": 11355.625, + "y": 6998.37451171875 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:359", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:359", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.6682692170143127, + "g": 0.6104382276535034, + "r": 0.5815227627754211 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 8.250591278076172, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeWeight": 0.09375, + "visible": true, + "width": 4.5002946853637695, + "x": 4.125, + "y": 1.8747045993804932 + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:4932;40:538": { + "id": "I44:4932;40:538", + "type": "TEXT", + "fields": { + "parentId": "44:4932", + "absoluteBoundingBox": { + "height": 21, + "width": 54, + "x": 11375.5, + "y": 6992 + }, + "absoluteRenderBounds": { + "height": 13.31396484375, + "width": 51.5458984375, + "x": 11376.6337890625, + "y": 6997.76611328125 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:348", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:956", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "Deapth3", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:348", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.3176470696926117, + "g": 0.250980406999588, + "r": 0.21568627655506137 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Bold" + }, + "fontSize": 14, + "height": 21, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "Deapth3", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:44614ba166a8fa9b3003396a689f04927b273a5d,", + "textTruncation": "DISABLED", + "visible": true, + "width": 54, + "x": 233, + "y": 0, + "styledTextSegments": [ + { + "characters": "Deapth3", + "end": 7, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:44614ba166a8fa9b3003396a689f04927b273a5d," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:4936;40:532": { + "id": "I44:4936;40:532", + "type": "INSTANCE", + "fields": { + "parentId": "44:4936", + "childrenIds": [ + "I44:4936;40:532;40:453" + ], + "absoluteBoundingBox": { + "height": 20, + "width": 20, + "x": 10588, + "y": 7247 + }, + "absoluteRenderBounds": { + "height": 20, + "width": 20, + "x": 10588, + "y": 7247 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": false + } + ], + "height": 20, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 2.499999523162842, + "paddingLeft": 2.5, + "paddingRight": 2.5, + "paddingTop": 1.8755793571472168, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "isAsset": true, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "House", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "targetAspectRatio": { + "x": 24, + "y": 24 + }, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 20, + "x": 0, + "y": 0, + "mainComponentId": "40:454", + "mainComponentName": "House", + "mainComponentSetId": null + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:4936;40:532;40:453": { + "id": "I44:4936;40:532;40:453", + "type": "VECTOR", + "fields": { + "parentId": "I44:4936;40:532", + "absoluteBoundingBox": { + "height": 15.62442111968994, + "width": 15, + "x": 10590.5, + "y": 7248.87548828125 + }, + "absoluteRenderBounds": { + "height": 15.62451171875, + "width": 15, + "x": 10590.5, + "y": 7248.87548828125 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:345", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:345", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.7323716878890991, + "g": 0.34177345037460327, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 15.62442111968994, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeWeight": 0.09375, + "visible": true, + "width": 15, + "x": 2.5, + "y": 1.8755793571472168 + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:4936;40:533": { + "id": "I44:4936;40:533", + "type": "INSTANCE", + "fields": { + "parentId": "44:4936", + "childrenIds": [ + "I44:4936;40:533;40:476" + ], + "absoluteBoundingBox": { + "height": 12, + "width": 12, + "x": 10620, + "y": 7251 + }, + "absoluteRenderBounds": { + "height": 12, + "width": 12, + "x": 10620, + "y": 7251 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "componentProperties": { + "속성 1": { + "boundVariables": {}, + "type": "VARIANT", + "value": "CaretRight" + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": false + } + ], + "height": 12, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 1.874704122543335, + "paddingLeft": 4.125, + "paddingRight": 3.3747053146362305, + "paddingTop": 1.8747045993804932, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "isAsset": true, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Caret-fill", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "targetAspectRatio": { + "x": 24, + "y": 24 + }, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "속성 1": "CaretRight" + }, + "visible": true, + "width": 12, + "x": 32, + "y": 4, + "mainComponentId": "40:479", + "mainComponentName": "속성 1=CaretRight", + "mainComponentSetId": "40:483" + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:4936;40:533;40:476": { + "id": "I44:4936;40:533;40:476", + "type": "VECTOR", + "fields": { + "parentId": "I44:4936;40:533", + "absoluteBoundingBox": { + "height": 8.250591278076172, + "width": 4.5002946853637695, + "x": 10624.125, + "y": 7252.87451171875 + }, + "absoluteRenderBounds": { + "height": 8.25048828125, + "width": 4.5, + "x": 10624.125, + "y": 7252.87451171875 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:359", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:359", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.6682692170143127, + "g": 0.6104382276535034, + "r": 0.5815227627754211 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 8.250591278076172, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeWeight": 0.09375, + "visible": true, + "width": 4.5002946853637695, + "x": 4.125, + "y": 1.8747045993804932 + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:4936;40:534": { + "id": "I44:4936;40:534", + "type": "TEXT", + "fields": { + "parentId": "44:4936", + "absoluteBoundingBox": { + "height": 20, + "width": 48, + "x": 10644, + "y": 7247 + }, + "absoluteRenderBounds": { + "height": 12.35009765625, + "width": 44.9775390625, + "x": 10645.052734375, + "y": 7252.509765625 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:348", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:956", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "Deapth1", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:348", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.3176470696926117, + "g": 0.250980406999588, + "r": 0.21568627655506137 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Bold" + }, + "fontSize": 13, + "height": 20, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "Deapth1", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:44614ba166a8fa9b3003396a689f04927b273a5d,", + "textTruncation": "DISABLED", + "visible": true, + "width": 48, + "x": 56, + "y": 0, + "styledTextSegments": [ + { + "characters": "Deapth1", + "end": 7, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:44614ba166a8fa9b3003396a689f04927b273a5d," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:4936;40:535": { + "id": "I44:4936;40:535", + "type": "INSTANCE", + "fields": { + "parentId": "44:4936", + "childrenIds": [ + "I44:4936;40:535;40:476" + ], + "absoluteBoundingBox": { + "height": 12, + "width": 12, + "x": 10704, + "y": 7251 + }, + "absoluteRenderBounds": { + "height": 12, + "width": 12, + "x": 10704, + "y": 7251 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "componentProperties": { + "속성 1": { + "boundVariables": {}, + "type": "VARIANT", + "value": "CaretRight" + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": false + } + ], + "height": 12, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 1.874704122543335, + "paddingLeft": 4.125, + "paddingRight": 3.3747053146362305, + "paddingTop": 1.8747045993804932, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "isAsset": true, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Caret-fill", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "targetAspectRatio": { + "x": 24, + "y": 24 + }, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "속성 1": "CaretRight" + }, + "visible": true, + "width": 12, + "x": 116, + "y": 4, + "mainComponentId": "40:479", + "mainComponentName": "속성 1=CaretRight", + "mainComponentSetId": "40:483" + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:4936;40:535;40:476": { + "id": "I44:4936;40:535;40:476", + "type": "VECTOR", + "fields": { + "parentId": "I44:4936;40:535", + "absoluteBoundingBox": { + "height": 8.250591278076172, + "width": 4.5002946853637695, + "x": 10708.125, + "y": 7252.87451171875 + }, + "absoluteRenderBounds": { + "height": 8.25048828125, + "width": 4.5, + "x": 10708.125, + "y": 7252.87451171875 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:359", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:359", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.6682692170143127, + "g": 0.6104382276535034, + "r": 0.5815227627754211 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 8.250591278076172, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeWeight": 0.09375, + "visible": true, + "width": 4.5002946853637695, + "x": 4.125, + "y": 1.8747045993804932 + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:4936;40:536": { + "id": "I44:4936;40:536", + "type": "TEXT", + "fields": { + "parentId": "44:4936", + "absoluteBoundingBox": { + "height": 20, + "width": 50, + "x": 10728, + "y": 7247 + }, + "absoluteRenderBounds": { + "height": 12.3759765625, + "width": 47.6689453125, + "x": 10729.052734375, + "y": 7252.48388671875 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:348", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:956", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "Deapth2", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:348", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.3176470696926117, + "g": 0.250980406999588, + "r": 0.21568627655506137 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Bold" + }, + "fontSize": 13, + "height": 20, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "Deapth2", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:44614ba166a8fa9b3003396a689f04927b273a5d,", + "textTruncation": "DISABLED", + "visible": true, + "width": 50, + "x": 140, + "y": 0, + "styledTextSegments": [ + { + "characters": "Deapth2", + "end": 7, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:44614ba166a8fa9b3003396a689f04927b273a5d," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:4936;40:537": { + "id": "I44:4936;40:537", + "type": "INSTANCE", + "fields": { + "parentId": "44:4936", + "childrenIds": [ + "I44:4936;40:537;40:476" + ], + "absoluteBoundingBox": { + "height": 12, + "width": 12, + "x": 10790, + "y": 7251 + }, + "absoluteRenderBounds": { + "height": 12, + "width": 12, + "x": 10790, + "y": 7251 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "componentProperties": { + "속성 1": { + "boundVariables": {}, + "type": "VARIANT", + "value": "CaretRight" + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": false + } + ], + "height": 12, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 1.874704122543335, + "paddingLeft": 4.125, + "paddingRight": 3.3747053146362305, + "paddingTop": 1.8747045993804932, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "isAsset": true, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Caret-fill", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "targetAspectRatio": { + "x": 24, + "y": 24 + }, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "속성 1": "CaretRight" + }, + "visible": true, + "width": 12, + "x": 202, + "y": 4, + "mainComponentId": "40:479", + "mainComponentName": "속성 1=CaretRight", + "mainComponentSetId": "40:483" + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:4936;40:537;40:476": { + "id": "I44:4936;40:537;40:476", + "type": "VECTOR", + "fields": { + "parentId": "I44:4936;40:537", + "absoluteBoundingBox": { + "height": 8.250591278076172, + "width": 4.5002946853637695, + "x": 10794.125, + "y": 7252.87451171875 + }, + "absoluteRenderBounds": { + "height": 8.25048828125, + "width": 4.5, + "x": 10794.125, + "y": 7252.87451171875 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:359", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:359", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.6682692170143127, + "g": 0.6104382276535034, + "r": 0.5815227627754211 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 8.250591278076172, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeWeight": 0.09375, + "visible": true, + "width": 4.5002946853637695, + "x": 4.125, + "y": 1.8747045993804932 + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:4936;40:538": { + "id": "I44:4936;40:538", + "type": "TEXT", + "fields": { + "parentId": "44:4936", + "absoluteBoundingBox": { + "height": 20, + "width": 50, + "x": 10814, + "y": 7247 + }, + "absoluteRenderBounds": { + "height": 12.36279296875, + "width": 47.8642578125, + "x": 10815.052734375, + "y": 7252.4970703125 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:348", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:956", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "Deapth3", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:348", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.3176470696926117, + "g": 0.250980406999588, + "r": 0.21568627655506137 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Bold" + }, + "fontSize": 13, + "height": 20, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "Deapth3", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:44614ba166a8fa9b3003396a689f04927b273a5d,", + "textTruncation": "DISABLED", + "visible": true, + "width": 50, + "x": 226, + "y": 0, + "styledTextSegments": [ + { + "characters": "Deapth3", + "end": 7, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:44614ba166a8fa9b3003396a689f04927b273a5d," + } + ] + }, + "extra": {}, + "fieldErrors": {} + } + }, + "diagnostics": [] +} diff --git a/crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_profile_card_set.json b/crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_profile_card_set.json new file mode 100644 index 00000000..dd8024bd --- /dev/null +++ b/crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_profile_card_set.json @@ -0,0 +1,2208 @@ +{ + "fileKey": "K2kUnxGgvooAJwCQiVTb7g", + "version": null, + "roots": [ + "41:750" + ], + "nodes": { + "41:727": { + "id": "41:727", + "type": "RECTANGLE", + "fields": { + "parentId": "41:733", + "absoluteBoundingBox": { + "height": 250, + "width": 200, + "x": 10541, + "y": 4810 + }, + "absoluteRenderBounds": { + "height": 250, + "width": 200, + "x": 10541, + "y": 4810 + }, + "bottomLeftRadius": 12, + "bottomRightRadius": 12, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 12, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.8509804010391235, + "g": 0.8509804010391235, + "r": 0.8509804010391235 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 250, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Rectangle 3", + "opacity": 1, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "targetAspectRatio": { + "x": 4, + "y": 5 + }, + "topLeftRadius": 12, + "topRightRadius": 12, + "visible": true, + "width": 200, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "41:728": { + "id": "41:728", + "type": "FRAME", + "fields": { + "parentId": "41:733", + "childrenIds": [ + "41:729", + "41:732" + ], + "absoluteBoundingBox": { + "height": 240, + "width": 974, + "x": 10765, + "y": 4810 + }, + "absoluteRenderBounds": { + "height": 240, + "width": 974, + "x": 10765, + "y": 4810 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "height": 240, + "inferredAutoLayout": { + "counterAxisAlignItems": "MIN", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 16, + "layoutAlign": "INHERIT", + "layoutGrow": 1, + "layoutMode": "VERTICAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 16, + "layoutGrow": 1, + "layoutMode": "VERTICAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "Frame 484", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 974, + "x": 224, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "41:729": { + "id": "41:729", + "type": "FRAME", + "fields": { + "parentId": "41:728", + "childrenIds": [ + "41:730", + "41:731" + ], + "absoluteBoundingBox": { + "height": 63, + "width": 974, + "x": 10765, + "y": 4810 + }, + "absoluteRenderBounds": { + "height": 63, + "width": 974, + "x": 10765, + "y": 4810 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "height": 63, + "inferredAutoLayout": { + "counterAxisAlignItems": "MIN", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 8, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 8, + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "Frame 486", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 974, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "41:730": { + "id": "41:730", + "type": "TEXT", + "fields": { + "parentId": "41:729", + "absoluteBoundingBox": { + "height": 21, + "width": 974, + "x": 10765, + "y": 4810 + }, + "absoluteRenderBounds": { + "height": 12.6416015625, + "width": 90.509765625, + "x": 10765.76953125, + "y": 4814.576171875 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:359", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:956", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "대표이사 (CEO)", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:359", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.6682692170143127, + "g": 0.6104382276535034, + "r": 0.5815227627754211 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "ExtraBold" + }, + "fontSize": 14, + "height": 21, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "대표이사 (CEO)", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "HEIGHT", + "textStyleId": "S:370e71e15f083213d6b6b56147987ae704a95d77,", + "textTruncation": "DISABLED", + "visible": true, + "width": 974, + "x": 0, + "y": 0, + "styledTextSegments": [ + { + "characters": "대표이사 (CEO)", + "end": 10, + "fillStyleId": "", + "fontWeight": 800, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:370e71e15f083213d6b6b56147987ae704a95d77," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "41:731": { + "id": "41:731", + "type": "TEXT", + "fields": { + "parentId": "41:729", + "absoluteBoundingBox": { + "height": 34, + "width": 974, + "x": 10765, + "y": 4839 + }, + "absoluteRenderBounds": { + "height": 21.3603515625, + "width": 59.185546875, + "x": 10766.0078125, + "y": 4845.4638671875 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:946", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "임군일", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.1529411822557449, + "g": 0.08627451211214066, + "r": 0.06666667014360428 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "ExtraBold" + }, + "fontSize": 24, + "height": 34, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 139.9999976158142 + }, + "maxHeight": null, + "maxWidth": null, + "name": "임군일", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "HEIGHT", + "textStyleId": "S:695d59044b4e4f7eea6a65d6dd08296043af8a66,", + "textTruncation": "DISABLED", + "visible": true, + "width": 974, + "x": 0, + "y": 29, + "styledTextSegments": [ + { + "characters": "임군일", + "end": 3, + "fillStyleId": "", + "fontWeight": 800, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:695d59044b4e4f7eea6a65d6dd08296043af8a66," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "41:732": { + "id": "41:732", + "type": "TEXT", + "fields": { + "parentId": "41:728", + "absoluteBoundingBox": { + "height": 161, + "width": 974, + "x": 10765, + "y": 4889 + }, + "absoluteRenderBounds": { + "height": 151.3203125, + "width": 301.94921875, + "x": 10767.7001953125, + "y": 4893.81982421875 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:348", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:40:430", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "· 동국대학교 의과대학 정형외과 교수\n· 연골재생 및 골관절염 세포치료 연구 30년\n· 세포치료제 실용화, 치료·연구자 임상 경험\n· 최근 10년 SCI 논문 120편 (피인용 100회 이상 4편)\n· 국내 특허 12건, 국제 특허 3건 등록\n· 세계골관절염연구학회장 역임 (아시아 최초)\n· 대한연골골관절염학회 설립", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:348", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.3176470696926117, + "g": 0.250980406999588, + "r": 0.21568627655506137 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Medium" + }, + "fontSize": 15, + "height": 161, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "· 동국대학교 의과대학 정형외과 교수 · 연골재생 및 골관절염 세포치료 연구 30년 · 세포치료제 실용화, 치료·연구자 임상 경험 · 최근 10년 SCI 논문 120편 (피인용 100회 이상 4편) · 국내 특허 12건, 국제 특허 3건 등록 · 세계골관절염연구학회장 역임 (아시아 최초) · 대한연골골관절염학회 설립", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "HEIGHT", + "textStyleId": "S:a2319c6931a342fd831baf32b252756b15de72a6,", + "textTruncation": "DISABLED", + "visible": true, + "width": 974, + "x": 0, + "y": 79, + "styledTextSegments": [ + { + "characters": "· 동국대학교 의과대학 정형외과 교수\n· 연골재생 및 골관절염 세포치료 연구 30년\n· 세포치료제 실용화, 치료·연구자 임상 경험\n· 최근 10년 SCI 논문 120편 (피인용 100회 이상 4편)\n· 국내 특허 12건, 국제 특허 3건 등록\n· 세계골관절염연구학회장 역임 (아시아 최초)\n· 대한연골골관절염학회 설립", + "end": 177, + "fillStyleId": "", + "fontWeight": 500, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:a2319c6931a342fd831baf32b252756b15de72a6," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "41:733": { + "id": "41:733", + "type": "COMPONENT", + "fields": { + "parentId": "41:750", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "41:727", + "41:728" + ], + "absoluteBoundingBox": { + "height": 250, + "width": 1198, + "x": 10541, + "y": 4810 + }, + "absoluteRenderBounds": { + "height": 250, + "width": 1198, + "x": 10541, + "y": 4810 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "height": 250, + "inferredAutoLayout": { + "counterAxisAlignItems": "MIN", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 24, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 24, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "type=Leader, viewport=Desktop", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "type": "Leader", + "viewport": "Desktop" + }, + "visible": true, + "width": 1198, + "x": 21, + "y": 21 + }, + "extra": {}, + "fieldErrors": {} + }, + "41:750": { + "id": "41:750", + "type": "COMPONENT_SET", + "fields": { + "parentId": "19:963", + "parentType": "SECTION", + "parentName": "Components", + "childrenIds": [ + "41:733", + "41:751", + "44:1252", + "43:1212" + ], + "absoluteBoundingBox": { + "height": 1093, + "width": 1240, + "x": 10520, + "y": 4789 + }, + "absoluteRenderBounds": { + "height": 1093, + "width": 1240, + "x": 10520, + "y": 4789 + }, + "bottomLeftRadius": 5, + "bottomRightRadius": 5, + "clipsContent": true, + "componentPropertyDefinitions": { + "type": { + "defaultValue": "Leader", + "type": "VARIANT", + "variantOptions": [ + "Leader", + "Team" + ] + }, + "viewport": { + "defaultValue": "Desktop", + "type": "VARIANT", + "variantOptions": [ + "Desktop", + "Mobile" + ] + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 5, + "dashPattern": [ + 10, + 5 + ], + "defaultVariant": { + "$nodeId": "41:733", + "$nodeType": "COMPONENT" + }, + "height": 1093, + "inferredAutoLayout": { + "counterAxisAlignItems": "MIN", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 20, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutPositioning": "AUTO", + "paddingBottom": 20, + "paddingLeft": 20, + "paddingRight": 20, + "paddingTop": 20, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 20, + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "ProfileCard", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 20, + "paddingLeft": 20, + "paddingRight": 20, + "paddingTop": 20, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 0.27843138575553894, + "r": 0.5921568870544434 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "topLeftRadius": 5, + "topRightRadius": 5, + "visible": true, + "width": 1240, + "x": 1850, + "y": 3751 + }, + "extra": {}, + "fieldErrors": {} + }, + "41:751": { + "id": "41:751", + "type": "COMPONENT", + "fields": { + "parentId": "41:750", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "41:752", + "41:754" + ], + "absoluteBoundingBox": { + "height": 364, + "width": 236, + "x": 10541, + "y": 5080 + }, + "absoluteRenderBounds": { + "height": 364, + "width": 236, + "x": 10541, + "y": 5080 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "height": 364, + "inferredAutoLayout": { + "counterAxisAlignItems": "MIN", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 16, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 16, + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "type=Team, viewport=Desktop", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "type": "Team", + "viewport": "Desktop" + }, + "visible": true, + "width": 236, + "x": 21, + "y": 291 + }, + "extra": {}, + "fieldErrors": {} + }, + "41:752": { + "id": "41:752", + "type": "RECTANGLE", + "fields": { + "parentId": "41:751", + "absoluteBoundingBox": { + "height": 295, + "width": 236, + "x": 10541, + "y": 5080 + }, + "absoluteRenderBounds": { + "height": 295, + "width": 236, + "x": 10541, + "y": 5080 + }, + "bottomLeftRadius": 12, + "bottomRightRadius": 12, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 12, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.8509804010391235, + "g": 0.8509804010391235, + "r": 0.8509804010391235 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 295, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Rectangle 3", + "opacity": 1, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "targetAspectRatio": { + "x": 4, + "y": 5 + }, + "topLeftRadius": 12, + "topRightRadius": 12, + "visible": true, + "width": 236, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "41:754": { + "id": "41:754", + "type": "FRAME", + "fields": { + "parentId": "41:751", + "childrenIds": [ + "41:756", + "41:755" + ], + "absoluteBoundingBox": { + "height": 53, + "width": 236, + "x": 10541, + "y": 5391 + }, + "absoluteRenderBounds": { + "height": 53, + "width": 236, + "x": 10541, + "y": 5391 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "height": 53, + "inferredAutoLayout": { + "counterAxisAlignItems": "MIN", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 4, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 4, + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "Frame 486", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 236, + "x": 0, + "y": 311 + }, + "extra": {}, + "fieldErrors": {} + }, + "41:755": { + "id": "41:755", + "type": "TEXT", + "fields": { + "parentId": "41:754", + "absoluteBoundingBox": { + "height": 21, + "width": 236, + "x": 10541, + "y": 5423 + }, + "absoluteRenderBounds": { + "height": 12.501953125, + "width": 89.6240234375, + "x": 10613.884765625, + "y": 5427.64599609375 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:13:153", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:956", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "대표이사 (CEO)", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:13:153", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.5160255432128906, + "g": 0.4530186951160431, + "r": 0.4333290457725525 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Bold" + }, + "fontSize": 14, + "height": 21, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "대표이사 (CEO)", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAlignHorizontal": "CENTER", + "textAutoResize": "HEIGHT", + "textStyleId": "S:44614ba166a8fa9b3003396a689f04927b273a5d,", + "textTruncation": "DISABLED", + "visible": true, + "width": 236, + "x": 0, + "y": 32, + "styledTextSegments": [ + { + "characters": "대표이사 (CEO)", + "end": 10, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:44614ba166a8fa9b3003396a689f04927b273a5d," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "41:756": { + "id": "41:756", + "type": "TEXT", + "fields": { + "parentId": "41:754", + "absoluteBoundingBox": { + "height": 28, + "width": 236, + "x": 10541, + "y": 5391 + }, + "absoluteRenderBounds": { + "height": 17.7998046875, + "width": 49.3203125, + "x": 10634.01953125, + "y": 5395.72021484375 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:950", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "임군일", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.1529411822557449, + "g": 0.08627451211214066, + "r": 0.06666667014360428 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "ExtraBold" + }, + "fontSize": 20, + "height": 28, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 139.9999976158142 + }, + "maxHeight": null, + "maxWidth": null, + "name": "임군일", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAlignHorizontal": "CENTER", + "textAutoResize": "HEIGHT", + "textStyleId": "S:0b5158f29530255590daed174bb87c3a635c314a,", + "textTruncation": "DISABLED", + "visible": true, + "width": 236, + "x": 0, + "y": 0, + "styledTextSegments": [ + { + "characters": "임군일", + "end": 3, + "fillStyleId": "", + "fontWeight": 800, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:0b5158f29530255590daed174bb87c3a635c314a," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "43:1212": { + "id": "43:1212", + "type": "COMPONENT", + "fields": { + "parentId": "41:750", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "44:1241", + "43:1218" + ], + "absoluteBoundingBox": { + "height": 277, + "width": 1198, + "x": 10541, + "y": 5584 + }, + "absoluteRenderBounds": { + "height": 277, + "width": 1198, + "x": 10541, + "y": 5584 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "height": 277, + "inferredAutoLayout": { + "counterAxisAlignItems": "MIN", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 16, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 16, + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "type=Leader, viewport=Mobile", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "type": "Leader", + "viewport": "Mobile" + }, + "visible": true, + "width": 1198, + "x": 21, + "y": 795 + }, + "extra": {}, + "fieldErrors": {} + }, + "43:1213": { + "id": "43:1213", + "type": "RECTANGLE", + "fields": { + "parentId": "44:1241", + "absoluteBoundingBox": { + "height": 100, + "width": 80, + "x": 10541, + "y": 5584 + }, + "absoluteRenderBounds": { + "height": 100, + "width": 80, + "x": 10541, + "y": 5584 + }, + "bottomLeftRadius": 8, + "bottomRightRadius": 8, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 8, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.8509804010391235, + "g": 0.8509804010391235, + "r": 0.8509804010391235 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 100, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Rectangle 3", + "opacity": 1, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "targetAspectRatio": { + "x": 4, + "y": 5 + }, + "topLeftRadius": 8, + "topRightRadius": 8, + "visible": true, + "width": 80, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "43:1215": { + "id": "43:1215", + "type": "FRAME", + "fields": { + "parentId": "44:1241", + "childrenIds": [ + "43:1216", + "43:1217" + ], + "absoluteBoundingBox": { + "height": 87, + "width": 1102, + "x": 10637, + "y": 5584 + }, + "absoluteRenderBounds": { + "height": 87, + "width": 1102, + "x": 10637, + "y": 5584 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "height": 87, + "inferredAutoLayout": { + "counterAxisAlignItems": "MIN", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 8, + "layoutAlign": "INHERIT", + "layoutGrow": 1, + "layoutMode": "VERTICAL", + "layoutPositioning": "AUTO", + "paddingBottom": 12, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 12, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 8, + "layoutGrow": 1, + "layoutMode": "VERTICAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "Frame 486", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 12, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 12, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 1102, + "x": 96, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "43:1216": { + "id": "43:1216", + "type": "TEXT", + "fields": { + "parentId": "43:1215", + "absoluteBoundingBox": { + "height": 21, + "width": 1102, + "x": 10637, + "y": 5596 + }, + "absoluteRenderBounds": { + "height": 12.6416015625, + "width": 90.509765625, + "x": 10637.76953125, + "y": 5600.576171875 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:359", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:956", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "대표이사 (CEO)", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:359", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.6682692170143127, + "g": 0.6104382276535034, + "r": 0.5815227627754211 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "ExtraBold" + }, + "fontSize": 14, + "height": 21, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "대표이사 (CEO)", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "HEIGHT", + "textStyleId": "S:370e71e15f083213d6b6b56147987ae704a95d77,", + "textTruncation": "DISABLED", + "visible": true, + "width": 1102, + "x": 0, + "y": 12, + "styledTextSegments": [ + { + "characters": "대표이사 (CEO)", + "end": 10, + "fillStyleId": "", + "fontWeight": 800, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:370e71e15f083213d6b6b56147987ae704a95d77," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "43:1217": { + "id": "43:1217", + "type": "TEXT", + "fields": { + "parentId": "43:1215", + "absoluteBoundingBox": { + "height": 34, + "width": 1102, + "x": 10637, + "y": 5625 + }, + "absoluteRenderBounds": { + "height": 21.3603515625, + "width": 59.185546875, + "x": 10638.0078125, + "y": 5631.4638671875 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:946", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "임군일", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.1529411822557449, + "g": 0.08627451211214066, + "r": 0.06666667014360428 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "ExtraBold" + }, + "fontSize": 24, + "height": 34, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 139.9999976158142 + }, + "maxHeight": null, + "maxWidth": null, + "name": "임군일", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "HEIGHT", + "textStyleId": "S:695d59044b4e4f7eea6a65d6dd08296043af8a66,", + "textTruncation": "DISABLED", + "visible": true, + "width": 1102, + "x": 0, + "y": 41, + "styledTextSegments": [ + { + "characters": "임군일", + "end": 3, + "fillStyleId": "", + "fontWeight": 800, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:695d59044b4e4f7eea6a65d6dd08296043af8a66," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "43:1218": { + "id": "43:1218", + "type": "TEXT", + "fields": { + "parentId": "43:1212", + "absoluteBoundingBox": { + "height": 161, + "width": 1198, + "x": 10541, + "y": 5700 + }, + "absoluteRenderBounds": { + "height": 151.3203125, + "width": 301.94921875, + "x": 10543.7001953125, + "y": 5704.81982421875 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:348", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:40:430", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "· 동국대학교 의과대학 정형외과 교수\n· 연골재생 및 골관절염 세포치료 연구 30년\n· 세포치료제 실용화, 치료·연구자 임상 경험\n· 최근 10년 SCI 논문 120편 (피인용 100회 이상 4편)\n· 국내 특허 12건, 국제 특허 3건 등록\n· 세계골관절염연구학회장 역임 (아시아 최초)\n· 대한연골골관절염학회 설립", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:348", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.3176470696926117, + "g": 0.250980406999588, + "r": 0.21568627655506137 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Medium" + }, + "fontSize": 15, + "height": 161, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "· 동국대학교 의과대학 정형외과 교수 · 연골재생 및 골관절염 세포치료 연구 30년 · 세포치료제 실용화, 치료·연구자 임상 경험 · 최근 10년 SCI 논문 120편 (피인용 100회 이상 4편) · 국내 특허 12건, 국제 특허 3건 등록 · 세계골관절염연구학회장 역임 (아시아 최초) · 대한연골골관절염학회 설립", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "HEIGHT", + "textStyleId": "S:a2319c6931a342fd831baf32b252756b15de72a6,", + "textTruncation": "DISABLED", + "visible": true, + "width": 1198, + "x": 0, + "y": 116, + "styledTextSegments": [ + { + "characters": "· 동국대학교 의과대학 정형외과 교수\n· 연골재생 및 골관절염 세포치료 연구 30년\n· 세포치료제 실용화, 치료·연구자 임상 경험\n· 최근 10년 SCI 논문 120편 (피인용 100회 이상 4편)\n· 국내 특허 12건, 국제 특허 3건 등록\n· 세계골관절염연구학회장 역임 (아시아 최초)\n· 대한연골골관절염학회 설립", + "end": 177, + "fillStyleId": "", + "fontWeight": 500, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:a2319c6931a342fd831baf32b252756b15de72a6," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "44:1241": { + "id": "44:1241", + "type": "FRAME", + "fields": { + "parentId": "43:1212", + "childrenIds": [ + "43:1213", + "43:1215" + ], + "absoluteBoundingBox": { + "height": 100, + "width": 1198, + "x": 10541, + "y": 5584 + }, + "absoluteRenderBounds": { + "height": 100, + "width": 1198, + "x": 10541, + "y": 5584 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "height": 100, + "inferredAutoLayout": { + "counterAxisAlignItems": "MIN", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 16, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 16, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "Frame 499", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 1198, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "44:1252": { + "id": "44:1252", + "type": "COMPONENT", + "fields": { + "parentId": "41:750", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "44:1253", + "44:1254" + ], + "absoluteBoundingBox": { + "height": 100, + "width": 236, + "x": 10541, + "y": 5464 + }, + "absoluteRenderBounds": { + "height": 100, + "width": 236, + "x": 10541, + "y": 5464 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "height": 100, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 16, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 16, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "type=Team, viewport=Mobile", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "type": "Team", + "viewport": "Mobile" + }, + "visible": true, + "width": 236, + "x": 21, + "y": 675 + }, + "extra": {}, + "fieldErrors": {} + }, + "44:1253": { + "id": "44:1253", + "type": "RECTANGLE", + "fields": { + "parentId": "44:1252", + "absoluteBoundingBox": { + "height": 100, + "width": 80, + "x": 10541, + "y": 5464 + }, + "absoluteRenderBounds": { + "height": 100, + "width": 80, + "x": 10541, + "y": 5464 + }, + "bottomLeftRadius": 8, + "bottomRightRadius": 8, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 8, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.8509804010391235, + "g": 0.8509804010391235, + "r": 0.8509804010391235 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 100, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Rectangle 3", + "opacity": 1, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "targetAspectRatio": { + "x": 4, + "y": 5 + }, + "topLeftRadius": 8, + "topRightRadius": 8, + "visible": true, + "width": 80, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "44:1254": { + "id": "44:1254", + "type": "FRAME", + "fields": { + "parentId": "44:1252", + "childrenIds": [ + "44:1255", + "44:1256" + ], + "absoluteBoundingBox": { + "height": 77, + "width": 140, + "x": 10637, + "y": 5475.5 + }, + "absoluteRenderBounds": { + "height": 77, + "width": 140, + "x": 10637, + "y": 5475.5 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "height": 77, + "inferredAutoLayout": { + "counterAxisAlignItems": "MIN", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 4, + "layoutAlign": "INHERIT", + "layoutGrow": 1, + "layoutMode": "VERTICAL", + "layoutPositioning": "AUTO", + "paddingBottom": 12, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 12, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 4, + "layoutGrow": 1, + "layoutMode": "VERTICAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "Frame 486", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 12, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 12, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 140, + "x": 96, + "y": 11.5 + }, + "extra": {}, + "fieldErrors": {} + }, + "44:1255": { + "id": "44:1255", + "type": "TEXT", + "fields": { + "parentId": "44:1254", + "absoluteBoundingBox": { + "height": 28, + "width": 140, + "x": 10637, + "y": 5487.5 + }, + "absoluteRenderBounds": { + "height": 17.7998046875, + "width": 49.3212890625, + "x": 10637.83984375, + "y": 5492.22021484375 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:950", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "임군일", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.1529411822557449, + "g": 0.08627451211214066, + "r": 0.06666667014360428 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "ExtraBold" + }, + "fontSize": 20, + "height": 28, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 139.9999976158142 + }, + "maxHeight": null, + "maxWidth": null, + "name": "임군일", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "HEIGHT", + "textStyleId": "S:0b5158f29530255590daed174bb87c3a635c314a,", + "textTruncation": "DISABLED", + "visible": true, + "width": 140, + "x": 0, + "y": 12, + "styledTextSegments": [ + { + "characters": "임군일", + "end": 3, + "fillStyleId": "", + "fontWeight": 800, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:0b5158f29530255590daed174bb87c3a635c314a," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "44:1256": { + "id": "44:1256", + "type": "TEXT", + "fields": { + "parentId": "44:1254", + "absoluteBoundingBox": { + "height": 21, + "width": 140, + "x": 10637, + "y": 5519.5 + }, + "absoluteRenderBounds": { + "height": 12.501953125, + "width": 89.6240234375, + "x": 10637.8681640625, + "y": 5524.14599609375 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:13:153", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:956", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "대표이사 (CEO)", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:13:153", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.5160255432128906, + "g": 0.4530186951160431, + "r": 0.4333290457725525 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Bold" + }, + "fontSize": 14, + "height": 21, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "대표이사 (CEO)", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "HEIGHT", + "textStyleId": "S:44614ba166a8fa9b3003396a689f04927b273a5d,", + "textTruncation": "DISABLED", + "visible": true, + "width": 140, + "x": 0, + "y": 44, + "styledTextSegments": [ + { + "characters": "대표이사 (CEO)", + "end": 10, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:44614ba166a8fa9b3003396a689f04927b273a5d," + } + ] + }, + "extra": {}, + "fieldErrors": {} + } + }, + "diagnostics": [] +} From 62b0fdc70b951fa0e1bcef82e1211ab8f04a896e Mon Sep 17 00:00:00 2001 From: leader Date: Sat, 24 Oct 2026 11:19:24 +0900 Subject: [PATCH 127/149] fix(codegen): keep single-source responsive src scalar and keep leaf src off container variant merges - one-sided responsive Image src becomes scalar src plus endpoint visibility - source splitting only applies to Image; container merges never take a leaf variant's src - guard test across all chondrogen fixtures: no scalar arrays, no src on non-Image elements --- .../changepack_log_eRTXLDvy0yMJ3YwboDGWu.json | 2 +- .../src/codegen/variant/drawn_when.rs | 4 +- .../src/codegen/variant/scalar_properties.rs | 33 +- .../src/codegen/variant/variant_projection.rs | 8 +- .../tests/chondrogen_component_typings.rs | 86 + .../tests/fixtures/chondrogen_footer_set.json | 5761 +++++ .../tests/fixtures/chondrogen_header_set.json | 18479 ++++++++++++++++ 7 files changed, 24365 insertions(+), 8 deletions(-) create mode 100644 crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_footer_set.json create mode 100644 crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_header_set.json diff --git a/.changepacks/changepack_log_eRTXLDvy0yMJ3YwboDGWu.json b/.changepacks/changepack_log_eRTXLDvy0yMJ3YwboDGWu.json index b1326be0..b252347d 100644 --- a/.changepacks/changepack_log_eRTXLDvy0yMJ3YwboDGWu.json +++ b/.changepacks/changepack_log_eRTXLDvy0yMJ3YwboDGWu.json @@ -2,6 +2,6 @@ "changes": { "Cargo.toml": "Patch" }, - "note": "componentTsx: fix button set panic, expose component text as unique props even when declared names normalize alike, drop set-canvas placement, emit undefined for absent variant values, preserve scalar typography and image sources across responsive variants, declare referenced boolean props, emit valid interaction selectors, and account explicitly for unrepresented variant children without claiming emitted property provenance", + "note": "componentTsx: fix button set panic, expose component text as unique props even when declared names normalize alike, drop set-canvas placement, emit undefined for absent variant values, preserve scalar typography and image sources across responsive variants including one-sided sources with explicit responsive visibility, prevent leaf variant image sources from leaking onto containers, declare referenced boolean props, emit valid interaction selectors, and account explicitly for unrepresented variant children without claiming emitted property provenance", "date": "2026-10-24T00:21:08.585604600Z" } diff --git a/crates/devup-mcp-devup-ui/src/codegen/variant/drawn_when.rs b/crates/devup-mcp-devup-ui/src/codegen/variant/drawn_when.rs index 37d4bfdd..5b46d969 100644 --- a/crates/devup-mcp-devup-ui/src/codegen/variant/drawn_when.rs +++ b/crates/devup-mcp-devup-ui/src/codegen/variant/drawn_when.rs @@ -104,7 +104,9 @@ fn merge_child_props( Some(&child_path), &child.props, ); - child.scalar_sources = scalar_sources(records, definitions, dimensions, viewport, effect_value, &child_path); + if child.component == "Image" { + child.scalar_sources = scalar_sources(records, definitions, dimensions, viewport, effect_value, &child_path); + } merge_child_props( child, records, diff --git a/crates/devup-mcp-devup-ui/src/codegen/variant/scalar_properties.rs b/crates/devup-mcp-devup-ui/src/codegen/variant/scalar_properties.rs index 5f607686..dd046c1c 100644 --- a/crates/devup-mcp-devup-ui/src/codegen/variant/scalar_properties.rs +++ b/crates/devup-mcp-devup-ui/src/codegen/variant/scalar_properties.rs @@ -28,7 +28,10 @@ fn scalar_sources( let tree = tree_at(&record.tree, path)?; (tree.component == "Image").then(|| tree.props.get("src").cloned()).flatten() }).collect::>(); - if sources.len() < 2 { + if sources.is_empty() { + return Vec::new(); + } + if sources.len() == 1 && expression_for_prop(records, definitions, dimensions, viewport, effect, None, path, "src").is_none_or(|expression| !has_responsive_value(&expression)) { return Vec::new(); } sources.into_iter().filter_map(|source| { @@ -43,11 +46,37 @@ fn scalar_sources( set(&mut record.tree, &source); record }).collect::>(); - let display = expression_for_prop(&visibility, definitions, dimensions, viewport, effect, None, path, "src")?; + let mut display = expression_for_prop(&visibility, definitions, dimensions, viewport, effect, None, path, "src")?; + fill_absent_display(&mut display); Some((render_expression_attr("src", &Expression::Literal(source)), render_expression_attr("display", &display))) }).collect() } +fn has_responsive_value(expression: &Expression) -> bool { + match expression { + Expression::Responsive(_) => true, + Expression::Variant(_, values) => values.iter().any(|(_, value)| has_responsive_value(value)), + Expression::Undefined | Expression::Literal(_) | Expression::Conditional(_, _, _) => false, + } +} + +fn fill_absent_display(expression: &mut Expression) { + match expression { + Expression::Responsive(slots) => { + for index in [0, slots.len().saturating_sub(1)] { + if let Some(slot) = slots.get_mut(index) { + slot.get_or_insert_with(|| "none".into()); + } + } + } + Expression::Variant(_, values) => { + for (_, value) in values { fill_absent_display(value); } + } + Expression::Undefined => *expression = Expression::Literal("none".into()), + Expression::Literal(_) | Expression::Conditional(_, _, _) => {} + } +} + fn effect_selector(value: &str) -> Option<&str> { match value.to_ascii_lowercase().as_str() { "default" => Some("&"), diff --git a/crates/devup-mcp-devup-ui/src/codegen/variant/variant_projection.rs b/crates/devup-mcp-devup-ui/src/codegen/variant/variant_projection.rs index 920b132b..fa348ab4 100644 --- a/crates/devup-mcp-devup-ui/src/codegen/variant/variant_projection.rs +++ b/crates/devup-mcp-devup-ui/src/codegen/variant/variant_projection.rs @@ -4,13 +4,11 @@ use devup_mcp_figma::{ DevupError, Diagnostic, DiagnosticSeverity, ErrorCode, FidelityImpact, RawNode, Snapshot, }; use serde_json::Value; - use super::{ component::{CodegenOptions, CodegenOutput, PropValue, legacy_component_name}, layout, style, text, }; use crate::provenance::mark_node; - #[derive(Clone, Debug)] pub(super) struct Tree { pub node_id: String, @@ -39,7 +37,6 @@ pub(super) struct Tree { pub slots: Vec)>>, pub scalar_sources: Vec<(String, String)>, } - #[derive(Clone, Debug)] struct Record { values: BTreeMap, @@ -210,7 +207,9 @@ pub(super) fn generate_variant_component_set( None, &default.tree.props, ); - root.scalar_sources = scalar_sources(&records, &definitions, &dimensions, viewport, effect.map_or("default", |definition| definition.default.as_str()), &[]); + if root.component == "Image" { + root.scalar_sources = scalar_sources(&records, &definitions, &dimensions, viewport, effect.map_or("default", |definition| definition.default.as_str()), &[]); + } merge_child_props( &mut root, &records, @@ -831,6 +830,7 @@ fn merged_props( } } keys.into_iter() + .filter(|prop| prop != "src" || defaults.contains_key("src")) .filter_map(|prop| { let expression = expression_for_prop( records, diff --git a/crates/devup-mcp-devup-ui/tests/chondrogen_component_typings.rs b/crates/devup-mcp-devup-ui/tests/chondrogen_component_typings.rs index 755c0b17..6fe80ebe 100644 --- a/crates/devup-mcp-devup-ui/tests/chondrogen_component_typings.rs +++ b/crates/devup-mcp-devup-ui/tests/chondrogen_component_typings.rs @@ -73,3 +73,89 @@ fn icon_effects_never_emit_attributes_inside_style_selectors() { assert!(!tsx.contains("_hoevr="), "{tsx}"); assert!(!tsx.contains("\"src\":"), "{tsx}"); } + +#[test] +fn desktop_only_footer_logo_has_scalar_source_and_explicit_visibility() { + let tsx = generate(include_str!("fixtures/chondrogen_footer_set.json")); + assert!(!tsx.contains("src={["), "{tsx}"); + let image = tsx + .split("") + .next() + .unwrap(); + assert!(image.contains("src=\"/icons/logo.svg\""), "{image}"); + assert!( + image.contains("display={[\"none\", null, null, null, \"inline\"]}"), + "{image}" + ); + assert!(!tsx.contains("typography={["), "{tsx}"); +} + +#[test] +fn header_container_does_not_inherit_a_leaf_variants_source() { + let tsx = generate(include_str!("fixtures/chondrogen_header_set.json")); + let root = tsx + .split("').next().unwrap_or_default(); + if attributes + .lines() + .any(|line| line.trim_start().starts_with("src=")) + { + assert_eq!(tag, "Image", "{name}: {attributes}"); + assert!(attributes.trim_end().ends_with('/'), "{name}: {attributes}"); + } + } + } +} diff --git a/crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_footer_set.json b/crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_footer_set.json new file mode 100644 index 00000000..abbe100f --- /dev/null +++ b/crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_footer_set.json @@ -0,0 +1,5761 @@ +{ + "fileKey": "K2kUnxGgvooAJwCQiVTb7g", + "version": null, + "roots": [ + "30:1118" + ], + "nodes": { + "30:1118": { + "id": "30:1118", + "type": "COMPONENT_SET", + "fields": { + "parentId": "19:963", + "parentType": "SECTION", + "parentName": "Components", + "childrenIds": [ + "30:1119", + "43:1187" + ], + "absoluteBoundingBox": { + "height": 615, + "width": 1686, + "x": 8782, + "y": 3714 + }, + "absoluteRenderBounds": { + "height": 615, + "width": 1686, + "x": 8782, + "y": 3714 + }, + "bottomLeftRadius": 5, + "bottomRightRadius": 5, + "clipsContent": true, + "componentPropertyDefinitions": { + "viewport": { + "defaultValue": "Desktop", + "type": "VARIANT", + "variantOptions": [ + "Desktop", + "Mobile" + ] + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 5, + "dashPattern": [ + 10, + 5 + ], + "defaultVariant": { + "$nodeId": "30:1119", + "$nodeType": "COMPONENT" + }, + "height": 615, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Footer", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 0.27843138575553894, + "r": 0.5921568870544434 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "topLeftRadius": 5, + "topRightRadius": 5, + "visible": true, + "width": 1686, + "x": 112, + "y": 2676 + }, + "extra": {}, + "fieldErrors": {} + }, + "30:1119": { + "id": "30:1119", + "type": "COMPONENT", + "fields": { + "parentId": "30:1118", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "30:1120" + ], + "absoluteBoundingBox": { + "height": 181, + "width": 1600, + "x": 8802, + "y": 3734 + }, + "absoluteRenderBounds": { + "height": 181, + "width": 1600, + "x": 8802, + "y": 3734 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:349", + "type": "VARIABLE_ALIAS" + } + ], + "paddingLeft": { + "id": "VariableID:30:628", + "type": "VARIABLE_ALIAS" + }, + "paddingRight": { + "id": "VariableID:30:628", + "type": "VARIABLE_ALIAS" + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:349", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.9843137264251708, + "g": 0.9803921580314636, + "r": 0.9764705896377563 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 181, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 10, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 48, + "paddingLeft": 40, + "paddingRight": 40, + "paddingTop": 48, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 10, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "viewport=Desktop", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 48, + "paddingLeft": 40, + "paddingRight": 40, + "paddingTop": 48, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 0, + "strokeLeftWeight": 0, + "strokeRightWeight": 0, + "strokeTopWeight": 1, + "strokeWeight": { + "$unsupported": "symbol" + }, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "viewport": "Desktop" + }, + "visible": true, + "width": 1600, + "x": 20, + "y": 20 + }, + "extra": {}, + "fieldErrors": {} + }, + "30:1120": { + "id": "30:1120", + "type": "FRAME", + "fields": { + "parentId": "30:1119", + "childrenIds": [ + "41:901", + "41:914" + ], + "absoluteBoundingBox": { + "height": 85, + "width": 1280, + "x": 8962, + "y": 3782 + }, + "absoluteRenderBounds": { + "height": 85, + "width": 1280, + "x": 8962, + "y": 3782 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "height": 85, + "inferredAutoLayout": { + "counterAxisAlignItems": "MIN", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 24, + "layoutAlign": "INHERIT", + "layoutGrow": 1, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 24, + "layoutGrow": 1, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": 1280, + "name": "Frame 15", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 1280, + "x": 160, + "y": 48 + }, + "extra": {}, + "fieldErrors": {} + }, + "30:1122": { + "id": "30:1122", + "type": "FRAME", + "fields": { + "parentId": "41:914", + "childrenIds": [ + "30:1123", + "30:1124", + "73:10999" + ], + "absoluteBoundingBox": { + "height": 24, + "width": 260, + "x": 9209, + "y": 3782 + }, + "absoluteRenderBounds": { + "height": 24, + "width": 260, + "x": 9209, + "y": 3782 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "height": 24, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 16, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 16, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "Frame 25", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 260, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "30:1123": { + "id": "30:1123", + "type": "INSTANCE", + "fields": { + "parentId": "30:1122", + "childrenIds": [ + "I30:1123;15:2101" + ], + "absoluteBoundingBox": { + "height": 24, + "width": 114, + "x": 9209, + "y": 3782 + }, + "absoluteRenderBounds": { + "height": 24, + "width": 114, + "x": 9209, + "y": 3782 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": false + } + ], + "height": 24, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 0, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "TextButton", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 114, + "x": 0, + "y": 0, + "mainComponentId": "30:1108", + "mainComponentName": "속성 1=기본", + "mainComponentSetId": "30:1107" + }, + "extra": {}, + "fieldErrors": { + "componentProperties": "in get_componentProperties: Component set for node has existing errors", + "variantProperties": "in get_variantProperties: Component set for node has existing errors" + } + }, + "30:1124": { + "id": "30:1124", + "type": "TEXT", + "fields": { + "parentId": "30:1122", + "absoluteBoundingBox": { + "height": 24, + "width": 55, + "x": 9339, + "y": 3782 + }, + "absoluteRenderBounds": { + "height": 14.031982421875, + "width": 53.392578125, + "x": 9340.15234375, + "y": 3787.152099609375 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:13:153", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:955", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "이용약관", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:13:153", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.5160255432128906, + "g": 0.4530186951160431, + "r": 0.4333290457725525 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Regular" + }, + "fontSize": 16, + "height": 24, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "이용약관", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:e4c7b7dacbd4e73f6c967cfc38e87b24df7d8775,", + "textTruncation": "DISABLED", + "visible": true, + "width": 55, + "x": 130, + "y": 0, + "styledTextSegments": [ + { + "characters": "이용약관", + "end": 4, + "fillStyleId": "", + "fontWeight": 400, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:e4c7b7dacbd4e73f6c967cfc38e87b24df7d8775," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "30:1126": { + "id": "30:1126", + "type": "TEXT", + "fields": { + "parentId": "41:926", + "absoluteBoundingBox": { + "height": 18, + "width": 228, + "x": 9209, + "y": 3849 + }, + "absoluteRenderBounds": { + "height": 12.16796875, + "width": 225.5576171875, + "x": 9210.1396484375, + "y": 3852.424072265625 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:13:153", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "© 2026 ChondroGen Inc. All rights reserved.", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:13:153", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.5160255432128906, + "g": 0.4530186951160431, + "r": 0.4333290457725525 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Medium" + }, + "fontSize": 12, + "height": 18, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "© 2026 ChondroGen Inc. All rights reserved.", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:695b4683d717713bed12afa28c899d6ea1758b53,", + "textTruncation": "DISABLED", + "visible": true, + "width": 228, + "x": 0, + "y": 27, + "styledTextSegments": [ + { + "characters": "© 2026 ChondroGen Inc. All rights reserved.", + "end": 43, + "fillStyleId": "", + "fontWeight": 500, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:695b4683d717713bed12afa28c899d6ea1758b53," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "41:901": { + "id": "41:901", + "type": "INSTANCE", + "fields": { + "parentId": "30:1120", + "childrenIds": [ + "I41:901;136:19765", + "I41:901;136:19766", + "I41:901;136:19779" + ], + "absoluteBoundingBox": { + "height": 36, + "width": 223, + "x": 8962, + "y": 3782 + }, + "absoluteRenderBounds": { + "height": 36, + "width": 223, + "x": 8962, + "y": 3782 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "componentProperties": { + "속성 1": { + "boundVariables": {}, + "type": "VARIANT", + "value": "gray" + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "height": 36, + "isAsset": true, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Logo", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "targetAspectRatio": { + "x": 1102, + "y": 178 + }, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "속성 1": "gray" + }, + "visible": true, + "width": 223, + "x": 0, + "y": 0, + "mainComponentId": "136:19764", + "mainComponentName": "속성 1=gray", + "mainComponentSetId": "136:19763" + }, + "extra": {}, + "fieldErrors": {} + }, + "41:908": { + "id": "41:908", + "type": "TEXT", + "fields": { + "parentId": "41:926", + "absoluteBoundingBox": { + "height": 21, + "width": 1033, + "x": 9209, + "y": 3822 + }, + "absoluteRenderBounds": { + "height": 14.3359375, + "width": 701.615234375, + "x": 9209.5595703125, + "y": 3826.64599609375 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:13:153", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:956", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "주식회사 콘드로젠 · 대표자 임군일 · 경기 고양시 일산동구 동국로 32 동국대학교 산학협력관 733호 · contact@chondrogen.kr", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:13:153", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.5160255432128906, + "g": 0.4530186951160431, + "r": 0.4333290457725525 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Regular" + }, + "fontSize": 14, + "height": 21, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "주식회사 콘드로젠 · 대표자 임군일 · 경기 고양시 일산동구 동국로 32 동국대학교 산학협력관 733호 · contact@chondrogen.kr", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "HEIGHT", + "textStyleId": "S:b24dbe253b56eab7caafdd82199d1cfb1188d02f,", + "textTruncation": "DISABLED", + "visible": true, + "width": 1033, + "x": 0, + "y": 0, + "styledTextSegments": [ + { + "characters": "주식회사 콘드로젠 · 대표자 임군일 · 경기 고양시 일산동구 동국로 32 동국대학교 산학협력관 733호 · contact@chondrogen.kr", + "end": 87, + "fillStyleId": "", + "fontWeight": 400, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:b24dbe253b56eab7caafdd82199d1cfb1188d02f," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "41:914": { + "id": "41:914", + "type": "FRAME", + "fields": { + "parentId": "30:1120", + "childrenIds": [ + "30:1122", + "41:926" + ], + "absoluteBoundingBox": { + "height": 85, + "width": 1033, + "x": 9209, + "y": 3782 + }, + "absoluteRenderBounds": { + "height": 85, + "width": 1033, + "x": 9209, + "y": 3782 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "height": 85, + "inferredAutoLayout": { + "counterAxisAlignItems": "MIN", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 16, + "layoutAlign": "INHERIT", + "layoutGrow": 1, + "layoutMode": "VERTICAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 16, + "layoutGrow": 1, + "layoutMode": "VERTICAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "Frame 496", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 1033, + "x": 247, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "41:926": { + "id": "41:926", + "type": "FRAME", + "fields": { + "parentId": "41:914", + "childrenIds": [ + "41:908", + "30:1126" + ], + "absoluteBoundingBox": { + "height": 45, + "width": 1033, + "x": 9209, + "y": 3822 + }, + "absoluteRenderBounds": { + "height": 45, + "width": 1033, + "x": 9209, + "y": 3822 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "height": 45, + "inferredAutoLayout": { + "counterAxisAlignItems": "MIN", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 6, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 6, + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "Frame 497", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 1033, + "x": 0, + "y": 40 + }, + "extra": {}, + "fieldErrors": {} + }, + "43:1187": { + "id": "43:1187", + "type": "COMPONENT", + "fields": { + "parentId": "30:1118", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "43:1188" + ], + "absoluteBoundingBox": { + "height": 247, + "width": 360, + "x": 8802, + "y": 3976 + }, + "absoluteRenderBounds": { + "height": 247, + "width": 360, + "x": 8802, + "y": 3976 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:349", + "type": "VARIABLE_ALIAS" + } + ], + "paddingLeft": { + "id": "VariableID:30:628", + "type": "VARIABLE_ALIAS" + }, + "paddingRight": { + "id": "VariableID:30:628", + "type": "VARIABLE_ALIAS" + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:349", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.9843137264251708, + "g": 0.9803921580314636, + "r": 0.9764705896377563 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 247, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 10, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 36, + "paddingLeft": 20, + "paddingRight": 20, + "paddingTop": 36, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 10, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "viewport=Mobile", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 36, + "paddingLeft": 20, + "paddingRight": 20, + "paddingTop": 36, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 0, + "strokeLeftWeight": 0, + "strokeRightWeight": 0, + "strokeTopWeight": 1, + "strokeWeight": { + "$unsupported": "symbol" + }, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "viewport": "Mobile" + }, + "visible": true, + "width": 360, + "x": 20, + "y": 262 + }, + "extra": {}, + "fieldErrors": {} + }, + "43:1188": { + "id": "43:1188", + "type": "FRAME", + "fields": { + "parentId": "43:1187", + "childrenIds": [ + "43:1189", + "43:1190" + ], + "absoluteBoundingBox": { + "height": 175, + "width": 320, + "x": 8822, + "y": 4012 + }, + "absoluteRenderBounds": { + "height": 175, + "width": 320, + "x": 8822, + "y": 4012 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "height": 175, + "inferredAutoLayout": { + "counterAxisAlignItems": "MIN", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 16, + "layoutAlign": "INHERIT", + "layoutGrow": 1, + "layoutMode": "VERTICAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 16, + "layoutGrow": 1, + "layoutMode": "VERTICAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": 1280, + "name": "Frame 15", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 320, + "x": 20, + "y": 36 + }, + "extra": {}, + "fieldErrors": {} + }, + "43:1189": { + "id": "43:1189", + "type": "INSTANCE", + "fields": { + "parentId": "43:1188", + "childrenIds": [ + "I43:1189;136:19765", + "I43:1189;136:19766", + "I43:1189;136:19779" + ], + "absoluteBoundingBox": { + "height": 36, + "width": 223, + "x": 8822, + "y": 4012 + }, + "absoluteRenderBounds": { + "height": 36, + "width": 223, + "x": 8822, + "y": 4012 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "componentProperties": { + "속성 1": { + "boundVariables": {}, + "type": "VARIANT", + "value": "gray" + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "height": 36, + "isAsset": true, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Logo", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "targetAspectRatio": { + "x": 1102, + "y": 178 + }, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "속성 1": "gray" + }, + "visible": true, + "width": 223, + "x": 0, + "y": 0, + "mainComponentId": "136:19764", + "mainComponentName": "속성 1=gray", + "mainComponentSetId": "136:19763" + }, + "extra": {}, + "fieldErrors": {} + }, + "43:1190": { + "id": "43:1190", + "type": "FRAME", + "fields": { + "parentId": "43:1188", + "childrenIds": [ + "43:1191", + "43:1194" + ], + "absoluteBoundingBox": { + "height": 123, + "width": 320, + "x": 8822, + "y": 4064 + }, + "absoluteRenderBounds": { + "height": 123, + "width": 320, + "x": 8822, + "y": 4064 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "height": 123, + "inferredAutoLayout": { + "counterAxisAlignItems": "MIN", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 16, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 16, + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "Frame 496", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 320, + "x": 0, + "y": 52 + }, + "extra": {}, + "fieldErrors": {} + }, + "43:1191": { + "id": "43:1191", + "type": "FRAME", + "fields": { + "parentId": "43:1190", + "childrenIds": [ + "43:1192", + "43:1193", + "73:11036" + ], + "absoluteBoundingBox": { + "height": 23, + "width": 245, + "x": 8822, + "y": 4064 + }, + "absoluteRenderBounds": { + "height": 23, + "width": 245, + "x": 8822, + "y": 4064 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "height": 23, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 16, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 16, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "Frame 25", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 245, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "43:1192": { + "id": "43:1192", + "type": "INSTANCE", + "fields": { + "parentId": "43:1191", + "childrenIds": [ + "I43:1192;15:2101" + ], + "absoluteBoundingBox": { + "height": 23, + "width": 106, + "x": 8822, + "y": 4064 + }, + "absoluteRenderBounds": { + "height": 23, + "width": 106, + "x": 8822, + "y": 4064 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": false + } + ], + "height": 23, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 0, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "TextButton", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 106, + "x": 0, + "y": 0, + "mainComponentId": "30:1108", + "mainComponentName": "속성 1=기본", + "mainComponentSetId": "30:1107" + }, + "extra": {}, + "fieldErrors": { + "componentProperties": "in get_componentProperties: Component set for node has existing errors", + "variantProperties": "in get_variantProperties: Component set for node has existing errors" + } + }, + "43:1193": { + "id": "43:1193", + "type": "TEXT", + "fields": { + "parentId": "43:1191", + "absoluteBoundingBox": { + "height": 23, + "width": 52, + "x": 8944, + "y": 4064 + }, + "absoluteRenderBounds": { + "height": 13.155029296875, + "width": 50.0556640625, + "x": 8945.080078125, + "y": 4068.955078125 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:13:153", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:955", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "이용약관", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:13:153", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.5160255432128906, + "g": 0.4530186951160431, + "r": 0.4333290457725525 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Regular" + }, + "fontSize": 15, + "height": 23, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "이용약관", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:e4c7b7dacbd4e73f6c967cfc38e87b24df7d8775,", + "textTruncation": "DISABLED", + "visible": true, + "width": 52, + "x": 122, + "y": 0, + "styledTextSegments": [ + { + "characters": "이용약관", + "end": 4, + "fillStyleId": "", + "fontWeight": 400, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:e4c7b7dacbd4e73f6c967cfc38e87b24df7d8775," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "43:1194": { + "id": "43:1194", + "type": "FRAME", + "fields": { + "parentId": "43:1190", + "childrenIds": [ + "43:1195", + "43:1196" + ], + "absoluteBoundingBox": { + "height": 84, + "width": 320, + "x": 8822, + "y": 4103 + }, + "absoluteRenderBounds": { + "height": 84, + "width": 320, + "x": 8822, + "y": 4103 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "height": 84, + "inferredAutoLayout": { + "counterAxisAlignItems": "MIN", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 6, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 6, + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "Frame 497", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 320, + "x": 0, + "y": 39 + }, + "extra": {}, + "fieldErrors": {} + }, + "43:1195": { + "id": "43:1195", + "type": "TEXT", + "fields": { + "parentId": "43:1194", + "absoluteBoundingBox": { + "height": 60, + "width": 320, + "x": 8822, + "y": 4103 + }, + "absoluteRenderBounds": { + "height": 53.2080078125, + "width": 310.5810546875, + "x": 8822.4814453125, + "y": 4107.56103515625 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:13:153", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:956", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "주식회사 콘드로젠 · 대표자 임군일 · 경기 고양시 일산동구 동국로 32 동국대학교 산학협력관 733호 · contact@chondrogen.kr", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:13:153", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.5160255432128906, + "g": 0.4530186951160431, + "r": 0.4333290457725525 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Regular" + }, + "fontSize": 13, + "height": 60, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "주식회사 콘드로젠 · 대표자 임군일 · 경기 고양시 일산동구 동국로 32 동국대학교 산학협력관 733호 · contact@chondrogen.kr", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "HEIGHT", + "textStyleId": "S:b24dbe253b56eab7caafdd82199d1cfb1188d02f,", + "textTruncation": "DISABLED", + "visible": true, + "width": 320, + "x": 0, + "y": 0, + "styledTextSegments": [ + { + "characters": "주식회사 콘드로젠 · 대표자 임군일 · 경기 고양시 일산동구 동국로 32 동국대학교 산학협력관 733호 · contact@chondrogen.kr", + "end": 87, + "fillStyleId": "", + "fontWeight": 400, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:b24dbe253b56eab7caafdd82199d1cfb1188d02f," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "43:1196": { + "id": "43:1196", + "type": "TEXT", + "fields": { + "parentId": "43:1194", + "absoluteBoundingBox": { + "height": 18, + "width": 228, + "x": 8822, + "y": 4169 + }, + "absoluteRenderBounds": { + "height": 12.16796875, + "width": 225.5576171875, + "x": 8823.1396484375, + "y": 4172.423828125 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:13:153", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "© 2026 ChondroGen Inc. All rights reserved.", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:13:153", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.5160255432128906, + "g": 0.4530186951160431, + "r": 0.4333290457725525 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Medium" + }, + "fontSize": 12, + "height": 18, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "© 2026 ChondroGen Inc. All rights reserved.", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:695b4683d717713bed12afa28c899d6ea1758b53,", + "textTruncation": "DISABLED", + "visible": true, + "width": 228, + "x": 0, + "y": 66, + "styledTextSegments": [ + { + "characters": "© 2026 ChondroGen Inc. All rights reserved.", + "end": 43, + "fillStyleId": "", + "fontWeight": 500, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:695b4683d717713bed12afa28c899d6ea1758b53," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "73:10999": { + "id": "73:10999", + "type": "TEXT", + "fields": { + "parentId": "30:1122", + "absoluteBoundingBox": { + "height": 24, + "width": 59, + "x": 9410, + "y": 3782 + }, + "absoluteRenderBounds": { + "height": 14.031982421875, + "width": 56.296875, + "x": 9410.6396484375, + "y": 3787.152099609375 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:13:153", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:955", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "오시는 길", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:13:153", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.5160255432128906, + "g": 0.4530186951160431, + "r": 0.4333290457725525 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Regular" + }, + "fontSize": 16, + "height": 24, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "오시는 길", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:e4c7b7dacbd4e73f6c967cfc38e87b24df7d8775,", + "textTruncation": "DISABLED", + "visible": true, + "width": 59, + "x": 201, + "y": 0, + "styledTextSegments": [ + { + "characters": "오시는 길", + "end": 5, + "fillStyleId": "", + "fontWeight": 400, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:e4c7b7dacbd4e73f6c967cfc38e87b24df7d8775," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "73:11036": { + "id": "73:11036", + "type": "TEXT", + "fields": { + "parentId": "43:1191", + "absoluteBoundingBox": { + "height": 23, + "width": 55, + "x": 9012, + "y": 4064 + }, + "absoluteRenderBounds": { + "height": 13.155029296875, + "width": 52.7783203125, + "x": 9012.599609375, + "y": 4068.955078125 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:13:153", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:955", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "오시는 길", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:13:153", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.5160255432128906, + "g": 0.4530186951160431, + "r": 0.4333290457725525 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Regular" + }, + "fontSize": 15, + "height": 23, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "오시는 길", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:e4c7b7dacbd4e73f6c967cfc38e87b24df7d8775,", + "textTruncation": "DISABLED", + "visible": true, + "width": 55, + "x": 190, + "y": 0, + "styledTextSegments": [ + { + "characters": "오시는 길", + "end": 5, + "fillStyleId": "", + "fontWeight": 400, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:e4c7b7dacbd4e73f6c967cfc38e87b24df7d8775," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "I30:1123;15:2101": { + "id": "I30:1123;15:2101", + "type": "TEXT", + "fields": { + "parentId": "30:1123", + "absoluteBoundingBox": { + "height": 24, + "width": 114, + "x": 9209, + "y": 3782 + }, + "absoluteRenderBounds": { + "height": 14.320068359375, + "width": 110.8896484375, + "x": 9209.416015625, + "y": 3786.991943359375 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:13:153", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:955", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "개인정보 처리방침", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:13:153", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.5160255432128906, + "g": 0.4530186951160431, + "r": 0.4333290457725525 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Bold" + }, + "fontSize": 16, + "height": 24, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "텍스트", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:cbc02b7831f02c3b00e92d3114823bdda982464e,", + "textTruncation": "DISABLED", + "visible": true, + "width": 114, + "x": 0, + "y": 0, + "styledTextSegments": [ + { + "characters": "개인정보 처리방침", + "end": 9, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:cbc02b7831f02c3b00e92d3114823bdda982464e," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "I41:901;136:19765": { + "id": "I41:901;136:19765", + "type": "VECTOR", + "fields": { + "parentId": "41:901", + "absoluteBoundingBox": { + "height": 36, + "width": 36.019962310791016, + "x": 8962, + "y": 3782 + }, + "absoluteRenderBounds": { + "height": 36, + "width": 36.01953125, + "x": 8962, + "y": 3782 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.7843137383460999, + "g": 0.7686274647712708, + "r": 0.7490196228027344 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.7647058963775635, + "g": 0.7490196228027344, + "r": 0.729411780834198 + }, + "position": 0.30000001192092896 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.729411780834198, + "g": 0.7137255072593689, + "r": 0.6941176652908325 + }, + "position": 0.44999998807907104 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.7019608020782471, + "g": 0.686274528503418, + "r": 0.6666666865348816 + }, + "position": 0.6100000143051147 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.686274528503418, + "g": 0.6705882549285889, + "r": 0.6509804129600525 + }, + "position": 0.7599999904632568 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.6823529601097107, + "g": 0.6666666865348816, + "r": 0.6470588445663452 + }, + "position": 0.9100000262260436 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.6823529601097107, + "g": 0.6666666865348816, + "r": 0.6470588445663452 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 0.6454866528511047, + 0, + 0.31524205207824707 + ], + [ + 0, + 0.6454866528511047, + 0.204462930560112 + ] + ], + "opacity": 1, + "type": "GRADIENT_RADIAL", + "visible": true + } + ], + "height": 36, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "sphere", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 36.019962310791016, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "I41:901;136:19766": { + "id": "I41:901;136:19766", + "type": "GROUP", + "fields": { + "parentId": "41:901", + "childrenIds": [ + "I41:901;136:19767", + "I41:901;136:19769" + ], + "absoluteBoundingBox": { + "height": 36, + "width": 36.019962310791016, + "x": 8962, + "y": 3782 + }, + "absoluteRenderBounds": { + "height": 36, + "width": 36.019962310791016, + "x": 8962, + "y": 3782 + }, + "height": 36, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Clip path group", + "opacity": 1, + "visible": true, + "width": 36.019962310791016, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "I41:901;136:19767": { + "id": "I41:901;136:19767", + "type": "GROUP", + "fields": { + "parentId": "I41:901;136:19766", + "childrenIds": [ + "I41:901;136:19768" + ], + "absoluteBoundingBox": { + "height": 36, + "width": 36.019962310791016, + "x": 8962, + "y": 3782 + }, + "absoluteRenderBounds": null, + "height": 36, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "isMask": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "sphere-clip", + "opacity": 1, + "visible": true, + "width": 36.019962310791016, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "I41:901;136:19768": { + "id": "I41:901;136:19768", + "type": "VECTOR", + "fields": { + "parentId": "I41:901;136:19767", + "absoluteBoundingBox": { + "height": 36, + "width": 36.019962310791016, + "x": 8962, + "y": 3782 + }, + "absoluteRenderBounds": { + "height": 36, + "width": 36.01953125, + "x": 8962, + "y": 3782 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0, + "g": 0, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 36, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 36.019962310791016, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "I41:901;136:19769": { + "id": "I41:901;136:19769", + "type": "GROUP", + "fields": { + "parentId": "I41:901;136:19766", + "childrenIds": [ + "I41:901;136:19770", + "I41:901;136:19771", + "I41:901;136:19772", + "I41:901;136:19773", + "I41:901;136:19774", + "I41:901;136:19775", + "I41:901;136:19776", + "I41:901;136:19777", + "I41:901;136:19778" + ], + "absoluteBoundingBox": { + "height": 36, + "width": 36.019962310791016, + "x": 8962, + "y": 3782 + }, + "absoluteRenderBounds": { + "height": 36, + "width": 36.01953125, + "x": 8962, + "y": 3782 + }, + "height": 36, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Group", + "opacity": 1, + "visible": true, + "width": 36.019962310791016, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "I41:901;136:19770": { + "id": "I41:901;136:19770", + "type": "VECTOR", + "fields": { + "parentId": "I41:901;136:19769", + "absoluteBoundingBox": { + "height": 36, + "width": 36.019962310791016, + "x": 8962, + "y": 3782 + }, + "absoluteRenderBounds": { + "height": 36, + "width": 36.01953125, + "x": 8962, + "y": 3782 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 0.41999998688697815, + "b": 0.6705882549285889, + "g": 0.6549019813537598, + "r": 0.6352941393852234 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 0.20000000298023224, + "b": 0.6705882549285889, + "g": 0.6549019813537598, + "r": 0.6352941393852234 + }, + "position": 0.550000011920929 + }, + { + "boundVariables": {}, + "color": { + "a": 0, + "b": 0.6705882549285889, + "g": 0.6549019813537598, + "r": 0.6352941393852234 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 1.252997875213623, + 0, + 0.08783221244812012 + ], + [ + 0, + 1.252997875213623, + -0.7154324054718018 + ] + ], + "opacity": 1, + "type": "GRADIENT_RADIAL", + "visible": true + } + ], + "height": 36, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "shade-outside-orbit", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 36.019962310791016, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "I41:901;136:19771": { + "id": "I41:901;136:19771", + "type": "VECTOR", + "fields": { + "parentId": "I41:901;136:19769", + "absoluteBoundingBox": { + "height": 36, + "width": 36.019962310791016, + "x": 8962, + "y": 3782 + }, + "absoluteRenderBounds": { + "height": 36, + "width": 36.01953125, + "x": 8962, + "y": 3782 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 0.14000000059604645, + "b": 0.8156862854957581, + "g": 0.800000011920929, + "r": 0.7803921699523926 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 0, + "b": 0.8156862854957581, + "g": 0.800000011920929, + "r": 0.7803921699523926 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 1.9364598989486697, + 0, + 0.468456894159317 + ], + [ + 0, + 1.9364598989486697, + -0.5502269268035889 + ] + ], + "opacity": 1, + "type": "GRADIENT_RADIAL", + "visible": true + } + ], + "height": 36, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "light-left-overlay", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 36.019962310791016, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "I41:901;136:19772": { + "id": "I41:901;136:19772", + "type": "VECTOR", + "fields": { + "parentId": "I41:901;136:19769", + "absoluteBoundingBox": { + "height": 7.538562297821045, + "width": 14.69009494781494, + "x": 8981.4521484375, + "y": 3807.662353515625 + }, + "absoluteRenderBounds": { + "height": 7.53857421875, + "width": 14.6904296875, + "x": 8981.4521484375, + "y": 3807.662353515625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 0, + "b": 0.729411780834198, + "g": 0.7137255072593689, + "r": 0.6941176652908325 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 0.550000011920929, + "b": 0.729411780834198, + "g": 0.7137255072593689, + "r": 0.6941176652908325 + }, + "position": 0.30000001192092896 + }, + { + "boundVariables": {}, + "color": { + "a": 0.5, + "b": 0.729411780834198, + "g": 0.7137255072593689, + "r": 0.6941176652908325 + }, + "position": 0.75 + }, + { + "boundVariables": {}, + "color": { + "a": 0, + "b": 0.729411780834198, + "g": 0.7137255072593689, + "r": 0.6941176652908325 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 0.8151227831840515, + -0.2092042565345764, + 0.19614684581756592 + ], + [ + 0.4074420928955078, + 0.41853103041648865, + 0.08879981189966202 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 7.538562297821045, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "rim-highlight (Stroke)", + "opacity": 1, + "strokeAlign": "CENTER", + "strokeWeight": 3.849999904632568, + "visible": true, + "width": 14.69009494781494, + "x": 19.45182228088379, + "y": 25.662315368652344 + }, + "extra": {}, + "fieldErrors": {} + }, + "I41:901;136:19773": { + "id": "I41:901;136:19773", + "type": "VECTOR", + "fields": { + "parentId": "I41:901;136:19769", + "absoluteBoundingBox": { + "height": 20.55927467346191, + "width": 20.555435180664062, + "x": 8966.123046875, + "y": 3789.4462890625 + }, + "absoluteRenderBounds": { + "height": 20.559326171875, + "width": 20.5556640625, + "x": 8966.123046875, + "y": 3789.4462890625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.8784313797950745, + "g": 0.8627451062202454, + "r": 0.843137264251709 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.7803921699523926, + "g": 0.7647058963775635, + "r": 0.7450980544090271 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 0.8613659739494324, + 0.45280781388282776, + -0.1629999577999115 + ], + [ + -0.4524722993373871, + 0.8620046973228455, + 0.31514695286750793 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 20.55927467346191, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "core", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 20.555435180664062, + "x": 4.122681140899658, + "y": 7.446366786956787 + }, + "extra": {}, + "fieldErrors": {} + }, + "I41:901;136:19774": { + "id": "I41:901;136:19774", + "type": "VECTOR", + "fields": { + "parentId": "I41:901;136:19769", + "absoluteBoundingBox": { + "height": 12.231562614440918, + "width": 12.2383451461792, + "x": 8969.732421875, + "y": 3793.64013671875 + }, + "absoluteRenderBounds": { + "height": 12.2314453125, + "width": 12.23828125, + "x": 8969.732421875, + "y": 3793.64013671875 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9803921580314636, + "g": 0.9647058844566344, + "r": 0.945098042488098 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.8509804010391235, + "g": 0.8352941274642944, + "r": 0.8156862854957581 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 0.8080573678016663, + 0.5874120593070984, + -0.19792257249355316 + ], + [ + -0.5874120593070984, + 0.8080573678016663, + 0.3895654380321502 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 12.231562614440918, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "ring", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 12.2383451461792, + "x": 7.732332706451416, + "y": 11.64023494720459 + }, + "extra": {}, + "fieldErrors": {} + }, + "I41:901;136:19775": { + "id": "I41:901;136:19775", + "type": "VECTOR", + "fields": { + "parentId": "I41:901;136:19769", + "absoluteBoundingBox": { + "height": 6.650156497955322, + "width": 6.653844356536865, + "x": 8972.4052734375, + "y": 3796.31201171875 + }, + "absoluteRenderBounds": { + "height": 6.650146484375, + "width": 6.654296875, + "x": 8972.4052734375, + "y": 3796.31201171875 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.7529411911964417, + "g": 0.7372549176216125, + "r": 0.7176470756530762 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.729411780834198, + "g": 0.7137255072593689, + "r": 0.6941176652908325 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 0.9339293241500854, + 0.3536636531352997, + -0.14337609708309174 + ], + [ + -0.3536636531352997, + 0.9339293241500854, + 0.2099337726831436 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 6.650156497955322, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "ring-center", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 6.653844356536865, + "x": 10.405689239501951, + "y": 14.31210994720459 + }, + "extra": {}, + "fieldErrors": {} + }, + "I41:901;136:19776": { + "id": "I41:901;136:19776", + "type": "VECTOR", + "fields": { + "parentId": "I41:901;136:19769", + "absoluteBoundingBox": { + "height": 29.376216888427734, + "width": 30.706666946411133, + "x": 8962.046875, + "y": 3785.271240234375 + }, + "absoluteRenderBounds": { + "height": 29.376220703125, + "width": 30.70703125, + "x": 8962.046875, + "y": 3785.271240234375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9960784316062928, + "g": 0.9803921580314636, + "r": 0.9607843160629272 + }, + "position": 0.1899999976158142 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9607843160629272, + "g": 0.945098042488098, + "r": 0.9254902005195618 + }, + "position": 0.3799999952316284 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9254902005195618, + "g": 0.9098039269447328, + "r": 0.8901960849761963 + }, + "position": 0.5 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.8823529481887817, + "g": 0.8666666746139526, + "r": 0.8470588326454163 + }, + "position": 0.6600000262260437 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.8509804010391235, + "g": 0.8352941274642944, + "r": 0.8156862854957581 + }, + "position": 0.8399999737739563 + } + ], + "gradientTransform": [ + [ + -1.6364001035690308, + 1.5055831670761108, + -0.08484271168708801 + ], + [ + -1.5728986263275146, + -1.5663667917251587, + 2.0555288791656494 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 29.376216888427734, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "orbit-outer (Stroke)", + "opacity": 1, + "strokeAlign": "CENTER", + "strokeWeight": 5.050000190734863, + "visible": true, + "width": 30.706666946411133, + "x": 0.046630535274744034, + "y": 3.271313428878784 + }, + "extra": {}, + "fieldErrors": {} + }, + "I41:901;136:19777": { + "id": "I41:901;136:19777", + "type": "VECTOR", + "fields": { + "parentId": "I41:901;136:19769", + "absoluteBoundingBox": { + "height": 16.860309600830078, + "width": 30.57485008239746, + "x": 8965.8515625, + "y": 3793.567138671875 + }, + "absoluteRenderBounds": { + "height": 16.8603515625, + "width": 30.5751953125, + "x": 8965.8515625, + "y": 3793.567138671875 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.9960784316062928, + "g": 0.9803921580314636, + "r": 0.9607843160629272 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 16.860309600830078, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "orbit-inner", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 30.57485008239746, + "x": 3.851651191711426, + "y": 11.567109107971191 + }, + "extra": {}, + "fieldErrors": {} + }, + "I41:901;136:19778": { + "id": "I41:901;136:19778", + "type": "VECTOR", + "fields": { + "parentId": "I41:901;136:19769", + "absoluteBoundingBox": { + "height": 8.95641803741455, + "width": 19.86937141418457, + "x": 8966.0849609375, + "y": 3789.470703125 + }, + "absoluteRenderBounds": { + "height": 8.956298828125, + "width": 19.869140625, + "x": 8966.0849609375, + "y": 3789.470703125 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9960784316062928, + "g": 0.9803921580314636, + "r": 0.9607843160629272 + }, + "position": 0.029999999329447743 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9647058844566344, + "g": 0.9490196108818054, + "r": 0.929411768913269 + }, + "position": 0.119999997317791 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9254902005195618, + "g": 0.9098039269447328, + "r": 0.8901960849761963 + }, + "position": 0.3499999940395355 + }, + { + "boundVariables": {}, + "color": { + "a": 0.8999999761581421, + "b": 0.8705882430076599, + "g": 0.8549019694328308, + "r": 0.8352941274642944 + }, + "position": 0.6200000047683716 + }, + { + "boundVariables": {}, + "color": { + "a": 0.6000000238418579, + "b": 0.843137264251709, + "g": 0.8274509906768799, + "r": 0.8078431487083435 + }, + "position": 0.8600000143051147 + }, + { + "boundVariables": {}, + "color": { + "a": 0, + "b": 0.8313725590705872, + "g": 0.8156862854957581, + "r": 0.7960784435272217 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 0.9895601272583008, + -0.06226864457130432, + 0.0533602200448513 + ], + [ + 0.13806335628032684, + 0.446306437253952, + 0.05430806800723076 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 8.95641803741455, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "orbit-inner-tail", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 19.86937141418457, + "x": 4.08460807800293, + "y": 7.470691680908203 + }, + "extra": {}, + "fieldErrors": {} + }, + "I41:901;136:19779": { + "id": "I41:901;136:19779", + "type": "GROUP", + "fields": { + "parentId": "41:901", + "childrenIds": [ + "I41:901;136:19780", + "I41:901;136:19781", + "I41:901;136:19782", + "I41:901;136:19783", + "I41:901;136:19784", + "I41:901;136:19785", + "I41:901;136:19786", + "I41:901;136:19787", + "I41:901;136:19788", + "I41:901;136:19789" + ], + "absoluteBoundingBox": { + "height": 21.29252243041992, + "width": 177.15370178222656, + "x": 9007.765625, + "y": 3789.10302734375 + }, + "absoluteRenderBounds": { + "height": 21.29252243041992, + "width": 177.15370178222656, + "x": 9007.765625, + "y": 3789.10302734375 + }, + "height": 21.29252243041992, + "inferredAutoLayout": { + "counterAxisAlignItems": "MAX", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 2.3733348846435547, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0.000370025634765625, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "AUTO" + }, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "ChondroGen", + "opacity": 1, + "visible": true, + "width": 177.15370178222656, + "x": 45.765625, + "y": 7.10302734375 + }, + "extra": {}, + "fieldErrors": {} + }, + "I41:901;136:19780": { + "id": "I41:901;136:19780", + "type": "VECTOR", + "fields": { + "parentId": "I41:901;136:19779", + "absoluteBoundingBox": { + "height": 15.289889335632324, + "width": 14.901744842529297, + "x": 9170.017578125, + "y": 3794.822509765625 + }, + "absoluteRenderBounds": { + "height": 15.289794921875, + "width": 14.9013671875, + "x": 9170.017578125, + "y": 3794.822509765625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.7964744567871094, + "g": 0.7761158347129822, + "r": 0.7543532252311707 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 15.289889335632324, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 14.901744842529297, + "x": 208.0172119140625, + "y": 12.822452545166016 + }, + "extra": {}, + "fieldErrors": {} + }, + "I41:901;136:19781": { + "id": "I41:901;136:19781", + "type": "VECTOR", + "fields": { + "parentId": "I41:901;136:19779", + "absoluteBoundingBox": { + "height": 15.48809051513672, + "width": 15.468348503112791, + "x": 9151.7138671875, + "y": 3794.822509765625 + }, + "absoluteRenderBounds": { + "height": 15.488037109375, + "width": 15.46875, + "x": 9151.7138671875, + "y": 3794.822509765625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.7964744567871094, + "g": 0.7761158347129822, + "r": 0.7543532252311707 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 15.48809051513672, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 15.468348503112791, + "x": 189.7138671875, + "y": 12.822452545166016 + }, + "extra": {}, + "fieldErrors": {} + }, + "I41:901;136:19782": { + "id": "I41:901;136:19782", + "type": "VECTOR", + "fields": { + "parentId": "I41:901;136:19779", + "absoluteBoundingBox": { + "height": 20.38651657104492, + "width": 18.698009490966797, + "x": 9130.56640625, + "y": 3790.009033203125 + }, + "absoluteRenderBounds": { + "height": 20.386474609375, + "width": 18.6982421875, + "x": 9130.56640625, + "y": 3790.009033203125 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.7964744567871094, + "g": 0.7761158347129822, + "r": 0.7543532252311707 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 20.38651657104492, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 18.698009490966797, + "x": 168.56642150878906, + "y": 8.009008407592773 + }, + "extra": {}, + "fieldErrors": {} + }, + "I41:901;136:19783": { + "id": "I41:901;136:19783", + "type": "VECTOR", + "fields": { + "parentId": "I41:901;136:19779", + "absoluteBoundingBox": { + "height": 15.48809051513672, + "width": 16.11993980407715, + "x": 9112.703125, + "y": 3794.822509765625 + }, + "absoluteRenderBounds": { + "height": 15.488037109375, + "width": 16.1201171875, + "x": 9112.703125, + "y": 3794.822509765625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.7291667461395264, + "g": 0.7133524417877197, + "r": 0.6952789425849915 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 15.48809051513672, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 16.11993980407715, + "x": 150.7032699584961, + "y": 12.822452545166016 + }, + "extra": {}, + "fieldErrors": {} + }, + "I41:901;136:19784": { + "id": "I41:901;136:19784", + "type": "VECTOR", + "fields": { + "parentId": "I41:901;136:19779", + "absoluteBoundingBox": { + "height": 15.289889335632324, + "width": 8.754060745239258, + "x": 9102.896484375, + "y": 3794.822509765625 + }, + "absoluteRenderBounds": { + "height": 15.289794921875, + "width": 8.75390625, + "x": 9102.896484375, + "y": 3794.822509765625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.7291667461395264, + "g": 0.7133524417877197, + "r": 0.6952789425849915 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 15.289889335632324, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 8.754060745239258, + "x": 140.89654541015625, + "y": 12.822452545166016 + }, + "extra": {}, + "fieldErrors": {} + }, + "I41:901;136:19785": { + "id": "I41:901;136:19785", + "type": "VECTOR", + "fields": { + "parentId": "I41:901;136:19779", + "absoluteBoundingBox": { + "height": 21.207639694213867, + "width": 16.03495216369629, + "x": 9082.767578125, + "y": 3789.10302734375 + }, + "absoluteRenderBounds": { + "height": 21.20751953125, + "width": 16.03515625, + "x": 9082.767578125, + "y": 3789.10302734375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.7291667461395264, + "g": 0.7133524417877197, + "r": 0.6952789425849915 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 21.207639694213867, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 16.03495216369629, + "x": 120.76722717285156, + "y": 7.102945327758789 + }, + "extra": {}, + "fieldErrors": {} + }, + "I41:901;136:19786": { + "id": "I41:901;136:19786", + "type": "VECTOR", + "fields": { + "parentId": "I41:901;136:19779", + "absoluteBoundingBox": { + "height": 15.289889335632324, + "width": 14.901739120483398, + "x": 9065.158203125, + "y": 3794.822509765625 + }, + "absoluteRenderBounds": { + "height": 15.289794921875, + "width": 14.9013671875, + "x": 9065.158203125, + "y": 3794.822509765625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.7291667461395264, + "g": 0.7133524417877197, + "r": 0.6952789425849915 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 15.289889335632324, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 14.901739120483398, + "x": 103.15860748291016, + "y": 12.822452545166016 + }, + "extra": {}, + "fieldErrors": {} + }, + "I41:901;136:19787": { + "id": "I41:901;136:19787", + "type": "VECTOR", + "fields": { + "parentId": "I41:901;136:19779", + "absoluteBoundingBox": { + "height": 15.48809051513672, + "width": 16.119945526123047, + "x": 9046.21875, + "y": 3794.822509765625 + }, + "absoluteRenderBounds": { + "height": 15.488037109375, + "width": 16.1201171875, + "x": 9046.21875, + "y": 3794.822509765625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.7291667461395264, + "g": 0.7133524417877197, + "r": 0.6952789425849915 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 15.48809051513672, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 16.119945526123047, + "x": 84.21893310546875, + "y": 12.822452545166016 + }, + "extra": {}, + "fieldErrors": {} + }, + "I41:901;136:19788": { + "id": "I41:901;136:19788", + "type": "VECTOR", + "fields": { + "parentId": "I41:901;136:19779", + "absoluteBoundingBox": { + "height": 21.00943946838379, + "width": 14.90174388885498, + "x": 9028.6103515625, + "y": 3789.10302734375 + }, + "absoluteRenderBounds": { + "height": 21.009521484375, + "width": 14.9013671875, + "x": 9028.6103515625, + "y": 3789.10302734375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.7291667461395264, + "g": 0.7133524417877197, + "r": 0.6952789425849915 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 21.00943946838379, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 14.90174388885498, + "x": 66.61032104492188, + "y": 7.102945327758789 + }, + "extra": {}, + "fieldErrors": {} + }, + "I41:901;136:19789": { + "id": "I41:901;136:19789", + "type": "VECTOR", + "fields": { + "parentId": "I41:901;136:19779", + "absoluteBoundingBox": { + "height": 20.38651657104492, + "width": 18.47136116027832, + "x": 9007.765625, + "y": 3790.009033203125 + }, + "absoluteRenderBounds": { + "height": 20.386474609375, + "width": 18.4716796875, + "x": 9007.765625, + "y": 3790.009033203125 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.7291667461395264, + "g": 0.7133524417877197, + "r": 0.6952789425849915 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 20.38651657104492, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 18.47136116027832, + "x": 45.765625, + "y": 8.009008407592773 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1189;136:19765": { + "id": "I43:1189;136:19765", + "type": "VECTOR", + "fields": { + "parentId": "43:1189", + "absoluteBoundingBox": { + "height": 36, + "width": 36.019962310791016, + "x": 8822, + "y": 4012 + }, + "absoluteRenderBounds": { + "height": 36, + "width": 36.01953125, + "x": 8822, + "y": 4012 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.7843137383460999, + "g": 0.7686274647712708, + "r": 0.7490196228027344 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.7647058963775635, + "g": 0.7490196228027344, + "r": 0.729411780834198 + }, + "position": 0.30000001192092896 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.729411780834198, + "g": 0.7137255072593689, + "r": 0.6941176652908325 + }, + "position": 0.44999998807907104 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.7019608020782471, + "g": 0.686274528503418, + "r": 0.6666666865348816 + }, + "position": 0.6100000143051147 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.686274528503418, + "g": 0.6705882549285889, + "r": 0.6509804129600525 + }, + "position": 0.7599999904632568 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.6823529601097107, + "g": 0.6666666865348816, + "r": 0.6470588445663452 + }, + "position": 0.9100000262260436 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.6823529601097107, + "g": 0.6666666865348816, + "r": 0.6470588445663452 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 0.6454866528511047, + 0, + 0.31524205207824707 + ], + [ + 0, + 0.6454866528511047, + 0.204462930560112 + ] + ], + "opacity": 1, + "type": "GRADIENT_RADIAL", + "visible": true + } + ], + "height": 36, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "sphere", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 36.019962310791016, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1189;136:19766": { + "id": "I43:1189;136:19766", + "type": "GROUP", + "fields": { + "parentId": "43:1189", + "childrenIds": [ + "I43:1189;136:19767", + "I43:1189;136:19769" + ], + "absoluteBoundingBox": { + "height": 36, + "width": 36.019962310791016, + "x": 8822, + "y": 4012 + }, + "absoluteRenderBounds": { + "height": 36, + "width": 36.019962310791016, + "x": 8822, + "y": 4012 + }, + "height": 36, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Clip path group", + "opacity": 1, + "visible": true, + "width": 36.019962310791016, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1189;136:19767": { + "id": "I43:1189;136:19767", + "type": "GROUP", + "fields": { + "parentId": "I43:1189;136:19766", + "childrenIds": [ + "I43:1189;136:19768" + ], + "absoluteBoundingBox": { + "height": 36, + "width": 36.019962310791016, + "x": 8822, + "y": 4012 + }, + "absoluteRenderBounds": null, + "height": 36, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "isMask": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "sphere-clip", + "opacity": 1, + "visible": true, + "width": 36.019962310791016, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1189;136:19768": { + "id": "I43:1189;136:19768", + "type": "VECTOR", + "fields": { + "parentId": "I43:1189;136:19767", + "absoluteBoundingBox": { + "height": 36, + "width": 36.019962310791016, + "x": 8822, + "y": 4012 + }, + "absoluteRenderBounds": { + "height": 36, + "width": 36.01953125, + "x": 8822, + "y": 4012 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0, + "g": 0, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 36, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 36.019962310791016, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1189;136:19769": { + "id": "I43:1189;136:19769", + "type": "GROUP", + "fields": { + "parentId": "I43:1189;136:19766", + "childrenIds": [ + "I43:1189;136:19770", + "I43:1189;136:19771", + "I43:1189;136:19772", + "I43:1189;136:19773", + "I43:1189;136:19774", + "I43:1189;136:19775", + "I43:1189;136:19776", + "I43:1189;136:19777", + "I43:1189;136:19778" + ], + "absoluteBoundingBox": { + "height": 36, + "width": 36.019962310791016, + "x": 8822, + "y": 4012 + }, + "absoluteRenderBounds": { + "height": 36, + "width": 36.01953125, + "x": 8822, + "y": 4012 + }, + "height": 36, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Group", + "opacity": 1, + "visible": true, + "width": 36.019962310791016, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1189;136:19770": { + "id": "I43:1189;136:19770", + "type": "VECTOR", + "fields": { + "parentId": "I43:1189;136:19769", + "absoluteBoundingBox": { + "height": 36, + "width": 36.019962310791016, + "x": 8822, + "y": 4012 + }, + "absoluteRenderBounds": { + "height": 36, + "width": 36.01953125, + "x": 8822, + "y": 4012 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 0.41999998688697815, + "b": 0.6705882549285889, + "g": 0.6549019813537598, + "r": 0.6352941393852234 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 0.20000000298023224, + "b": 0.6705882549285889, + "g": 0.6549019813537598, + "r": 0.6352941393852234 + }, + "position": 0.550000011920929 + }, + { + "boundVariables": {}, + "color": { + "a": 0, + "b": 0.6705882549285889, + "g": 0.6549019813537598, + "r": 0.6352941393852234 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 1.252997875213623, + 0, + 0.08783221244812012 + ], + [ + 0, + 1.252997875213623, + -0.7154324054718018 + ] + ], + "opacity": 1, + "type": "GRADIENT_RADIAL", + "visible": true + } + ], + "height": 36, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "shade-outside-orbit", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 36.019962310791016, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1189;136:19771": { + "id": "I43:1189;136:19771", + "type": "VECTOR", + "fields": { + "parentId": "I43:1189;136:19769", + "absoluteBoundingBox": { + "height": 36, + "width": 36.019962310791016, + "x": 8822, + "y": 4012 + }, + "absoluteRenderBounds": { + "height": 36, + "width": 36.01953125, + "x": 8822, + "y": 4012 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 0.14000000059604645, + "b": 0.8156862854957581, + "g": 0.800000011920929, + "r": 0.7803921699523926 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 0, + "b": 0.8156862854957581, + "g": 0.800000011920929, + "r": 0.7803921699523926 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 1.9364598989486697, + 0, + 0.468456894159317 + ], + [ + 0, + 1.9364598989486697, + -0.5502269268035889 + ] + ], + "opacity": 1, + "type": "GRADIENT_RADIAL", + "visible": true + } + ], + "height": 36, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "light-left-overlay", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 36.019962310791016, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1189;136:19772": { + "id": "I43:1189;136:19772", + "type": "VECTOR", + "fields": { + "parentId": "I43:1189;136:19769", + "absoluteBoundingBox": { + "height": 7.538562297821045, + "width": 14.69009494781494, + "x": 8841.4521484375, + "y": 4037.662353515625 + }, + "absoluteRenderBounds": { + "height": 7.53857421875, + "width": 14.6904296875, + "x": 8841.4521484375, + "y": 4037.662353515625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 0, + "b": 0.729411780834198, + "g": 0.7137255072593689, + "r": 0.6941176652908325 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 0.550000011920929, + "b": 0.729411780834198, + "g": 0.7137255072593689, + "r": 0.6941176652908325 + }, + "position": 0.30000001192092896 + }, + { + "boundVariables": {}, + "color": { + "a": 0.5, + "b": 0.729411780834198, + "g": 0.7137255072593689, + "r": 0.6941176652908325 + }, + "position": 0.75 + }, + { + "boundVariables": {}, + "color": { + "a": 0, + "b": 0.729411780834198, + "g": 0.7137255072593689, + "r": 0.6941176652908325 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 0.8151227831840515, + -0.2092042565345764, + 0.19614684581756592 + ], + [ + 0.4074420928955078, + 0.41853103041648865, + 0.08879981189966202 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 7.538562297821045, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "rim-highlight (Stroke)", + "opacity": 1, + "strokeAlign": "CENTER", + "strokeWeight": 3.849999904632568, + "visible": true, + "width": 14.69009494781494, + "x": 19.45182228088379, + "y": 25.662315368652344 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1189;136:19773": { + "id": "I43:1189;136:19773", + "type": "VECTOR", + "fields": { + "parentId": "I43:1189;136:19769", + "absoluteBoundingBox": { + "height": 20.55927467346191, + "width": 20.555435180664062, + "x": 8826.123046875, + "y": 4019.4462890625 + }, + "absoluteRenderBounds": { + "height": 20.559326171875, + "width": 20.5556640625, + "x": 8826.123046875, + "y": 4019.4462890625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.8784313797950745, + "g": 0.8627451062202454, + "r": 0.843137264251709 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.7803921699523926, + "g": 0.7647058963775635, + "r": 0.7450980544090271 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 0.8613659739494324, + 0.45280781388282776, + -0.1629999577999115 + ], + [ + -0.4524722993373871, + 0.8620046973228455, + 0.31514695286750793 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 20.55927467346191, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "core", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 20.555435180664062, + "x": 4.122681140899658, + "y": 7.446366786956787 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1189;136:19774": { + "id": "I43:1189;136:19774", + "type": "VECTOR", + "fields": { + "parentId": "I43:1189;136:19769", + "absoluteBoundingBox": { + "height": 12.231562614440918, + "width": 12.2383451461792, + "x": 8829.732421875, + "y": 4023.64013671875 + }, + "absoluteRenderBounds": { + "height": 12.2314453125, + "width": 12.23828125, + "x": 8829.732421875, + "y": 4023.64013671875 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9803921580314636, + "g": 0.9647058844566344, + "r": 0.945098042488098 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.8509804010391235, + "g": 0.8352941274642944, + "r": 0.8156862854957581 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 0.8080573678016663, + 0.5874120593070984, + -0.19792257249355316 + ], + [ + -0.5874120593070984, + 0.8080573678016663, + 0.3895654380321502 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 12.231562614440918, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "ring", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 12.2383451461792, + "x": 7.732332706451416, + "y": 11.64023494720459 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1189;136:19775": { + "id": "I43:1189;136:19775", + "type": "VECTOR", + "fields": { + "parentId": "I43:1189;136:19769", + "absoluteBoundingBox": { + "height": 6.650156497955322, + "width": 6.653844356536865, + "x": 8832.4052734375, + "y": 4026.31201171875 + }, + "absoluteRenderBounds": { + "height": 6.650146484375, + "width": 6.654296875, + "x": 8832.4052734375, + "y": 4026.31201171875 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.7529411911964417, + "g": 0.7372549176216125, + "r": 0.7176470756530762 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.729411780834198, + "g": 0.7137255072593689, + "r": 0.6941176652908325 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 0.9339293241500854, + 0.3536636531352997, + -0.14337609708309174 + ], + [ + -0.3536636531352997, + 0.9339293241500854, + 0.2099337726831436 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 6.650156497955322, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "ring-center", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 6.653844356536865, + "x": 10.405689239501951, + "y": 14.31210994720459 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1189;136:19776": { + "id": "I43:1189;136:19776", + "type": "VECTOR", + "fields": { + "parentId": "I43:1189;136:19769", + "absoluteBoundingBox": { + "height": 29.376216888427734, + "width": 30.706666946411133, + "x": 8822.046875, + "y": 4015.271240234375 + }, + "absoluteRenderBounds": { + "height": 29.376220703125, + "width": 30.70703125, + "x": 8822.046875, + "y": 4015.271240234375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9960784316062928, + "g": 0.9803921580314636, + "r": 0.9607843160629272 + }, + "position": 0.1899999976158142 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9607843160629272, + "g": 0.945098042488098, + "r": 0.9254902005195618 + }, + "position": 0.3799999952316284 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9254902005195618, + "g": 0.9098039269447328, + "r": 0.8901960849761963 + }, + "position": 0.5 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.8823529481887817, + "g": 0.8666666746139526, + "r": 0.8470588326454163 + }, + "position": 0.6600000262260437 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.8509804010391235, + "g": 0.8352941274642944, + "r": 0.8156862854957581 + }, + "position": 0.8399999737739563 + } + ], + "gradientTransform": [ + [ + -1.6364001035690308, + 1.5055831670761108, + -0.08484271168708801 + ], + [ + -1.5728986263275146, + -1.5663667917251587, + 2.0555288791656494 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 29.376216888427734, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "orbit-outer (Stroke)", + "opacity": 1, + "strokeAlign": "CENTER", + "strokeWeight": 5.050000190734863, + "visible": true, + "width": 30.706666946411133, + "x": 0.046630535274744034, + "y": 3.271313428878784 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1189;136:19777": { + "id": "I43:1189;136:19777", + "type": "VECTOR", + "fields": { + "parentId": "I43:1189;136:19769", + "absoluteBoundingBox": { + "height": 16.860309600830078, + "width": 30.57485008239746, + "x": 8825.8515625, + "y": 4023.567138671875 + }, + "absoluteRenderBounds": { + "height": 16.8603515625, + "width": 30.5751953125, + "x": 8825.8515625, + "y": 4023.567138671875 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.9960784316062928, + "g": 0.9803921580314636, + "r": 0.9607843160629272 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 16.860309600830078, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "orbit-inner", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 30.57485008239746, + "x": 3.851651191711426, + "y": 11.567109107971191 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1189;136:19778": { + "id": "I43:1189;136:19778", + "type": "VECTOR", + "fields": { + "parentId": "I43:1189;136:19769", + "absoluteBoundingBox": { + "height": 8.95641803741455, + "width": 19.86937141418457, + "x": 8826.0849609375, + "y": 4019.470703125 + }, + "absoluteRenderBounds": { + "height": 8.956298828125, + "width": 19.869140625, + "x": 8826.0849609375, + "y": 4019.470703125 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9960784316062928, + "g": 0.9803921580314636, + "r": 0.9607843160629272 + }, + "position": 0.029999999329447743 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9647058844566344, + "g": 0.9490196108818054, + "r": 0.929411768913269 + }, + "position": 0.119999997317791 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9254902005195618, + "g": 0.9098039269447328, + "r": 0.8901960849761963 + }, + "position": 0.3499999940395355 + }, + { + "boundVariables": {}, + "color": { + "a": 0.8999999761581421, + "b": 0.8705882430076599, + "g": 0.8549019694328308, + "r": 0.8352941274642944 + }, + "position": 0.6200000047683716 + }, + { + "boundVariables": {}, + "color": { + "a": 0.6000000238418579, + "b": 0.843137264251709, + "g": 0.8274509906768799, + "r": 0.8078431487083435 + }, + "position": 0.8600000143051147 + }, + { + "boundVariables": {}, + "color": { + "a": 0, + "b": 0.8313725590705872, + "g": 0.8156862854957581, + "r": 0.7960784435272217 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 0.9895601272583008, + -0.06226864457130432, + 0.0533602200448513 + ], + [ + 0.13806335628032684, + 0.446306437253952, + 0.05430806800723076 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 8.95641803741455, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "orbit-inner-tail", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 19.86937141418457, + "x": 4.08460807800293, + "y": 7.470691680908203 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1189;136:19779": { + "id": "I43:1189;136:19779", + "type": "GROUP", + "fields": { + "parentId": "43:1189", + "childrenIds": [ + "I43:1189;136:19780", + "I43:1189;136:19781", + "I43:1189;136:19782", + "I43:1189;136:19783", + "I43:1189;136:19784", + "I43:1189;136:19785", + "I43:1189;136:19786", + "I43:1189;136:19787", + "I43:1189;136:19788", + "I43:1189;136:19789" + ], + "absoluteBoundingBox": { + "height": 21.29252243041992, + "width": 177.15370178222656, + "x": 8867.765625, + "y": 4019.10302734375 + }, + "absoluteRenderBounds": { + "height": 21.29252243041992, + "width": 177.15370178222656, + "x": 8867.765625, + "y": 4019.10302734375 + }, + "height": 21.29252243041992, + "inferredAutoLayout": { + "counterAxisAlignItems": "MAX", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 2.3733348846435547, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0.000370025634765625, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "AUTO" + }, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "ChondroGen", + "opacity": 1, + "visible": true, + "width": 177.15370178222656, + "x": 45.765625, + "y": 7.10302734375 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1189;136:19780": { + "id": "I43:1189;136:19780", + "type": "VECTOR", + "fields": { + "parentId": "I43:1189;136:19779", + "absoluteBoundingBox": { + "height": 15.289889335632324, + "width": 14.901744842529297, + "x": 9030.017578125, + "y": 4024.822509765625 + }, + "absoluteRenderBounds": { + "height": 15.289794921875, + "width": 14.9013671875, + "x": 9030.017578125, + "y": 4024.822509765625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.7964744567871094, + "g": 0.7761158347129822, + "r": 0.7543532252311707 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 15.289889335632324, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 14.901744842529297, + "x": 208.0172119140625, + "y": 12.822452545166016 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1189;136:19781": { + "id": "I43:1189;136:19781", + "type": "VECTOR", + "fields": { + "parentId": "I43:1189;136:19779", + "absoluteBoundingBox": { + "height": 15.48809051513672, + "width": 15.468348503112791, + "x": 9011.7138671875, + "y": 4024.822509765625 + }, + "absoluteRenderBounds": { + "height": 15.488037109375, + "width": 15.46875, + "x": 9011.7138671875, + "y": 4024.822509765625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.7964744567871094, + "g": 0.7761158347129822, + "r": 0.7543532252311707 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 15.48809051513672, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 15.468348503112791, + "x": 189.7138671875, + "y": 12.822452545166016 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1189;136:19782": { + "id": "I43:1189;136:19782", + "type": "VECTOR", + "fields": { + "parentId": "I43:1189;136:19779", + "absoluteBoundingBox": { + "height": 20.38651657104492, + "width": 18.698009490966797, + "x": 8990.56640625, + "y": 4020.009033203125 + }, + "absoluteRenderBounds": { + "height": 20.386474609375, + "width": 18.6982421875, + "x": 8990.56640625, + "y": 4020.009033203125 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.7964744567871094, + "g": 0.7761158347129822, + "r": 0.7543532252311707 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 20.38651657104492, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 18.698009490966797, + "x": 168.56642150878906, + "y": 8.009008407592773 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1189;136:19783": { + "id": "I43:1189;136:19783", + "type": "VECTOR", + "fields": { + "parentId": "I43:1189;136:19779", + "absoluteBoundingBox": { + "height": 15.48809051513672, + "width": 16.11993980407715, + "x": 8972.703125, + "y": 4024.822509765625 + }, + "absoluteRenderBounds": { + "height": 15.488037109375, + "width": 16.1201171875, + "x": 8972.703125, + "y": 4024.822509765625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.7291667461395264, + "g": 0.7133524417877197, + "r": 0.6952789425849915 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 15.48809051513672, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 16.11993980407715, + "x": 150.7032699584961, + "y": 12.822452545166016 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1189;136:19784": { + "id": "I43:1189;136:19784", + "type": "VECTOR", + "fields": { + "parentId": "I43:1189;136:19779", + "absoluteBoundingBox": { + "height": 15.289889335632324, + "width": 8.754060745239258, + "x": 8962.896484375, + "y": 4024.822509765625 + }, + "absoluteRenderBounds": { + "height": 15.289794921875, + "width": 8.75390625, + "x": 8962.896484375, + "y": 4024.822509765625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.7291667461395264, + "g": 0.7133524417877197, + "r": 0.6952789425849915 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 15.289889335632324, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 8.754060745239258, + "x": 140.89654541015625, + "y": 12.822452545166016 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1189;136:19785": { + "id": "I43:1189;136:19785", + "type": "VECTOR", + "fields": { + "parentId": "I43:1189;136:19779", + "absoluteBoundingBox": { + "height": 21.207639694213867, + "width": 16.03495216369629, + "x": 8942.767578125, + "y": 4019.10302734375 + }, + "absoluteRenderBounds": { + "height": 21.20751953125, + "width": 16.03515625, + "x": 8942.767578125, + "y": 4019.10302734375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.7291667461395264, + "g": 0.7133524417877197, + "r": 0.6952789425849915 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 21.207639694213867, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 16.03495216369629, + "x": 120.76722717285156, + "y": 7.102945327758789 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1189;136:19786": { + "id": "I43:1189;136:19786", + "type": "VECTOR", + "fields": { + "parentId": "I43:1189;136:19779", + "absoluteBoundingBox": { + "height": 15.289889335632324, + "width": 14.901739120483398, + "x": 8925.158203125, + "y": 4024.822509765625 + }, + "absoluteRenderBounds": { + "height": 15.289794921875, + "width": 14.9013671875, + "x": 8925.158203125, + "y": 4024.822509765625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.7291667461395264, + "g": 0.7133524417877197, + "r": 0.6952789425849915 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 15.289889335632324, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 14.901739120483398, + "x": 103.15860748291016, + "y": 12.822452545166016 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1189;136:19787": { + "id": "I43:1189;136:19787", + "type": "VECTOR", + "fields": { + "parentId": "I43:1189;136:19779", + "absoluteBoundingBox": { + "height": 15.48809051513672, + "width": 16.119945526123047, + "x": 8906.21875, + "y": 4024.822509765625 + }, + "absoluteRenderBounds": { + "height": 15.488037109375, + "width": 16.1201171875, + "x": 8906.21875, + "y": 4024.822509765625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.7291667461395264, + "g": 0.7133524417877197, + "r": 0.6952789425849915 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 15.48809051513672, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 16.119945526123047, + "x": 84.21893310546875, + "y": 12.822452545166016 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1189;136:19788": { + "id": "I43:1189;136:19788", + "type": "VECTOR", + "fields": { + "parentId": "I43:1189;136:19779", + "absoluteBoundingBox": { + "height": 21.00943946838379, + "width": 14.90174388885498, + "x": 8888.6103515625, + "y": 4019.10302734375 + }, + "absoluteRenderBounds": { + "height": 21.009521484375, + "width": 14.9013671875, + "x": 8888.6103515625, + "y": 4019.10302734375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.7291667461395264, + "g": 0.7133524417877197, + "r": 0.6952789425849915 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 21.00943946838379, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 14.90174388885498, + "x": 66.61032104492188, + "y": 7.102945327758789 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1189;136:19789": { + "id": "I43:1189;136:19789", + "type": "VECTOR", + "fields": { + "parentId": "I43:1189;136:19779", + "absoluteBoundingBox": { + "height": 20.38651657104492, + "width": 18.47136116027832, + "x": 8867.765625, + "y": 4020.009033203125 + }, + "absoluteRenderBounds": { + "height": 20.386474609375, + "width": 18.4716796875, + "x": 8867.765625, + "y": 4020.009033203125 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.7291667461395264, + "g": 0.7133524417877197, + "r": 0.6952789425849915 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 20.38651657104492, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 18.47136116027832, + "x": 45.765625, + "y": 8.009008407592773 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1192;15:2101": { + "id": "I43:1192;15:2101", + "type": "TEXT", + "fields": { + "parentId": "43:1192", + "absoluteBoundingBox": { + "height": 23, + "width": 106, + "x": 8822, + "y": 4064 + }, + "absoluteRenderBounds": { + "height": 13.425048828125, + "width": 103.9599609375, + "x": 8822.3896484375, + "y": 4068.804931640625 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:13:153", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:955", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "개인정보 처리방침", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:13:153", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.5160255432128906, + "g": 0.4530186951160431, + "r": 0.4333290457725525 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Bold" + }, + "fontSize": 15, + "height": 23, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "텍스트", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:cbc02b7831f02c3b00e92d3114823bdda982464e,", + "textTruncation": "DISABLED", + "visible": true, + "width": 106, + "x": 0, + "y": 0, + "styledTextSegments": [ + { + "characters": "개인정보 처리방침", + "end": 9, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:cbc02b7831f02c3b00e92d3114823bdda982464e," + } + ] + }, + "extra": {}, + "fieldErrors": {} + } + }, + "diagnostics": [] +} diff --git a/crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_header_set.json b/crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_header_set.json new file mode 100644 index 00000000..73577179 --- /dev/null +++ b/crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_header_set.json @@ -0,0 +1,18479 @@ +{ + "fileKey": "K2kUnxGgvooAJwCQiVTb7g", + "version": null, + "roots": [ + "23:1575" + ], + "nodes": { + "23:1575": { + "id": "23:1575", + "type": "COMPONENT_SET", + "fields": { + "parentId": "19:963", + "parentType": "SECTION", + "parentName": "Components", + "childrenIds": [ + "23:1576", + "44:5181", + "40:265", + "43:1099", + "44:5193", + "43:1087" + ], + "absoluteBoundingBox": { + "height": 570, + "width": 1482, + "x": 10520, + "y": 3243 + }, + "absoluteRenderBounds": { + "height": 570, + "width": 1482, + "x": 10520, + "y": 3243 + }, + "bottomLeftRadius": 5, + "bottomRightRadius": 5, + "clipsContent": true, + "componentPropertyDefinitions": { + "type": { + "defaultValue": "scroll", + "type": "VARIANT", + "variantOptions": [ + "scroll", + "default", + "pageTop" + ] + }, + "viewport": { + "defaultValue": "Desktop", + "type": "VARIANT", + "variantOptions": [ + "Desktop", + "Mobile" + ] + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 5, + "dashPattern": [ + 10, + 5 + ], + "defaultVariant": { + "$nodeId": "23:1576", + "$nodeType": "COMPONENT" + }, + "height": 570, + "inferredAutoLayout": { + "counterAxisAlignItems": "MIN", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 20, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutPositioning": "AUTO", + "paddingBottom": 20, + "paddingLeft": 20, + "paddingRight": 20, + "paddingTop": 20, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 20, + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "header", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 20, + "paddingLeft": 20, + "paddingRight": 20, + "paddingTop": 20, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 0.27843138575553894, + "r": 0.5921568870544434 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "topLeftRadius": 5, + "topRightRadius": 5, + "visible": true, + "width": 1482, + "x": 1850, + "y": 2205 + }, + "extra": {}, + "fieldErrors": {} + }, + "23:1576": { + "id": "23:1576", + "type": "COMPONENT", + "fields": { + "parentId": "23:1575", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "23:1577" + ], + "absoluteBoundingBox": { + "height": 80, + "width": 1440, + "x": 10541, + "y": 3264 + }, + "absoluteRenderBounds": { + "height": 80, + "width": 1440, + "x": 10541, + "y": 3264 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:350", + "type": "VARIABLE_ALIAS" + } + ], + "strokes": [ + { + "id": "VariableID:9:351", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:350", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 80, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 29, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 16, + "paddingRight": 16, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 29, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "type=scroll, viewport=Desktop", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 16, + "paddingRight": 16, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 0, + "strokeRightWeight": 0, + "strokeTopWeight": 0, + "strokeWeight": { + "$unsupported": "symbol" + }, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:351", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.9215686321258544, + "g": 0.9058823585510254, + "r": 0.8980392217636108 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "type": "scroll", + "viewport": "Desktop" + }, + "visible": true, + "width": 1440, + "x": 21, + "y": 21 + }, + "extra": {}, + "fieldErrors": {} + }, + "23:1577": { + "id": "23:1577", + "type": "FRAME", + "fields": { + "parentId": "23:1576", + "childrenIds": [ + "37:224", + "23:1582", + "23:1589" + ], + "absoluteBoundingBox": { + "height": 48, + "width": 1408, + "x": 10557, + "y": 3279.5 + }, + "absoluteRenderBounds": { + "height": 48, + "width": 1408, + "x": 10557, + "y": 3279.5 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "height": 48, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 29, + "layoutAlign": "INHERIT", + "layoutGrow": 1, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "SPACE_BETWEEN", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 29, + "layoutGrow": 1, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "Frame 450", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "SPACE_BETWEEN", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 1408, + "x": 16, + "y": 15.5 + }, + "extra": {}, + "fieldErrors": {} + }, + "23:1582": { + "id": "23:1582", + "type": "FRAME", + "fields": { + "parentId": "23:1577", + "childrenIds": [ + "23:1583", + "23:1584", + "23:1585", + "23:1586", + "23:1588" + ], + "absoluteBoundingBox": { + "height": 48, + "width": 462, + "x": 11045, + "y": 3279.5 + }, + "absoluteRenderBounds": { + "height": 48, + "width": 462, + "x": 11045, + "y": 3279.5 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "height": 48, + "inferredAutoLayout": { + "counterAxisAlignItems": "MIN", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 40, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 40, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "FILL", + "maxHeight": null, + "maxWidth": null, + "name": "Frame 446", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 462, + "x": 488, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "23:1583": { + "id": "23:1583", + "type": "INSTANCE", + "fields": { + "parentId": "23:1582", + "childrenIds": [ + "I23:1583;18:959" + ], + "absoluteBoundingBox": { + "height": 48, + "width": 59, + "x": 11045, + "y": 3279.5 + }, + "absoluteRenderBounds": { + "height": 48, + "width": 59, + "x": 11045, + "y": 3279.5 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "componentProperties": { + "effect": { + "boundVariables": {}, + "type": "VARIANT", + "value": "default" + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "height": 48, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 10, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 8, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 8, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 10, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "FILL", + "maxHeight": null, + "maxWidth": null, + "name": "menu", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 8, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 8, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "effect": "default" + }, + "visible": true, + "width": 59, + "x": 0, + "y": 0, + "mainComponentId": "19:972", + "mainComponentName": "effect=default", + "mainComponentSetId": "19:973" + }, + "extra": {}, + "fieldErrors": {} + }, + "23:1584": { + "id": "23:1584", + "type": "INSTANCE", + "fields": { + "parentId": "23:1582", + "childrenIds": [ + "I23:1584;18:959" + ], + "absoluteBoundingBox": { + "height": 48, + "width": 95, + "x": 11144, + "y": 3279.5 + }, + "absoluteRenderBounds": { + "height": 48, + "width": 95, + "x": 11144, + "y": 3279.5 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "componentProperties": { + "effect": { + "boundVariables": {}, + "type": "VARIANT", + "value": "default" + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "height": 48, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 10, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 8, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 8, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 10, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "FILL", + "maxHeight": null, + "maxWidth": null, + "name": "menu", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 8, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 8, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "effect": "default" + }, + "visible": true, + "width": 95, + "x": 99, + "y": 0, + "mainComponentId": "19:972", + "mainComponentName": "effect=default", + "mainComponentSetId": "19:973" + }, + "extra": {}, + "fieldErrors": {} + }, + "23:1585": { + "id": "23:1585", + "type": "INSTANCE", + "fields": { + "parentId": "23:1582", + "childrenIds": [ + "I23:1585;18:959" + ], + "absoluteBoundingBox": { + "height": 48, + "width": 59, + "x": 11279, + "y": 3279.5 + }, + "absoluteRenderBounds": { + "height": 48, + "width": 59, + "x": 11279, + "y": 3279.5 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "componentProperties": { + "effect": { + "boundVariables": {}, + "type": "VARIANT", + "value": "default" + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "height": 48, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 10, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 8, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 8, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 10, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "FILL", + "maxHeight": null, + "maxWidth": null, + "name": "menu", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 8, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 8, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "effect": "default" + }, + "visible": true, + "width": 59, + "x": 234, + "y": 0, + "mainComponentId": "19:972", + "mainComponentName": "effect=default", + "mainComponentSetId": "19:973" + }, + "extra": {}, + "fieldErrors": {} + }, + "23:1586": { + "id": "23:1586", + "type": "INSTANCE", + "fields": { + "parentId": "23:1582", + "childrenIds": [ + "I23:1586;18:959" + ], + "absoluteBoundingBox": { + "height": 48, + "width": 30, + "x": 11378, + "y": 3279.5 + }, + "absoluteRenderBounds": { + "height": 48, + "width": 30, + "x": 11378, + "y": 3279.5 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "componentProperties": { + "effect": { + "boundVariables": {}, + "type": "VARIANT", + "value": "default" + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "height": 48, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 10, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 8, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 8, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 10, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "FILL", + "maxHeight": null, + "maxWidth": null, + "name": "menu", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 8, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 8, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "effect": "default" + }, + "visible": true, + "width": 30, + "x": 333, + "y": 0, + "mainComponentId": "19:972", + "mainComponentName": "effect=default", + "mainComponentSetId": "19:973" + }, + "extra": {}, + "fieldErrors": {} + }, + "23:1588": { + "id": "23:1588", + "type": "INSTANCE", + "fields": { + "parentId": "23:1582", + "childrenIds": [ + "I23:1588;18:959" + ], + "absoluteBoundingBox": { + "height": 48, + "width": 59, + "x": 11448, + "y": 3279.5 + }, + "absoluteRenderBounds": { + "height": 48, + "width": 59, + "x": 11448, + "y": 3279.5 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "componentProperties": { + "effect": { + "boundVariables": {}, + "type": "VARIANT", + "value": "default" + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "height": 48, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 10, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 8, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 8, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 10, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "FILL", + "maxHeight": null, + "maxWidth": null, + "name": "menu", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 8, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 8, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "effect": "default" + }, + "visible": true, + "width": 59, + "x": 403, + "y": 0, + "mainComponentId": "19:972", + "mainComponentName": "effect=default", + "mainComponentSetId": "19:973" + }, + "extra": {}, + "fieldErrors": {} + }, + "23:1589": { + "id": "23:1589", + "type": "FRAME", + "fields": { + "parentId": "23:1577", + "childrenIds": [ + "23:1590", + "23:1591" + ], + "absoluteBoundingBox": { + "height": 48, + "width": 170, + "x": 11795, + "y": 3279.5 + }, + "absoluteRenderBounds": { + "height": 48, + "width": 170, + "x": 11795, + "y": 3279.5 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "height": 48, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 0, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "MAX", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "Frame 448", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "MAX", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 170, + "x": 1238, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "23:1590": { + "id": "23:1590", + "type": "INSTANCE", + "fields": { + "parentId": "23:1589", + "childrenIds": [ + "I23:1590;20:1333", + "I23:1590;20:1321" + ], + "absoluteBoundingBox": { + "height": 48, + "width": 48, + "x": 11829, + "y": 3279.5 + }, + "absoluteRenderBounds": null, + "bottomLeftRadius": 100, + "bottomRightRadius": 100, + "componentProperties": { + "effect": { + "boundVariables": {}, + "type": "VARIANT", + "value": "default" + }, + "text#20:1": { + "type": "BOOLEAN", + "value": false + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 100, + "counterAxisAlignItems": "CENTER", + "height": 48, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 8, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 12, + "paddingLeft": 12, + "paddingRight": 12, + "paddingTop": 12, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 8, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "iconButton", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 12, + "paddingLeft": 12, + "paddingRight": 12, + "paddingTop": 12, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 100, + "topRightRadius": 100, + "variantProperties": { + "effect": "default" + }, + "visible": false, + "width": 48, + "x": 34, + "y": 0, + "mainComponentId": "20:1348", + "mainComponentName": "effect=default", + "mainComponentSetId": "20:1349" + }, + "extra": {}, + "fieldErrors": {} + }, + "23:1591": { + "id": "23:1591", + "type": "INSTANCE", + "fields": { + "parentId": "23:1589", + "childrenIds": [ + "I23:1591;20:1333", + "I23:1591;20:1321" + ], + "absoluteBoundingBox": { + "height": 48, + "width": 88, + "x": 11877, + "y": 3279.5 + }, + "absoluteRenderBounds": { + "height": 48, + "width": 88, + "x": 11877, + "y": 3279.5 + }, + "bottomLeftRadius": 100, + "bottomRightRadius": 100, + "componentProperties": { + "effect": { + "boundVariables": {}, + "type": "VARIANT", + "value": "default" + }, + "text#20:1": { + "type": "BOOLEAN", + "value": true + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 100, + "counterAxisAlignItems": "CENTER", + "height": 48, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 8, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 12, + "paddingLeft": 12, + "paddingRight": 12, + "paddingTop": 12, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 8, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "iconButton", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 12, + "paddingLeft": 12, + "paddingRight": 12, + "paddingTop": 12, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 100, + "topRightRadius": 100, + "variantProperties": { + "effect": "default" + }, + "visible": true, + "width": 88, + "x": 82, + "y": 0, + "mainComponentId": "20:1348", + "mainComponentName": "effect=default", + "mainComponentSetId": "20:1349" + }, + "extra": {}, + "fieldErrors": {} + }, + "37:224": { + "id": "37:224", + "type": "INSTANCE", + "fields": { + "parentId": "23:1577", + "childrenIds": [ + "I37:224;136:9027", + "I37:224;136:9028", + "I37:224;136:9041" + ], + "absoluteBoundingBox": { + "height": 32, + "width": 200, + "x": 10557, + "y": 3287.5 + }, + "absoluteRenderBounds": { + "height": 32, + "width": 200, + "x": 10557, + "y": 3287.5 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "componentProperties": { + "속성 1": { + "boundVariables": {}, + "type": "VARIANT", + "value": "기본" + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "height": 32, + "isAsset": true, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Logo", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "targetAspectRatio": { + "x": 1102, + "y": 178 + }, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "속성 1": "기본" + }, + "visible": true, + "width": 200, + "x": 0, + "y": 8, + "mainComponentId": "37:217", + "mainComponentName": "속성 1=기본", + "mainComponentSetId": "136:19763" + }, + "extra": {}, + "fieldErrors": {} + }, + "40:265": { + "id": "40:265", + "type": "COMPONENT", + "fields": { + "parentId": "23:1575", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "40:266" + ], + "absoluteBoundingBox": { + "height": 80, + "width": 1440, + "x": 10541, + "y": 3464 + }, + "absoluteRenderBounds": { + "height": 80, + "width": 1440, + "x": 10541, + "y": 3464 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "height": 80, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 29, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 16, + "paddingRight": 16, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 29, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "type=default, viewport=Desktop", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 16, + "paddingRight": 16, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "type": "default", + "viewport": "Desktop" + }, + "visible": true, + "width": 1440, + "x": 21, + "y": 221 + }, + "extra": {}, + "fieldErrors": {} + }, + "40:266": { + "id": "40:266", + "type": "FRAME", + "fields": { + "parentId": "40:265", + "childrenIds": [ + "40:267", + "40:268", + "40:275" + ], + "absoluteBoundingBox": { + "height": 48, + "width": 1408, + "x": 10557, + "y": 3480 + }, + "absoluteRenderBounds": { + "height": 48, + "width": 1408, + "x": 10557, + "y": 3480 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "height": 48, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 29, + "layoutAlign": "INHERIT", + "layoutGrow": 1, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "SPACE_BETWEEN", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 29, + "layoutGrow": 1, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "Frame 450", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "SPACE_BETWEEN", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 1408, + "x": 16, + "y": 16 + }, + "extra": {}, + "fieldErrors": {} + }, + "40:267": { + "id": "40:267", + "type": "INSTANCE", + "fields": { + "parentId": "40:266", + "childrenIds": [ + "I40:267;136:9027", + "I40:267;136:9028", + "I40:267;136:9041" + ], + "absoluteBoundingBox": { + "height": 32, + "width": 200, + "x": 10557, + "y": 3488 + }, + "absoluteRenderBounds": { + "height": 32, + "width": 200, + "x": 10557, + "y": 3488 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "componentProperties": { + "속성 1": { + "boundVariables": {}, + "type": "VARIANT", + "value": "기본" + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "height": 32, + "isAsset": true, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Logo", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "targetAspectRatio": { + "x": 1102, + "y": 178 + }, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "속성 1": "기본" + }, + "visible": true, + "width": 200, + "x": 0, + "y": 8, + "mainComponentId": "37:217", + "mainComponentName": "속성 1=기본", + "mainComponentSetId": "136:19763" + }, + "extra": {}, + "fieldErrors": {} + }, + "40:268": { + "id": "40:268", + "type": "FRAME", + "fields": { + "parentId": "40:266", + "childrenIds": [ + "40:269", + "40:270", + "40:271", + "40:272", + "40:273" + ], + "absoluteBoundingBox": { + "height": 48, + "width": 462, + "x": 11045, + "y": 3480 + }, + "absoluteRenderBounds": { + "height": 48, + "width": 462, + "x": 11045, + "y": 3480 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "height": 48, + "inferredAutoLayout": { + "counterAxisAlignItems": "MIN", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 40, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 40, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "FILL", + "maxHeight": null, + "maxWidth": null, + "name": "Frame 446", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 462, + "x": 488, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "40:269": { + "id": "40:269", + "type": "INSTANCE", + "fields": { + "parentId": "40:268", + "childrenIds": [ + "I40:269;18:959" + ], + "absoluteBoundingBox": { + "height": 48, + "width": 59, + "x": 11045, + "y": 3480 + }, + "absoluteRenderBounds": { + "height": 48, + "width": 59, + "x": 11045, + "y": 3480 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "componentProperties": { + "effect": { + "boundVariables": {}, + "type": "VARIANT", + "value": "default" + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "height": 48, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 10, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 8, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 8, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 10, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "FILL", + "maxHeight": null, + "maxWidth": null, + "name": "menu", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 8, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 8, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "effect": "default" + }, + "visible": true, + "width": 59, + "x": 0, + "y": 0, + "mainComponentId": "19:972", + "mainComponentName": "effect=default", + "mainComponentSetId": "19:973" + }, + "extra": {}, + "fieldErrors": {} + }, + "40:270": { + "id": "40:270", + "type": "INSTANCE", + "fields": { + "parentId": "40:268", + "childrenIds": [ + "I40:270;18:959" + ], + "absoluteBoundingBox": { + "height": 48, + "width": 95, + "x": 11144, + "y": 3480 + }, + "absoluteRenderBounds": { + "height": 48, + "width": 95, + "x": 11144, + "y": 3480 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "componentProperties": { + "effect": { + "boundVariables": {}, + "type": "VARIANT", + "value": "default" + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "height": 48, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 10, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 8, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 8, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 10, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "FILL", + "maxHeight": null, + "maxWidth": null, + "name": "menu", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 8, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 8, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "effect": "default" + }, + "visible": true, + "width": 95, + "x": 99, + "y": 0, + "mainComponentId": "19:972", + "mainComponentName": "effect=default", + "mainComponentSetId": "19:973" + }, + "extra": {}, + "fieldErrors": {} + }, + "40:271": { + "id": "40:271", + "type": "INSTANCE", + "fields": { + "parentId": "40:268", + "childrenIds": [ + "I40:271;18:959" + ], + "absoluteBoundingBox": { + "height": 48, + "width": 59, + "x": 11279, + "y": 3480 + }, + "absoluteRenderBounds": { + "height": 48, + "width": 59, + "x": 11279, + "y": 3480 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "componentProperties": { + "effect": { + "boundVariables": {}, + "type": "VARIANT", + "value": "default" + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "height": 48, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 10, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 8, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 8, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 10, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "FILL", + "maxHeight": null, + "maxWidth": null, + "name": "menu", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 8, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 8, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "effect": "default" + }, + "visible": true, + "width": 59, + "x": 234, + "y": 0, + "mainComponentId": "19:972", + "mainComponentName": "effect=default", + "mainComponentSetId": "19:973" + }, + "extra": {}, + "fieldErrors": {} + }, + "40:272": { + "id": "40:272", + "type": "INSTANCE", + "fields": { + "parentId": "40:268", + "childrenIds": [ + "I40:272;18:959" + ], + "absoluteBoundingBox": { + "height": 48, + "width": 30, + "x": 11378, + "y": 3480 + }, + "absoluteRenderBounds": { + "height": 48, + "width": 30, + "x": 11378, + "y": 3480 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "componentProperties": { + "effect": { + "boundVariables": {}, + "type": "VARIANT", + "value": "default" + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "height": 48, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 10, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 8, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 8, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 10, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "FILL", + "maxHeight": null, + "maxWidth": null, + "name": "menu", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 8, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 8, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "effect": "default" + }, + "visible": true, + "width": 30, + "x": 333, + "y": 0, + "mainComponentId": "19:972", + "mainComponentName": "effect=default", + "mainComponentSetId": "19:973" + }, + "extra": {}, + "fieldErrors": {} + }, + "40:273": { + "id": "40:273", + "type": "INSTANCE", + "fields": { + "parentId": "40:268", + "childrenIds": [ + "I40:273;18:959" + ], + "absoluteBoundingBox": { + "height": 48, + "width": 59, + "x": 11448, + "y": 3480 + }, + "absoluteRenderBounds": { + "height": 48, + "width": 59, + "x": 11448, + "y": 3480 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "componentProperties": { + "effect": { + "boundVariables": {}, + "type": "VARIANT", + "value": "default" + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "height": 48, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 10, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 8, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 8, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 10, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "FILL", + "maxHeight": null, + "maxWidth": null, + "name": "menu", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 8, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 8, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "effect": "default" + }, + "visible": true, + "width": 59, + "x": 403, + "y": 0, + "mainComponentId": "19:972", + "mainComponentName": "effect=default", + "mainComponentSetId": "19:973" + }, + "extra": {}, + "fieldErrors": {} + }, + "40:275": { + "id": "40:275", + "type": "FRAME", + "fields": { + "parentId": "40:266", + "childrenIds": [ + "40:276", + "40:277" + ], + "absoluteBoundingBox": { + "height": 48, + "width": 170, + "x": 11795, + "y": 3480 + }, + "absoluteRenderBounds": { + "height": 48, + "width": 170, + "x": 11795, + "y": 3480 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "height": 48, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 0, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "MAX", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "Frame 448", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "MAX", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 170, + "x": 1238, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "40:276": { + "id": "40:276", + "type": "INSTANCE", + "fields": { + "parentId": "40:275", + "childrenIds": [ + "I40:276;20:1333", + "I40:276;20:1321" + ], + "absoluteBoundingBox": { + "height": 48, + "width": 48, + "x": 11829, + "y": 3480 + }, + "absoluteRenderBounds": null, + "bottomLeftRadius": 100, + "bottomRightRadius": 100, + "componentProperties": { + "effect": { + "boundVariables": {}, + "type": "VARIANT", + "value": "default" + }, + "text#20:1": { + "type": "BOOLEAN", + "value": false + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 100, + "counterAxisAlignItems": "CENTER", + "height": 48, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 8, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 12, + "paddingLeft": 12, + "paddingRight": 12, + "paddingTop": 12, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 8, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "iconButton", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 12, + "paddingLeft": 12, + "paddingRight": 12, + "paddingTop": 12, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 100, + "topRightRadius": 100, + "variantProperties": { + "effect": "default" + }, + "visible": false, + "width": 48, + "x": 34, + "y": 0, + "mainComponentId": "20:1348", + "mainComponentName": "effect=default", + "mainComponentSetId": "20:1349" + }, + "extra": {}, + "fieldErrors": {} + }, + "40:277": { + "id": "40:277", + "type": "INSTANCE", + "fields": { + "parentId": "40:275", + "childrenIds": [ + "I40:277;20:1333", + "I40:277;20:1321" + ], + "absoluteBoundingBox": { + "height": 48, + "width": 88, + "x": 11877, + "y": 3480 + }, + "absoluteRenderBounds": { + "height": 48, + "width": 88, + "x": 11877, + "y": 3480 + }, + "bottomLeftRadius": 100, + "bottomRightRadius": 100, + "componentProperties": { + "effect": { + "boundVariables": {}, + "type": "VARIANT", + "value": "default" + }, + "text#20:1": { + "type": "BOOLEAN", + "value": true + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 100, + "counterAxisAlignItems": "CENTER", + "height": 48, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 8, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 12, + "paddingLeft": 12, + "paddingRight": 12, + "paddingTop": 12, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 8, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "iconButton", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 12, + "paddingLeft": 12, + "paddingRight": 12, + "paddingTop": 12, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 100, + "topRightRadius": 100, + "variantProperties": { + "effect": "default" + }, + "visible": true, + "width": 88, + "x": 82, + "y": 0, + "mainComponentId": "20:1348", + "mainComponentName": "effect=default", + "mainComponentSetId": "20:1349" + }, + "extra": {}, + "fieldErrors": {} + }, + "43:1087": { + "id": "43:1087", + "type": "COMPONENT", + "fields": { + "parentId": "23:1575", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "43:1088", + "43:1147" + ], + "absoluteBoundingBox": { + "height": 60, + "width": 360, + "x": 10541, + "y": 3732 + }, + "absoluteRenderBounds": { + "height": 60, + "width": 360, + "x": 10541, + "y": 3732 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "height": 60, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 29, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 12, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 29, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "type=default, viewport=Mobile", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 12, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "type": "default", + "viewport": "Mobile" + }, + "visible": true, + "width": 360, + "x": 21, + "y": 489 + }, + "extra": {}, + "fieldErrors": {} + }, + "43:1088": { + "id": "43:1088", + "type": "FRAME", + "fields": { + "parentId": "43:1087", + "childrenIds": [ + "43:1089" + ], + "absoluteBoundingBox": { + "height": 28, + "width": 261, + "x": 10553, + "y": 3748 + }, + "absoluteRenderBounds": { + "height": 28, + "width": 261, + "x": 10553, + "y": 3748 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "height": 28, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 29, + "layoutAlign": "INHERIT", + "layoutGrow": 1, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "SPACE_BETWEEN", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 29, + "layoutGrow": 1, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "Frame 450", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "SPACE_BETWEEN", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 261, + "x": 12, + "y": 16 + }, + "extra": {}, + "fieldErrors": {} + }, + "43:1089": { + "id": "43:1089", + "type": "INSTANCE", + "fields": { + "parentId": "43:1088", + "childrenIds": [ + "I43:1089;136:9027", + "I43:1089;136:9028", + "I43:1089;136:9041" + ], + "absoluteBoundingBox": { + "height": 28, + "width": 173, + "x": 10553, + "y": 3748 + }, + "absoluteRenderBounds": { + "height": 28, + "width": 173, + "x": 10553, + "y": 3748 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "componentProperties": { + "속성 1": { + "boundVariables": {}, + "type": "VARIANT", + "value": "기본" + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "height": 28, + "isAsset": true, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Logo", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "targetAspectRatio": { + "x": 1102, + "y": 178 + }, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "속성 1": "기본" + }, + "visible": true, + "width": 173, + "x": 0, + "y": 0, + "mainComponentId": "37:217", + "mainComponentName": "속성 1=기본", + "mainComponentSetId": "136:19763" + }, + "extra": {}, + "fieldErrors": {} + }, + "43:1099": { + "id": "43:1099", + "type": "COMPONENT", + "fields": { + "parentId": "23:1575", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "43:1100", + "43:1146" + ], + "absoluteBoundingBox": { + "height": 64, + "width": 360, + "x": 10541, + "y": 3564 + }, + "absoluteRenderBounds": { + "height": 64, + "width": 360, + "x": 10541, + "y": 3564 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:350", + "type": "VARIABLE_ALIAS" + } + ], + "strokes": [ + { + "id": "VariableID:9:351", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:350", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 64, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 29, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 12, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 29, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "type=scroll, viewport=Mobile", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 12, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 0, + "strokeRightWeight": 0, + "strokeTopWeight": 0, + "strokeWeight": { + "$unsupported": "symbol" + }, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:351", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.9215686321258544, + "g": 0.9058823585510254, + "r": 0.8980392217636108 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "type": "scroll", + "viewport": "Mobile" + }, + "visible": true, + "width": 360, + "x": 21, + "y": 321 + }, + "extra": {}, + "fieldErrors": {} + }, + "43:1100": { + "id": "43:1100", + "type": "FRAME", + "fields": { + "parentId": "43:1099", + "childrenIds": [ + "43:1101" + ], + "absoluteBoundingBox": { + "height": 28, + "width": 261, + "x": 10553, + "y": 3581.5 + }, + "absoluteRenderBounds": { + "height": 28, + "width": 261, + "x": 10553, + "y": 3581.5 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "height": 28, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 29, + "layoutAlign": "INHERIT", + "layoutGrow": 1, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "SPACE_BETWEEN", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 29, + "layoutGrow": 1, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "Frame 450", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "SPACE_BETWEEN", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 261, + "x": 12, + "y": 17.5 + }, + "extra": {}, + "fieldErrors": {} + }, + "43:1101": { + "id": "43:1101", + "type": "INSTANCE", + "fields": { + "parentId": "43:1100", + "childrenIds": [ + "I43:1101;136:9027", + "I43:1101;136:9028", + "I43:1101;136:9041" + ], + "absoluteBoundingBox": { + "height": 28, + "width": 173, + "x": 10553, + "y": 3581.5 + }, + "absoluteRenderBounds": { + "height": 28, + "width": 173, + "x": 10553, + "y": 3581.5 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "componentProperties": { + "속성 1": { + "boundVariables": {}, + "type": "VARIANT", + "value": "기본" + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "height": 28, + "isAsset": true, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Logo", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "targetAspectRatio": { + "x": 1102, + "y": 178 + }, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "속성 1": "기본" + }, + "visible": true, + "width": 173, + "x": 0, + "y": 0, + "mainComponentId": "37:217", + "mainComponentName": "속성 1=기본", + "mainComponentSetId": "136:19763" + }, + "extra": {}, + "fieldErrors": {} + }, + "43:1140": { + "id": "43:1140", + "type": "INSTANCE", + "fields": { + "parentId": "43:1146", + "childrenIds": [ + "I43:1140;43:1138" + ], + "absoluteBoundingBox": { + "height": 26, + "width": 26, + "x": 10859, + "y": 3582.5 + }, + "absoluteRenderBounds": { + "height": 26, + "width": 26, + "x": 10859, + "y": 3582.5 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": false + } + ], + "height": 26, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 5.6875, + "paddingLeft": 3.25, + "paddingRight": 3.25, + "paddingTop": 5.6875, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "isAsset": true, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "List", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "targetAspectRatio": { + "x": 24, + "y": 24 + }, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 26, + "x": 16, + "y": 18.5, + "mainComponentId": "43:1139", + "mainComponentName": "List", + "mainComponentSetId": null + }, + "extra": {}, + "fieldErrors": {} + }, + "43:1146": { + "id": "43:1146", + "type": "FRAME", + "fields": { + "parentId": "43:1099", + "childrenIds": [ + "90:10317", + "43:1140" + ], + "absoluteBoundingBox": { + "height": 63, + "width": 58, + "x": 10843, + "y": 3564 + }, + "absoluteRenderBounds": { + "height": 63, + "width": 58, + "x": 10843, + "y": 3564 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "height": 63, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 12, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 16, + "paddingRight": 16, + "paddingTop": 0, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 12, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "FILL", + "maxHeight": null, + "maxWidth": null, + "name": "Frame 498", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 16, + "paddingRight": 16, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 58, + "x": 302, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "43:1147": { + "id": "43:1147", + "type": "FRAME", + "fields": { + "parentId": "43:1087", + "childrenIds": [ + "90:10393", + "43:1148" + ], + "absoluteBoundingBox": { + "height": 60, + "width": 58, + "x": 10843, + "y": 3732 + }, + "absoluteRenderBounds": { + "height": 60, + "width": 58, + "x": 10843, + "y": 3732 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "height": 60, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 12, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 16, + "paddingRight": 16, + "paddingTop": 0, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 12, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "FILL", + "maxHeight": null, + "maxWidth": null, + "name": "Frame 498", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 16, + "paddingRight": 16, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 58, + "x": 302, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "43:1148": { + "id": "43:1148", + "type": "INSTANCE", + "fields": { + "parentId": "43:1147", + "childrenIds": [ + "I43:1148;43:1138" + ], + "absoluteBoundingBox": { + "height": 26, + "width": 26, + "x": 10859, + "y": 3749 + }, + "absoluteRenderBounds": { + "height": 26, + "width": 26, + "x": 10859, + "y": 3749 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": false + } + ], + "height": 26, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 5.6875, + "paddingLeft": 3.25, + "paddingRight": 3.25, + "paddingTop": 5.6875, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "isAsset": true, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "List", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "targetAspectRatio": { + "x": 24, + "y": 24 + }, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 26, + "x": 16, + "y": 17, + "mainComponentId": "43:1139", + "mainComponentName": "List", + "mainComponentSetId": null + }, + "extra": {}, + "fieldErrors": {} + }, + "44:5181": { + "id": "44:5181", + "type": "COMPONENT", + "fields": { + "parentId": "23:1575", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "44:5182" + ], + "absoluteBoundingBox": { + "height": 80, + "width": 1440, + "x": 10541, + "y": 3364 + }, + "absoluteRenderBounds": { + "height": 80, + "width": 1440, + "x": 10541, + "y": 3364 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "height": 80, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 29, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 16, + "paddingRight": 16, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 29, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "type=pageTop, viewport=Desktop", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 16, + "paddingRight": 16, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 0, + "strokeRightWeight": 0, + "strokeTopWeight": 0, + "strokeWeight": { + "$unsupported": "symbol" + }, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "type": "pageTop", + "viewport": "Desktop" + }, + "visible": true, + "width": 1440, + "x": 21, + "y": 121 + }, + "extra": {}, + "fieldErrors": {} + }, + "44:5182": { + "id": "44:5182", + "type": "FRAME", + "fields": { + "parentId": "44:5181", + "childrenIds": [ + "44:5183", + "44:5184", + "44:5190" + ], + "absoluteBoundingBox": { + "height": 48, + "width": 1408, + "x": 10557, + "y": 3380 + }, + "absoluteRenderBounds": { + "height": 48, + "width": 1408, + "x": 10557, + "y": 3380 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "height": 48, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 29, + "layoutAlign": "INHERIT", + "layoutGrow": 1, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "SPACE_BETWEEN", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 29, + "layoutGrow": 1, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "Frame 450", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "SPACE_BETWEEN", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 1408, + "x": 16, + "y": 16 + }, + "extra": {}, + "fieldErrors": {} + }, + "44:5183": { + "id": "44:5183", + "type": "INSTANCE", + "fields": { + "parentId": "44:5182", + "childrenIds": [ + "I44:5183;136:9027", + "I44:5183;136:9028", + "I44:5183;136:9041" + ], + "absoluteBoundingBox": { + "height": 32, + "width": 200, + "x": 10557, + "y": 3388 + }, + "absoluteRenderBounds": { + "height": 32, + "width": 200, + "x": 10557, + "y": 3388 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "componentProperties": { + "속성 1": { + "boundVariables": {}, + "type": "VARIANT", + "value": "기본" + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "height": 32, + "isAsset": true, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Logo", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "targetAspectRatio": { + "x": 1102, + "y": 178 + }, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "속성 1": "기본" + }, + "visible": true, + "width": 200, + "x": 0, + "y": 8, + "mainComponentId": "37:217", + "mainComponentName": "속성 1=기본", + "mainComponentSetId": "136:19763" + }, + "extra": {}, + "fieldErrors": {} + }, + "44:5184": { + "id": "44:5184", + "type": "FRAME", + "fields": { + "parentId": "44:5182", + "childrenIds": [ + "44:5185", + "44:5186", + "44:5187", + "44:5188", + "44:5189" + ], + "absoluteBoundingBox": { + "height": 48, + "width": 462, + "x": 11045, + "y": 3380 + }, + "absoluteRenderBounds": { + "height": 48, + "width": 462, + "x": 11045, + "y": 3380 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "height": 48, + "inferredAutoLayout": { + "counterAxisAlignItems": "MIN", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 40, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 40, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "FILL", + "maxHeight": null, + "maxWidth": null, + "name": "Frame 446", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 462, + "x": 488, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "44:5185": { + "id": "44:5185", + "type": "INSTANCE", + "fields": { + "parentId": "44:5184", + "childrenIds": [ + "I44:5185;18:959" + ], + "absoluteBoundingBox": { + "height": 48, + "width": 59, + "x": 11045, + "y": 3380 + }, + "absoluteRenderBounds": { + "height": 48, + "width": 59, + "x": 11045, + "y": 3380 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "componentProperties": { + "effect": { + "boundVariables": {}, + "type": "VARIANT", + "value": "default" + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "height": 48, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 10, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 8, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 8, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 10, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "FILL", + "maxHeight": null, + "maxWidth": null, + "name": "menu", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 8, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 8, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "effect": "default" + }, + "visible": true, + "width": 59, + "x": 0, + "y": 0, + "mainComponentId": "19:972", + "mainComponentName": "effect=default", + "mainComponentSetId": "19:973" + }, + "extra": {}, + "fieldErrors": {} + }, + "44:5186": { + "id": "44:5186", + "type": "INSTANCE", + "fields": { + "parentId": "44:5184", + "childrenIds": [ + "I44:5186;18:959" + ], + "absoluteBoundingBox": { + "height": 48, + "width": 95, + "x": 11144, + "y": 3380 + }, + "absoluteRenderBounds": { + "height": 48, + "width": 95, + "x": 11144, + "y": 3380 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "componentProperties": { + "effect": { + "boundVariables": {}, + "type": "VARIANT", + "value": "default" + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "height": 48, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 10, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 8, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 8, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 10, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "FILL", + "maxHeight": null, + "maxWidth": null, + "name": "menu", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 8, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 8, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "effect": "default" + }, + "visible": true, + "width": 95, + "x": 99, + "y": 0, + "mainComponentId": "19:972", + "mainComponentName": "effect=default", + "mainComponentSetId": "19:973" + }, + "extra": {}, + "fieldErrors": {} + }, + "44:5187": { + "id": "44:5187", + "type": "INSTANCE", + "fields": { + "parentId": "44:5184", + "childrenIds": [ + "I44:5187;18:959" + ], + "absoluteBoundingBox": { + "height": 48, + "width": 59, + "x": 11279, + "y": 3380 + }, + "absoluteRenderBounds": { + "height": 48, + "width": 59, + "x": 11279, + "y": 3380 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "componentProperties": { + "effect": { + "boundVariables": {}, + "type": "VARIANT", + "value": "default" + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "height": 48, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 10, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 8, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 8, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 10, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "FILL", + "maxHeight": null, + "maxWidth": null, + "name": "menu", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 8, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 8, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "effect": "default" + }, + "visible": true, + "width": 59, + "x": 234, + "y": 0, + "mainComponentId": "19:972", + "mainComponentName": "effect=default", + "mainComponentSetId": "19:973" + }, + "extra": {}, + "fieldErrors": {} + }, + "44:5188": { + "id": "44:5188", + "type": "INSTANCE", + "fields": { + "parentId": "44:5184", + "childrenIds": [ + "I44:5188;18:959" + ], + "absoluteBoundingBox": { + "height": 48, + "width": 30, + "x": 11378, + "y": 3380 + }, + "absoluteRenderBounds": { + "height": 48, + "width": 30, + "x": 11378, + "y": 3380 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "componentProperties": { + "effect": { + "boundVariables": {}, + "type": "VARIANT", + "value": "default" + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "height": 48, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 10, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 8, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 8, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 10, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "FILL", + "maxHeight": null, + "maxWidth": null, + "name": "menu", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 8, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 8, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "effect": "default" + }, + "visible": true, + "width": 30, + "x": 333, + "y": 0, + "mainComponentId": "19:972", + "mainComponentName": "effect=default", + "mainComponentSetId": "19:973" + }, + "extra": {}, + "fieldErrors": {} + }, + "44:5189": { + "id": "44:5189", + "type": "INSTANCE", + "fields": { + "parentId": "44:5184", + "childrenIds": [ + "I44:5189;18:959" + ], + "absoluteBoundingBox": { + "height": 48, + "width": 59, + "x": 11448, + "y": 3380 + }, + "absoluteRenderBounds": { + "height": 48, + "width": 59, + "x": 11448, + "y": 3380 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "componentProperties": { + "effect": { + "boundVariables": {}, + "type": "VARIANT", + "value": "default" + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "height": 48, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 10, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 8, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 8, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 10, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "FILL", + "maxHeight": null, + "maxWidth": null, + "name": "menu", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 8, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 8, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "effect": "default" + }, + "visible": true, + "width": 59, + "x": 403, + "y": 0, + "mainComponentId": "19:972", + "mainComponentName": "effect=default", + "mainComponentSetId": "19:973" + }, + "extra": {}, + "fieldErrors": {} + }, + "44:5190": { + "id": "44:5190", + "type": "FRAME", + "fields": { + "parentId": "44:5182", + "childrenIds": [ + "44:5191", + "44:5192" + ], + "absoluteBoundingBox": { + "height": 48, + "width": 170, + "x": 11795, + "y": 3380 + }, + "absoluteRenderBounds": { + "height": 48, + "width": 170, + "x": 11795, + "y": 3380 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "height": 48, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 0, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "MAX", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "Frame 448", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "MAX", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 170, + "x": 1238, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "44:5191": { + "id": "44:5191", + "type": "INSTANCE", + "fields": { + "parentId": "44:5190", + "childrenIds": [ + "I44:5191;20:1333", + "I44:5191;20:1321" + ], + "absoluteBoundingBox": { + "height": 48, + "width": 48, + "x": 11829, + "y": 3380 + }, + "absoluteRenderBounds": null, + "bottomLeftRadius": 100, + "bottomRightRadius": 100, + "componentProperties": { + "effect": { + "boundVariables": {}, + "type": "VARIANT", + "value": "default" + }, + "text#20:1": { + "type": "BOOLEAN", + "value": false + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 100, + "counterAxisAlignItems": "CENTER", + "height": 48, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 8, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 12, + "paddingLeft": 12, + "paddingRight": 12, + "paddingTop": 12, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 8, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "iconButton", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 12, + "paddingLeft": 12, + "paddingRight": 12, + "paddingTop": 12, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 100, + "topRightRadius": 100, + "variantProperties": { + "effect": "default" + }, + "visible": false, + "width": 48, + "x": 34, + "y": 0, + "mainComponentId": "20:1348", + "mainComponentName": "effect=default", + "mainComponentSetId": "20:1349" + }, + "extra": {}, + "fieldErrors": {} + }, + "44:5192": { + "id": "44:5192", + "type": "INSTANCE", + "fields": { + "parentId": "44:5190", + "childrenIds": [ + "I44:5192;20:1333", + "I44:5192;20:1321" + ], + "absoluteBoundingBox": { + "height": 48, + "width": 88, + "x": 11877, + "y": 3380 + }, + "absoluteRenderBounds": { + "height": 48, + "width": 88, + "x": 11877, + "y": 3380 + }, + "bottomLeftRadius": 100, + "bottomRightRadius": 100, + "componentProperties": { + "effect": { + "boundVariables": {}, + "type": "VARIANT", + "value": "default" + }, + "text#20:1": { + "type": "BOOLEAN", + "value": true + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 100, + "counterAxisAlignItems": "CENTER", + "height": 48, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 8, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 12, + "paddingLeft": 12, + "paddingRight": 12, + "paddingTop": 12, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 8, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "iconButton", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 12, + "paddingLeft": 12, + "paddingRight": 12, + "paddingTop": 12, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 100, + "topRightRadius": 100, + "variantProperties": { + "effect": "default" + }, + "visible": true, + "width": 88, + "x": 82, + "y": 0, + "mainComponentId": "20:1348", + "mainComponentName": "effect=default", + "mainComponentSetId": "20:1349" + }, + "extra": {}, + "fieldErrors": {} + }, + "44:5193": { + "id": "44:5193", + "type": "COMPONENT", + "fields": { + "parentId": "23:1575", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "44:5194", + "44:5196" + ], + "absoluteBoundingBox": { + "height": 64, + "width": 360, + "x": 10541, + "y": 3648 + }, + "absoluteRenderBounds": { + "height": 64, + "width": 360, + "x": 10541, + "y": 3648 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "height": 64, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 29, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 12, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 29, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "type=pageTop, viewport=Mobile", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 12, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 0, + "strokeRightWeight": 0, + "strokeTopWeight": 0, + "strokeWeight": { + "$unsupported": "symbol" + }, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "type": "pageTop", + "viewport": "Mobile" + }, + "visible": true, + "width": 360, + "x": 21, + "y": 405 + }, + "extra": {}, + "fieldErrors": {} + }, + "44:5194": { + "id": "44:5194", + "type": "FRAME", + "fields": { + "parentId": "44:5193", + "childrenIds": [ + "44:5195" + ], + "absoluteBoundingBox": { + "height": 28, + "width": 261, + "x": 10553, + "y": 3666 + }, + "absoluteRenderBounds": { + "height": 28, + "width": 261, + "x": 10553, + "y": 3666 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "height": 28, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 29, + "layoutAlign": "INHERIT", + "layoutGrow": 1, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "SPACE_BETWEEN", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 29, + "layoutGrow": 1, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "Frame 450", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "SPACE_BETWEEN", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 261, + "x": 12, + "y": 18 + }, + "extra": {}, + "fieldErrors": {} + }, + "44:5195": { + "id": "44:5195", + "type": "INSTANCE", + "fields": { + "parentId": "44:5194", + "childrenIds": [ + "I44:5195;136:9027", + "I44:5195;136:9028", + "I44:5195;136:9041" + ], + "absoluteBoundingBox": { + "height": 28, + "width": 173, + "x": 10553, + "y": 3666 + }, + "absoluteRenderBounds": { + "height": 28, + "width": 173, + "x": 10553, + "y": 3666 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "componentProperties": { + "속성 1": { + "boundVariables": {}, + "type": "VARIANT", + "value": "기본" + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "height": 28, + "isAsset": true, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Logo", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "targetAspectRatio": { + "x": 1102, + "y": 178 + }, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "속성 1": "기본" + }, + "visible": true, + "width": 173, + "x": 0, + "y": 0, + "mainComponentId": "37:217", + "mainComponentName": "속성 1=기본", + "mainComponentSetId": "136:19763" + }, + "extra": {}, + "fieldErrors": {} + }, + "44:5196": { + "id": "44:5196", + "type": "FRAME", + "fields": { + "parentId": "44:5193", + "childrenIds": [ + "90:10324", + "44:5197" + ], + "absoluteBoundingBox": { + "height": 64, + "width": 58, + "x": 10843, + "y": 3648 + }, + "absoluteRenderBounds": { + "height": 64, + "width": 58, + "x": 10843, + "y": 3648 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "height": 64, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 12, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 16, + "paddingRight": 16, + "paddingTop": 0, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 12, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "FILL", + "maxHeight": null, + "maxWidth": null, + "name": "Frame 498", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 16, + "paddingRight": 16, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 58, + "x": 302, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "44:5197": { + "id": "44:5197", + "type": "INSTANCE", + "fields": { + "parentId": "44:5196", + "childrenIds": [ + "I44:5197;43:1138" + ], + "absoluteBoundingBox": { + "height": 26, + "width": 26, + "x": 10859, + "y": 3667 + }, + "absoluteRenderBounds": { + "height": 26, + "width": 26, + "x": 10859, + "y": 3667 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": false + } + ], + "height": 26, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 5.6875, + "paddingLeft": 3.25, + "paddingRight": 3.25, + "paddingTop": 5.6875, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "isAsset": true, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "List", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "targetAspectRatio": { + "x": 24, + "y": 24 + }, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 26, + "x": 16, + "y": 19, + "mainComponentId": "43:1139", + "mainComponentName": "List", + "mainComponentSetId": null + }, + "extra": {}, + "fieldErrors": {} + }, + "90:10317": { + "id": "90:10317", + "type": "INSTANCE", + "fields": { + "parentId": "43:1146", + "childrenIds": [ + "I90:10317;40:437" + ], + "absoluteBoundingBox": { + "height": 26, + "width": 26, + "x": 10859, + "y": 3582.5 + }, + "absoluteRenderBounds": null, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": false + } + ], + "height": 26, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 2.437045097351074, + "paddingLeft": 2.4150514602661133, + "paddingRight": 2.437045097351074, + "paddingTop": 2.4150514602661133, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "MagnifyingGlass", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "targetAspectRatio": { + "x": 24, + "y": 24 + }, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": false, + "width": 26, + "x": 16, + "y": 18.5, + "mainComponentId": "40:438", + "mainComponentName": "MagnifyingGlass", + "mainComponentSetId": null + }, + "extra": {}, + "fieldErrors": {} + }, + "90:10324": { + "id": "90:10324", + "type": "INSTANCE", + "fields": { + "parentId": "44:5196", + "childrenIds": [ + "I90:10324;40:437" + ], + "absoluteBoundingBox": { + "height": 26, + "width": 26, + "x": 10859, + "y": 3667 + }, + "absoluteRenderBounds": null, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": false + } + ], + "height": 26, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 2.437045097351074, + "paddingLeft": 2.4150514602661133, + "paddingRight": 2.437045097351074, + "paddingTop": 2.4150514602661133, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "MagnifyingGlass", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "targetAspectRatio": { + "x": 24, + "y": 24 + }, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": false, + "width": 26, + "x": 16, + "y": 19, + "mainComponentId": "40:438", + "mainComponentName": "MagnifyingGlass", + "mainComponentSetId": null + }, + "extra": {}, + "fieldErrors": {} + }, + "90:10393": { + "id": "90:10393", + "type": "INSTANCE", + "fields": { + "parentId": "43:1147", + "childrenIds": [ + "I90:10393;40:437" + ], + "absoluteBoundingBox": { + "height": 26, + "width": 26, + "x": 10859, + "y": 3749 + }, + "absoluteRenderBounds": null, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": false + } + ], + "height": 26, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 2.437045097351074, + "paddingLeft": 2.4150514602661133, + "paddingRight": 2.437045097351074, + "paddingTop": 2.4150514602661133, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "MagnifyingGlass", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "targetAspectRatio": { + "x": 24, + "y": 24 + }, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": false, + "width": 26, + "x": 16, + "y": 17, + "mainComponentId": "40:438", + "mainComponentName": "MagnifyingGlass", + "mainComponentSetId": null + }, + "extra": {}, + "fieldErrors": {} + }, + "I23:1583;18:959": { + "id": "I23:1583;18:959", + "type": "TEXT", + "fields": { + "parentId": "23:1583", + "absoluteBoundingBox": { + "height": 17, + "width": 59, + "x": 11045, + "y": 3295 + }, + "absoluteRenderBounds": { + "height": 15.130126953125, + "width": 56.3564453125, + "x": 11045.6630859375, + "y": 3296.22998046875 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:44:5420", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "기업소개", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.1529411822557449, + "g": 0.08627451211214066, + "r": 0.06666667014360428 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Bold" + }, + "fontSize": 17, + "height": 17, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 100 + }, + "maxHeight": null, + "maxWidth": null, + "name": "menu", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:ab9a4b96cc300838fb4b14185fdd981a07ac2475,", + "textTruncation": "DISABLED", + "visible": true, + "width": 59, + "x": 0, + "y": 15.5, + "styledTextSegments": [ + { + "characters": "기업소개", + "end": 4, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:ab9a4b96cc300838fb4b14185fdd981a07ac2475," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "I23:1584;18:959": { + "id": "I23:1584;18:959", + "type": "TEXT", + "fields": { + "parentId": "23:1584", + "absoluteBoundingBox": { + "height": 17, + "width": 95, + "x": 11144, + "y": 3295 + }, + "absoluteRenderBounds": { + "height": 15.1640625, + "width": 93.427734375, + "x": 11144.6630859375, + "y": 3296.22998046875 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:44:5420", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "기술/연구개발", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.1529411822557449, + "g": 0.08627451211214066, + "r": 0.06666667014360428 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Bold" + }, + "fontSize": 17, + "height": 17, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 100 + }, + "maxHeight": null, + "maxWidth": null, + "name": "menu", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:ab9a4b96cc300838fb4b14185fdd981a07ac2475,", + "textTruncation": "DISABLED", + "visible": true, + "width": 95, + "x": 0, + "y": 15.5, + "styledTextSegments": [ + { + "characters": "기술/연구개발", + "end": 7, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:ab9a4b96cc300838fb4b14185fdd981a07ac2475," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "I23:1585;18:959": { + "id": "I23:1585;18:959", + "type": "TEXT", + "fields": { + "parentId": "23:1585", + "absoluteBoundingBox": { + "height": 17, + "width": 59, + "x": 11279, + "y": 3295 + }, + "absoluteRenderBounds": { + "height": 15.130126953125, + "width": 56.7138671875, + "x": 11279.3056640625, + "y": 3296.22998046875 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:44:5420", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "제품소개", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.1529411822557449, + "g": 0.08627451211214066, + "r": 0.06666667014360428 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Bold" + }, + "fontSize": 17, + "height": 17, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 100 + }, + "maxHeight": null, + "maxWidth": null, + "name": "menu", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:ab9a4b96cc300838fb4b14185fdd981a07ac2475,", + "textTruncation": "DISABLED", + "visible": true, + "width": 59, + "x": 0, + "y": 15.5, + "styledTextSegments": [ + { + "characters": "제품소개", + "end": 4, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:ab9a4b96cc300838fb4b14185fdd981a07ac2475," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "I23:1586;18:959": { + "id": "I23:1586;18:959", + "type": "TEXT", + "fields": { + "parentId": "23:1586", + "absoluteBoundingBox": { + "height": 17, + "width": 30, + "x": 11378, + "y": 3295 + }, + "absoluteRenderBounds": { + "height": 15.1298828125, + "width": 26.99609375, + "x": 11378.578125, + "y": 3296.2470703125 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:44:5420", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "소식", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.1529411822557449, + "g": 0.08627451211214066, + "r": 0.06666667014360428 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Bold" + }, + "fontSize": 17, + "height": 17, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 100 + }, + "maxHeight": null, + "maxWidth": null, + "name": "menu", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:ab9a4b96cc300838fb4b14185fdd981a07ac2475,", + "textTruncation": "DISABLED", + "visible": true, + "width": 30, + "x": 0, + "y": 15.5, + "styledTextSegments": [ + { + "characters": "소식", + "end": 2, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:ab9a4b96cc300838fb4b14185fdd981a07ac2475," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "I23:1588;18:959": { + "id": "I23:1588;18:959", + "type": "TEXT", + "fields": { + "parentId": "23:1588", + "absoluteBoundingBox": { + "height": 17, + "width": 59, + "x": 11448, + "y": 3295 + }, + "absoluteRenderBounds": { + "height": 15.18115234375, + "width": 56.0849609375, + "x": 11448.560546875, + "y": 3296.212890625 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:44:5420", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "문의하기", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.1529411822557449, + "g": 0.08627451211214066, + "r": 0.06666667014360428 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Bold" + }, + "fontSize": 17, + "height": 17, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 100 + }, + "maxHeight": null, + "maxWidth": null, + "name": "menu", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:ab9a4b96cc300838fb4b14185fdd981a07ac2475,", + "textTruncation": "DISABLED", + "visible": true, + "width": 59, + "x": 0, + "y": 15.5, + "styledTextSegments": [ + { + "characters": "문의하기", + "end": 4, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:ab9a4b96cc300838fb4b14185fdd981a07ac2475," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "I23:1590;20:1321": { + "id": "I23:1590;20:1321", + "type": "TEXT", + "fields": { + "parentId": "23:1590", + "absoluteBoundingBox": { + "height": 17, + "width": 37, + "x": 11873, + "y": 3291.5 + }, + "absoluteRenderBounds": null, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:44:5420", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "KOR", + "componentPropertyReferences": { + "visible": "text#20:1" + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.1529411822557449, + "g": 0.08627451211214066, + "r": 0.06666667014360428 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Bold" + }, + "fontSize": 17, + "height": 17, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 100 + }, + "maxHeight": null, + "maxWidth": null, + "name": "KOR", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:ab9a4b96cc300838fb4b14185fdd981a07ac2475,", + "textTruncation": "DISABLED", + "visible": false, + "width": 37, + "x": 44, + "y": 12, + "styledTextSegments": [ + { + "characters": "KOR", + "end": 3, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:ab9a4b96cc300838fb4b14185fdd981a07ac2475," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "I23:1590;20:1333": { + "id": "I23:1590;20:1333", + "type": "INSTANCE", + "fields": { + "parentId": "23:1590", + "childrenIds": [ + "I23:1590;20:1333;40:437" + ], + "absoluteBoundingBox": { + "height": 24, + "width": 24, + "x": 11841, + "y": 3291.5 + }, + "absoluteRenderBounds": null, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": false + } + ], + "height": 24, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 2.2495806217193604, + "paddingLeft": 2.229278326034546, + "paddingRight": 2.2495806217193604, + "paddingTop": 2.229278326034546, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "MagnifyingGlass", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "targetAspectRatio": { + "x": 24, + "y": 24 + }, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 24, + "x": 12, + "y": 12, + "mainComponentId": "40:438", + "mainComponentName": "MagnifyingGlass", + "mainComponentSetId": null + }, + "extra": {}, + "fieldErrors": {} + }, + "I23:1590;20:1333;40:437": { + "id": "I23:1590;20:1333;40:437", + "type": "VECTOR", + "fields": { + "parentId": "I23:1590;20:1333", + "absoluteBoundingBox": { + "height": 19.521141052246097, + "width": 19.521141052246097, + "x": 11843.2294921875, + "y": 3293.729248046875 + }, + "absoluteRenderBounds": null, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.1529411822557449, + "g": 0.08627451211214066, + "r": 0.06666667014360428 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 19.521141052246097, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeWeight": 0.09375, + "visible": true, + "width": 19.521141052246097, + "x": 2.229278326034546, + "y": 2.229278326034546 + }, + "extra": {}, + "fieldErrors": {} + }, + "I23:1591;20:1321": { + "id": "I23:1591;20:1321", + "type": "TEXT", + "fields": { + "parentId": "23:1591", + "absoluteBoundingBox": { + "height": 23, + "width": 32, + "x": 11921, + "y": 3292 + }, + "absoluteRenderBounds": { + "height": 11.31005859375, + "width": 29.6357421875, + "x": 11922.2001953125, + "y": 3297.929931640625 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:40:430", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "KOR", + "componentPropertyReferences": { + "visible": "text#20:1" + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.1529411822557449, + "g": 0.08627451211214066, + "r": 0.06666667014360428 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Bold" + }, + "fontSize": 15, + "height": 23, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "KOR", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:01da56e122aa148e08cea720f74dbe1f2d759cdb,", + "textTruncation": "DISABLED", + "visible": true, + "width": 32, + "x": 44, + "y": 12.5, + "styledTextSegments": [ + { + "characters": "KOR", + "end": 3, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:01da56e122aa148e08cea720f74dbe1f2d759cdb," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "I23:1591;20:1333": { + "id": "I23:1591;20:1333", + "type": "INSTANCE", + "fields": { + "parentId": "23:1591", + "childrenIds": [ + "I23:1591;20:1333;40:440" + ], + "absoluteBoundingBox": { + "height": 24, + "width": 24, + "x": 11889, + "y": 3291.5 + }, + "absoluteRenderBounds": { + "height": 24, + "width": 24, + "x": 11889, + "y": 3291.5 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": false + } + ], + "height": 24, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 2.2499961853027344, + "paddingLeft": 2.249995708465576, + "paddingRight": 2.250000476837158, + "paddingTop": 2.25, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "isAsset": true, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Globe", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "targetAspectRatio": { + "x": 24, + "y": 24 + }, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 24, + "x": 12, + "y": 12, + "mainComponentId": "40:441", + "mainComponentName": "Globe", + "mainComponentSetId": null + }, + "extra": {}, + "fieldErrors": {} + }, + "I23:1591;20:1333;40:440": { + "id": "I23:1591;20:1333;40:440", + "type": "VECTOR", + "fields": { + "parentId": "I23:1591;20:1333", + "absoluteBoundingBox": { + "height": 19.500003814697266, + "width": 19.500003814697266, + "x": 11891.25, + "y": 3293.75 + }, + "absoluteRenderBounds": { + "height": 19.5, + "width": 19.5, + "x": 11891.25, + "y": 3293.75 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.1529411822557449, + "g": 0.08627451211214066, + "r": 0.06666667014360428 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 19.500003814697266, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeWeight": 0.09375, + "visible": true, + "width": 19.500003814697266, + "x": 2.249995708465576, + "y": 2.25 + }, + "extra": {}, + "fieldErrors": {} + }, + "I37:224;136:9027": { + "id": "I37:224;136:9027", + "type": "VECTOR", + "fields": { + "parentId": "37:224", + "absoluteBoundingBox": { + "height": 32, + "width": 32.304901123046875, + "x": 10557, + "y": 3287.5 + }, + "absoluteRenderBounds": { + "height": 32, + "width": 32.3046875, + "x": 10557, + "y": 3287.5 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.7490196228027344, + "g": 0.5607843399047852, + "r": 0.03921568766236305 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.6823529601097107, + "g": 0.5058823823928833, + "r": 0.007843137718737125 + }, + "position": 0.30000001192092896 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.5607843399047852, + "g": 0.3764705955982208, + "r": 0.003921568859368563 + }, + "position": 0.44999998807907104 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.46666666865348816, + "g": 0.2823529541492462, + "r": 0 + }, + "position": 0.6100000143051147 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.40784314274787903, + "g": 0.22745098173618317, + "r": 0 + }, + "position": 0.7599999904632568 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.3843137323856354, + "g": 0.2078431397676468, + "r": 0.003921568859368563 + }, + "position": 0.9100000262260436 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.3843137323856354, + "g": 0.2117647081613541, + "r": 0.003921568859368563 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 0.6454866528511047, + 0, + 0.31524205207824707 + ], + [ + 0, + 0.6454866528511047, + 0.204462930560112 + ] + ], + "opacity": 1, + "type": "GRADIENT_RADIAL", + "visible": true + } + ], + "height": 32, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "sphere", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 32.304901123046875, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "I37:224;136:9028": { + "id": "I37:224;136:9028", + "type": "GROUP", + "fields": { + "parentId": "37:224", + "childrenIds": [ + "I37:224;136:9029", + "I37:224;136:9031" + ], + "absoluteBoundingBox": { + "height": 32, + "width": 32.304901123046875, + "x": 10557, + "y": 3287.5 + }, + "absoluteRenderBounds": { + "height": 32, + "width": 32.304901123046875, + "x": 10557, + "y": 3287.5 + }, + "height": 32, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Clip path group", + "opacity": 1, + "visible": true, + "width": 32.304901123046875, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "I37:224;136:9029": { + "id": "I37:224;136:9029", + "type": "GROUP", + "fields": { + "parentId": "I37:224;136:9028", + "childrenIds": [ + "I37:224;136:9030" + ], + "absoluteBoundingBox": { + "height": 32, + "width": 32.304901123046875, + "x": 10557, + "y": 3287.5 + }, + "absoluteRenderBounds": null, + "height": 32, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "isMask": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "sphere-clip", + "opacity": 1, + "visible": true, + "width": 32.304901123046875, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "I37:224;136:9030": { + "id": "I37:224;136:9030", + "type": "VECTOR", + "fields": { + "parentId": "I37:224;136:9029", + "absoluteBoundingBox": { + "height": 32, + "width": 32.304901123046875, + "x": 10557, + "y": 3287.5 + }, + "absoluteRenderBounds": { + "height": 32, + "width": 32.3046875, + "x": 10557, + "y": 3287.5 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0, + "g": 0, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 32, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 32.304901123046875, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "I37:224;136:9031": { + "id": "I37:224;136:9031", + "type": "GROUP", + "fields": { + "parentId": "I37:224;136:9028", + "childrenIds": [ + "I37:224;136:9032", + "I37:224;136:9033", + "I37:224;136:9034", + "I37:224;136:9035", + "I37:224;136:9036", + "I37:224;136:9037", + "I37:224;136:9038", + "I37:224;136:9039", + "I37:224;136:9040" + ], + "absoluteBoundingBox": { + "height": 32, + "width": 32.304901123046875, + "x": 10557, + "y": 3287.5 + }, + "absoluteRenderBounds": { + "height": 32, + "width": 32.3046875, + "x": 10557, + "y": 3287.5 + }, + "height": 32, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Group", + "opacity": 1, + "visible": true, + "width": 32.304901123046875, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "I37:224;136:9032": { + "id": "I37:224;136:9032", + "type": "VECTOR", + "fields": { + "parentId": "I37:224;136:9031", + "absoluteBoundingBox": { + "height": 32, + "width": 32.304901123046875, + "x": 10557, + "y": 3287.5 + }, + "absoluteRenderBounds": { + "height": 32, + "width": 32.3046875, + "x": 10557, + "y": 3287.5 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 0.41999998688697815, + "b": 0.3333333432674408, + "g": 0.16470588743686676, + "r": 0 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 0.20000000298023224, + "b": 0.3333333432674408, + "g": 0.16470588743686676, + "r": 0 + }, + "position": 0.550000011920929 + }, + { + "boundVariables": {}, + "color": { + "a": 0, + "b": 0.3333333432674408, + "g": 0.16470588743686676, + "r": 0 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 1.252997875213623, + 0, + 0.08783221244812012 + ], + [ + 0, + 1.252997875213623, + -0.7154324054718018 + ] + ], + "opacity": 1, + "type": "GRADIENT_RADIAL", + "visible": true + } + ], + "height": 32, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "shade-outside-orbit", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 32.304901123046875, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "I37:224;136:9033": { + "id": "I37:224;136:9033", + "type": "VECTOR", + "fields": { + "parentId": "I37:224;136:9031", + "absoluteBoundingBox": { + "height": 32, + "width": 32.304901123046875, + "x": 10557, + "y": 3287.5 + }, + "absoluteRenderBounds": { + "height": 32, + "width": 32.3046875, + "x": 10557, + "y": 3287.5 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 0.14000000059604645, + "b": 0.8470588326454163, + "g": 0.6509804129600525, + "r": 0.10588235408067705 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 0, + "b": 0.8470588326454163, + "g": 0.6509804129600525, + "r": 0.10588235408067705 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 1.9364598989486697, + 0, + 0.468456894159317 + ], + [ + 0, + 1.9364598989486697, + -0.5502269268035889 + ] + ], + "opacity": 1, + "type": "GRADIENT_RADIAL", + "visible": true + } + ], + "height": 32, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "light-left-overlay", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 32.304901123046875, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "I37:224;136:9034": { + "id": "I37:224;136:9034", + "type": "VECTOR", + "fields": { + "parentId": "I37:224;136:9031", + "absoluteBoundingBox": { + "height": 6.700944423675537, + "width": 13.174972534179688, + "x": 10574.4453125, + "y": 3310.31103515625 + }, + "absoluteRenderBounds": { + "height": 6.700927734375, + "width": 13.1748046875, + "x": 10574.4453125, + "y": 3310.31103515625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 0, + "b": 0.5764706134796143, + "g": 0.3529411852359772, + "r": 0.04313725605607033 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 0.550000011920929, + "b": 0.5764706134796143, + "g": 0.3529411852359772, + "r": 0.04313725605607033 + }, + "position": 0.30000001192092896 + }, + { + "boundVariables": {}, + "color": { + "a": 0.5, + "b": 0.5764706134796143, + "g": 0.3529411852359772, + "r": 0.04313725605607033 + }, + "position": 0.75 + }, + { + "boundVariables": {}, + "color": { + "a": 0, + "b": 0.5764706134796143, + "g": 0.3529411852359772, + "r": 0.04313725605607033 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 0.8151227831840515, + -0.2092042565345764, + 0.19614684581756592 + ], + [ + 0.4074420928955078, + 0.41853103041648865, + 0.08879981189966202 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 6.700944423675537, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "rim-highlight (Stroke)", + "opacity": 1, + "strokeAlign": "CENTER", + "strokeWeight": 3.849999904632568, + "visible": true, + "width": 13.174972534179688, + "x": 17.445581436157227, + "y": 22.81094741821289 + }, + "extra": {}, + "fieldErrors": {} + }, + "I37:224;136:9035": { + "id": "I37:224;136:9035", + "type": "VECTOR", + "fields": { + "parentId": "I37:224;136:9031", + "absoluteBoundingBox": { + "height": 18.27490997314453, + "width": 18.435367584228516, + "x": 10560.697265625, + "y": 3294.118896484375 + }, + "absoluteRenderBounds": { + "height": 18.27490234375, + "width": 18.435546875, + "x": 10560.697265625, + "y": 3294.118896484375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.8156862854957581, + "g": 0.7843137383460999, + "r": 0.03921568766236305 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.5882353186607361, + "g": 0.4313725531101227, + "r": 0 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 0.8613659739494324, + 0.45280781388282776, + -0.1629999577999115 + ], + [ + -0.4524722993373871, + 0.8620046973228455, + 0.31514695286750793 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 18.27490997314453, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "core", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 18.435367584228516, + "x": 3.697471857070923, + "y": 6.618992328643799 + }, + "extra": {}, + "fieldErrors": {} + }, + "I37:224;136:9036": { + "id": "I37:224;136:9036", + "type": "VECTOR", + "fields": { + "parentId": "I37:224;136:9031", + "absoluteBoundingBox": { + "height": 10.8725004196167, + "width": 10.976094245910645, + "x": 10563.9345703125, + "y": 3297.846923828125 + }, + "absoluteRenderBounds": { + "height": 10.87255859375, + "width": 10.9765625, + "x": 10563.9345703125, + "y": 3297.846923828125 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.8745098114013672, + "g": 0.9333333373069764, + "r": 0.6705882549285889 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.7607843279838562, + "g": 0.7215686440467834, + "r": 0.0235294122248888 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 0.8080573678016663, + 0.5874120593070984, + -0.19792257249355316 + ], + [ + -0.5874120593070984, + 0.8080573678016663, + 0.3895654380321502 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 10.8725004196167, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "ring", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 10.976094245910645, + "x": 6.9348273277282715, + "y": 10.346875190734863 + }, + "extra": {}, + "fieldErrors": {} + }, + "I37:224;136:9037": { + "id": "I37:224;136:9037", + "type": "VECTOR", + "fields": { + "parentId": "I37:224;136:9031", + "absoluteBoundingBox": { + "height": 5.911250114440918, + "width": 5.967573165893555, + "x": 10566.33203125, + "y": 3300.221923828125 + }, + "absoluteRenderBounds": { + "height": 5.9111328125, + "width": 5.9677734375, + "x": 10566.33203125, + "y": 3300.221923828125 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.6196078658103943, + "g": 0.4745098054409027, + "r": 0.007843137718737125 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.545098066329956, + "g": 0.3764705955982208, + "r": 0.003921568859368563 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 0.9339293241500854, + 0.3536636531352997, + -0.14337609708309174 + ], + [ + -0.3536636531352997, + 0.9339293241500854, + 0.2099337726831436 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 5.911250114440918, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "ring-center", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 5.967573165893555, + "x": 9.332456588745115, + "y": 12.72187614440918 + }, + "extra": {}, + "fieldErrors": {} + }, + "I37:224;136:9038": { + "id": "I37:224;136:9038", + "type": "VECTOR", + "fields": { + "parentId": "I37:224;136:9031", + "absoluteBoundingBox": { + "height": 26.112192153930664, + "width": 27.539609909057617, + "x": 10557.0419921875, + "y": 3290.40771484375 + }, + "absoluteRenderBounds": { + "height": 26.1123046875, + "width": 27.5400390625, + "x": 10557.0419921875, + "y": 3290.40771484375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9843137264251708, + "g": 0.9764705896377563, + "r": 0.9607843160629272 + }, + "position": 0.1899999976158142 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9803921580314636, + "g": 0.91372549533844, + "r": 0.7764706015586853 + }, + "position": 0.3799999952316284 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9725490212440492, + "g": 0.8588235378265381, + "r": 0.6039215922355652 + }, + "position": 0.5 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.95686274766922, + "g": 0.7843137383460999, + "r": 0.3843137323856354 + }, + "position": 0.6600000262260437 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9333333373069764, + "g": 0.729411780834198, + "r": 0.21960784494876864 + }, + "position": 0.8399999737739563 + } + ], + "gradientTransform": [ + [ + -1.6364001035690308, + 1.5055831670761108, + -0.08484271168708801 + ], + [ + -1.5728986263275146, + -1.5663667917251587, + 2.0555288791656494 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 26.112192153930664, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "orbit-outer (Stroke)", + "opacity": 1, + "strokeAlign": "CENTER", + "strokeWeight": 5.050000190734863, + "visible": true, + "width": 27.539609909057617, + "x": 0.041821110993623734, + "y": 2.907834053039551 + }, + "extra": {}, + "fieldErrors": {} + }, + "I37:224;136:9039": { + "id": "I37:224;136:9039", + "type": "VECTOR", + "fields": { + "parentId": "I37:224;136:9031", + "absoluteBoundingBox": { + "height": 14.986942291259766, + "width": 27.421390533447266, + "x": 10560.4541015625, + "y": 3297.781982421875 + }, + "absoluteRenderBounds": { + "height": 14.987060546875, + "width": 27.421875, + "x": 10560.4541015625, + "y": 3297.781982421875 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.9843137264251708, + "g": 0.9803921580314636, + "r": 0.9647058844566344 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 14.986942291259766, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "orbit-inner", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 27.421390533447266, + "x": 3.454395532608032, + "y": 10.281874656677246 + }, + "extra": {}, + "fieldErrors": {} + }, + "I37:224;136:9040": { + "id": "I37:224;136:9040", + "type": "VECTOR", + "fields": { + "parentId": "I37:224;136:9031", + "absoluteBoundingBox": { + "height": 7.961260318756104, + "width": 17.820064544677734, + "x": 10560.6630859375, + "y": 3294.140625 + }, + "absoluteRenderBounds": { + "height": 7.961181640625, + "width": 17.8203125, + "x": 10560.6630859375, + "y": 3294.140625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9843137264251708, + "g": 0.9803921580314636, + "r": 0.9647058844566344 + }, + "position": 0.029999999329447743 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9176470637321472, + "g": 0.9529411792755128, + "r": 0.7686274647712708 + }, + "position": 0.119999997317791 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.8549019694328308, + "g": 0.8941176533699036, + "r": 0.5607843399047852 + }, + "position": 0.3499999940395355 + }, + { + "boundVariables": {}, + "color": { + "a": 0.8999999761581421, + "b": 0.8274509906768799, + "g": 0.8156862854957581, + "r": 0.2705882489681244 + }, + "position": 0.6200000047683716 + }, + { + "boundVariables": {}, + "color": { + "a": 0.6000000238418579, + "b": 0.8039215803146362, + "g": 0.7607843279838562, + "r": 0.14901961386203766 + }, + "position": 0.8600000143051147 + }, + { + "boundVariables": {}, + "color": { + "a": 0, + "b": 0.772549033164978, + "g": 0.7215686440467834, + "r": 0.11372549086809158 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 0.9895601272583008, + -0.06226864457130432, + 0.0533602200448513 + ], + [ + 0.13806335628032684, + 0.446306437253952, + 0.05430806800723076 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 7.961260318756104, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "orbit-inner-tail", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 17.820064544677734, + "x": 3.663325548171997, + "y": 6.6406145095825195 + }, + "extra": {}, + "fieldErrors": {} + }, + "I37:224;136:9041": { + "id": "I37:224;136:9041", + "type": "GROUP", + "fields": { + "parentId": "37:224", + "childrenIds": [ + "I37:224;136:9042", + "I37:224;136:9043", + "I37:224;136:9044", + "I37:224;136:9045", + "I37:224;136:9046", + "I37:224;136:9047", + "I37:224;136:9048", + "I37:224;136:9049", + "I37:224;136:9050", + "I37:224;136:9051" + ], + "absoluteBoundingBox": { + "height": 18.926769256591797, + "width": 158.8823699951172, + "x": 10598.044921875, + "y": 3293.813720703125 + }, + "absoluteRenderBounds": { + "height": 18.926769256591797, + "width": 158.8828125, + "x": 10598.044921875, + "y": 3293.813720703125 + }, + "height": 18.926769256591797, + "inferredAutoLayout": { + "counterAxisAlignItems": "MAX", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 2.128551483154297, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0.00002288818359375, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0.00000762939453125, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "AUTO" + }, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "ChondroGen", + "opacity": 1, + "visible": true, + "width": 158.8823699951172, + "x": 41.044921875, + "y": 6.313720703125 + }, + "extra": {}, + "fieldErrors": {} + }, + "I37:224;136:9042": { + "id": "I37:224;136:9042", + "type": "VECTOR", + "fields": { + "parentId": "I37:224;136:9041", + "absoluteBoundingBox": { + "height": 13.591012001037598, + "width": 13.36479377746582, + "x": 10743.5625, + "y": 3298.897705078125 + }, + "absoluteRenderBounds": { + "height": 13.591064453125, + "width": 13.365234375, + "x": 10743.5625, + "y": 3298.897705078125 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.6980392336845398, + "g": 0.6117647290229797, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 13.591012001037598, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 13.36479377746582, + "x": 186.56251525878903, + "y": 11.397735595703123 + }, + "extra": {}, + "fieldErrors": {} + }, + "I37:224;136:9043": { + "id": "I37:224;136:9043", + "type": "VECTOR", + "fields": { + "parentId": "I37:224;136:9041", + "absoluteBoundingBox": { + "height": 13.76719093322754, + "width": 13.87295913696289, + "x": 10727.146484375, + "y": 3298.897705078125 + }, + "absoluteRenderBounds": { + "height": 13.76708984375, + "width": 13.873046875, + "x": 10727.146484375, + "y": 3298.897705078125 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.6980392336845398, + "g": 0.6117647290229797, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 13.76719093322754, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 13.87295913696289, + "x": 170.14695739746094, + "y": 11.397735595703123 + }, + "extra": {}, + "fieldErrors": {} + }, + "I37:224;136:9044": { + "id": "I37:224;136:9044", + "type": "VECTOR", + "fields": { + "parentId": "I37:224;136:9041", + "absoluteBoundingBox": { + "height": 18.121349334716797, + "width": 16.769514083862305, + "x": 10708.1806640625, + "y": 3294.619140625 + }, + "absoluteRenderBounds": { + "height": 18.121337890625, + "width": 16.76953125, + "x": 10708.1806640625, + "y": 3294.619140625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.6980392336845398, + "g": 0.6117647290229797, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 18.121349334716797, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 16.769514083862305, + "x": 151.18064880371094, + "y": 7.119117736816406 + }, + "extra": {}, + "fieldErrors": {} + }, + "I37:224;136:9045": { + "id": "I37:224;136:9045", + "type": "VECTOR", + "fields": { + "parentId": "I37:224;136:9041", + "absoluteBoundingBox": { + "height": 13.76719093322754, + "width": 14.457345008850098, + "x": 10692.16015625, + "y": 3298.897705078125 + }, + "absoluteRenderBounds": { + "height": 13.76708984375, + "width": 14.45703125, + "x": 10692.16015625, + "y": 3298.897705078125 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.3803921639919281, + "g": 0.2078431397676468, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 13.76719093322754, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 14.457345008850098, + "x": 135.15988159179688, + "y": 11.397735595703123 + }, + "extra": {}, + "fieldErrors": {} + }, + "I37:224;136:9046": { + "id": "I37:224;136:9046", + "type": "VECTOR", + "fields": { + "parentId": "I37:224;136:9041", + "absoluteBoundingBox": { + "height": 13.591012001037598, + "width": 7.851175785064697, + "x": 10683.3642578125, + "y": 3298.897705078125 + }, + "absoluteRenderBounds": { + "height": 13.591064453125, + "width": 7.8515625, + "x": 10683.3642578125, + "y": 3298.897705078125 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.3803921639919281, + "g": 0.2078431397676468, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 13.591012001037598, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 7.851175785064697, + "x": 126.36461639404295, + "y": 11.397735595703123 + }, + "extra": {}, + "fieldErrors": {} + }, + "I37:224;136:9047": { + "id": "I37:224;136:9047", + "type": "VECTOR", + "fields": { + "parentId": "I37:224;136:9041", + "absoluteBoundingBox": { + "height": 18.85123634338379, + "width": 14.381123542785645, + "x": 10665.3115234375, + "y": 3293.813720703125 + }, + "absoluteRenderBounds": { + "height": 18.851318359375, + "width": 14.380859375, + "x": 10665.3115234375, + "y": 3293.813720703125 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.3803921639919281, + "g": 0.2078431397676468, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 18.85123634338379, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 14.381123542785645, + "x": 108.31141662597656, + "y": 6.313728332519531 + }, + "extra": {}, + "fieldErrors": {} + }, + "I37:224;136:9048": { + "id": "I37:224;136:9048", + "type": "VECTOR", + "fields": { + "parentId": "I37:224;136:9041", + "absoluteBoundingBox": { + "height": 13.591012001037598, + "width": 13.364788055419922, + "x": 10649.5185546875, + "y": 3298.897705078125 + }, + "absoluteRenderBounds": { + "height": 13.591064453125, + "width": 13.365234375, + "x": 10649.5185546875, + "y": 3298.897705078125 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.3803921639919281, + "g": 0.2078431397676468, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 13.591012001037598, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 13.364788055419922, + "x": 92.51893615722656, + "y": 11.397735595703123 + }, + "extra": {}, + "fieldErrors": {} + }, + "I37:224;136:9049": { + "id": "I37:224;136:9049", + "type": "VECTOR", + "fields": { + "parentId": "I37:224;136:9041", + "absoluteBoundingBox": { + "height": 13.76719093322754, + "width": 14.457350730895996, + "x": 10632.5322265625, + "y": 3298.897705078125 + }, + "absoluteRenderBounds": { + "height": 13.76708984375, + "width": 14.45703125, + "x": 10632.5322265625, + "y": 3298.897705078125 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.3803921639919281, + "g": 0.2078431397676468, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 13.76719093322754, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 14.457350730895996, + "x": 75.53268432617188, + "y": 11.397735595703123 + }, + "extra": {}, + "fieldErrors": {} + }, + "I37:224;136:9050": { + "id": "I37:224;136:9050", + "type": "VECTOR", + "fields": { + "parentId": "I37:224;136:9041", + "absoluteBoundingBox": { + "height": 18.67505645751953, + "width": 13.364792823791504, + "x": 10616.740234375, + "y": 3293.813720703125 + }, + "absoluteRenderBounds": { + "height": 18.675048828125, + "width": 13.365234375, + "x": 10616.740234375, + "y": 3293.813720703125 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.3803921639919281, + "g": 0.2078431397676468, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 18.67505645751953, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 13.364792823791504, + "x": 59.74018859863281, + "y": 6.313728332519531 + }, + "extra": {}, + "fieldErrors": {} + }, + "I37:224;136:9051": { + "id": "I37:224;136:9051", + "type": "VECTOR", + "fields": { + "parentId": "I37:224;136:9041", + "absoluteBoundingBox": { + "height": 18.121349334716797, + "width": 16.566242218017578, + "x": 10598.044921875, + "y": 3294.619140625 + }, + "absoluteRenderBounds": { + "height": 18.121337890625, + "width": 16.56640625, + "x": 10598.044921875, + "y": 3294.619140625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.3803921639919281, + "g": 0.2078431397676468, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 18.121349334716797, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 16.566242218017578, + "x": 41.04539489746094, + "y": 7.119117736816406 + }, + "extra": {}, + "fieldErrors": {} + }, + "I40:267;136:9027": { + "id": "I40:267;136:9027", + "type": "VECTOR", + "fields": { + "parentId": "40:267", + "absoluteBoundingBox": { + "height": 32, + "width": 32.304901123046875, + "x": 10557, + "y": 3488 + }, + "absoluteRenderBounds": { + "height": 32, + "width": 32.3046875, + "x": 10557, + "y": 3488 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.7490196228027344, + "g": 0.5607843399047852, + "r": 0.03921568766236305 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.6823529601097107, + "g": 0.5058823823928833, + "r": 0.007843137718737125 + }, + "position": 0.30000001192092896 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.5607843399047852, + "g": 0.3764705955982208, + "r": 0.003921568859368563 + }, + "position": 0.44999998807907104 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.46666666865348816, + "g": 0.2823529541492462, + "r": 0 + }, + "position": 0.6100000143051147 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.40784314274787903, + "g": 0.22745098173618317, + "r": 0 + }, + "position": 0.7599999904632568 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.3843137323856354, + "g": 0.2078431397676468, + "r": 0.003921568859368563 + }, + "position": 0.9100000262260436 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.3843137323856354, + "g": 0.2117647081613541, + "r": 0.003921568859368563 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 0.6454866528511047, + 0, + 0.31524205207824707 + ], + [ + 0, + 0.6454866528511047, + 0.204462930560112 + ] + ], + "opacity": 1, + "type": "GRADIENT_RADIAL", + "visible": true + } + ], + "height": 32, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "sphere", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 32.304901123046875, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "I40:267;136:9028": { + "id": "I40:267;136:9028", + "type": "GROUP", + "fields": { + "parentId": "40:267", + "childrenIds": [ + "I40:267;136:9029", + "I40:267;136:9031" + ], + "absoluteBoundingBox": { + "height": 32, + "width": 32.304901123046875, + "x": 10557, + "y": 3488 + }, + "absoluteRenderBounds": { + "height": 32, + "width": 32.304901123046875, + "x": 10557, + "y": 3488 + }, + "height": 32, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Clip path group", + "opacity": 1, + "visible": true, + "width": 32.304901123046875, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "I40:267;136:9029": { + "id": "I40:267;136:9029", + "type": "GROUP", + "fields": { + "parentId": "I40:267;136:9028", + "childrenIds": [ + "I40:267;136:9030" + ], + "absoluteBoundingBox": { + "height": 32, + "width": 32.304901123046875, + "x": 10557, + "y": 3488 + }, + "absoluteRenderBounds": null, + "height": 32, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "isMask": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "sphere-clip", + "opacity": 1, + "visible": true, + "width": 32.304901123046875, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "I40:267;136:9030": { + "id": "I40:267;136:9030", + "type": "VECTOR", + "fields": { + "parentId": "I40:267;136:9029", + "absoluteBoundingBox": { + "height": 32, + "width": 32.304901123046875, + "x": 10557, + "y": 3488 + }, + "absoluteRenderBounds": { + "height": 32, + "width": 32.3046875, + "x": 10557, + "y": 3488 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0, + "g": 0, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 32, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 32.304901123046875, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "I40:267;136:9031": { + "id": "I40:267;136:9031", + "type": "GROUP", + "fields": { + "parentId": "I40:267;136:9028", + "childrenIds": [ + "I40:267;136:9032", + "I40:267;136:9033", + "I40:267;136:9034", + "I40:267;136:9035", + "I40:267;136:9036", + "I40:267;136:9037", + "I40:267;136:9038", + "I40:267;136:9039", + "I40:267;136:9040" + ], + "absoluteBoundingBox": { + "height": 32, + "width": 32.304901123046875, + "x": 10557, + "y": 3488 + }, + "absoluteRenderBounds": { + "height": 32, + "width": 32.3046875, + "x": 10557, + "y": 3488 + }, + "height": 32, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Group", + "opacity": 1, + "visible": true, + "width": 32.304901123046875, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "I40:267;136:9032": { + "id": "I40:267;136:9032", + "type": "VECTOR", + "fields": { + "parentId": "I40:267;136:9031", + "absoluteBoundingBox": { + "height": 32, + "width": 32.304901123046875, + "x": 10557, + "y": 3488 + }, + "absoluteRenderBounds": { + "height": 32, + "width": 32.3046875, + "x": 10557, + "y": 3488 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 0.41999998688697815, + "b": 0.3333333432674408, + "g": 0.16470588743686676, + "r": 0 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 0.20000000298023224, + "b": 0.3333333432674408, + "g": 0.16470588743686676, + "r": 0 + }, + "position": 0.550000011920929 + }, + { + "boundVariables": {}, + "color": { + "a": 0, + "b": 0.3333333432674408, + "g": 0.16470588743686676, + "r": 0 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 1.252997875213623, + 0, + 0.08783221244812012 + ], + [ + 0, + 1.252997875213623, + -0.7154324054718018 + ] + ], + "opacity": 1, + "type": "GRADIENT_RADIAL", + "visible": true + } + ], + "height": 32, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "shade-outside-orbit", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 32.304901123046875, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "I40:267;136:9033": { + "id": "I40:267;136:9033", + "type": "VECTOR", + "fields": { + "parentId": "I40:267;136:9031", + "absoluteBoundingBox": { + "height": 32, + "width": 32.304901123046875, + "x": 10557, + "y": 3488 + }, + "absoluteRenderBounds": { + "height": 32, + "width": 32.3046875, + "x": 10557, + "y": 3488 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 0.14000000059604645, + "b": 0.8470588326454163, + "g": 0.6509804129600525, + "r": 0.10588235408067705 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 0, + "b": 0.8470588326454163, + "g": 0.6509804129600525, + "r": 0.10588235408067705 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 1.9364598989486697, + 0, + 0.468456894159317 + ], + [ + 0, + 1.9364598989486697, + -0.5502269268035889 + ] + ], + "opacity": 1, + "type": "GRADIENT_RADIAL", + "visible": true + } + ], + "height": 32, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "light-left-overlay", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 32.304901123046875, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "I40:267;136:9034": { + "id": "I40:267;136:9034", + "type": "VECTOR", + "fields": { + "parentId": "I40:267;136:9031", + "absoluteBoundingBox": { + "height": 6.700944423675537, + "width": 13.174972534179688, + "x": 10574.4453125, + "y": 3510.81103515625 + }, + "absoluteRenderBounds": { + "height": 6.700927734375, + "width": 13.1748046875, + "x": 10574.4453125, + "y": 3510.81103515625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 0, + "b": 0.5764706134796143, + "g": 0.3529411852359772, + "r": 0.04313725605607033 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 0.550000011920929, + "b": 0.5764706134796143, + "g": 0.3529411852359772, + "r": 0.04313725605607033 + }, + "position": 0.30000001192092896 + }, + { + "boundVariables": {}, + "color": { + "a": 0.5, + "b": 0.5764706134796143, + "g": 0.3529411852359772, + "r": 0.04313725605607033 + }, + "position": 0.75 + }, + { + "boundVariables": {}, + "color": { + "a": 0, + "b": 0.5764706134796143, + "g": 0.3529411852359772, + "r": 0.04313725605607033 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 0.8151227831840515, + -0.2092042565345764, + 0.19614684581756592 + ], + [ + 0.4074420928955078, + 0.41853103041648865, + 0.08879981189966202 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 6.700944423675537, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "rim-highlight (Stroke)", + "opacity": 1, + "strokeAlign": "CENTER", + "strokeWeight": 3.849999904632568, + "visible": true, + "width": 13.174972534179688, + "x": 17.445581436157227, + "y": 22.81094741821289 + }, + "extra": {}, + "fieldErrors": {} + }, + "I40:267;136:9035": { + "id": "I40:267;136:9035", + "type": "VECTOR", + "fields": { + "parentId": "I40:267;136:9031", + "absoluteBoundingBox": { + "height": 18.27490997314453, + "width": 18.435367584228516, + "x": 10560.697265625, + "y": 3494.618896484375 + }, + "absoluteRenderBounds": { + "height": 18.27490234375, + "width": 18.435546875, + "x": 10560.697265625, + "y": 3494.618896484375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.8156862854957581, + "g": 0.7843137383460999, + "r": 0.03921568766236305 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.5882353186607361, + "g": 0.4313725531101227, + "r": 0 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 0.8613659739494324, + 0.45280781388282776, + -0.1629999577999115 + ], + [ + -0.4524722993373871, + 0.8620046973228455, + 0.31514695286750793 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 18.27490997314453, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "core", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 18.435367584228516, + "x": 3.697471857070923, + "y": 6.618992328643799 + }, + "extra": {}, + "fieldErrors": {} + }, + "I40:267;136:9036": { + "id": "I40:267;136:9036", + "type": "VECTOR", + "fields": { + "parentId": "I40:267;136:9031", + "absoluteBoundingBox": { + "height": 10.8725004196167, + "width": 10.976094245910645, + "x": 10563.9345703125, + "y": 3498.346923828125 + }, + "absoluteRenderBounds": { + "height": 10.87255859375, + "width": 10.9765625, + "x": 10563.9345703125, + "y": 3498.346923828125 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.8745098114013672, + "g": 0.9333333373069764, + "r": 0.6705882549285889 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.7607843279838562, + "g": 0.7215686440467834, + "r": 0.0235294122248888 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 0.8080573678016663, + 0.5874120593070984, + -0.19792257249355316 + ], + [ + -0.5874120593070984, + 0.8080573678016663, + 0.3895654380321502 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 10.8725004196167, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "ring", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 10.976094245910645, + "x": 6.9348273277282715, + "y": 10.346875190734863 + }, + "extra": {}, + "fieldErrors": {} + }, + "I40:267;136:9037": { + "id": "I40:267;136:9037", + "type": "VECTOR", + "fields": { + "parentId": "I40:267;136:9031", + "absoluteBoundingBox": { + "height": 5.911250114440918, + "width": 5.967573165893555, + "x": 10566.33203125, + "y": 3500.721923828125 + }, + "absoluteRenderBounds": { + "height": 5.9111328125, + "width": 5.9677734375, + "x": 10566.33203125, + "y": 3500.721923828125 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.6196078658103943, + "g": 0.4745098054409027, + "r": 0.007843137718737125 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.545098066329956, + "g": 0.3764705955982208, + "r": 0.003921568859368563 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 0.9339293241500854, + 0.3536636531352997, + -0.14337609708309174 + ], + [ + -0.3536636531352997, + 0.9339293241500854, + 0.2099337726831436 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 5.911250114440918, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "ring-center", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 5.967573165893555, + "x": 9.332456588745115, + "y": 12.72187614440918 + }, + "extra": {}, + "fieldErrors": {} + }, + "I40:267;136:9038": { + "id": "I40:267;136:9038", + "type": "VECTOR", + "fields": { + "parentId": "I40:267;136:9031", + "absoluteBoundingBox": { + "height": 26.112192153930664, + "width": 27.539609909057617, + "x": 10557.0419921875, + "y": 3490.90771484375 + }, + "absoluteRenderBounds": { + "height": 26.1123046875, + "width": 27.5400390625, + "x": 10557.0419921875, + "y": 3490.90771484375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9843137264251708, + "g": 0.9764705896377563, + "r": 0.9607843160629272 + }, + "position": 0.1899999976158142 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9803921580314636, + "g": 0.91372549533844, + "r": 0.7764706015586853 + }, + "position": 0.3799999952316284 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9725490212440492, + "g": 0.8588235378265381, + "r": 0.6039215922355652 + }, + "position": 0.5 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.95686274766922, + "g": 0.7843137383460999, + "r": 0.3843137323856354 + }, + "position": 0.6600000262260437 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9333333373069764, + "g": 0.729411780834198, + "r": 0.21960784494876864 + }, + "position": 0.8399999737739563 + } + ], + "gradientTransform": [ + [ + -1.6364001035690308, + 1.5055831670761108, + -0.08484271168708801 + ], + [ + -1.5728986263275146, + -1.5663667917251587, + 2.0555288791656494 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 26.112192153930664, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "orbit-outer (Stroke)", + "opacity": 1, + "strokeAlign": "CENTER", + "strokeWeight": 5.050000190734863, + "visible": true, + "width": 27.539609909057617, + "x": 0.041821110993623734, + "y": 2.907834053039551 + }, + "extra": {}, + "fieldErrors": {} + }, + "I40:267;136:9039": { + "id": "I40:267;136:9039", + "type": "VECTOR", + "fields": { + "parentId": "I40:267;136:9031", + "absoluteBoundingBox": { + "height": 14.986942291259766, + "width": 27.421390533447266, + "x": 10560.4541015625, + "y": 3498.281982421875 + }, + "absoluteRenderBounds": { + "height": 14.987060546875, + "width": 27.421875, + "x": 10560.4541015625, + "y": 3498.281982421875 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.9843137264251708, + "g": 0.9803921580314636, + "r": 0.9647058844566344 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 14.986942291259766, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "orbit-inner", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 27.421390533447266, + "x": 3.454395532608032, + "y": 10.281874656677246 + }, + "extra": {}, + "fieldErrors": {} + }, + "I40:267;136:9040": { + "id": "I40:267;136:9040", + "type": "VECTOR", + "fields": { + "parentId": "I40:267;136:9031", + "absoluteBoundingBox": { + "height": 7.961260318756104, + "width": 17.820064544677734, + "x": 10560.6630859375, + "y": 3494.640625 + }, + "absoluteRenderBounds": { + "height": 7.961181640625, + "width": 17.8203125, + "x": 10560.6630859375, + "y": 3494.640625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9843137264251708, + "g": 0.9803921580314636, + "r": 0.9647058844566344 + }, + "position": 0.029999999329447743 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9176470637321472, + "g": 0.9529411792755128, + "r": 0.7686274647712708 + }, + "position": 0.119999997317791 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.8549019694328308, + "g": 0.8941176533699036, + "r": 0.5607843399047852 + }, + "position": 0.3499999940395355 + }, + { + "boundVariables": {}, + "color": { + "a": 0.8999999761581421, + "b": 0.8274509906768799, + "g": 0.8156862854957581, + "r": 0.2705882489681244 + }, + "position": 0.6200000047683716 + }, + { + "boundVariables": {}, + "color": { + "a": 0.6000000238418579, + "b": 0.8039215803146362, + "g": 0.7607843279838562, + "r": 0.14901961386203766 + }, + "position": 0.8600000143051147 + }, + { + "boundVariables": {}, + "color": { + "a": 0, + "b": 0.772549033164978, + "g": 0.7215686440467834, + "r": 0.11372549086809158 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 0.9895601272583008, + -0.06226864457130432, + 0.0533602200448513 + ], + [ + 0.13806335628032684, + 0.446306437253952, + 0.05430806800723076 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 7.961260318756104, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "orbit-inner-tail", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 17.820064544677734, + "x": 3.663325548171997, + "y": 6.6406145095825195 + }, + "extra": {}, + "fieldErrors": {} + }, + "I40:267;136:9041": { + "id": "I40:267;136:9041", + "type": "GROUP", + "fields": { + "parentId": "40:267", + "childrenIds": [ + "I40:267;136:9042", + "I40:267;136:9043", + "I40:267;136:9044", + "I40:267;136:9045", + "I40:267;136:9046", + "I40:267;136:9047", + "I40:267;136:9048", + "I40:267;136:9049", + "I40:267;136:9050", + "I40:267;136:9051" + ], + "absoluteBoundingBox": { + "height": 18.926769256591797, + "width": 158.8823699951172, + "x": 10598.044921875, + "y": 3494.313720703125 + }, + "absoluteRenderBounds": { + "height": 18.926769256591797, + "width": 158.8828125, + "x": 10598.044921875, + "y": 3494.313720703125 + }, + "height": 18.926769256591797, + "inferredAutoLayout": { + "counterAxisAlignItems": "MAX", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 2.128551483154297, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0.00002288818359375, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0.00000762939453125, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "AUTO" + }, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "ChondroGen", + "opacity": 1, + "visible": true, + "width": 158.8823699951172, + "x": 41.044921875, + "y": 6.313720703125 + }, + "extra": {}, + "fieldErrors": {} + }, + "I40:267;136:9042": { + "id": "I40:267;136:9042", + "type": "VECTOR", + "fields": { + "parentId": "I40:267;136:9041", + "absoluteBoundingBox": { + "height": 13.591012001037598, + "width": 13.36479377746582, + "x": 10743.5625, + "y": 3499.397705078125 + }, + "absoluteRenderBounds": { + "height": 13.591064453125, + "width": 13.365234375, + "x": 10743.5625, + "y": 3499.397705078125 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.6980392336845398, + "g": 0.6117647290229797, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 13.591012001037598, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 13.36479377746582, + "x": 186.56251525878903, + "y": 11.397735595703123 + }, + "extra": {}, + "fieldErrors": {} + }, + "I40:267;136:9043": { + "id": "I40:267;136:9043", + "type": "VECTOR", + "fields": { + "parentId": "I40:267;136:9041", + "absoluteBoundingBox": { + "height": 13.76719093322754, + "width": 13.87295913696289, + "x": 10727.146484375, + "y": 3499.397705078125 + }, + "absoluteRenderBounds": { + "height": 13.76708984375, + "width": 13.873046875, + "x": 10727.146484375, + "y": 3499.397705078125 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.6980392336845398, + "g": 0.6117647290229797, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 13.76719093322754, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 13.87295913696289, + "x": 170.14695739746094, + "y": 11.397735595703123 + }, + "extra": {}, + "fieldErrors": {} + }, + "I40:267;136:9044": { + "id": "I40:267;136:9044", + "type": "VECTOR", + "fields": { + "parentId": "I40:267;136:9041", + "absoluteBoundingBox": { + "height": 18.121349334716797, + "width": 16.769514083862305, + "x": 10708.1806640625, + "y": 3495.119140625 + }, + "absoluteRenderBounds": { + "height": 18.121337890625, + "width": 16.76953125, + "x": 10708.1806640625, + "y": 3495.119140625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.6980392336845398, + "g": 0.6117647290229797, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 18.121349334716797, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 16.769514083862305, + "x": 151.18064880371094, + "y": 7.119117736816406 + }, + "extra": {}, + "fieldErrors": {} + }, + "I40:267;136:9045": { + "id": "I40:267;136:9045", + "type": "VECTOR", + "fields": { + "parentId": "I40:267;136:9041", + "absoluteBoundingBox": { + "height": 13.76719093322754, + "width": 14.457345008850098, + "x": 10692.16015625, + "y": 3499.397705078125 + }, + "absoluteRenderBounds": { + "height": 13.76708984375, + "width": 14.45703125, + "x": 10692.16015625, + "y": 3499.397705078125 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.3803921639919281, + "g": 0.2078431397676468, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 13.76719093322754, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 14.457345008850098, + "x": 135.15988159179688, + "y": 11.397735595703123 + }, + "extra": {}, + "fieldErrors": {} + }, + "I40:267;136:9046": { + "id": "I40:267;136:9046", + "type": "VECTOR", + "fields": { + "parentId": "I40:267;136:9041", + "absoluteBoundingBox": { + "height": 13.591012001037598, + "width": 7.851175785064697, + "x": 10683.3642578125, + "y": 3499.397705078125 + }, + "absoluteRenderBounds": { + "height": 13.591064453125, + "width": 7.8515625, + "x": 10683.3642578125, + "y": 3499.397705078125 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.3803921639919281, + "g": 0.2078431397676468, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 13.591012001037598, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 7.851175785064697, + "x": 126.36461639404295, + "y": 11.397735595703123 + }, + "extra": {}, + "fieldErrors": {} + }, + "I40:267;136:9047": { + "id": "I40:267;136:9047", + "type": "VECTOR", + "fields": { + "parentId": "I40:267;136:9041", + "absoluteBoundingBox": { + "height": 18.85123634338379, + "width": 14.381123542785645, + "x": 10665.3115234375, + "y": 3494.313720703125 + }, + "absoluteRenderBounds": { + "height": 18.851318359375, + "width": 14.380859375, + "x": 10665.3115234375, + "y": 3494.313720703125 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.3803921639919281, + "g": 0.2078431397676468, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 18.85123634338379, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 14.381123542785645, + "x": 108.31141662597656, + "y": 6.313728332519531 + }, + "extra": {}, + "fieldErrors": {} + }, + "I40:267;136:9048": { + "id": "I40:267;136:9048", + "type": "VECTOR", + "fields": { + "parentId": "I40:267;136:9041", + "absoluteBoundingBox": { + "height": 13.591012001037598, + "width": 13.364788055419922, + "x": 10649.5185546875, + "y": 3499.397705078125 + }, + "absoluteRenderBounds": { + "height": 13.591064453125, + "width": 13.365234375, + "x": 10649.5185546875, + "y": 3499.397705078125 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.3803921639919281, + "g": 0.2078431397676468, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 13.591012001037598, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 13.364788055419922, + "x": 92.51893615722656, + "y": 11.397735595703123 + }, + "extra": {}, + "fieldErrors": {} + }, + "I40:267;136:9049": { + "id": "I40:267;136:9049", + "type": "VECTOR", + "fields": { + "parentId": "I40:267;136:9041", + "absoluteBoundingBox": { + "height": 13.76719093322754, + "width": 14.457350730895996, + "x": 10632.5322265625, + "y": 3499.397705078125 + }, + "absoluteRenderBounds": { + "height": 13.76708984375, + "width": 14.45703125, + "x": 10632.5322265625, + "y": 3499.397705078125 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.3803921639919281, + "g": 0.2078431397676468, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 13.76719093322754, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 14.457350730895996, + "x": 75.53268432617188, + "y": 11.397735595703123 + }, + "extra": {}, + "fieldErrors": {} + }, + "I40:267;136:9050": { + "id": "I40:267;136:9050", + "type": "VECTOR", + "fields": { + "parentId": "I40:267;136:9041", + "absoluteBoundingBox": { + "height": 18.67505645751953, + "width": 13.364792823791504, + "x": 10616.740234375, + "y": 3494.313720703125 + }, + "absoluteRenderBounds": { + "height": 18.675048828125, + "width": 13.365234375, + "x": 10616.740234375, + "y": 3494.313720703125 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.3803921639919281, + "g": 0.2078431397676468, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 18.67505645751953, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 13.364792823791504, + "x": 59.74018859863281, + "y": 6.313728332519531 + }, + "extra": {}, + "fieldErrors": {} + }, + "I40:267;136:9051": { + "id": "I40:267;136:9051", + "type": "VECTOR", + "fields": { + "parentId": "I40:267;136:9041", + "absoluteBoundingBox": { + "height": 18.121349334716797, + "width": 16.566242218017578, + "x": 10598.044921875, + "y": 3495.119140625 + }, + "absoluteRenderBounds": { + "height": 18.121337890625, + "width": 16.56640625, + "x": 10598.044921875, + "y": 3495.119140625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.3803921639919281, + "g": 0.2078431397676468, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 18.121349334716797, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 16.566242218017578, + "x": 41.04539489746094, + "y": 7.119117736816406 + }, + "extra": {}, + "fieldErrors": {} + }, + "I40:269;18:959": { + "id": "I40:269;18:959", + "type": "TEXT", + "fields": { + "parentId": "40:269", + "absoluteBoundingBox": { + "height": 17, + "width": 59, + "x": 11045, + "y": 3495.5 + }, + "absoluteRenderBounds": { + "height": 15.130126953125, + "width": 56.3564453125, + "x": 11045.6630859375, + "y": 3496.72998046875 + }, + "boundVariables": { + "fontSize": [ + { + "id": "VariableID:44:5420", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "기업소개", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Bold" + }, + "fontSize": 17, + "height": 17, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 100 + }, + "maxHeight": null, + "maxWidth": null, + "name": "menu", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:ab9a4b96cc300838fb4b14185fdd981a07ac2475,", + "textTruncation": "DISABLED", + "visible": true, + "width": 59, + "x": 0, + "y": 15.5, + "styledTextSegments": [ + { + "characters": "기업소개", + "end": 4, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:ab9a4b96cc300838fb4b14185fdd981a07ac2475," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "I40:270;18:959": { + "id": "I40:270;18:959", + "type": "TEXT", + "fields": { + "parentId": "40:270", + "absoluteBoundingBox": { + "height": 17, + "width": 95, + "x": 11144, + "y": 3495.5 + }, + "absoluteRenderBounds": { + "height": 15.1640625, + "width": 93.427734375, + "x": 11144.6630859375, + "y": 3496.72998046875 + }, + "boundVariables": { + "fontSize": [ + { + "id": "VariableID:44:5420", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "기술/연구개발", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Bold" + }, + "fontSize": 17, + "height": 17, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 100 + }, + "maxHeight": null, + "maxWidth": null, + "name": "menu", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:ab9a4b96cc300838fb4b14185fdd981a07ac2475,", + "textTruncation": "DISABLED", + "visible": true, + "width": 95, + "x": 0, + "y": 15.5, + "styledTextSegments": [ + { + "characters": "기술/연구개발", + "end": 7, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:ab9a4b96cc300838fb4b14185fdd981a07ac2475," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "I40:271;18:959": { + "id": "I40:271;18:959", + "type": "TEXT", + "fields": { + "parentId": "40:271", + "absoluteBoundingBox": { + "height": 17, + "width": 59, + "x": 11279, + "y": 3495.5 + }, + "absoluteRenderBounds": { + "height": 15.130126953125, + "width": 56.7138671875, + "x": 11279.3056640625, + "y": 3496.72998046875 + }, + "boundVariables": { + "fontSize": [ + { + "id": "VariableID:44:5420", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "제품소개", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Bold" + }, + "fontSize": 17, + "height": 17, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 100 + }, + "maxHeight": null, + "maxWidth": null, + "name": "menu", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:ab9a4b96cc300838fb4b14185fdd981a07ac2475,", + "textTruncation": "DISABLED", + "visible": true, + "width": 59, + "x": 0, + "y": 15.5, + "styledTextSegments": [ + { + "characters": "제품소개", + "end": 4, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:ab9a4b96cc300838fb4b14185fdd981a07ac2475," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "I40:272;18:959": { + "id": "I40:272;18:959", + "type": "TEXT", + "fields": { + "parentId": "40:272", + "absoluteBoundingBox": { + "height": 17, + "width": 30, + "x": 11378, + "y": 3495.5 + }, + "absoluteRenderBounds": { + "height": 15.1298828125, + "width": 26.99609375, + "x": 11378.578125, + "y": 3496.7470703125 + }, + "boundVariables": { + "fontSize": [ + { + "id": "VariableID:44:5420", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "소식", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Bold" + }, + "fontSize": 17, + "height": 17, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 100 + }, + "maxHeight": null, + "maxWidth": null, + "name": "menu", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:ab9a4b96cc300838fb4b14185fdd981a07ac2475,", + "textTruncation": "DISABLED", + "visible": true, + "width": 30, + "x": 0, + "y": 15.5, + "styledTextSegments": [ + { + "characters": "소식", + "end": 2, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:ab9a4b96cc300838fb4b14185fdd981a07ac2475," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "I40:273;18:959": { + "id": "I40:273;18:959", + "type": "TEXT", + "fields": { + "parentId": "40:273", + "absoluteBoundingBox": { + "height": 17, + "width": 59, + "x": 11448, + "y": 3495.5 + }, + "absoluteRenderBounds": { + "height": 15.18115234375, + "width": 56.0849609375, + "x": 11448.560546875, + "y": 3496.712890625 + }, + "boundVariables": { + "fontSize": [ + { + "id": "VariableID:44:5420", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "문의하기", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Bold" + }, + "fontSize": 17, + "height": 17, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 100 + }, + "maxHeight": null, + "maxWidth": null, + "name": "menu", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:ab9a4b96cc300838fb4b14185fdd981a07ac2475,", + "textTruncation": "DISABLED", + "visible": true, + "width": 59, + "x": 0, + "y": 15.5, + "styledTextSegments": [ + { + "characters": "문의하기", + "end": 4, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:ab9a4b96cc300838fb4b14185fdd981a07ac2475," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "I40:276;20:1321": { + "id": "I40:276;20:1321", + "type": "TEXT", + "fields": { + "parentId": "40:276", + "absoluteBoundingBox": { + "height": 17, + "width": 37, + "x": 11873, + "y": 3492 + }, + "absoluteRenderBounds": null, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:44:5420", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "KOR", + "componentPropertyReferences": { + "visible": "text#20:1" + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.1529411822557449, + "g": 0.08627451211214066, + "r": 0.06666667014360428 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Bold" + }, + "fontSize": 17, + "height": 17, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 100 + }, + "maxHeight": null, + "maxWidth": null, + "name": "KOR", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:ab9a4b96cc300838fb4b14185fdd981a07ac2475,", + "textTruncation": "DISABLED", + "visible": false, + "width": 37, + "x": 44, + "y": 12, + "styledTextSegments": [ + { + "characters": "KOR", + "end": 3, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:ab9a4b96cc300838fb4b14185fdd981a07ac2475," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "I40:276;20:1333": { + "id": "I40:276;20:1333", + "type": "INSTANCE", + "fields": { + "parentId": "40:276", + "childrenIds": [ + "I40:276;20:1333;40:437" + ], + "absoluteBoundingBox": { + "height": 24, + "width": 24, + "x": 11841, + "y": 3492 + }, + "absoluteRenderBounds": null, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": false + } + ], + "height": 24, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 2.2495806217193604, + "paddingLeft": 2.229278326034546, + "paddingRight": 2.2495806217193604, + "paddingTop": 2.229278326034546, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "MagnifyingGlass", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "targetAspectRatio": { + "x": 24, + "y": 24 + }, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 24, + "x": 12, + "y": 12, + "mainComponentId": "40:438", + "mainComponentName": "MagnifyingGlass", + "mainComponentSetId": null + }, + "extra": {}, + "fieldErrors": {} + }, + "I40:276;20:1333;40:437": { + "id": "I40:276;20:1333;40:437", + "type": "VECTOR", + "fields": { + "parentId": "I40:276;20:1333", + "absoluteBoundingBox": { + "height": 19.521141052246097, + "width": 19.521141052246097, + "x": 11843.2294921875, + "y": 3494.229248046875 + }, + "absoluteRenderBounds": null, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 19.521141052246097, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeWeight": 0.09375, + "visible": true, + "width": 19.521141052246097, + "x": 2.229278326034546, + "y": 2.229278326034546 + }, + "extra": {}, + "fieldErrors": {} + }, + "I40:277;20:1321": { + "id": "I40:277;20:1321", + "type": "TEXT", + "fields": { + "parentId": "40:277", + "absoluteBoundingBox": { + "height": 23, + "width": 32, + "x": 11921, + "y": 3492.5 + }, + "absoluteRenderBounds": { + "height": 11.31005859375, + "width": 29.6357421875, + "x": 11922.2001953125, + "y": 3498.429931640625 + }, + "boundVariables": { + "fontSize": [ + { + "id": "VariableID:40:430", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "KOR", + "componentPropertyReferences": { + "visible": "text#20:1" + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Bold" + }, + "fontSize": 15, + "height": 23, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "KOR", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:01da56e122aa148e08cea720f74dbe1f2d759cdb,", + "textTruncation": "DISABLED", + "visible": true, + "width": 32, + "x": 44, + "y": 12.5, + "styledTextSegments": [ + { + "characters": "KOR", + "end": 3, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:01da56e122aa148e08cea720f74dbe1f2d759cdb," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "I40:277;20:1333": { + "id": "I40:277;20:1333", + "type": "INSTANCE", + "fields": { + "parentId": "40:277", + "childrenIds": [ + "I40:277;20:1333;40:440" + ], + "absoluteBoundingBox": { + "height": 24, + "width": 24, + "x": 11889, + "y": 3492 + }, + "absoluteRenderBounds": { + "height": 24, + "width": 24, + "x": 11889, + "y": 3492 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": false + } + ], + "height": 24, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 2.2499961853027344, + "paddingLeft": 2.249995708465576, + "paddingRight": 2.250000476837158, + "paddingTop": 2.25, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "isAsset": true, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Globe", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "targetAspectRatio": { + "x": 24, + "y": 24 + }, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 24, + "x": 12, + "y": 12, + "mainComponentId": "40:441", + "mainComponentName": "Globe", + "mainComponentSetId": null + }, + "extra": {}, + "fieldErrors": {} + }, + "I40:277;20:1333;40:440": { + "id": "I40:277;20:1333;40:440", + "type": "VECTOR", + "fields": { + "parentId": "I40:277;20:1333", + "absoluteBoundingBox": { + "height": 19.500003814697266, + "width": 19.500003814697266, + "x": 11891.25, + "y": 3494.25 + }, + "absoluteRenderBounds": { + "height": 19.5, + "width": 19.5, + "x": 11891.25, + "y": 3494.25 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 19.500003814697266, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeWeight": 0.09375, + "visible": true, + "width": 19.500003814697266, + "x": 2.249995708465576, + "y": 2.25 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1089;136:9027": { + "id": "I43:1089;136:9027", + "type": "VECTOR", + "fields": { + "parentId": "43:1089", + "absoluteBoundingBox": { + "height": 28, + "width": 27.94373893737793, + "x": 10553, + "y": 3748 + }, + "absoluteRenderBounds": { + "height": 28, + "width": 27.943359375, + "x": 10553, + "y": 3748 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.7490196228027344, + "g": 0.5607843399047852, + "r": 0.03921568766236305 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.6823529601097107, + "g": 0.5058823823928833, + "r": 0.007843137718737125 + }, + "position": 0.30000001192092896 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.5607843399047852, + "g": 0.3764705955982208, + "r": 0.003921568859368563 + }, + "position": 0.44999998807907104 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.46666666865348816, + "g": 0.2823529541492462, + "r": 0 + }, + "position": 0.6100000143051147 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.40784314274787903, + "g": 0.22745098173618317, + "r": 0 + }, + "position": 0.7599999904632568 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.3843137323856354, + "g": 0.2078431397676468, + "r": 0.003921568859368563 + }, + "position": 0.9100000262260436 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.3843137323856354, + "g": 0.2117647081613541, + "r": 0.003921568859368563 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 0.6454866528511047, + 0, + 0.31524205207824707 + ], + [ + 0, + 0.6454866528511047, + 0.204462930560112 + ] + ], + "opacity": 1, + "type": "GRADIENT_RADIAL", + "visible": true + } + ], + "height": 28, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "sphere", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 27.94373893737793, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1089;136:9028": { + "id": "I43:1089;136:9028", + "type": "GROUP", + "fields": { + "parentId": "43:1089", + "childrenIds": [ + "I43:1089;136:9029", + "I43:1089;136:9031" + ], + "absoluteBoundingBox": { + "height": 28, + "width": 27.94373893737793, + "x": 10553, + "y": 3748 + }, + "absoluteRenderBounds": { + "height": 28, + "width": 27.94373893737793, + "x": 10553, + "y": 3748 + }, + "height": 28, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Clip path group", + "opacity": 1, + "visible": true, + "width": 27.94373893737793, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1089;136:9029": { + "id": "I43:1089;136:9029", + "type": "GROUP", + "fields": { + "parentId": "I43:1089;136:9028", + "childrenIds": [ + "I43:1089;136:9030" + ], + "absoluteBoundingBox": { + "height": 28, + "width": 27.94373893737793, + "x": 10553, + "y": 3748 + }, + "absoluteRenderBounds": null, + "height": 28, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "isMask": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "sphere-clip", + "opacity": 1, + "visible": true, + "width": 27.94373893737793, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1089;136:9030": { + "id": "I43:1089;136:9030", + "type": "VECTOR", + "fields": { + "parentId": "I43:1089;136:9029", + "absoluteBoundingBox": { + "height": 28, + "width": 27.94373893737793, + "x": 10553, + "y": 3748 + }, + "absoluteRenderBounds": { + "height": 28, + "width": 27.943359375, + "x": 10553, + "y": 3748 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0, + "g": 0, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 28, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 27.94373893737793, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1089;136:9031": { + "id": "I43:1089;136:9031", + "type": "GROUP", + "fields": { + "parentId": "I43:1089;136:9028", + "childrenIds": [ + "I43:1089;136:9032", + "I43:1089;136:9033", + "I43:1089;136:9034", + "I43:1089;136:9035", + "I43:1089;136:9036", + "I43:1089;136:9037", + "I43:1089;136:9038", + "I43:1089;136:9039", + "I43:1089;136:9040" + ], + "absoluteBoundingBox": { + "height": 28, + "width": 27.94373893737793, + "x": 10553, + "y": 3748 + }, + "absoluteRenderBounds": { + "height": 28, + "width": 27.943359375, + "x": 10553, + "y": 3748 + }, + "height": 28, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Group", + "opacity": 1, + "visible": true, + "width": 27.94373893737793, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1089;136:9032": { + "id": "I43:1089;136:9032", + "type": "VECTOR", + "fields": { + "parentId": "I43:1089;136:9031", + "absoluteBoundingBox": { + "height": 28, + "width": 27.94373893737793, + "x": 10553, + "y": 3748 + }, + "absoluteRenderBounds": { + "height": 28, + "width": 27.943359375, + "x": 10553, + "y": 3748 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 0.41999998688697815, + "b": 0.3333333432674408, + "g": 0.16470588743686676, + "r": 0 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 0.20000000298023224, + "b": 0.3333333432674408, + "g": 0.16470588743686676, + "r": 0 + }, + "position": 0.550000011920929 + }, + { + "boundVariables": {}, + "color": { + "a": 0, + "b": 0.3333333432674408, + "g": 0.16470588743686676, + "r": 0 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 1.252997875213623, + 0, + 0.08783221244812012 + ], + [ + 0, + 1.252997875213623, + -0.7154324054718018 + ] + ], + "opacity": 1, + "type": "GRADIENT_RADIAL", + "visible": true + } + ], + "height": 28, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "shade-outside-orbit", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 27.94373893737793, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1089;136:9033": { + "id": "I43:1089;136:9033", + "type": "VECTOR", + "fields": { + "parentId": "I43:1089;136:9031", + "absoluteBoundingBox": { + "height": 28, + "width": 27.94373893737793, + "x": 10553, + "y": 3748 + }, + "absoluteRenderBounds": { + "height": 28, + "width": 27.943359375, + "x": 10553, + "y": 3748 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 0.14000000059604645, + "b": 0.8470588326454163, + "g": 0.6509804129600525, + "r": 0.10588235408067705 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 0, + "b": 0.8470588326454163, + "g": 0.6509804129600525, + "r": 0.10588235408067705 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 1.9364598989486697, + 0, + 0.468456894159317 + ], + [ + 0, + 1.9364598989486697, + -0.5502269268035889 + ] + ], + "opacity": 1, + "type": "GRADIENT_RADIAL", + "visible": true + } + ], + "height": 28, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "light-left-overlay", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 27.94373893737793, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1089;136:9034": { + "id": "I43:1089;136:9034", + "type": "VECTOR", + "fields": { + "parentId": "I43:1089;136:9031", + "absoluteBoundingBox": { + "height": 5.863326072692871, + "width": 11.39635181427002, + "x": 10568.0908203125, + "y": 3767.95947265625 + }, + "absoluteRenderBounds": { + "height": 5.86328125, + "width": 11.396484375, + "x": 10568.0908203125, + "y": 3767.95947265625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 0, + "b": 0.5764706134796143, + "g": 0.3529411852359772, + "r": 0.04313725605607033 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 0.550000011920929, + "b": 0.5764706134796143, + "g": 0.3529411852359772, + "r": 0.04313725605607033 + }, + "position": 0.30000001192092896 + }, + { + "boundVariables": {}, + "color": { + "a": 0.5, + "b": 0.5764706134796143, + "g": 0.3529411852359772, + "r": 0.04313725605607033 + }, + "position": 0.75 + }, + { + "boundVariables": {}, + "color": { + "a": 0, + "b": 0.5764706134796143, + "g": 0.3529411852359772, + "r": 0.04313725605607033 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 0.8151227831840515, + -0.2092042565345764, + 0.19614684581756592 + ], + [ + 0.4074420928955078, + 0.41853103041648865, + 0.08879981189966202 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 5.863326072692871, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "rim-highlight (Stroke)", + "opacity": 1, + "strokeAlign": "CENTER", + "strokeWeight": 3.849999904632568, + "visible": true, + "width": 11.39635181427002, + "x": 15.09042739868164, + "y": 19.95957946777344 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1089;136:9035": { + "id": "I43:1089;136:9035", + "type": "VECTOR", + "fields": { + "parentId": "I43:1089;136:9031", + "absoluteBoundingBox": { + "height": 15.99054718017578, + "width": 15.946592330932615, + "x": 10556.1982421875, + "y": 3753.79150390625 + }, + "absoluteRenderBounds": { + "height": 15.990478515625, + "width": 15.9462890625, + "x": 10556.1982421875, + "y": 3753.79150390625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.8156862854957581, + "g": 0.7843137383460999, + "r": 0.03921568766236305 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.5882353186607361, + "g": 0.4313725531101227, + "r": 0 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 0.8613659739494324, + 0.45280781388282776, + -0.1629999577999115 + ], + [ + -0.4524722993373871, + 0.8620046973228455, + 0.31514695286750793 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 15.99054718017578, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "core", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 15.946592330932615, + "x": 3.1983132362365723, + "y": 5.791618347167969 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1089;136:9036": { + "id": "I43:1089;136:9036", + "type": "VECTOR", + "fields": { + "parentId": "I43:1089;136:9031", + "absoluteBoundingBox": { + "height": 9.51343822479248, + "width": 9.494321823120115, + "x": 10558.9990234375, + "y": 3757.053466796875 + }, + "absoluteRenderBounds": { + "height": 9.513427734375, + "width": 9.494140625, + "x": 10558.9990234375, + "y": 3757.053466796875 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.8745098114013672, + "g": 0.9333333373069764, + "r": 0.6705882549285889 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.7607843279838562, + "g": 0.7215686440467834, + "r": 0.0235294122248888 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 0.8080573678016663, + 0.5874120593070984, + -0.19792257249355316 + ], + [ + -0.5874120593070984, + 0.8080573678016663, + 0.3895654380321502 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 9.51343822479248, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "ring", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 9.494321823120115, + "x": 5.998625755310059, + "y": 9.053515434265137 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1089;136:9037": { + "id": "I43:1089;136:9037", + "type": "VECTOR", + "fields": { + "parentId": "I43:1089;136:9031", + "absoluteBoundingBox": { + "height": 5.172343730926514, + "width": 5.161951065063477, + "x": 10561.072265625, + "y": 3759.131591796875 + }, + "absoluteRenderBounds": { + "height": 5.17236328125, + "width": 5.162109375, + "x": 10561.072265625, + "y": 3759.131591796875 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.6196078658103943, + "g": 0.4745098054409027, + "r": 0.007843137718737125 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.545098066329956, + "g": 0.3764705955982208, + "r": 0.003921568859368563 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 0.9339293241500854, + 0.3536636531352997, + -0.14337609708309174 + ], + [ + -0.3536636531352997, + 0.9339293241500854, + 0.2099337726831436 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 5.172343730926514, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "ring-center", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 5.161951065063477, + "x": 8.072575569152832, + "y": 11.131641387939451 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1089;136:9038": { + "id": "I43:1089;136:9038", + "type": "VECTOR", + "fields": { + "parentId": "I43:1089;136:9031", + "absoluteBoundingBox": { + "height": 22.848167419433594, + "width": 23.82176399230957, + "x": 10553.0361328125, + "y": 3750.54443359375 + }, + "absoluteRenderBounds": { + "height": 22.84814453125, + "width": 23.8212890625, + "x": 10553.0361328125, + "y": 3750.54443359375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9843137264251708, + "g": 0.9764705896377563, + "r": 0.9607843160629272 + }, + "position": 0.1899999976158142 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9803921580314636, + "g": 0.91372549533844, + "r": 0.7764706015586853 + }, + "position": 0.3799999952316284 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9725490212440492, + "g": 0.8588235378265381, + "r": 0.6039215922355652 + }, + "position": 0.5 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.95686274766922, + "g": 0.7843137383460999, + "r": 0.3843137323856354 + }, + "position": 0.6600000262260437 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9333333373069764, + "g": 0.729411780834198, + "r": 0.21960784494876864 + }, + "position": 0.8399999737739563 + } + ], + "gradientTransform": [ + [ + -1.6364001035690308, + 1.5055831670761108, + -0.08484271168708801 + ], + [ + -1.5728986263275146, + -1.5663667917251587, + 2.0555288791656494 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 22.848167419433594, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "orbit-outer (Stroke)", + "opacity": 1, + "strokeAlign": "CENTER", + "strokeWeight": 5.050000190734863, + "visible": true, + "width": 23.82176399230957, + "x": 0.036175258457660675, + "y": 2.5443549156188965 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1089;136:9039": { + "id": "I43:1089;136:9039", + "type": "VECTOR", + "fields": { + "parentId": "I43:1089;136:9031", + "absoluteBoundingBox": { + "height": 13.113574981689451, + "width": 23.719501495361328, + "x": 10555.98828125, + "y": 3756.99658203125 + }, + "absoluteRenderBounds": { + "height": 13.113525390625, + "width": 23.7197265625, + "x": 10555.98828125, + "y": 3756.99658203125 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.9843137264251708, + "g": 0.9803921580314636, + "r": 0.9647058844566344 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 13.113574981689451, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "orbit-inner", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 23.719501495361328, + "x": 2.9880521297454834, + "y": 8.9966402053833 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1089;136:9040": { + "id": "I43:1089;136:9040", + "type": "VECTOR", + "fields": { + "parentId": "I43:1089;136:9031", + "absoluteBoundingBox": { + "height": 6.9661030769348145, + "width": 15.414355278015137, + "x": 10556.1689453125, + "y": 3753.810546875 + }, + "absoluteRenderBounds": { + "height": 6.966064453125, + "width": 15.4140625, + "x": 10556.1689453125, + "y": 3753.810546875 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9843137264251708, + "g": 0.9803921580314636, + "r": 0.9647058844566344 + }, + "position": 0.029999999329447743 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9176470637321472, + "g": 0.9529411792755128, + "r": 0.7686274647712708 + }, + "position": 0.119999997317791 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.8549019694328308, + "g": 0.8941176533699036, + "r": 0.5607843399047852 + }, + "position": 0.3499999940395355 + }, + { + "boundVariables": {}, + "color": { + "a": 0.8999999761581421, + "b": 0.8274509906768799, + "g": 0.8156862854957581, + "r": 0.2705882489681244 + }, + "position": 0.6200000047683716 + }, + { + "boundVariables": {}, + "color": { + "a": 0.6000000238418579, + "b": 0.8039215803146362, + "g": 0.7607843279838562, + "r": 0.14901961386203766 + }, + "position": 0.8600000143051147 + }, + { + "boundVariables": {}, + "color": { + "a": 0, + "b": 0.772549033164978, + "g": 0.7215686440467834, + "r": 0.11372549086809158 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 0.9895601272583008, + -0.06226864457130432, + 0.0533602200448513 + ], + [ + 0.13806335628032684, + 0.446306437253952, + 0.05430806800723076 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 6.9661030769348145, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "orbit-inner-tail", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 15.414355278015137, + "x": 3.168776512145996, + "y": 5.810537815093994 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1089;136:9041": { + "id": "I43:1089;136:9041", + "type": "GROUP", + "fields": { + "parentId": "43:1089", + "childrenIds": [ + "I43:1089;136:9042", + "I43:1089;136:9043", + "I43:1089;136:9044", + "I43:1089;136:9045", + "I43:1089;136:9046", + "I43:1089;136:9047", + "I43:1089;136:9048", + "I43:1089;136:9049", + "I43:1089;136:9050", + "I43:1089;136:9051" + ], + "absoluteBoundingBox": { + "height": 16.56101417541504, + "width": 137.43359375, + "x": 10588.50390625, + "y": 3753.5244140625 + }, + "absoluteRenderBounds": { + "height": 16.56103515625, + "width": 137.43359375, + "x": 10588.50390625, + "y": 3753.5244140625 + }, + "height": 16.56101417541504, + "inferredAutoLayout": { + "counterAxisAlignItems": "MAX", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 1.8411893844604492, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0.000019073486328125, + "paddingLeft": 0.0003662109375, + "paddingRight": 0.00037384033203125, + "paddingTop": 0.00009918212890625, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "AUTO" + }, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "ChondroGen", + "opacity": 1, + "visible": true, + "width": 137.43359375, + "x": 35.50390625, + "y": 5.5244140625 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1089;136:9042": { + "id": "I43:1089;136:9042", + "type": "VECTOR", + "fields": { + "parentId": "I43:1089;136:9041", + "absoluteBoundingBox": { + "height": 11.892135620117188, + "width": 11.560546875, + "x": 10714.376953125, + "y": 3757.972900390625 + }, + "absoluteRenderBounds": { + "height": 11.89208984375, + "width": 11.560546875, + "x": 10714.376953125, + "y": 3757.972900390625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.6980392336845398, + "g": 0.6117647290229797, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 11.892135620117188, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 11.560546875, + "x": 161.37657928466797, + "y": 9.973018646240234 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1089;136:9043": { + "id": "I43:1089;136:9043", + "type": "VECTOR", + "fields": { + "parentId": "I43:1089;136:9041", + "absoluteBoundingBox": { + "height": 12.046292304992676, + "width": 12.000109672546388, + "x": 10700.1767578125, + "y": 3757.972900390625 + }, + "absoluteRenderBounds": { + "height": 12.04638671875, + "width": 12, + "x": 10700.1767578125, + "y": 3757.972900390625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.6980392336845398, + "g": 0.6117647290229797, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 12.046292304992676, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 12.000109672546388, + "x": 147.1771240234375, + "y": 9.973018646240234 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1089;136:9044": { + "id": "I43:1089;136:9044", + "type": "VECTOR", + "fields": { + "parentId": "I43:1089;136:9041", + "absoluteBoundingBox": { + "height": 15.85618019104004, + "width": 14.505629539489746, + "x": 10683.771484375, + "y": 3754.229248046875 + }, + "absoluteRenderBounds": { + "height": 15.856201171875, + "width": 14.505859375, + "x": 10683.771484375, + "y": 3754.229248046875 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.6980392336845398, + "g": 0.6117647290229797, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 15.85618019104004, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 14.505629539489746, + "x": 130.7712631225586, + "y": 6.229228973388672 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1089;136:9045": { + "id": "I43:1089;136:9045", + "type": "VECTOR", + "fields": { + "parentId": "I43:1089;136:9041", + "absoluteBoundingBox": { + "height": 12.046292304992676, + "width": 12.505603790283203, + "x": 10669.9130859375, + "y": 3757.972900390625 + }, + "absoluteRenderBounds": { + "height": 12.04638671875, + "width": 12.505859375, + "x": 10669.9130859375, + "y": 3757.972900390625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.3803921639919281, + "g": 0.2078431397676468, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 12.046292304992676, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 12.505603790283203, + "x": 116.91329956054688, + "y": 9.973018646240234 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1089;136:9046": { + "id": "I43:1089;136:9046", + "type": "VECTOR", + "fields": { + "parentId": "I43:1089;136:9041", + "absoluteBoundingBox": { + "height": 11.892135620117188, + "width": 6.791267395019531, + "x": 10662.3056640625, + "y": 3757.972900390625 + }, + "absoluteRenderBounds": { + "height": 11.89208984375, + "width": 6.791015625, + "x": 10662.3056640625, + "y": 3757.972900390625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.3803921639919281, + "g": 0.2078431397676468, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 11.892135620117188, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 6.791267395019531, + "x": 109.3053970336914, + "y": 9.973018646240234 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1089;136:9047": { + "id": "I43:1089;136:9047", + "type": "VECTOR", + "fields": { + "parentId": "I43:1089;136:9041", + "absoluteBoundingBox": { + "height": 16.494831085205078, + "width": 12.439671516418455, + "x": 10646.689453125, + "y": 3753.5244140625 + }, + "absoluteRenderBounds": { + "height": 16.494873046875, + "width": 12.439453125, + "x": 10646.689453125, + "y": 3753.5244140625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.3803921639919281, + "g": 0.2078431397676468, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 16.494831085205078, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 12.439671516418455, + "x": 93.68937683105467, + "y": 5.524513244628906 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1089;136:9048": { + "id": "I43:1089;136:9048", + "type": "VECTOR", + "fields": { + "parentId": "I43:1089;136:9041", + "absoluteBoundingBox": { + "height": 11.892135620117188, + "width": 11.560542106628418, + "x": 10633.029296875, + "y": 3757.972900390625 + }, + "absoluteRenderBounds": { + "height": 11.89208984375, + "width": 11.560546875, + "x": 10633.029296875, + "y": 3757.972900390625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.3803921639919281, + "g": 0.2078431397676468, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 11.892135620117188, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 11.560542106628418, + "x": 80.02886962890625, + "y": 9.973018646240234 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1089;136:9049": { + "id": "I43:1089;136:9049", + "type": "VECTOR", + "fields": { + "parentId": "I43:1089;136:9041", + "absoluteBoundingBox": { + "height": 12.046292304992676, + "width": 12.505608558654783, + "x": 10618.3359375, + "y": 3757.972900390625 + }, + "absoluteRenderBounds": { + "height": 12.04638671875, + "width": 12.505859375, + "x": 10618.3359375, + "y": 3757.972900390625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.3803921639919281, + "g": 0.2078431397676468, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 12.046292304992676, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 12.505608558654783, + "x": 65.33576965332031, + "y": 9.973018646240234 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1089;136:9050": { + "id": "I43:1089;136:9050", + "type": "VECTOR", + "fields": { + "parentId": "I43:1089;136:9041", + "absoluteBoundingBox": { + "height": 16.340675354003906, + "width": 11.560545921325684, + "x": 10604.6748046875, + "y": 3753.5244140625 + }, + "absoluteRenderBounds": { + "height": 16.340576171875, + "width": 11.560546875, + "x": 10604.6748046875, + "y": 3753.5244140625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.3803921639919281, + "g": 0.2078431397676468, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 16.340675354003906, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 11.560545921325684, + "x": 51.675262451171875, + "y": 5.524513244628906 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1089;136:9051": { + "id": "I43:1089;136:9051", + "type": "VECTOR", + "fields": { + "parentId": "I43:1089;136:9041", + "absoluteBoundingBox": { + "height": 15.85618019104004, + "width": 14.329800605773926, + "x": 10588.50390625, + "y": 3754.229248046875 + }, + "absoluteRenderBounds": { + "height": 15.856201171875, + "width": 14.330078125, + "x": 10588.50390625, + "y": 3754.229248046875 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.3803921639919281, + "g": 0.2078431397676468, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 15.85618019104004, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 14.329800605773926, + "x": 35.5042724609375, + "y": 6.229228973388672 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1101;136:9027": { + "id": "I43:1101;136:9027", + "type": "VECTOR", + "fields": { + "parentId": "43:1101", + "absoluteBoundingBox": { + "height": 28, + "width": 27.94373893737793, + "x": 10553, + "y": 3581.5 + }, + "absoluteRenderBounds": { + "height": 28, + "width": 27.943359375, + "x": 10553, + "y": 3581.5 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.7490196228027344, + "g": 0.5607843399047852, + "r": 0.03921568766236305 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.6823529601097107, + "g": 0.5058823823928833, + "r": 0.007843137718737125 + }, + "position": 0.30000001192092896 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.5607843399047852, + "g": 0.3764705955982208, + "r": 0.003921568859368563 + }, + "position": 0.44999998807907104 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.46666666865348816, + "g": 0.2823529541492462, + "r": 0 + }, + "position": 0.6100000143051147 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.40784314274787903, + "g": 0.22745098173618317, + "r": 0 + }, + "position": 0.7599999904632568 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.3843137323856354, + "g": 0.2078431397676468, + "r": 0.003921568859368563 + }, + "position": 0.9100000262260436 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.3843137323856354, + "g": 0.2117647081613541, + "r": 0.003921568859368563 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 0.6454866528511047, + 0, + 0.31524205207824707 + ], + [ + 0, + 0.6454866528511047, + 0.204462930560112 + ] + ], + "opacity": 1, + "type": "GRADIENT_RADIAL", + "visible": true + } + ], + "height": 28, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "sphere", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 27.94373893737793, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1101;136:9028": { + "id": "I43:1101;136:9028", + "type": "GROUP", + "fields": { + "parentId": "43:1101", + "childrenIds": [ + "I43:1101;136:9029", + "I43:1101;136:9031" + ], + "absoluteBoundingBox": { + "height": 28, + "width": 27.94373893737793, + "x": 10553, + "y": 3581.5 + }, + "absoluteRenderBounds": { + "height": 28, + "width": 27.94373893737793, + "x": 10553, + "y": 3581.5 + }, + "height": 28, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Clip path group", + "opacity": 1, + "visible": true, + "width": 27.94373893737793, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1101;136:9029": { + "id": "I43:1101;136:9029", + "type": "GROUP", + "fields": { + "parentId": "I43:1101;136:9028", + "childrenIds": [ + "I43:1101;136:9030" + ], + "absoluteBoundingBox": { + "height": 28, + "width": 27.94373893737793, + "x": 10553, + "y": 3581.5 + }, + "absoluteRenderBounds": null, + "height": 28, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "isMask": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "sphere-clip", + "opacity": 1, + "visible": true, + "width": 27.94373893737793, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1101;136:9030": { + "id": "I43:1101;136:9030", + "type": "VECTOR", + "fields": { + "parentId": "I43:1101;136:9029", + "absoluteBoundingBox": { + "height": 28, + "width": 27.94373893737793, + "x": 10553, + "y": 3581.5 + }, + "absoluteRenderBounds": { + "height": 28, + "width": 27.943359375, + "x": 10553, + "y": 3581.5 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0, + "g": 0, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 28, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 27.94373893737793, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1101;136:9031": { + "id": "I43:1101;136:9031", + "type": "GROUP", + "fields": { + "parentId": "I43:1101;136:9028", + "childrenIds": [ + "I43:1101;136:9032", + "I43:1101;136:9033", + "I43:1101;136:9034", + "I43:1101;136:9035", + "I43:1101;136:9036", + "I43:1101;136:9037", + "I43:1101;136:9038", + "I43:1101;136:9039", + "I43:1101;136:9040" + ], + "absoluteBoundingBox": { + "height": 28, + "width": 27.94373893737793, + "x": 10553, + "y": 3581.5 + }, + "absoluteRenderBounds": { + "height": 28, + "width": 27.943359375, + "x": 10553, + "y": 3581.5 + }, + "height": 28, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Group", + "opacity": 1, + "visible": true, + "width": 27.94373893737793, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1101;136:9032": { + "id": "I43:1101;136:9032", + "type": "VECTOR", + "fields": { + "parentId": "I43:1101;136:9031", + "absoluteBoundingBox": { + "height": 28, + "width": 27.94373893737793, + "x": 10553, + "y": 3581.5 + }, + "absoluteRenderBounds": { + "height": 28, + "width": 27.943359375, + "x": 10553, + "y": 3581.5 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 0.41999998688697815, + "b": 0.3333333432674408, + "g": 0.16470588743686676, + "r": 0 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 0.20000000298023224, + "b": 0.3333333432674408, + "g": 0.16470588743686676, + "r": 0 + }, + "position": 0.550000011920929 + }, + { + "boundVariables": {}, + "color": { + "a": 0, + "b": 0.3333333432674408, + "g": 0.16470588743686676, + "r": 0 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 1.252997875213623, + 0, + 0.08783221244812012 + ], + [ + 0, + 1.252997875213623, + -0.7154324054718018 + ] + ], + "opacity": 1, + "type": "GRADIENT_RADIAL", + "visible": true + } + ], + "height": 28, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "shade-outside-orbit", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 27.94373893737793, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1101;136:9033": { + "id": "I43:1101;136:9033", + "type": "VECTOR", + "fields": { + "parentId": "I43:1101;136:9031", + "absoluteBoundingBox": { + "height": 28, + "width": 27.94373893737793, + "x": 10553, + "y": 3581.5 + }, + "absoluteRenderBounds": { + "height": 28, + "width": 27.943359375, + "x": 10553, + "y": 3581.5 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 0.14000000059604645, + "b": 0.8470588326454163, + "g": 0.6509804129600525, + "r": 0.10588235408067705 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 0, + "b": 0.8470588326454163, + "g": 0.6509804129600525, + "r": 0.10588235408067705 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 1.9364598989486697, + 0, + 0.468456894159317 + ], + [ + 0, + 1.9364598989486697, + -0.5502269268035889 + ] + ], + "opacity": 1, + "type": "GRADIENT_RADIAL", + "visible": true + } + ], + "height": 28, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "light-left-overlay", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 27.94373893737793, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1101;136:9034": { + "id": "I43:1101;136:9034", + "type": "VECTOR", + "fields": { + "parentId": "I43:1101;136:9031", + "absoluteBoundingBox": { + "height": 5.863326072692871, + "width": 11.39635181427002, + "x": 10568.0908203125, + "y": 3601.45947265625 + }, + "absoluteRenderBounds": { + "height": 5.86328125, + "width": 11.396484375, + "x": 10568.0908203125, + "y": 3601.45947265625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 0, + "b": 0.5764706134796143, + "g": 0.3529411852359772, + "r": 0.04313725605607033 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 0.550000011920929, + "b": 0.5764706134796143, + "g": 0.3529411852359772, + "r": 0.04313725605607033 + }, + "position": 0.30000001192092896 + }, + { + "boundVariables": {}, + "color": { + "a": 0.5, + "b": 0.5764706134796143, + "g": 0.3529411852359772, + "r": 0.04313725605607033 + }, + "position": 0.75 + }, + { + "boundVariables": {}, + "color": { + "a": 0, + "b": 0.5764706134796143, + "g": 0.3529411852359772, + "r": 0.04313725605607033 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 0.8151227831840515, + -0.2092042565345764, + 0.19614684581756592 + ], + [ + 0.4074420928955078, + 0.41853103041648865, + 0.08879981189966202 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 5.863326072692871, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "rim-highlight (Stroke)", + "opacity": 1, + "strokeAlign": "CENTER", + "strokeWeight": 3.849999904632568, + "visible": true, + "width": 11.39635181427002, + "x": 15.09042739868164, + "y": 19.95957946777344 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1101;136:9035": { + "id": "I43:1101;136:9035", + "type": "VECTOR", + "fields": { + "parentId": "I43:1101;136:9031", + "absoluteBoundingBox": { + "height": 15.99054718017578, + "width": 15.946592330932615, + "x": 10556.1982421875, + "y": 3587.29150390625 + }, + "absoluteRenderBounds": { + "height": 15.990478515625, + "width": 15.9462890625, + "x": 10556.1982421875, + "y": 3587.29150390625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.8156862854957581, + "g": 0.7843137383460999, + "r": 0.03921568766236305 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.5882353186607361, + "g": 0.4313725531101227, + "r": 0 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 0.8613659739494324, + 0.45280781388282776, + -0.1629999577999115 + ], + [ + -0.4524722993373871, + 0.8620046973228455, + 0.31514695286750793 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 15.99054718017578, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "core", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 15.946592330932615, + "x": 3.1983132362365723, + "y": 5.791618347167969 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1101;136:9036": { + "id": "I43:1101;136:9036", + "type": "VECTOR", + "fields": { + "parentId": "I43:1101;136:9031", + "absoluteBoundingBox": { + "height": 9.51343822479248, + "width": 9.494321823120115, + "x": 10558.9990234375, + "y": 3590.553466796875 + }, + "absoluteRenderBounds": { + "height": 9.513427734375, + "width": 9.494140625, + "x": 10558.9990234375, + "y": 3590.553466796875 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.8745098114013672, + "g": 0.9333333373069764, + "r": 0.6705882549285889 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.7607843279838562, + "g": 0.7215686440467834, + "r": 0.0235294122248888 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 0.8080573678016663, + 0.5874120593070984, + -0.19792257249355316 + ], + [ + -0.5874120593070984, + 0.8080573678016663, + 0.3895654380321502 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 9.51343822479248, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "ring", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 9.494321823120115, + "x": 5.998625755310059, + "y": 9.053515434265137 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1101;136:9037": { + "id": "I43:1101;136:9037", + "type": "VECTOR", + "fields": { + "parentId": "I43:1101;136:9031", + "absoluteBoundingBox": { + "height": 5.172343730926514, + "width": 5.161951065063477, + "x": 10561.072265625, + "y": 3592.631591796875 + }, + "absoluteRenderBounds": { + "height": 5.17236328125, + "width": 5.162109375, + "x": 10561.072265625, + "y": 3592.631591796875 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.6196078658103943, + "g": 0.4745098054409027, + "r": 0.007843137718737125 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.545098066329956, + "g": 0.3764705955982208, + "r": 0.003921568859368563 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 0.9339293241500854, + 0.3536636531352997, + -0.14337609708309174 + ], + [ + -0.3536636531352997, + 0.9339293241500854, + 0.2099337726831436 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 5.172343730926514, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "ring-center", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 5.161951065063477, + "x": 8.072575569152832, + "y": 11.131641387939451 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1101;136:9038": { + "id": "I43:1101;136:9038", + "type": "VECTOR", + "fields": { + "parentId": "I43:1101;136:9031", + "absoluteBoundingBox": { + "height": 22.848167419433594, + "width": 23.82176399230957, + "x": 10553.0361328125, + "y": 3584.04443359375 + }, + "absoluteRenderBounds": { + "height": 22.84814453125, + "width": 23.8212890625, + "x": 10553.0361328125, + "y": 3584.04443359375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9843137264251708, + "g": 0.9764705896377563, + "r": 0.9607843160629272 + }, + "position": 0.1899999976158142 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9803921580314636, + "g": 0.91372549533844, + "r": 0.7764706015586853 + }, + "position": 0.3799999952316284 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9725490212440492, + "g": 0.8588235378265381, + "r": 0.6039215922355652 + }, + "position": 0.5 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.95686274766922, + "g": 0.7843137383460999, + "r": 0.3843137323856354 + }, + "position": 0.6600000262260437 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9333333373069764, + "g": 0.729411780834198, + "r": 0.21960784494876864 + }, + "position": 0.8399999737739563 + } + ], + "gradientTransform": [ + [ + -1.6364001035690308, + 1.5055831670761108, + -0.08484271168708801 + ], + [ + -1.5728986263275146, + -1.5663667917251587, + 2.0555288791656494 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 22.848167419433594, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "orbit-outer (Stroke)", + "opacity": 1, + "strokeAlign": "CENTER", + "strokeWeight": 5.050000190734863, + "visible": true, + "width": 23.82176399230957, + "x": 0.036175258457660675, + "y": 2.5443549156188965 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1101;136:9039": { + "id": "I43:1101;136:9039", + "type": "VECTOR", + "fields": { + "parentId": "I43:1101;136:9031", + "absoluteBoundingBox": { + "height": 13.113574981689451, + "width": 23.719501495361328, + "x": 10555.98828125, + "y": 3590.49658203125 + }, + "absoluteRenderBounds": { + "height": 13.113525390625, + "width": 23.7197265625, + "x": 10555.98828125, + "y": 3590.49658203125 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.9843137264251708, + "g": 0.9803921580314636, + "r": 0.9647058844566344 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 13.113574981689451, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "orbit-inner", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 23.719501495361328, + "x": 2.9880521297454834, + "y": 8.9966402053833 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1101;136:9040": { + "id": "I43:1101;136:9040", + "type": "VECTOR", + "fields": { + "parentId": "I43:1101;136:9031", + "absoluteBoundingBox": { + "height": 6.9661030769348145, + "width": 15.414355278015137, + "x": 10556.1689453125, + "y": 3587.310546875 + }, + "absoluteRenderBounds": { + "height": 6.966064453125, + "width": 15.4140625, + "x": 10556.1689453125, + "y": 3587.310546875 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9843137264251708, + "g": 0.9803921580314636, + "r": 0.9647058844566344 + }, + "position": 0.029999999329447743 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9176470637321472, + "g": 0.9529411792755128, + "r": 0.7686274647712708 + }, + "position": 0.119999997317791 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.8549019694328308, + "g": 0.8941176533699036, + "r": 0.5607843399047852 + }, + "position": 0.3499999940395355 + }, + { + "boundVariables": {}, + "color": { + "a": 0.8999999761581421, + "b": 0.8274509906768799, + "g": 0.8156862854957581, + "r": 0.2705882489681244 + }, + "position": 0.6200000047683716 + }, + { + "boundVariables": {}, + "color": { + "a": 0.6000000238418579, + "b": 0.8039215803146362, + "g": 0.7607843279838562, + "r": 0.14901961386203766 + }, + "position": 0.8600000143051147 + }, + { + "boundVariables": {}, + "color": { + "a": 0, + "b": 0.772549033164978, + "g": 0.7215686440467834, + "r": 0.11372549086809158 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 0.9895601272583008, + -0.06226864457130432, + 0.0533602200448513 + ], + [ + 0.13806335628032684, + 0.446306437253952, + 0.05430806800723076 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 6.9661030769348145, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "orbit-inner-tail", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 15.414355278015137, + "x": 3.168776512145996, + "y": 5.810537815093994 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1101;136:9041": { + "id": "I43:1101;136:9041", + "type": "GROUP", + "fields": { + "parentId": "43:1101", + "childrenIds": [ + "I43:1101;136:9042", + "I43:1101;136:9043", + "I43:1101;136:9044", + "I43:1101;136:9045", + "I43:1101;136:9046", + "I43:1101;136:9047", + "I43:1101;136:9048", + "I43:1101;136:9049", + "I43:1101;136:9050", + "I43:1101;136:9051" + ], + "absoluteBoundingBox": { + "height": 16.56101417541504, + "width": 137.43359375, + "x": 10588.50390625, + "y": 3587.0244140625 + }, + "absoluteRenderBounds": { + "height": 16.56103515625, + "width": 137.43359375, + "x": 10588.50390625, + "y": 3587.0244140625 + }, + "height": 16.56101417541504, + "inferredAutoLayout": { + "counterAxisAlignItems": "MAX", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 1.8411893844604492, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0.000019073486328125, + "paddingLeft": 0.0003662109375, + "paddingRight": 0.00037384033203125, + "paddingTop": 0.00009918212890625, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "AUTO" + }, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "ChondroGen", + "opacity": 1, + "visible": true, + "width": 137.43359375, + "x": 35.50390625, + "y": 5.5244140625 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1101;136:9042": { + "id": "I43:1101;136:9042", + "type": "VECTOR", + "fields": { + "parentId": "I43:1101;136:9041", + "absoluteBoundingBox": { + "height": 11.892135620117188, + "width": 11.560546875, + "x": 10714.376953125, + "y": 3591.472900390625 + }, + "absoluteRenderBounds": { + "height": 11.89208984375, + "width": 11.560546875, + "x": 10714.376953125, + "y": 3591.472900390625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.6980392336845398, + "g": 0.6117647290229797, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 11.892135620117188, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 11.560546875, + "x": 161.37657928466797, + "y": 9.973018646240234 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1101;136:9043": { + "id": "I43:1101;136:9043", + "type": "VECTOR", + "fields": { + "parentId": "I43:1101;136:9041", + "absoluteBoundingBox": { + "height": 12.046292304992676, + "width": 12.000109672546388, + "x": 10700.1767578125, + "y": 3591.472900390625 + }, + "absoluteRenderBounds": { + "height": 12.04638671875, + "width": 12, + "x": 10700.1767578125, + "y": 3591.472900390625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.6980392336845398, + "g": 0.6117647290229797, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 12.046292304992676, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 12.000109672546388, + "x": 147.1771240234375, + "y": 9.973018646240234 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1101;136:9044": { + "id": "I43:1101;136:9044", + "type": "VECTOR", + "fields": { + "parentId": "I43:1101;136:9041", + "absoluteBoundingBox": { + "height": 15.85618019104004, + "width": 14.505629539489746, + "x": 10683.771484375, + "y": 3587.729248046875 + }, + "absoluteRenderBounds": { + "height": 15.856201171875, + "width": 14.505859375, + "x": 10683.771484375, + "y": 3587.729248046875 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.6980392336845398, + "g": 0.6117647290229797, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 15.85618019104004, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 14.505629539489746, + "x": 130.7712631225586, + "y": 6.229228973388672 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1101;136:9045": { + "id": "I43:1101;136:9045", + "type": "VECTOR", + "fields": { + "parentId": "I43:1101;136:9041", + "absoluteBoundingBox": { + "height": 12.046292304992676, + "width": 12.505603790283203, + "x": 10669.9130859375, + "y": 3591.472900390625 + }, + "absoluteRenderBounds": { + "height": 12.04638671875, + "width": 12.505859375, + "x": 10669.9130859375, + "y": 3591.472900390625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.3803921639919281, + "g": 0.2078431397676468, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 12.046292304992676, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 12.505603790283203, + "x": 116.91329956054688, + "y": 9.973018646240234 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1101;136:9046": { + "id": "I43:1101;136:9046", + "type": "VECTOR", + "fields": { + "parentId": "I43:1101;136:9041", + "absoluteBoundingBox": { + "height": 11.892135620117188, + "width": 6.791267395019531, + "x": 10662.3056640625, + "y": 3591.472900390625 + }, + "absoluteRenderBounds": { + "height": 11.89208984375, + "width": 6.791015625, + "x": 10662.3056640625, + "y": 3591.472900390625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.3803921639919281, + "g": 0.2078431397676468, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 11.892135620117188, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 6.791267395019531, + "x": 109.3053970336914, + "y": 9.973018646240234 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1101;136:9047": { + "id": "I43:1101;136:9047", + "type": "VECTOR", + "fields": { + "parentId": "I43:1101;136:9041", + "absoluteBoundingBox": { + "height": 16.494831085205078, + "width": 12.439671516418455, + "x": 10646.689453125, + "y": 3587.0244140625 + }, + "absoluteRenderBounds": { + "height": 16.494873046875, + "width": 12.439453125, + "x": 10646.689453125, + "y": 3587.0244140625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.3803921639919281, + "g": 0.2078431397676468, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 16.494831085205078, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 12.439671516418455, + "x": 93.68937683105467, + "y": 5.524513244628906 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1101;136:9048": { + "id": "I43:1101;136:9048", + "type": "VECTOR", + "fields": { + "parentId": "I43:1101;136:9041", + "absoluteBoundingBox": { + "height": 11.892135620117188, + "width": 11.560542106628418, + "x": 10633.029296875, + "y": 3591.472900390625 + }, + "absoluteRenderBounds": { + "height": 11.89208984375, + "width": 11.560546875, + "x": 10633.029296875, + "y": 3591.472900390625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.3803921639919281, + "g": 0.2078431397676468, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 11.892135620117188, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 11.560542106628418, + "x": 80.02886962890625, + "y": 9.973018646240234 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1101;136:9049": { + "id": "I43:1101;136:9049", + "type": "VECTOR", + "fields": { + "parentId": "I43:1101;136:9041", + "absoluteBoundingBox": { + "height": 12.046292304992676, + "width": 12.505608558654783, + "x": 10618.3359375, + "y": 3591.472900390625 + }, + "absoluteRenderBounds": { + "height": 12.04638671875, + "width": 12.505859375, + "x": 10618.3359375, + "y": 3591.472900390625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.3803921639919281, + "g": 0.2078431397676468, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 12.046292304992676, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 12.505608558654783, + "x": 65.33576965332031, + "y": 9.973018646240234 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1101;136:9050": { + "id": "I43:1101;136:9050", + "type": "VECTOR", + "fields": { + "parentId": "I43:1101;136:9041", + "absoluteBoundingBox": { + "height": 16.340675354003906, + "width": 11.560545921325684, + "x": 10604.6748046875, + "y": 3587.0244140625 + }, + "absoluteRenderBounds": { + "height": 16.340576171875, + "width": 11.560546875, + "x": 10604.6748046875, + "y": 3587.0244140625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.3803921639919281, + "g": 0.2078431397676468, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 16.340675354003906, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 11.560545921325684, + "x": 51.675262451171875, + "y": 5.524513244628906 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1101;136:9051": { + "id": "I43:1101;136:9051", + "type": "VECTOR", + "fields": { + "parentId": "I43:1101;136:9041", + "absoluteBoundingBox": { + "height": 15.85618019104004, + "width": 14.329800605773926, + "x": 10588.50390625, + "y": 3587.729248046875 + }, + "absoluteRenderBounds": { + "height": 15.856201171875, + "width": 14.330078125, + "x": 10588.50390625, + "y": 3587.729248046875 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.3803921639919281, + "g": 0.2078431397676468, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 15.85618019104004, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 14.329800605773926, + "x": 35.5042724609375, + "y": 6.229228973388672 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1140;43:1138": { + "id": "I43:1140;43:1138", + "type": "VECTOR", + "fields": { + "parentId": "43:1140", + "absoluteBoundingBox": { + "height": 14.625, + "width": 19.5, + "x": 10862.25, + "y": 3588.1875 + }, + "absoluteRenderBounds": { + "height": 14.625, + "width": 19.5, + "x": 10862.25, + "y": 3588.1875 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0, + "g": 0, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 14.625, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeWeight": 0.09375, + "visible": true, + "width": 19.5, + "x": 3.25, + "y": 5.6875 + }, + "extra": {}, + "fieldErrors": {} + }, + "I43:1148;43:1138": { + "id": "I43:1148;43:1138", + "type": "VECTOR", + "fields": { + "parentId": "43:1148", + "absoluteBoundingBox": { + "height": 14.625, + "width": 19.5, + "x": 10862.25, + "y": 3754.6875 + }, + "absoluteRenderBounds": { + "height": 14.625, + "width": 19.5, + "x": 10862.25, + "y": 3754.6875 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 14.625, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeWeight": 0.09375, + "visible": true, + "width": 19.5, + "x": 3.25, + "y": 5.6875 + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5183;136:9027": { + "id": "I44:5183;136:9027", + "type": "VECTOR", + "fields": { + "parentId": "44:5183", + "absoluteBoundingBox": { + "height": 32, + "width": 32.304901123046875, + "x": 10557, + "y": 3388 + }, + "absoluteRenderBounds": { + "height": 32, + "width": 32.3046875, + "x": 10557, + "y": 3388 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.7490196228027344, + "g": 0.5607843399047852, + "r": 0.03921568766236305 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.6823529601097107, + "g": 0.5058823823928833, + "r": 0.007843137718737125 + }, + "position": 0.30000001192092896 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.5607843399047852, + "g": 0.3764705955982208, + "r": 0.003921568859368563 + }, + "position": 0.44999998807907104 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.46666666865348816, + "g": 0.2823529541492462, + "r": 0 + }, + "position": 0.6100000143051147 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.40784314274787903, + "g": 0.22745098173618317, + "r": 0 + }, + "position": 0.7599999904632568 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.3843137323856354, + "g": 0.2078431397676468, + "r": 0.003921568859368563 + }, + "position": 0.9100000262260436 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.3843137323856354, + "g": 0.2117647081613541, + "r": 0.003921568859368563 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 0.6454866528511047, + 0, + 0.31524205207824707 + ], + [ + 0, + 0.6454866528511047, + 0.204462930560112 + ] + ], + "opacity": 1, + "type": "GRADIENT_RADIAL", + "visible": true + } + ], + "height": 32, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "sphere", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 32.304901123046875, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5183;136:9028": { + "id": "I44:5183;136:9028", + "type": "GROUP", + "fields": { + "parentId": "44:5183", + "childrenIds": [ + "I44:5183;136:9029", + "I44:5183;136:9031" + ], + "absoluteBoundingBox": { + "height": 32, + "width": 32.304901123046875, + "x": 10557, + "y": 3388 + }, + "absoluteRenderBounds": { + "height": 32, + "width": 32.304901123046875, + "x": 10557, + "y": 3388 + }, + "height": 32, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Clip path group", + "opacity": 1, + "visible": true, + "width": 32.304901123046875, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5183;136:9029": { + "id": "I44:5183;136:9029", + "type": "GROUP", + "fields": { + "parentId": "I44:5183;136:9028", + "childrenIds": [ + "I44:5183;136:9030" + ], + "absoluteBoundingBox": { + "height": 32, + "width": 32.304901123046875, + "x": 10557, + "y": 3388 + }, + "absoluteRenderBounds": null, + "height": 32, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "isMask": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "sphere-clip", + "opacity": 1, + "visible": true, + "width": 32.304901123046875, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5183;136:9030": { + "id": "I44:5183;136:9030", + "type": "VECTOR", + "fields": { + "parentId": "I44:5183;136:9029", + "absoluteBoundingBox": { + "height": 32, + "width": 32.304901123046875, + "x": 10557, + "y": 3388 + }, + "absoluteRenderBounds": { + "height": 32, + "width": 32.3046875, + "x": 10557, + "y": 3388 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0, + "g": 0, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 32, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 32.304901123046875, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5183;136:9031": { + "id": "I44:5183;136:9031", + "type": "GROUP", + "fields": { + "parentId": "I44:5183;136:9028", + "childrenIds": [ + "I44:5183;136:9032", + "I44:5183;136:9033", + "I44:5183;136:9034", + "I44:5183;136:9035", + "I44:5183;136:9036", + "I44:5183;136:9037", + "I44:5183;136:9038", + "I44:5183;136:9039", + "I44:5183;136:9040" + ], + "absoluteBoundingBox": { + "height": 32, + "width": 32.304901123046875, + "x": 10557, + "y": 3388 + }, + "absoluteRenderBounds": { + "height": 32, + "width": 32.3046875, + "x": 10557, + "y": 3388 + }, + "height": 32, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Group", + "opacity": 1, + "visible": true, + "width": 32.304901123046875, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5183;136:9032": { + "id": "I44:5183;136:9032", + "type": "VECTOR", + "fields": { + "parentId": "I44:5183;136:9031", + "absoluteBoundingBox": { + "height": 32, + "width": 32.304901123046875, + "x": 10557, + "y": 3388 + }, + "absoluteRenderBounds": { + "height": 32, + "width": 32.3046875, + "x": 10557, + "y": 3388 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 0.41999998688697815, + "b": 0.3333333432674408, + "g": 0.16470588743686676, + "r": 0 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 0.20000000298023224, + "b": 0.3333333432674408, + "g": 0.16470588743686676, + "r": 0 + }, + "position": 0.550000011920929 + }, + { + "boundVariables": {}, + "color": { + "a": 0, + "b": 0.3333333432674408, + "g": 0.16470588743686676, + "r": 0 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 1.252997875213623, + 0, + 0.08783221244812012 + ], + [ + 0, + 1.252997875213623, + -0.7154324054718018 + ] + ], + "opacity": 1, + "type": "GRADIENT_RADIAL", + "visible": true + } + ], + "height": 32, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "shade-outside-orbit", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 32.304901123046875, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5183;136:9033": { + "id": "I44:5183;136:9033", + "type": "VECTOR", + "fields": { + "parentId": "I44:5183;136:9031", + "absoluteBoundingBox": { + "height": 32, + "width": 32.304901123046875, + "x": 10557, + "y": 3388 + }, + "absoluteRenderBounds": { + "height": 32, + "width": 32.3046875, + "x": 10557, + "y": 3388 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 0.14000000059604645, + "b": 0.8470588326454163, + "g": 0.6509804129600525, + "r": 0.10588235408067705 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 0, + "b": 0.8470588326454163, + "g": 0.6509804129600525, + "r": 0.10588235408067705 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 1.9364598989486697, + 0, + 0.468456894159317 + ], + [ + 0, + 1.9364598989486697, + -0.5502269268035889 + ] + ], + "opacity": 1, + "type": "GRADIENT_RADIAL", + "visible": true + } + ], + "height": 32, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "light-left-overlay", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 32.304901123046875, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5183;136:9034": { + "id": "I44:5183;136:9034", + "type": "VECTOR", + "fields": { + "parentId": "I44:5183;136:9031", + "absoluteBoundingBox": { + "height": 6.700944423675537, + "width": 13.174972534179688, + "x": 10574.4453125, + "y": 3410.81103515625 + }, + "absoluteRenderBounds": { + "height": 6.700927734375, + "width": 13.1748046875, + "x": 10574.4453125, + "y": 3410.81103515625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 0, + "b": 0.5764706134796143, + "g": 0.3529411852359772, + "r": 0.04313725605607033 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 0.550000011920929, + "b": 0.5764706134796143, + "g": 0.3529411852359772, + "r": 0.04313725605607033 + }, + "position": 0.30000001192092896 + }, + { + "boundVariables": {}, + "color": { + "a": 0.5, + "b": 0.5764706134796143, + "g": 0.3529411852359772, + "r": 0.04313725605607033 + }, + "position": 0.75 + }, + { + "boundVariables": {}, + "color": { + "a": 0, + "b": 0.5764706134796143, + "g": 0.3529411852359772, + "r": 0.04313725605607033 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 0.8151227831840515, + -0.2092042565345764, + 0.19614684581756592 + ], + [ + 0.4074420928955078, + 0.41853103041648865, + 0.08879981189966202 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 6.700944423675537, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "rim-highlight (Stroke)", + "opacity": 1, + "strokeAlign": "CENTER", + "strokeWeight": 3.849999904632568, + "visible": true, + "width": 13.174972534179688, + "x": 17.445581436157227, + "y": 22.81094741821289 + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5183;136:9035": { + "id": "I44:5183;136:9035", + "type": "VECTOR", + "fields": { + "parentId": "I44:5183;136:9031", + "absoluteBoundingBox": { + "height": 18.27490997314453, + "width": 18.435367584228516, + "x": 10560.697265625, + "y": 3394.618896484375 + }, + "absoluteRenderBounds": { + "height": 18.27490234375, + "width": 18.435546875, + "x": 10560.697265625, + "y": 3394.618896484375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.8156862854957581, + "g": 0.7843137383460999, + "r": 0.03921568766236305 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.5882353186607361, + "g": 0.4313725531101227, + "r": 0 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 0.8613659739494324, + 0.45280781388282776, + -0.1629999577999115 + ], + [ + -0.4524722993373871, + 0.8620046973228455, + 0.31514695286750793 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 18.27490997314453, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "core", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 18.435367584228516, + "x": 3.697471857070923, + "y": 6.618992328643799 + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5183;136:9036": { + "id": "I44:5183;136:9036", + "type": "VECTOR", + "fields": { + "parentId": "I44:5183;136:9031", + "absoluteBoundingBox": { + "height": 10.8725004196167, + "width": 10.976094245910645, + "x": 10563.9345703125, + "y": 3398.346923828125 + }, + "absoluteRenderBounds": { + "height": 10.87255859375, + "width": 10.9765625, + "x": 10563.9345703125, + "y": 3398.346923828125 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.8745098114013672, + "g": 0.9333333373069764, + "r": 0.6705882549285889 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.7607843279838562, + "g": 0.7215686440467834, + "r": 0.0235294122248888 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 0.8080573678016663, + 0.5874120593070984, + -0.19792257249355316 + ], + [ + -0.5874120593070984, + 0.8080573678016663, + 0.3895654380321502 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 10.8725004196167, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "ring", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 10.976094245910645, + "x": 6.9348273277282715, + "y": 10.346875190734863 + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5183;136:9037": { + "id": "I44:5183;136:9037", + "type": "VECTOR", + "fields": { + "parentId": "I44:5183;136:9031", + "absoluteBoundingBox": { + "height": 5.911250114440918, + "width": 5.967573165893555, + "x": 10566.33203125, + "y": 3400.721923828125 + }, + "absoluteRenderBounds": { + "height": 5.9111328125, + "width": 5.9677734375, + "x": 10566.33203125, + "y": 3400.721923828125 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.6196078658103943, + "g": 0.4745098054409027, + "r": 0.007843137718737125 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.545098066329956, + "g": 0.3764705955982208, + "r": 0.003921568859368563 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 0.9339293241500854, + 0.3536636531352997, + -0.14337609708309174 + ], + [ + -0.3536636531352997, + 0.9339293241500854, + 0.2099337726831436 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 5.911250114440918, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "ring-center", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 5.967573165893555, + "x": 9.332456588745115, + "y": 12.72187614440918 + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5183;136:9038": { + "id": "I44:5183;136:9038", + "type": "VECTOR", + "fields": { + "parentId": "I44:5183;136:9031", + "absoluteBoundingBox": { + "height": 26.112192153930664, + "width": 27.539609909057617, + "x": 10557.0419921875, + "y": 3390.90771484375 + }, + "absoluteRenderBounds": { + "height": 26.1123046875, + "width": 27.5400390625, + "x": 10557.0419921875, + "y": 3390.90771484375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9843137264251708, + "g": 0.9764705896377563, + "r": 0.9607843160629272 + }, + "position": 0.1899999976158142 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9803921580314636, + "g": 0.91372549533844, + "r": 0.7764706015586853 + }, + "position": 0.3799999952316284 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9725490212440492, + "g": 0.8588235378265381, + "r": 0.6039215922355652 + }, + "position": 0.5 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.95686274766922, + "g": 0.7843137383460999, + "r": 0.3843137323856354 + }, + "position": 0.6600000262260437 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9333333373069764, + "g": 0.729411780834198, + "r": 0.21960784494876864 + }, + "position": 0.8399999737739563 + } + ], + "gradientTransform": [ + [ + -1.6364001035690308, + 1.5055831670761108, + -0.08484271168708801 + ], + [ + -1.5728986263275146, + -1.5663667917251587, + 2.0555288791656494 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 26.112192153930664, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "orbit-outer (Stroke)", + "opacity": 1, + "strokeAlign": "CENTER", + "strokeWeight": 5.050000190734863, + "visible": true, + "width": 27.539609909057617, + "x": 0.041821110993623734, + "y": 2.907834053039551 + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5183;136:9039": { + "id": "I44:5183;136:9039", + "type": "VECTOR", + "fields": { + "parentId": "I44:5183;136:9031", + "absoluteBoundingBox": { + "height": 14.986942291259766, + "width": 27.421390533447266, + "x": 10560.4541015625, + "y": 3398.281982421875 + }, + "absoluteRenderBounds": { + "height": 14.987060546875, + "width": 27.421875, + "x": 10560.4541015625, + "y": 3398.281982421875 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.9843137264251708, + "g": 0.9803921580314636, + "r": 0.9647058844566344 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 14.986942291259766, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "orbit-inner", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 27.421390533447266, + "x": 3.454395532608032, + "y": 10.281874656677246 + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5183;136:9040": { + "id": "I44:5183;136:9040", + "type": "VECTOR", + "fields": { + "parentId": "I44:5183;136:9031", + "absoluteBoundingBox": { + "height": 7.961260318756104, + "width": 17.820064544677734, + "x": 10560.6630859375, + "y": 3394.640625 + }, + "absoluteRenderBounds": { + "height": 7.961181640625, + "width": 17.8203125, + "x": 10560.6630859375, + "y": 3394.640625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9843137264251708, + "g": 0.9803921580314636, + "r": 0.9647058844566344 + }, + "position": 0.029999999329447743 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9176470637321472, + "g": 0.9529411792755128, + "r": 0.7686274647712708 + }, + "position": 0.119999997317791 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.8549019694328308, + "g": 0.8941176533699036, + "r": 0.5607843399047852 + }, + "position": 0.3499999940395355 + }, + { + "boundVariables": {}, + "color": { + "a": 0.8999999761581421, + "b": 0.8274509906768799, + "g": 0.8156862854957581, + "r": 0.2705882489681244 + }, + "position": 0.6200000047683716 + }, + { + "boundVariables": {}, + "color": { + "a": 0.6000000238418579, + "b": 0.8039215803146362, + "g": 0.7607843279838562, + "r": 0.14901961386203766 + }, + "position": 0.8600000143051147 + }, + { + "boundVariables": {}, + "color": { + "a": 0, + "b": 0.772549033164978, + "g": 0.7215686440467834, + "r": 0.11372549086809158 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 0.9895601272583008, + -0.06226864457130432, + 0.0533602200448513 + ], + [ + 0.13806335628032684, + 0.446306437253952, + 0.05430806800723076 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 7.961260318756104, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "orbit-inner-tail", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 17.820064544677734, + "x": 3.663325548171997, + "y": 6.6406145095825195 + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5183;136:9041": { + "id": "I44:5183;136:9041", + "type": "GROUP", + "fields": { + "parentId": "44:5183", + "childrenIds": [ + "I44:5183;136:9042", + "I44:5183;136:9043", + "I44:5183;136:9044", + "I44:5183;136:9045", + "I44:5183;136:9046", + "I44:5183;136:9047", + "I44:5183;136:9048", + "I44:5183;136:9049", + "I44:5183;136:9050", + "I44:5183;136:9051" + ], + "absoluteBoundingBox": { + "height": 18.926769256591797, + "width": 158.8823699951172, + "x": 10598.044921875, + "y": 3394.313720703125 + }, + "absoluteRenderBounds": { + "height": 18.926769256591797, + "width": 158.8828125, + "x": 10598.044921875, + "y": 3394.313720703125 + }, + "height": 18.926769256591797, + "inferredAutoLayout": { + "counterAxisAlignItems": "MAX", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 2.128551483154297, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0.00002288818359375, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0.00000762939453125, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "AUTO" + }, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "ChondroGen", + "opacity": 1, + "visible": true, + "width": 158.8823699951172, + "x": 41.044921875, + "y": 6.313720703125 + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5183;136:9042": { + "id": "I44:5183;136:9042", + "type": "VECTOR", + "fields": { + "parentId": "I44:5183;136:9041", + "absoluteBoundingBox": { + "height": 13.591012001037598, + "width": 13.36479377746582, + "x": 10743.5625, + "y": 3399.397705078125 + }, + "absoluteRenderBounds": { + "height": 13.591064453125, + "width": 13.365234375, + "x": 10743.5625, + "y": 3399.397705078125 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.6980392336845398, + "g": 0.6117647290229797, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 13.591012001037598, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 13.36479377746582, + "x": 186.56251525878903, + "y": 11.397735595703123 + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5183;136:9043": { + "id": "I44:5183;136:9043", + "type": "VECTOR", + "fields": { + "parentId": "I44:5183;136:9041", + "absoluteBoundingBox": { + "height": 13.76719093322754, + "width": 13.87295913696289, + "x": 10727.146484375, + "y": 3399.397705078125 + }, + "absoluteRenderBounds": { + "height": 13.76708984375, + "width": 13.873046875, + "x": 10727.146484375, + "y": 3399.397705078125 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.6980392336845398, + "g": 0.6117647290229797, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 13.76719093322754, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 13.87295913696289, + "x": 170.14695739746094, + "y": 11.397735595703123 + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5183;136:9044": { + "id": "I44:5183;136:9044", + "type": "VECTOR", + "fields": { + "parentId": "I44:5183;136:9041", + "absoluteBoundingBox": { + "height": 18.121349334716797, + "width": 16.769514083862305, + "x": 10708.1806640625, + "y": 3395.119140625 + }, + "absoluteRenderBounds": { + "height": 18.121337890625, + "width": 16.76953125, + "x": 10708.1806640625, + "y": 3395.119140625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.6980392336845398, + "g": 0.6117647290229797, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 18.121349334716797, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 16.769514083862305, + "x": 151.18064880371094, + "y": 7.119117736816406 + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5183;136:9045": { + "id": "I44:5183;136:9045", + "type": "VECTOR", + "fields": { + "parentId": "I44:5183;136:9041", + "absoluteBoundingBox": { + "height": 13.76719093322754, + "width": 14.457345008850098, + "x": 10692.16015625, + "y": 3399.397705078125 + }, + "absoluteRenderBounds": { + "height": 13.76708984375, + "width": 14.45703125, + "x": 10692.16015625, + "y": 3399.397705078125 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.3803921639919281, + "g": 0.2078431397676468, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 13.76719093322754, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 14.457345008850098, + "x": 135.15988159179688, + "y": 11.397735595703123 + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5183;136:9046": { + "id": "I44:5183;136:9046", + "type": "VECTOR", + "fields": { + "parentId": "I44:5183;136:9041", + "absoluteBoundingBox": { + "height": 13.591012001037598, + "width": 7.851175785064697, + "x": 10683.3642578125, + "y": 3399.397705078125 + }, + "absoluteRenderBounds": { + "height": 13.591064453125, + "width": 7.8515625, + "x": 10683.3642578125, + "y": 3399.397705078125 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.3803921639919281, + "g": 0.2078431397676468, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 13.591012001037598, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 7.851175785064697, + "x": 126.36461639404295, + "y": 11.397735595703123 + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5183;136:9047": { + "id": "I44:5183;136:9047", + "type": "VECTOR", + "fields": { + "parentId": "I44:5183;136:9041", + "absoluteBoundingBox": { + "height": 18.85123634338379, + "width": 14.381123542785645, + "x": 10665.3115234375, + "y": 3394.313720703125 + }, + "absoluteRenderBounds": { + "height": 18.851318359375, + "width": 14.380859375, + "x": 10665.3115234375, + "y": 3394.313720703125 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.3803921639919281, + "g": 0.2078431397676468, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 18.85123634338379, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 14.381123542785645, + "x": 108.31141662597656, + "y": 6.313728332519531 + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5183;136:9048": { + "id": "I44:5183;136:9048", + "type": "VECTOR", + "fields": { + "parentId": "I44:5183;136:9041", + "absoluteBoundingBox": { + "height": 13.591012001037598, + "width": 13.364788055419922, + "x": 10649.5185546875, + "y": 3399.397705078125 + }, + "absoluteRenderBounds": { + "height": 13.591064453125, + "width": 13.365234375, + "x": 10649.5185546875, + "y": 3399.397705078125 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.3803921639919281, + "g": 0.2078431397676468, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 13.591012001037598, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 13.364788055419922, + "x": 92.51893615722656, + "y": 11.397735595703123 + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5183;136:9049": { + "id": "I44:5183;136:9049", + "type": "VECTOR", + "fields": { + "parentId": "I44:5183;136:9041", + "absoluteBoundingBox": { + "height": 13.76719093322754, + "width": 14.457350730895996, + "x": 10632.5322265625, + "y": 3399.397705078125 + }, + "absoluteRenderBounds": { + "height": 13.76708984375, + "width": 14.45703125, + "x": 10632.5322265625, + "y": 3399.397705078125 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.3803921639919281, + "g": 0.2078431397676468, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 13.76719093322754, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 14.457350730895996, + "x": 75.53268432617188, + "y": 11.397735595703123 + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5183;136:9050": { + "id": "I44:5183;136:9050", + "type": "VECTOR", + "fields": { + "parentId": "I44:5183;136:9041", + "absoluteBoundingBox": { + "height": 18.67505645751953, + "width": 13.364792823791504, + "x": 10616.740234375, + "y": 3394.313720703125 + }, + "absoluteRenderBounds": { + "height": 18.675048828125, + "width": 13.365234375, + "x": 10616.740234375, + "y": 3394.313720703125 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.3803921639919281, + "g": 0.2078431397676468, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 18.67505645751953, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 13.364792823791504, + "x": 59.74018859863281, + "y": 6.313728332519531 + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5183;136:9051": { + "id": "I44:5183;136:9051", + "type": "VECTOR", + "fields": { + "parentId": "I44:5183;136:9041", + "absoluteBoundingBox": { + "height": 18.121349334716797, + "width": 16.566242218017578, + "x": 10598.044921875, + "y": 3395.119140625 + }, + "absoluteRenderBounds": { + "height": 18.121337890625, + "width": 16.56640625, + "x": 10598.044921875, + "y": 3395.119140625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.3803921639919281, + "g": 0.2078431397676468, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 18.121349334716797, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 16.566242218017578, + "x": 41.04539489746094, + "y": 7.119117736816406 + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5185;18:959": { + "id": "I44:5185;18:959", + "type": "TEXT", + "fields": { + "parentId": "44:5185", + "absoluteBoundingBox": { + "height": 17, + "width": 59, + "x": 11045, + "y": 3395.5 + }, + "absoluteRenderBounds": { + "height": 15.130126953125, + "width": 56.3564453125, + "x": 11045.6630859375, + "y": 3396.72998046875 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:44:5420", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "기업소개", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.1529411822557449, + "g": 0.08627451211214066, + "r": 0.06666667014360428 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Bold" + }, + "fontSize": 17, + "height": 17, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 100 + }, + "maxHeight": null, + "maxWidth": null, + "name": "menu", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:ab9a4b96cc300838fb4b14185fdd981a07ac2475,", + "textTruncation": "DISABLED", + "visible": true, + "width": 59, + "x": 0, + "y": 15.5, + "styledTextSegments": [ + { + "characters": "기업소개", + "end": 4, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:ab9a4b96cc300838fb4b14185fdd981a07ac2475," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5186;18:959": { + "id": "I44:5186;18:959", + "type": "TEXT", + "fields": { + "parentId": "44:5186", + "absoluteBoundingBox": { + "height": 17, + "width": 95, + "x": 11144, + "y": 3395.5 + }, + "absoluteRenderBounds": { + "height": 15.1640625, + "width": 93.427734375, + "x": 11144.6630859375, + "y": 3396.72998046875 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:44:5420", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "기술/연구개발", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.1529411822557449, + "g": 0.08627451211214066, + "r": 0.06666667014360428 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Bold" + }, + "fontSize": 17, + "height": 17, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 100 + }, + "maxHeight": null, + "maxWidth": null, + "name": "menu", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:ab9a4b96cc300838fb4b14185fdd981a07ac2475,", + "textTruncation": "DISABLED", + "visible": true, + "width": 95, + "x": 0, + "y": 15.5, + "styledTextSegments": [ + { + "characters": "기술/연구개발", + "end": 7, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:ab9a4b96cc300838fb4b14185fdd981a07ac2475," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5187;18:959": { + "id": "I44:5187;18:959", + "type": "TEXT", + "fields": { + "parentId": "44:5187", + "absoluteBoundingBox": { + "height": 17, + "width": 59, + "x": 11279, + "y": 3395.5 + }, + "absoluteRenderBounds": { + "height": 15.130126953125, + "width": 56.7138671875, + "x": 11279.3056640625, + "y": 3396.72998046875 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:44:5420", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "제품소개", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.1529411822557449, + "g": 0.08627451211214066, + "r": 0.06666667014360428 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Bold" + }, + "fontSize": 17, + "height": 17, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 100 + }, + "maxHeight": null, + "maxWidth": null, + "name": "menu", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:ab9a4b96cc300838fb4b14185fdd981a07ac2475,", + "textTruncation": "DISABLED", + "visible": true, + "width": 59, + "x": 0, + "y": 15.5, + "styledTextSegments": [ + { + "characters": "제품소개", + "end": 4, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:ab9a4b96cc300838fb4b14185fdd981a07ac2475," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5188;18:959": { + "id": "I44:5188;18:959", + "type": "TEXT", + "fields": { + "parentId": "44:5188", + "absoluteBoundingBox": { + "height": 17, + "width": 30, + "x": 11378, + "y": 3395.5 + }, + "absoluteRenderBounds": { + "height": 15.1298828125, + "width": 26.99609375, + "x": 11378.578125, + "y": 3396.7470703125 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:44:5420", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "소식", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.1529411822557449, + "g": 0.08627451211214066, + "r": 0.06666667014360428 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Bold" + }, + "fontSize": 17, + "height": 17, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 100 + }, + "maxHeight": null, + "maxWidth": null, + "name": "menu", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:ab9a4b96cc300838fb4b14185fdd981a07ac2475,", + "textTruncation": "DISABLED", + "visible": true, + "width": 30, + "x": 0, + "y": 15.5, + "styledTextSegments": [ + { + "characters": "소식", + "end": 2, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:ab9a4b96cc300838fb4b14185fdd981a07ac2475," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5189;18:959": { + "id": "I44:5189;18:959", + "type": "TEXT", + "fields": { + "parentId": "44:5189", + "absoluteBoundingBox": { + "height": 17, + "width": 59, + "x": 11448, + "y": 3395.5 + }, + "absoluteRenderBounds": { + "height": 15.18115234375, + "width": 56.0849609375, + "x": 11448.560546875, + "y": 3396.712890625 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:44:5420", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "문의하기", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.1529411822557449, + "g": 0.08627451211214066, + "r": 0.06666667014360428 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Bold" + }, + "fontSize": 17, + "height": 17, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 100 + }, + "maxHeight": null, + "maxWidth": null, + "name": "menu", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:ab9a4b96cc300838fb4b14185fdd981a07ac2475,", + "textTruncation": "DISABLED", + "visible": true, + "width": 59, + "x": 0, + "y": 15.5, + "styledTextSegments": [ + { + "characters": "문의하기", + "end": 4, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:ab9a4b96cc300838fb4b14185fdd981a07ac2475," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5191;20:1321": { + "id": "I44:5191;20:1321", + "type": "TEXT", + "fields": { + "parentId": "44:5191", + "absoluteBoundingBox": { + "height": 17, + "width": 37, + "x": 11873, + "y": 3392 + }, + "absoluteRenderBounds": null, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:44:5420", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "KOR", + "componentPropertyReferences": { + "visible": "text#20:1" + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.1529411822557449, + "g": 0.08627451211214066, + "r": 0.06666667014360428 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Bold" + }, + "fontSize": 17, + "height": 17, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 100 + }, + "maxHeight": null, + "maxWidth": null, + "name": "KOR", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:ab9a4b96cc300838fb4b14185fdd981a07ac2475,", + "textTruncation": "DISABLED", + "visible": false, + "width": 37, + "x": 44, + "y": 12, + "styledTextSegments": [ + { + "characters": "KOR", + "end": 3, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:ab9a4b96cc300838fb4b14185fdd981a07ac2475," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5191;20:1333": { + "id": "I44:5191;20:1333", + "type": "INSTANCE", + "fields": { + "parentId": "44:5191", + "childrenIds": [ + "I44:5191;20:1333;40:437" + ], + "absoluteBoundingBox": { + "height": 24, + "width": 24, + "x": 11841, + "y": 3392 + }, + "absoluteRenderBounds": null, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": false + } + ], + "height": 24, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 2.2495806217193604, + "paddingLeft": 2.229278326034546, + "paddingRight": 2.2495806217193604, + "paddingTop": 2.229278326034546, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "MagnifyingGlass", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "targetAspectRatio": { + "x": 24, + "y": 24 + }, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 24, + "x": 12, + "y": 12, + "mainComponentId": "40:438", + "mainComponentName": "MagnifyingGlass", + "mainComponentSetId": null + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5191;20:1333;40:437": { + "id": "I44:5191;20:1333;40:437", + "type": "VECTOR", + "fields": { + "parentId": "I44:5191;20:1333", + "absoluteBoundingBox": { + "height": 19.521141052246097, + "width": 19.521141052246097, + "x": 11843.2294921875, + "y": 3394.229248046875 + }, + "absoluteRenderBounds": null, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.1529411822557449, + "g": 0.08627451211214066, + "r": 0.06666667014360428 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 19.521141052246097, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeWeight": 0.09375, + "visible": true, + "width": 19.521141052246097, + "x": 2.229278326034546, + "y": 2.229278326034546 + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5192;20:1321": { + "id": "I44:5192;20:1321", + "type": "TEXT", + "fields": { + "parentId": "44:5192", + "absoluteBoundingBox": { + "height": 23, + "width": 32, + "x": 11921, + "y": 3392.5 + }, + "absoluteRenderBounds": { + "height": 11.31005859375, + "width": 29.6357421875, + "x": 11922.2001953125, + "y": 3398.429931640625 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:40:430", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "KOR", + "componentPropertyReferences": { + "visible": "text#20:1" + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.1529411822557449, + "g": 0.08627451211214066, + "r": 0.06666667014360428 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Bold" + }, + "fontSize": 15, + "height": 23, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "KOR", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:01da56e122aa148e08cea720f74dbe1f2d759cdb,", + "textTruncation": "DISABLED", + "visible": true, + "width": 32, + "x": 44, + "y": 12.5, + "styledTextSegments": [ + { + "characters": "KOR", + "end": 3, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:01da56e122aa148e08cea720f74dbe1f2d759cdb," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5192;20:1333": { + "id": "I44:5192;20:1333", + "type": "INSTANCE", + "fields": { + "parentId": "44:5192", + "childrenIds": [ + "I44:5192;20:1333;40:440" + ], + "absoluteBoundingBox": { + "height": 24, + "width": 24, + "x": 11889, + "y": 3392 + }, + "absoluteRenderBounds": { + "height": 24, + "width": 24, + "x": 11889, + "y": 3392 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": false + } + ], + "height": 24, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 2.2499961853027344, + "paddingLeft": 2.249995708465576, + "paddingRight": 2.250000476837158, + "paddingTop": 2.25, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "isAsset": true, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Globe", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "targetAspectRatio": { + "x": 24, + "y": 24 + }, + "topLeftRadius": 0, + "topRightRadius": 0, + "visible": true, + "width": 24, + "x": 12, + "y": 12, + "mainComponentId": "40:441", + "mainComponentName": "Globe", + "mainComponentSetId": null + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5192;20:1333;40:440": { + "id": "I44:5192;20:1333;40:440", + "type": "VECTOR", + "fields": { + "parentId": "I44:5192;20:1333", + "absoluteBoundingBox": { + "height": 19.500003814697266, + "width": 19.500003814697266, + "x": 11891.25, + "y": 3394.25 + }, + "absoluteRenderBounds": { + "height": 19.5, + "width": 19.5, + "x": 11891.25, + "y": 3394.25 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.1529411822557449, + "g": 0.08627451211214066, + "r": 0.06666667014360428 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 19.500003814697266, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeWeight": 0.09375, + "visible": true, + "width": 19.500003814697266, + "x": 2.249995708465576, + "y": 2.25 + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5195;136:9027": { + "id": "I44:5195;136:9027", + "type": "VECTOR", + "fields": { + "parentId": "44:5195", + "absoluteBoundingBox": { + "height": 28, + "width": 27.94373893737793, + "x": 10553, + "y": 3666 + }, + "absoluteRenderBounds": { + "height": 28, + "width": 27.943359375, + "x": 10553, + "y": 3666 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.7490196228027344, + "g": 0.5607843399047852, + "r": 0.03921568766236305 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.6823529601097107, + "g": 0.5058823823928833, + "r": 0.007843137718737125 + }, + "position": 0.30000001192092896 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.5607843399047852, + "g": 0.3764705955982208, + "r": 0.003921568859368563 + }, + "position": 0.44999998807907104 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.46666666865348816, + "g": 0.2823529541492462, + "r": 0 + }, + "position": 0.6100000143051147 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.40784314274787903, + "g": 0.22745098173618317, + "r": 0 + }, + "position": 0.7599999904632568 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.3843137323856354, + "g": 0.2078431397676468, + "r": 0.003921568859368563 + }, + "position": 0.9100000262260436 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.3843137323856354, + "g": 0.2117647081613541, + "r": 0.003921568859368563 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 0.6454866528511047, + 0, + 0.31524205207824707 + ], + [ + 0, + 0.6454866528511047, + 0.204462930560112 + ] + ], + "opacity": 1, + "type": "GRADIENT_RADIAL", + "visible": true + } + ], + "height": 28, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "sphere", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 27.94373893737793, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5195;136:9028": { + "id": "I44:5195;136:9028", + "type": "GROUP", + "fields": { + "parentId": "44:5195", + "childrenIds": [ + "I44:5195;136:9029", + "I44:5195;136:9031" + ], + "absoluteBoundingBox": { + "height": 28, + "width": 27.94373893737793, + "x": 10553, + "y": 3666 + }, + "absoluteRenderBounds": { + "height": 28, + "width": 27.94373893737793, + "x": 10553, + "y": 3666 + }, + "height": 28, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Clip path group", + "opacity": 1, + "visible": true, + "width": 27.94373893737793, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5195;136:9029": { + "id": "I44:5195;136:9029", + "type": "GROUP", + "fields": { + "parentId": "I44:5195;136:9028", + "childrenIds": [ + "I44:5195;136:9030" + ], + "absoluteBoundingBox": { + "height": 28, + "width": 27.94373893737793, + "x": 10553, + "y": 3666 + }, + "absoluteRenderBounds": null, + "height": 28, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "VERTICAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "isMask": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "sphere-clip", + "opacity": 1, + "visible": true, + "width": 27.94373893737793, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5195;136:9030": { + "id": "I44:5195;136:9030", + "type": "VECTOR", + "fields": { + "parentId": "I44:5195;136:9029", + "absoluteBoundingBox": { + "height": 28, + "width": 27.94373893737793, + "x": 10553, + "y": 3666 + }, + "absoluteRenderBounds": { + "height": 28, + "width": 27.943359375, + "x": 10553, + "y": 3666 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0, + "g": 0, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 28, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 27.94373893737793, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5195;136:9031": { + "id": "I44:5195;136:9031", + "type": "GROUP", + "fields": { + "parentId": "I44:5195;136:9028", + "childrenIds": [ + "I44:5195;136:9032", + "I44:5195;136:9033", + "I44:5195;136:9034", + "I44:5195;136:9035", + "I44:5195;136:9036", + "I44:5195;136:9037", + "I44:5195;136:9038", + "I44:5195;136:9039", + "I44:5195;136:9040" + ], + "absoluteBoundingBox": { + "height": 28, + "width": 27.94373893737793, + "x": 10553, + "y": 3666 + }, + "absoluteRenderBounds": { + "height": 28, + "width": 27.943359375, + "x": 10553, + "y": 3666 + }, + "height": 28, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Group", + "opacity": 1, + "visible": true, + "width": 27.94373893737793, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5195;136:9032": { + "id": "I44:5195;136:9032", + "type": "VECTOR", + "fields": { + "parentId": "I44:5195;136:9031", + "absoluteBoundingBox": { + "height": 28, + "width": 27.94373893737793, + "x": 10553, + "y": 3666 + }, + "absoluteRenderBounds": { + "height": 28, + "width": 27.943359375, + "x": 10553, + "y": 3666 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 0.41999998688697815, + "b": 0.3333333432674408, + "g": 0.16470588743686676, + "r": 0 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 0.20000000298023224, + "b": 0.3333333432674408, + "g": 0.16470588743686676, + "r": 0 + }, + "position": 0.550000011920929 + }, + { + "boundVariables": {}, + "color": { + "a": 0, + "b": 0.3333333432674408, + "g": 0.16470588743686676, + "r": 0 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 1.252997875213623, + 0, + 0.08783221244812012 + ], + [ + 0, + 1.252997875213623, + -0.7154324054718018 + ] + ], + "opacity": 1, + "type": "GRADIENT_RADIAL", + "visible": true + } + ], + "height": 28, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "shade-outside-orbit", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 27.94373893737793, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5195;136:9033": { + "id": "I44:5195;136:9033", + "type": "VECTOR", + "fields": { + "parentId": "I44:5195;136:9031", + "absoluteBoundingBox": { + "height": 28, + "width": 27.94373893737793, + "x": 10553, + "y": 3666 + }, + "absoluteRenderBounds": { + "height": 28, + "width": 27.943359375, + "x": 10553, + "y": 3666 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 0.14000000059604645, + "b": 0.8470588326454163, + "g": 0.6509804129600525, + "r": 0.10588235408067705 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 0, + "b": 0.8470588326454163, + "g": 0.6509804129600525, + "r": 0.10588235408067705 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 1.9364598989486697, + 0, + 0.468456894159317 + ], + [ + 0, + 1.9364598989486697, + -0.5502269268035889 + ] + ], + "opacity": 1, + "type": "GRADIENT_RADIAL", + "visible": true + } + ], + "height": 28, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "light-left-overlay", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 27.94373893737793, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5195;136:9034": { + "id": "I44:5195;136:9034", + "type": "VECTOR", + "fields": { + "parentId": "I44:5195;136:9031", + "absoluteBoundingBox": { + "height": 5.863326072692871, + "width": 11.39635181427002, + "x": 10568.0908203125, + "y": 3685.95947265625 + }, + "absoluteRenderBounds": { + "height": 5.86328125, + "width": 11.396484375, + "x": 10568.0908203125, + "y": 3685.95947265625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 0, + "b": 0.5764706134796143, + "g": 0.3529411852359772, + "r": 0.04313725605607033 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 0.550000011920929, + "b": 0.5764706134796143, + "g": 0.3529411852359772, + "r": 0.04313725605607033 + }, + "position": 0.30000001192092896 + }, + { + "boundVariables": {}, + "color": { + "a": 0.5, + "b": 0.5764706134796143, + "g": 0.3529411852359772, + "r": 0.04313725605607033 + }, + "position": 0.75 + }, + { + "boundVariables": {}, + "color": { + "a": 0, + "b": 0.5764706134796143, + "g": 0.3529411852359772, + "r": 0.04313725605607033 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 0.8151227831840515, + -0.2092042565345764, + 0.19614684581756592 + ], + [ + 0.4074420928955078, + 0.41853103041648865, + 0.08879981189966202 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 5.863326072692871, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "rim-highlight (Stroke)", + "opacity": 1, + "strokeAlign": "CENTER", + "strokeWeight": 3.849999904632568, + "visible": true, + "width": 11.39635181427002, + "x": 15.09042739868164, + "y": 19.95957946777344 + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5195;136:9035": { + "id": "I44:5195;136:9035", + "type": "VECTOR", + "fields": { + "parentId": "I44:5195;136:9031", + "absoluteBoundingBox": { + "height": 15.99054718017578, + "width": 15.946592330932615, + "x": 10556.1982421875, + "y": 3671.79150390625 + }, + "absoluteRenderBounds": { + "height": 15.990478515625, + "width": 15.9462890625, + "x": 10556.1982421875, + "y": 3671.79150390625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.8156862854957581, + "g": 0.7843137383460999, + "r": 0.03921568766236305 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.5882353186607361, + "g": 0.4313725531101227, + "r": 0 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 0.8613659739494324, + 0.45280781388282776, + -0.1629999577999115 + ], + [ + -0.4524722993373871, + 0.8620046973228455, + 0.31514695286750793 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 15.99054718017578, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "core", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 15.946592330932615, + "x": 3.1983132362365723, + "y": 5.791618347167969 + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5195;136:9036": { + "id": "I44:5195;136:9036", + "type": "VECTOR", + "fields": { + "parentId": "I44:5195;136:9031", + "absoluteBoundingBox": { + "height": 9.51343822479248, + "width": 9.494321823120115, + "x": 10558.9990234375, + "y": 3675.053466796875 + }, + "absoluteRenderBounds": { + "height": 9.513427734375, + "width": 9.494140625, + "x": 10558.9990234375, + "y": 3675.053466796875 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.8745098114013672, + "g": 0.9333333373069764, + "r": 0.6705882549285889 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.7607843279838562, + "g": 0.7215686440467834, + "r": 0.0235294122248888 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 0.8080573678016663, + 0.5874120593070984, + -0.19792257249355316 + ], + [ + -0.5874120593070984, + 0.8080573678016663, + 0.3895654380321502 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 9.51343822479248, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "ring", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 9.494321823120115, + "x": 5.998625755310059, + "y": 9.053515434265137 + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5195;136:9037": { + "id": "I44:5195;136:9037", + "type": "VECTOR", + "fields": { + "parentId": "I44:5195;136:9031", + "absoluteBoundingBox": { + "height": 5.172343730926514, + "width": 5.161951065063477, + "x": 10561.072265625, + "y": 3677.131591796875 + }, + "absoluteRenderBounds": { + "height": 5.17236328125, + "width": 5.162109375, + "x": 10561.072265625, + "y": 3677.131591796875 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.6196078658103943, + "g": 0.4745098054409027, + "r": 0.007843137718737125 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.545098066329956, + "g": 0.3764705955982208, + "r": 0.003921568859368563 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 0.9339293241500854, + 0.3536636531352997, + -0.14337609708309174 + ], + [ + -0.3536636531352997, + 0.9339293241500854, + 0.2099337726831436 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 5.172343730926514, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "ring-center", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 5.161951065063477, + "x": 8.072575569152832, + "y": 11.131641387939451 + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5195;136:9038": { + "id": "I44:5195;136:9038", + "type": "VECTOR", + "fields": { + "parentId": "I44:5195;136:9031", + "absoluteBoundingBox": { + "height": 22.848167419433594, + "width": 23.82176399230957, + "x": 10553.0361328125, + "y": 3668.54443359375 + }, + "absoluteRenderBounds": { + "height": 22.84814453125, + "width": 23.8212890625, + "x": 10553.0361328125, + "y": 3668.54443359375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9843137264251708, + "g": 0.9764705896377563, + "r": 0.9607843160629272 + }, + "position": 0.1899999976158142 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9803921580314636, + "g": 0.91372549533844, + "r": 0.7764706015586853 + }, + "position": 0.3799999952316284 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9725490212440492, + "g": 0.8588235378265381, + "r": 0.6039215922355652 + }, + "position": 0.5 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.95686274766922, + "g": 0.7843137383460999, + "r": 0.3843137323856354 + }, + "position": 0.6600000262260437 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9333333373069764, + "g": 0.729411780834198, + "r": 0.21960784494876864 + }, + "position": 0.8399999737739563 + } + ], + "gradientTransform": [ + [ + -1.6364001035690308, + 1.5055831670761108, + -0.08484271168708801 + ], + [ + -1.5728986263275146, + -1.5663667917251587, + 2.0555288791656494 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 22.848167419433594, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "orbit-outer (Stroke)", + "opacity": 1, + "strokeAlign": "CENTER", + "strokeWeight": 5.050000190734863, + "visible": true, + "width": 23.82176399230957, + "x": 0.036175258457660675, + "y": 2.5443549156188965 + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5195;136:9039": { + "id": "I44:5195;136:9039", + "type": "VECTOR", + "fields": { + "parentId": "I44:5195;136:9031", + "absoluteBoundingBox": { + "height": 13.113574981689451, + "width": 23.719501495361328, + "x": 10555.98828125, + "y": 3674.99658203125 + }, + "absoluteRenderBounds": { + "height": 13.113525390625, + "width": 23.7197265625, + "x": 10555.98828125, + "y": 3674.99658203125 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.9843137264251708, + "g": 0.9803921580314636, + "r": 0.9647058844566344 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 13.113574981689451, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "orbit-inner", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 23.719501495361328, + "x": 2.9880521297454834, + "y": 8.9966402053833 + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5195;136:9040": { + "id": "I44:5195;136:9040", + "type": "VECTOR", + "fields": { + "parentId": "I44:5195;136:9031", + "absoluteBoundingBox": { + "height": 6.9661030769348145, + "width": 15.414355278015137, + "x": 10556.1689453125, + "y": 3671.810546875 + }, + "absoluteRenderBounds": { + "height": 6.966064453125, + "width": 15.4140625, + "x": 10556.1689453125, + "y": 3671.810546875 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9843137264251708, + "g": 0.9803921580314636, + "r": 0.9647058844566344 + }, + "position": 0.029999999329447743 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9176470637321472, + "g": 0.9529411792755128, + "r": 0.7686274647712708 + }, + "position": 0.119999997317791 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.8549019694328308, + "g": 0.8941176533699036, + "r": 0.5607843399047852 + }, + "position": 0.3499999940395355 + }, + { + "boundVariables": {}, + "color": { + "a": 0.8999999761581421, + "b": 0.8274509906768799, + "g": 0.8156862854957581, + "r": 0.2705882489681244 + }, + "position": 0.6200000047683716 + }, + { + "boundVariables": {}, + "color": { + "a": 0.6000000238418579, + "b": 0.8039215803146362, + "g": 0.7607843279838562, + "r": 0.14901961386203766 + }, + "position": 0.8600000143051147 + }, + { + "boundVariables": {}, + "color": { + "a": 0, + "b": 0.772549033164978, + "g": 0.7215686440467834, + "r": 0.11372549086809158 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 0.9895601272583008, + -0.06226864457130432, + 0.0533602200448513 + ], + [ + 0.13806335628032684, + 0.446306437253952, + 0.05430806800723076 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 6.9661030769348145, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "orbit-inner-tail", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 15.414355278015137, + "x": 3.168776512145996, + "y": 5.810537815093994 + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5195;136:9041": { + "id": "I44:5195;136:9041", + "type": "GROUP", + "fields": { + "parentId": "44:5195", + "childrenIds": [ + "I44:5195;136:9042", + "I44:5195;136:9043", + "I44:5195;136:9044", + "I44:5195;136:9045", + "I44:5195;136:9046", + "I44:5195;136:9047", + "I44:5195;136:9048", + "I44:5195;136:9049", + "I44:5195;136:9050", + "I44:5195;136:9051" + ], + "absoluteBoundingBox": { + "height": 16.56101417541504, + "width": 137.43359375, + "x": 10588.50390625, + "y": 3671.5244140625 + }, + "absoluteRenderBounds": { + "height": 16.56103515625, + "width": 137.43359375, + "x": 10588.50390625, + "y": 3671.5244140625 + }, + "height": 16.56101417541504, + "inferredAutoLayout": { + "counterAxisAlignItems": "MAX", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 1.8411893844604492, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0.000019073486328125, + "paddingLeft": 0.0003662109375, + "paddingRight": 0.00037384033203125, + "paddingTop": 0.00009918212890625, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "AUTO" + }, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "ChondroGen", + "opacity": 1, + "visible": true, + "width": 137.43359375, + "x": 35.50390625, + "y": 5.5244140625 + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5195;136:9042": { + "id": "I44:5195;136:9042", + "type": "VECTOR", + "fields": { + "parentId": "I44:5195;136:9041", + "absoluteBoundingBox": { + "height": 11.892135620117188, + "width": 11.560546875, + "x": 10714.376953125, + "y": 3675.972900390625 + }, + "absoluteRenderBounds": { + "height": 11.89208984375, + "width": 11.560546875, + "x": 10714.376953125, + "y": 3675.972900390625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.6980392336845398, + "g": 0.6117647290229797, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 11.892135620117188, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 11.560546875, + "x": 161.37657928466797, + "y": 9.973018646240234 + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5195;136:9043": { + "id": "I44:5195;136:9043", + "type": "VECTOR", + "fields": { + "parentId": "I44:5195;136:9041", + "absoluteBoundingBox": { + "height": 12.046292304992676, + "width": 12.000109672546388, + "x": 10700.1767578125, + "y": 3675.972900390625 + }, + "absoluteRenderBounds": { + "height": 12.04638671875, + "width": 12, + "x": 10700.1767578125, + "y": 3675.972900390625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.6980392336845398, + "g": 0.6117647290229797, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 12.046292304992676, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 12.000109672546388, + "x": 147.1771240234375, + "y": 9.973018646240234 + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5195;136:9044": { + "id": "I44:5195;136:9044", + "type": "VECTOR", + "fields": { + "parentId": "I44:5195;136:9041", + "absoluteBoundingBox": { + "height": 15.85618019104004, + "width": 14.505629539489746, + "x": 10683.771484375, + "y": 3672.229248046875 + }, + "absoluteRenderBounds": { + "height": 15.856201171875, + "width": 14.505859375, + "x": 10683.771484375, + "y": 3672.229248046875 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.6980392336845398, + "g": 0.6117647290229797, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 15.85618019104004, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 14.505629539489746, + "x": 130.7712631225586, + "y": 6.229228973388672 + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5195;136:9045": { + "id": "I44:5195;136:9045", + "type": "VECTOR", + "fields": { + "parentId": "I44:5195;136:9041", + "absoluteBoundingBox": { + "height": 12.046292304992676, + "width": 12.505603790283203, + "x": 10669.9130859375, + "y": 3675.972900390625 + }, + "absoluteRenderBounds": { + "height": 12.04638671875, + "width": 12.505859375, + "x": 10669.9130859375, + "y": 3675.972900390625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.3803921639919281, + "g": 0.2078431397676468, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 12.046292304992676, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 12.505603790283203, + "x": 116.91329956054688, + "y": 9.973018646240234 + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5195;136:9046": { + "id": "I44:5195;136:9046", + "type": "VECTOR", + "fields": { + "parentId": "I44:5195;136:9041", + "absoluteBoundingBox": { + "height": 11.892135620117188, + "width": 6.791267395019531, + "x": 10662.3056640625, + "y": 3675.972900390625 + }, + "absoluteRenderBounds": { + "height": 11.89208984375, + "width": 6.791015625, + "x": 10662.3056640625, + "y": 3675.972900390625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.3803921639919281, + "g": 0.2078431397676468, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 11.892135620117188, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 6.791267395019531, + "x": 109.3053970336914, + "y": 9.973018646240234 + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5195;136:9047": { + "id": "I44:5195;136:9047", + "type": "VECTOR", + "fields": { + "parentId": "I44:5195;136:9041", + "absoluteBoundingBox": { + "height": 16.494831085205078, + "width": 12.439671516418455, + "x": 10646.689453125, + "y": 3671.5244140625 + }, + "absoluteRenderBounds": { + "height": 16.494873046875, + "width": 12.439453125, + "x": 10646.689453125, + "y": 3671.5244140625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.3803921639919281, + "g": 0.2078431397676468, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 16.494831085205078, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 12.439671516418455, + "x": 93.68937683105467, + "y": 5.524513244628906 + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5195;136:9048": { + "id": "I44:5195;136:9048", + "type": "VECTOR", + "fields": { + "parentId": "I44:5195;136:9041", + "absoluteBoundingBox": { + "height": 11.892135620117188, + "width": 11.560542106628418, + "x": 10633.029296875, + "y": 3675.972900390625 + }, + "absoluteRenderBounds": { + "height": 11.89208984375, + "width": 11.560546875, + "x": 10633.029296875, + "y": 3675.972900390625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.3803921639919281, + "g": 0.2078431397676468, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 11.892135620117188, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 11.560542106628418, + "x": 80.02886962890625, + "y": 9.973018646240234 + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5195;136:9049": { + "id": "I44:5195;136:9049", + "type": "VECTOR", + "fields": { + "parentId": "I44:5195;136:9041", + "absoluteBoundingBox": { + "height": 12.046292304992676, + "width": 12.505608558654783, + "x": 10618.3359375, + "y": 3675.972900390625 + }, + "absoluteRenderBounds": { + "height": 12.04638671875, + "width": 12.505859375, + "x": 10618.3359375, + "y": 3675.972900390625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.3803921639919281, + "g": 0.2078431397676468, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 12.046292304992676, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 12.505608558654783, + "x": 65.33576965332031, + "y": 9.973018646240234 + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5195;136:9050": { + "id": "I44:5195;136:9050", + "type": "VECTOR", + "fields": { + "parentId": "I44:5195;136:9041", + "absoluteBoundingBox": { + "height": 16.340675354003906, + "width": 11.560545921325684, + "x": 10604.6748046875, + "y": 3671.5244140625 + }, + "absoluteRenderBounds": { + "height": 16.340576171875, + "width": 11.560546875, + "x": 10604.6748046875, + "y": 3671.5244140625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.3803921639919281, + "g": 0.2078431397676468, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 16.340675354003906, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 11.560545921325684, + "x": 51.675262451171875, + "y": 5.524513244628906 + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5195;136:9051": { + "id": "I44:5195;136:9051", + "type": "VECTOR", + "fields": { + "parentId": "I44:5195;136:9041", + "absoluteBoundingBox": { + "height": 15.85618019104004, + "width": 14.329800605773926, + "x": 10588.50390625, + "y": 3672.229248046875 + }, + "absoluteRenderBounds": { + "height": 15.856201171875, + "width": 14.330078125, + "x": 10588.50390625, + "y": 3672.229248046875 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.3803921639919281, + "g": 0.2078431397676468, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 15.85618019104004, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 14.329800605773926, + "x": 35.5042724609375, + "y": 6.229228973388672 + }, + "extra": {}, + "fieldErrors": {} + }, + "I44:5197;43:1138": { + "id": "I44:5197;43:1138", + "type": "VECTOR", + "fields": { + "parentId": "44:5197", + "absoluteBoundingBox": { + "height": 14.625, + "width": 19.5, + "x": 10862.25, + "y": 3672.6875 + }, + "absoluteRenderBounds": { + "height": 14.625, + "width": 19.5, + "x": 10862.25, + "y": 3672.6875 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0, + "g": 0, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 14.625, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeWeight": 0.09375, + "visible": true, + "width": 19.5, + "x": 3.25, + "y": 5.6875 + }, + "extra": {}, + "fieldErrors": {} + }, + "I90:10317;40:437": { + "id": "I90:10317;40:437", + "type": "VECTOR", + "fields": { + "parentId": "90:10317", + "absoluteBoundingBox": { + "height": 21.147903442382812, + "width": 21.147903442382812, + "x": 10861.4150390625, + "y": 3584.9150390625 + }, + "absoluteRenderBounds": null, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.1529411822557449, + "g": 0.08627451211214066, + "r": 0.06666667014360428 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 21.147903442382812, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeWeight": 0.09375, + "visible": true, + "width": 21.147903442382812, + "x": 2.4150514602661133, + "y": 2.4150514602661133 + }, + "extra": {}, + "fieldErrors": {} + }, + "I90:10324;40:437": { + "id": "I90:10324;40:437", + "type": "VECTOR", + "fields": { + "parentId": "90:10324", + "absoluteBoundingBox": { + "height": 21.147903442382812, + "width": 21.147903442382812, + "x": 10861.4150390625, + "y": 3669.4150390625 + }, + "absoluteRenderBounds": null, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.1529411822557449, + "g": 0.08627451211214066, + "r": 0.06666667014360428 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 21.147903442382812, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeWeight": 0.09375, + "visible": true, + "width": 21.147903442382812, + "x": 2.4150514602661133, + "y": 2.4150514602661133 + }, + "extra": {}, + "fieldErrors": {} + }, + "I90:10393;40:437": { + "id": "I90:10393;40:437", + "type": "VECTOR", + "fields": { + "parentId": "90:10393", + "absoluteBoundingBox": { + "height": 21.147903442382812, + "width": 21.147903442382812, + "x": 10861.4150390625, + "y": 3751.4150390625 + }, + "absoluteRenderBounds": null, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 21.147903442382812, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeWeight": 0.09375, + "visible": true, + "width": 21.147903442382812, + "x": 2.4150514602661133, + "y": 2.4150514602661133 + }, + "extra": {}, + "fieldErrors": {} + } + }, + "diagnostics": [] +} From 90b85213ac7e799e7c515b2bf8467ed61e077de0 Mon Sep 17 00:00:00 2001 From: leader Date: Sat, 24 Oct 2026 11:59:54 +0900 Subject: [PATCH 128/149] fix(codegen): resolve variant expressions by raw Figma property key and keep boolean-controlled layers - variant lookups use Definition.source, so icon/mask/src/state styles are no longer dropped (empty Caret/Logo/DropdownOption bodies) - children with a visible property reference count as visible for asset classification (IconButton text layer) - responsive expressions drop redundant trailing inheritance and collapse slot-0-only arrays to scalars - guard test: every declared prop is referenced in the body --- .../changepack_log_eRTXLDvy0yMJ3YwboDGWu.json | 2 +- .../src/codegen/style/style_properties.rs | 11 +- .../src/codegen/variant/drawn_when.rs | 26 +- .../src/codegen/variant/variant_projection.rs | 8 +- .../tests/chondrogen_component_typings.rs | 83 + .../tests/fixtures/chondrogen_caret_set.json | 709 ++++ .../chondrogen_dropdown_option_set.json | 737 ++++ .../tests/fixtures/chondrogen_logo_set.json | 3665 +++++++++++++++++ 8 files changed, 5221 insertions(+), 20 deletions(-) create mode 100644 crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_caret_set.json create mode 100644 crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_dropdown_option_set.json create mode 100644 crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_logo_set.json diff --git a/.changepacks/changepack_log_eRTXLDvy0yMJ3YwboDGWu.json b/.changepacks/changepack_log_eRTXLDvy0yMJ3YwboDGWu.json index b252347d..1531f126 100644 --- a/.changepacks/changepack_log_eRTXLDvy0yMJ3YwboDGWu.json +++ b/.changepacks/changepack_log_eRTXLDvy0yMJ3YwboDGWu.json @@ -2,6 +2,6 @@ "changes": { "Cargo.toml": "Patch" }, - "note": "componentTsx: fix button set panic, expose component text as unique props even when declared names normalize alike, drop set-canvas placement, emit undefined for absent variant values, preserve scalar typography and image sources across responsive variants including one-sided sources with explicit responsive visibility, prevent leaf variant image sources from leaking onto containers, declare referenced boolean props, emit valid interaction selectors, and account explicitly for unrepresented variant children without claiming emitted property provenance", + "note": "componentTsx: fix button set panic, expose component text as unique props even when declared names normalize alike, drop set-canvas placement, emit undefined for absent variant values, preserve scalar typography and image sources across responsive variants including one-sided sources with explicit responsive visibility, prevent leaf variant image sources from leaking onto containers, declare referenced boolean props, emit valid interaction selectors, fix defect G by resolving variant values with original Figma property keys to preserve icon sources and state styles and keeping boolean-controlled hidden layers out of flattened assets, and account explicitly for unrepresented variant children without claiming emitted property provenance", "date": "2026-10-24T00:21:08.585604600Z" } diff --git a/crates/devup-mcp-devup-ui/src/codegen/style/style_properties.rs b/crates/devup-mcp-devup-ui/src/codegen/style/style_properties.rs index 832f8e1f..c5e61df2 100644 --- a/crates/devup-mcp-devup-ui/src/codegen/style/style_properties.rs +++ b/crates/devup-mcp-devup-ui/src/codegen/style/style_properties.rs @@ -1,8 +1,6 @@ use std::collections::{BTreeMap, BTreeSet}; - use devup_mcp_figma::{RawNode, Snapshot, TypedNode}; use serde_json::Value; - use super::{ component::{LengthToken, Prop, PropValue}, layout::{format_number, px, string_prop}, @@ -14,11 +12,9 @@ pub(crate) enum AssetKind { SvgMask, Png, } - pub(crate) fn asset_kind(snapshot: &Snapshot, node: &RawNode) -> Option { asset_kind_nested(snapshot, node, false) } - fn asset_kind_nested(snapshot: &Snapshot, node: &RawNode, nested: bool) -> Option { let view = node.typed_view(); if matches!(view.node_type(), "TEXT" | "COMPONENT_SET") @@ -87,7 +83,12 @@ fn asset_kind_nested(snapshot: &Snapshot, node: &RawNode, nested: bool) -> Optio let mut visible_children = Vec::new(); for child_id in child_ids { let child = snapshot.nodes.get(child_id)?; - if child.typed_view().bool("visible") != Some(false) { + if child.typed_view().bool("visible") != Some(false) + || child.typed_view().value("componentPropertyReferences") + .and_then(|references| references.get("visible")) + .and_then(Value::as_str) + .is_some() + { visible_children.push(child); } } diff --git a/crates/devup-mcp-devup-ui/src/codegen/variant/drawn_when.rs b/crates/devup-mcp-devup-ui/src/codegen/variant/drawn_when.rs index 5b46d969..31f16558 100644 --- a/crates/devup-mcp-devup-ui/src/codegen/variant/drawn_when.rs +++ b/crates/devup-mcp-devup-ui/src/codegen/variant/drawn_when.rs @@ -130,7 +130,7 @@ fn changed_effect_props( let effect_name = definitions .iter() .find(|definition| definition.name.eq_ignore_ascii_case("effect") && !dimensions.iter().any(|dimension| dimension.name == definition.name)) - .map(|definition| definition.name.clone()); + .map(|definition| definition.source.clone()); let keys = records .iter() .filter(|record| { @@ -187,7 +187,7 @@ fn expression_for_prop( let effect_name = definitions .iter() .find(|definition| definition.name.eq_ignore_ascii_case("effect") && !dimensions.iter().any(|dimension| dimension.name == definition.name)) - .map(|definition| definition.name.as_str()); + .map(|definition| definition.source.as_str()); let dependencies = dimensions .iter() .filter(|dimension| !dimension.boolean) @@ -281,7 +281,7 @@ fn dimension_depends( let set = records .iter() .filter(|record| { - record.values.get(&dimension.name) == Some(option) + record.values.get(&dimension.source) == Some(option) && effect_name.is_none_or(|name| { record.values.get(name).map(String::as_str) == Some(effect_value) }) @@ -362,14 +362,14 @@ fn nested_expression( .iter() .filter(|definition| !definition.boolean) .map(|definition| { - let value = if Some(definition.name.as_str()) == effect_name { + let value = if Some(definition.source.as_str()) == effect_name { effect_value } else if let Some(value) = pinned.get(&definition.name) { value.as_str() } else { &definition.default }; - (definition.name.clone(), value.to_owned()) + (definition.source.clone(), value.to_owned()) }) .collect::>(); find_record(records, &filters).is_some() @@ -486,7 +486,7 @@ fn viewport_expression( .iter() .filter(|definition| !definition.boolean) .map(|definition| { - let value = if Some(definition.name.as_str()) == effect_name { + let value = if Some(definition.source.as_str()) == effect_name { effect_value } else if definition.name.eq_ignore_ascii_case("viewport") { viewport_value.unwrap_or(&definition.default) @@ -495,7 +495,7 @@ fn viewport_expression( } else { &definition.default }; - (definition.name.clone(), value.to_owned()) + (definition.source.clone(), value.to_owned()) }) .collect::>(); find_record(records, &filters) @@ -800,16 +800,22 @@ fn render_expression_value(expression: &Expression, depth: usize) -> String { match expression { Expression::Undefined => "undefined".into(), Expression::Literal(value) => serde_json::to_string(value).unwrap_or_default(), - Expression::Responsive(slots) => format!( + Expression::Responsive(slots) => { + let end = slots.iter().rposition(Option::is_some).map_or(0, |index| index + 1); + if end == 1 { + return serde_json::to_string(&slots[0]).unwrap_or_default(); + } + format!( "[{}]", - slots + slots[..end] .iter() .map(|slot| slot .as_ref() .map_or("null".to_owned(), |value| serde_json::to_string(value).unwrap_or_default())) .collect::>() .join(", ") - ), + ) + }, Expression::Variant(prop, values) => { let indent = " ".repeat(depth); let child_indent = " ".repeat(depth + 1); diff --git a/crates/devup-mcp-devup-ui/src/codegen/variant/variant_projection.rs b/crates/devup-mcp-devup-ui/src/codegen/variant/variant_projection.rs index fa348ab4..8974546f 100644 --- a/crates/devup-mcp-devup-ui/src/codegen/variant/variant_projection.rs +++ b/crates/devup-mcp-devup-ui/src/codegen/variant/variant_projection.rs @@ -43,7 +43,6 @@ struct Record { tree: Tree, node_id: String, } - #[derive(Clone, Debug)] struct Definition { /// The name as it is written in code. @@ -57,7 +56,6 @@ struct Definition { /// which may carry a `#id` suffix that `name` has dropped. source: String, } - #[derive(Clone, Debug)] enum Expression { Undefined, @@ -70,7 +68,6 @@ enum Expression { Variant(String, Vec<(String, Expression)>), Conditional(String, String, String), } - pub(super) fn generate_variant_component_set( snapshot: &Snapshot, set_id: &str, @@ -195,7 +192,7 @@ pub(super) fn generate_variant_component_set( &records, &[], &mut unrepresented_variant_nodes, - effect.map(|definition| definition.name.as_str()), + effect.map(|definition| definition.source.as_str()), &default.values, ); root.props = merged_props( @@ -611,6 +608,9 @@ fn project_tree_inner( &options.length_tokens, &mut used_tokens, ); + if visible_when(&view).is_some() && view.bool("visible") == Some(false) { + props.retain(|(name, _)| name != "display"); + } if asset.is_some() { props.retain(|(name, _)| { !matches!( diff --git a/crates/devup-mcp-devup-ui/tests/chondrogen_component_typings.rs b/crates/devup-mcp-devup-ui/tests/chondrogen_component_typings.rs index 6fe80ebe..f0ce838e 100644 --- a/crates/devup-mcp-devup-ui/tests/chondrogen_component_typings.rs +++ b/crates/devup-mcp-devup-ui/tests/chondrogen_component_typings.rs @@ -159,3 +159,86 @@ fn chondrogen_scalar_props_and_image_elements_are_valid() { } } } + +#[test] +fn caret_variants_keep_mask_sources_and_dimensions() { + let tsx = generate(include_str!("fixtures/chondrogen_caret_set.json")); + assert!(tsx.contains("maskImage={{"), "{tsx}"); + for direction in ["caretdown", "caretleft", "caretright", "caretup"] { + assert!(tsx.contains(&format!("1-{direction}.svg")), "{tsx}"); + } + assert!(tsx.contains("boxSize=\"24px\""), "{tsx}"); + assert!(tsx.contains("}[property1]"), "{tsx}"); +} + +#[test] +fn logo_variants_keep_scalar_image_sources() { + let tsx = generate(include_str!("fixtures/chondrogen_logo_set.json")); + assert!(tsx.contains(">(); + for field in interface.lines() { + let property = field + .split(':') + .next() + .unwrap() + .trim() + .trim_end_matches('?'); + assert!( + identifiers.contains(&property), + "{name}: unused {property}\n{tsx}" + ); + } + } +} + +#[test] +fn responsive_values_omit_redundant_trailing_inheritance() { + let tsx = generate(include_str!("fixtures/chondrogen_board_row_set.json")); + assert!(!tsx.contains(", null]}"), "{tsx}"); + assert!(!tsx.contains("={[\"1\"]}"), "{tsx}"); +} diff --git a/crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_caret_set.json b/crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_caret_set.json new file mode 100644 index 00000000..07f942ce --- /dev/null +++ b/crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_caret_set.json @@ -0,0 +1,709 @@ +{ + "fileKey": "K2kUnxGgvooAJwCQiVTb7g", + "version": null, + "roots": [ + "40:459" + ], + "nodes": { + "40:445": { + "id": "40:445", + "type": "VECTOR", + "fields": { + "parentId": "40:458", + "absoluteBoundingBox": { + "height": 16.500835418701172, + "width": 9.001008033752441, + "x": 8063.74951171875, + "y": 1118.7496337890625 + }, + "absoluteRenderBounds": { + "height": 16.5008544921875, + "width": 9.0009765625, + "x": 8063.74951171875, + "y": 1118.7496337890625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0, + "g": 0, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 16.500835418701172, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeWeight": 0.09375, + "visible": true, + "width": 9.001008033752441, + "x": 6.7494096755981445, + "y": 3.7495830059051514 + }, + "extra": {}, + "fieldErrors": {} + }, + "40:447": { + "id": "40:447", + "type": "VECTOR", + "fields": { + "parentId": "40:457", + "absoluteBoundingBox": { + "height": 9.001006126403809, + "width": 16.500835418701172, + "x": 8092.74951171875, + "y": 1123.2496337890625 + }, + "absoluteRenderBounds": { + "height": 9.0009765625, + "width": 16.5009765625, + "x": 8092.74951171875, + "y": 1123.2496337890625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0, + "g": 0, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 9.001006126403809, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeWeight": 0.09375, + "visible": true, + "width": 16.500835418701172, + "x": 3.7495830059051514, + "y": 8.24958324432373 + }, + "extra": {}, + "fieldErrors": {} + }, + "40:449": { + "id": "40:449", + "type": "VECTOR", + "fields": { + "parentId": "40:456", + "absoluteBoundingBox": { + "height": 9.001179695129395, + "width": 16.501007080078125, + "x": 8124.74951171875, + "y": 1121.7493896484375 + }, + "absoluteRenderBounds": { + "height": 9.001220703125, + "width": 16.5009765625, + "x": 8124.74951171875, + "y": 1121.7493896484375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0, + "g": 0, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 9.001179695129395, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeWeight": 0.09375, + "visible": true, + "width": 16.501007080078125, + "x": 3.7495830059051514, + "y": 6.7494096755981445 + }, + "extra": {}, + "fieldErrors": {} + }, + "40:451": { + "id": "40:451", + "type": "VECTOR", + "fields": { + "parentId": "40:455", + "absoluteBoundingBox": { + "height": 16.500835418701172, + "width": 9.001006126403809, + "x": 8161.24951171875, + "y": 1118.7496337890625 + }, + "absoluteRenderBounds": { + "height": 16.5008544921875, + "width": 9.0009765625, + "x": 8161.24951171875, + "y": 1118.7496337890625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0, + "g": 0, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 16.500835418701172, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeWeight": 0.09375, + "visible": true, + "width": 9.001006126403809, + "x": 8.24958324432373, + "y": 3.7495830059051514 + }, + "extra": {}, + "fieldErrors": {} + }, + "40:455": { + "id": "40:455", + "type": "COMPONENT", + "fields": { + "parentId": "40:459", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "40:451" + ], + "absoluteBoundingBox": { + "height": 24, + "width": 24, + "x": 8153, + "y": 1115 + }, + "absoluteRenderBounds": { + "height": 24, + "width": 24, + "x": 8153, + "y": 1115 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": false + } + ], + "height": 24, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 3.7495815753936768, + "paddingLeft": 8.24958324432373, + "paddingRight": 6.749410629272461, + "paddingTop": 3.7495830059051514, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "isAsset": true, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "속성 1=CaretRight", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "targetAspectRatio": { + "x": 24, + "y": 24 + }, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "속성 1": "CaretRight" + }, + "visible": true, + "width": 24, + "x": 116, + "y": 20 + }, + "extra": {}, + "fieldErrors": {} + }, + "40:456": { + "id": "40:456", + "type": "COMPONENT", + "fields": { + "parentId": "40:459", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "40:449" + ], + "absoluteBoundingBox": { + "height": 24, + "width": 24, + "x": 8121, + "y": 1115 + }, + "absoluteRenderBounds": { + "height": 24, + "width": 24, + "x": 8121, + "y": 1115 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": false + } + ], + "height": 24, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 8.249410629272461, + "paddingLeft": 3.7495830059051514, + "paddingRight": 3.749409914016723, + "paddingTop": 6.7494096755981445, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "isAsset": true, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "속성 1=CaretUp", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "targetAspectRatio": { + "x": 24, + "y": 24 + }, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "속성 1": "CaretUp" + }, + "visible": true, + "width": 24, + "x": 84, + "y": 20 + }, + "extra": {}, + "fieldErrors": {} + }, + "40:457": { + "id": "40:457", + "type": "COMPONENT", + "fields": { + "parentId": "40:459", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "40:447" + ], + "absoluteBoundingBox": { + "height": 24, + "width": 24, + "x": 8089, + "y": 1115 + }, + "absoluteRenderBounds": { + "height": 24, + "width": 24, + "x": 8089, + "y": 1115 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": false + } + ], + "height": 24, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 6.749410629272461, + "paddingLeft": 3.7495830059051514, + "paddingRight": 3.7495815753936768, + "paddingTop": 8.24958324432373, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "isAsset": true, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "속성 1=CaretDown", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "targetAspectRatio": { + "x": 24, + "y": 24 + }, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "속성 1": "CaretDown" + }, + "visible": true, + "width": 24, + "x": 52, + "y": 20 + }, + "extra": {}, + "fieldErrors": {} + }, + "40:458": { + "id": "40:458", + "type": "COMPONENT", + "fields": { + "parentId": "40:459", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "40:445" + ], + "absoluteBoundingBox": { + "height": 24, + "width": 24, + "x": 8057, + "y": 1115 + }, + "absoluteRenderBounds": { + "height": 24, + "width": 24, + "x": 8057, + "y": 1115 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": false + } + ], + "height": 24, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 3.7495815753936768, + "paddingLeft": 6.7494096755981445, + "paddingRight": 8.249582290649414, + "paddingTop": 3.7495830059051514, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "isAsset": true, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "속성 1=CaretLeft", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "targetAspectRatio": { + "x": 24, + "y": 24 + }, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "속성 1": "CaretLeft" + }, + "visible": true, + "width": 24, + "x": 20, + "y": 20 + }, + "extra": {}, + "fieldErrors": {} + }, + "40:459": { + "id": "40:459", + "type": "COMPONENT_SET", + "fields": { + "parentId": "40:514", + "parentType": "SECTION", + "parentName": "Icons", + "childrenIds": [ + "40:458", + "40:457", + "40:456", + "40:455" + ], + "absoluteBoundingBox": { + "height": 64, + "width": 160, + "x": 8037, + "y": 1095 + }, + "absoluteRenderBounds": { + "height": 64, + "width": 160, + "x": 8037, + "y": 1095 + }, + "bottomLeftRadius": 5, + "bottomRightRadius": 5, + "clipsContent": true, + "componentPropertyDefinitions": { + "속성 1": { + "defaultValue": "CaretLeft", + "type": "VARIANT", + "variantOptions": [ + "CaretLeft", + "CaretDown", + "CaretUp", + "CaretRight" + ] + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 5, + "dashPattern": [ + 10, + 5 + ], + "defaultVariant": { + "$nodeId": "40:458", + "$nodeType": "COMPONENT" + }, + "height": 64, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Caret", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 0.27843138575553894, + "r": 0.5921568870544434 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "topLeftRadius": 5, + "topRightRadius": 5, + "visible": true, + "width": 160, + "x": 17, + "y": 57 + }, + "extra": {}, + "fieldErrors": {} + } + }, + "diagnostics": [] +} diff --git a/crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_dropdown_option_set.json b/crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_dropdown_option_set.json new file mode 100644 index 00000000..da7eee34 --- /dev/null +++ b/crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_dropdown_option_set.json @@ -0,0 +1,737 @@ +{ + "fileKey": "K2kUnxGgvooAJwCQiVTb7g", + "version": null, + "roots": [ + "45:2034" + ], + "nodes": { + "45:2025": { + "id": "45:2025", + "type": "TEXT", + "fields": { + "parentId": "45:2033", + "absoluteBoundingBox": { + "height": 24, + "width": 37, + "x": 8818, + "y": 4955 + }, + "absoluteRenderBounds": { + "height": 13.7919921875, + "width": 34.408203125, + "x": 8818.6396484375, + "y": 4960.18408203125 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:348", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:955", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "옵션 1", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:348", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.3176470696926117, + "g": 0.250980406999588, + "r": 0.21568627655506137 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Regular" + }, + "fontSize": 16, + "height": 24, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "옵션 1", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:e4c7b7dacbd4e73f6c967cfc38e87b24df7d8775,", + "textTruncation": "DISABLED", + "visible": true, + "width": 37, + "x": 16, + "y": 12, + "styledTextSegments": [ + { + "characters": "옵션 1", + "end": 4, + "fillStyleId": "", + "fontWeight": 400, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:e4c7b7dacbd4e73f6c967cfc38e87b24df7d8775," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "45:2028": { + "id": "45:2028", + "type": "TEXT", + "fields": { + "parentId": "45:2032", + "absoluteBoundingBox": { + "height": 24, + "width": 37, + "x": 8818, + "y": 5031 + }, + "absoluteRenderBounds": { + "height": 13.7919921875, + "width": 34.408203125, + "x": 8818.6396484375, + "y": 5036.18408203125 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:348", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:955", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "옵션 1", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:348", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.3176470696926117, + "g": 0.250980406999588, + "r": 0.21568627655506137 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Regular" + }, + "fontSize": 16, + "height": 24, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "옵션 1", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:e4c7b7dacbd4e73f6c967cfc38e87b24df7d8775,", + "textTruncation": "DISABLED", + "visible": true, + "width": 37, + "x": 16, + "y": 12, + "styledTextSegments": [ + { + "characters": "옵션 1", + "end": 4, + "fillStyleId": "", + "fontWeight": 400, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:e4c7b7dacbd4e73f6c967cfc38e87b24df7d8775," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "45:2030": { + "id": "45:2030", + "type": "TEXT", + "fields": { + "parentId": "45:2031", + "absoluteBoundingBox": { + "height": 24, + "width": 37, + "x": 8818, + "y": 5107 + }, + "absoluteRenderBounds": { + "height": 13.7919921875, + "width": 34.408203125, + "x": 8818.6396484375, + "y": 5112.18408203125 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:348", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:955", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "옵션 1", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:9:348", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.3176470696926117, + "g": 0.250980406999588, + "r": 0.21568627655506137 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "fontName": { + "family": "SUIT", + "style": "Regular" + }, + "fontSize": 16, + "height": 24, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -2 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "옵션 1", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:e4c7b7dacbd4e73f6c967cfc38e87b24df7d8775,", + "textTruncation": "DISABLED", + "visible": true, + "width": 37, + "x": 16, + "y": 12, + "styledTextSegments": [ + { + "characters": "옵션 1", + "end": 4, + "fillStyleId": "", + "fontWeight": 400, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:e4c7b7dacbd4e73f6c967cfc38e87b24df7d8775," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "45:2031": { + "id": "45:2031", + "type": "COMPONENT", + "fields": { + "parentId": "45:2034", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "45:2030" + ], + "absoluteBoundingBox": { + "height": 48, + "width": 540, + "x": 8802, + "y": 5095 + }, + "absoluteRenderBounds": { + "height": 48, + "width": 540, + "x": 8802, + "y": 5095 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "boundVariables": { + "fills": [ + { + "id": "VariableID:11:3", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:11:3", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.9839743375778198, + "g": 0.9189276099205016, + "r": 0.8625540137290955 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 48, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 10, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 12, + "paddingLeft": 16, + "paddingRight": 16, + "paddingTop": 12, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 10, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "속성 1=active", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 12, + "paddingLeft": 16, + "paddingRight": 16, + "paddingTop": 12, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "속성 1": "active" + }, + "visible": true, + "width": 540, + "x": 20, + "y": 172 + }, + "extra": {}, + "fieldErrors": {} + }, + "45:2032": { + "id": "45:2032", + "type": "COMPONENT", + "fields": { + "parentId": "45:2034", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "45:2028" + ], + "absoluteBoundingBox": { + "height": 48, + "width": 540, + "x": 8802, + "y": 5019 + }, + "absoluteRenderBounds": { + "height": 48, + "width": 540, + "x": 8802, + "y": 5019 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "boundVariables": { + "fills": [ + { + "id": "VariableID:11:2", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:11:2", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 1, + "g": 0.9715099334716796, + "r": 0.9487178325653076 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 48, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 10, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 12, + "paddingLeft": 16, + "paddingRight": 16, + "paddingTop": 12, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 10, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "속성 1=hover", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 12, + "paddingLeft": 16, + "paddingRight": 16, + "paddingTop": 12, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "속성 1": "hover" + }, + "visible": true, + "width": 540, + "x": 20, + "y": 96 + }, + "extra": {}, + "fieldErrors": {} + }, + "45:2033": { + "id": "45:2033", + "type": "COMPONENT", + "fields": { + "parentId": "45:2034", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "45:2025" + ], + "absoluteBoundingBox": { + "height": 48, + "width": 540, + "x": 8802, + "y": 4943 + }, + "absoluteRenderBounds": { + "height": 48, + "width": 540, + "x": 8802, + "y": 4943 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "boundVariables": { + "fills": [ + { + "id": "VariableID:13:161", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:13:161", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 48, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 10, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 12, + "paddingLeft": 16, + "paddingRight": 16, + "paddingTop": 12, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "FIXED" + }, + "itemSpacing": 10, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "속성 1=default", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 12, + "paddingLeft": 16, + "paddingRight": 16, + "paddingTop": 12, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "속성 1": "default" + }, + "visible": true, + "width": 540, + "x": 20, + "y": 20 + }, + "extra": {}, + "fieldErrors": {} + }, + "45:2034": { + "id": "45:2034", + "type": "COMPONENT_SET", + "fields": { + "parentId": "19:963", + "parentType": "SECTION", + "parentName": "Components", + "childrenIds": [ + "45:2033", + "45:2032", + "45:2031" + ], + "absoluteBoundingBox": { + "height": 240, + "width": 580, + "x": 8782, + "y": 4923 + }, + "absoluteRenderBounds": { + "height": 240, + "width": 580, + "x": 8782, + "y": 4923 + }, + "bottomLeftRadius": 5, + "bottomRightRadius": 5, + "clipsContent": true, + "componentPropertyDefinitions": { + "속성 1": { + "defaultValue": "default", + "type": "VARIANT", + "variantOptions": [ + "default", + "hover", + "active" + ] + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 5, + "dashPattern": [ + 10, + 5 + ], + "defaultVariant": { + "$nodeId": "45:2033", + "$nodeType": "COMPONENT" + }, + "height": 240, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "DropdownOption", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 0.27843138575553894, + "r": 0.5921568870544434 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "topLeftRadius": 5, + "topRightRadius": 5, + "visible": true, + "width": 580, + "x": 112, + "y": 3885 + }, + "extra": {}, + "fieldErrors": {} + } + }, + "diagnostics": [] +} diff --git a/crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_logo_set.json b/crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_logo_set.json new file mode 100644 index 00000000..3667226d --- /dev/null +++ b/crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_logo_set.json @@ -0,0 +1,3665 @@ +{ + "fileKey": "K2kUnxGgvooAJwCQiVTb7g", + "version": null, + "roots": [ + "136:19763" + ], + "nodes": { + "136:19763": { + "id": "136:19763", + "type": "COMPONENT_SET", + "fields": { + "parentId": "19:963", + "parentType": "SECTION", + "parentName": "Components", + "childrenIds": [ + "37:217", + "136:19764" + ], + "absoluteBoundingBox": { + "height": 416, + "width": 1142, + "x": 10847, + "y": 1294 + }, + "absoluteRenderBounds": { + "height": 416, + "width": 1142, + "x": 10847, + "y": 1294 + }, + "bottomLeftRadius": 5, + "bottomRightRadius": 5, + "clipsContent": true, + "componentPropertyDefinitions": { + "속성 1": { + "defaultValue": "기본", + "type": "VARIANT", + "variantOptions": [ + "기본", + "gray" + ] + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 5, + "dashPattern": [ + 10, + 5 + ], + "defaultVariant": { + "$nodeId": "37:217", + "$nodeType": "COMPONENT" + }, + "height": 416, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Logo", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 0.27843138575553894, + "r": 0.5921568870544434 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "topLeftRadius": 5, + "topRightRadius": 5, + "visible": true, + "width": 1142, + "x": 2177, + "y": 256 + }, + "extra": {}, + "fieldErrors": {} + }, + "136:19764": { + "id": "136:19764", + "type": "COMPONENT", + "fields": { + "parentId": "136:19763", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "136:19765", + "136:19766", + "136:19779" + ], + "absoluteBoundingBox": { + "height": 178, + "width": 1102, + "x": 10867, + "y": 1512 + }, + "absoluteRenderBounds": { + "height": 178, + "width": 1102, + "x": 10867, + "y": 1512 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "height": 178, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "속성 1=gray", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "속성 1": "gray" + }, + "visible": true, + "width": 1102, + "x": 20, + "y": 218 + }, + "extra": {}, + "fieldErrors": {} + }, + "136:19765": { + "id": "136:19765", + "type": "VECTOR", + "fields": { + "parentId": "136:19764", + "absoluteBoundingBox": { + "height": 178, + "width": 178, + "x": 10867, + "y": 1512 + }, + "absoluteRenderBounds": { + "height": 178, + "width": 178, + "x": 10867, + "y": 1512 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.7843137383460999, + "g": 0.7686274647712708, + "r": 0.7490196228027344 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.7647058963775635, + "g": 0.7490196228027344, + "r": 0.729411780834198 + }, + "position": 0.30000001192092896 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.729411780834198, + "g": 0.7137255072593689, + "r": 0.6941176652908325 + }, + "position": 0.44999998807907104 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.7019608020782471, + "g": 0.686274528503418, + "r": 0.6666666865348816 + }, + "position": 0.6100000143051147 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.686274528503418, + "g": 0.6705882549285889, + "r": 0.6509804129600525 + }, + "position": 0.7599999904632568 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.6823529601097107, + "g": 0.6666666865348816, + "r": 0.6470588445663452 + }, + "position": 0.9100000262260436 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.6823529601097107, + "g": 0.6666666865348816, + "r": 0.6470588445663452 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 0.6454866528511047, + 0, + 0.31524205207824707 + ], + [ + 0, + 0.6454866528511047, + 0.204462930560112 + ] + ], + "opacity": 1, + "type": "GRADIENT_RADIAL", + "visible": true + } + ], + "height": 178, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "sphere", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 178, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "136:19766": { + "id": "136:19766", + "type": "GROUP", + "fields": { + "parentId": "136:19764", + "childrenIds": [ + "136:19767", + "136:19769" + ], + "absoluteBoundingBox": { + "height": 178, + "width": 178, + "x": 10867, + "y": 1512 + }, + "absoluteRenderBounds": { + "height": 178, + "width": 178, + "x": 10867, + "y": 1512 + }, + "height": 178, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Clip path group", + "opacity": 1, + "visible": true, + "width": 178, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "136:19767": { + "id": "136:19767", + "type": "GROUP", + "fields": { + "parentId": "136:19766", + "childrenIds": [ + "136:19768" + ], + "absoluteBoundingBox": { + "height": 178, + "width": 178, + "x": 10867, + "y": 1512 + }, + "absoluteRenderBounds": null, + "height": 178, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "isMask": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "sphere-clip", + "opacity": 1, + "visible": true, + "width": 178, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "136:19768": { + "id": "136:19768", + "type": "VECTOR", + "fields": { + "parentId": "136:19767", + "absoluteBoundingBox": { + "height": 178, + "width": 178, + "x": 10867, + "y": 1512 + }, + "absoluteRenderBounds": { + "height": 178, + "width": 178, + "x": 10867, + "y": 1512 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0, + "g": 0, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 178, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 178, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "136:19769": { + "id": "136:19769", + "type": "GROUP", + "fields": { + "parentId": "136:19766", + "childrenIds": [ + "136:19770", + "136:19771", + "136:19772", + "136:19773", + "136:19774", + "136:19775", + "136:19776", + "136:19777", + "136:19778" + ], + "absoluteBoundingBox": { + "height": 178, + "width": 178, + "x": 10867, + "y": 1512 + }, + "absoluteRenderBounds": { + "height": 178, + "width": 178, + "x": 10867, + "y": 1512 + }, + "height": 178, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Group", + "opacity": 1, + "visible": true, + "width": 178, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "136:19770": { + "id": "136:19770", + "type": "VECTOR", + "fields": { + "parentId": "136:19769", + "absoluteBoundingBox": { + "height": 178, + "width": 178, + "x": 10867, + "y": 1512 + }, + "absoluteRenderBounds": { + "height": 178, + "width": 178, + "x": 10867, + "y": 1512 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 0.41999998688697815, + "b": 0.6705882549285889, + "g": 0.6549019813537598, + "r": 0.6352941393852234 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 0.20000000298023224, + "b": 0.6705882549285889, + "g": 0.6549019813537598, + "r": 0.6352941393852234 + }, + "position": 0.550000011920929 + }, + { + "boundVariables": {}, + "color": { + "a": 0, + "b": 0.6705882549285889, + "g": 0.6549019813537598, + "r": 0.6352941393852234 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 1.252997875213623, + 0, + 0.08783221244812012 + ], + [ + 0, + 1.252997875213623, + -0.7154324054718018 + ] + ], + "opacity": 1, + "type": "GRADIENT_RADIAL", + "visible": true + } + ], + "height": 178, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "shade-outside-orbit", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 178, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "136:19771": { + "id": "136:19771", + "type": "VECTOR", + "fields": { + "parentId": "136:19769", + "absoluteBoundingBox": { + "height": 178, + "width": 178, + "x": 10867, + "y": 1512 + }, + "absoluteRenderBounds": { + "height": 178, + "width": 178, + "x": 10867, + "y": 1512 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 0.14000000059604645, + "b": 0.8156862854957581, + "g": 0.800000011920929, + "r": 0.7803921699523926 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 0, + "b": 0.8156862854957581, + "g": 0.800000011920929, + "r": 0.7803921699523926 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 1.9364598989486697, + 0, + 0.468456894159317 + ], + [ + 0, + 1.9364598989486697, + -0.5502269268035889 + ] + ], + "opacity": 1, + "type": "GRADIENT_RADIAL", + "visible": true + } + ], + "height": 178, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "light-left-overlay", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 178, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "136:19772": { + "id": "136:19772", + "type": "VECTOR", + "fields": { + "parentId": "136:19769", + "absoluteBoundingBox": { + "height": 37.27400207519531, + "width": 72.59410095214844, + "x": 10963.125, + "y": 1638.8858642578125 + }, + "absoluteRenderBounds": { + "height": 37.2740478515625, + "width": 72.59375, + "x": 10963.125, + "y": 1638.8858642578125 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 0, + "b": 0.729411780834198, + "g": 0.7137255072593689, + "r": 0.6941176652908325 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 0.550000011920929, + "b": 0.729411780834198, + "g": 0.7137255072593689, + "r": 0.6941176652908325 + }, + "position": 0.30000001192092896 + }, + { + "boundVariables": {}, + "color": { + "a": 0.5, + "b": 0.729411780834198, + "g": 0.7137255072593689, + "r": 0.6941176652908325 + }, + "position": 0.75 + }, + { + "boundVariables": {}, + "color": { + "a": 0, + "b": 0.729411780834198, + "g": 0.7137255072593689, + "r": 0.6941176652908325 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 0.8151227831840515, + -0.2092042565345764, + 0.19614684581756592 + ], + [ + 0.4074420928955078, + 0.41853103041648865, + 0.08879981189966202 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 37.27400207519531, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "rim-highlight (Stroke)", + "opacity": 1, + "strokeAlign": "CENTER", + "strokeWeight": 3.849999904632568, + "visible": true, + "width": 72.59410095214844, + "x": 96.12515258789062, + "y": 126.88589477539062 + }, + "extra": {}, + "fieldErrors": {} + }, + "136:19773": { + "id": "136:19773", + "type": "VECTOR", + "fields": { + "parentId": "136:19769", + "absoluteBoundingBox": { + "height": 101.65419006347656, + "width": 101.57887268066406, + "x": 10887.373046875, + "y": 1548.818115234375 + }, + "absoluteRenderBounds": { + "height": 101.6541748046875, + "width": 101.5791015625, + "x": 10887.373046875, + "y": 1548.818115234375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.8784313797950745, + "g": 0.8627451062202454, + "r": 0.843137264251709 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.7803921699523926, + "g": 0.7647058963775635, + "r": 0.7450980544090271 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 0.8613659739494324, + 0.45280781388282776, + -0.1629999577999115 + ], + [ + -0.4524722993373871, + 0.8620046973228455, + 0.31514695286750793 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 101.65419006347656, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "core", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 101.57887268066406, + "x": 20.373069763183594, + "y": 36.818145751953125 + }, + "extra": {}, + "fieldErrors": {} + }, + "136:19774": { + "id": "136:19774", + "type": "VECTOR", + "fields": { + "parentId": "136:19769", + "absoluteBoundingBox": { + "height": 60.4782829284668, + "width": 60.47827911376953, + "x": 10905.2109375, + "y": 1569.554443359375 + }, + "absoluteRenderBounds": { + "height": 60.478271484375, + "width": 60.478515625, + "x": 10905.2109375, + "y": 1569.554443359375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9803921580314636, + "g": 0.9647058844566344, + "r": 0.945098042488098 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.8509804010391235, + "g": 0.8352941274642944, + "r": 0.8156862854957581 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 0.8080573678016663, + 0.5874120593070984, + -0.19792257249355316 + ], + [ + -0.5874120593070984, + 0.8080573678016663, + 0.3895654380321502 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 60.4782829284668, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "ring", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 60.47827911376953, + "x": 38.210899353027344, + "y": 57.55449295043945 + }, + "extra": {}, + "fieldErrors": {} + }, + "136:19775": { + "id": "136:19775", + "type": "VECTOR", + "fields": { + "parentId": "136:19769", + "absoluteBoundingBox": { + "height": 32.88132858276367, + "width": 32.88132858276367, + "x": 10918.421875, + "y": 1582.765380859375 + }, + "absoluteRenderBounds": { + "height": 32.88134765625, + "width": 32.880859375, + "x": 10918.421875, + "y": 1582.765380859375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.7529411911964417, + "g": 0.7372549176216125, + "r": 0.7176470756530762 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.729411780834198, + "g": 0.7137255072593689, + "r": 0.6941176652908325 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 0.9339293241500854, + 0.3536636531352997, + -0.14337609708309174 + ], + [ + -0.3536636531352997, + 0.9339293241500854, + 0.2099337726831436 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 32.88132858276367, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "ring-center", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 32.88132858276367, + "x": 51.421836853027344, + "y": 70.76543426513672 + }, + "extra": {}, + "fieldErrors": {} + }, + "136:19776": { + "id": "136:19776", + "type": "VECTOR", + "fields": { + "parentId": "136:19769", + "absoluteBoundingBox": { + "height": 145.2490692138672, + "width": 151.74325561523438, + "x": 10867.23046875, + "y": 1528.1748046875 + }, + "absoluteRenderBounds": { + "height": 145.2490234375, + "width": 151.7431640625, + "x": 10867.23046875, + "y": 1528.1748046875 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9960784316062928, + "g": 0.9803921580314636, + "r": 0.9607843160629272 + }, + "position": 0.1899999976158142 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9607843160629272, + "g": 0.945098042488098, + "r": 0.9254902005195618 + }, + "position": 0.3799999952316284 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9254902005195618, + "g": 0.9098039269447328, + "r": 0.8901960849761963 + }, + "position": 0.5 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.8823529481887817, + "g": 0.8666666746139526, + "r": 0.8470588326454163 + }, + "position": 0.6600000262260437 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.8509804010391235, + "g": 0.8352941274642944, + "r": 0.8156862854957581 + }, + "position": 0.8399999737739563 + } + ], + "gradientTransform": [ + [ + -1.6364001035690308, + 1.5055831670761108, + -0.08484271168708801 + ], + [ + -1.5728986263275146, + -1.5663667917251587, + 2.0555288791656494 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 145.2490692138672, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "orbit-outer (Stroke)", + "opacity": 1, + "strokeAlign": "CENTER", + "strokeWeight": 5.050000190734863, + "visible": true, + "width": 151.74325561523438, + "x": 0.23043431341648105, + "y": 16.174827575683594 + }, + "extra": {}, + "fieldErrors": {} + }, + "136:19777": { + "id": "136:19777", + "type": "VECTOR", + "fields": { + "parentId": "136:19769", + "absoluteBoundingBox": { + "height": 83.3648681640625, + "width": 151.09185791015625, + "x": 10886.0341796875, + "y": 1569.19287109375 + }, + "absoluteRenderBounds": { + "height": 83.3648681640625, + "width": 151.091796875, + "x": 10886.0341796875, + "y": 1569.19287109375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.9960784316062928, + "g": 0.9803921580314636, + "r": 0.9607843160629272 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 83.3648681640625, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "orbit-inner", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 151.09185791015625, + "x": 19.033720016479492, + "y": 57.192928314208984 + }, + "extra": {}, + "fieldErrors": {} + }, + "136:19778": { + "id": "136:19778", + "type": "VECTOR", + "fields": { + "parentId": "136:19769", + "absoluteBoundingBox": { + "height": 44.28451156616211, + "width": 98.18855285644533, + "x": 10887.1845703125, + "y": 1548.9384765625 + }, + "absoluteRenderBounds": { + "height": 44.2845458984375, + "width": 98.1884765625, + "x": 10887.1845703125, + "y": 1548.9384765625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9960784316062928, + "g": 0.9803921580314636, + "r": 0.9607843160629272 + }, + "position": 0.029999999329447743 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9647058844566344, + "g": 0.9490196108818054, + "r": 0.929411768913269 + }, + "position": 0.119999997317791 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9254902005195618, + "g": 0.9098039269447328, + "r": 0.8901960849761963 + }, + "position": 0.3499999940395355 + }, + { + "boundVariables": {}, + "color": { + "a": 0.8999999761581421, + "b": 0.8705882430076599, + "g": 0.8549019694328308, + "r": 0.8352941274642944 + }, + "position": 0.6200000047683716 + }, + { + "boundVariables": {}, + "color": { + "a": 0.6000000238418579, + "b": 0.843137264251709, + "g": 0.8274509906768799, + "r": 0.8078431487083435 + }, + "position": 0.8600000143051147 + }, + { + "boundVariables": {}, + "color": { + "a": 0, + "b": 0.8313725590705872, + "g": 0.8156862854957581, + "r": 0.7960784435272217 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 0.9895601272583008, + -0.06226864457130432, + 0.0533602200448513 + ], + [ + 0.13806335628032684, + 0.446306437253952, + 0.05430806800723076 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 44.28451156616211, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "orbit-inner-tail", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 98.18855285644533, + "x": 20.18492317199707, + "y": 36.938419342041016 + }, + "extra": {}, + "fieldErrors": {} + }, + "136:19779": { + "id": "136:19779", + "type": "GROUP", + "fields": { + "parentId": "136:19764", + "childrenIds": [ + "136:19780", + "136:19781", + "136:19782", + "136:19783", + "136:19784", + "136:19785", + "136:19786", + "136:19787", + "136:19788", + "136:19789" + ], + "absoluteBoundingBox": { + "height": 105.2799835205078, + "width": 875.4388427734375, + "x": 11093.16015625, + "y": 1547.1201171875 + }, + "absoluteRenderBounds": { + "height": 105.280029296875, + "width": 875.4388427734375, + "x": 11093.16015625, + "y": 1547.1201171875 + }, + "height": 105.2799835205078, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "ChondroGen", + "opacity": 1, + "visible": true, + "width": 875.4388427734375, + "x": 226.16015625, + "y": 35.1201171875 + }, + "extra": {}, + "fieldErrors": {} + }, + "136:19780": { + "id": "136:19780", + "type": "VECTOR", + "fields": { + "parentId": "136:19779", + "absoluteBoundingBox": { + "height": 75.60000610351562, + "width": 73.6400146484375, + "x": 11894.958984375, + "y": 1575.39990234375 + }, + "absoluteRenderBounds": { + "height": 75.5999755859375, + "width": 73.6396484375, + "x": 11894.958984375, + "y": 1575.39990234375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.7964744567871094, + "g": 0.7761158347129822, + "r": 0.7543532252311707 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 75.60000610351562, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 73.6400146484375, + "x": 1027.95947265625, + "y": 63.39990234375 + }, + "extra": {}, + "fieldErrors": {} + }, + "136:19781": { + "id": "136:19781", + "type": "VECTOR", + "fields": { + "parentId": "136:19779", + "absoluteBoundingBox": { + "height": 76.58000183105469, + "width": 76.44000244140625, + "x": 11804.509765625, + "y": 1575.39990234375 + }, + "absoluteRenderBounds": { + "height": 76.5799560546875, + "width": 76.4404296875, + "x": 11804.509765625, + "y": 1575.39990234375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.7964744567871094, + "g": 0.7761158347129822, + "r": 0.7543532252311707 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 76.58000183105469, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 76.44000244140625, + "x": 937.509765625, + "y": 63.39990234375 + }, + "extra": {}, + "fieldErrors": {} + }, + "136:19782": { + "id": "136:19782", + "type": "VECTOR", + "fields": { + "parentId": "136:19779", + "absoluteBoundingBox": { + "height": 100.8000030517578, + "width": 92.4000244140625, + "x": 11700.005859375, + "y": 1551.60009765625 + }, + "absoluteRenderBounds": { + "height": 100.800048828125, + "width": 92.400390625, + "x": 11700.005859375, + "y": 1551.60009765625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.7964744567871094, + "g": 0.7761158347129822, + "r": 0.7543532252311707 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 100.8000030517578, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 92.4000244140625, + "x": 833.00537109375, + "y": 39.60009765625 + }, + "extra": {}, + "fieldErrors": {} + }, + "136:19783": { + "id": "136:19783", + "type": "VECTOR", + "fields": { + "parentId": "136:19779", + "absoluteBoundingBox": { + "height": 76.58000183105469, + "width": 79.65997314453125, + "x": 11611.73046875, + "y": 1575.39990234375 + }, + "absoluteRenderBounds": { + "height": 76.5799560546875, + "width": 79.66015625, + "x": 11611.73046875, + "y": 1575.39990234375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.7291667461395264, + "g": 0.7133524417877197, + "r": 0.6952789425849915 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 76.58000183105469, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 79.65997314453125, + "x": 744.73095703125, + "y": 63.39990234375 + }, + "extra": {}, + "fieldErrors": {} + }, + "136:19784": { + "id": "136:19784", + "type": "VECTOR", + "fields": { + "parentId": "136:19779", + "absoluteBoundingBox": { + "height": 75.60000610351562, + "width": 43.259979248046875, + "x": 11563.26953125, + "y": 1575.39990234375 + }, + "absoluteRenderBounds": { + "height": 75.5999755859375, + "width": 43.259765625, + "x": 11563.26953125, + "y": 1575.39990234375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.7291667461395264, + "g": 0.7133524417877197, + "r": 0.6952789425849915 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 75.60000610351562, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 43.259979248046875, + "x": 696.26904296875, + "y": 63.39990234375 + }, + "extra": {}, + "fieldErrors": {} + }, + "136:19785": { + "id": "136:19785", + "type": "VECTOR", + "fields": { + "parentId": "136:19779", + "absoluteBoundingBox": { + "height": 104.86000061035156, + "width": 79.239990234375, + "x": 11463.7958984375, + "y": 1547.1201171875 + }, + "absoluteRenderBounds": { + "height": 104.8599853515625, + "width": 79.240234375, + "x": 11463.7958984375, + "y": 1547.1201171875 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.7291667461395264, + "g": 0.7133524417877197, + "r": 0.6952789425849915 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 104.86000061035156, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 79.239990234375, + "x": 596.7958984375, + "y": 35.1201171875 + }, + "extra": {}, + "fieldErrors": {} + }, + "136:19786": { + "id": "136:19786", + "type": "VECTOR", + "fields": { + "parentId": "136:19779", + "absoluteBoundingBox": { + "height": 75.60000610351562, + "width": 73.63998413085938, + "x": 11376.779296875, + "y": 1575.39990234375 + }, + "absoluteRenderBounds": { + "height": 75.5999755859375, + "width": 73.6396484375, + "x": 11376.779296875, + "y": 1575.39990234375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.7291667461395264, + "g": 0.7133524417877197, + "r": 0.6952789425849915 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 75.60000610351562, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 73.63998413085938, + "x": 509.779296875, + "y": 63.39990234375 + }, + "extra": {}, + "fieldErrors": {} + }, + "136:19787": { + "id": "136:19787", + "type": "VECTOR", + "fields": { + "parentId": "136:19779", + "absoluteBoundingBox": { + "height": 76.58000183105469, + "width": 79.66000366210938, + "x": 11283.185546875, + "y": 1575.39990234375 + }, + "absoluteRenderBounds": { + "height": 76.5799560546875, + "width": 79.66015625, + "x": 11283.185546875, + "y": 1575.39990234375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.7291667461395264, + "g": 0.7133524417877197, + "r": 0.6952789425849915 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 76.58000183105469, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 79.66000366210938, + "x": 416.18505859375, + "y": 63.39990234375 + }, + "extra": {}, + "fieldErrors": {} + }, + "136:19788": { + "id": "136:19788", + "type": "VECTOR", + "fields": { + "parentId": "136:19779", + "absoluteBoundingBox": { + "height": 103.8800048828125, + "width": 73.64000701904297, + "x": 11196.16796875, + "y": 1547.1201171875 + }, + "absoluteRenderBounds": { + "height": 103.8800048828125, + "width": 73.6396484375, + "x": 11196.16796875, + "y": 1547.1201171875 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.7291667461395264, + "g": 0.7133524417877197, + "r": 0.6952789425849915 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 103.8800048828125, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 73.64000701904297, + "x": 329.16845703125, + "y": 35.1201171875 + }, + "extra": {}, + "fieldErrors": {} + }, + "136:19789": { + "id": "136:19789", + "type": "VECTOR", + "fields": { + "parentId": "136:19779", + "absoluteBoundingBox": { + "height": 100.8000030517578, + "width": 91.27999877929688, + "x": 11093.16015625, + "y": 1551.60009765625 + }, + "absoluteRenderBounds": { + "height": 100.800048828125, + "width": 91.2802734375, + "x": 11093.16015625, + "y": 1551.60009765625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.7291667461395264, + "g": 0.7133524417877197, + "r": 0.6952789425849915 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 100.8000030517578, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 91.27999877929688, + "x": 226.16015625, + "y": 39.60009765625 + }, + "extra": {}, + "fieldErrors": {} + }, + "136:9027": { + "id": "136:9027", + "type": "VECTOR", + "fields": { + "parentId": "37:217", + "absoluteBoundingBox": { + "height": 178, + "width": 178, + "x": 10867, + "y": 1314 + }, + "absoluteRenderBounds": { + "height": 178, + "width": 178, + "x": 10867, + "y": 1314 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.7490196228027344, + "g": 0.5607843399047852, + "r": 0.03921568766236305 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.6823529601097107, + "g": 0.5058823823928833, + "r": 0.007843137718737125 + }, + "position": 0.30000001192092896 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.5607843399047852, + "g": 0.3764705955982208, + "r": 0.003921568859368563 + }, + "position": 0.44999998807907104 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.46666666865348816, + "g": 0.2823529541492462, + "r": 0 + }, + "position": 0.6100000143051147 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.40784314274787903, + "g": 0.22745098173618317, + "r": 0 + }, + "position": 0.7599999904632568 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.3843137323856354, + "g": 0.2078431397676468, + "r": 0.003921568859368563 + }, + "position": 0.9100000262260436 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.3843137323856354, + "g": 0.2117647081613541, + "r": 0.003921568859368563 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 0.6454866528511047, + 0, + 0.31524205207824707 + ], + [ + 0, + 0.6454866528511047, + 0.204462930560112 + ] + ], + "opacity": 1, + "type": "GRADIENT_RADIAL", + "visible": true + } + ], + "height": 178, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "sphere", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 178, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "136:9028": { + "id": "136:9028", + "type": "GROUP", + "fields": { + "parentId": "37:217", + "childrenIds": [ + "136:9029", + "136:9031" + ], + "absoluteBoundingBox": { + "height": 178, + "width": 178, + "x": 10867, + "y": 1314 + }, + "absoluteRenderBounds": { + "height": 178, + "width": 178, + "x": 10867, + "y": 1314 + }, + "height": 178, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Clip path group", + "opacity": 1, + "visible": true, + "width": 178, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "136:9029": { + "id": "136:9029", + "type": "GROUP", + "fields": { + "parentId": "136:9028", + "childrenIds": [ + "136:9030" + ], + "absoluteBoundingBox": { + "height": 178, + "width": 178, + "x": 10867, + "y": 1314 + }, + "absoluteRenderBounds": null, + "height": 178, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 0, + "layoutAlign": "MIN", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "FIXED" + }, + "isMask": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "sphere-clip", + "opacity": 1, + "visible": true, + "width": 178, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "136:9030": { + "id": "136:9030", + "type": "VECTOR", + "fields": { + "parentId": "136:9029", + "absoluteBoundingBox": { + "height": 178, + "width": 178, + "x": 10867, + "y": 1314 + }, + "absoluteRenderBounds": { + "height": 178, + "width": 178, + "x": 10867, + "y": 1314 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0, + "g": 0, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 178, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 178, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "136:9031": { + "id": "136:9031", + "type": "GROUP", + "fields": { + "parentId": "136:9028", + "childrenIds": [ + "136:9032", + "136:9033", + "136:9034", + "136:9035", + "136:9036", + "136:9037", + "136:9038", + "136:9039", + "136:9040" + ], + "absoluteBoundingBox": { + "height": 178, + "width": 178, + "x": 10867, + "y": 1314 + }, + "absoluteRenderBounds": { + "height": 178, + "width": 178, + "x": 10867, + "y": 1314 + }, + "height": 178, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Group", + "opacity": 1, + "visible": true, + "width": 178, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "136:9032": { + "id": "136:9032", + "type": "VECTOR", + "fields": { + "parentId": "136:9031", + "absoluteBoundingBox": { + "height": 178, + "width": 178, + "x": 10867, + "y": 1314 + }, + "absoluteRenderBounds": { + "height": 178, + "width": 178, + "x": 10867, + "y": 1314 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 0.41999998688697815, + "b": 0.3333333432674408, + "g": 0.16470588743686676, + "r": 0 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 0.20000000298023224, + "b": 0.3333333432674408, + "g": 0.16470588743686676, + "r": 0 + }, + "position": 0.550000011920929 + }, + { + "boundVariables": {}, + "color": { + "a": 0, + "b": 0.3333333432674408, + "g": 0.16470588743686676, + "r": 0 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 1.252997875213623, + 0, + 0.08783221244812012 + ], + [ + 0, + 1.252997875213623, + -0.7154324054718018 + ] + ], + "opacity": 1, + "type": "GRADIENT_RADIAL", + "visible": true + } + ], + "height": 178, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "shade-outside-orbit", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 178, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "136:9033": { + "id": "136:9033", + "type": "VECTOR", + "fields": { + "parentId": "136:9031", + "absoluteBoundingBox": { + "height": 178, + "width": 178, + "x": 10867, + "y": 1314 + }, + "absoluteRenderBounds": { + "height": 178, + "width": 178, + "x": 10867, + "y": 1314 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 0.14000000059604645, + "b": 0.8470588326454163, + "g": 0.6509804129600525, + "r": 0.10588235408067705 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 0, + "b": 0.8470588326454163, + "g": 0.6509804129600525, + "r": 0.10588235408067705 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 1.9364598989486697, + 0, + 0.468456894159317 + ], + [ + 0, + 1.9364598989486697, + -0.5502269268035889 + ] + ], + "opacity": 1, + "type": "GRADIENT_RADIAL", + "visible": true + } + ], + "height": 178, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "light-left-overlay", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 178, + "x": 0, + "y": 0 + }, + "extra": {}, + "fieldErrors": {} + }, + "136:9034": { + "id": "136:9034", + "type": "VECTOR", + "fields": { + "parentId": "136:9031", + "absoluteBoundingBox": { + "height": 37.27400207519531, + "width": 72.59410095214844, + "x": 10963.125, + "y": 1440.8858642578125 + }, + "absoluteRenderBounds": { + "height": 37.2740478515625, + "width": 72.59375, + "x": 10963.125, + "y": 1440.8858642578125 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 0, + "b": 0.5764706134796143, + "g": 0.3529411852359772, + "r": 0.04313725605607033 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 0.550000011920929, + "b": 0.5764706134796143, + "g": 0.3529411852359772, + "r": 0.04313725605607033 + }, + "position": 0.30000001192092896 + }, + { + "boundVariables": {}, + "color": { + "a": 0.5, + "b": 0.5764706134796143, + "g": 0.3529411852359772, + "r": 0.04313725605607033 + }, + "position": 0.75 + }, + { + "boundVariables": {}, + "color": { + "a": 0, + "b": 0.5764706134796143, + "g": 0.3529411852359772, + "r": 0.04313725605607033 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 0.8151227831840515, + -0.2092042565345764, + 0.19614684581756592 + ], + [ + 0.4074420928955078, + 0.41853103041648865, + 0.08879981189966202 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 37.27400207519531, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "rim-highlight (Stroke)", + "opacity": 1, + "strokeAlign": "CENTER", + "strokeWeight": 3.849999904632568, + "visible": true, + "width": 72.59410095214844, + "x": 96.12515258789062, + "y": 126.88589477539062 + }, + "extra": {}, + "fieldErrors": {} + }, + "136:9035": { + "id": "136:9035", + "type": "VECTOR", + "fields": { + "parentId": "136:9031", + "absoluteBoundingBox": { + "height": 101.65419006347656, + "width": 101.57887268066406, + "x": 10887.373046875, + "y": 1350.818115234375 + }, + "absoluteRenderBounds": { + "height": 101.6541748046875, + "width": 101.5791015625, + "x": 10887.373046875, + "y": 1350.818115234375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.8156862854957581, + "g": 0.7843137383460999, + "r": 0.03921568766236305 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.5882353186607361, + "g": 0.4313725531101227, + "r": 0 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 0.8613659739494324, + 0.45280781388282776, + -0.1629999577999115 + ], + [ + -0.4524722993373871, + 0.8620046973228455, + 0.31514695286750793 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 101.65419006347656, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "core", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 101.57887268066406, + "x": 20.373069763183594, + "y": 36.818145751953125 + }, + "extra": {}, + "fieldErrors": {} + }, + "136:9036": { + "id": "136:9036", + "type": "VECTOR", + "fields": { + "parentId": "136:9031", + "absoluteBoundingBox": { + "height": 60.4782829284668, + "width": 60.47827911376953, + "x": 10905.2109375, + "y": 1371.554443359375 + }, + "absoluteRenderBounds": { + "height": 60.478271484375, + "width": 60.478515625, + "x": 10905.2109375, + "y": 1371.554443359375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.8745098114013672, + "g": 0.9333333373069764, + "r": 0.6705882549285889 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.7607843279838562, + "g": 0.7215686440467834, + "r": 0.0235294122248888 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 0.8080573678016663, + 0.5874120593070984, + -0.19792257249355316 + ], + [ + -0.5874120593070984, + 0.8080573678016663, + 0.3895654380321502 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 60.4782829284668, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "ring", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 60.47827911376953, + "x": 38.210899353027344, + "y": 57.55449295043945 + }, + "extra": {}, + "fieldErrors": {} + }, + "136:9037": { + "id": "136:9037", + "type": "VECTOR", + "fields": { + "parentId": "136:9031", + "absoluteBoundingBox": { + "height": 32.88132858276367, + "width": 32.88132858276367, + "x": 10918.421875, + "y": 1384.765380859375 + }, + "absoluteRenderBounds": { + "height": 32.88134765625, + "width": 32.880859375, + "x": 10918.421875, + "y": 1384.765380859375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.6196078658103943, + "g": 0.4745098054409027, + "r": 0.007843137718737125 + }, + "position": 0 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.545098066329956, + "g": 0.3764705955982208, + "r": 0.003921568859368563 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 0.9339293241500854, + 0.3536636531352997, + -0.14337609708309174 + ], + [ + -0.3536636531352997, + 0.9339293241500854, + 0.2099337726831436 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 32.88132858276367, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "ring-center", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 32.88132858276367, + "x": 51.421836853027344, + "y": 70.76543426513672 + }, + "extra": {}, + "fieldErrors": {} + }, + "136:9038": { + "id": "136:9038", + "type": "VECTOR", + "fields": { + "parentId": "136:9031", + "absoluteBoundingBox": { + "height": 145.2490692138672, + "width": 151.74325561523438, + "x": 10867.23046875, + "y": 1330.1748046875 + }, + "absoluteRenderBounds": { + "height": 145.2490234375, + "width": 151.7431640625, + "x": 10867.23046875, + "y": 1330.1748046875 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9843137264251708, + "g": 0.9764705896377563, + "r": 0.9607843160629272 + }, + "position": 0.1899999976158142 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9803921580314636, + "g": 0.91372549533844, + "r": 0.7764706015586853 + }, + "position": 0.3799999952316284 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9725490212440492, + "g": 0.8588235378265381, + "r": 0.6039215922355652 + }, + "position": 0.5 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.95686274766922, + "g": 0.7843137383460999, + "r": 0.3843137323856354 + }, + "position": 0.6600000262260437 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9333333373069764, + "g": 0.729411780834198, + "r": 0.21960784494876864 + }, + "position": 0.8399999737739563 + } + ], + "gradientTransform": [ + [ + -1.6364001035690308, + 1.5055831670761108, + -0.08484271168708801 + ], + [ + -1.5728986263275146, + -1.5663667917251587, + 2.0555288791656494 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 145.2490692138672, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "orbit-outer (Stroke)", + "opacity": 1, + "strokeAlign": "CENTER", + "strokeWeight": 5.050000190734863, + "visible": true, + "width": 151.74325561523438, + "x": 0.23043431341648105, + "y": 16.174827575683594 + }, + "extra": {}, + "fieldErrors": {} + }, + "136:9039": { + "id": "136:9039", + "type": "VECTOR", + "fields": { + "parentId": "136:9031", + "absoluteBoundingBox": { + "height": 83.3648681640625, + "width": 151.09185791015625, + "x": 10886.0341796875, + "y": 1371.19287109375 + }, + "absoluteRenderBounds": { + "height": 83.3648681640625, + "width": 151.091796875, + "x": 10886.0341796875, + "y": 1371.19287109375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.9843137264251708, + "g": 0.9803921580314636, + "r": 0.9647058844566344 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 83.3648681640625, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "orbit-inner", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 151.09185791015625, + "x": 19.033720016479492, + "y": 57.192928314208984 + }, + "extra": {}, + "fieldErrors": {} + }, + "136:9040": { + "id": "136:9040", + "type": "VECTOR", + "fields": { + "parentId": "136:9031", + "absoluteBoundingBox": { + "height": 44.28451156616211, + "width": 98.18855285644533, + "x": 10887.1845703125, + "y": 1350.9384765625 + }, + "absoluteRenderBounds": { + "height": 44.2845458984375, + "width": 98.1884765625, + "x": 10887.1845703125, + "y": 1350.9384765625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "gradientStops": [ + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9843137264251708, + "g": 0.9803921580314636, + "r": 0.9647058844566344 + }, + "position": 0.029999999329447743 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.9176470637321472, + "g": 0.9529411792755128, + "r": 0.7686274647712708 + }, + "position": 0.119999997317791 + }, + { + "boundVariables": {}, + "color": { + "a": 1, + "b": 0.8549019694328308, + "g": 0.8941176533699036, + "r": 0.5607843399047852 + }, + "position": 0.3499999940395355 + }, + { + "boundVariables": {}, + "color": { + "a": 0.8999999761581421, + "b": 0.8274509906768799, + "g": 0.8156862854957581, + "r": 0.2705882489681244 + }, + "position": 0.6200000047683716 + }, + { + "boundVariables": {}, + "color": { + "a": 0.6000000238418579, + "b": 0.8039215803146362, + "g": 0.7607843279838562, + "r": 0.14901961386203766 + }, + "position": 0.8600000143051147 + }, + { + "boundVariables": {}, + "color": { + "a": 0, + "b": 0.772549033164978, + "g": 0.7215686440467834, + "r": 0.11372549086809158 + }, + "position": 1 + } + ], + "gradientTransform": [ + [ + 0.9895601272583008, + -0.06226864457130432, + 0.0533602200448513 + ], + [ + 0.13806335628032684, + 0.446306437253952, + 0.05430806800723076 + ] + ], + "opacity": 1, + "type": "GRADIENT_LINEAR", + "visible": true + } + ], + "height": 44.28451156616211, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "orbit-inner-tail", + "opacity": 1, + "strokeWeight": 1, + "visible": true, + "width": 98.18855285644533, + "x": 20.18492317199707, + "y": 36.938419342041016 + }, + "extra": {}, + "fieldErrors": {} + }, + "136:9041": { + "id": "136:9041", + "type": "GROUP", + "fields": { + "parentId": "37:217", + "childrenIds": [ + "136:9042", + "136:9043", + "136:9044", + "136:9045", + "136:9046", + "136:9047", + "136:9048", + "136:9049", + "136:9050", + "136:9051" + ], + "absoluteBoundingBox": { + "height": 105.2799835205078, + "width": 875.4388427734375, + "x": 11093.16015625, + "y": 1349.1201171875 + }, + "absoluteRenderBounds": { + "height": 105.280029296875, + "width": 875.4388427734375, + "x": 11093.16015625, + "y": 1349.1201171875 + }, + "height": 105.2799835205078, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "ChondroGen", + "opacity": 1, + "visible": true, + "width": 875.4388427734375, + "x": 226.16015625, + "y": 35.1201171875 + }, + "extra": {}, + "fieldErrors": {} + }, + "136:9042": { + "id": "136:9042", + "type": "VECTOR", + "fields": { + "parentId": "136:9041", + "absoluteBoundingBox": { + "height": 75.60000610351562, + "width": 73.6400146484375, + "x": 11894.958984375, + "y": 1377.39990234375 + }, + "absoluteRenderBounds": { + "height": 75.5999755859375, + "width": 73.6396484375, + "x": 11894.958984375, + "y": 1377.39990234375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.6980392336845398, + "g": 0.6117647290229797, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 75.60000610351562, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 73.6400146484375, + "x": 1027.95947265625, + "y": 63.39990234375 + }, + "extra": {}, + "fieldErrors": {} + }, + "136:9043": { + "id": "136:9043", + "type": "VECTOR", + "fields": { + "parentId": "136:9041", + "absoluteBoundingBox": { + "height": 76.58000183105469, + "width": 76.44000244140625, + "x": 11804.509765625, + "y": 1377.39990234375 + }, + "absoluteRenderBounds": { + "height": 76.5799560546875, + "width": 76.4404296875, + "x": 11804.509765625, + "y": 1377.39990234375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.6980392336845398, + "g": 0.6117647290229797, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 76.58000183105469, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 76.44000244140625, + "x": 937.509765625, + "y": 63.39990234375 + }, + "extra": {}, + "fieldErrors": {} + }, + "136:9044": { + "id": "136:9044", + "type": "VECTOR", + "fields": { + "parentId": "136:9041", + "absoluteBoundingBox": { + "height": 100.8000030517578, + "width": 92.4000244140625, + "x": 11700.005859375, + "y": 1353.60009765625 + }, + "absoluteRenderBounds": { + "height": 100.800048828125, + "width": 92.400390625, + "x": 11700.005859375, + "y": 1353.60009765625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.6980392336845398, + "g": 0.6117647290229797, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 100.8000030517578, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 92.4000244140625, + "x": 833.00537109375, + "y": 39.60009765625 + }, + "extra": {}, + "fieldErrors": {} + }, + "136:9045": { + "id": "136:9045", + "type": "VECTOR", + "fields": { + "parentId": "136:9041", + "absoluteBoundingBox": { + "height": 76.58000183105469, + "width": 79.65997314453125, + "x": 11611.73046875, + "y": 1377.39990234375 + }, + "absoluteRenderBounds": { + "height": 76.5799560546875, + "width": 79.66015625, + "x": 11611.73046875, + "y": 1377.39990234375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.3803921639919281, + "g": 0.2078431397676468, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 76.58000183105469, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 79.65997314453125, + "x": 744.73095703125, + "y": 63.39990234375 + }, + "extra": {}, + "fieldErrors": {} + }, + "136:9046": { + "id": "136:9046", + "type": "VECTOR", + "fields": { + "parentId": "136:9041", + "absoluteBoundingBox": { + "height": 75.60000610351562, + "width": 43.259979248046875, + "x": 11563.26953125, + "y": 1377.39990234375 + }, + "absoluteRenderBounds": { + "height": 75.5999755859375, + "width": 43.259765625, + "x": 11563.26953125, + "y": 1377.39990234375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.3803921639919281, + "g": 0.2078431397676468, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 75.60000610351562, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 43.259979248046875, + "x": 696.26904296875, + "y": 63.39990234375 + }, + "extra": {}, + "fieldErrors": {} + }, + "136:9047": { + "id": "136:9047", + "type": "VECTOR", + "fields": { + "parentId": "136:9041", + "absoluteBoundingBox": { + "height": 104.86000061035156, + "width": 79.239990234375, + "x": 11463.7958984375, + "y": 1349.1201171875 + }, + "absoluteRenderBounds": { + "height": 104.8599853515625, + "width": 79.240234375, + "x": 11463.7958984375, + "y": 1349.1201171875 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.3803921639919281, + "g": 0.2078431397676468, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 104.86000061035156, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 79.239990234375, + "x": 596.7958984375, + "y": 35.1201171875 + }, + "extra": {}, + "fieldErrors": {} + }, + "136:9048": { + "id": "136:9048", + "type": "VECTOR", + "fields": { + "parentId": "136:9041", + "absoluteBoundingBox": { + "height": 75.60000610351562, + "width": 73.63998413085938, + "x": 11376.779296875, + "y": 1377.39990234375 + }, + "absoluteRenderBounds": { + "height": 75.5999755859375, + "width": 73.6396484375, + "x": 11376.779296875, + "y": 1377.39990234375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.3803921639919281, + "g": 0.2078431397676468, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 75.60000610351562, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 73.63998413085938, + "x": 509.779296875, + "y": 63.39990234375 + }, + "extra": {}, + "fieldErrors": {} + }, + "136:9049": { + "id": "136:9049", + "type": "VECTOR", + "fields": { + "parentId": "136:9041", + "absoluteBoundingBox": { + "height": 76.58000183105469, + "width": 79.66000366210938, + "x": 11283.185546875, + "y": 1377.39990234375 + }, + "absoluteRenderBounds": { + "height": 76.5799560546875, + "width": 79.66015625, + "x": 11283.185546875, + "y": 1377.39990234375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.3803921639919281, + "g": 0.2078431397676468, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 76.58000183105469, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 79.66000366210938, + "x": 416.18505859375, + "y": 63.39990234375 + }, + "extra": {}, + "fieldErrors": {} + }, + "136:9050": { + "id": "136:9050", + "type": "VECTOR", + "fields": { + "parentId": "136:9041", + "absoluteBoundingBox": { + "height": 103.8800048828125, + "width": 73.64000701904297, + "x": 11196.16796875, + "y": 1349.1201171875 + }, + "absoluteRenderBounds": { + "height": 103.8800048828125, + "width": 73.6396484375, + "x": 11196.16796875, + "y": 1349.1201171875 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.3803921639919281, + "g": 0.2078431397676468, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 103.8800048828125, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 73.64000701904297, + "x": 329.16845703125, + "y": 35.1201171875 + }, + "extra": {}, + "fieldErrors": {} + }, + "136:9051": { + "id": "136:9051", + "type": "VECTOR", + "fields": { + "parentId": "136:9041", + "absoluteBoundingBox": { + "height": 100.8000030517578, + "width": 91.27999877929688, + "x": 11093.16015625, + "y": 1353.60009765625 + }, + "absoluteRenderBounds": { + "height": 100.800048828125, + "width": 91.2802734375, + "x": 11093.16015625, + "y": 1353.60009765625 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 0.3803921639919281, + "g": 0.2078431397676468, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "height": 100.8000030517578, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "visible": true, + "width": 91.27999877929688, + "x": 226.16015625, + "y": 39.60009765625 + }, + "extra": {}, + "fieldErrors": {} + }, + "37:217": { + "id": "37:217", + "type": "COMPONENT", + "fields": { + "parentId": "136:19763", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "136:9027", + "136:9028", + "136:9041" + ], + "absoluteBoundingBox": { + "height": 178, + "width": 1102, + "x": 10867, + "y": 1314 + }, + "absoluteRenderBounds": { + "height": 178, + "width": 1102, + "x": 10867, + "y": 1314 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "height": 178, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "속성 1=기본", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 0, + "topRightRadius": 0, + "variantProperties": { + "속성 1": "기본" + }, + "visible": true, + "width": 1102, + "x": 20, + "y": 20 + }, + "extra": {}, + "fieldErrors": {} + } + }, + "diagnostics": [] +} From 0dc21e6ba83912c784cb627e9ee9644086364fa3 Mon Sep 17 00:00:00 2001 From: leader Date: Sat, 24 Oct 2026 12:19:24 +0900 Subject: [PATCH 129/149] fix(ui_validate): accept devup-ui props on primitives, resolve as/props href, and treat controls inside links as wired --- .../style_props/devup_primitive_elements.rs | 1 + .../src/ui_validate/wiring.rs | 15 ++++++++-- .../src/server/feedback_interactions_tests.rs | 15 ++++++++++ crates/devup-mcp/src/server/navigation.rs | 30 ++++++++++++++++++- 4 files changed, 58 insertions(+), 3 deletions(-) diff --git a/crates/devup-mcp-devup-ui/src/style_props/devup_primitive_elements.rs b/crates/devup-mcp-devup-ui/src/style_props/devup_primitive_elements.rs index af4732b8..d81b85f7 100644 --- a/crates/devup-mcp-devup-ui/src/style_props/devup_primitive_elements.rs +++ b/crates/devup-mcp-devup-ui/src/style_props/devup_primitive_elements.rs @@ -15,6 +15,7 @@ pub const DEVUP_PRIMITIVE_ELEMENTS: &[&str] = &["Box", "Flex", "Text", "Center", /// `is_known_non_style_prop`. const DEVUP_NON_STYLE_PROPS: &[&str] = &[ "as", + "props", "selectors", "children", "className", diff --git a/crates/devup-mcp-devup-ui/src/ui_validate/wiring.rs b/crates/devup-mcp-devup-ui/src/ui_validate/wiring.rs index 1cf07eec..285d1a7a 100644 --- a/crates/devup-mcp-devup-ui/src/ui_validate/wiring.rs +++ b/crates/devup-mcp-devup-ui/src/ui_validate/wiring.rs @@ -30,7 +30,7 @@ pub fn findings(source: &str) -> Vec { found.push(error(comment.span, "deferred-wiring", "Deferred implementation comment leaves application wiring unfinished.")); } } - let mut collector = Collector { source, found, functions: std::collections::BTreeMap::new(), forms: Vec::new(), submit_depth: 0 }; + let mut collector = Collector { source, found, functions: std::collections::BTreeMap::new(), forms: Vec::new(), submit_depth: 0, link_depth: 0 }; collector.visit_program(&parsed.program); for (span, handler) in &collector.forms { let body = collector.functions.get(handler).map(String::as_str).unwrap_or(handler); @@ -93,6 +93,7 @@ struct Collector<'s> { functions: std::collections::BTreeMap, forms: Vec<(Span, String)>, submit_depth: usize, + link_depth: usize, } fn attr_name<'a>(item: &'a JSXAttributeItem<'a>) -> Option<&'a str> { @@ -153,7 +154,8 @@ impl<'a> Visit<'a> for Collector<'_> { }); self.forms.push((opening.span, text)); } - if !spread && !has("disabled") && !has("readOnly") { + let links = matches!(effective.as_str(), "a" | "Link") || has("href"); + if !spread && !has("disabled") && !has("readOnly") && self.link_depth == 0 { let input = matches!(effective.as_str(), "input" | "Input" | "select" | "Select" | "textarea" | "Textarea"); let button = matches!(effective.as_str(), "button" | "Button"); let submit = attribute("type").and_then(value_text).is_none_or(|kind| kind == "submit") && self.submit_depth > 0; @@ -164,7 +166,9 @@ impl<'a> Visit<'a> for Collector<'_> { } } self.submit_depth += usize::from(submits); + self.link_depth += usize::from(links); walk::walk_jsx_element(self, node); + self.link_depth -= usize::from(links); self.submit_depth -= usize::from(submits); } } @@ -182,6 +186,13 @@ const view = <>;"#; + assert_eq!(count(source, "link-underline"), 4); +} + +#[test] +fn link_underline_accepts_explicit_decoration_but_not_hover_only() { + let source = r#"const View = () => <>A + B + C;"#; + assert_eq!(count(source, "link-underline"), 1); +} + +#[test] +fn link_underline_accepts_a_bundle_global_reset() { + let files = [ + SourceFile { path: "src/Reset.ts".into(), content: "import { globalCss as reset } from '@devup-ui/react'; reset({ a: { textDecoration: 'none', color: 'inherit' } });".into() }, + SourceFile { path: "src/components/View.tsx".into(), content: "export const View = () => Go;".into() }, + ]; + let report = validate_bundle(&files, None, false).unwrap(); + assert!(!report.violations.iter().any(|finding| finding.rule == "link-underline")); +} + +#[test] +fn link_underline_does_not_accept_unrelated_or_hover_global_styles() { + for reset in [ + "const styles = { a: { textDecoration: 'none' } };", + "globalCss({ button: { textDecoration: 'none' } });", + "globalCss({ 'a:hover': { textDecoration: 'none' } });", + "globalCss({ a: { _hover: { textDecoration: 'none' } } });", + "// globalCss({ a: { textDecoration: 'none' } });", + ] { + assert_eq!(count(&format!("{reset}\nconst View = () => Go;"), "link-underline"), 1); + } +} + +#[test] +fn marquee_static_reports_repeated_single_line_text_without_motion() { + let source = "const View = () => Research ✦ Repair ✦ Renew ✦ Research;"; + let report = validate_devup_ui_tsx(source, None, false); + let finding = report.violations.iter().find(|finding| finding.rule == "marquee-static").unwrap(); + assert_eq!(finding.severity, Severity::Info); + assert_eq!(finding.context["confidence"], "heuristic"); +} + +#[test] +fn marquee_static_ignores_short_multiline_and_animated_content() { + for source in [ + "const View = () => A / B / C;", + "const View = () => Research ✦ Repair\n✦ Renew ✦ Research;", + "const View = () => Research ✦ Repair ✦ Renew ✦ Research;", + "const strip = keyframes({ from: { transform: 'translateX(0)' }, to: { transform: 'translateX(-50%)' } }); const View = () => Research ✦ Repair ✦ Renew ✦ Research;", + "const View = () => https://example.com/a/b/c/d/e;", + ] { + assert_eq!(count(source, "marquee-static"), 0, "{source}"); + } +} diff --git a/crates/devup-mcp-devup-ui/tests/codegen_defect_regressions.rs b/crates/devup-mcp-devup-ui/tests/codegen_defect_regressions.rs new file mode 100644 index 00000000..cfe7209a --- /dev/null +++ b/crates/devup-mcp-devup-ui/tests/codegen_defect_regressions.rs @@ -0,0 +1,174 @@ +use devup_mcp_devup_ui::codegen::{CodegenOptions, LengthToken, generate_component}; +use devup_mcp_figma::{Snapshot, discover_asset_manifest}; +use serde_json::{Value, json}; + +fn snapshot(nodes: Value) -> Snapshot { + serde_json::from_value( + json!({"fileKey":"regression","roots":["root"],"nodes":nodes,"diagnostics":[]}), + ) + .unwrap() +} + +#[test] +fn auto_layout_assets_preserve_container_and_individual_icons() { + for kind in ["FRAME", "INSTANCE", "COMPONENT"] { + for mode in ["HORIZONTAL", "VERTICAL", "GRID"] { + let input = snapshot(json!({ + "root":{"id":"root","type":kind,"fields":{"name":"Header","layoutMode":mode,"width":360,"height":64,"paddingLeft":12,"itemSpacing":29,"childrenIds":["logo","menu"]}}, + "logo":{"id":"logo","type":"VECTOR","fields":{"name":"Logo","parentId":"root","width":120,"height":24}}, + "menu":{"id":"menu","type":"VECTOR","fields":{"name":"Menu","parentId":"root","width":24,"height":24}} + })); + let output = generate_component(&input, "root", &CodegenOptions::default()).unwrap(); + assert!( + !output.tsx.contains("/icons/header.svg"), + "{kind}/{mode}: {}", + output.tsx + ); + let manifest = discover_asset_manifest(&input); + assert_eq!(manifest.assets.len(), 2, "{kind}/{mode}: {manifest:?}"); + } + } +} + +#[test] +fn exported_root_never_inherits_absolute_placement_from_uncollected_parent() { + let input = snapshot( + json!({"root":{"id":"root","type":"FRAME","fields":{"layoutPositioning":"ABSOLUTE","parentId":"outside","width":360,"height":64,"layoutSizingHorizontal":"FIXED","layoutSizingVertical":"FIXED"}}}), + ); + let output = generate_component(&input, "root", &CodegenOptions::default()).unwrap(); + assert!(!output.tsx.contains("pos=\"absolute\""), "{}", output.tsx); +} + +#[test] +fn root_fill_and_hug_axes_are_independent() { + for (horizontal, vertical, width, height) in [ + ("FILL", "HUG", true, false), + ("HUG", "FILL", false, true), + ("HUG", "HUG", false, false), + ("FILL", "FILL", true, true), + ] { + let input = snapshot( + json!({"root":{"id":"root","type":"FRAME","fields":{"layoutMode":"VERTICAL","layoutSizingHorizontal":horizontal,"layoutSizingVertical":vertical,"width":360,"height":80}}}), + ); + let output = generate_component(&input, "root", &CodegenOptions::default()).unwrap(); + assert_eq!( + output.tsx.contains("w=\"100%\"") || output.tsx.contains("boxSize=\"100%\""), + width, + "{}", + output.tsx + ); + assert_eq!( + output.tsx.contains("h=\"100%\"") || output.tsx.contains("boxSize=\"100%\""), + height, + "{}", + output.tsx + ); + } +} + +#[test] +fn typography_deduplicates_resolved_tokens_and_retains_real_overrides() { + for size in [16, 18] { + let input = snapshot( + json!({"root":{"id":"root","type":"TEXT","fields":{"width":360,"characters":"Body","textStyleId":"style","fontSize":size,"fontWeight":400,"lineHeight":{"unit":"PIXELS","value":24},"letterSpacing":{"unit":"PIXELS","value":1},"boundVariables":{"fontSize":{"id":"size","type":"VARIABLE_ALIAS"}}}}}), + ); + let options = CodegenOptions { + text_style_tokens: [("style".into(), "body".into())].into(), + typography_values: [("body".into(), json!({"fontSize":"$body","fontWeight":400,"lineHeight":"24px","letterSpacing":"0px"}))].into(), + length_tokens: [("size".into(), LengthToken {token:"body".into(),value:json!("16px")})].into(), + ..Default::default() + }; + let output = generate_component(&input, "root", &options).unwrap(); + assert_eq!( + output.tsx.contains("fontSize="), + size != 16, + "{}", + output.tsx + ); + assert!(!output.tsx.contains("fontWeight="), "{}", output.tsx); + assert!(!output.tsx.contains("lineHeight="), "{}", output.tsx); + assert!( + output.tsx.contains("letterSpacing=\"1px\""), + "{}", + output.tsx + ); + } +} + +#[test] +fn suspicious_values_are_advisory_and_preserve_original_dimensions() { + let input = snapshot( + json!({"root":{"id":"root","type":"FRAME","fields":{"width":171,"height":80,"layoutSizingHorizontal":"FIXED","layoutSizingVertical":"FIXED","targetAspectRatio":{"x":6.19,"y":1}}}}), + ); + let output = generate_component(&input, "root", &CodegenOptions::default()).unwrap(); + let issues = output + .diagnostics + .iter() + .filter(|d| d.code == "DEVUP_CODEGEN_SUSPICIOUS_VALUE") + .collect::>(); + assert_eq!(issues.len(), 2, "{:?}", output.diagnostics); + assert!( + issues + .iter() + .all(|issue| issue.fidelity_impact() == devup_mcp_figma::FidelityImpact::None) + ); + assert!( + issues + .iter() + .all(|issue| issue.details.as_ref().unwrap()["nextAction"].is_string()) + ); + assert!(output.tsx.contains("w=\"171px\""), "{}", output.tsx); + assert!( + output.tsx.contains("aspectRatio=\"6.19\""), + "{}", + output.tsx + ); +} + +#[test] +fn suspicious_values_exclude_centered_arithmetic_text_and_simple_ratios() { + for mode in ["centered", "arithmetic", "text", "asset", "simple"] { + let mut input = snapshot(json!({ + "root":{"id":"root","type":"FRAME","fields":{"width":360,"height":80,"layoutMode":"HORIZONTAL","paddingLeft":9,"paddingRight":9,"itemSpacing":0,"childrenIds":["child","sibling"]}}, + "child":{"id":"child","type":"FRAME","fields":{"parentId":"root","width":171,"height":80,"layoutSizingHorizontal":"FIXED","layoutSizingVertical":"FIXED"}}, + "sibling":{"id":"sibling","type":"TEXT","fields":{"parentId":"root","characters":"Sibling","width":171}} + })); + match mode { + "centered" => { + input + .nodes + .get_mut("root") + .unwrap() + .fields + .insert("counterAxisAlignItems".into(), json!("CENTER")); + } + "arithmetic" => {} + "text" => { + let node = input.nodes.get_mut("child").unwrap(); + node.node_type = "TEXT".into(); + node.fields + .insert("layoutSizingHorizontal".into(), json!("HUG")); + } + "asset" => { + input.nodes.get_mut("child").unwrap().node_type = "VECTOR".into(); + } + "simple" => { + let node = input.nodes.get_mut("child").unwrap(); + node.fields.insert("width".into(), json!(160)); + node.fields + .insert("targetAspectRatio".into(), json!({"x":16,"y":9})); + } + _ => unreachable!(), + } + let output = generate_component(&input, "root", &CodegenOptions::default()).unwrap(); + assert!( + !output + .diagnostics + .iter() + .any(|d| d.code == "DEVUP_CODEGEN_SUSPICIOUS_VALUE" + && d.node_id.as_deref() == Some("child")), + "{mode}: {:?}", + output.diagnostics + ); + } +} diff --git a/crates/devup-mcp-devup-ui/tests/feedback_theme.rs b/crates/devup-mcp-devup-ui/tests/feedback_theme.rs index 1f3160ad..e45ae9d1 100644 --- a/crates/devup-mcp-devup-ui/tests/feedback_theme.rs +++ b/crates/devup-mcp-devup-ui/tests/feedback_theme.rs @@ -217,7 +217,7 @@ fn collected_equal_typography_properties_are_elided_but_local_overrides_stay() { } #[test] -fn responsive_typography_elides_only_properties_equal_at_every_slot() { +fn responsive_typography_elides_values_the_token_defines_at_any_slot() { let options = CodegenOptions { text_style_tokens: [("title".into(), "heading".into())].into(), typography_values: [( @@ -229,7 +229,20 @@ fn responsive_typography_elides_only_properties_equal_at_every_slot() { }; let output = generate_component(&text(700), "t", &options).unwrap(); assert!(!output.tsx.contains("fontWeight="), "{}", output.tsx); - assert!(output.tsx.contains("fontSize=\"32px\""), "{}", output.tsx); + assert!(!output.tsx.contains("fontSize="), "{}", output.tsx); + let mut overridden = text(700); + overridden + .nodes + .get_mut("t") + .unwrap() + .fields + .insert("fontSize".into(), json!(24)); + let overridden = generate_component(&overridden, "t", &options).unwrap(); + assert!( + overridden.tsx.contains("fontSize=\"24px\""), + "{}", + overridden.tsx + ); assert!( generate_component(&text(400), "t", &options) .unwrap() diff --git a/crates/devup-mcp-devup-ui/tests/image_card_layout.rs b/crates/devup-mcp-devup-ui/tests/image_card_layout.rs index e1f1131e..4ec8f4b1 100644 --- a/crates/devup-mcp-devup-ui/tests/image_card_layout.rs +++ b/crates/devup-mcp-devup-ui/tests/image_card_layout.rs @@ -428,6 +428,12 @@ fn omitted_or_absolute_images_and_non_flex_boxes_do_not_claim_intrinsic_flex_ris .unwrap() .fields .remove("inferredAutoLayout"); + snapshot + .nodes + .get_mut("root") + .unwrap() + .fields + .remove("layoutMode"); } let output = generate(&snapshot); assert!( diff --git a/crates/devup-mcp-devup-ui/tests/instance_overrides.rs b/crates/devup-mcp-devup-ui/tests/instance_overrides.rs new file mode 100644 index 00000000..7cc02192 --- /dev/null +++ b/crates/devup-mcp-devup-ui/tests/instance_overrides.rs @@ -0,0 +1,98 @@ +use devup_mcp_devup_ui::codegen::{ + CodegenOptions, generate_component, instance_overrides::instance_overrides, +}; +use devup_mcp_figma::{RawNode, Snapshot}; +use serde_json::json; + +fn button() -> (Snapshot, CodegenOptions) { + let mut snapshot: Snapshot = + serde_json::from_str(include_str!("fixtures/chondrogen_button_set.json")).unwrap(); + let mut instance = snapshot.nodes["24:1853"].clone(); + instance.id = "51:5705".into(); + instance.node_type = "INSTANCE".into(); + instance + .fields + .insert("mainComponentId".into(), json!("24:1853")); + instance + .fields + .insert("mainComponentSetId".into(), json!("23:1613")); + instance.fields.insert( + "variantProperties".into(), + json!({"effect":"default","size":"md","type":"primary"}), + ); + instance.fields.insert("childrenIds".into(), json!([])); + instance.fields.insert( + "overrides".into(), + json!([{"id":"51:5705","overriddenFields":["fills"]}]), + ); + instance.fields.insert("fills".into(), json!([{"type":"SOLID","color":{"r":0.0235294118,"g":0.51372549,"b":0.56862745},"boundVariables":{"color":{"type":"VARIABLE_ALIAS","id":"VariableID:9:346"}}}])); + snapshot.nodes.insert(instance.id.clone(), instance); + snapshot.roots = vec!["51:5705".into()]; + let options = CodegenOptions { + variable_tokens: [("VariableID:9:346".into(), "secondary".into())].into(), + inline_instances: true, + ..CodegenOptions::default() + }; + (snapshot, options) +} + +#[test] +fn chondrogen_primary_button_fill_override_requires_preservation() { + let (snapshot, options) = button(); + let records = instance_overrides(&snapshot, "51:5705", &options); + assert_eq!(records.len(), 1); + assert_eq!(records[0]["classification"], "not-expressible"); + assert_eq!(records[0]["mainComponentId"], "24:1853"); + assert_eq!(records[0]["mainComponentSetId"], "23:1613"); + assert_eq!(records[0]["differsFromMaster"], true); + assert!( + records[0]["generatedProps"] + .as_array() + .unwrap() + .contains(&json!("bg=\"$secondary\"")) + ); + let output = generate_component(&snapshot, "51:5705", &options).unwrap(); + assert!(output.tsx.contains("// @devup-override ")); + assert!( + output + .diagnostics + .iter() + .any(|issue| issue.code == "DEVUP_CODEGEN_INSTANCE_OVERRIDE") + ); +} + +#[test] +fn text_characters_bound_to_text_component_property_is_not_loss() { + let (mut snapshot, options) = button(); + let child: RawNode = + serde_json::from_value(json!({"id":"I51:5705;24:1852","type":"TEXT","fields":{ + "characters":"Custom label","componentPropertyReferences":{"characters":"Label#1:2"}}})) + .unwrap(); + snapshot.nodes.insert(child.id.clone(), child); + let instance = snapshot.nodes.get_mut("51:5705").unwrap(); + instance + .fields + .insert("childrenIds".into(), json!(["I51:5705;24:1852"])); + instance.fields.insert( + "componentProperties".into(), + json!({"Label#1:2":{"type":"TEXT","value":"Custom label"}}), + ); + instance.fields.insert( + "overrides".into(), + json!([{"id":"I51:5705;24:1852","overriddenFields":["characters"]}]), + ); + assert!(instance_overrides(&snapshot, "51:5705", &options).is_empty()); +} + +#[test] +fn unbound_characters_and_missing_descendants_are_reported() { + let (mut snapshot, options) = button(); + snapshot.nodes.get_mut("51:5705").unwrap().fields.insert( + "overrides".into(), + json!([ + {"id":"missing","overriddenFields":["characters"]}]), + ); + let records = instance_overrides(&snapshot, "51:5705", &options); + assert_eq!(records[0]["resolution"], "unresolved"); + assert_eq!(records[0]["generatedProps"], json!([])); +} diff --git a/crates/devup-mcp-devup-ui/tests/placement_contract.rs b/crates/devup-mcp-devup-ui/tests/placement_contract.rs index eaa763fa..7caf93c6 100644 --- a/crates/devup-mcp-devup-ui/tests/placement_contract.rs +++ b/crates/devup-mcp-devup-ui/tests/placement_contract.rs @@ -100,7 +100,7 @@ fn r4_embedded_root_anchors_children_and_discloses_host_size() { } #[test] -fn r4_absolute_capture_root_discloses_external_containing_block() { +fn r4_absolute_capture_root_uses_normal_flow_without_collected_parent() { let mut snapshot = snapshot(); snapshot.nodes.remove("3997:46582"); snapshot.roots = vec!["3997:46621".into()]; @@ -112,11 +112,11 @@ fn r4_absolute_capture_root_discloses_external_containing_block() { .unwrap(); assert_eq!( contract.details.as_ref().unwrap()["containingBlock"], - "external-host" + "normal-flow-host" ); assert_eq!( contract.fidelity_impact(), - devup_mcp_figma::FidelityImpact::Approximated + devup_mcp_figma::FidelityImpact::None ); } @@ -254,7 +254,7 @@ fn r4_absolute_root_isolates_its_background_child() { ) .unwrap(); let tag = root_tag(&output.tsx); - assert!(tag.contains("pos=\"absolute\""), "{tag}"); + assert!(tag.contains("pos=\"relative\""), "{tag}"); assert!(tag.contains("zIndex=\"0\""), "{tag}"); assert!(output.tsx.contains("zIndex=\"-1\""), "{}", output.tsx); } diff --git a/crates/devup-mcp-devup-ui/tests/provenance.rs b/crates/devup-mcp-devup-ui/tests/provenance.rs index 4383150c..c33effb7 100644 --- a/crates/devup-mcp-devup-ui/tests/provenance.rs +++ b/crates/devup-mcp-devup-ui/tests/provenance.rs @@ -37,6 +37,7 @@ fn fidelity_snapshot(characters: &str) -> Snapshot { "name": "Card", "childrenIds": ["1:2"], "layoutMode": "VERTICAL", "inferredAutoLayout": {"layoutMode": "VERTICAL"}, "width": 320, "height": 200, "clipsContent": true, + "layoutSizingHorizontal": "FIXED", "layoutSizingVertical": "FIXED", "fills": [{ "type": "SOLID", "color": {"r": 1, "g": 0, "b": 0}, "boundVariables": {"color": {"type": "VARIABLE_ALIAS", "id": "v"}} @@ -347,7 +348,7 @@ fn asset_boundaries_exclude_internal_and_descendant_layout_fields() { json!({ "name": "Folded icon", "parentId": "root", "childrenIds": ["glyph-left", "glyph-right"], - "layoutMode": "HORIZONTAL", "layoutPositioning": "ABSOLUTE", + "layoutMode": "NONE", "layoutPositioning": "ABSOLUTE", "layoutSizingHorizontal": "FIXED", "layoutSizingVertical": "FIXED", "itemSpacing": 4, "paddingTop": 1, "paddingRight": 2, "paddingBottom": 3, "paddingLeft": 4, diff --git a/crates/devup-mcp-devup-ui/tests/responsive_contract.rs b/crates/devup-mcp-devup-ui/tests/responsive_contract.rs index 790dd216..95ddcbb1 100644 --- a/crates/devup-mcp-devup-ui/tests/responsive_contract.rs +++ b/crates/devup-mcp-devup-ui/tests/responsive_contract.rs @@ -279,7 +279,7 @@ fn a_frame_laid_out_differently_by_width_keeps_each_direction() { }); let compact = merged.tsx.split_whitespace().collect::(); assert!( - compact.contains(r#"flexDir={["column",null,null,null,"initial"]}"#), + compact.contains(r#"flexDir={["column",null,null,null,"row"]}"#), "{}", merged.tsx ); diff --git a/crates/devup-mcp-devup-ui/tests/responsive_merge.rs b/crates/devup-mcp-devup-ui/tests/responsive_merge.rs index eb9f6425..65fb3e04 100644 --- a/crates/devup-mcp-devup-ui/tests/responsive_merge.rs +++ b/crates/devup-mcp-devup-ui/tests/responsive_merge.rs @@ -354,24 +354,22 @@ fn a_prop_outside_the_cleared_set_is_left_to_inherit() { ); } -/// A first value that is simply what the prop already is need not be written. -/// `flexDir="row"` and `alignItems="flex-start"` are the two this hits in -/// practice. +/// A drawn value must override any layout implied by the merged primitive. #[test] -fn a_leading_default_is_dropped() { +fn a_leading_drawn_default_is_preserved() { assert_eq!( slots( "flexDir", [Set("row"), Absent, Absent, Absent, Set("column")] ), - array(&[None, None, None, None, Some("column")]) + array(&[Some("row"), None, None, None, Some("column")]) ); assert_eq!( slots( "alignItems", [Set("flex-start"), Absent, Absent, Absent, Set("center")] ), - array(&[None, None, None, None, Some("center")]) + array(&[Some("flex-start"), None, None, None, Some("center")]) ); // The same value on a prop with no registered default stays put. assert_eq!( diff --git a/crates/devup-mcp-devup-ui/tests/responsive_root_width.rs b/crates/devup-mcp-devup-ui/tests/responsive_root_width.rs new file mode 100644 index 00000000..6f5edba8 --- /dev/null +++ b/crates/devup-mcp-devup-ui/tests/responsive_root_width.rs @@ -0,0 +1,49 @@ +//! A screen's widths are its frame widths, not a layout: the merged root +//! takes its container's width instead of `w={['360px', ..., '1440px']}`. + +use devup_mcp_devup_ui::codegen::{CodegenOptions, responsive::merge_breakpoints}; +use devup_mcp_figma::Snapshot; +use serde_json::json; + +fn snapshot() -> Snapshot { + let frame = |id: &str, name: &str, width: u32, child: &str| { + json!({"id":id,"type":"FRAME","fields":{"name":name,"width":width,"height":600, + "layoutMode":"VERTICAL","inferredAutoLayout":{"layoutMode":"VERTICAL"}, + "layoutSizingHorizontal":"FIXED","layoutSizingVertical":"FIXED", + "childrenIds":[child]}}) + }; + let card = |id: &str, parent: &str, width: u32| { + json!({"id":id,"type":"FRAME","fields":{"name":"card","parentId":parent, + "width":width,"height":120,"layoutSizingHorizontal":"FIXED", + "layoutSizingVertical":"FIXED","visible":true}}) + }; + serde_json::from_value(json!({ + "fileKey":"root-width","version":null,"roots":["m","d"],"diagnostics":[], + "nodes":{ + "m": frame("m", "mobile", 360, "mc"), + "mc": card("mc", "m", 320), + "d": frame("d", "desktop", 1440, "dc"), + "dc": card("dc", "d", 1200), + } + })) + .unwrap() +} + +#[test] +fn the_merged_root_does_not_carry_its_frame_widths() { + let merged = merge_breakpoints(&snapshot(), &CodegenOptions::default()) + .unwrap() + .unwrap(); + let root = merged.tsx.split('>').next().unwrap(); + assert!(!root.contains("w="), "{}", merged.tsx); + assert!(!merged.tsx.contains("\"1440px\""), "{}", merged.tsx); + assert!(merged.tsx.contains("\"1200px\""), "{}", merged.tsx); + assert!( + !merged + .gaps + .iter() + .any(|gap| gap["reason"] == "attribute-only-at-width" && gap["property"] == "w"), + "{:?}", + merged.gaps + ); +} diff --git a/crates/devup-mcp-devup-ui/tests/responsive_structural.rs b/crates/devup-mcp-devup-ui/tests/responsive_structural.rs new file mode 100644 index 00000000..a102d7b2 --- /dev/null +++ b/crates/devup-mcp-devup-ui/tests/responsive_structural.rs @@ -0,0 +1,192 @@ +use devup_mcp_devup_ui::codegen::{CodegenOptions, responsive::merge_breakpoints}; +use devup_mcp_figma::Snapshot; +use serde_json::{Value, json}; + +fn merge( + changes: impl Fn(usize, &mut Value), +) -> devup_mcp_devup_ui::codegen::responsive::MergedScreen { + let mut nodes = serde_json::Map::new(); + for (index, width) in [1440, 360].into_iter().enumerate() { + let frame = json!({"id":format!("root:{index}"),"type":"FRAME","fields":{ + "name":"screen","width":width,"height":200,"layoutMode":"VERTICAL", + "childrenIds":[format!("section:{index}")] + }}); + let mut section = json!({"id":format!("section:{index}"),"type":"FRAME","fields":{ + "name":"Footer","parentId":format!("root:{index}"),"width":width,"height":120, + "layoutMode":"HORIZONTAL","layoutSizingHorizontal":"FIXED","layoutSizingVertical":"FIXED", + "paddingTop":0,"paddingRight":0,"paddingBottom":0,"paddingLeft":0, + "childrenIds":[format!("logo:{index}"),format!("links:{index}")] + }}); + changes(index, &mut section); + let layout = section["fields"].clone(); + section["fields"]["inferredAutoLayout"] = layout; + let logo = json!({"id":format!("logo:{index}"),"type":"FRAME","fields":{ + "name":"Logo","parentId":format!("section:{index}"),"width":if index == 0 {240} else {173}, + "height":28,"layoutSizingHorizontal":"FIXED","layoutSizingVertical":"FIXED" + }}); + let links = json!({"id":format!("links:{index}"),"type":"TEXT","fields":{ + "name":"Links","parentId":format!("section:{index}"),"characters":"Company", + "fontSize":16,"width":100,"height":24,"textAutoResize":"WIDTH_AND_HEIGHT" + }}); + for node in [frame, section, logo, links] { + nodes.insert(node["id"].as_str().unwrap().to_owned(), node.clone()); + } + for name in ["search", "menu", "locale"] { + let id = format!("{name}:{index}"); + nodes.insert( + id.clone(), + json!({"id":id,"type":"TEXT","fields":{ + "name":name,"parentId":format!("section:{index}"),"characters":name, + "fontSize":16,"width":36,"height":24,"textAutoResize":"WIDTH_AND_HEIGHT" + }}), + ); + } + } + let snapshot: Snapshot = serde_json::from_value(json!({ + "fileKey":"structural","version":null,"roots":["root:0","root:1"],"nodes":nodes,"diagnostics":[] + })).unwrap(); + merge_breakpoints(&snapshot, &CodegenOptions::default()) + .unwrap() + .unwrap() +} + +#[test] +fn component_identity_pairs_renamed_containers_before_shape_matching() { + let merged = merge(|index, section| { + section["fields"]["mainComponentId"] = json!("footer-master"); + section["fields"]["name"] = json!(if index == 0 { + "wide footer" + } else { + "compact footer" + }); + section["fields"]["layoutMode"] = json!(if index == 0 { "HORIZONTAL" } else { "VERTICAL" }); + }); + let sections = merged + .elements + .iter() + .filter(|element| element.ids.iter().any(|(_, id)| id.starts_with("section:"))) + .collect::>(); + assert_eq!(sections.len(), 1, "{}", merged.tsx); + assert_eq!(sections[0].ids.len(), 2, "{}", merged.tsx); + assert!(sections[0].hidden_at.is_empty()); + assert!(merged.diagnostics.is_empty(), "{:?}", merged.diagnostics); +} + +#[test] +fn footer_direction_and_padding_merge_despite_different_container_shapes() { + let merged = merge(|index, section| { + section["fields"]["layoutMode"] = json!(if index == 0 { "HORIZONTAL" } else { "VERTICAL" }); + section["fields"]["paddingTop"] = json!(if index == 0 { 60 } else { 36 }); + section["fields"]["paddingBottom"] = json!(if index == 0 { 60 } else { 36 }); + }); + let compact = merged.tsx.split_whitespace().collect::(); + assert!( + compact.contains("flexDir={[\"column\",null,null,null,\"row\"]}"), + "{}", + merged.tsx + ); + assert!( + compact.contains("py={[\"36px\",null,null,null,\"60px\"]}"), + "{}", + merged.tsx + ); + assert!(!compact.contains("\"none\""), "{}", merged.tsx); + assert_eq!( + merged + .elements + .iter() + .filter(|element| element.ids.iter().any(|(_, id)| id.starts_with("logo:"))) + .count(), + 1 + ); +} + +#[test] +fn prop_names_do_not_split_the_same_child_between_widths() { + let merged = merge(|index, section| { + if index == 1 { + section["fields"]["paddingLeft"] = json!(12); + section["fields"]["paddingRight"] = json!(16); + } + }); + let compact = merged.tsx.split_whitespace().collect::(); + assert!(!compact.contains("\"none\""), "{}", merged.tsx); + assert!(compact.contains("pl={[\"12px\""), "{}", merged.tsx); + assert!(compact.contains("pr={[\"16px\""), "{}", merged.tsx); +} + +#[test] +fn header_shared_logo_survives_different_sibling_sets() { + let merged = merge(|index, section| { + section["fields"]["name"] = json!("Header"); + section["fields"]["childrenIds"] = if index == 1 { + json!(["logo:1", "search:1", "menu:1"]) + } else { + json!(["logo:0", "links:0", "locale:0"]) + }; + }); + let logos = merged + .elements + .iter() + .filter(|element| element.ids.iter().any(|(_, id)| id.starts_with("logo:"))) + .collect::>(); + assert_eq!(logos.len(), 1, "{}", merged.tsx); + assert_eq!(logos[0].ids.len(), 2, "{}", merged.tsx); + assert!(logos[0].hidden_at.is_empty(), "{}", merged.tsx); + for (id, hidden_width) in [ + ("links:0", 360), + ("locale:0", 360), + ("search:1", 1440), + ("menu:1", 1440), + ] { + assert!( + merged + .elements + .iter() + .any(|element| element.ids.iter().any(|(_, node)| node == id) + && element.hidden_at == [hidden_width]), + "{id}: {}", + merged.tsx + ); + } + assert!( + merged + .tsx + .split_whitespace() + .collect::() + .contains("w={[\"173px\",null,null,null,\"240px\"]}"), + "{}", + merged.tsx + ); +} + +#[test] +fn ambiguous_counterparts_report_node_ids_instead_of_silently_hiding() { + let merged = merge(|index, section| { + if index == 0 { + section["fields"]["childrenIds"] = json!(["logo:0", "logo:0", "links:0"]); + } + }); + let issue = merged + .diagnostics + .iter() + .find(|issue| issue.code == "DEVUP_CODEGEN_RESPONSIVE_UNMATCHED") + .expect("ambiguous pairing must be reported"); + let details = issue.details.as_ref().unwrap(); + assert!( + details["nodesByWidth"] + .as_array() + .unwrap() + .iter() + .any(|node| node["nodeId"] == "logo:0") + ); + assert!( + details["nodesByWidth"] + .as_array() + .unwrap() + .iter() + .any(|node| node["nodeId"] == "logo:1") + ); + assert!(details.get("reason").is_some()); + assert!(details.get("nextAction").is_some()); +} diff --git a/crates/devup-mcp-devup-ui/tests/single_edge_stroke.rs b/crates/devup-mcp-devup-ui/tests/single_edge_stroke.rs index 7f549da0..12e672cc 100644 --- a/crates/devup-mcp-devup-ui/tests/single_edge_stroke.rs +++ b/crates/devup-mcp-devup-ui/tests/single_edge_stroke.rs @@ -160,8 +160,11 @@ fn exported_asset_boundary_is_not_treated_as_a_live_css_layout_container() { "../../../fixtures/devup-figma-plugin/cases/codegen/upstream-codegen-181-5afa626d49.json" )) .unwrap(); - let snapshot: Snapshot = + let mut snapshot: Snapshot = serde_json::from_value(fixture["payload"]["snapshot"].clone()).unwrap(); + let fields = &mut snapshot.nodes.get_mut("80:40").unwrap().fields; + fields.insert("layoutMode".into(), json!("NONE")); + fields.remove("inferredAutoLayout"); let output = generate_component(&snapshot, "80:40", &CodegenOptions::default()).unwrap(); assert!( output.tsx.contains("borderTop=\"solid 1px #000\""), diff --git a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151__wquw_151_proofread_devup_ui.snap b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151__wquw_151_proofread_devup_ui.snap index df0e41a8..e1de8ac8 100644 --- a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151__wquw_151_proofread_devup_ui.snap +++ b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151__wquw_151_proofread_devup_ui.snap @@ -25,6 +25,7 @@ export function AStoryFProofread() { color="$text" fontSize="28px" fontWeight="700" + letterSpacing="-0.02em" lineHeight="39px" textAlign="center" typography="h3" @@ -40,6 +41,7 @@ export function AStoryFProofread() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -50,6 +52,7 @@ export function AStoryFProofread() { color="$primary" fontSize="17px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="29px" typography="bodySmSemibold" wordBreak="keep-all" @@ -64,6 +67,7 @@ export function AStoryFProofread() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -74,6 +78,7 @@ export function AStoryFProofread() { color="$primary" fontSize="17px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="29px" typography="bodySmSemibold" wordBreak="keep-all" @@ -118,6 +123,7 @@ export function AStoryFProofread() { color="$textLight" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" textAlign="center" typography="caption" @@ -135,6 +141,7 @@ export function AStoryFProofread() { color="$textLight" fontSize="14px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSm" wordBreak="keep-all" @@ -154,6 +161,7 @@ export function AStoryFProofread() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" > @@ -163,6 +171,7 @@ export function AStoryFProofread() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" whiteSpace="pre-wrap" @@ -205,6 +214,7 @@ export function AStoryFProofread() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -217,6 +227,7 @@ export function AStoryFProofread() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -231,6 +242,7 @@ export function AStoryFProofread() { flex="1" fontSize="22px" fontWeight="700" + letterSpacing="-0.02em" lineHeight="31px" typography="h5" wordBreak="keep-all" @@ -252,6 +264,7 @@ export function AStoryFProofread() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -264,6 +277,7 @@ export function AStoryFProofread() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -277,6 +291,7 @@ export function AStoryFProofread() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" typography="body" w="100%" @@ -299,6 +314,7 @@ export function AStoryFProofread() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -311,6 +327,7 @@ export function AStoryFProofread() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -324,6 +341,7 @@ export function AStoryFProofread() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" typography="body" w="100%" @@ -354,6 +372,7 @@ export function AStoryFProofread() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -366,6 +385,7 @@ export function AStoryFProofread() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -379,6 +399,7 @@ export function AStoryFProofread() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" typography="body" w="100%" @@ -401,6 +422,7 @@ export function AStoryFProofread() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -413,6 +435,7 @@ export function AStoryFProofread() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -426,6 +449,7 @@ export function AStoryFProofread() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" typography="body" w="100%" @@ -460,6 +484,7 @@ export function AStoryFProofread() { color="$text" fontSize="18px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="31px" typography="bodySemibold" wordBreak="keep-all" @@ -472,6 +497,7 @@ export function AStoryFProofread() { color="#FFF" fontSize="18px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="31px" typography="bodySemibold" wordBreak="keep-all" diff --git a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151__wquw_151_proofread_devup_ui_embedded.snap b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151__wquw_151_proofread_devup_ui_embedded.snap index e738d7e0..a4e743cf 100644 --- a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151__wquw_151_proofread_devup_ui_embedded.snap +++ b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151__wquw_151_proofread_devup_ui_embedded.snap @@ -19,6 +19,7 @@ export function AStoryFProofread() { color="$text" fontSize="28px" fontWeight="700" + letterSpacing="-0.02em" lineHeight="39px" textAlign="center" typography="h3" @@ -34,6 +35,7 @@ export function AStoryFProofread() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -44,6 +46,7 @@ export function AStoryFProofread() { color="$primary" fontSize="17px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="29px" typography="bodySmSemibold" wordBreak="keep-all" @@ -58,6 +61,7 @@ export function AStoryFProofread() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -68,6 +72,7 @@ export function AStoryFProofread() { color="$primary" fontSize="17px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="29px" typography="bodySmSemibold" wordBreak="keep-all" @@ -112,6 +117,7 @@ export function AStoryFProofread() { color="$textLight" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" textAlign="center" typography="caption" @@ -129,6 +135,7 @@ export function AStoryFProofread() { color="$textLight" fontSize="14px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSm" wordBreak="keep-all" @@ -148,6 +155,7 @@ export function AStoryFProofread() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" > @@ -157,6 +165,7 @@ export function AStoryFProofread() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" whiteSpace="pre-wrap" @@ -199,6 +208,7 @@ export function AStoryFProofread() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -211,6 +221,7 @@ export function AStoryFProofread() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -225,6 +236,7 @@ export function AStoryFProofread() { flex="1" fontSize="22px" fontWeight="700" + letterSpacing="-0.02em" lineHeight="31px" typography="h5" wordBreak="keep-all" @@ -246,6 +258,7 @@ export function AStoryFProofread() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -258,6 +271,7 @@ export function AStoryFProofread() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -271,6 +285,7 @@ export function AStoryFProofread() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" typography="body" w="100%" @@ -293,6 +308,7 @@ export function AStoryFProofread() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -305,6 +321,7 @@ export function AStoryFProofread() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -318,6 +335,7 @@ export function AStoryFProofread() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" typography="body" w="100%" @@ -348,6 +366,7 @@ export function AStoryFProofread() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -360,6 +379,7 @@ export function AStoryFProofread() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -373,6 +393,7 @@ export function AStoryFProofread() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" typography="body" w="100%" @@ -395,6 +416,7 @@ export function AStoryFProofread() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -407,6 +429,7 @@ export function AStoryFProofread() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -420,6 +443,7 @@ export function AStoryFProofread() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" typography="body" w="100%" @@ -454,6 +478,7 @@ export function AStoryFProofread() { color="$text" fontSize="18px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="31px" typography="bodySemibold" wordBreak="keep-all" @@ -466,6 +491,7 @@ export function AStoryFProofread() { color="#FFF" fontSize="18px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="31px" typography="bodySemibold" wordBreak="keep-all" diff --git a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151__wquw_151_proofread_diagnostics.snap b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151__wquw_151_proofread_diagnostics.snap index 8fa1a3f2..f59ac3b9 100644 --- a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151__wquw_151_proofread_diagnostics.snap +++ b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151__wquw_151_proofread_diagnostics.snap @@ -53,7 +53,7 @@ expression: json!(output.diagnostics) "appliedValue": { "state": "emitted", "generatedNodeId": "3879:35547", - "generatedSource": " \n 작은 시장, 큰 사랑\n ", + "generatedSource": " \n 작은 시장, 큰 사랑\n ", "sourceTruncated": false, "propertyMappingVerified": false }, @@ -68,7 +68,7 @@ expression: json!(output.diagnostics) "parentLayoutMode": "HORIZONTAL", "childSizing": "FILL", "generatedParent": "
\n \n \n 편집 설정\n \n
\n
\n \n 검토 완료\n \n
\n " + " \n \n \n 편집 설정\n \n \n
\n \n 검토 완료\n \n
\n " ], "fallback": null, "heightPreservation": { diff --git a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151__wquw_151_proofread_source_map.snap b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151__wquw_151_proofread_source_map.snap index f11b2f46..8bd68c00 100644 --- a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151__wquw_151_proofread_source_map.snap +++ b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151__wquw_151_proofread_source_map.snap @@ -156,6 +156,13 @@ expression: output.source_map "resolution": "raw-fallback", "elementIndex": 2 }, + { + "generatedProperty": "letterSpacing=\"-0.02em\"", + "nodeId": "3879:35520", + "property": "letterSpacing", + "resolution": "raw-fallback", + "elementIndex": 2 + }, { "calculation": "round(fontSize * percent / 100) = round(28 * 140 / 100) = 39px", "generatedProperty": "lineHeight=\"39px\"", @@ -335,6 +342,13 @@ expression: output.source_map "resolution": "raw-fallback", "elementIndex": 6 }, + { + "generatedProperty": "letterSpacing=\"-0.01em\"", + "nodeId": "3879:35523", + "property": "letterSpacing", + "resolution": "raw-fallback", + "elementIndex": 6 + }, { "calculation": "round(fontSize * percent / 100) = round(16 * 160 / 100) = 26px", "generatedProperty": "lineHeight=\"26px\"", @@ -409,6 +423,13 @@ expression: output.source_map "resolution": "raw-fallback", "elementIndex": 7 }, + { + "generatedProperty": "letterSpacing=\"-0.01em\"", + "nodeId": "3879:35524", + "property": "letterSpacing", + "resolution": "raw-fallback", + "elementIndex": 7 + }, { "calculation": "round(fontSize * percent / 100) = round(17 * 170 / 100) = 29px", "generatedProperty": "lineHeight=\"29px\"", @@ -611,6 +632,13 @@ expression: output.source_map "resolution": "raw-fallback", "elementIndex": 11 }, + { + "generatedProperty": "letterSpacing=\"-0.01em\"", + "nodeId": "3879:35528", + "property": "letterSpacing", + "resolution": "raw-fallback", + "elementIndex": 11 + }, { "calculation": "round(fontSize * percent / 100) = round(16 * 160 / 100) = 26px", "generatedProperty": "lineHeight=\"26px\"", @@ -685,6 +713,13 @@ expression: output.source_map "resolution": "raw-fallback", "elementIndex": 12 }, + { + "generatedProperty": "letterSpacing=\"-0.01em\"", + "nodeId": "3879:35529", + "property": "letterSpacing", + "resolution": "raw-fallback", + "elementIndex": 12 + }, { "calculation": "round(fontSize * percent / 100) = round(17 * 170 / 100) = 29px", "generatedProperty": "lineHeight=\"29px\"", @@ -977,6 +1012,13 @@ expression: output.source_map "resolution": "raw-fallback", "elementIndex": 17 }, + { + "generatedProperty": "letterSpacing=\"-0.01em\"", + "nodeId": "3879:35535", + "property": "letterSpacing", + "resolution": "raw-fallback", + "elementIndex": 17 + }, { "calculation": "round(fontSize * percent / 100) = round(15 * 160 / 100) = 24px", "generatedProperty": "lineHeight=\"24px\"", @@ -1101,6 +1143,13 @@ expression: output.source_map "resolution": "raw-fallback", "elementIndex": 22 }, + { + "generatedProperty": "letterSpacing=\"-0.01em\"", + "nodeId": "3879:35536", + "property": "letterSpacing", + "resolution": "raw-fallback", + "elementIndex": 22 + }, { "calculation": "round(fontSize * percent / 100) = round(14 * 160 / 100) = 22px", "generatedProperty": "lineHeight=\"22px\"", @@ -1253,6 +1302,13 @@ expression: output.source_map "resolution": "raw-fallback", "elementIndex": 24 }, + { + "generatedProperty": "letterSpacing=\"-0.01em\"", + "nodeId": "3879:35538", + "property": "letterSpacing", + "resolution": "raw-fallback", + "elementIndex": 24 + }, { "calculation": "round(fontSize * percent / 100) = round(16 * 160 / 100) = 26px", "generatedProperty": "lineHeight=\"26px\"", @@ -1320,6 +1376,13 @@ expression: output.source_map "resolution": "raw-fallback", "elementIndex": 25 }, + { + "generatedProperty": "letterSpacing=\"-0.01em\"", + "nodeId": "3879:35539", + "property": "letterSpacing", + "resolution": "raw-fallback", + "elementIndex": 25 + }, { "calculation": "round(fontSize * percent / 100) = round(16 * 160 / 100) = 26px", "generatedProperty": "lineHeight=\"26px\"", @@ -1629,6 +1692,13 @@ expression: output.source_map "resolution": "raw-fallback", "elementIndex": 32 }, + { + "generatedProperty": "letterSpacing=\"-0.01em\"", + "nodeId": "I3879:35545;1690:32933", + "property": "letterSpacing", + "resolution": "raw-fallback", + "elementIndex": 32 + }, { "calculation": "round(fontSize * percent / 100) = round(16 * 160 / 100) = 26px", "generatedProperty": "lineHeight=\"26px\"", @@ -1767,6 +1837,13 @@ expression: output.source_map "resolution": "raw-fallback", "elementIndex": 35 }, + { + "generatedProperty": "letterSpacing=\"-0.01em\"", + "nodeId": "I3879:35545;1690:32947", + "property": "letterSpacing", + "resolution": "raw-fallback", + "elementIndex": 35 + }, { "calculation": "round(fontSize * percent / 100) = round(14 * 160 / 100) = 22px", "generatedProperty": "lineHeight=\"22px\"", @@ -1898,6 +1975,13 @@ expression: output.source_map "resolution": "raw-fallback", "elementIndex": 37 }, + { + "generatedProperty": "letterSpacing=\"-0.02em\"", + "nodeId": "3879:35547", + "property": "letterSpacing", + "resolution": "raw-fallback", + "elementIndex": 37 + }, { "calculation": "round(fontSize * percent / 100) = round(22 * 140 / 100) = 31px", "generatedProperty": "lineHeight=\"31px\"", @@ -2057,6 +2141,13 @@ expression: output.source_map "resolution": "raw-fallback", "elementIndex": 40 }, + { + "generatedProperty": "letterSpacing=\"-0.01em\"", + "nodeId": "I3879:35549;1690:32933", + "property": "letterSpacing", + "resolution": "raw-fallback", + "elementIndex": 40 + }, { "calculation": "round(fontSize * percent / 100) = round(16 * 160 / 100) = 26px", "generatedProperty": "lineHeight=\"26px\"", @@ -2195,6 +2286,13 @@ expression: output.source_map "resolution": "raw-fallback", "elementIndex": 43 }, + { + "generatedProperty": "letterSpacing=\"-0.01em\"", + "nodeId": "I3879:35549;1690:32947", + "property": "letterSpacing", + "resolution": "raw-fallback", + "elementIndex": 43 + }, { "calculation": "round(fontSize * percent / 100) = round(14 * 160 / 100) = 22px", "generatedProperty": "lineHeight=\"22px\"", @@ -2326,6 +2424,13 @@ expression: output.source_map "resolution": "raw-fallback", "elementIndex": 45 }, + { + "generatedProperty": "letterSpacing=\"-0.01em\"", + "nodeId": "3879:35551", + "property": "letterSpacing", + "resolution": "raw-fallback", + "elementIndex": 45 + }, { "calculation": "round(fontSize * percent / 100) = round(18 * 180 / 100) = 32px", "generatedProperty": "lineHeight=\"32px\"", @@ -2492,6 +2597,13 @@ expression: output.source_map "resolution": "raw-fallback", "elementIndex": 50 }, + { + "generatedProperty": "letterSpacing=\"-0.01em\"", + "nodeId": "I3879:35553;1690:32933", + "property": "letterSpacing", + "resolution": "raw-fallback", + "elementIndex": 50 + }, { "calculation": "round(fontSize * percent / 100) = round(16 * 160 / 100) = 26px", "generatedProperty": "lineHeight=\"26px\"", @@ -2630,6 +2742,13 @@ expression: output.source_map "resolution": "raw-fallback", "elementIndex": 53 }, + { + "generatedProperty": "letterSpacing=\"-0.01em\"", + "nodeId": "I3879:35553;1690:32947", + "property": "letterSpacing", + "resolution": "raw-fallback", + "elementIndex": 53 + }, { "calculation": "round(fontSize * percent / 100) = round(14 * 160 / 100) = 22px", "generatedProperty": "lineHeight=\"22px\"", @@ -2761,6 +2880,13 @@ expression: output.source_map "resolution": "raw-fallback", "elementIndex": 55 }, + { + "generatedProperty": "letterSpacing=\"-0.01em\"", + "nodeId": "3879:35555", + "property": "letterSpacing", + "resolution": "raw-fallback", + "elementIndex": 55 + }, { "generatedProperty": "lineHeight=\"32px\"", "nodeId": "3879:35555", @@ -3010,6 +3136,13 @@ expression: output.source_map "resolution": "raw-fallback", "elementIndex": 68 }, + { + "generatedProperty": "letterSpacing=\"-0.01em\"", + "nodeId": "I3879:35557;1690:32933", + "property": "letterSpacing", + "resolution": "raw-fallback", + "elementIndex": 68 + }, { "calculation": "round(fontSize * percent / 100) = round(16 * 160 / 100) = 26px", "generatedProperty": "lineHeight=\"26px\"", @@ -3148,6 +3281,13 @@ expression: output.source_map "resolution": "raw-fallback", "elementIndex": 71 }, + { + "generatedProperty": "letterSpacing=\"-0.01em\"", + "nodeId": "I3879:35557;1690:32947", + "property": "letterSpacing", + "resolution": "raw-fallback", + "elementIndex": 71 + }, { "calculation": "round(fontSize * percent / 100) = round(14 * 160 / 100) = 22px", "generatedProperty": "lineHeight=\"22px\"", @@ -3279,6 +3419,13 @@ expression: output.source_map "resolution": "raw-fallback", "elementIndex": 73 }, + { + "generatedProperty": "letterSpacing=\"-0.01em\"", + "nodeId": "3879:35559", + "property": "letterSpacing", + "resolution": "raw-fallback", + "elementIndex": 73 + }, { "calculation": "round(fontSize * percent / 100) = round(18 * 180 / 100) = 32px", "generatedProperty": "lineHeight=\"32px\"", @@ -3445,6 +3592,13 @@ expression: output.source_map "resolution": "raw-fallback", "elementIndex": 84 }, + { + "generatedProperty": "letterSpacing=\"-0.01em\"", + "nodeId": "I3879:35561;1690:32933", + "property": "letterSpacing", + "resolution": "raw-fallback", + "elementIndex": 84 + }, { "calculation": "round(fontSize * percent / 100) = round(16 * 160 / 100) = 26px", "generatedProperty": "lineHeight=\"26px\"", @@ -3583,6 +3737,13 @@ expression: output.source_map "resolution": "raw-fallback", "elementIndex": 87 }, + { + "generatedProperty": "letterSpacing=\"-0.01em\"", + "nodeId": "I3879:35561;1690:32947", + "property": "letterSpacing", + "resolution": "raw-fallback", + "elementIndex": 87 + }, { "calculation": "round(fontSize * percent / 100) = round(14 * 160 / 100) = 22px", "generatedProperty": "lineHeight=\"22px\"", @@ -3714,6 +3875,13 @@ expression: output.source_map "resolution": "raw-fallback", "elementIndex": 89 }, + { + "generatedProperty": "letterSpacing=\"-0.01em\"", + "nodeId": "3879:35563", + "property": "letterSpacing", + "resolution": "raw-fallback", + "elementIndex": 89 + }, { "calculation": "round(fontSize * percent / 100) = round(18 * 180 / 100) = 32px", "generatedProperty": "lineHeight=\"32px\"", @@ -3967,6 +4135,13 @@ expression: output.source_map "resolution": "raw-fallback", "elementIndex": 98 }, + { + "generatedProperty": "letterSpacing=\"-0.01em\"", + "nodeId": "3879:35566", + "property": "letterSpacing", + "resolution": "raw-fallback", + "elementIndex": 98 + }, { "calculation": "round(fontSize * percent / 100) = round(18 * 170 / 100) = 31px", "generatedProperty": "lineHeight=\"31px\"", @@ -4089,6 +4264,13 @@ expression: output.source_map "resolution": "raw-fallback", "elementIndex": 100 }, + { + "generatedProperty": "letterSpacing=\"-0.01em\"", + "nodeId": "3879:35568", + "property": "letterSpacing", + "resolution": "raw-fallback", + "elementIndex": 100 + }, { "calculation": "round(fontSize * percent / 100) = round(18 * 170 / 100) = 31px", "generatedProperty": "lineHeight=\"31px\"", diff --git a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35503.snap b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35503.snap index 76846968..a8363672 100644 --- a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35503.snap +++ b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35503.snap @@ -35,6 +35,7 @@ export function Wquw151Frame387935503() { color="$text" fontSize="18px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="31px" textAlign="center" typography="bodySemibold" @@ -57,6 +58,7 @@ export function Wquw151Frame387935503() { flex="1" fontSize="15px" fontWeight="600" + letterSpacing="-0.05em" lineHeight="24px" overflow="hidden" textAlign="center" diff --git a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35518.snap b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35518.snap index df3691b5..24d6de14 100644 --- a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35518.snap +++ b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35518.snap @@ -25,6 +25,7 @@ export function Wquw151Frame387935518() { color="$text" fontSize="28px" fontWeight="700" + letterSpacing="-0.02em" lineHeight="39px" overflow="hidden" textAlign="center" @@ -42,6 +43,7 @@ export function Wquw151Frame387935518() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -52,6 +54,7 @@ export function Wquw151Frame387935518() { color="$primary" fontSize="17px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="29px" typography="bodySmSemibold" wordBreak="keep-all" @@ -66,6 +69,7 @@ export function Wquw151Frame387935518() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -76,6 +80,7 @@ export function Wquw151Frame387935518() { color="$primary" fontSize="17px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="29px" typography="bodySmSemibold" wordBreak="keep-all" @@ -120,6 +125,7 @@ export function Wquw151Frame387935518() { color="$textLight" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" overflow="hidden" textAlign="center" @@ -139,6 +145,7 @@ export function Wquw151Frame387935518() { color="$textLight" fontSize="14px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="22px" overflow="hidden" textOverflow="ellipsis" @@ -160,6 +167,7 @@ export function Wquw151Frame387935518() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" > @@ -169,6 +177,7 @@ export function Wquw151Frame387935518() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" overflow="hidden" textOverflow="ellipsis" @@ -213,6 +222,7 @@ export function Wquw151Frame387935518() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -225,6 +235,7 @@ export function Wquw151Frame387935518() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -239,6 +250,7 @@ export function Wquw151Frame387935518() { flex="1" fontSize="22px" fontWeight="700" + letterSpacing="-0.02em" lineHeight="31px" overflow="hidden" textOverflow="ellipsis" @@ -262,6 +274,7 @@ export function Wquw151Frame387935518() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -274,6 +287,7 @@ export function Wquw151Frame387935518() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -287,6 +301,7 @@ export function Wquw151Frame387935518() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" overflow="hidden" textOverflow="ellipsis" @@ -311,6 +326,7 @@ export function Wquw151Frame387935518() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -323,6 +339,7 @@ export function Wquw151Frame387935518() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -336,6 +353,7 @@ export function Wquw151Frame387935518() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" overflow="hidden" textOverflow="ellipsis" @@ -368,6 +386,7 @@ export function Wquw151Frame387935518() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -380,6 +399,7 @@ export function Wquw151Frame387935518() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -393,6 +413,7 @@ export function Wquw151Frame387935518() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" overflow="hidden" textOverflow="ellipsis" @@ -417,6 +438,7 @@ export function Wquw151Frame387935518() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -429,6 +451,7 @@ export function Wquw151Frame387935518() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -442,6 +465,7 @@ export function Wquw151Frame387935518() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" overflow="hidden" textOverflow="ellipsis" @@ -478,6 +502,7 @@ export function Wquw151Frame387935518() { color="$text" fontSize="18px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="31px" typography="bodySemibold" wordBreak="keep-all" @@ -490,6 +515,7 @@ export function Wquw151Frame387935518() { color="#FFF" fontSize="18px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="31px" typography="bodySemibold" wordBreak="keep-all" diff --git a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35569.snap b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35569.snap index 0b2a9e57..52eeb53c 100644 --- a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35569.snap +++ b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35569.snap @@ -25,6 +25,7 @@ export function Wquw151Frame387935569() { color="$text" fontSize="28px" fontWeight="700" + letterSpacing="-0.02em" lineHeight="39px" overflow="hidden" textAlign="center" @@ -42,6 +43,7 @@ export function Wquw151Frame387935569() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -52,6 +54,7 @@ export function Wquw151Frame387935569() { color="$primary" fontSize="17px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="29px" typography="bodySmSemibold" wordBreak="keep-all" @@ -71,6 +74,7 @@ export function Wquw151Frame387935569() { color="$textLight" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" overflow="hidden" textOverflow="ellipsis" @@ -93,6 +97,7 @@ export function Wquw151Frame387935569() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -113,6 +118,7 @@ export function Wquw151Frame387935569() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -128,6 +134,7 @@ export function Wquw151Frame387935569() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -138,6 +145,7 @@ export function Wquw151Frame387935569() { color="$primary" fontSize="17px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="29px" typography="bodySmSemibold" wordBreak="keep-all" @@ -182,6 +190,7 @@ export function Wquw151Frame387935569() { color="$textLight" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" overflow="hidden" textAlign="center" @@ -201,6 +210,7 @@ export function Wquw151Frame387935569() { color="$textLight" fontSize="14px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="22px" overflow="hidden" textOverflow="ellipsis" @@ -222,6 +232,7 @@ export function Wquw151Frame387935569() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" > @@ -231,6 +242,7 @@ export function Wquw151Frame387935569() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" overflow="hidden" textOverflow="ellipsis" @@ -275,6 +287,7 @@ export function Wquw151Frame387935569() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -287,6 +300,7 @@ export function Wquw151Frame387935569() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -301,6 +315,7 @@ export function Wquw151Frame387935569() { flex="1" fontSize="22px" fontWeight="700" + letterSpacing="-0.02em" lineHeight="31px" overflow="hidden" textOverflow="ellipsis" @@ -324,6 +339,7 @@ export function Wquw151Frame387935569() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -336,6 +352,7 @@ export function Wquw151Frame387935569() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -349,6 +366,7 @@ export function Wquw151Frame387935569() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" overflow="hidden" textOverflow="ellipsis" @@ -373,6 +391,7 @@ export function Wquw151Frame387935569() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -385,6 +404,7 @@ export function Wquw151Frame387935569() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -398,6 +418,7 @@ export function Wquw151Frame387935569() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" overflow="hidden" textOverflow="ellipsis" @@ -430,6 +451,7 @@ export function Wquw151Frame387935569() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -442,6 +464,7 @@ export function Wquw151Frame387935569() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -455,6 +478,7 @@ export function Wquw151Frame387935569() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" overflow="hidden" textOverflow="ellipsis" @@ -479,6 +503,7 @@ export function Wquw151Frame387935569() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -491,6 +516,7 @@ export function Wquw151Frame387935569() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -504,6 +530,7 @@ export function Wquw151Frame387935569() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" overflow="hidden" textOverflow="ellipsis" @@ -534,6 +561,7 @@ export function Wquw151Frame387935569() { color="$primary" fontSize="18px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="31px" typography="bodySemibold" wordBreak="keep-all" @@ -546,6 +574,7 @@ export function Wquw151Frame387935569() { color="#FFF" fontSize="18px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="31px" typography="bodySemibold" wordBreak="keep-all" @@ -582,6 +611,7 @@ export function Wquw151Frame387935569() { color="$text" fontSize="20px" fontWeight="700" + letterSpacing="-0.02em" lineHeight="28px" typography="h6" wordBreak="keep-all" @@ -592,6 +622,7 @@ export function Wquw151Frame387935569() { color="$textLight" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -638,6 +669,7 @@ export function Wquw151Frame387935569() { color="$primary" fontSize="18px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="31px" typography="bodySemibold" wordBreak="keep-all" @@ -648,6 +680,7 @@ export function Wquw151Frame387935569() { color="$caption" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" typography="caption" wordBreak="keep-all" @@ -672,6 +705,7 @@ export function Wquw151Frame387935569() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" typography="body" wordBreak="keep-all" @@ -682,6 +716,7 @@ export function Wquw151Frame387935569() { color="$caption" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" typography="caption" wordBreak="keep-all" @@ -706,6 +741,7 @@ export function Wquw151Frame387935569() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" typography="body" wordBreak="keep-all" @@ -716,6 +752,7 @@ export function Wquw151Frame387935569() { color="$caption" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" typography="caption" wordBreak="keep-all" @@ -731,6 +768,7 @@ export function Wquw151Frame387935569() { color="$text" fontSize="20px" fontWeight="700" + letterSpacing="-0.02em" lineHeight="28px" typography="h6" wordBreak="keep-all" @@ -741,6 +779,7 @@ export function Wquw151Frame387935569() { color="$textLight" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -768,6 +807,7 @@ export function Wquw151Frame387935569() { color="$primary" fontSize="18px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="31px" typography="bodySemibold" wordBreak="keep-all" @@ -778,6 +818,7 @@ export function Wquw151Frame387935569() { color="$caption" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" typography="caption" wordBreak="keep-all" @@ -802,6 +843,7 @@ export function Wquw151Frame387935569() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" typography="body" wordBreak="keep-all" @@ -812,6 +854,7 @@ export function Wquw151Frame387935569() { color="$caption" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" typography="caption" wordBreak="keep-all" diff --git a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35652.snap b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35652.snap index 4214318a..69bfca9a 100644 --- a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35652.snap +++ b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35652.snap @@ -25,6 +25,7 @@ export function Wquw151Frame387935652() { color="$text" fontSize="28px" fontWeight="700" + letterSpacing="-0.02em" lineHeight="39px" overflow="hidden" textAlign="center" @@ -42,6 +43,7 @@ export function Wquw151Frame387935652() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -52,6 +54,7 @@ export function Wquw151Frame387935652() { color="$primary" fontSize="17px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="29px" typography="bodySmSemibold" wordBreak="keep-all" @@ -71,6 +74,7 @@ export function Wquw151Frame387935652() { color="$textLight" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" overflow="hidden" textOverflow="ellipsis" @@ -93,6 +97,7 @@ export function Wquw151Frame387935652() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -113,6 +118,7 @@ export function Wquw151Frame387935652() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -128,6 +134,7 @@ export function Wquw151Frame387935652() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -138,6 +145,7 @@ export function Wquw151Frame387935652() { color="$primary" fontSize="17px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="29px" typography="bodySmSemibold" wordBreak="keep-all" @@ -182,6 +190,7 @@ export function Wquw151Frame387935652() { color="$textLight" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" overflow="hidden" textAlign="center" @@ -201,6 +210,7 @@ export function Wquw151Frame387935652() { color="$textLight" fontSize="14px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="22px" overflow="hidden" textOverflow="ellipsis" @@ -222,6 +232,7 @@ export function Wquw151Frame387935652() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" > @@ -231,6 +242,7 @@ export function Wquw151Frame387935652() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" overflow="hidden" textOverflow="ellipsis" @@ -275,6 +287,7 @@ export function Wquw151Frame387935652() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -287,6 +300,7 @@ export function Wquw151Frame387935652() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -301,6 +315,7 @@ export function Wquw151Frame387935652() { flex="1" fontSize="22px" fontWeight="700" + letterSpacing="-0.02em" lineHeight="31px" overflow="hidden" textOverflow="ellipsis" @@ -324,6 +339,7 @@ export function Wquw151Frame387935652() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -336,6 +352,7 @@ export function Wquw151Frame387935652() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -349,6 +366,7 @@ export function Wquw151Frame387935652() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" overflow="hidden" textOverflow="ellipsis" @@ -373,6 +391,7 @@ export function Wquw151Frame387935652() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -385,6 +404,7 @@ export function Wquw151Frame387935652() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -398,6 +418,7 @@ export function Wquw151Frame387935652() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" overflow="hidden" textOverflow="ellipsis" @@ -430,6 +451,7 @@ export function Wquw151Frame387935652() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -442,6 +464,7 @@ export function Wquw151Frame387935652() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -455,6 +478,7 @@ export function Wquw151Frame387935652() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" overflow="hidden" textOverflow="ellipsis" @@ -479,6 +503,7 @@ export function Wquw151Frame387935652() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -491,6 +516,7 @@ export function Wquw151Frame387935652() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -504,6 +530,7 @@ export function Wquw151Frame387935652() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" overflow="hidden" textOverflow="ellipsis" @@ -534,6 +561,7 @@ export function Wquw151Frame387935652() { color="$primary" fontSize="18px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="31px" typography="bodySemibold" wordBreak="keep-all" @@ -546,6 +574,7 @@ export function Wquw151Frame387935652() { color="#FFF" fontSize="18px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="31px" typography="bodySemibold" wordBreak="keep-all" @@ -582,6 +611,7 @@ export function Wquw151Frame387935652() { color="$text" fontSize="20px" fontWeight="700" + letterSpacing="-0.02em" lineHeight="28px" typography="h6" wordBreak="keep-all" @@ -592,6 +622,7 @@ export function Wquw151Frame387935652() { color="$textLight" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -638,6 +669,7 @@ export function Wquw151Frame387935652() { color="$primary" fontSize="18px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="31px" typography="bodySemibold" wordBreak="keep-all" @@ -648,6 +680,7 @@ export function Wquw151Frame387935652() { color="$caption" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" typography="caption" wordBreak="keep-all" @@ -672,6 +705,7 @@ export function Wquw151Frame387935652() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" typography="body" wordBreak="keep-all" @@ -682,6 +716,7 @@ export function Wquw151Frame387935652() { color="$caption" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" typography="caption" wordBreak="keep-all" @@ -706,6 +741,7 @@ export function Wquw151Frame387935652() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" typography="body" wordBreak="keep-all" @@ -716,6 +752,7 @@ export function Wquw151Frame387935652() { color="$caption" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" typography="caption" wordBreak="keep-all" diff --git a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35729.snap b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35729.snap index 21a960e9..139c17b1 100644 --- a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35729.snap +++ b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35729.snap @@ -25,6 +25,7 @@ export function Wquw151Frame387935729() { color="$text" fontSize="28px" fontWeight="700" + letterSpacing="-0.02em" lineHeight="39px" overflow="hidden" textAlign="center" @@ -42,6 +43,7 @@ export function Wquw151Frame387935729() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -52,6 +54,7 @@ export function Wquw151Frame387935729() { color="$primary" fontSize="17px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="29px" typography="bodySmSemibold" wordBreak="keep-all" @@ -71,6 +74,7 @@ export function Wquw151Frame387935729() { color="$textLight" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" overflow="hidden" textOverflow="ellipsis" @@ -93,6 +97,7 @@ export function Wquw151Frame387935729() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -113,6 +118,7 @@ export function Wquw151Frame387935729() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -128,6 +134,7 @@ export function Wquw151Frame387935729() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -138,6 +145,7 @@ export function Wquw151Frame387935729() { color="$primary" fontSize="17px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="29px" typography="bodySmSemibold" wordBreak="keep-all" @@ -182,6 +190,7 @@ export function Wquw151Frame387935729() { color="$textLight" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" overflow="hidden" textAlign="center" @@ -201,6 +210,7 @@ export function Wquw151Frame387935729() { color="$textLight" fontSize="14px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="22px" overflow="hidden" textOverflow="ellipsis" @@ -222,6 +232,7 @@ export function Wquw151Frame387935729() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" > @@ -231,6 +242,7 @@ export function Wquw151Frame387935729() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" overflow="hidden" textOverflow="ellipsis" @@ -275,6 +287,7 @@ export function Wquw151Frame387935729() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -287,6 +300,7 @@ export function Wquw151Frame387935729() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -301,6 +315,7 @@ export function Wquw151Frame387935729() { flex="1" fontSize="22px" fontWeight="700" + letterSpacing="-0.02em" lineHeight="31px" overflow="hidden" textOverflow="ellipsis" @@ -324,6 +339,7 @@ export function Wquw151Frame387935729() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -336,6 +352,7 @@ export function Wquw151Frame387935729() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -349,6 +366,7 @@ export function Wquw151Frame387935729() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" overflow="hidden" textOverflow="ellipsis" @@ -373,6 +391,7 @@ export function Wquw151Frame387935729() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -385,6 +404,7 @@ export function Wquw151Frame387935729() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -398,6 +418,7 @@ export function Wquw151Frame387935729() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" overflow="hidden" textOverflow="ellipsis" @@ -430,6 +451,7 @@ export function Wquw151Frame387935729() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -442,6 +464,7 @@ export function Wquw151Frame387935729() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -455,6 +478,7 @@ export function Wquw151Frame387935729() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" overflow="hidden" textOverflow="ellipsis" @@ -479,6 +503,7 @@ export function Wquw151Frame387935729() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -491,6 +516,7 @@ export function Wquw151Frame387935729() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -504,6 +530,7 @@ export function Wquw151Frame387935729() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" overflow="hidden" textOverflow="ellipsis" @@ -534,6 +561,7 @@ export function Wquw151Frame387935729() { color="$primary" fontSize="18px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="31px" typography="bodySemibold" wordBreak="keep-all" @@ -546,6 +574,7 @@ export function Wquw151Frame387935729() { color="#FFF" fontSize="18px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="31px" typography="bodySemibold" wordBreak="keep-all" @@ -582,6 +611,7 @@ export function Wquw151Frame387935729() { color="$text" fontSize="20px" fontWeight="700" + letterSpacing="-0.02em" lineHeight="28px" typography="h6" wordBreak="keep-all" @@ -592,6 +622,7 @@ export function Wquw151Frame387935729() { color="$textLight" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -638,6 +669,7 @@ export function Wquw151Frame387935729() { color="$primary" fontSize="18px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="31px" typography="bodySemibold" wordBreak="keep-all" @@ -648,6 +680,7 @@ export function Wquw151Frame387935729() { color="$caption" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" typography="caption" wordBreak="keep-all" @@ -672,6 +705,7 @@ export function Wquw151Frame387935729() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" typography="body" wordBreak="keep-all" @@ -682,6 +716,7 @@ export function Wquw151Frame387935729() { color="$caption" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" typography="caption" wordBreak="keep-all" diff --git a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35887.snap b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35887.snap index e4900a3d..87683d55 100644 --- a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35887.snap +++ b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35887.snap @@ -25,6 +25,7 @@ export function Wquw151Frame387935887() { color="$text" fontSize="28px" fontWeight="700" + letterSpacing="-0.02em" lineHeight="39px" overflow="hidden" textAlign="center" @@ -42,6 +43,7 @@ export function Wquw151Frame387935887() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -52,6 +54,7 @@ export function Wquw151Frame387935887() { color="$primary" fontSize="17px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="29px" typography="bodySmSemibold" wordBreak="keep-all" @@ -71,6 +74,7 @@ export function Wquw151Frame387935887() { color="$textLight" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" overflow="hidden" textOverflow="ellipsis" @@ -93,6 +97,7 @@ export function Wquw151Frame387935887() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -113,6 +118,7 @@ export function Wquw151Frame387935887() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -128,6 +134,7 @@ export function Wquw151Frame387935887() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -138,6 +145,7 @@ export function Wquw151Frame387935887() { color="$primary" fontSize="17px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="29px" typography="bodySmSemibold" wordBreak="keep-all" @@ -182,6 +190,7 @@ export function Wquw151Frame387935887() { color="$textLight" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" overflow="hidden" textAlign="center" @@ -201,6 +210,7 @@ export function Wquw151Frame387935887() { color="$textLight" fontSize="14px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="22px" overflow="hidden" textOverflow="ellipsis" @@ -222,6 +232,7 @@ export function Wquw151Frame387935887() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" > @@ -231,6 +242,7 @@ export function Wquw151Frame387935887() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" overflow="hidden" textOverflow="ellipsis" @@ -275,6 +287,7 @@ export function Wquw151Frame387935887() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -287,6 +300,7 @@ export function Wquw151Frame387935887() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -301,6 +315,7 @@ export function Wquw151Frame387935887() { flex="1" fontSize="22px" fontWeight="700" + letterSpacing="-0.02em" lineHeight="31px" overflow="hidden" textOverflow="ellipsis" @@ -324,6 +339,7 @@ export function Wquw151Frame387935887() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -336,6 +352,7 @@ export function Wquw151Frame387935887() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -349,6 +366,7 @@ export function Wquw151Frame387935887() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" overflow="hidden" textOverflow="ellipsis" @@ -373,6 +391,7 @@ export function Wquw151Frame387935887() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -385,6 +404,7 @@ export function Wquw151Frame387935887() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -398,6 +418,7 @@ export function Wquw151Frame387935887() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" overflow="hidden" textOverflow="ellipsis" @@ -430,6 +451,7 @@ export function Wquw151Frame387935887() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -442,6 +464,7 @@ export function Wquw151Frame387935887() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -455,6 +478,7 @@ export function Wquw151Frame387935887() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" overflow="hidden" textOverflow="ellipsis" @@ -479,6 +503,7 @@ export function Wquw151Frame387935887() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -491,6 +516,7 @@ export function Wquw151Frame387935887() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -504,6 +530,7 @@ export function Wquw151Frame387935887() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" overflow="hidden" textOverflow="ellipsis" @@ -534,6 +561,7 @@ export function Wquw151Frame387935887() { color="$primary" fontSize="18px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="31px" typography="bodySemibold" wordBreak="keep-all" @@ -546,6 +574,7 @@ export function Wquw151Frame387935887() { color="#FFF" fontSize="18px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="31px" typography="bodySemibold" wordBreak="keep-all" @@ -582,6 +611,7 @@ export function Wquw151Frame387935887() { color="$text" fontSize="20px" fontWeight="700" + letterSpacing="-0.02em" lineHeight="28px" typography="h6" wordBreak="keep-all" @@ -592,6 +622,7 @@ export function Wquw151Frame387935887() { color="$textLight" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" typography="caption" wordBreak="keep-all" @@ -635,6 +666,7 @@ export function Wquw151Frame387935887() { color="$primary" fontSize="18px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="31px" typography="bodySemibold" wordBreak="keep-all" @@ -645,6 +677,7 @@ export function Wquw151Frame387935887() { color="$caption" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" overflow="hidden" textOverflow="ellipsis" @@ -671,6 +704,7 @@ export function Wquw151Frame387935887() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" typography="body" wordBreak="keep-all" @@ -681,6 +715,7 @@ export function Wquw151Frame387935887() { color="$caption" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" overflow="hidden" textOverflow="ellipsis" @@ -698,6 +733,7 @@ export function Wquw151Frame387935887() { color="$text" fontSize="20px" fontWeight="700" + letterSpacing="-0.02em" lineHeight="28px" typography="h6" wordBreak="keep-all" @@ -708,6 +744,7 @@ export function Wquw151Frame387935887() { color="$textLight" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -735,6 +772,7 @@ export function Wquw151Frame387935887() { color="$primary" fontSize="18px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="31px" typography="bodySemibold" wordBreak="keep-all" @@ -745,6 +783,7 @@ export function Wquw151Frame387935887() { color="$caption" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" typography="caption" wordBreak="keep-all" @@ -769,6 +808,7 @@ export function Wquw151Frame387935887() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" typography="body" wordBreak="keep-all" @@ -779,6 +819,7 @@ export function Wquw151Frame387935887() { color="$caption" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" typography="caption" wordBreak="keep-all" @@ -815,6 +856,7 @@ export function Wquw151Frame387935887() { color="$text" fontSize="20px" fontWeight="700" + letterSpacing="-0.02em" lineHeight="28px" typography="h6" wordBreak="keep-all" @@ -826,6 +868,7 @@ export function Wquw151Frame387935887() { color="$text" fontSize="17px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="29px" overflow="hidden" textAlign="center" @@ -845,6 +888,7 @@ export function Wquw151Frame387935887() { color="$textLight" fontSize="17px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="29px" typography="bodySmSemibold" wordBreak="keep-all" @@ -863,6 +907,7 @@ export function Wquw151Frame387935887() { color="$primary" fontSize="17px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="29px" typography="bodySmSemibold" wordBreak="keep-all" diff --git a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35973.snap b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35973.snap index 15097227..ca4c98d8 100644 --- a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35973.snap +++ b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35973.snap @@ -25,6 +25,7 @@ export function Wquw151Frame387935973() { color="$text" fontSize="28px" fontWeight="700" + letterSpacing="-0.02em" lineHeight="39px" overflow="hidden" textAlign="center" @@ -42,6 +43,7 @@ export function Wquw151Frame387935973() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -52,6 +54,7 @@ export function Wquw151Frame387935973() { color="$primary" fontSize="17px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="29px" typography="bodySmSemibold" wordBreak="keep-all" @@ -71,6 +74,7 @@ export function Wquw151Frame387935973() { color="$textLight" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" overflow="hidden" textOverflow="ellipsis" @@ -93,6 +97,7 @@ export function Wquw151Frame387935973() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -113,6 +118,7 @@ export function Wquw151Frame387935973() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -128,6 +134,7 @@ export function Wquw151Frame387935973() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -138,6 +145,7 @@ export function Wquw151Frame387935973() { color="$primary" fontSize="17px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="29px" typography="bodySmSemibold" wordBreak="keep-all" @@ -182,6 +190,7 @@ export function Wquw151Frame387935973() { color="$textLight" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" overflow="hidden" textAlign="center" @@ -201,6 +210,7 @@ export function Wquw151Frame387935973() { color="$textLight" fontSize="14px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="22px" overflow="hidden" textOverflow="ellipsis" @@ -222,6 +232,7 @@ export function Wquw151Frame387935973() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" > @@ -231,6 +242,7 @@ export function Wquw151Frame387935973() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" overflow="hidden" textOverflow="ellipsis" @@ -275,6 +287,7 @@ export function Wquw151Frame387935973() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -287,6 +300,7 @@ export function Wquw151Frame387935973() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -301,6 +315,7 @@ export function Wquw151Frame387935973() { flex="1" fontSize="22px" fontWeight="700" + letterSpacing="-0.02em" lineHeight="31px" overflow="hidden" textOverflow="ellipsis" @@ -324,6 +339,7 @@ export function Wquw151Frame387935973() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -336,6 +352,7 @@ export function Wquw151Frame387935973() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -349,6 +366,7 @@ export function Wquw151Frame387935973() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" overflow="hidden" textOverflow="ellipsis" @@ -373,6 +391,7 @@ export function Wquw151Frame387935973() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -385,6 +404,7 @@ export function Wquw151Frame387935973() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -398,6 +418,7 @@ export function Wquw151Frame387935973() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" overflow="hidden" textOverflow="ellipsis" @@ -430,6 +451,7 @@ export function Wquw151Frame387935973() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -442,6 +464,7 @@ export function Wquw151Frame387935973() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -455,6 +478,7 @@ export function Wquw151Frame387935973() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" overflow="hidden" textOverflow="ellipsis" @@ -479,6 +503,7 @@ export function Wquw151Frame387935973() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -491,6 +516,7 @@ export function Wquw151Frame387935973() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -504,6 +530,7 @@ export function Wquw151Frame387935973() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" overflow="hidden" textOverflow="ellipsis" @@ -534,6 +561,7 @@ export function Wquw151Frame387935973() { color="$primary" fontSize="18px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="31px" typography="bodySemibold" wordBreak="keep-all" @@ -546,6 +574,7 @@ export function Wquw151Frame387935973() { color="#FFF" fontSize="18px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="31px" typography="bodySemibold" wordBreak="keep-all" @@ -582,6 +611,7 @@ export function Wquw151Frame387935973() { color="$text" fontSize="20px" fontWeight="700" + letterSpacing="-0.02em" lineHeight="28px" typography="h6" wordBreak="keep-all" @@ -592,6 +622,7 @@ export function Wquw151Frame387935973() { color="$textLight" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" typography="caption" wordBreak="keep-all" @@ -635,6 +666,7 @@ export function Wquw151Frame387935973() { color="$primary" fontSize="18px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="31px" typography="bodySemibold" wordBreak="keep-all" @@ -645,6 +677,7 @@ export function Wquw151Frame387935973() { color="$caption" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" overflow="hidden" textOverflow="ellipsis" @@ -671,6 +704,7 @@ export function Wquw151Frame387935973() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" typography="body" wordBreak="keep-all" @@ -681,6 +715,7 @@ export function Wquw151Frame387935973() { color="$caption" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" overflow="hidden" textOverflow="ellipsis" @@ -698,6 +733,7 @@ export function Wquw151Frame387935973() { color="$text" fontSize="20px" fontWeight="700" + letterSpacing="-0.02em" lineHeight="28px" typography="h6" wordBreak="keep-all" @@ -708,6 +744,7 @@ export function Wquw151Frame387935973() { color="$textLight" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -735,6 +772,7 @@ export function Wquw151Frame387935973() { color="$primary" fontSize="18px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="31px" typography="bodySemibold" wordBreak="keep-all" @@ -745,6 +783,7 @@ export function Wquw151Frame387935973() { color="$caption" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" typography="caption" wordBreak="keep-all" @@ -769,6 +808,7 @@ export function Wquw151Frame387935973() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" typography="body" wordBreak="keep-all" @@ -779,6 +819,7 @@ export function Wquw151Frame387935973() { color="$caption" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" typography="caption" wordBreak="keep-all" @@ -816,6 +857,7 @@ export function Wquw151Frame387935973() { color="$text" fontSize="20px" fontWeight="700" + letterSpacing="-0.02em" lineHeight="28px" typography="h6" wordBreak="keep-all" @@ -827,6 +869,7 @@ export function Wquw151Frame387935973() { color="$text" fontSize="17px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="29px" overflow="hidden" textAlign="center" @@ -850,6 +893,7 @@ export function Wquw151Frame387935973() { color="$textLight" fontSize="17px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="29px" typography="bodySmSemibold" wordBreak="keep-all" @@ -868,6 +912,7 @@ export function Wquw151Frame387935973() { color="$primary" fontSize="17px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="29px" typography="bodySmSemibold" wordBreak="keep-all" diff --git a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_36059.snap b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_36059.snap index 86cc64e4..18c58e17 100644 --- a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_36059.snap +++ b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_36059.snap @@ -46,6 +46,7 @@ export function Wquw151Frame387936059() { color="$textLight" fontSize="15px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="24px" typography="captionSemibold" wordBreak="keep-all" @@ -66,6 +67,7 @@ export function Wquw151Frame387936059() { color="$text" fontSize="24px" fontWeight="700" + letterSpacing="-0.02em" lineHeight="34px" overflow="hidden" textOverflow="ellipsis" @@ -96,6 +98,7 @@ export function Wquw151Frame387936059() { color="$secondary" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -108,6 +111,7 @@ export function Wquw151Frame387936059() { flex="1" fontSize="18px" fontWeight="500" + letterSpacing="-0.02em" lineHeight="31px" overflow="hidden" textOverflow="ellipsis" @@ -123,6 +127,7 @@ export function Wquw151Frame387936059() { color="$textLight" fontSize="15px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="24px" typography="captionSemibold" wordBreak="keep-all" @@ -136,6 +141,7 @@ export function Wquw151Frame387936059() { color="$textLight" fontSize="15px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="24px" typography="captionSemibold" wordBreak="keep-all" @@ -149,6 +155,7 @@ export function Wquw151Frame387936059() { color="$textLight" fontSize="15px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="24px" typography="captionSemibold" wordBreak="keep-all" @@ -162,6 +169,7 @@ export function Wquw151Frame387936059() { color="$caption" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -195,6 +203,7 @@ export function Wquw151Frame387936059() { color="$text" fontSize="20px" fontWeight="700" + letterSpacing="-0.02em" lineHeight="28px" typography="h6" wordBreak="keep-all" @@ -206,6 +215,7 @@ export function Wquw151Frame387936059() { color="$text" fontSize="17px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="29px" overflow="hidden" textAlign="center" @@ -228,6 +238,7 @@ export function Wquw151Frame387936059() { color="$textLight" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" overflow="hidden" textOverflow="ellipsis" @@ -250,6 +261,7 @@ export function Wquw151Frame387936059() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -270,6 +282,7 @@ export function Wquw151Frame387936059() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -287,6 +300,7 @@ export function Wquw151Frame387936059() { color="$textLight" fontSize="17px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="29px" typography="bodySmSemibold" wordBreak="keep-all" @@ -305,6 +319,7 @@ export function Wquw151Frame387936059() { color="$primary" fontSize="17px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="29px" typography="bodySmSemibold" wordBreak="keep-all" diff --git a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_36108.snap b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_36108.snap index ff9400aa..bbe01ccc 100644 --- a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_36108.snap +++ b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_36108.snap @@ -40,6 +40,7 @@ export function Wquw151Frame387936108() { color="$textLight" fontSize="15px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="24px" typography="captionSemibold" wordBreak="keep-all" @@ -60,6 +61,7 @@ export function Wquw151Frame387936108() { color="$text" fontSize="24px" fontWeight="700" + letterSpacing="-0.02em" lineHeight="34px" overflow="hidden" textOverflow="ellipsis" @@ -90,6 +92,7 @@ export function Wquw151Frame387936108() { color="$secondary" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -102,6 +105,7 @@ export function Wquw151Frame387936108() { flex="1" fontSize="18px" fontWeight="500" + letterSpacing="-0.02em" lineHeight="31px" overflow="hidden" textOverflow="ellipsis" @@ -117,6 +121,7 @@ export function Wquw151Frame387936108() { color="$textLight" fontSize="15px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="24px" typography="captionSemibold" wordBreak="keep-all" @@ -130,6 +135,7 @@ export function Wquw151Frame387936108() { color="$textLight" fontSize="15px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="24px" typography="captionSemibold" wordBreak="keep-all" @@ -143,6 +149,7 @@ export function Wquw151Frame387936108() { color="$textLight" fontSize="15px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="24px" typography="captionSemibold" wordBreak="keep-all" @@ -156,6 +163,7 @@ export function Wquw151Frame387936108() { color="$caption" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" diff --git a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_36144.snap b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_36144.snap index e2d928e8..69e99d0b 100644 --- a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_36144.snap +++ b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_36144.snap @@ -25,6 +25,7 @@ export function Wquw151Frame387936144() { color="$text" fontSize="28px" fontWeight="700" + letterSpacing="-0.02em" lineHeight="39px" overflow="hidden" textAlign="center" @@ -43,6 +44,7 @@ export function Wquw151Frame387936144() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -53,6 +55,7 @@ export function Wquw151Frame387936144() { color="$primary" fontSize="17px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="29px" typography="bodySmSemibold" wordBreak="keep-all" @@ -97,6 +100,7 @@ export function Wquw151Frame387936144() { color="$textLight" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" overflow="hidden" textAlign="center" @@ -116,6 +120,7 @@ export function Wquw151Frame387936144() { color="$textLight" fontSize="14px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="22px" overflow="hidden" textOverflow="ellipsis" @@ -137,6 +142,7 @@ export function Wquw151Frame387936144() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" > @@ -146,6 +152,7 @@ export function Wquw151Frame387936144() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" overflow="hidden" textOverflow="ellipsis" @@ -190,6 +197,7 @@ export function Wquw151Frame387936144() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -202,6 +210,7 @@ export function Wquw151Frame387936144() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -216,6 +225,7 @@ export function Wquw151Frame387936144() { flex="1" fontSize="22px" fontWeight="700" + letterSpacing="-0.02em" lineHeight="31px" overflow="hidden" textOverflow="ellipsis" @@ -239,6 +249,7 @@ export function Wquw151Frame387936144() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -251,6 +262,7 @@ export function Wquw151Frame387936144() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -264,6 +276,7 @@ export function Wquw151Frame387936144() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" overflow="hidden" textOverflow="ellipsis" @@ -288,6 +301,7 @@ export function Wquw151Frame387936144() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -300,6 +314,7 @@ export function Wquw151Frame387936144() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -313,6 +328,7 @@ export function Wquw151Frame387936144() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" overflow="hidden" textOverflow="ellipsis" @@ -345,6 +361,7 @@ export function Wquw151Frame387936144() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -357,6 +374,7 @@ export function Wquw151Frame387936144() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -370,6 +388,7 @@ export function Wquw151Frame387936144() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" overflow="hidden" textOverflow="ellipsis" @@ -394,6 +413,7 @@ export function Wquw151Frame387936144() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -406,6 +426,7 @@ export function Wquw151Frame387936144() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -419,6 +440,7 @@ export function Wquw151Frame387936144() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" overflow="hidden" textOverflow="ellipsis" @@ -455,6 +477,7 @@ export function Wquw151Frame387936144() { color="$text" fontSize="18px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="31px" typography="bodySemibold" wordBreak="keep-all" @@ -467,6 +490,7 @@ export function Wquw151Frame387936144() { color="#FFF" fontSize="18px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="31px" typography="bodySemibold" wordBreak="keep-all" diff --git a/crates/devup-mcp-devup-ui/tests/theme.rs b/crates/devup-mcp-devup-ui/tests/theme.rs index 9de192dc..92216874 100644 --- a/crates/devup-mcp-devup-ui/tests/theme.rs +++ b/crates/devup-mcp-devup-ui/tests/theme.rs @@ -104,6 +104,10 @@ fn maps_variables_modes_aliases_and_styles_to_devup_json() { " \"darkMode\": {\n", " \"elevationCard\": \"0 4px 12px 0 #0000001a\"\n", " }\n", + " },\n", + " \"colorScheme\": {\n", + " \"light\": \"light\",\n", + " \"darkMode\": \"dark\"\n", " }\n", " }\n", "}\n" diff --git a/crates/devup-mcp-devup-ui/tests/theme_modes.rs b/crates/devup-mcp-devup-ui/tests/theme_modes.rs new file mode 100644 index 00000000..b846403d --- /dev/null +++ b/crates/devup-mcp-devup-ui/tests/theme_modes.rs @@ -0,0 +1,129 @@ +use devup_mcp_devup_ui::theme::{ThemeScope, VariableSnapshot, generate_devup_json}; +use serde_json::{Value, json}; + +fn incident() -> VariableSnapshot { + serde_json::from_value(json!({ + "collections": [ + {"id":"primitives","name":"Primitives","defaultModeId":"value","modes":[{"modeId":"value","name":"value"}]}, + {"id":"leftover","name":"Leftover","defaultModeId":"one","modes":[{"modeId":"one","name":"Mode 1"}]}, + {"id":"site","name":"Site","defaultModeId":"light","modes":[{"modeId":"light","name":"Light"},{"modeId":"dark","name":"Dark"}]}, + {"id":"sds","name":"Simple Design System","remote":true,"defaultModeId":"sl","modes":[{"modeId":"sl","name":"SDS Light"},{"modeId":"sd","name":"SDS Dark"}]}, + {"id":"empty","name":"Empty","defaultModeId":"empty","modes":[{"modeId":"empty","name":"Empty"}]} + ], + "variables": [ + {"id":"primitive","name":"white","resolvedType":"COLOR","variableCollectionId":"primitives","valuesByMode":{"value":{"r":1,"g":1,"b":1}}}, + {"id":"leftover","name":"accent","resolvedType":"COLOR","variableCollectionId":"leftover","valuesByMode":{"one":{"r":0,"g":0,"b":1}}}, + {"id":"bg","name":"background","resolvedType":"COLOR","variableCollectionId":"site","valuesByMode":{"light":{"type":"VARIABLE_ALIAS","id":"primitive"},"dark":{"r":0,"g":0,"b":0}}} + ], + "usedRemoteVariables":[ + {"id":"sds-icon","name":"sdsIcon","resolvedType":"COLOR","variableCollectionId":"sds","valuesByMode":{"sl":{"r":1,"g":1,"b":1},"sd":{"r":0,"g":0,"b":0}}} + ], + "usedVariableIds":["bg"],"styles":[],"localComplete":true,"usedRemoteComplete":true + })).unwrap() +} + +#[test] +fn incident_exports_only_site_themes_with_drop_reasons() { + let output = generate_devup_json(&incident(), ThemeScope::File).unwrap(); + let theme: Value = serde_json::from_str(&output.json).unwrap(); + assert_eq!( + theme["theme"]["colors"] + .as_object() + .unwrap() + .keys() + .collect::>(), + ["light", "dark"] + ); + assert_eq!(theme["theme"]["colors"]["light"]["accent"], "#0000ff"); + assert_eq!(theme["theme"]["colors"]["light"]["white"], "#ffffff"); + let output = serde_json::to_value(output).unwrap(); + let dropped = output["themeModesDropped"].as_array().unwrap(); + assert!( + dropped.iter().any(|mode| mode["collectionId"] == "sds" + && mode["reason"] == "unreferenced-remote-collection") + ); + assert!( + dropped + .iter() + .any(|mode| mode["collectionId"] == "empty" + && mode["reason"] == "no-defined-color-values") + ); + assert!( + dropped + .iter() + .any(|mode| mode["modeName"] == "Mode 1" && mode["reason"] == "single-default-mode") + ); +} + +#[test] +fn referenced_library_modes_merge_by_meaning() { + let mut snapshot = incident(); + snapshot.used_variable_ids.push("sds-icon".into()); + let output = generate_devup_json(&snapshot, ThemeScope::File).unwrap(); + let theme: Value = serde_json::from_str(&output.json).unwrap(); + assert_eq!( + theme["theme"]["colors"] + .as_object() + .unwrap() + .keys() + .collect::>(), + ["light", "dark"] + ); + assert_eq!(theme["theme"]["colors"]["dark"]["sdsIcon"], "#000000"); +} + +#[test] +fn used_only_drops_unused_modes_and_missing_only_preserves_existing_values() { + use devup_mcp_devup_ui::theme::{ + TokenUsage, generate_used_devup_json, missing_token_diff, parse_project_theme, + }; + let mut usage = TokenUsage::default(); + usage.add_tsx(r#""#).unwrap(); + let output = generate_used_devup_json(&incident(), &usage).unwrap(); + let theme: Value = serde_json::from_str(&output.json).unwrap(); + assert_eq!( + theme["theme"]["colors"], + json!({"light":{"background":"#ffffff"},"dark":{"background":"#000000"}}) + ); + assert_eq!( + theme["theme"]["colorScheme"], + json!({"light":"light","dark":"dark"}) + ); + let existing = + parse_project_theme(r##"{"theme":{"colors":{"light":{"background":"#123456"}}}}"##) + .unwrap(); + let diff = missing_token_diff(&output.json, &usage, &existing).unwrap(); + assert!( + diff["additions"]["theme"]["colors"]["light"] + .as_object() + .unwrap() + .is_empty() + ); + assert_eq!(existing.colors["light"]["background"], "#123456"); +} + +#[test] +fn incompatible_same_meaning_tokens_report_conflict() { + let mut snapshot = incident(); + snapshot.used_variable_ids.push("sds-icon".into()); + snapshot.used_remote_variables[0].name = "background".into(); + snapshot.used_remote_variables[0] + .values_by_mode + .insert("sd".into(), json!({"r":1,"g":0,"b":0})); + let output = generate_devup_json(&snapshot, ThemeScope::File).unwrap(); + assert!( + output + .conflicts + .iter() + .any(|conflict| conflict.mode == "dark" && conflict.token == "background") + ); +} + +#[test] +fn custom_dark_mode_has_explicit_color_scheme() { + let mut snapshot = incident(); + snapshot.collections[2].modes[1].name = "Dark Brand".into(); + let output = generate_devup_json(&snapshot, ThemeScope::File).unwrap(); + let theme: Value = serde_json::from_str(&output.json).unwrap(); + assert_eq!(theme["theme"]["colorScheme"]["darkBrand"], "dark"); +} diff --git a/crates/devup-mcp/src/server/handlers/devup_ui_validate.rs b/crates/devup-mcp/src/server/handlers/devup_ui_validate.rs index b796a52a..244e34c9 100644 --- a/crates/devup-mcp/src/server/handlers/devup_ui_validate.rs +++ b/crates/devup-mcp/src/server/handlers/devup_ui_validate.rs @@ -1,4 +1,7 @@ use super::*; +use crate::server::tools::UiValidateDetail; +#[path = "ui_validate_sources.rs"] +mod sources; #[tool_router(router = devup_ui_validate_router, vis = "pub(super)")] impl DevupServer { @@ -8,10 +11,18 @@ impl DevupServer { #[doc = include_str!("../text/devup-ui-validate.md")] pub(super) async fn devup_ui_validate( &self, - Parameters(input): Parameters, + Parameters(mut input): Parameters, ) -> Result { use devup_mcp_devup_ui::ui_validate::{bundle_theme, check_bundle, validate_bundle}; let invalid = |message: String| ErrorData::invalid_params(message, None); + let paths_mode = !input.paths.is_empty(); + if paths_mode { + if !input.tsx.is_empty() || input.files.is_some() { + return Err(invalid("Supply paths or tsx/files, not both".into())); + } + let root = input.project_root.as_deref().ok_or_else(|| invalid("paths requires projectRoot".into()))?; + input.files = Some(sources::read_paths(root, &input.paths).map_err(invalid)?); + } // Neither source supplied. This used to validate the empty string and // answer `ok: true, okReason: "clean"` — which reads as "this code // passed" about code that was never sent. There is no verdict to give @@ -23,7 +34,9 @@ impl DevupServer { .into(), )); } - let theme_lookup = if let Some(files) = &input.files { + let theme_lookup = if paths_mode { + project_context::theme_for_validation(input.project_root.as_deref()).map_err(to_mcp_error)? + } else if let Some(files) = &input.files { check_bundle(files).map_err(invalid)?; if !input.tsx.is_empty() { return Err(invalid("Supply either tsx or files, not both".into())); @@ -37,7 +50,9 @@ impl DevupServer { project_context::theme_for_validation(input.project_root.as_deref()) .map_err(to_mcp_error)? }; - let mut report = if let Some(files) = &input.files { + let mut report = if paths_mode { + devup_mcp_devup_ui::ui_validate::validate_sources(input.files.as_deref().unwrap_or_default(), theme_lookup.theme.as_ref(), input.strict) + } else if let Some(files) = &input.files { validate_bundle(files, theme_lookup.theme.as_ref(), input.strict).map_err(invalid)? } else { devup_mcp_devup_ui::ui_validate::validate_devup_ui_tsx( @@ -46,6 +61,19 @@ impl DevupServer { input.strict, ) }; + if paths_mode + && let Some(root) = input.project_root.as_deref() + && std::path::Path::new(root).join("src").is_dir() + { + let project_sources = sources::read_paths(root, &["src".into()]).map_err(invalid)?; + if project_sources.iter().any(|file| devup_mcp_devup_ui::ui_validate::visual_behavior::has_global_link_decoration(&file.content)) { + report.violations.retain(|finding| finding.rule != "link-underline"); + report.ok = report.violations.iter().all(|finding| { + finding.severity != devup_mcp_devup_ui::ui_validate::Severity::Error + && !(input.strict && finding.severity == devup_mcp_devup_ui::ui_validate::Severity::Warning) + }); + } + } // Site completeness, for code checked against a project: feedback on // every interactive element (bundle mode checks it per file), and an // icon for the app. @@ -223,6 +251,8 @@ impl DevupServer { "install": skill.install_action(&lookup), }); } + if paths_mode { result["pathsRead"] = json!(sources.len()); } + if matches!(input.detail, UiValidateDetail::Summary) { sources::summarize(&mut result, &sources); } Ok(tool_result(result)) } } diff --git a/crates/devup-mcp/src/server/handlers/ui_validate_sources.rs b/crates/devup-mcp/src/server/handlers/ui_validate_sources.rs new file mode 100644 index 00000000..4b93a826 --- /dev/null +++ b/crates/devup-mcp/src/server/handlers/ui_validate_sources.rs @@ -0,0 +1,94 @@ +use devup_mcp_devup_ui::ui_validate::SourceFile; +use std::{collections::BTreeSet, io::Read}; + +pub(super) fn read_paths(root: &str, paths: &[String]) -> Result, String> { + if paths.len() > 512 { return Err("paths exceeds 512 entries".into()); } + let root = std::fs::canonicalize(root).map_err(|error| format!("projectRoot: {error}"))?; + let mut pending = paths.iter().map(|path| root.join(path)).collect::>(); + let mut visited = BTreeSet::new(); + let mut files = Vec::new(); + let mut total: usize = 0; + while let Some(path) = pending.pop() { + let path = std::fs::canonicalize(&path) + .map_err(|error| format!("{}: {error}", path.display()))?; + if !path.starts_with(&root) { + return Err(format!("paths must stay inside projectRoot: {}", path.display())); + } + if !visited.insert(path.clone()) { + continue; + } + if path.is_dir() { + for entry in std::fs::read_dir(&path).map_err(|error| error.to_string())? { + let entry = entry.map_err(|error| error.to_string())?; + let name = entry.file_name(); + let name = name.to_string_lossy(); + if name.starts_with('.') || ["node_modules", "dist", "build", "target", "df"].contains(&name.as_ref()) { + continue; + } + pending.push(entry.path()); + } + } else if matches!(path.extension().and_then(|value| value.to_str()), Some("tsx" | "ts" | "jsx" | "js")) { + if files.len() == 512 { + return Err("paths exceeds 512 source files".into()); + } + let file = std::fs::File::open(&path).map_err(|error| error.to_string())?; + let mut bytes = Vec::new(); + file.take(u64::try_from(8 * 1024 * 1024 - total + 1).map_err(|error| error.to_string())?) + .read_to_end(&mut bytes).map_err(|error| error.to_string())?; + total += bytes.len(); + if total > 8 * 1024 * 1024 { + return Err("paths exceeds 8 MiB total read bytes".into()); + } + let relative = path.strip_prefix(&root).map_err(|error| error.to_string())?; + files.push(SourceFile { + path: relative.to_string_lossy().replace('\\', "/"), + content: String::from_utf8(bytes).map_err(|error| format!("{}: {error}", path.display()))?, + }); + } + } + files.sort_by(|left, right| left.path.cmp(&right.path)); + if files.is_empty() { + return Err("paths contains no .tsx, .ts, .jsx or .js sources".into()); + } + Ok(files) +} + +pub(super) fn summarize(result: &mut serde_json::Value, sources: &[( &str, &str)]) { + use serde_json::{Value, json}; + let mut rows: Vec = Vec::new(); + let mut groups = std::collections::BTreeMap::new(); + for finding in result["violations"].as_array().into_iter().flatten() { + let name = finding.get("sourceName").or_else(|| finding.get("path")).or_else(|| finding["context"].get("path")) + .or_else(|| finding["context"].get("sourceName")); + let key = (name.cloned().unwrap_or(Value::Null).to_string(), finding["rule"].to_string(), finding["message"].to_string()); + if let Some(&index) = groups.get(&key) { + let row: &mut Value = &mut rows[index]; + row["count"] = json!(row["count"].as_u64().unwrap_or(1) + 1); + continue; + } + let mut row = json!({"rule":finding["rule"],"severity":finding["severity"],"message":finding["message"]}); + if let Some(name) = name { row["sourceName"] = name.clone(); } + let source = sources.iter().find(|(path, _)| name.and_then(Value::as_str).is_none_or(|name| name == *path)); + if let Some(line) = finding.get("line").or_else(|| finding["context"].get("line")) { + row["line"] = line.clone(); + } else if let Some((_, source)) = source + && let Some(start) = finding["byteRange"][0].as_u64().and_then(|value| usize::try_from(value).ok()) + && let Some(prefix) = source.get(..start) { + row["line"] = json!(prefix.bytes().filter(|byte| *byte == b'\n').count() + 1); + } + if let Some(suggestion) = finding.get("suggestion").filter(|value| !value.is_null()) { row["suggestion"] = suggestion.clone(); } + groups.insert(key, rows.len()); + rows.push(row); + } + let total = rows.len(); + rows.truncate(100); + let risk_count = result["layoutRisks"].as_array().map_or(0, Vec::len); + if let Some(object) = result.as_object_mut() { + object.retain(|key, value| matches!(key.as_str(), "ok" | "okReason" | "strict" | "violationCounts" | "themeAvailable" | "tokens" | "nextAction" | "recoveryState" | "recoveryReason" | "nextActionReason" | "skillGap" | "themeNotes" | "pathsRead") || (key == "themeGuardrail" && !value.is_null())); + } + result["violations"] = json!(rows); + result["layoutRiskCount"] = json!(risk_count); + result["detail"] = json!("summary"); + result["full"] = json!({"how":"repeat with detail: \"full\""}); + if total > 100 { result["violationsTruncated"] = json!({"shown":100,"total":total}); } +} diff --git a/crates/devup-mcp/src/server/text/devup-ui-validate.md b/crates/devup-mcp/src/server/text/devup-ui-validate.md index d777e4d2..2770cc3d 100644 --- a/crates/devup-mcp/src/server/text/devup-ui-validate.md +++ b/crates/devup-mcp/src/server/text/devup-ui-validate.md @@ -1 +1,4 @@ -Validate DevupUI TSX against a project's real devup.json: unknown $token references, hardcoded colors/lengths, unknown props on Box/Flex/Text/Center/Grid/Image, and non-static values inside css()/globalCss()/keyframes() calls. `ok` is decided by severity, not by count: errors fail, and `strict: true` also fails warnings. Hardcoded values with exact matching tokens are warnings with token advice; unmatched values are info without token advice and never fail strict mode. `okReason` distinguishes clean, clean-unverified-tokens, info-only, warnings-only, warnings-and-info, strict-warnings, and error-violations; `clean-unverified-tokens` means no violation was found but no devup.json was resolved, so the $token references this code makes were never checked. Supplying neither tsx nor files is rejected rather than answered `clean`. Structural rules (file-placement, one-component-per-file, client-boundary, inline-style, react-query-states) run only in `files` bundle mode; framework-required default exports (page, layout, loading, error, not-found, template, and the metadata conventions) are exempt from file-placement. With projectRoot, a maskImage whose public SVG paints more than one colour warns `multicolor-mask` (use ), and a root-relative asset URL (src or url(...)) with no file under any public directory is an error (`asset-missing`), always failing `ok`. `themeNotes` summarizes each encountered empty token category once. Site completeness: an interactive element (as=a/button, Link, button, onClick) with no _hover or _focusVisible is a warning (interactive-states), one with no focus style is a warning (focus-style), too much motion - over 400ms, scale above 1.05, infinite - is a warning (too-much-motion), an icon-only button without aria-label is a warning (icon-button-label); a button with no _active, a _hover with no transition, motion with no _motionReduce and a nav link with no aria-current are info (button-active, hover-transition, motion-reduce, nav-current); a page or layout with no metadata export is a warning (page-metadata, files mode), and with projectRoot an app with no icon or favicon is a warning (app-icon). Navigation rules also report unmapped action candidates/accessibility warnings, missing static App Router href errors when projectRoot inventory is complete, and literal active-state info. Dynamic hrefs/custom routing and incomplete bundles remain unverified. Optional renderedHtml reports captured anchor counts separately from source coverage; neither proves runtime behavior. `checkedTokens` counts references and `availableTokenCount` counts definitions; they are not a ratio. Supply sectionCoverage from outputResults.tsx.sectionCoverage (or frames[].outputResults.tsx.sectionCoverage) to report omitted sections, images, masks and backgrounds as section-coverage errors. This compares literal JSX node identity and asset references across the supplied sources; it does not prove runtime visibility or component use. Omitted sectionCoverage leaves design coverage unverified. \ No newline at end of file +Validate DevupUI TSX against a project's real devup.json: unknown $token references, hardcoded colors/lengths, unknown props on Box/Flex/Text/Center/Grid/Image, and non-static values inside css()/globalCss()/keyframes() calls. `ok` is decided by severity, not by count: errors fail, and `strict: true` also fails warnings. Hardcoded values with exact matching tokens are warnings with token advice; unmatched values are info without token advice and never fail strict mode. `okReason` distinguishes clean, clean-unverified-tokens, info-only, warnings-only, warnings-and-info, strict-warnings, and error-violations; `clean-unverified-tokens` means no violation was found but no devup.json was resolved, so the $token references this code makes were never checked. Supplying neither tsx nor files is rejected rather than answered `clean`. Structural rules (file-placement, one-component-per-file, client-boundary, inline-style, react-query-states) run only in `files` bundle mode; framework-required default exports (page, layout, loading, error, not-found, template, and the metadata conventions) are exempt from file-placement. With projectRoot, a maskImage whose public SVG paints more than one colour warns `multicolor-mask` (use ), and a root-relative asset URL (src or url(...)) with no file under any public directory is an error (`asset-missing`), always failing `ok`. `themeNotes` summarizes each encountered empty token category once. Site completeness: an interactive element (as=a/button, Link, button, onClick) with no _hover or _focusVisible is a warning (interactive-states), one with no focus style is a warning (focus-style), too much motion - over 400ms, scale above 1.05, infinite - is a warning (too-much-motion), an icon-only button without aria-label is a warning (icon-button-label); a button with no _active, a _hover with no transition, motion with no _motionReduce and a nav link with no aria-current are info (button-active, hover-transition, motion-reduce, nav-current); a page or layout with no metadata export is a warning (page-metadata, files mode), and with projectRoot an app with no icon or favicon is a warning (app-icon). Navigation rules also report unmapped action candidates/accessibility warnings, missing static App Router href errors when projectRoot inventory is complete, and literal active-state info. Dynamic hrefs/custom routing and incomplete bundles remain unverified. Optional renderedHtml reports captured anchor counts separately from source coverage; neither proves runtime behavior. `checkedTokens` counts references and `availableTokenCount` counts definitions; they are not a ratio. Supply sectionCoverage from outputResults.tsx.sectionCoverage (or frames[].outputResults.tsx.sectionCoverage) to report omitted sections, images, masks and backgrounds as section-coverage errors. This compares literal JSX node identity and asset references across the supplied sources; it does not prove runtime visibility or component use. Omitted sectionCoverage leaves design coverage unverified. +Default detail is summary: compact findings merged by source/rule/message (count for repeats), at most 100 rows, severity counts and layoutRiskCount. Use detail:"full" for all diagnostics including navigation and verificationScope. paths accepts files or recursive directories inside projectRoot, exclusive with tsx/files, up to 512 source files and 8 MiB; it runs bundle structural checks with the disk theme and reports pathsRead. Directory reads skip generated and hidden directories. Supplied files retain their 64-file/1 MiB limits and never read their theme from disk. + +`link-underline` warns when a native anchor, next/link (including import aliases), or polymorphic link has no explicit textDecoration. Set textDecoration="none" on the link or globalCss({ a: { textDecoration: 'none', color: 'inherit' } }); reset-css does not remove anchor underlines. Parsed globalCss anchor rules in the supplied bundle suppress this warning; paths mode also checks projectRoot/src within the source read limits. Hover-only decoration does not reset the resting state. `marquee-static` is informational: long single-line JSX text with at least three repeated separators and no keyframes/animation in the same file suggests a static scrolling strip. This is heuristic, not proof of animation intent or runtime behavior. diff --git a/crates/devup-mcp/tests/ui_validate_response.rs b/crates/devup-mcp/tests/ui_validate_response.rs index cc11f7b8..d1a0b81d 100644 --- a/crates/devup-mcp/tests/ui_validate_response.rs +++ b/crates/devup-mcp/tests/ui_validate_response.rs @@ -13,3 +13,5 @@ include!("ui_validate_response/fixtures_and_cases.rs"); include!("ui_validate_response/a_page_named_module_outside_the_router_is_not_judged_as_a_route.rs"); include!("ui_validate_response/section_coverage.rs"); +include!("ui_validate_response/summary_and_paths.rs"); +include!("ui_validate_response/effective_attributes.rs"); diff --git a/crates/devup-mcp/tests/ui_validate_response/effective_attributes.rs b/crates/devup-mcp/tests/ui_validate_response/effective_attributes.rs new file mode 100644 index 00000000..51a1bde8 --- /dev/null +++ b/crates/devup-mcp/tests/ui_validate_response/effective_attributes.rs @@ -0,0 +1,60 @@ +#[tokio::test] +async fn effective_props_wire_native_handlers_and_navigation() -> anyhow::Result<()> { + let output = validate(json!({"tsx": r#"import RouteLink from 'next/link'; const Header = () => "#})).await?; + assert!(!has_rule(&output, "interaction-unmapped"), "{output}"); + assert!(!has_rule(&output, "hover-transition"), "{output}"); + assert_eq!(output["navigation"]["sources"][0]["report"]["coverage"]["wiredCount"], 3); + Ok(()) +} + +#[tokio::test] +async fn sort_findings_produce_one_action_across_bundle_files() -> anyhow::Result<()> { + let output = validate(json!({"files": [ + {"path": "one.tsx", "content": ""}, + {"path": "two.tsx", "content": ""} + ]})).await?; + assert_eq!(output["nextAction"]["fixable"], 2); + assert_eq!(output["nextAction"]["command"], "bun run lint:fix"); + Ok(()) +} + +#[tokio::test] +async fn chondrogen_header_props_preserve_navigation_and_hover_states() -> anyhow::Result<()> { + let source = r#" + import Link from 'next/link'; + export function Header() { + return ; + } + "#; + let output = structural(source, "src/components/layout/Header.tsx").await?; + assert!(!has_rule(&output, "nav-current"), "{output}"); + assert!(!has_rule(&output, "hover-transition"), "{output}"); + let missing_states = source + .replace("'aria-current': isActive('/x') ? 'page' : undefined", "") + .replace("transition=\"color 0.2s ease\"", ""); + let output = structural(&missing_states, "src/components/layout/Header.tsx").await?; + assert!(has_rule(&output, "nav-current"), "{output}"); + assert!(has_rule(&output, "hover-transition"), "{output}"); + Ok(()) +} + +#[tokio::test] +async fn chondrogen_header_desktop_nav_has_no_narrow_fixed_columns() -> anyhow::Result<()> { + for display in ["['none', null, null, null, 'flex']", "'flex'"] { + let output = validate(json!({"tsx": format!(r#" + + CompanyResearch + + "#)})).await?; + let risks = output["layoutRisks"].as_array().unwrap(); + assert_eq!( + risks.iter().any(|risk| risk["rule"] == "narrow-fixed-columns"), + display == "'flex'", + "{output}" + ); + } + Ok(()) +} diff --git a/crates/devup-mcp/tests/ui_validate_response/fixtures_and_cases.rs b/crates/devup-mcp/tests/ui_validate_response/fixtures_and_cases.rs index a2c7f28a..854bcd3a 100644 --- a/crates/devup-mcp/tests/ui_validate_response/fixtures_and_cases.rs +++ b/crates/devup-mcp/tests/ui_validate_response/fixtures_and_cases.rs @@ -55,7 +55,8 @@ async fn validate(arguments: Value) -> anyhow::Result { anyhow::Ok(()) }); let client = ().serve(client_transport).await?; - let arguments: Map = arguments.as_object().cloned().unwrap(); + let mut arguments: Map = arguments.as_object().cloned().unwrap(); + arguments.entry("detail").or_insert(json!("full")); let result = client .call_tool(CallToolRequestParams::new("devup_ui_validate").with_arguments(arguments)) .await?; @@ -69,17 +70,6 @@ async fn validate(arguments: Value) -> anyhow::Result { Ok(result.structured_content.unwrap()) } -#[tokio::test] -async fn sort_findings_produce_one_action_across_bundle_files() -> anyhow::Result<()> { - let output = validate(json!({"files": [ - {"path": "one.tsx", "content": ""}, - {"path": "two.tsx", "content": ""} - ]})).await?; - assert_eq!(output["nextAction"]["fixable"], 2); - assert_eq!(output["nextAction"]["command"], "bun run lint:fix"); - Ok(()) -} - #[tokio::test] async fn r12_failed_validation_explains_why_source_cannot_be_corrected_automatically() -> anyhow::Result<()> { diff --git a/crates/devup-mcp/tests/ui_validate_response/summary_and_paths.rs b/crates/devup-mcp/tests/ui_validate_response/summary_and_paths.rs new file mode 100644 index 00000000..04ae6e03 --- /dev/null +++ b/crates/devup-mcp/tests/ui_validate_response/summary_and_paths.rs @@ -0,0 +1,49 @@ +#[tokio::test] +async fn summary_compacts_and_merges_findings() -> anyhow::Result<()> { + let output = validate(json!({"detail":"summary","tsx":"<>"})).await?; + assert_eq!(output["detail"], "summary"); + assert!(output.get("navigation").is_none()); + assert!(output.get("verificationScope").is_none()); + assert!(output.get("checkedTokens").is_none()); + assert!(output["violationCounts"]["error"].as_u64().unwrap() >= 2); + let rows = output["violations"].as_array().unwrap(); + assert!(rows.iter().any(|row| row["count"] == 2)); + assert!(rows.iter().all(|row| row.get("context").is_none() && row.get("byteRange").is_none())); + Ok(()) +} + +#[tokio::test] +async fn summary_caps_distinct_findings_without_losing_error_counts() -> anyhow::Result<()> { + let elements = (0..105).map(|index| format!("")).collect::(); + let output = validate(json!({"detail":"summary","tsx":format!("<>\n{elements}")})).await?; + assert_eq!(output["violations"].as_array().unwrap().len(), 100); + assert_eq!(output["violationsTruncated"], json!({"shown":100,"total":105})); + assert_eq!(output["violationCounts"]["error"], 105); + assert!(output["violations"].as_array().unwrap().iter().all(|row| row["line"] == 2)); + Ok(()) +} + +#[tokio::test] +async fn paths_expands_sources_and_uses_disk_theme() -> anyhow::Result<()> { + let root = std::env::temp_dir().join(format!("devup-validation-paths-{}", rand::random::())); + std::fs::create_dir_all(root.join("src/app"))?; + std::fs::create_dir_all(root.join("src/components"))?; + std::fs::create_dir_all(root.join("src/node_modules"))?; + std::fs::create_dir_all(root.join("src/.next"))?; + std::fs::write(root.join("devup.json"), r##"{"theme":{"colors":{"default":{"bg":"#ffffff"}}}}"##)?; + std::fs::write(root.join("src/app/page.tsx"), "export default function HomePage() { return }")?; + std::fs::write(root.join("src/components/Card.tsx"), "export function Card() { return }")?; + std::fs::write(root.join("src/node_modules/ignored.tsx"), "invalid")?; + std::fs::write(root.join("src/.next/ignored.tsx"), "invalid")?; + for detail in ["summary", "full"] { + let output = validate(json!({"paths":["src"],"projectRoot":root,"detail":detail})).await?; + assert_eq!(output["pathsRead"], 2); + assert_eq!(output["themeAvailable"], true); + assert!(!output["violations"].as_array().unwrap().iter().any(|row| row["rule"] == "unknown-token")); + assert!(output["violations"].as_array().unwrap().iter().any(|row| row["sourceName"] == "src/components/Card.tsx" || row["path"] == "src/components/Card.tsx")); + } + assert!(validate(json!({"paths":["src"],"tsx":"","projectRoot":root})).await.is_err()); + assert!(validate(json!({"paths":[root.parent().unwrap()],"projectRoot":root})).await.is_err()); + std::fs::remove_dir_all(root)?; + Ok(()) +} From 15cf0db22dc49b33aee44d72ecb5d77bf1ab53a9 Mon Sep 17 00:00:00 2001 From: leader Date: Wed, 28 Oct 2026 01:32:15 +0900 Subject: [PATCH 139/149] feat(screen-plan,audit): component widths, responsive contract, dynamic URLs and settled scroll reveals --- .../changepack_log_8kyvms1No9ruHAr5hCy8h.json | 7 + crates/devup-mcp/src/server/audit_plan.rs | 3 + .../src/server/audit_plan/component_width.rs | 45 ++ .../src/server/audit_plan/dynamic_urls.rs | 78 +++ .../src/server/audit_plan/plan_document.rs | 89 ++++ .../server/audit_plan/planning_and_capture.rs | 221 ++++---- .../devup-mcp/src/server/audit_plan_tests.rs | 471 ++++++++---------- .../audit_plan_tests_routes_and_drift.rs | 267 ++++++++++ .../src/server/handlers/devup_screen_plan.rs | 3 +- .../mod_parts/component_width_acquisition.rs | 85 ++++ crates/devup-mcp/src/server/runtime_audit.rs | 2 + .../runtime_audit/capture_comparison.rs | 16 +- .../runtime_audit/inputs_and_capture.rs | 161 ++---- .../src/server/runtime_audit/plan_columns.rs | 127 +++++ .../runtime_audit/screenshot_integrity.rs | 197 ++++++++ .../src/server/runtime_audit/tests.rs | 86 +++- .../src/server/runtime_audit/write_plan.rs | 314 ++++-------- crates/devup-mcp/src/server/screen_plan.rs | 3 + .../src/server/screen_plan/actions.rs | 11 +- .../src/server/screen_plan/audit_base_url.rs | 38 ++ .../src/server/screen_plan/component_width.rs | 84 ++++ .../devup-mcp/src/server/screen_plan/draft.rs | 2 + .../src/server/screen_plan/draft_plan_with.rs | 2 + .../server/screen_plan/responsive_contract.rs | 58 +++ .../devup-mcp/src/server/screen_plan/state.rs | 3 + .../src/server/screen_plan/status.rs | 17 +- .../devup-mcp/src/server/screen_plan_tests.rs | 2 + .../screen_plan_tests/component_width.rs | 79 +++ .../screen_plan_tests/responsive_contract.rs | 73 +++ .../text/audit_plan-url_plan-a1031a26.md | 2 +- .../src/server/text/devup-screen-plan.md | 3 + .../server/text/runtime_audit-page-height.md | 1 + ...untime_audit-visual_comparison-8010a061.md | 2 +- harness/runtime-audit/README.md | 16 + harness/runtime-audit/run.mjs | 122 ++++- harness/runtime-audit/test/browser.test.mjs | 58 +++ harness/runtime-audit/test/layout.test.mjs | 61 +++ 37 files changed, 2049 insertions(+), 760 deletions(-) create mode 100644 .changepacks/changepack_log_8kyvms1No9ruHAr5hCy8h.json create mode 100644 crates/devup-mcp/src/server/audit_plan/component_width.rs create mode 100644 crates/devup-mcp/src/server/audit_plan/dynamic_urls.rs create mode 100644 crates/devup-mcp/src/server/audit_plan/plan_document.rs create mode 100644 crates/devup-mcp/src/server/audit_plan_tests_routes_and_drift.rs create mode 100644 crates/devup-mcp/src/server/mod_parts/component_width_acquisition.rs create mode 100644 crates/devup-mcp/src/server/runtime_audit/plan_columns.rs create mode 100644 crates/devup-mcp/src/server/runtime_audit/screenshot_integrity.rs create mode 100644 crates/devup-mcp/src/server/screen_plan/audit_base_url.rs create mode 100644 crates/devup-mcp/src/server/screen_plan/component_width.rs create mode 100644 crates/devup-mcp/src/server/screen_plan/responsive_contract.rs create mode 100644 crates/devup-mcp/src/server/screen_plan_tests/component_width.rs create mode 100644 crates/devup-mcp/src/server/screen_plan_tests/responsive_contract.rs create mode 100644 crates/devup-mcp/src/server/text/runtime_audit-page-height.md diff --git a/.changepacks/changepack_log_8kyvms1No9ruHAr5hCy8h.json b/.changepacks/changepack_log_8kyvms1No9ruHAr5hCy8h.json new file mode 100644 index 00000000..91030d13 --- /dev/null +++ b/.changepacks/changepack_log_8kyvms1No9ruHAr5hCy8h.json @@ -0,0 +1,7 @@ +{ + "changes": { + "Cargo.toml": "Minor" + }, + "note": "Report instance overrides that shared component properties cannot preserve, with generated prop evidence and required export review.", + "date": "2026-10-27T12:59:41.181270100Z" +} \ No newline at end of file diff --git a/crates/devup-mcp/src/server/audit_plan.rs b/crates/devup-mcp/src/server/audit_plan.rs index 04f980bf..2b559e95 100644 --- a/crates/devup-mcp/src/server/audit_plan.rs +++ b/crates/devup-mcp/src/server/audit_plan.rs @@ -12,3 +12,6 @@ include!("audit_plan/planning_and_capture.rs"); include!("audit_plan/static_params.rs"); include!("audit_plan/prerequisites.rs"); +include!("audit_plan/component_width.rs"); +include!("audit_plan/plan_document.rs"); +include!("audit_plan/dynamic_urls.rs"); diff --git a/crates/devup-mcp/src/server/audit_plan/component_width.rs b/crates/devup-mcp/src/server/audit_plan/component_width.rs new file mode 100644 index 00000000..9ad0fc47 --- /dev/null +++ b/crates/devup-mcp/src/server/audit_plan/component_width.rs @@ -0,0 +1,45 @@ +impl Sources { + fn effective(&self, frames: &[(u32, String, Option)]) -> (Self, Vec) { + let Some((_, first)) = self.payloads.first() else { + return (Self { payloads: Vec::new(), substitutions: Vec::new() }, Vec::new()); + }; + let mut snapshot = first.snapshot.clone(); + for (_, payload) in &self.payloads { + snapshot.nodes.extend(payload.snapshot.nodes.clone()); + } + let roots = snapshot.nodes.iter().filter(|(_, node)| node.node_type == "FRAME") + .filter(|(id, _)| !snapshot.nodes.values().any(|parent| parent.typed_view().child_ids().any(|child| child == id.as_str()))) + .map(|(id, _)| id.clone()).collect::>(); + let substitutions = if self.substitutions.is_empty() { + super::screen_plan::component_width_mismatches(&snapshot, &roots) + } else { self.substitutions.clone() }.into_iter().filter(|row| frames.iter().any(|(_,id,_)|row["screen"] == *id)).collect::>(); + let mut payloads = Vec::new(); + for (artifact, original) in &self.payloads { + let mut payload = original.as_ref().clone(); + for substitution in &substitutions { + let (Some(suspect), Some(correct), Some(screen)) = (substitution["nodeId"].as_str(),substitution["correctNodeId"].as_str(),substitution["screen"].as_str()) else { continue }; + if !payload.snapshot.nodes.contains_key(suspect) { continue; } + let Some(source) = self.payloads.iter().map(|(_,payload)|payload.as_ref()).find(|payload|payload.snapshot.nodes.contains_key(correct)) else { continue }; + let (Some(old_box),Some(new_box),Some(frame_box)) = (bounds(&payload,suspect),bounds(source,correct),bounds(&payload,screen)) else { continue }; + let mut stack = vec![correct.to_owned()]; + let mut seen = BTreeSet::new(); + while let Some(id) = stack.pop() { + if !seen.insert(id.clone()) { continue; } + let Some(node) = source.snapshot.nodes.get(&id) else { continue }; + stack.extend(node.typed_view().child_ids().map(str::to_owned)); + let mut replacement = node.clone(); + if let Some(position) = bounds(source,&id) { + replacement.fields.insert("absoluteBoundingBox".into(),json!({ + "x":frame_box.x + position.x - new_box.x,"y":old_box.y + position.y - new_box.y, + "width":if id == correct {frame_box.width} else {position.width},"height":position.height})); + } + let destination = if id == correct {suspect} else {&id}; + replacement.id = destination.into(); + payload.snapshot.nodes.insert(destination.into(),replacement); + } + } + payloads.push((artifact.clone(),Arc::new(payload))); + } + (Self {payloads,substitutions:substitutions.clone()},substitutions) + } +} diff --git a/crates/devup-mcp/src/server/audit_plan/dynamic_urls.rs b/crates/devup-mcp/src/server/audit_plan/dynamic_urls.rs new file mode 100644 index 00000000..7cf3eb3d --- /dev/null +++ b/crates/devup-mcp/src/server/audit_plan/dynamic_urls.rs @@ -0,0 +1,78 @@ +/// The concrete URLs a dynamic route is audited at, and where they came from. +pub(super) struct Dynamic { + pub urls: Vec, + pub source: &'static str, + pub why: String, + pub assumption: Option, +} + +/// `[slug]` -> ("slug", false); `[...slug]` and `[[...slug]]` -> ("slug", true). +fn params(pattern: &str) -> Vec<(String, bool)> { + pattern + .split('/') + .filter(|segment| segment.starts_with('[') && segment.ends_with(']')) + .map(|segment| { + let inner = segment.trim_matches(|c| c == '[' || c == ']'); + match inner.strip_prefix("...") { + Some(name) => (name.to_owned(), true), + None => (inner.to_owned(), false), + } + }) + .collect() +} + +fn fill(pattern: &str, values: &BTreeMap) -> Option { + let mut out = Vec::new(); + for segment in pattern.split('/') { + if segment.starts_with('[') && segment.ends_with(']') { + let inner = segment.trim_matches(|c| c == '[' || c == ']'); + out.push(values.get(inner.trim_start_matches("...")).cloned()?); + } else { + out.push(segment.to_owned()); + } + } + Some(out.join("/")) +} + +/// Every literal value of `key` in `text`: `key: 'a'`, `"key": "a"`, or +/// `key: ['a', 'b']` (joined with `/` for a catch-all). +pub(super) fn values_of(text: &str, key: &str) -> Vec { + let mut found = Vec::new(); + let bytes = text.as_bytes(); + let mut from = 0; + while let Some(at) = text[from..].find(key) { + let start = from + at; + from = start + key.len(); + let before = start.checked_sub(1).map(|i| bytes[i] as char); + if before.is_some_and(|c| c.is_alphanumeric() || c == '_' || c == '$' || c == '.') { + continue; + } + let mut rest = text[from..].trim_start_matches(['"', '\'']).trim_start(); + let Some(after) = rest.strip_prefix(':') else { + continue; + }; + rest = after.trim_start(); + let quoted = |rest: &str| -> Option<(String, usize)> { + let quote = rest + .chars() + .next() + .filter(|c| matches!(c, '"' | '\'' | '`'))?; + let end = rest[1..].find(quote)?; + Some((rest[1..=end].to_owned(), end + 2)) + }; + if let Some((value, _)) = quoted(rest) { + found.push(value); + } else if let Some(list) = rest.strip_prefix('[') { + let end = list.find(']').unwrap_or(list.len()); + let parts = list[..end] + .split(',') + .filter_map(|part| quoted(part.trim()).map(|(value, _)| value)) + .collect::>(); + if !parts.is_empty() { + found.push(parts.join("/")); + } + } + } + found.dedup(); + found +} diff --git a/crates/devup-mcp/src/server/audit_plan/plan_document.rs b/crates/devup-mcp/src/server/audit_plan/plan_document.rs new file mode 100644 index 00000000..9b65da64 --- /dev/null +++ b/crates/devup-mcp/src/server/audit_plan/plan_document.rs @@ -0,0 +1,89 @@ +fn write_component_substitutions( + plan_path: &Path, + policy: &OutputPolicy, + substitutions: &[Value], +) -> Result<(), DevupError> { + if !substitutions.is_empty() { + let bytes = std::fs::read(plan_path).map_err(|error| invalid(error.to_string()))?; + let mut document: Value = serde_json::from_slice(&bytes).map_err(|error| invalid(error.to_string()))?; + document["componentSubstitutions"] = json!(substitutions); + let target = policy.resolve(&plan_path.to_string_lossy())?; + let mut transaction = super::output::OutputTransaction::new(); + transaction.stage("componentSubstitutions", target, &serde_json::to_vec_pretty(&document).map_err(|error| invalid(error.to_string()))?)?; + transaction.commit()?; + } + Ok(()) +} + +const REFERENCES_DIR: &str = ".devup/runtime-audit/references"; + +/// Writes each frame's referencePng beside the audit plans, so a page +/// comparison can still run as `reference: {path}` after the in-memory +/// artifact has expired or the server restarted. +fn persist_references( + root: &str, + sources: &Sources, + policy: &OutputPolicy, +) -> Result, DevupError> { + use base64::Engine as _; + let mut transaction = super::output::OutputTransaction::new(); + let mut written = Vec::new(); + for (artifact_id, payload) in &sources.payloads { + let (Some(node_id), Some(reference)) = + (payload.target.node_id.as_deref(), payload.reference_png.as_ref()) + else { + continue; + }; + let Ok(bytes) = + base64::engine::general_purpose::STANDARD.decode(&reference.data_base64) + else { + continue; + }; + let path = Path::new(root) + .join(REFERENCES_DIR) + .join(format!("{}.png", slug(node_id))); + let target = policy.resolve(&path.to_string_lossy())?; + transaction.stage(format!("reference:{node_id}"), target, &bytes)?; + written.push(json!({"nodeId": node_id, "artifactId": artifact_id, + "path": path.display().to_string(), "sha256": reference.sha256, + "compareWith": {"reference": {"path": path.display().to_string()}}})); + } + if !written.is_empty() { + transaction.commit()?; + } + Ok(written) +} + +fn stored_references(root: &str) -> Vec { + let mut found = std::fs::read_dir(Path::new(root).join(REFERENCES_DIR)) + .into_iter() + .flatten() + .flatten() + .map(|entry| entry.path()) + .filter(|path| path.extension().is_some_and(|ext| ext == "png")) + .map(|path| path.display().to_string()) + .collect::>(); + found.sort(); + found +} + +fn local_imports(source: &str) -> Vec<(String, String)> { + let mut found = Vec::new(); + for line in source.lines() { + let line = line.trim(); + let Some(rest) = line.strip_prefix("import ") else { + continue; + }; + let Some((names, module)) = rest.rsplit_once(" from ") else { + continue; + }; + let module = module + .trim() + .trim_end_matches(';') + .trim_matches(['"', '\'']); + if module.starts_with("./") || module.starts_with("../") || module.starts_with("@/") { + found.push((names.to_owned(), module.to_owned())); + } + } + found +} diff --git a/crates/devup-mcp/src/server/audit_plan/planning_and_capture.rs b/crates/devup-mcp/src/server/audit_plan/planning_and_capture.rs index 9532de9c..a6f26544 100644 --- a/crates/devup-mcp/src/server/audit_plan/planning_and_capture.rs +++ b/crates/devup-mcp/src/server/audit_plan/planning_and_capture.rs @@ -74,6 +74,7 @@ impl VisualThresholds { /// its referencePng and the native renders of its sections and assets. pub(super) struct Sources { payloads: Vec<(String, Arc)>, + substitutions: Vec, } impl Sources { @@ -87,12 +88,19 @@ impl Sources { "issue": "missing or expired; export the frame again and pass the new artifactId"})), } } - (Self { payloads }, missing) + ( + Self { + payloads, + substitutions: Vec::new(), + }, + missing, + ) } #[cfg(test)] pub(super) fn from_payloads(payloads: Vec<(String, CollectedPayload)>) -> Self { Self { + substitutions: Vec::new(), payloads: payloads .into_iter() .map(|(id, payload)| (id, Arc::new(payload))) @@ -128,6 +136,12 @@ impl Sources { /// A native-node PNG of the node at scale 1, the only render an element /// capture at deviceScaleFactor 1 can be compared with pixel for pixel. fn native_render(&self, node_id: &str) -> Option<(&str, &str)> { + let node_id = self + .substitutions + .iter() + .find(|row| row["nodeId"] == node_id) + .and_then(|row| row["correctNodeId"].as_str()) + .unwrap_or(node_id); self.payloads.iter().find_map(|(id, payload)| { payload .assets @@ -251,6 +265,7 @@ pub(super) struct UrlPlan { pub plan: PlanInput, pub missing: Vec, pub notes: Vec, + pub substitutions: Vec, } pub(super) fn url_plan( @@ -259,6 +274,8 @@ pub(super) fn url_plan( sources: &Sources, thresholds: &VisualThresholds, ) -> Result { + let (effective, substitutions) = sources.effective(&target.frames); + let sources = &effective; let origin = url::Url::parse(base_url) .map_err(|_| invalid(format!("baseUrl {base_url} is not a URL.")))? .origin() @@ -307,15 +324,21 @@ pub(super) fn url_plan( // Health and containment at every width. let mut checks = vec![ - check(json!({"kind":"links-resolve", "label":"all connected URLs resolve", "plannedTargets":[target.url]})), - check(json!({"kind": "no-clipped-content", "label": "navigation and card content not cut off"})), + check( + json!({"kind":"links-resolve", "label":"all connected URLs resolve", "plannedTargets":[target.url]}), + ), + check( + json!({"kind": "no-clipped-content", "label": "navigation and card content not cut off"}), + ), check(json!({"kind": "sequence-order", "label": "numbered cards in reading order"})), check(json!({"kind": "section-layout", "label": "responsive sections and text columns"})), check(json!({"kind": "page-health", "label": "page health"})), check(json!({"kind": "styles-applied", "label": "page styles delivered"})), check(json!({"kind": "no-low-contrast-text", "label": "visible text contrast"})), check(json!({"kind": "image-loaded", "label": "images and backgrounds loaded"})), - check(json!({"kind": "native-appearance", "label": "controls drawn without native browser chrome"})), + check( + json!({"kind": "native-appearance", "label": "controls drawn without native browser chrome"}), + ), check(json!({"kind": "stale-resources", "label": "no stale or missing build resources"})), check(json!({"kind": "no-horizontal-overflow", "label": "no horizontal overflow"})), check(json!({"kind": "no-clipped-text", "label": "no clipped text"})), @@ -325,16 +348,26 @@ pub(super) fn url_plan( let mut frame_refs = Vec::new(); let assets_everywhere = sources.asset_nodes(); let mut nav_checked = false; - for (width, frame_id, _) in &target.frames { + for (width, frame_id, target_height) in &target.frames { + let frame_height = target_height + .or_else(|| { + sources + .frame(frame_id) + .and_then(|(_, payload)| bounds(payload, frame_id).map(|bounds| bounds.height)) + }) + .map(|height| height.round() as u32); let Some((artifact_id, payload)) = sources.frame(frame_id) else { checks.push(check(json!({"kind": "page-health", "label": "design reference unavailable", "widths": [width], "unavailableReason": "Design comparison inconclusive: frame artifact missing or expired; export the frame again."}))); missing.push(json!({"kind": "frame", "nodeId": frame_id, "width": width, "why": "No artifact passed holds this frame; section, asset and geometry checks for this width are missing.", "call": {"tool": "devup_figma_export", "arguments": {"url": format!(""), "outputs": ["tsx", "referencePng"], "projectRoot": ""}}})); - frame_refs.push(json!({"width": width, "nodeId": frame_id})); + frame_refs.push(json!({"width": width, "height": frame_height, "nodeId": frame_id})); continue; }; - let reference = sources.reference_png(frame_id); + let corrected = substitutions.iter().any(|row| row["screen"] == *frame_id); + let reference = (!corrected) + .then(|| sources.reference_png(frame_id)) + .flatten(); if reference.is_none() { checks.push(check(json!({"kind": "page-health", "label": "design reference unavailable", "widths": [width], "unavailableReason": "Design comparison inconclusive: referencePng missing or expired; export the frame again."}))); missing.push(json!({"kind": "referencePng", "nodeId": frame_id, "width": width, @@ -342,7 +375,7 @@ pub(super) fn url_plan( "call": {"tool": "devup_figma_export", "arguments": {"url": format!(""), "outputs": ["referencePng"]}}})); } frame_refs.push( - json!({"width": width, "nodeId": frame_id, "artifactId": reference, + json!({"width": width, "height": frame_height, "nodeId": frame_id, "artifactId": reference, "threshold": thresholds.for_node(frame_id, "page")}), ); let _ = artifact_id; @@ -378,7 +411,10 @@ pub(super) fn url_plan( "subject": node(section), "container": node(frame_id), "nodeId": section}), )); } - if !is_asset && bounds(payload, section).is_some_and(|section_box| section_box.width >= frame_box.width * 0.5) { + if !is_asset + && bounds(payload, section) + .is_some_and(|section_box| section_box.width >= frame_box.width * 0.5) + { layout.push(check(json!({"kind": "section-layout", "label": format!("{name} remains a visible section"), "subject": node_at(*width, section), "container": node_at(*width, frame_id), "widths": viewports.iter().filter(|viewport| { target.frames.iter().filter(|(drawn, _, _)| *drawn <= viewport.width).max_by_key(|(drawn, _, _)| *drawn) @@ -424,7 +460,15 @@ pub(super) fn url_plan( *width, thresholds.for_node(section, scope), )), - None => renders_missing.push(section.clone()), + None => renders_missing.push( + sources + .substitutions + .iter() + .find(|row| row["nodeId"] == *section) + .and_then(|row| row["correctNodeId"].as_str()) + .unwrap_or(section) + .to_owned(), + ), } } // Assets below the sections, each against its own render and in @@ -555,6 +599,7 @@ pub(super) fn url_plan( plan, missing, notes, + substitutions, }) } @@ -676,10 +721,34 @@ pub(super) async fn write( artifact_ids, thresholds, } = *request; - let (sources, expired) = Sources::load(artifact_ids, store).await; + let mut ids = artifact_ids.to_vec(); + ids.extend( + plan["source"]["componentWidthArtifacts"] + .as_array() + .into_iter() + .flatten() + .filter_map(Value::as_str) + .map(str::to_owned), + ); + ids.sort(); + ids.dedup(); + let (mut sources, expired) = Sources::load(&ids, store).await; + let persisted_references = persist_references(root, &sources, policy)?; + let expired = expired + .into_iter() + .map(|mut entry| { + entry["persistedReferences"] = json!(stored_references(root)); + entry + }) + .collect::>(); + sources.substitutions = plan["componentWidthMismatches"] + .as_array() + .cloned() + .unwrap_or_default(); let evidence = super::screen_plan::project_evidence(root).await?; let audit_routes = super::screen_plan::audit_routes(plan); - let routes = audit_routes.iter() + let routes = audit_routes + .iter() .filter(|route| route["implementation"] == "page" && route["url"].is_string()) .collect::>(); let records = super::export_record::records(Path::new(root)); @@ -784,16 +853,29 @@ pub(super) async fn write( if let Some(node) = route["state"]["nodeId"].as_str() { built.notes.push(format!("State {} ({node}) on {url}: configure the runtime screen's steps to reach this state before running; no interaction is inferred from the frame name.", route["state"]["name"].as_str().unwrap_or_default())); for check in &mut built.plan.screens[0].checks { - check.label = Some(format!("State {node}: {}", check.label.as_deref().unwrap_or(&check.kind))); + check.label = Some(format!( + "State {node}: {}", + check.label.as_deref().unwrap_or(&check.kind) + )); } } for check in &mut built.plan.screens[0].checks { if check.kind == "links-resolve" { - check.planned_targets = routes.iter().filter_map(|route| route["url"].as_str().map(str::to_owned)).collect(); - check.existing_routes = evidence.routes.iter().map(|(route, _)| route.clone()).collect(); + check.planned_targets = routes + .iter() + .filter_map(|route| route["url"].as_str().map(str::to_owned)) + .collect(); + check.existing_routes = evidence + .routes + .iter() + .map(|(route, _)| route.clone()) + .collect(); } } - let plan_path = Path::new(root).join(AUDIT_DIR).join(&screen).join("plan.json"); + let plan_path = Path::new(root) + .join(AUDIT_DIR) + .join(&screen) + .join("plan.json"); let report_dir = Path::new(root).join(AUDIT_DIR).join(&screen).join("report"); let counts = built.plan.screens[0].checks.iter().fold( BTreeMap::::new(), @@ -816,9 +898,12 @@ pub(super) async fn write( store, ) .await?; + write_component_substitutions(&plan_path, policy, &built.substitutions)?; written.push(json!({ "url": url, "screen": screen, + "app": app, + "baseUrl": base, "state": route["state"], "planPath": created["writtenTo"], "planId": created["planId"], @@ -827,6 +912,7 @@ pub(super) async fn write( "checks": counts, "missingReferences": built.missing, "notes": built.notes, + "componentSubstitutions": built.substitutions, "dynamic": dynamic, "run": ["node", "run.mjs", "--plan", plan_path.display().to_string(), "--out", report_dir.display().to_string()], "startServer": dev_server_command(Path::new(root), app, base), @@ -848,6 +934,7 @@ pub(super) async fn write( "productionStrip": strip, "missingReferences": missing, "expiredArtifacts": expired, + "persistedReferences": persisted_references, "runner": {"directory": runner.display().to_string(), "state": template_state, "install": [["npm", "ci"], ["npx", "playwright", "install", "chromium"]], "ifInstallFails": "Record it with devup_screen_plan action status, auditPlanPath and toolingUnavailable: the URL's audit stays inconclusive and the work goes on."}, @@ -895,105 +982,3 @@ fn hidden_elements(records: &[Value], frames: &[(u32, String, Option)]) -> } hidden } - -/// The concrete URLs a dynamic route is audited at, and where they came from. -pub(super) struct Dynamic { - pub urls: Vec, - pub source: &'static str, - pub why: String, - pub assumption: Option, -} - -/// `[slug]` -> ("slug", false); `[...slug]` and `[[...slug]]` -> ("slug", true). -fn params(pattern: &str) -> Vec<(String, bool)> { - pattern - .split('/') - .filter(|segment| segment.starts_with('[') && segment.ends_with(']')) - .map(|segment| { - let inner = segment.trim_matches(|c| c == '[' || c == ']'); - match inner.strip_prefix("...") { - Some(name) => (name.to_owned(), true), - None => (inner.to_owned(), false), - } - }) - .collect() -} - -fn fill(pattern: &str, values: &BTreeMap) -> Option { - let mut out = Vec::new(); - for segment in pattern.split('/') { - if segment.starts_with('[') && segment.ends_with(']') { - let inner = segment.trim_matches(|c| c == '[' || c == ']'); - out.push(values.get(inner.trim_start_matches("...")).cloned()?); - } else { - out.push(segment.to_owned()); - } - } - Some(out.join("/")) -} - -/// Every literal value of `key` in `text`: `key: 'a'`, `"key": "a"`, or -/// `key: ['a', 'b']` (joined with `/` for a catch-all). -pub(super) fn values_of(text: &str, key: &str) -> Vec { - let mut found = Vec::new(); - let bytes = text.as_bytes(); - let mut from = 0; - while let Some(at) = text[from..].find(key) { - let start = from + at; - from = start + key.len(); - let before = start.checked_sub(1).map(|i| bytes[i] as char); - if before.is_some_and(|c| c.is_alphanumeric() || c == '_' || c == '$' || c == '.') { - continue; - } - let mut rest = text[from..].trim_start_matches(['"', '\'']).trim_start(); - let Some(after) = rest.strip_prefix(':') else { - continue; - }; - rest = after.trim_start(); - let quoted = |rest: &str| -> Option<(String, usize)> { - let quote = rest - .chars() - .next() - .filter(|c| matches!(c, '"' | '\'' | '`'))?; - let end = rest[1..].find(quote)?; - Some((rest[1..=end].to_owned(), end + 2)) - }; - if let Some((value, _)) = quoted(rest) { - found.push(value); - } else if let Some(list) = rest.strip_prefix('[') { - let end = list.find(']').unwrap_or(list.len()); - let parts = list[..end] - .split(',') - .filter_map(|part| quoted(part.trim()).map(|(value, _)| value)) - .collect::>(); - if !parts.is_empty() { - found.push(parts.join("/")); - } - } - } - found.dedup(); - found -} - -/// Local modules a page imports: `./x`, `../x` or `@/x`, with the names -/// imported from each. -fn local_imports(source: &str) -> Vec<(String, String)> { - let mut found = Vec::new(); - for line in source.lines() { - let line = line.trim(); - let Some(rest) = line.strip_prefix("import ") else { - continue; - }; - let Some((names, module)) = rest.rsplit_once(" from ") else { - continue; - }; - let module = module - .trim() - .trim_end_matches(';') - .trim_matches(['"', '\'']); - if module.starts_with("./") || module.starts_with("../") || module.starts_with("@/") { - found.push((names.to_owned(), module.to_owned())); - } - } - found -} diff --git a/crates/devup-mcp/src/server/audit_plan_tests.rs b/crates/devup-mcp/src/server/audit_plan_tests.rs index 2fd09232..1aaedc69 100644 --- a/crates/devup-mcp/src/server/audit_plan_tests.rs +++ b/crates/devup-mcp/src/server/audit_plan_tests.rs @@ -1,37 +1,190 @@ use super::*; +#[test] +fn width_mismatch_audit_uses_mobile_subtree_at_suspect_position() { + let mut nodes = serde_json::Map::new(); + for (screen, width, master, y, height) in [ + ("mobile-a", 360.0, "mobile", 700.0, 400.0), + ("mobile-b", 360.0, "mobile", 600.0, 400.0), + ("mobile-c", 360.0, "mobile", 800.0, 400.0), + ("home", 360.0, "desktop", 1200.0, 200.0), + ("wide", 1440.0, "desktop", 900.0, 200.0), + ] { + let footer = format!("{screen}-footer"); + let child = format!("{screen}-text"); + let (id, node) = frame( + screen, + screen, + [0.0, 0.0, width, 1600.0], + &[&footer], + json!({}), + ); + nodes.insert(id, node); + let (id, mut node) = frame( + &footer, + "Footer", + [0.0, y, width, height], + &[&child], + json!({"mainComponentId":master,"mainComponentName":"Footer"}), + ); + node["type"] = json!("INSTANCE"); + nodes.insert(id, node); + let (id, node) = frame( + &child, + "Text", + [20.0, y + 30.0, 300.0, 50.0], + &[], + json!({}), + ); + nodes.insert(id, node); + } + let sources = Sources::from_payloads(vec![( + "all".into(), + payload( + "home", + Value::Object(nodes), + json!([native("mobile-a-footer", "mobile-render")]), + true, + ), + )]); + let built = url_plan( + &Target { + url: "/", + screen: "home".into(), + file: None, + frames: vec![(360, "home".into(), Some(1600.0))], + next_url: None, + hidden: vec![], + }, + "http://localhost:3000", + &sources, + &VisualThresholds::default(), + ) + .unwrap(); + let checks = &built.plan.screens[0].checks; + let footer = checks + .iter() + .find(|check| check.kind == "bounds" && check.node_id.as_deref() == Some("home-footer")) + .unwrap(); + let expected = serde_json::to_value(&footer.expected).unwrap(); + assert_eq!( + expected, + json!({"x":0.0,"y":1200.0,"width":360.0,"height":400.0}) + ); + let capture = checks + .iter() + .find(|check| check.kind == "capture" && check.node_id.as_deref() == Some("home-footer")) + .unwrap(); + assert_eq!( + serde_json::to_value(&capture.reference).unwrap()["assetId"], + "mobile-render" + ); + assert!( + checks.iter().any( + |check| check.kind == "bounds" && check.node_id.as_deref() == Some("mobile-a-text") + ) + ); + assert!( + !checks + .iter() + .any(|check| check.node_id.as_deref() == Some("home-text")) + ); + assert_eq!(built.substitutions[0]["correctNodeId"], "mobile-a-footer"); +} + #[test] fn dev_server_command_uses_the_route_app_and_its_package_manager() { let project = Project::new("dev-command"); project.write("bun.lock", ""); - project.write("apps/front/package.json", r#"{"scripts":{"dev":"vinext dev"}}"#); - let command = dev_server_command(Path::new(project.root()), "apps/front", "http://localhost:3000"); + project.write( + "apps/front/package.json", + r#"{"scripts":{"dev":"vinext dev"}}"#, + ); + let command = dev_server_command( + Path::new(project.root()), + "apps/front", + "http://localhost:3000", + ); assert_eq!(command["command"], json!(["bun", "run", "dev"])); - assert!(command["cwd"].as_str().unwrap().ends_with("apps\\front") || command["cwd"].as_str().unwrap().ends_with("apps/front")); + assert!( + command["cwd"].as_str().unwrap().ends_with("apps\\front") + || command["cwd"].as_str().unwrap().ends_with("apps/front") + ); assert_eq!(command["script"], "vinext dev"); - assert!(command["windowsCommand"].as_str().unwrap().contains("cmd.exe /d /s /c")); + assert!( + command["windowsCommand"] + .as_str() + .unwrap() + .contains("cmd.exe /d /s /c") + ); } #[test] fn absent_source_anchors_leave_design_checks_not_run() { let project = Project::new("missing-anchors"); - project.write("src/app/page.tsx", "export default function Page() { return
}"); - let mut built = url_plan(&home(), "http://localhost:3000", &sources(), &VisualThresholds::default()).unwrap(); + project.write( + "src/app/page.tsx", + "export default function Page() { return
}", + ); + let mut built = url_plan( + &home(), + "http://localhost:3000", + &sources(), + &VisualThresholds::default(), + ) + .unwrap(); gate_source_anchors(&mut built, Path::new(project.root())); let checks = &built.plan.screens[0].checks; - assert!(checks.iter().any(|check| check.kind == "bounds" && check.unavailable_reason.as_deref().is_some_and(|reason| reason.contains("anchors absent")))); - assert!(checks.iter().filter(|check| check.unavailable_reason.is_some()).all(|check| check.subject.is_none() && check.container.is_none())); - assert!(checks.iter().any(|check| check.kind == "page-health" && check.unavailable_reason.is_none())); + assert!(checks.iter().any(|check| { + check.kind == "bounds" + && check + .unavailable_reason + .as_deref() + .is_some_and(|reason| reason.contains("anchors absent")) + })); + assert!( + checks + .iter() + .filter(|check| check.unavailable_reason.is_some()) + .all(|check| check.subject.is_none() && check.container.is_none()) + ); + assert!( + checks + .iter() + .any(|check| check.kind == "page-health" && check.unavailable_reason.is_none()) + ); } #[test] fn partial_source_anchors_do_not_enable_unmatched_nodes() { let project = Project::new("partial-anchors"); - project.write("src/app/page.tsx", "
"); - let mut built = url_plan(&home(), "http://localhost:3000", &sources(), &VisualThresholds::default()).unwrap(); + project.write( + "src/app/page.tsx", + "
", + ); + let mut built = url_plan( + &home(), + "http://localhost:3000", + &sources(), + &VisualThresholds::default(), + ) + .unwrap(); gate_source_anchors(&mut built, Path::new(project.root())); - assert!(built.plan.screens[0].checks.iter().any(|check| check.kind == "bounds" && check.node_id.as_deref() == Some("10:12") && check.unavailable_reason.is_none())); - assert!(built.plan.screens[0].checks.iter().filter(|check| check.node_id.as_deref() == Some("10:131")).all(|check| check.unavailable_reason.is_some())); + assert!( + built.plan.screens[0] + .checks + .iter() + .any(|check| check.kind == "bounds" + && check.node_id.as_deref() == Some("10:12") + && check.unavailable_reason.is_none()) + ); + assert!( + built.plan.screens[0] + .checks + .iter() + .filter(|check| check.node_id.as_deref() == Some("10:131")) + .all(|check| check.unavailable_reason.is_some()) + ); } fn payload(target: &str, nodes: Value, assets: Value, reference: bool) -> CollectedPayload { @@ -179,6 +332,37 @@ fn home() -> Target<'static> { } } +#[test] +fn tall_audit_frames_keep_drawn_height_without_oversized_viewports() { + let mut target = home(); + target.frames = vec![(1440, "10:1".into(), Some(5953.0))]; + let built = url_plan( + &target, + "http://localhost:3000", + &sources(), + &VisualThresholds::default(), + ) + .unwrap(); + let document = serde_json::to_value(&built.plan).unwrap(); + assert_eq!(document["screens"][0]["figma"]["frames"][0]["height"], 5953); + assert_eq!( + built + .plan + .viewports + .iter() + .find(|viewport| viewport.width == 1440) + .unwrap() + .height, + 900 + ); + assert!( + !built + .notes + .iter() + .any(|note| note.contains("comparison stays inconclusive")) + ); +} + fn kinds(plan: &PlanInput) -> BTreeSet<&str> { plan.screens[0] .checks @@ -225,9 +409,16 @@ fn a_two_url_audit_plan_checks_the_page_its_sections_assets_geometry_overflow_an } let checks = &home.plan.screens[0].checks; for kind in ["no-low-contrast-text", "image-loaded"] { - assert!(checks.iter().any(|check| check.kind == kind && check.widths.is_empty())); + assert!( + checks + .iter() + .any(|check| check.kind == kind && check.widths.is_empty()) + ); } - let completeness = checks.iter().filter(|check| check.kind == "rendered-completeness").collect::>(); + let completeness = checks + .iter() + .filter(|check| check.kind == "rendered-completeness") + .collect::>(); assert_eq!(completeness.len(), 2); for check in completeness { assert_eq!(check.widths.len(), 1); @@ -632,252 +823,4 @@ impl Drop for Project { } } -fn blog_route() -> Value { - json!({"url": "/blog/[slug]", "file": "src/app/blog/[slug]/page.tsx", "app": ".", "nodeIds": ["20:1"], "drawnWidths": [1440]}) -} - -#[test] -fn a_dynamic_route_takes_its_literal_static_params() { - let project = Project::new("literal"); - project.write( - "src/app/blog/[slug]/page.tsx", - "export function generateStaticParams() {\n return [{ slug: 'hello' }, { slug: \"second-post\" }]\n}\nexport default function Page() { return null }\n", - ); - let found = dynamic_urls( - project.root(), - &blog_route(), - &Value::Null, - &Sources::from_payloads(Vec::new()), - ); - assert_eq!(found.source, "generateStaticParams"); - assert_eq!(found.urls, ["/blog/hello", "/blog/second-post"]); -} - -#[test] -fn a_dynamic_route_reads_the_local_data_its_static_params_map() { - let project = Project::new("data"); - project.write( - "src/app/blog/[slug]/page.tsx", - "import { posts } from '@/data/posts'\nexport async function generateStaticParams() {\n return posts.map((post) => ({ slug: post.slug }))\n}\nexport default function Page() { return null }\n", - ); - project.write( - "src/data/posts.ts", - "export const posts = [\n { slug: 'launch', title: 'Launch' },\n { slug: 'roadmap', title: 'Roadmap' },\n]\n", - ); - let found = dynamic_urls( - project.root(), - &blog_route(), - &Value::Null, - &Sources::from_payloads(Vec::new()), - ); - assert_eq!(found.urls, ["/blog/launch", "/blog/roadmap"]); -} - -#[test] -fn a_dynamic_route_without_params_takes_the_links_audited_pages_hold() { - let project = Project::new("links"); - let links = - json!({"/": ["/blog/from-home", "/about", "/blog/other"], "/about": ["/blog/from-home"]}); - let found = dynamic_urls( - project.root(), - &blog_route(), - &links, - &Sources::from_payloads(Vec::new()), - ); - assert_eq!(found.source, "links on audited pages"); - assert_eq!(found.urls, ["/blog/from-home", "/blog/other"]); - assert!(found.assumption.is_none()); -} - -#[test] -fn a_dynamic_route_falls_back_to_its_detail_frame_as_an_assumption() { - let project = Project::new("frame"); - let detail = [ - frame("20:1", "Blog detail", [0.0, 0.0, 1440.0, 900.0], &["20:2"], json!({})), - ("20:2".to_owned(), json!({"id": "20:2", "type": "TEXT", "fields": {"characters": "Our First Release!", "parentId": "20:1"}})), - ] - .into_iter() - .collect::>(); - let sources = Sources::from_payloads(vec![( - "art-blog".into(), - payload("20:1", Value::Object(detail), json!([]), true), - )]); - let found = dynamic_urls(project.root(), &blog_route(), &Value::Null, &sources); - assert_eq!(found.source, "Figma detail frame"); - assert_eq!(found.urls, ["/blog/our-first-release"]); - assert!(found.assumption.unwrap().contains("Our First Release!")); - - let nothing = dynamic_urls( - project.root(), - &blog_route(), - &Value::Null, - &Sources::from_payloads(Vec::new()), - ); - assert!(nothing.urls.is_empty()); - assert!(nothing.why.contains("generateStaticParams")); -} - -#[test] -fn a_catch_all_route_joins_its_segments() { - assert_eq!( - values_of("{ slug: ['docs', 'intro'] }", "slug"), - ["docs/intro"] - ); - assert_eq!(params("/docs/[...slug]"), [("slug".to_owned(), true)]); - assert_eq!( - fill( - "/docs/[...slug]", - &BTreeMap::from([("slug".to_owned(), "docs/intro".to_owned())]) - ) - .as_deref(), - Some("/docs/docs/intro") - ); -} - -const EXPORTED: &str = "export default function Page() {\n return (\n \n \n \n )\n}\n"; - -fn geometry_audit() -> Value { - json!({"findings": [{"id": "home|bounds|10:132|1440", "class": "layout-position", "kind": "bounds", - "nodeId": "10:132", "routePath": "/", "width": 1440, "reason": "position: x off by 1px", - "evidence": {"screenshot": "report/home.png", "measured": { - "expected": {"x": 560, "y": 40, "width": 400, "height": 300}, - "actual": {"x": 561, "y": 40, "width": 400, "height": 300}}}}]}) -} - -fn exported_project(name: &str) -> (Project, OutputPolicy) { - let project = Project::new(name); - let policy = OutputPolicy::from_roots(vec![project.0.clone()]).unwrap(); - let response = json!({"tsx": EXPORTED, "figmaNodeIds": {"root": "10:1"}, - "outputResults": {"tsx": {"_sourceMap": [ - {"nodeId": "10:132", "property": "width", "generatedProperty": "w=\"400px\"", "resolution": "exact"}]}}}); - let written = - super::super::export_record::write(&policy, project.root(), "FILEKEY", "10:1", &response) - .unwrap() - .unwrap(); - assert!(written.ends_with("10-1.json"), "{written}"); - (project, policy) -} - -#[test] -fn a_mismatch_with_the_code_still_equal_to_the_export_is_a_converter_bug_with_a_repro() { - let (project, policy) = exported_project("converter-bug"); - // The agent added behaviour, not layout: still the export's element. - project.write( - "src/app/page.tsx", - &EXPORTED.replace( - "", "w=\"401px\" ml=\"2px\" />"), - ); - let mut audit = geometry_audit(); - classify_geometry( - &mut audit, - project.root(), - &["src/app/page.tsx".to_owned()], - &policy, - ); - let finding = &audit["findings"][0]; - assert_eq!(finding["class"], "code-drift", "{finding}"); - assert_eq!( - finding["evidence"]["drift"]["missing"], - json!(["w=\"400px\""]) - ); - assert_eq!(finding["evidence"]["drift"]["added"], json!(["ml=\"2px\""])); - assert!( - finding["fixHint"] - .as_str() - .unwrap() - .contains("Restore the values the export wrote") - ); - assert!(!project.0.join(".devup/converter-bugs").exists()); -} - -#[test] -fn a_mismatch_without_an_export_record_keeps_its_class_and_says_why() { - let project = Project::new("no-record"); - let policy = OutputPolicy::from_roots(vec![project.0.clone()]).unwrap(); - let mut audit = geometry_audit(); - classify_geometry(&mut audit, project.root(), &[], &policy); - assert_eq!(audit["findings"][0]["class"], "layout-position"); - assert!( - audit["findings"][0]["evidence"]["cause"] - .as_str() - .unwrap() - .contains("No export record") - ); -} - -#[test] -fn an_asset_capture_waiting_for_comparison_blocks_completion() { - let mut plan = json!({"audit":{}}); - let audit = json!({"screenTable":[{"routePath":"/", "width":1440}], "visualComparisons":[ - {"id":"home|asset|icon|1440", "routePath":"/", "viewport":{"width":1440}, "blockers":[]} - ]}); - record_round(&mut plan, &audit, "run", &[], &BTreeMap::new(), 3); - assert_eq!(plan["audit"]["visualPending"].as_array().unwrap().len(), 1); - record_round(&mut plan, &audit, "run", &[VisualResultInput { id:"home|asset|icon|1440".into(), verdict:"pass".into(), changed_ratio:Some(0.0), diff_path:None, reason:None }], &BTreeMap::new(), 3); - assert!(plan["audit"]["visualPending"].as_array().unwrap().is_empty()); -} +include!("audit_plan_tests_routes_and_drift.rs"); diff --git a/crates/devup-mcp/src/server/audit_plan_tests_routes_and_drift.rs b/crates/devup-mcp/src/server/audit_plan_tests_routes_and_drift.rs new file mode 100644 index 00000000..482ed285 --- /dev/null +++ b/crates/devup-mcp/src/server/audit_plan_tests_routes_and_drift.rs @@ -0,0 +1,267 @@ +fn blog_route() -> Value { + json!({"url": "/blog/[slug]", "file": "src/app/blog/[slug]/page.tsx", "app": ".", "nodeIds": ["20:1"], "drawnWidths": [1440]}) +} + +#[test] +fn a_dynamic_route_takes_its_literal_static_params() { + let project = Project::new("literal"); + project.write( + "src/app/blog/[slug]/page.tsx", + "export function generateStaticParams() {\n return [{ slug: 'hello' }, { slug: \"second-post\" }]\n}\nexport default function Page() { return null }\n", + ); + let found = dynamic_urls( + project.root(), + &blog_route(), + &Value::Null, + &Sources::from_payloads(Vec::new()), + ); + assert_eq!(found.source, "generateStaticParams"); + assert_eq!(found.urls, ["/blog/hello", "/blog/second-post"]); +} + +#[test] +fn a_dynamic_route_reads_the_local_data_its_static_params_map() { + let project = Project::new("data"); + project.write( + "src/app/blog/[slug]/page.tsx", + "import { posts } from '@/data/posts'\nexport async function generateStaticParams() {\n return posts.map((post) => ({ slug: post.slug }))\n}\nexport default function Page() { return null }\n", + ); + project.write( + "src/data/posts.ts", + "export const posts = [\n { slug: 'launch', title: 'Launch' },\n { slug: 'roadmap', title: 'Roadmap' },\n]\n", + ); + let found = dynamic_urls( + project.root(), + &blog_route(), + &Value::Null, + &Sources::from_payloads(Vec::new()), + ); + assert_eq!(found.urls, ["/blog/launch", "/blog/roadmap"]); +} + +#[test] +fn a_dynamic_route_without_params_takes_the_links_audited_pages_hold() { + let project = Project::new("links"); + let links = + json!({"/": ["/blog/from-home", "/about", "/blog/other"], "/about": ["/blog/from-home"]}); + let found = dynamic_urls( + project.root(), + &blog_route(), + &links, + &Sources::from_payloads(Vec::new()), + ); + assert_eq!(found.source, "links on audited pages"); + assert_eq!(found.urls, ["/blog/from-home", "/blog/other"]); + assert!(found.assumption.is_none()); +} + +#[test] +fn a_dynamic_route_falls_back_to_its_detail_frame_as_an_assumption() { + let project = Project::new("frame"); + let detail = [ + frame("20:1", "Blog detail", [0.0, 0.0, 1440.0, 900.0], &["20:2"], json!({})), + ("20:2".to_owned(), json!({"id": "20:2", "type": "TEXT", "fields": {"characters": "Our First Release!", "parentId": "20:1"}})), + ] + .into_iter() + .collect::>(); + let sources = Sources::from_payloads(vec![( + "art-blog".into(), + payload("20:1", Value::Object(detail), json!([]), true), + )]); + let found = dynamic_urls(project.root(), &blog_route(), &Value::Null, &sources); + assert_eq!(found.source, "Figma detail frame"); + assert_eq!(found.urls, ["/blog/our-first-release"]); + assert!(found.assumption.unwrap().contains("Our First Release!")); + + let nothing = dynamic_urls( + project.root(), + &blog_route(), + &Value::Null, + &Sources::from_payloads(Vec::new()), + ); + assert!(nothing.urls.is_empty()); + assert!(nothing.why.contains("generateStaticParams")); +} + +#[test] +fn a_catch_all_route_joins_its_segments() { + assert_eq!( + values_of("{ slug: ['docs', 'intro'] }", "slug"), + ["docs/intro"] + ); + assert_eq!(params("/docs/[...slug]"), [("slug".to_owned(), true)]); + assert_eq!( + fill( + "/docs/[...slug]", + &BTreeMap::from([("slug".to_owned(), "docs/intro".to_owned())]) + ) + .as_deref(), + Some("/docs/docs/intro") + ); +} + +const EXPORTED: &str = "export default function Page() {\n return (\n \n \n \n )\n}\n"; + +fn geometry_audit() -> Value { + json!({"findings": [{"id": "home|bounds|10:132|1440", "class": "layout-position", "kind": "bounds", + "nodeId": "10:132", "routePath": "/", "width": 1440, "reason": "position: x off by 1px", + "evidence": {"screenshot": "report/home.png", "measured": { + "expected": {"x": 560, "y": 40, "width": 400, "height": 300}, + "actual": {"x": 561, "y": 40, "width": 400, "height": 300}}}}]}) +} + +fn exported_project(name: &str) -> (Project, OutputPolicy) { + let project = Project::new(name); + let policy = OutputPolicy::from_roots(vec![project.0.clone()]).unwrap(); + let response = json!({"tsx": EXPORTED, "figmaNodeIds": {"root": "10:1"}, + "outputResults": {"tsx": {"_sourceMap": [ + {"nodeId": "10:132", "property": "width", "generatedProperty": "w=\"400px\"", "resolution": "exact"}]}}}); + let written = + super::super::export_record::write(&policy, project.root(), "FILEKEY", "10:1", &response) + .unwrap() + .unwrap(); + assert!(written.ends_with("10-1.json"), "{written}"); + (project, policy) +} + +#[test] +fn a_mismatch_with_the_code_still_equal_to_the_export_is_a_converter_bug_with_a_repro() { + let (project, policy) = exported_project("converter-bug"); + // The agent added behaviour, not layout: still the export's element. + project.write( + "src/app/page.tsx", + &EXPORTED.replace( + "", "w=\"401px\" ml=\"2px\" />"), + ); + let mut audit = geometry_audit(); + classify_geometry( + &mut audit, + project.root(), + &["src/app/page.tsx".to_owned()], + &policy, + ); + let finding = &audit["findings"][0]; + assert_eq!(finding["class"], "code-drift", "{finding}"); + assert_eq!( + finding["evidence"]["drift"]["missing"], + json!(["w=\"400px\""]) + ); + assert_eq!(finding["evidence"]["drift"]["added"], json!(["ml=\"2px\""])); + assert!( + finding["fixHint"] + .as_str() + .unwrap() + .contains("Restore the values the export wrote") + ); + assert!(!project.0.join(".devup/converter-bugs").exists()); +} + +#[test] +fn a_mismatch_without_an_export_record_keeps_its_class_and_says_why() { + let project = Project::new("no-record"); + let policy = OutputPolicy::from_roots(vec![project.0.clone()]).unwrap(); + let mut audit = geometry_audit(); + classify_geometry(&mut audit, project.root(), &[], &policy); + assert_eq!(audit["findings"][0]["class"], "layout-position"); + assert!( + audit["findings"][0]["evidence"]["cause"] + .as_str() + .unwrap() + .contains("No export record") + ); +} + +#[test] +fn an_asset_capture_waiting_for_comparison_blocks_completion() { + let mut plan = json!({"audit":{}}); + let audit = json!({"screenTable":[{"routePath":"/", "width":1440}], "visualComparisons":[ + {"id":"home|asset|icon|1440", "routePath":"/", "viewport":{"width":1440}, "blockers":[]} + ]}); + record_round(&mut plan, &audit, "run", &[], &BTreeMap::new(), 3); + assert_eq!(plan["audit"]["visualPending"].as_array().unwrap().len(), 1); + record_round( + &mut plan, + &audit, + "run", + &[VisualResultInput { + id: "home|asset|icon|1440".into(), + verdict: "pass".into(), + changed_ratio: Some(0.0), + diff_path: None, + reason: None, + }], + &BTreeMap::new(), + 3, + ); + assert!( + plan["audit"]["visualPending"] + .as_array() + .unwrap() + .is_empty() + ); +} diff --git a/crates/devup-mcp/src/server/handlers/devup_screen_plan.rs b/crates/devup-mcp/src/server/handlers/devup_screen_plan.rs index 1abb637a..21139d77 100644 --- a/crates/devup-mcp/src/server/handlers/devup_screen_plan.rs +++ b/crates/devup-mcp/src/server/handlers/devup_screen_plan.rs @@ -12,7 +12,8 @@ impl DevupServer { ) -> Result { let mut result = match input.action.as_str() { "draft" => { - let (screens, source) = self.plan_screens(&input).await.map_err(to_mcp_error)?; + let (screens, mut source) = self.plan_screens(&input).await.map_err(to_mcp_error)?; + self.plan_component_widths(&input, &screens, &mut source).await.map_err(to_mcp_error)?; let mut assets = input.assets.clone(); assets.extend( source["assetCandidates"] diff --git a/crates/devup-mcp/src/server/mod_parts/component_width_acquisition.rs b/crates/devup-mcp/src/server/mod_parts/component_width_acquisition.rs new file mode 100644 index 00000000..282184cb --- /dev/null +++ b/crates/devup-mcp/src/server/mod_parts/component_width_acquisition.rs @@ -0,0 +1,85 @@ +impl DevupServer { + async fn plan_component_widths( + &self, + input: &screen_plan::ScreenPlanInput, + screens: &[screen_plan::PlanScreenInput], + source: &mut Value, + ) -> Result<(), DevupError> { + if !input.screens.is_empty() + && self.services.upstream.bridge_path_snapshot().await.is_none_or(|bridge| bridge.attached_files.len() != 1) { + source["componentWidthEvidence"] = json!({"state":"unavailable","reason":"Caller screen summaries have no component identity; no unique Bridge file is attached."}); + return Ok(()); + } + let target = self.bind_to_bridge(parse_link(input.url.as_deref())?, Unnamed::Selection, + &json!({"tool":"devup_screen_plan","arguments":{"action":"draft"}})).await?; + let mut snapshot = None; + let mut payloads = Vec::new(); + for screen in screens.iter().filter(|screen| screen.visible != Some(false)) { + let mut target = target.clone(); + target.node_id = Some(screen.node_id.clone()); + let request = CollectionRequest::new(target, CollectionScope::Node); + let key = ArtifactRequestKey::from_collection(&request); + let payload = CollectedPayload::try_from(self.run_direct(request, &PendingOperation::Collect, None).await?)?; + let artifact = self.artifacts.insert(key, payload).await?; + match &mut snapshot { + None => snapshot = Some(artifact.payload.snapshot.clone()), + Some(combined) => combined.nodes.extend(artifact.payload.snapshot.nodes.clone()), + } + payloads.push(artifact.artifact_id); + } + let Some(snapshot) = snapshot else { return Ok(()) }; + let roots = screens.iter().map(|screen| screen.node_id.clone()).collect::>(); + let mut mismatches = screen_plan::component_width_mismatches(&snapshot, &roots); + for mismatch in &mut mismatches { + let mut references = Vec::new(); + let mut render_bytes = Vec::new(); + for field in ["nodeId", "correctNodeId"] { + let Some(id) = mismatch[field].as_str() else { continue }; + let mut target = target.clone(); + target.node_id = Some(id.into()); + let mut request = CollectionRequest::new(target, CollectionScope::Node); + request.asset_selections.push(devup_mcp_figma::AssetSelection { + asset_id: format!("{id}:node"), format: devup_mcp_figma::AssetFormat::Png, + scale: 1.0, render_mode: devup_mcp_figma::AssetRenderMode::Node, + usage_aspect_ratio: None, max_width: None, srcset: false, + visible_scope_node_id: None, quality: None, lossless: None, + }); + let key = ArtifactRequestKey::from_collection(&request); + match self.run_direct(request, &PendingOperation::Collect, None).await.and_then(CollectedPayload::try_from) { + Ok(payload) => { + let artifact = self.artifacts.insert(key, payload).await?; + for asset in &artifact.payload.assets { + if asset.node_id == id + && asset.status == devup_mcp_figma::AssetStatus::Exported + && asset.format == Some(devup_mcp_figma::AssetFormat::Png) + && asset.scale == Some(1.0) + && asset.render_info.as_ref().is_some_and(|info|info["representation"] == "native-node") { + references.push(json!({"nodeId":id,"artifactId":artifact.artifact_id,"assetId":asset.asset_id,"renderMode":"node","format":"png","scale":1})); + if let Some(encoded) = &asset.data_base64 { + use base64::Engine as _; + if let Ok(bytes) = base64::engine::general_purpose::STANDARD.decode(encoded) { + render_bytes.push(bytes); + } + } + } + } + payloads.push(artifact.artifact_id); + } + Err(error) => mismatch["renderError"] = json!(error.to_string()), + } + } + mismatch["render"] = json!(if render_bytes.len() == 2 { "captured" } else { "unavailable" }); + mismatch["renderReferences"] = json!(references); + mismatch["comparison"] = json!({"method":"drawn-width-majority","pixelComparison":"unverified","decision":"use expected master as one responsive component"}); + if let [suspect, correct] = render_bytes.as_slice() { + match devup_mcp_visual::compare_png_bytes(correct, suspect, &devup_mcp_visual::CompareOptions::default(), false) { + Ok((report, _)) => mismatch["comparison"]["pixelComparison"] = serde_json::to_value(report).map_err(|error|DevupError::new(ErrorCode::DevupInvalidInput,error.to_string(),false))?, + Err(error) => mismatch["comparison"]["renderError"] = json!(error.to_string()), + } + } + } + source["componentWidthArtifacts"] = json!(payloads); + source["componentWidthMismatches"] = json!(mismatches); + Ok(()) + } +} diff --git a/crates/devup-mcp/src/server/runtime_audit.rs b/crates/devup-mcp/src/server/runtime_audit.rs index c88718e9..3a8f528a 100644 --- a/crates/devup-mcp/src/server/runtime_audit.rs +++ b/crates/devup-mcp/src/server/runtime_audit.rs @@ -15,4 +15,6 @@ include!("runtime_audit/inputs_and_capture.rs"); include!("runtime_audit/write_plan.rs"); include!("runtime_audit/capture_comparison.rs"); +include!("runtime_audit/plan_columns.rs"); +include!("runtime_audit/screenshot_integrity.rs"); include!("runtime_audit/tests.rs"); diff --git a/crates/devup-mcp/src/server/runtime_audit/capture_comparison.rs b/crates/devup-mcp/src/server/runtime_audit/capture_comparison.rs index 2dbb16d7..20449bad 100644 --- a/crates/devup-mcp/src/server/runtime_audit/capture_comparison.rs +++ b/crates/devup-mcp/src/server/runtime_audit/capture_comparison.rs @@ -1,5 +1,3 @@ - - /// The `devup_visual_compare` call for one element capture against the /// Figma node's own native render. fn capture_comparison( @@ -113,12 +111,6 @@ fn visual_comparison( viewport.device_scale_factor )); } - if shot.height != viewport.height { - blockers.push(format!( - include_str!("../text/runtime_audit-visual_comparison-8010a061.md"), - shot.height, viewport.height, shot.width, shot.height - )); - } if !environment_valid { blockers.push( "This run reported environment errors or an unverified screenshot; the comparison stays inconclusive." @@ -130,6 +122,14 @@ fn visual_comparison( .iter() .flat_map(|figma| &figma.frames) .find(|frame| frame.width == viewport.width); + if let Some(height) = frame.and_then(|frame| frame.height) + && (shot.width != viewport.width || shot.height != height) + { + blockers.push(format!( + include_str!("../text/runtime_audit-visual_comparison-8010a061.md"), + shot.width, shot.height, viewport.width, height + )); + } let node_id = frame.map_or(node_id, |frame| frame.node_id.as_str()); let artifact = frame.and_then(|frame| frame.artifact_id.clone()); if artifact.is_some() { diff --git a/crates/devup-mcp/src/server/runtime_audit/inputs_and_capture.rs b/crates/devup-mcp/src/server/runtime_audit/inputs_and_capture.rs index 79edf4c3..a6437f28 100644 --- a/crates/devup-mcp/src/server/runtime_audit/inputs_and_capture.rs +++ b/crates/devup-mcp/src/server/runtime_audit/inputs_and_capture.rs @@ -1,13 +1,12 @@ - -use std::collections::BTreeMap; -use std::path::Path; +use super::artifacts::ArtifactStore; +use super::output::{OutputPolicy, OutputTransaction}; use devup_mcp_figma::{DevupError, ErrorCode}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_json::{Value, json}; use sha2::{Digest, Sha256}; -use super::artifacts::ArtifactStore; -use super::output::{OutputPolicy, OutputTransaction}; +use std::collections::BTreeMap; +use std::path::Path; pub(crate) const PLAN_KIND: &str = "devup-runtime-audit-plan"; pub(crate) const REPORT_KIND: &str = "devup-runtime-audit-report"; const SCHEMA_VERSION: u64 = 1; @@ -356,6 +355,9 @@ pub struct FigmaAnchorInput { #[schemars(extend("additionalProperties" = json!({"not": {}})))] pub struct FrameReferenceInput { pub width: u32, + /// Drawn frame height in logical px, independent of the browser viewport. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub height: Option, pub node_id: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub artifact_id: Option, @@ -593,6 +595,15 @@ pub(crate) fn validate_plan(plan: &PlanInput) -> Result<(), DevupError> { if figma.drawn_widths.is_empty() { return Err(invalid("figma.drawnWidths must name at least one width.")); } + if figma.frames.iter().any(|frame| { + frame + .height + .is_some_and(|height| height == 0 || height > 100_000) + }) { + return Err(invalid( + "figma.frames[].height must be between 1 and 100000 px.", + )); + } } } for font in &plan.fonts { @@ -623,12 +634,17 @@ fn validate_check(check: &CheckInput) -> Result<(), DevupError> { Ok(()) }; match check.kind.as_str() { - "links-resolve" | "no-horizontal-overflow" | "page-health" | "no-clipped-content" => needs(false, false)?, + "links-resolve" | "no-horizontal-overflow" | "page-health" | "no-clipped-content" => { + needs(false, false)? + } "visible" | "view-transition" | "hidden" => needs(true, false)?, "stale-resources" | "styles-applied" => needs(false, false)?, "no-clipped-text" | "no-low-contrast-text" | "image-loaded" | "native-appearance" => { if check.container.is_some() { - return Err(invalid(format!("A {} check takes no container.", check.kind))); + return Err(invalid(format!( + "A {} check takes no container.", + check.kind + ))); } } "bounds" => { @@ -695,13 +711,22 @@ fn validate_check(check: &CheckInput) -> Result<(), DevupError> { } "text-align" => { needs(true, false)?; - if !matches!(check.edge.as_deref(), Some("left" | "right" | "center" | "justify")) { - return Err(invalid("A text-align check needs edge left, right, center or justify.")); + if !matches!( + check.edge.as_deref(), + Some("left" | "right" | "center" | "justify") + ) { + return Err(invalid( + "A text-align check needs edge left, right, center or justify.", + )); } } "sequence-order" => needs(false, false)?, "section-layout" => { - if check.subject.is_some() { needs(true, true)?; } else { needs(false, false)?; } + if check.subject.is_some() { + needs(true, true)?; + } else { + needs(false, false)?; + } } "rendered-completeness" => { needs(false, false)?; @@ -881,119 +906,3 @@ pub(crate) async fn create_plan( write_plan(input, policy, output_path, template_path, references) } -/// One screen at one width by what was checked: health (renders, no -/// overflow, no clipped text), geometry (size and position), interaction -/// (states and view transitions), and the three visual levels, which stay -/// "to compare" until devup_visual_compare results come back through -/// devup_screen_plan status. -fn columns( - screen: &ScreenInput, - viewport: &ViewportInput, - judged: &[(&str, &CheckInput)], - row: &Value, -) -> Value { - let fold = |kinds: &[&str]| { - let statuses = judged - .iter() - .filter(|(_, check)| kinds.contains(&check.kind.as_str())) - .map(|(status, _)| *status) - .collect::>(); - if statuses.is_empty() { - "-" - } else if statuses.contains(&"fail") { - "fail" - } else if statuses.iter().all(|status| *status == "pass") { - "pass" - } else if statuses.contains(&"inconclusive") { - "inconclusive" - } else { - "not-run" - } - }; - let unreached = row["reasons"] - .as_array() - .into_iter() - .flatten() - .filter_map(Value::as_str) - .any(|reason| reason.starts_with("page error") || reason.starts_with("screen not reached")); - let health = if unreached { - "fail" - } else { - fold(&["links-resolve", "page-health", "no-horizontal-overflow", "no-clipped-content", "styles-applied", "no-clipped-text", "no-low-contrast-text", "image-loaded", "native-appearance", "stale-resources"]) - }; - let captures = |scope: &str| { - let count = judged - .iter() - .filter(|(status, check)| { - *status == "pass" - && check - .reference - .as_ref() - .is_some_and(|reference| reference.scope == scope) - }) - .count(); - let missing = judged - .iter() - .filter(|(status, check)| { - *status != "pass" - && check - .reference - .as_ref() - .is_some_and(|reference| reference.scope == scope) - }) - .count(); - match (count, missing) { - (0, 0) => json!("-"), - (_, 0) => json!(format!("{count} to compare")), - _ => json!(format!("{count} to compare, {missing} not captured")), - } - }; - let drawn = screen - .figma - .as_ref() - .is_some_and(|figma| figma.drawn_widths.contains(&viewport.width)); - let completeness = match fold(&["rendered-completeness"]) { - "-" if drawn => "not-run", - "-" => "not drawn", - other => other, - }; - json!({ - "health": health, - "completeness": completeness, - "geometry": fold(&["text-align", "sequence-order", "section-layout", "bounds", "dimensions", "contained", "aligned", "spans", "visible"]), - "interaction": fold(&["interaction", "view-transition"]), - "page": if drawn { json!(if row["screenshot"].is_string() { "1 to compare" } else { "not captured" }) } else { json!("not drawn") }, - "section": captures("section"), - "asset": captures("asset"), - }) -} - -/// A check limited to some widths does not exist at the others. -fn applies(check: &CheckInput, width: u32) -> bool { - check.widths.is_empty() || check.widths.contains(&width) -} - -fn planned_items(plan: &PlanInput) -> usize { - plan.viewports - .iter() - .map(|viewport| { - plan.screens - .iter() - .flat_map(|screen| &screen.checks) - .filter(|check| applies(check, viewport.width)) - .count() - }) - .sum() -} - -/// A view-transition check clicks away from its screen, so every one moves -/// behind the screen's other checks, keeping their order otherwise. The -/// executor also returns to the screen's URL after it; together a plan -/// cannot measure the wrong page whatever order it was written in. -pub(crate) fn order_checks(input: &mut PlanInput) { - for screen in &mut input.screens { - screen - .checks - .sort_by_key(|check| check.kind == "view-transition"); - } -} diff --git a/crates/devup-mcp/src/server/runtime_audit/plan_columns.rs b/crates/devup-mcp/src/server/runtime_audit/plan_columns.rs new file mode 100644 index 00000000..8a0648fa --- /dev/null +++ b/crates/devup-mcp/src/server/runtime_audit/plan_columns.rs @@ -0,0 +1,127 @@ +/// One screen at one width by what was checked: health (renders, no +/// overflow, no clipped text), geometry (size and position), interaction +/// (states and view transitions), and the three visual levels, which stay +/// "to compare" until devup_visual_compare results come back through +/// devup_screen_plan status. +fn columns( + screen: &ScreenInput, + viewport: &ViewportInput, + judged: &[(&str, &CheckInput)], + row: &Value, +) -> Value { + let fold = |kinds: &[&str]| { + let statuses = judged + .iter() + .filter(|(_, check)| kinds.contains(&check.kind.as_str())) + .map(|(status, _)| *status) + .collect::>(); + if statuses.is_empty() { + "-" + } else if statuses.contains(&"fail") { + "fail" + } else if statuses.iter().all(|status| *status == "pass") { + "pass" + } else if statuses.contains(&"inconclusive") { + "inconclusive" + } else { + "not-run" + } + }; + let unreached = row["reasons"] + .as_array() + .into_iter() + .flatten() + .filter_map(Value::as_str) + .any(|reason| reason.starts_with("page error") || reason.starts_with("screen not reached")); + let health = if unreached { + "fail" + } else { + fold(&[ + "links-resolve", + "page-health", + "no-horizontal-overflow", + "no-clipped-content", + "styles-applied", + "no-clipped-text", + "no-low-contrast-text", + "image-loaded", + "native-appearance", + "stale-resources", + ]) + }; + let captures = |scope: &str| { + let count = judged + .iter() + .filter(|(status, check)| { + *status == "pass" + && check + .reference + .as_ref() + .is_some_and(|reference| reference.scope == scope) + }) + .count(); + let missing = judged + .iter() + .filter(|(status, check)| { + *status != "pass" + && check + .reference + .as_ref() + .is_some_and(|reference| reference.scope == scope) + }) + .count(); + match (count, missing) { + (0, 0) => json!("-"), + (_, 0) => json!(format!("{count} to compare")), + _ => json!(format!("{count} to compare, {missing} not captured")), + } + }; + let drawn = screen + .figma + .as_ref() + .is_some_and(|figma| figma.drawn_widths.contains(&viewport.width)); + let completeness = match fold(&["rendered-completeness"]) { + "-" if drawn => "not-run", + "-" => "not drawn", + other => other, + }; + json!({ + "health": health, + "completeness": completeness, + "geometry": fold(&["text-align", "sequence-order", "section-layout", "bounds", "dimensions", "contained", "aligned", "spans", "visible"]), + "interaction": fold(&["interaction", "view-transition"]), + "page": if drawn { json!(if row["screenshot"].is_string() { "1 to compare" } else { "not captured" }) } else { json!("not drawn") }, + "section": captures("section"), + "asset": captures("asset"), + }) +} + +/// A check limited to some widths does not exist at the others. +fn applies(check: &CheckInput, width: u32) -> bool { + check.widths.is_empty() || check.widths.contains(&width) +} + +fn planned_items(plan: &PlanInput) -> usize { + plan.viewports + .iter() + .map(|viewport| { + plan.screens + .iter() + .flat_map(|screen| &screen.checks) + .filter(|check| applies(check, viewport.width)) + .count() + }) + .sum() +} + +/// A view-transition check clicks away from its screen, so every one moves +/// behind the screen's other checks, keeping their order otherwise. The +/// executor also returns to the screen's URL after it; together a plan +/// cannot measure the wrong page whatever order it was written in. +pub(crate) fn order_checks(input: &mut PlanInput) { + for screen in &mut input.screens { + screen + .checks + .sort_by_key(|check| check.kind == "view-transition"); + } +} diff --git a/crates/devup-mcp/src/server/runtime_audit/screenshot_integrity.rs b/crates/devup-mcp/src/server/runtime_audit/screenshot_integrity.rs new file mode 100644 index 00000000..9488ddcb --- /dev/null +++ b/crates/devup-mcp/src/server/runtime_audit/screenshot_integrity.rs @@ -0,0 +1,197 @@ +enum Integrity { + Tampered(String), + Unverified(String), +} +struct VerifiedShot { + path: String, + sha256: String, + width: u32, + height: u32, +} +fn verify_screenshot( + policy: &OutputPolicy, + report_dir: &Path, + shot: &Value, + viewport: &ViewportInput, +) -> Result { + verify_capture(policy, report_dir, shot, Some(viewport)) +} + +/// An element capture has the element's size, not the viewport's; only its +/// bytes and PNG decoding are checked. +fn verify_capture( + policy: &OutputPolicy, + report_dir: &Path, + shot: &Value, + viewport: Option<&ViewportInput>, +) -> Result { + let path = shot["path"] + .as_str() + .ok_or_else(|| Integrity::Unverified("no path".into()))?; + let relative = Path::new(path); + if relative.is_absolute() + || relative + .components() + .any(|part| !matches!(part, std::path::Component::Normal(_))) + { + return Err(Integrity::Unverified(format!( + "{path} must be relative to the report directory, without .. or a root" + ))); + } + let joined = report_dir.join(relative); + let target = policy + .resolve(&joined.to_string_lossy()) + .map_err(|error| Integrity::Unverified(format!("{path}: {}", error.message)))?; + let bytes = target + .read_bounded(MAX_SCREENSHOT_BYTES) + .map_err(|error| Integrity::Unverified(format!("{path}: {}", error.message)))?; + let digest = sha256(&bytes); + if shot["sha256"].as_str() != Some(digest.as_str()) + || shot["bytes"].as_u64() != Some(bytes.len() as u64) + { + return Err(Integrity::Tampered(format!( + "{path}: its SHA-256 or length differs from the report" + ))); + } + let [width, height] = devup_mcp_visual::png_dimensions(&bytes).map_err(|_| { + Integrity::Unverified(format!( + "{path}: not a decodable PNG within 8192px per side" + )) + })?; + if let Some(viewport) = viewport { + let expected = (f64::from(viewport.width) * viewport.device_scale_factor).round() as u32; + let minimum = (f64::from(viewport.height) * viewport.device_scale_factor).round() as u32; + if width != expected || height < minimum { + return Err(Integrity::Unverified(format!( + "{path}: {width}x{height} is not {expected} wide and at least {minimum} tall; the capture does not show this viewport" + ))); + } + } + Ok(VerifiedShot { + path: target.display_path().display().to_string(), + sha256: digest, + width, + height, + }) +} + +/// The one place a finding's identity is made, so a later run of the same +/// check at the same width finds its earlier attempts. +fn comparison_id(screen: &ScreenInput, kind: &str, subject: &str, width: u32) -> String { + format!("{}|{kind}|{subject}|{width}", screen.id) +} + +fn diff_path(actual: &str, id: &str) -> String { + let directory = Path::new(actual).parent().unwrap_or(Path::new("")); + let name = id + .chars() + .map(|c| { + if c.is_ascii_alphanumeric() || c == '-' { + c + } else { + '_' + } + }) + .collect::(); + directory + .join("diff") + .join(format!("{name}.png")) + .display() + .to_string() +} + +fn array_text(value: &Value) -> String { + value + .as_array() + .into_iter() + .flatten() + .filter_map(Value::as_str) + .collect::>() + .join("; ") +} + +/// What kind of problem a failed check is, so a fix starts in the right +/// place: a page that does not render, content wider than its box, a box in +/// the wrong place or of the wrong size, a state or a transition missing. +pub(crate) fn failure_class(kind: &str, reason: &str) -> &'static str { + match kind { + "page-health" => "runtime-error", + "image-loaded" | "stale-resources" => "asset-mismatch", + "no-low-contrast-text" | "native-appearance" => "section-visual", + "rendered-completeness" => "page-visual", + "no-horizontal-overflow" | "no-clipped-text" => "overflow", + "bounds" if reason.starts_with("position") => "layout-position", + "bounds" | "dimensions" | "visible" => "layout-size", + "contained" | "aligned" | "spans" => "layout-position", + "interaction" => "interaction", + "view-transition" => "view-transition", + _ => "layout-position", + } +} + +/// A concrete first step for each class. The measured evidence names the +/// element where the executor reported one. +pub(crate) fn fix_hint(class: &str, kind: &str, measured: &Value) -> String { + match class { + "runtime-error" => include_str!("../text/runtime_audit-fix_hint-3a12f919.md").to_owned(), + "overflow" if kind == "no-clipped-text" => { + include_str!("../text/runtime_audit-fix_hint-b3941912.md").to_owned() + } + "overflow" => format!( + include_str!("../text/runtime_audit-fix_hint-975acb57.md"), + measured["widest"] + .as_str() + .map(|widest| format!(" (widest: {widest})")) + .unwrap_or_default() + ), + "layout-position" | "layout-size" if kind == "bounds" => { + include_str!("../text/runtime_audit-fix_hint-1f874fed.md").to_owned() + } + "layout-position" => include_str!("../text/runtime_audit-fix_hint-516a692f.md").to_owned(), + "layout-size" => include_str!("../text/runtime_audit-fix_hint-1ca5b7eb.md").to_owned(), + "code-drift" => include_str!("../text/runtime_audit-fix_hint-60ba4f27.md").to_owned(), + "converter-bug" => include_str!("../text/runtime_audit-fix_hint-0e677049.md").to_owned(), + "asset-mismatch" => include_str!("../text/runtime_audit-fix_hint-650b18d4.md").to_owned(), + "section-visual" => include_str!("../text/runtime_audit-fix_hint-a13b2885.md").to_owned(), + "page-visual" => include_str!("../text/runtime_audit-fix_hint-5960cebc.md").to_owned(), + "interaction" => include_str!("../text/runtime_audit-fix_hint-e19dc0a1.md").to_owned(), + "view-transition" => include_str!("../text/runtime_audit-fix_hint-7a003bba.md").to_owned(), + _ => "Re-run the check and read its reason.".to_owned(), + } +} + +fn finding( + screen: &ScreenInput, + viewport: &ViewportInput, + check: Option<(usize, &CheckInput)>, + class: &str, + reason: &str, + measured: &Value, + screenshot: Option<&str>, +) -> Value { + let kind = check.map_or("screen", |(_, check)| check.kind.as_str()); + let subject = check + .and_then(|(index, check)| { + check + .node_id + .clone() + .or_else(|| check.label.clone()) + .or_else(|| Some(format!("#{index}"))) + }) + .unwrap_or_else(|| "page".to_owned()); + json!({ + "id": comparison_id(screen, kind, &subject, viewport.width), + "class": class, + "screen": screen.id, + "routePath": screen.route_path, + "width": viewport.width, + "check": check.map(|(index, _)| index), + "kind": kind, + "label": check.and_then(|(_, check)| check.label.clone()), + "nodeId": check.and_then(|(_, check)| check.node_id.clone()), + "codeFile": screen.component_path, + "reason": reason, + "fixHint": fix_hint(class, kind, measured), + "evidence": {"screenshot": screenshot, "measured": measured}, + }) +} diff --git a/crates/devup-mcp/src/server/runtime_audit/tests.rs b/crates/devup-mcp/src/server/runtime_audit/tests.rs index 25e1e6f1..ae371939 100644 --- a/crates/devup-mcp/src/server/runtime_audit/tests.rs +++ b/crates/devup-mcp/src/server/runtime_audit/tests.rs @@ -1,5 +1,3 @@ - - #[cfg(test)] mod tests { use super::*; @@ -370,8 +368,10 @@ mod tests { #[test] fn drawn_widths_need_completeness_before_they_can_pass() { let mut screen = plan_input().screens.remove(0); - screen.figma = Some(serde_json::from_value(json!({"nodeId": "1:1", "drawnWidths": [1440]})).unwrap()); - let viewport: ViewportInput = serde_json::from_value(json!({"width": 1440, "height": 900})).unwrap(); + screen.figma = + Some(serde_json::from_value(json!({"nodeId": "1:1", "drawnWidths": [1440]})).unwrap()); + let viewport: ViewportInput = + serde_json::from_value(json!({"width": 1440, "height": 900})).unwrap(); let mut missing = json!({"status": "pass", "reasons": []}); require_completeness(&mut missing, &screen, &viewport, &[]); assert_eq!(missing["status"], "not-run"); @@ -395,7 +395,12 @@ mod tests { }; let node = |id: &str| json!({"css": format!("[data-figma-node-id=\"{id}\"]")}); assert!(ok(json!({"kind": "page-health"})).is_ok()); - for kind in ["sequence-order", "section-layout", "no-clipped-content", "styles-applied"] { + for kind in [ + "sequence-order", + "section-layout", + "no-clipped-content", + "styles-applied", + ] { assert!(ok(json!({"kind": kind})).is_ok()); } assert!(ok(json!({"kind": "styles-applied", "subject": node("1:2")})).is_err()); @@ -418,7 +423,10 @@ mod tests { ] { assert!(ok(json!({"kind": "rendered-completeness", "widths": [1440], "completeness": completeness})).is_err()); } - assert!(ok(json!({"kind": "rendered-completeness", "completeness": {"frameHeight": 100}})).is_err()); + assert!( + ok(json!({"kind": "rendered-completeness", "completeness": {"frameHeight": 100}})) + .is_err() + ); assert!(ok(json!({"kind": "no-clipped-text"})).is_ok()); assert!(ok(json!({"kind": "interaction", "state": "hover"})).is_ok()); @@ -691,6 +699,72 @@ mod tests { std::fs::remove_dir_all(dir).unwrap(); } + #[test] + fn tall_audit_frame_compares_clipped_capture_and_short_page_fails() { + for height in [5953, 5900, 500] { + let (dir, policy) = root(); + let input: PlanInput = serde_json::from_value(json!({ + "baseUrl": "http://localhost:3000", "allowedOrigins": ["http://localhost:3000"], + "viewports": [{"width":1440,"height":900}], + "screens": [{"id":"home","routePath":"/", "figma": { + "nodeId":"1:1","drawnWidths":[1440],"frames":[{"nodeId":"1:1","width":1440,"height":5953,"artifactId":"art-home"}] + },"checks":[{"kind":"rendered-completeness","widths":[1440],"completeness":{"frameHeight":5953}}]}] + })).unwrap(); + let written = plan( + input, + &policy, + Some(&dir.join("plan.json").to_string_lossy()), + ) + .unwrap(); + let bytes = png(1440, height); + std::fs::write(dir.join("frame-clipped.png"), &bytes).unwrap(); + let report = json!({"schemaVersion":1,"kind":REPORT_KIND,"planId":written["planId"], + "planSha256":sha256(&std::fs::read(dir.join("plan.json")).unwrap()),"status":"completed", + "runs":[{"viewport":{"width":1440,"height":900,"deviceScaleFactor":1},"environmentErrors":[],"screens":[{ + "id":"home","status":"completed","checks":[{"index":0,"status":"pass","measured":{}}], + "screenshot":{"path":"frame-clipped.png","sha256":sha256(&bytes),"bytes":bytes.len()} + }]}]}); + std::fs::write( + dir.join("report.json"), + serde_json::to_vec(&report).unwrap(), + ) + .unwrap(); + let result = ingest( + &policy, + &dir.join("plan.json").to_string_lossy(), + &dir.join("report.json").to_string_lossy(), + ) + .unwrap(); + let comparison = &result["visualComparisons"][0]; + assert!( + comparison["call"]["arguments"]["actual"] + .as_str() + .unwrap() + .ends_with("frame-clipped.png") + ); + if height == 5953 { + assert_eq!(comparison["blockers"], json!([]), "{result}"); + } else { + assert_eq!(result["verdict"], "fail", "{result}"); + let finding = result["findings"] + .as_array() + .unwrap() + .iter() + .find(|row| row["kind"] == "page-height") + .unwrap(); + assert!( + finding["reason"] + .as_str() + .unwrap() + .contains(&height.to_string()) + ); + assert!(finding["reason"].as_str().unwrap().contains("5953")); + } + drop(policy); + std::fs::remove_dir_all(dir).unwrap(); + } + } + /// Every failed check comes back as a finding of one class with the /// Figma node, the code file and a fix hint; a capture comes back as a /// ready asset comparison against the node's own render, and checks diff --git a/crates/devup-mcp/src/server/runtime_audit/write_plan.rs b/crates/devup-mcp/src/server/runtime_audit/write_plan.rs index 9c39a175..e040b8d4 100644 --- a/crates/devup-mcp/src/server/runtime_audit/write_plan.rs +++ b/crates/devup-mcp/src/server/runtime_audit/write_plan.rs @@ -1,4 +1,3 @@ - fn write_plan( input: PlanInput, policy: &OutputPolicy, @@ -70,8 +69,16 @@ fn write_plan( let mut template_transaction = OutputTransaction::new(); template_transaction.refuse_existing(); for (name, bytes) in [ - ("dev-environment.mjs", include_bytes!("../../../../../harness/runtime-audit/dev-environment.mjs").as_slice()), - ("resource-checks.mjs", include_bytes!("../../../../../harness/runtime-audit/resource-checks.mjs").as_slice()), + ( + "dev-environment.mjs", + include_bytes!("../../../../../harness/runtime-audit/dev-environment.mjs") + .as_slice(), + ), + ( + "resource-checks.mjs", + include_bytes!("../../../../../harness/runtime-audit/resource-checks.mjs") + .as_slice(), + ), ( "run.mjs", include_bytes!("../../../../../harness/runtime-audit/run.mjs").as_slice(), @@ -227,6 +234,7 @@ fn ingest_references( let mut dimension_rows = Vec::new(); let mut screen_rows = Vec::new(); let mut findings = Vec::new(); + let mut page_height_failed = false; let mut missing_nodes = Vec::new(); let mut link_table = Vec::::new(); let mut links = BTreeMap::>::new(); @@ -305,8 +313,19 @@ fn ingest_references( ); } if let Some(reported) = reported { - for check in reported["checks"].as_array().into_iter().flatten().filter(|check| check["kind"] == "links-resolve") { - link_table.extend(check["measured"]["linkTable"].as_array().into_iter().flatten().cloned()); + for check in reported["checks"] + .as_array() + .into_iter() + .flatten() + .filter(|check| check["kind"] == "links-resolve") + { + link_table.extend( + check["measured"]["linkTable"] + .as_array() + .into_iter() + .flatten() + .cloned(), + ); } } let mut outcomes = Vec::new(); @@ -350,20 +369,28 @@ fn ingest_references( && reported.is_some_and(|s| s["status"] == "completed"), ) }); - let status = if let Some(reason) = &check.unavailable_reason { if reason.starts_with("not-run:") { "not-run" } else { "inconclusive" } } else { match dimension - .as_ref() - .map(|row| &row["status"]) - .or_else(|| item.map(|item| &item["status"])) - .and_then(Value::as_str) - { - Some("pass") => "pass", - Some("fail") => "fail", - Some("inconclusive") => "inconclusive", - Some("skipped") => "skipped", - Some("not-run") => "not-run", - Some(_) => "inconclusive", - None => "not-run", - }}; + let status = if let Some(reason) = &check.unavailable_reason { + if reason.starts_with("not-run:") { + "not-run" + } else { + "inconclusive" + } + } else { + match dimension + .as_ref() + .map(|row| &row["status"]) + .or_else(|| item.map(|item| &item["status"])) + .and_then(Value::as_str) + { + Some("pass") => "pass", + Some("fail") => "fail", + Some("inconclusive") => "inconclusive", + Some("skipped") => "skipped", + Some("not-run") => "not-run", + Some(_) => "inconclusive", + None => "not-run", + } + }; *counts.entry(status).or_default() += 1; if status == "fail" && failures.len() < MAX_LISTED { failures.push(json!({ @@ -447,6 +474,9 @@ fn ingest_references( &outcomes, ); require_completeness(&mut row, screen, viewport, &judged); + row["frozenAnimations"] = reported + .map(|screen| json!({"executorDeclared": screen["frozenAnimations"], "verified": false})) + .unwrap_or(Value::Null); row["columns"] = columns(screen, viewport, &judged, &row); // A screen that never rendered fails as a runtime error, not as // the checks that could not measure it. @@ -479,7 +509,17 @@ fn ingest_references( continue; } screenshot_budget -= 1; - match verify_screenshot(policy, &report_dir, shot, viewport) { + let mut capture_viewport = viewport.clone(); + if screen.figma.as_ref().is_some_and(|figma| { + figma.drawn_widths.contains(&viewport.width) + && figma + .frames + .iter() + .any(|frame| frame.width == viewport.width && frame.height.is_some()) + }) { + capture_viewport.height = 1; + } + match verify_screenshot(policy, &report_dir, shot, &capture_viewport) { Ok(verified) => { screenshots_verified += 1; if let Some(figma) = screen @@ -487,6 +527,41 @@ fn ingest_references( .as_ref() .filter(|figma| figma.drawn_widths.contains(&viewport.width)) { + if let Some(height) = figma + .frames + .iter() + .find(|frame| frame.width == viewport.width) + .and_then(|frame| frame.height) + && (viewport.device_scale_factor - 1.0).abs() <= f64::EPSILON + && verified.height < height + { + page_height_failed = true; + let reason = format!( + include_str!("../text/runtime_audit-page-height.md"), + verified.width, verified.height, viewport.width, height + ); + let mut issue = finding( + screen, + viewport, + None, + "page-visual", + &reason, + &json!({"captureHeight": verified.height, "frameHeight": height}), + Some(&verified.path), + ); + issue["kind"] = json!("page-height"); + issue["id"] = + json!(comparison_id(screen, "page-height", "page", viewport.width)); + findings.push(issue); + if let Some(row) = screen_rows.last_mut() { + row["status"] = json!("fail"); + row["reasons"] + .as_array_mut() + .expect("screen reasons") + .push(json!(reason)); + row["columns"]["completeness"] = json!("fail"); + } + } drawn_width_shots.push((screen, figma.node_id.as_str(), verified)); } } @@ -529,7 +604,7 @@ fn ingest_references( } let planned = planned_items(&plan); let count = |status: &str| counts.get(status).copied().unwrap_or(0); - let verdict = if count("fail") > 0 && !environment_invalid { + let verdict = if (count("fail") > 0 || page_height_failed) && !environment_invalid { "fail" } else if count("pass") == planned && !environment_invalid && report["status"] == "completed" { "pass" @@ -798,200 +873,3 @@ fn dimension_row( } row } -enum Integrity { - Tampered(String), - Unverified(String), -} -struct VerifiedShot { - path: String, - sha256: String, - width: u32, - height: u32, -} -fn verify_screenshot( - policy: &OutputPolicy, - report_dir: &Path, - shot: &Value, - viewport: &ViewportInput, -) -> Result { - verify_capture(policy, report_dir, shot, Some(viewport)) -} - -/// An element capture has the element's size, not the viewport's; only its -/// bytes and PNG decoding are checked. -fn verify_capture( - policy: &OutputPolicy, - report_dir: &Path, - shot: &Value, - viewport: Option<&ViewportInput>, -) -> Result { - let path = shot["path"] - .as_str() - .ok_or_else(|| Integrity::Unverified("no path".into()))?; - let relative = Path::new(path); - if relative.is_absolute() - || relative - .components() - .any(|part| !matches!(part, std::path::Component::Normal(_))) - { - return Err(Integrity::Unverified(format!( - "{path} must be relative to the report directory, without .. or a root" - ))); - } - let joined = report_dir.join(relative); - let target = policy - .resolve(&joined.to_string_lossy()) - .map_err(|error| Integrity::Unverified(format!("{path}: {}", error.message)))?; - let bytes = target - .read_bounded(MAX_SCREENSHOT_BYTES) - .map_err(|error| Integrity::Unverified(format!("{path}: {}", error.message)))?; - let digest = sha256(&bytes); - if shot["sha256"].as_str() != Some(digest.as_str()) - || shot["bytes"].as_u64() != Some(bytes.len() as u64) - { - return Err(Integrity::Tampered(format!( - "{path}: its SHA-256 or length differs from the report" - ))); - } - let [width, height] = devup_mcp_visual::png_dimensions(&bytes).map_err(|_| { - Integrity::Unverified(format!( - "{path}: not a decodable PNG within 8192px per side" - )) - })?; - if let Some(viewport) = viewport { - let expected = (f64::from(viewport.width) * viewport.device_scale_factor).round() as u32; - let minimum = (f64::from(viewport.height) * viewport.device_scale_factor).round() as u32; - if width != expected || height < minimum { - return Err(Integrity::Unverified(format!( - "{path}: {width}x{height} is not {expected} wide and at least {minimum} tall; the capture does not show this viewport" - ))); - } - } - Ok(VerifiedShot { - path: target.display_path().display().to_string(), - sha256: digest, - width, - height, - }) -} - -/// The one place a finding's identity is made, so a later run of the same -/// check at the same width finds its earlier attempts. -fn comparison_id(screen: &ScreenInput, kind: &str, subject: &str, width: u32) -> String { - format!("{}|{kind}|{subject}|{width}", screen.id) -} - -fn diff_path(actual: &str, id: &str) -> String { - let directory = Path::new(actual).parent().unwrap_or(Path::new("")); - let name = id - .chars() - .map(|c| { - if c.is_ascii_alphanumeric() || c == '-' { - c - } else { - '_' - } - }) - .collect::(); - directory - .join("diff") - .join(format!("{name}.png")) - .display() - .to_string() -} - -fn array_text(value: &Value) -> String { - value - .as_array() - .into_iter() - .flatten() - .filter_map(Value::as_str) - .collect::>() - .join("; ") -} - -/// What kind of problem a failed check is, so a fix starts in the right -/// place: a page that does not render, content wider than its box, a box in -/// the wrong place or of the wrong size, a state or a transition missing. -pub(crate) fn failure_class(kind: &str, reason: &str) -> &'static str { - match kind { - "page-health" => "runtime-error", - "image-loaded" | "stale-resources" => "asset-mismatch", - "no-low-contrast-text" | "native-appearance" => "section-visual", - "rendered-completeness" => "page-visual", - "no-horizontal-overflow" | "no-clipped-text" => "overflow", - "bounds" if reason.starts_with("position") => "layout-position", - "bounds" | "dimensions" | "visible" => "layout-size", - "contained" | "aligned" | "spans" => "layout-position", - "interaction" => "interaction", - "view-transition" => "view-transition", - _ => "layout-position", - } -} - -/// A concrete first step for each class. The measured evidence names the -/// element where the executor reported one. -pub(crate) fn fix_hint(class: &str, kind: &str, measured: &Value) -> String { - match class { - "runtime-error" => include_str!("../text/runtime_audit-fix_hint-3a12f919.md").to_owned(), - "overflow" if kind == "no-clipped-text" => { - include_str!("../text/runtime_audit-fix_hint-b3941912.md").to_owned() - } - "overflow" => format!( - include_str!("../text/runtime_audit-fix_hint-975acb57.md"), - measured["widest"] - .as_str() - .map(|widest| format!(" (widest: {widest})")) - .unwrap_or_default() - ), - "layout-position" | "layout-size" if kind == "bounds" => { - include_str!("../text/runtime_audit-fix_hint-1f874fed.md").to_owned() - } - "layout-position" => include_str!("../text/runtime_audit-fix_hint-516a692f.md").to_owned(), - "layout-size" => include_str!("../text/runtime_audit-fix_hint-1ca5b7eb.md").to_owned(), - "code-drift" => include_str!("../text/runtime_audit-fix_hint-60ba4f27.md").to_owned(), - "converter-bug" => include_str!("../text/runtime_audit-fix_hint-0e677049.md").to_owned(), - "asset-mismatch" => include_str!("../text/runtime_audit-fix_hint-650b18d4.md").to_owned(), - "section-visual" => include_str!("../text/runtime_audit-fix_hint-a13b2885.md").to_owned(), - "page-visual" => include_str!("../text/runtime_audit-fix_hint-5960cebc.md").to_owned(), - "interaction" => include_str!("../text/runtime_audit-fix_hint-e19dc0a1.md").to_owned(), - "view-transition" => include_str!("../text/runtime_audit-fix_hint-7a003bba.md").to_owned(), - _ => "Re-run the check and read its reason.".to_owned(), - } -} - -fn finding( - screen: &ScreenInput, - viewport: &ViewportInput, - check: Option<(usize, &CheckInput)>, - class: &str, - reason: &str, - measured: &Value, - screenshot: Option<&str>, -) -> Value { - let kind = check.map_or("screen", |(_, check)| check.kind.as_str()); - let subject = check - .and_then(|(index, check)| { - check - .node_id - .clone() - .or_else(|| check.label.clone()) - .or_else(|| Some(format!("#{index}"))) - }) - .unwrap_or_else(|| "page".to_owned()); - json!({ - "id": comparison_id(screen, kind, &subject, viewport.width), - "class": class, - "screen": screen.id, - "routePath": screen.route_path, - "width": viewport.width, - "check": check.map(|(index, _)| index), - "kind": kind, - "label": check.and_then(|(_, check)| check.label.clone()), - "nodeId": check.and_then(|(_, check)| check.node_id.clone()), - "codeFile": screen.component_path, - "reason": reason, - "fixHint": fix_hint(class, kind, measured), - "evidence": {"screenshot": screenshot, "measured": measured}, - }) -} diff --git a/crates/devup-mcp/src/server/screen_plan.rs b/crates/devup-mcp/src/server/screen_plan.rs index e84b1d74..9a9cc9ae 100644 --- a/crates/devup-mcp/src/server/screen_plan.rs +++ b/crates/devup-mcp/src/server/screen_plan.rs @@ -32,9 +32,12 @@ include!("screen_plan/routes.rs"); include!("screen_plan/app_select.rs"); include!("screen_plan/draft.rs"); include!("screen_plan/actions.rs"); +include!("screen_plan/audit_base_url.rs"); include!("screen_plan/remap.rs"); include!("screen_plan/status.rs"); include!("screen_plan/draft_plan_with.rs"); include!("screen_plan/plan_route_group.rs"); include!("screen_plan/state_variants.rs"); include!("screen_plan/lookup.rs"); +include!("screen_plan/component_width.rs"); +include!("screen_plan/responsive_contract.rs"); diff --git a/crates/devup-mcp/src/server/screen_plan/actions.rs b/crates/devup-mcp/src/server/screen_plan/actions.rs index b396e16c..f90d7d35 100644 --- a/crates/devup-mcp/src/server/screen_plan/actions.rs +++ b/crates/devup-mcp/src/server/screen_plan/actions.rs @@ -394,6 +394,7 @@ pub(super) fn confirm(input: &ScreenPlanInput, policy: &OutputPolicy) -> Result< "nodeIds": route["nodeIds"], "drawnWidths": route["drawnWidths"], "states": state_exports(route), + "nextExport": if multi_width(route) { responsive_export_action(&plan, route, root) } else { Value::Null }, }) }) .collect::>()); @@ -593,11 +594,12 @@ pub(super) async fn audit_plan( } let config = read_config(root)?.unwrap_or_default(); let thresholds = config.visual_thresholds.clone().unwrap_or_default(); + let base_url = audit_base_url(input, &plan); let mut result = super::audit_plan::write( &plan, &super::audit_plan::AuditRequest { root, - base_url: input.base_url.as_deref().unwrap_or("http://localhost:3000"), + base_url: &base_url, base_urls: &input.base_urls, only: &input.urls, artifact_ids: &input.artifacts, @@ -612,6 +614,7 @@ pub(super) async fn audit_plan( entries.retain(|entry| !replaced.contains(&entry["url"])); for written in array(&result["plans"]) { entries.push(json!({"url": written["url"], "screen": written["screen"], + "app": written["app"], "baseUrl": written["baseUrl"], "state": written["state"], "missingReferences": written["missingReferences"], "planPath": written["planPath"], "reportPath": format!("{}/report.json", text(&written["reportDirectory"]).replace('\\', "/"))})); } @@ -630,6 +633,10 @@ pub(super) async fn audit_plan( } plan["assumptions"] = json!(assumptions); let max_fix_attempts = config.max_fix_attempts.unwrap_or(3); + if let Some(mismatch) = base_url_mismatch(&entries) { + result["baseUrlMismatch"] = mismatch; + } + plan["audit"]["baseUrl"] = json!(base_url); plan["audit"]["plans"] = json!(entries); plan["audit"]["maxFixAttempts"] = json!(max_fix_attempts); plan["audit"]["generatedAt"] = json!(now()); @@ -896,6 +903,8 @@ pub(super) async fn status( if plan["status"] != "confirmed" { return Ok(json!({ "status": "not-confirmed", + "componentWidthMismatches": plan["componentWidthMismatches"], + "assumptions": plan["assumptions"], "planId": plan["planId"], "questionRounds": question_rounds(array(&plan["questions"]), per_round(&plan)), "questions": ask_shape(array(&plan["questions"])), diff --git a/crates/devup-mcp/src/server/screen_plan/audit_base_url.rs b/crates/devup-mcp/src/server/screen_plan/audit_base_url.rs new file mode 100644 index 00000000..24af4ea8 --- /dev/null +++ b/crates/devup-mcp/src/server/screen_plan/audit_base_url.rs @@ -0,0 +1,38 @@ +/// The server auditPlan targets: the call's baseUrl, else the one an earlier +/// auditPlan recorded, so a later call limited to some `urls` does not +/// silently fall back to localhost:3000 and split the plans across servers. +fn audit_base_url(input: &ScreenPlanInput, plan: &Value) -> String { + input + .base_url + .clone() + .or_else(|| plan["audit"]["baseUrl"].as_str().map(str::to_owned)) + .unwrap_or_else(|| "http://localhost:3000".to_owned()) +} + +fn base_url_mismatch(entries: &[Value]) -> Option { + let mut origins = BTreeMap::>::new(); + for entry in entries { + if let Some(base) = entry["baseUrl"].as_str() { + origins + .entry(text(&entry["app"]).to_owned()) + .or_default() + .insert(base.to_owned()); + } + } + let mismatched = origins + .iter() + .filter(|(_, bases)| bases.len() > 1) + .map(|(app, bases)| { + let urls = entries + .iter() + .filter(|entry| text(&entry["app"]) == app.as_str()) + .map(|entry| json!({"url": entry["url"], "baseUrl": entry["baseUrl"]})) + .collect::>(); + json!({"app": app, "baseUrls": bases, "urls": urls}) + }) + .collect::>(); + (!mismatched.is_empty()).then(|| json!({ + "apps": mismatched, + "fix": "The audit plans of one app point at different servers. Run auditPlan again for every URL with one baseUrl (or baseUrls per app) so each report measures the same build.", + })) +} diff --git a/crates/devup-mcp/src/server/screen_plan/component_width.rs b/crates/devup-mcp/src/server/screen_plan/component_width.rs new file mode 100644 index 00000000..87747f59 --- /dev/null +++ b/crates/devup-mcp/src/server/screen_plan/component_width.rs @@ -0,0 +1,84 @@ +pub(super) fn component_width_mismatches( + snapshot: &devup_mcp_figma::Snapshot, + roots: &[String], +) -> Vec { + let mut families = BTreeMap::<(String, String), BTreeMap>>::new(); + for screen in roots { + let Some(root) = snapshot.nodes.get(screen) else { continue }; + let view = root.typed_view(); + let Some(width) = view.number("width").or_else(|| view.value("absoluteBoundingBox")?["width"].as_f64()) else { continue }; + let mut stack = view.child_ids().map(str::to_owned).collect::>(); + let mut seen = BTreeSet::new(); + while let Some(id) = stack.pop() { + if !seen.insert(id.clone()) { continue; } + let Some(node) = snapshot.nodes.get(&id) else { continue }; + let view = node.typed_view(); + if view.bool("visible") == Some(false) { continue; } + stack.extend(view.child_ids().map(str::to_owned)); + if node.node_type != "INSTANCE" { continue; } + let (Some(name), Some(master)) = (view.name(), view.string("mainComponentId")) else { continue }; + let family = view.string("mainComponentSetId").or_else(|| view.string("mainComponentName")); + let Some(family) = family else { continue }; + families.entry((name.to_lowercase(), family.into())).or_default() + .entry(width.to_string()).or_default().push(json!({"screen":screen,"nodeId":id,"master":master,"width":width})); + } + } + let mut result = Vec::new(); + for ((role, family), widths) in families { + let majority = |members: &[Value]| -> Option<(String, usize, usize)> { + let screens = members.iter().filter_map(|member| member["screen"].as_str()).collect::>(); + let mut counts = BTreeMap::>::new(); + for member in members { + if let (Some(master), Some(screen)) = (member["master"].as_str(), member["screen"].as_str()) { + counts.entry(master.into()).or_default().insert(screen.into()); + } + } + counts.into_iter().find_map(|(master, count)| (count.len() > screens.len()/2).then_some((master,count.len(),screens.len()))) + }; + for (width, members) in &widths { + let Some((expected, count, total)) = majority(members) else { continue }; + for suspect in members.iter().filter(|member| member["master"] != expected) { + let other = widths.iter().filter(|(other, _)| *other != width).find_map(|(other, members)| { + let (master,count,total) = majority(members)?; + (suspect["master"] == master).then(|| json!({"width":other,"master":master,"count":count,"total":total})) + }); + let Some(other) = other else { continue }; + let correct = members.iter().filter(|member| member["master"] == expected).min_by_key(|member| text(&member["nodeId"])); + let Some(correct) = correct else { continue }; + result.push(json!({"code":"DEVUP_DESIGN_COMPONENT_WIDTH_MISMATCH","role":role,"family":family, + "screen":suspect["screen"],"width":suspect["width"],"nodeId":suspect["nodeId"], + "usedMaster":suspect["master"],"expectedMaster":expected,"correctNodeId":correct["nodeId"], + "expectedCount":count,"usedCount":members.iter().filter(|member|member["master"] == suspect["master"]).filter_map(|member|member["screen"].as_str()).collect::>().len(),"totalCount":total,"otherWidth":other,"render":"unavailable"})); + } + } + } + result +} + +pub(super) fn apply_component_width_mismatches(plan: &mut Value, mismatches: &[Value]) { + let mut assumptions = array(&plan["assumptions"]).to_vec(); + for mismatch in mismatches { + let decision = format!("use {} ({})",text(&mismatch["expectedMaster"]),text(&mismatch["correctNodeId"])); + let assumption = json!({"about":mismatch["nodeId"],"decision":decision, + "reason":format!("{}/{} screens at {} use {}; used master is the majority at another width; render={}. Implement one responsive component, not a second variant.",mismatch["expectedCount"],mismatch["totalCount"],mismatch["width"],text(&mismatch["expectedMaster"]),text(&mismatch["render"])), + "evidence":mismatch,"recordedAt":now()}); + if let Some(existing) = assumptions.iter_mut().find(|row| row["about"] == mismatch["nodeId"] && row["evidence"]["code"] == mismatch["code"]) { + *existing = assumption; + } else { assumptions.push(assumption); } + } + plan["assumptions"] = json!(assumptions); + plan["componentWidthMismatches"] = json!(mismatches); + for screen in plan["screens"].as_array_mut().into_iter().flatten() { + let corrections = mismatches.iter().filter(|row| array(&screen["frames"]).iter().any(|frame|frame["nodeId"] == row["screen"])).cloned().collect::>(); + screen["componentSubstitutions"] = json!(corrections); + } +} + +pub(super) fn attach_component_width_plan(result: &mut Value, root: &str) -> Result<(), DevupError> { + let Some(plan) = read_plan(root)? else { return Ok(()) }; + let mismatches = array(&plan["componentWidthMismatches"]); + if mismatches.is_empty() { return Ok(()) } + result["componentWidthMismatches"] = json!(mismatches); + result["componentWidthAssumptions"] = json!(array(&plan["assumptions"]).iter().filter(|row| row["evidence"]["code"] == "DEVUP_DESIGN_COMPONENT_WIDTH_MISMATCH").collect::>()); + Ok(()) +} diff --git a/crates/devup-mcp/src/server/screen_plan/draft.rs b/crates/devup-mcp/src/server/screen_plan/draft.rs index 83ed49f1..0a7e9076 100644 --- a/crates/devup-mcp/src/server/screen_plan/draft.rs +++ b/crates/devup-mcp/src/server/screen_plan/draft.rs @@ -532,6 +532,8 @@ pub(super) async fn draft_with( let proposal_rows = proposal(array(&plan["questions"])); Ok(json!({ "status": "draft", + "componentWidthMismatches": plan["componentWidthMismatches"], + "assumptions": plan["assumptions"], "planId": plan["planId"], "proposal": proposal_rows, "proposalMarkdown": proposal_markdown(&proposal_rows), diff --git a/crates/devup-mcp/src/server/screen_plan/draft_plan_with.rs b/crates/devup-mcp/src/server/screen_plan/draft_plan_with.rs index 62817947..99e5fae4 100644 --- a/crates/devup-mcp/src/server/screen_plan/draft_plan_with.rs +++ b/crates/devup-mcp/src/server/screen_plan/draft_plan_with.rs @@ -649,6 +649,8 @@ pub(super) fn draft_plan_with( "routes": [], "assumptions": [], }); + let mismatches = array(&plan["source"]["componentWidthMismatches"]).to_vec(); + apply_component_width_mismatches(&mut plan, &mismatches); refresh_inventory_checks(&mut plan); Ok(plan) } diff --git a/crates/devup-mcp/src/server/screen_plan/responsive_contract.rs b/crates/devup-mcp/src/server/screen_plan/responsive_contract.rs new file mode 100644 index 00000000..24f367c0 --- /dev/null +++ b/crates/devup-mcp/src/server/screen_plan/responsive_contract.rs @@ -0,0 +1,58 @@ +pub(super) fn responsive_export_action(plan: &Value, route: &Value, root: &str) -> Value { + let mut frames = array(&route["nodeIds"]).iter().enumerate().collect::>(); + frames.sort_by(|(left, _), (right, _)| { + array(&route["drawnWidths"]).get(*left).and_then(Value::as_f64).unwrap_or_default() + .total_cmp(&array(&route["drawnWidths"]).get(*right).and_then(Value::as_f64).unwrap_or_default()) + }); + let mut arguments = json!({"projectRoot": root, "frameIds": frames.iter().map(|(_, id)| *id).collect::>(), "outputs": ["responsiveTsx"]}); + if let Some(url) = plan["source"]["url"].as_str() { + arguments["url"] = json!(url); + } + json!({"tool": "devup_figma_export", "arguments": arguments}) +} + +fn multi_width(route: &Value) -> bool { + array(&route["drawnWidths"]).iter().map(Value::to_string).collect::>().len() > 1 + && array(&route["nodeIds"]).len() > 1 && route["implementation"] != "state" +} + +pub(super) fn responsive_required(root: &str, frames: &[String], outputs: &[String]) -> Result, DevupError> { + let responsive = outputs.iter().any(|output| output == "responsiveTsx"); + if !outputs.iter().any(|output| matches!(output.as_str(), "tsx" | "componentTsx" | "pageScaffold" | "responsiveTsx")) { + return Ok(None); + } + for ancestor in Path::new(root).ancestors() { + let Some(plan) = read_plan(&ancestor.to_string_lossy())? else { continue; }; + if plan["status"] != "confirmed" { continue; } + let route = array(&plan["routes"]).iter().find(|route| multi_width(route) && frames.iter().any(|id| { + plan["source"]["nodeId"] == id.as_str() || array(&route["nodeIds"]).contains(&json!(id)) || array(&plan["frameInventory"]["frames"]).iter().any(|frame| { + frame["nodeId"] == id.as_str() && array(&frame["ancestors"]).iter().any(|parent| array(&route["nodeIds"]).contains(parent)) + }) + })); + if let Some(route) = route { + if responsive && !outputs.iter().any(|output| matches!(output.as_str(), "tsx" | "componentTsx")) + && array(&route["nodeIds"]).iter().all(|id| frames.iter().any(|frame| id == frame)) { + return Ok(None); + } + return Ok(Some(json!({"status": "responsive_required", "written": false, + "responsiveRequired": {"code": "DEVUP_RESPONSIVE_REQUIRED", "url": route["url"], "frameIds": route["nodeIds"], + "reason": "This confirmed URL has multiple drawn widths. responsiveTsx is the deliverable; never start from single-width TSX or change generated layout values."}, + "nextAction": responsive_export_action(&plan, route, root)}))); + } + break; + } + Ok(None) +} + +fn responsive_status(plan: &Value, route: &Value, evidence: &ProjectEvidence, records: &[Value]) -> Value { + let ids = array(&route["nodeIds"]); + let matching = records.iter().filter(|record| array(&record["frameIds"]).iter().any(|id| ids.contains(id)) + || ids.contains(&record["rootNodeId"])).collect::>(); + let responsive = matching.iter().find(|record| record["output"] == "responsiveTsx" + && ids.iter().all(|id| array(&record["frameIds"]).contains(id))); + let sources = evidence.route_sources.get(text(&route["url"])).cloned().unwrap_or_default(); + json!({"required": multi_width(route), "recorded": !matching.is_empty(), "responsive": responsive.is_some(), + "issue": if multi_width(route) && responsive.is_none() && !matching.is_empty() { Some("single-width export used for a multi-width URL") } else { None }, + "nextAction": if multi_width(route) { responsive_export_action(plan, route, &evidence.root.to_string_lossy()) } else { Value::Null }, + "generatedDrift": responsive.map(|record| super::export_record::generated_drift(record, &evidence.root, &sources))}) +} diff --git a/crates/devup-mcp/src/server/screen_plan/state.rs b/crates/devup-mcp/src/server/screen_plan/state.rs index 35485e26..616bc110 100644 --- a/crates/devup-mcp/src/server/screen_plan/state.rs +++ b/crates/devup-mcp/src/server/screen_plan/state.rs @@ -518,6 +518,7 @@ pub(super) struct ProjectEvidence { pub root: PathBuf, /// Route pages and the components they render, relative to `root`. pub sources: Vec, + pub route_sources: BTreeMap>, pub icon_files: Vec, pub og_files: Vec, /// (value, where it was found). @@ -627,6 +628,8 @@ pub(super) async fn project_evidence(project_root: &str) -> Result>::new(); for route in array(&inventory["routes"]) { let path = text(&route["route"]).to_owned(); + evidence.route_sources.insert(path.clone(), std::iter::once(text(&route["page"]).to_owned()) + .chain(array(&route["components"]).iter().map(|component| text(component).to_owned())).collect()); evidence .routes .push((path.clone(), text(&route["page"]).to_owned())); diff --git a/crates/devup-mcp/src/server/screen_plan/status.rs b/crates/devup-mcp/src/server/screen_plan/status.rs index 77bb4c34..ead312c3 100644 --- a/crates/devup-mcp/src/server/screen_plan/status.rs +++ b/crates/devup-mcp/src/server/screen_plan/status.rs @@ -40,6 +40,7 @@ pub(super) fn status_report( let audited = !widths.is_empty(); let mut rows = Vec::new(); let mut states = Vec::new(); + let export_records = super::export_record::records(&evidence.root); for route in array(&plan["routes"]) { if route["implementation"] == "state" { states.push(json!({"screen": route["screen"], "name": route["name"], "hostUrl": route["hostUrl"]})); @@ -133,6 +134,7 @@ pub(super) fn status_report( "nodeIds": route["nodeIds"], "figmaPages": route["pages"], "states": route_states, + "generatedExport": responsive_status(plan, route, evidence, &export_records), "file": page.clone().map_or_else(|| route["file"].clone(), Value::from), "implemented": implemented_state(page.is_some(), &route["drawnWidths"], &per_width), "widths": per_width, @@ -344,6 +346,13 @@ pub(super) fn status_report( for item in &mut queue { if let Some(row) = rows.iter().find(|row| row["url"] == item["url"]) { item["states"] = row["states"].clone(); + item["generatedExport"] = row["generatedExport"].clone(); + if row["generatedExport"]["required"] == true { + item["nextExport"] = row["generatedExport"]["nextAction"].clone(); + } + if row["generatedExport"]["issue"].is_string() || row["generatedExport"]["generatedDrift"]["findings"].as_array().is_some_and(|findings| !findings.is_empty()) { + item["done"] = json!(false); + } } } let theme_queue = theme_sync_queue(plan, &evidence.root.to_string_lossy()); @@ -355,6 +364,8 @@ pub(super) fn status_report( let final_report = super::audit_plan::final_report(plan, &rows, &trailing) + &link_markdown; let blocking_urls = rows.iter().filter(|row| { row["implemented"] != "yes" || array(&row["widths"]).iter().any(|width| width["result"] != "pass") + || row["generatedExport"]["issue"].is_string() + || row["generatedExport"]["generatedDrift"]["findings"].as_array().is_some_and(|findings| !findings.is_empty()) }).map(|row| row["url"].clone()).collect::>(); let done = blocking_urls.is_empty() && !theme_pending && !links_fail && incomplete == 0 && open.is_empty(); let converter_bugs = plan["audit"]["converterBugs"] @@ -384,6 +395,7 @@ pub(super) fn status_report( "audit": audit.map(|audit| json!({"verdict": audit["verdict"], "planId": audit["planId"], "reason": audit["reason"]})), "markdownTable": table, "assumptions": assumptions, + "componentWidthMismatches": plan["componentWidthMismatches"], "siteCompleteness": completeness, "defaults": plan["defaults"], "designFixes": design_fixes, @@ -686,12 +698,11 @@ pub(super) fn export_gate(request: &ExportRequest<'_>) -> Result devup_mcp_figma::Snapshot { + let mut nodes = serde_json::Map::new(); + for (screen, width, master) in [ + ("mobile-a", 360, "mobile"), + ("mobile-b", 360, "mobile"), + ("mobile-c", 360, "mobile"), + ("home", 360, "desktop"), + ("wide", 1440, "desktop"), + ] { + let footer = format!("{screen}-footer"); + nodes.insert(screen.into(), json!({"id":screen,"type":"FRAME","fields":{ + "name":screen,"width":width,"childrenIds":[footer]}})); + nodes.insert(footer.clone(), json!({"id":footer,"type":"INSTANCE","fields":{ + "name":"Footer","mainComponentName":"Footer","mainComponentId":master}})); + } + serde_json::from_value(json!({"fileKey":"fixture","roots":["mobile-a","mobile-b","mobile-c","home","wide"],"nodes":nodes,"diagnostics":[]})).unwrap() +} + +#[test] +fn width_mismatch_selects_majority_sibling_and_keeps_singletons() { + let snapshot = width_fixture(); + let mismatches = component_width_mismatches(&snapshot, &snapshot.roots); + assert_eq!(mismatches.len(), 1); + assert_eq!(mismatches[0]["nodeId"], "home-footer"); + assert_eq!(mismatches[0]["usedMaster"], "desktop"); + assert_eq!(mismatches[0]["expectedMaster"], "mobile"); + assert_eq!(mismatches[0]["expectedCount"], 3); + assert_eq!(mismatches[0]["totalCount"], 4); + assert_eq!(mismatches[0]["correctNodeId"], "mobile-a-footer"); +} + +#[test] +fn width_mismatch_does_not_resolve_ties_or_unrelated_families() { + let mut snapshot = width_fixture(); + snapshot.nodes.remove("mobile-c"); + snapshot.nodes.remove("mobile-c-footer"); + snapshot.nodes.get_mut("mobile-b-footer").unwrap().fields.insert("mainComponentId".into(), json!("desktop")); + assert!(component_width_mismatches(&snapshot, &snapshot.roots).is_empty()); + snapshot.nodes.get_mut("mobile-b-footer").unwrap().fields.insert("mainComponentName".into(), json!("OtherFooter")); + snapshot.nodes.get_mut("home-footer").unwrap().fields.insert("mainComponentName".into(), json!("OtherFooter")); + assert!(component_width_mismatches(&snapshot, &snapshot.roots).is_empty()); +} + +#[test] +fn width_mismatch_records_idempotent_assumption_and_status() { + let snapshot = width_fixture(); + let mut plan = json!({"assumptions":[],"routes":[],"screens":[]}); + let mismatches = component_width_mismatches(&snapshot, &snapshot.roots); + apply_component_width_mismatches(&mut plan, &mismatches); + apply_component_width_mismatches(&mut plan, &mismatches); + assert_eq!(plan["assumptions"].as_array().unwrap().len(), 1); + assert_eq!(plan["assumptions"][0]["about"], "home-footer"); + assert_eq!(plan["assumptions"][0]["decision"], "use mobile (mobile-a-footer)"); + assert_eq!(plan["assumptions"][0]["evidence"]["render"], "unavailable"); + let report = status_report(&plan, &evidence(), None); + assert_eq!(report["componentWidthMismatches"][0]["code"], "DEVUP_DESIGN_COMPONENT_WIDTH_MISMATCH"); +} + +#[test] +fn width_mismatch_draft_persists_source_decision() { + let snapshot = width_fixture(); + let mismatches = component_width_mismatches(&snapshot,&snapshot.roots); + let plan = draft_plan(&[screen("home","Home",360.0)],&BTreeMap::from([("home".into(),"/".into())]),&evidence(),json!({"componentWidthMismatches":mismatches}),None).unwrap(); + assert_eq!(plan["componentWidthMismatches"][0]["expectedMaster"],"mobile"); + assert_eq!(plan["assumptions"][0]["decision"],"use mobile (mobile-a-footer)"); +} + +#[test] +fn width_mismatch_export_reads_persisted_assumptions() { + let fixture = Fixture::new(); + let snapshot = width_fixture(); + let mut plan = json!({"kind":PLAN_KIND,"schemaVersion":1,"screens":[],"assumptions":[]}); + apply_component_width_mismatches(&mut plan,&component_width_mismatches(&snapshot,&snapshot.roots)); + fixture.write(PLAN_FILE,&plan.to_string()); + let mut exported = json!({"source":{"nodeId":"home"}}); + attach_component_width_plan(&mut exported,fixture.root()).unwrap(); + assert_eq!(exported["componentWidthMismatches"][0]["nodeId"],"home-footer"); + assert_eq!(exported["componentWidthAssumptions"][0]["decision"],"use mobile (mobile-a-footer)"); +} diff --git a/crates/devup-mcp/src/server/screen_plan_tests/responsive_contract.rs b/crates/devup-mcp/src/server/screen_plan_tests/responsive_contract.rs new file mode 100644 index 00000000..e653ecb6 --- /dev/null +++ b/crates/devup-mcp/src/server/screen_plan_tests/responsive_contract.rs @@ -0,0 +1,73 @@ +#[test] +fn multi_width_export_redirects_single_width_code_before_collection() { + let fixture = Fixture::new(); + write_plan_file(&fixture, &confirmed_plan()); + for output in ["tsx", "componentTsx", "pageScaffold"] { + let response = responsive_required(fixture.root(), &["10:1".into()], &[output.into()]).unwrap().unwrap(); + assert_eq!(response["status"], "responsive_required"); + assert_eq!(response["written"], false); + assert_eq!(response["nextAction"]["arguments"]["outputs"], json!(["responsiveTsx"])); + assert_eq!(response["nextAction"]["arguments"]["frameIds"], json!(["10:2", "10:1"])); + } + assert!(responsive_required(fixture.root(), &["10:1".into(), "10:2".into()], &["responsiveTsx".into()]).unwrap().is_none()); + assert!(responsive_required(fixture.root(), &["10:3".into()], &["tsx".into()]).unwrap().is_none()); + assert!(responsive_required(fixture.root(), &["10:1".into()], &["outline".into()]).unwrap().is_none()); + assert!(responsive_required(fixture.root(), &["10:1".into()], &["responsiveTsx".into()]).unwrap().is_some()); + assert!(responsive_required(fixture.root(), &["10:1".into(), "10:2".into()], &["responsiveTsx".into(), "tsx".into()]).unwrap().is_some()); +} + +#[test] +fn responsive_scaffold_uses_the_merged_module_instead_of_the_first_frame() { + let fixture = Fixture::new(); + let code = "export function Main() { return ; }"; + let mut result = json!({"responsiveTsx": code, "frames": [{"nodeId": "10:1", "tsx": "desktop"}, {"nodeId": "10:2", "tsx": "mobile"}]}).as_object().unwrap().clone(); + let paths = BTreeMap::from([("pageScaffold".into(), fixture.root().into())]); + super::super::projection::page_scaffold::attach(&mut result, + &super::super::projection::page_scaffold::PageScaffoldOptions { route: "/".into(), write: false, overwrite: false }, + &paths, None, &BTreeMap::new(), &fixture.policy(), false).unwrap(); + assert!(array(&result["pageScaffold"]["files"]).iter().any(|file| file["content"] == code)); +} + +#[test] +fn status_reports_single_width_export_and_responsive_recovery() { + let fixture = Fixture::new(); + super::super::export_record::write(&fixture.policy(), fixture.root(), "file", "10:1", &json!({"tsx": ""})).unwrap(); + let evidence = ProjectEvidence { root: fixture.0.clone(), ..evidence() }; + let report = status_report(&confirmed_plan(), &evidence, None); + assert_eq!(report["rows"][0]["generatedExport"]["responsive"], false); + assert!(report["rows"][0]["generatedExport"]["issue"].is_string()); + let item = array(&report["workQueue"]).iter().find(|item| item["url"] == "/").unwrap(); + assert_eq!(item["nextExport"]["arguments"]["frameIds"], json!(["10:2", "10:1"])); +} + +#[test] +fn status_detects_collapsed_removed_and_added_styles_without_identity_attributes() { + let fixture = Fixture::new(); + super::super::export_record::write(&fixture.policy(), fixture.root(), "file", "10:0", &json!({ + "responsiveTsx": "", + "responsiveEvidence": {"frames": [{"nodeId": "10:1"}, {"nodeId": "10:2"}]} + })).unwrap(); + fixture.write("src/components/Main.tsx", ""); + let evidence = ProjectEvidence { root: fixture.0.clone(), route_sources: BTreeMap::from([("/".into(), vec!["src/components/Main.tsx".into()])]), ..evidence() }; + let report = status_report(&confirmed_plan(), &evidence, None); + let export = &report["rows"][0]["generatedExport"]; + assert_eq!(export["responsive"], true); + let findings = array(&export["generatedDrift"]["findings"]); + assert!(findings.iter().any(|finding| finding["kind"] == "responsive-array-collapsed" && finding["prop"] == "w")); + assert!(findings.iter().any(|finding| finding["kind"] == "prop-removed" && finding["prop"] == "gap")); + assert!(findings.iter().any(|finding| finding["kind"] == "style-added" && finding["prop"] == "h")); + assert!(findings.iter().all(|finding| finding["file"] == "src/components/Main.tsx")); +} + +#[test] +fn status_allows_link_and_event_wiring_without_layout_drift() { + let fixture = Fixture::new(); + super::super::export_record::write(&fixture.policy(), fixture.root(), "file", "10:0", &json!({ + "responsiveTsx": "", + "responsiveEvidence": {"frames": [{"nodeId": "10:1"}, {"nodeId": "10:2"}]} + })).unwrap(); + fixture.write("src/components/Main.tsx", ""); + let evidence = ProjectEvidence { root: fixture.0.clone(), route_sources: BTreeMap::from([("/".into(), vec!["src/components/Main.tsx".into()])]), ..evidence() }; + let report = status_report(&confirmed_plan(), &evidence, None); + assert_eq!(report["rows"][0]["generatedExport"]["generatedDrift"]["findings"], json!([])); +} diff --git a/crates/devup-mcp/src/server/text/audit_plan-url_plan-a1031a26.md b/crates/devup-mcp/src/server/text/audit_plan-url_plan-a1031a26.md index 0c8f7690..bef05df7 100644 --- a/crates/devup-mcp/src/server/text/audit_plan-url_plan-a1031a26.md +++ b/crates/devup-mcp/src/server/text/audit_plan-url_plan-a1031a26.md @@ -1 +1 @@ -The {width}px frame {node_id} is {height}px tall, over the {MAX_VIEWPORT_SIDE}px a viewport can be; its page comparison stays inconclusive and its sections carry the visual check. \ No newline at end of file +The {width}px frame {node_id} is {height}px tall, outside the 1..={MAX_VIEWPORT_SIDE}px viewport range. Use the default viewport height and capture the page at its recorded drawn frame height for visual comparison. diff --git a/crates/devup-mcp/src/server/text/devup-screen-plan.md b/crates/devup-mcp/src/server/text/devup-screen-plan.md index b9feadb4..c6b72fec 100644 --- a/crates/devup-mcp/src/server/text/devup-screen-plan.md +++ b/crates/devup-mcp/src/server/text/devup-screen-plan.md @@ -1,8 +1,11 @@ Plan which Figma screen becomes which URL before any page is written, confirm it up front, then report it URL by width (action: draft | confirm | status | assume | auditPlan | defer). draft reads the linked Section or frame (or the Bridge selection, or `screens` from an earlier devup_figma_explore) and proposes a route only from evidence: an existing page built from the frame, a nav link label bound to an existing href, a navigationTargets entry, or .devup/screen-plan.config.json (modalKeywords, routeAliases, widthGrouping). A route inferred from a layer name is routeEvidence name-match and is put to the user; with no evidence it is UNVERIFIED and asked. Only frames under the link are candidates, never the rest of the file; frames in trash, archive, draft, copy or hidden areas are excluded and listed (excludeKeywords/includeKeywords in the config). Frames sharing a name in one section are asked about per group; the same name in different sections, a screen split across sections, a frame outside any section and duplicates each get an explicit question with a "the Figma structure is wrong" option whose frames are reported, never built. It writes .devup/screen-plan.json and .md and returns `proposal` (and `proposalMarkdown`): every open decision as one table row with the recommended answer and the alternatives. Do not interview the user one decision at a time: show the table and ask ONE question with your question tool - accept it, or name the rows to change - in the language the user first wrote in (translate for display only; answers stay keyed by question id with the original labels or custom text). confirm with `acceptRecommended: true` fills every unanswered applicable question with its recommended option, so send only the changed rows in `answers`; without it, confirm refuses and names the missing ids. `questions`/`questionRounds` remain for reviewing individual rows. A confirm that puts two screens on one URL writes nothing and returns `details.conflict` (both screens and the questions placing them) and `details.retryAnswers` with a distinct suggested URL to retry with. After confirm, never ask the user: take the recommended or most conservative option and record it with assume. status compares the confirmed plan with the routes on disk and an optional devup_runtime_audit plan/report: per URL, implemented yes/no and per-width pass, fail with reason, or not-run - a width without an audit item is never pass - plus every assumption. draft also lists the stack defaults - Next App Router on vinext, a new project from dev-five-git/service-template, and the dependency update `bunx @dependency-check-updates/cli -d -u --rm` before implementing - and asks once whether a next-CLI app migrates to vinext; with several workspace apps it picks the app by meaning (the public app unless the frames clearly say admin, per screen in a mixed file) and asks only when no app looks like the public site; then the site defaults it fills in without asking (an interaction recipe per element role, restrained; one shared transition; reduced motion; view transitions always on (React's ViewTransition; skipped only when the project config opts out), verified by a runtime-audit view-transition check; metadata, lang, alt text, not-found) and asks only what needs a file or a fact: favicon and share image (from existing files or Figma logo/og nodes, never generated), site name and URL when the project does not say, analytics when the project shows it, and scroll reveal; `extraQuestions` adds the agent's own questions to the same rounds so nothing is asked after confirm; .devup/screen-plan.config.json may pre-answer them (favicon, ogImage, siteName, siteUrl, motion). status adds site completeness: favicon, metadata per URL, share image, hover/focus coverage and reduced motion as filled, asked-and-answered or missing. auditPlan writes one devup_runtime_audit plan per confirmed URL from the export artifacts: page health, overflow and clipped text at every width; the page, each section and each asset against its Figma render (devup_visual_compare calls returned by status) and the size and position of each section, card and asset at the drawn widths, exactly (half a pixel of rounding, no tolerance); merged pages found per width through data-figma-node-ids; dynamic routes at concrete URLs from generateStaticParams, the site's links or the detail frame; hover and focus; a view-transition check placed last; a Vite plugin beside each app's vite.config that strips the ids from production builds unless DEVUP_AUDIT=1; missing referencePngs and renders are listed with the export call that makes them. status then classifies every failure (runtime-error, overflow, code-drift or converter-bug for geometry - compared with the export record each export writes to .devup/exports, a converter bug written as a repro to .devup/converter-bugs and never retried - layout-position, layout-size, asset-mismatch, section-visual, page-visual, interaction, view-transition) with its Figma node, code file and fix hint, folds visualResults in, marks a finding unresolved with its evidence after maxFixAttempts (config, default 3), and returns a workQueue per URL and the finalReport. defer records a decision met after confirm as pending with its recommended option so the work never waits; assume takes it at the end, and a destructive one stays unbuilt. Done = zero fails, or listed as unresolved with evidence. The devup-implementation-plan skill (devup_skills) carries the whole workflow; /devup starts it. Audit build policy: do not change app build config for auditing or register a strip plugin, including any legacy auditPlan suggestion to do so. DevupUI strips audit attributes by default; DEVUP_AUDIT=1 keeps them. Figma node attributes are off by default; figmaNodeIds true is opt-in only where supported by the export schema. Missing anchors leave coverage unverified. Before reporting done, run the runtime audit for every confirmed URL at every required width (default 1440/1100/858/390), read the report, fix failures and rerun until every cell passes. Missing cells are not-run and block completion. At drawn widths require rendered completeness and page, section and asset visual comparisons. Check sequence-order and section-layout at every width. +For every multi-width URL, status and its work queue provide the responsiveTsx export call with all sibling frameIds in width order. responsiveTsx is the deliverable: never start from single-width tsx, and never change generated layout values. Only wire links/events/data. generatedExport reports recorded responsive coverage, flags single-width exports used for multi-width URLs, and includes heuristic generatedDrift with confidence and source file paths; absent mappings remain unverified. Ask known site, structure and URL decisions up front in one runtime question call. Keep table approval separate from individual decisions. New user-owned decisions after confirm still use the question tool: headers up to 30 characters, two to four options, recommended first and labelled "(Recommended)". Never ask for numbers in chat. Record the chosen answer, decision id, affected model/endpoint/file and rationale through existing defer/assume actions; do not invent an ask action. Re-confirm source model changes before dependent work. Without a question tool, report the limitation and leave the decision unresolved. Workers escalate to the supervisor; continue independent work while answers are pending. The saved plan, draft, confirm and status expose frameInventory (every listed frame, including excluded, out-of-scope and site assets) and linkLabelCheck. Incomplete exploration is refused. A unique link-label match follows its frame's confirmed route; explicit prototype destinations take priority and ambiguous labels stay unresolved. A found frame without a bound route is still unresolved. Search frameInventory and run devup_figma_search before claiming a screen does not exist; an unlisted name is unverified. Never bind an unresolved label to a guessed route. After confirm, extract used COMPONENT_SET/COMPONENT nodes first with outputs [componentTsx] into components/common or components/layout. Record mainComponent id -> generated file and artifact identity in .devup/component-map.json. Assemble each page from generated TSX by screen-plan section (a direct child of the screen frame root). Never hand-write TSX from Figma nodes. Audit through component/section file mappings and explicit rendered targets, not DOM Figma attributes; missing mapping support stays unverified. Report unsupported section exports or overrides rather than inventing them. Connected URLs: run `links-resolve` at every width. Each same-origin anchor contributes source URL, accessible label, href, resolved pathname and width to `linkTable` (also Markdown in `finalReport`). GET each distinct target once, including concrete dynamic hrefs, and reject HTTP failures, error pages and `unplanned-target` URLs absent from the confirmed plan and existing routes. Any failing link row blocks done. + +Cross-screen component consistency: `DEVUP_DESIGN_COMPONENT_WIDTH_MISMATCH` identifies a shared role whose master disagrees with the strict screen majority at its drawn width and instead belongs to another width. Singletons participate. Draft automatically records `use ()` as an assumption, with counts and native-node PNG references for both candidates (node render, PNG, scale 1). Images are compared when available; unavailable renders are recorded, and the width majority still decides. Do not ask again or create a second Footer/Header/Nav variant: implement one responsive component. Status and project exports expose the correction. Audit planning uses the correct sibling subtree and render, screen width, and the suspect's original y position. The uncorrected whole-page PNG is not a valid corrected-page reference and remains unavailable. diff --git a/crates/devup-mcp/src/server/text/runtime_audit-page-height.md b/crates/devup-mcp/src/server/text/runtime_audit-page-height.md new file mode 100644 index 00000000..bcbd5048 --- /dev/null +++ b/crates/devup-mcp/src/server/text/runtime_audit-page-height.md @@ -0,0 +1 @@ +The page capture is {}x{}px, shorter than the drawn Figma frame at {}x{}px. Render the missing page content before comparing pixels. diff --git a/crates/devup-mcp/src/server/text/runtime_audit-visual_comparison-8010a061.md b/crates/devup-mcp/src/server/text/runtime_audit-visual_comparison-8010a061.md index c675108f..f13d2fad 100644 --- a/crates/devup-mcp/src/server/text/runtime_audit-visual_comparison-8010a061.md +++ b/crates/devup-mcp/src/server/text/runtime_audit-visual_comparison-8010a061.md @@ -1 +1 @@ -The page is {}px tall in a {}px viewport, so the full-page capture is {}x{}. devup_visual_compare needs the viewport to be the reference's size: plan this width at the frame's drawn height. \ No newline at end of file +The page capture is {}x{}px but the drawn Figma frame is {}x{}px. Capture the frame-sized region when the page is tall enough; a shorter page is a page-height failure, not a viewport-height problem. diff --git a/harness/runtime-audit/README.md b/harness/runtime-audit/README.md index 1edfd832..6a4c4f9f 100644 --- a/harness/runtime-audit/README.md +++ b/harness/runtime-audit/README.md @@ -1,5 +1,21 @@ # Real-render verification +At drawn widths, `figma.frames[].height` records the reference frame height, +independently of the viewport height (tall frames can use a 900px viewport). +The main page screenshot is clipped at the top-left to the drawn frame size +when the page is at least that tall. Content below the frame remains covered by +the layout and completeness checks, not the page pixel comparison. A shorter +page is captured at its actual height, never padded: completeness and ingest's +`page-height` finding report the actual and required heights. Older plans without +a frame height keep full-page captures; re-plan to enable frame-sized captures. + +Geometry and captures pause running animations at `currentTime = 0` and disable +CSS transitions after scroll reveals settle. The runner restores motion before +interaction and view-transition checks, without changing reduced-motion media +preferences. Each screen reports `frozenAnimations` with unique target count, +selectors and animation count. Ingest exposes this as executor-declared evidence, +not independently verified browser state. + For a dev server, capture its stdout/stderr to a log and pass `--server-log /absolute/path/server.log` to `run.mjs` (or `serverLog` to `runPlan`). The file must already exist. Only output appended during the audit diff --git a/harness/runtime-audit/run.mjs b/harness/runtime-audit/run.mjs index 192b9080..c1f0c1cf 100644 --- a/harness/runtime-audit/run.mjs +++ b/harness/runtime-audit/run.mjs @@ -277,6 +277,10 @@ export function observeClippedContent(page) { // A scroller is reachable content only when the user can see it scrolls. if (!vertical && ['auto', 'scroll'].includes(style.overflowX) && scrollbar) continue; if (element.closest('[aria-hidden="true"]')) continue; + const media = [...element.querySelectorAll('img, svg, video, canvas')]; + const decorative = style.pointerEvents === 'none' && !element.textContent.trim() + && media.every((node) => node.tagName === 'IMG' ? node.getAttribute('alt') === '' : node.tagName.toLowerCase() === 'svg'); + if (decorative) continue; const animated = [element, ...element.querySelectorAll('*')].some((node) => { const s = getComputedStyle(node); return (s.animationName && s.animationName !== 'none') || (node.getAnimations?.().length ?? 0) > 0; @@ -345,8 +349,42 @@ export function observeCompleteness(page, completeness, width, subject) { let imageBehind = false; let backgroundExposed = true; let opacity = 1; - for (let node = element; node && node.nodeType === 1; node = node.parentElement) { + const centerX = rect.left + rect.width / 2; + const centerY = rect.top + rect.height / 2; + const covers = (candidate) => { + const box = candidate.getBoundingClientRect(); + return box.width > 0 && box.height > 0 && centerX >= box.left && centerX <= box.right && centerY >= box.top && centerY <= box.bottom; + }; + const opaque = (color) => /^rgb\(/.test(color) || /,\s*1\)$/.test(color); + const stackingContext = (nodeStyle) => nodeStyle.isolation === 'isolate' + || (nodeStyle.position !== 'static' && nodeStyle.zIndex !== 'auto') + || Number(nodeStyle.opacity) < 1 || nodeStyle.transform !== 'none' || nodeStyle.filter !== 'none'; + let child = null; + for (let node = element; node && node.nodeType === 1; child = node, node = node.parentElement) { const nodeStyle = getComputedStyle(node); + // A positioned child with a negative z-index (a background layer the + // generator draws as a sibling of the content) paints between this + // node's own background and the text. It is visible when this node is + // its stacking context or paints no background of its own. + if (backgroundExposed && child && (stackingContext(nodeStyle) || !opaque(nodeStyle.backgroundColor))) { + const underlays = [...node.children].filter((layer) => { + if (layer === child || !shown(layer)) return false; + const layerStyle = getComputedStyle(layer); + return layerStyle.position !== 'static' && layerStyle.zIndex !== 'auto' && Number(layerStyle.zIndex) < 0 && covers(layer); + }); + // Later siblings paint above earlier ones; nearest first means last first. + for (const layer of underlays.reverse()) { + const painted = [layer, ...layer.querySelectorAll('*')].filter((part) => shown(part) && covers(part)).reverse(); + for (const part of painted) { + const partStyle = getComputedStyle(part); + if (/^(IMG|PICTURE|VIDEO|CANVAS|SVG|svg)$/.test(part.tagName) || (partStyle.backgroundImage && partStyle.backgroundImage !== 'none')) imageBehind = true; + if (partStyle.backgroundColor && partStyle.backgroundColor !== 'rgba(0, 0, 0, 0)') { + backgrounds.push(partStyle.backgroundColor); + layers.push({ color: partStyle.backgroundColor, opacity: Number(partStyle.opacity) }); + } + } + } + } opacity *= Number(nodeStyle.opacity); layers.push({ color: nodeStyle.backgroundColor, opacity: Number(nodeStyle.opacity) }); if (backgroundExposed) { @@ -700,6 +738,49 @@ export async function settleScrollReveals(page, { stepMs = 120, maxMs = 15000 } }, { stepMs, maxMs }); } +export async function freezeAnimations(page) { + return page.evaluate(() => { + if (window.__devupFrozenMotion) return window.__devupFrozenMotion.report; + const animations = document.getAnimations().filter((animation) => animation.playState === 'running' || animation.pending); + const saved = animations.map((animation) => ({ animation, currentTime: animation.currentTime, playState: animation.playState })); + const selector = (element) => { + if (!(element instanceof Element)) return '(unknown target)'; + if (element.id) return `#${CSS.escape(element.id)}`; + const parts = []; + while (element && element !== document.documentElement) { + const siblings = [...element.parentElement.children]; + parts.unshift(`${element.localName}:nth-child(${siblings.indexOf(element) + 1})`); + element = element.parentElement; + } + return `html > ${parts.join(' > ')}`; + }; + const selectors = [...new Set(animations.map((animation) => selector(animation.effect?.target)))]; + for (const animation of animations) { + animation.pause(); + animation.currentTime = 0; + } + const style = document.createElement('style'); + style.textContent = '*,*::before,*::after{transition-property:none!important;transition-duration:0s!important;transition-delay:0s!important}'; + document.documentElement.append(style); + const report = { count: selectors.length, selectors, animationCount: animations.length }; + window.__devupFrozenMotion = { saved, style, report }; + return report; + }); +} + +export async function restoreAnimations(page) { + await page.evaluate(() => { + const frozen = window.__devupFrozenMotion; + if (!frozen) return; + frozen.style.remove(); + for (const { animation, currentTime, playState } of frozen.saved) { + animation.currentTime = currentTime; + if (playState === 'running') animation.play(); + } + delete window.__devupFrozenMotion; + }); +} + async function waitReady(page, plan, timeout) { await waitStylesStable(page).catch(() => null); await page.evaluate(async () => { @@ -835,6 +916,7 @@ async function runViewport(browser, plan, viewport, out, deadline, references, r } reached = true; record.url = page.url(); + record.frozenAnimations = await freezeAnimations(page); const layoutBefore = await layoutFingerprint(page); const metrics = await pageMetrics(page); if (metrics.viewportWidth !== viewport.width) { @@ -940,7 +1022,7 @@ async function runViewport(browser, plan, viewport, out, deadline, references, r second = await freshLoad({ browser, contextOptions, url: page.url(), timeout: stepTimeout, install: (fresh) => guardOrigins(fresh, allowed, blocked), - ready: (fresh) => waitReady(fresh, plan, stepTimeout), + ready: async (fresh) => { await waitReady(fresh, plan, stepTimeout); await freezeAnimations(fresh); }, }); } catch (error) { reloadError = String(error?.message ?? error).split('\n')[0]; @@ -949,20 +1031,21 @@ async function runViewport(browser, plan, viewport, out, deadline, references, r continue; } if (check.kind === 'rendered-completeness') { - record.checks.push({ ...item, ...evaluateRenderedCompleteness(check.completeness, await observeCompleteness(page, check.completeness, viewport.width)) }); + const observed = await observeCompleteness(page, check.completeness, viewport.width); + const frame = screen.figma?.frames?.find((frame) => frame.width === viewport.width); + const result = evaluateRenderedCompleteness(check.completeness, observed); + if (frame?.height && observed.pageHeight < frame.height) { + result.status = 'fail'; + result.reason = `the page is ${observed.pageHeight}px tall, shorter than the ${frame.height}px drawn frame`; + } + record.checks.push({ ...item, ...result }); continue; } if (check.kind === 'no-clipped-text') { record.checks.push({ ...item, ...evaluateClippedText(await clippedText(page, check.subject)) }); continue; } - if (check.kind === 'interaction') { - const observed = await observeInteraction(page, check, stepTimeout).catch((error) => ({ subjectCount: 1, error: String(error?.message ?? error) })); - record.checks.push({ ...item, ...(observed.error - ? { status: 'inconclusive', reason: `the subject could not be ${check.state === 'focus' ? 'focused' : 'hovered'}: ${observed.error.split('\n')[0]}`, measured: {} } - : evaluateInteraction(check, observed)) }); - continue; - } + if (check.kind === 'interaction') continue; if (check.kind === 'capture') { const target = locate(page, check.subject); const count = await target.count(); @@ -971,7 +1054,7 @@ async function runViewport(browser, plan, viewport, out, deadline, references, r continue; } const shot = `${viewport.width}x${viewport.height}@${viewport.deviceScaleFactor ?? 1}-${safeName(screen.id)}-c${index}.png`; - const bytes = await target.screenshot({ animations: 'disabled', caret: 'hide' }); + const bytes = await target.screenshot({ animations: 'allow', caret: 'hide' }); await writeFile(join(out, shot), bytes); record.checks.push({ ...item, status: 'pass', reason: 'captured for the server to compare with its Figma render', measured: {}, screenshot: { path: shot, sha256: sha256(bytes), bytes: bytes.length } }); continue; @@ -994,10 +1077,14 @@ async function runViewport(browser, plan, viewport, out, deadline, references, r } } const name = `${viewport.width}x${viewport.height}@${viewport.deviceScaleFactor ?? 1}-${safeName(screen.id)}.png`; + const frame = screen.figma?.drawnWidths?.includes(viewport.width) + ? screen.figma.frames?.find((frame) => frame.width === viewport.width) + : undefined; + const captureHeight = frame?.height ? Math.min(metrics.fullHeight, frame.height) : metrics.fullHeight; const bytes = await page.screenshot({ fullPage: true, - clip: { x: 0, y: 0, width: metrics.viewportWidth, height: metrics.fullHeight }, - animations: 'disabled', + clip: { x: 0, y: 0, width: metrics.viewportWidth, height: captureHeight }, + animations: 'allow', caret: 'hide', }); await writeFile(join(out, name), bytes); @@ -1008,6 +1095,14 @@ async function runViewport(browser, plan, viewport, out, deadline, references, r if (layoutAfter !== layoutBefore) { environmentErrors.push({ kind: 'layout-unstable', message: `${screen.id}: layout changed between measurement and screenshot` }); } + await restoreAnimations(page); + for (const [index, check] of screen.checks.entries()) { + if (check.kind !== 'interaction' || check.unavailableReason || (check.widths?.length && !check.widths.includes(viewport.width))) continue; + const observed = await observeInteraction(page, check, stepTimeout).catch((error) => ({ subjectCount: 1, error: String(error?.message ?? error) })); + record.checks.push({ index, kind: check.kind, label: check.label, ...(observed.error + ? { status: 'inconclusive', reason: `the subject could not be ${check.state === 'focus' ? 'focused' : 'hovered'}: ${observed.error.split('\n')[0]}`, measured: {} } + : evaluateInteraction(check, observed)) }); + } // Links on this page, so a dynamic route ([slug]) can be audited at the // URLs the site itself links to. const anchors = await page.evaluate(() => [...document.querySelectorAll('a[href]')].flatMap((anchor) => { @@ -1049,6 +1144,7 @@ async function runViewport(browser, plan, viewport, out, deadline, references, r if (!invalidateReloadedScreen(record, environmentErrors.slice(errorsAtStart))) throw error; notMeasured('inconclusive', record.reason); } finally { + await restoreAnimations(page).catch(() => {}); environmentErrors.push(...await readServerLog()); if (invalidateReloadedScreen(record, environmentErrors.slice(errorsAtStart))) reached = false; } diff --git a/harness/runtime-audit/test/browser.test.mjs b/harness/runtime-audit/test/browser.test.mjs index f1183e97..ba1be3a0 100644 --- a/harness/runtime-audit/test/browser.test.mjs +++ b/harness/runtime-audit/test/browser.test.mjs @@ -71,6 +71,39 @@ function serve() { return new Promise((resolve) => server.listen(0, '127.0.0.1', () => resolve(server))); } +test('tall drawn frames clip the main screenshot and short pages fail completeness', async () => { + const server = createServer((request, response) => { + const height = request.url === '/short' ? 5900 : 6174; + response.writeHead(200, { 'content-type': 'text/html' }).end(``); + }); + await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); + const baseUrl = `http://127.0.0.1:${server.address().port}`; + const out = await mkdtemp(join(tmpdir(), 'devup-tall-frame-')); + try { + const plan = join(out, 'plan.json'); + await writeFile(plan, JSON.stringify({ schemaVersion: 1, kind: 'devup-runtime-audit-plan', planId: 'tall', plan: { + baseUrl, allowedOrigins: [baseUrl], viewports: [{ width: 1440, height: 900 }], + screens: ['tall', 'short'].map((id) => ({ id, routePath: `/${id}`, + figma: { nodeId: 'home', drawnWidths: [1440], frames: [{ width: 1440, height: 5953, nodeId: 'home' }] }, + checks: [{ kind: 'rendered-completeness', completeness: { frameHeight: 5953 } }], + })), + } })); + const report = await runPlan({ plan, out, maxMinutes: 1 }); + const [tall, short] = report.runs[0].screens; + const bytes = await readFile(join(out, tall.screenshot.path)); + assert.equal(bytes.readUInt32BE(16), 1440); + assert.equal(bytes.readUInt32BE(20), 5953); + assert.equal(tall.checks[0].status, 'pass'); + assert.equal(short.checks[0].status, 'fail'); + assert.match(short.checks[0].reason, /5900.*5953/); + const shortBytes = await readFile(join(out, short.screenshot.path)); + assert.equal(shortBytes.readUInt32BE(20), 5900); + } finally { + server.close(); + await rm(out, { recursive: true, force: true }); + } +}); + const CHECKS = [ { kind: 'no-horizontal-overflow', label: 'page' }, { kind: 'contained', label: 'badge text', subject: { testId: 'badge-text' }, container: { testId: 'badge' } }, @@ -463,3 +496,28 @@ test('white outline no-fill button overrides the reset and retains contrast over assert.equal(evaluateNoLowContrastText({}, await observeCompleteness(page, {}, 800)).status, 'fail'); } finally { await browser.close(); } }); + +test('a negative z-index sibling layer behind text counts as its background', async () => { + const { chromium } = await import('playwright'); + const { evaluateNoLowContrastText } = await import('../checks.mjs'); + const browser = await chromium.launch({ headless: true }); + try { + const page = await browser.newPage(); + // The generator draws a section background as an absolutely positioned + // sibling with z-index -1 inside an isolated container. + await page.setContent(`
+
+

Over navy

`); + assert.equal(evaluateNoLowContrastText({}, await observeCompleteness(page, {}, 800)).status, 'pass'); + // An image layer cannot be judged from colours: inconclusive, never a false fail. + await page.setContent(`
+
+

Over a photo

`); + assert.equal(evaluateNoLowContrastText({}, await observeCompleteness(page, {}, 800)).status, 'inconclusive'); + // White text on a white underlay still fails. + await page.setContent(`
+
+

Invisible

`); + assert.equal(evaluateNoLowContrastText({}, await observeCompleteness(page, {}, 800)).status, 'fail'); + } finally { await browser.close(); } +}); diff --git a/harness/runtime-audit/test/layout.test.mjs b/harness/runtime-audit/test/layout.test.mjs index 63b8bed3..3a248160 100644 --- a/harness/runtime-audit/test/layout.test.mjs +++ b/harness/runtime-audit/test/layout.test.mjs @@ -6,6 +6,52 @@ import { observeInteraction } from '../run.mjs'; import { evaluateInteraction } from '../checks.mjs'; import { observeCompleteness } from '../run.mjs'; +test('geometry freezes marquee at its start and restores animation and transitions', async () => { + const { freezeAnimations, restoreAnimations } = await import('../run.mjs'); + const { chromium } = await import('playwright'); + const browser = await chromium.launch(); + try { + const page = await browser.newPage(); + await page.setContent('
Marquee
'); + await page.evaluate(() => { const animation = document.getAnimations()[0]; animation.currentTime = 5000; }); + assert.ok((await page.locator('#track').boundingBox()).x < 0); + const frozen = await freezeAnimations(page); + assert.equal(frozen.count, 1); + assert.deepEqual(frozen.selectors, ['#track']); + assert.equal((await page.locator('#track').boundingBox()).x, 8); + assert.equal(await page.locator('#track').evaluate((e) => getComputedStyle(e).transitionDuration), '0s'); + await restoreAnimations(page); + assert.equal(await page.evaluate(() => document.getAnimations()[0].playState), 'running'); + assert.equal(await page.locator('#track').evaluate((e) => getComputedStyle(e).transitionDuration), '0.2s'); + } finally { await browser.close(); } +}); + +test('runner measures an infinite marquee at rest and declares frozen targets', async () => { + const { createServer } = await import('node:http'); + const { mkdtemp, writeFile, rm } = await import('node:fs/promises'); + const { tmpdir } = await import('node:os'); + const { join } = await import('node:path'); + const server = createServer((request, response) => { + response.setHeader('content-type', 'text/html'); + response.end('
Marquee
'); + }); + await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); + const dir = await mkdtemp(join(tmpdir(), 'devup-marquee-')); + try { + const baseUrl = `http://127.0.0.1:${server.address().port}`; + const path = join(dir, 'plan.json'); + await writeFile(path, JSON.stringify({ schemaVersion: 1, kind: 'devup-runtime-audit-plan', planId: 'marquee', plan: { baseUrl, allowedOrigins: [baseUrl], viewports: [{ width: 390, height: 900 }], screens: [{ id: 'home', routePath: '/', checks: [{ kind: 'bounds', subject: { css: '#track' }, container: { css: 'main' }, expected: { x: 0, y: 0, width: 200, height: 40 } }] }] } })); + const report = await runPlan({ plan: path, out: join(dir, 'out'), maxMinutes: 1 }); + const screen = report.runs[0].screens[0]; + assert.equal(screen.checks[0].status, 'pass', JSON.stringify(screen)); + assert.deepEqual(screen.frozenAnimations.selectors, ['#track']); + assert.equal(screen.frozenAnimations.count, 1); + } finally { + await new Promise((resolve) => server.close(resolve)); + await rm(dir, { recursive: true, force: true }); + } +}); + test('decorative and marquee contrast is excluded without hiding ordinary animated text', async () => { const { chromium } = await import('playwright'); const browser = await chromium.launch(); @@ -137,6 +183,21 @@ test('internal nav and thumbnail clipping fails even without page overflow', asy } finally { await browser.close(); } }); +test('a decorative fill layer cropping its image is not clipped content', async () => { + const { chromium } = await import('playwright'); + const browser = await chromium.launch({ headless: true }); + const page = await browser.newPage({ viewport: { width: 858, height: 900 } }); + const svg = "data:image/svg+xml,"; + try { + const layer = (alt) => `
${alt}
`; + await page.setContent(layer('')); + assert.equal((await observeClippedContent(page)).clipped.length, 0); + // A meaningful image cut off by its box is still reported. + await page.setContent(layer('Product photo')); + assert.deepEqual((await observeClippedContent(page)).clipped.map((item) => item.name), ['layer']); + } finally { await browser.close(); } +}); + test('text alignment follows Figma even when its text box stays full width', () => { assert.equal(evaluateTextAlignment({ edge: 'center' }, { count: 1, visible: true, align: 'left', direction: 'ltr' }).status, 'fail'); assert.equal(evaluateTextAlignment({ edge: 'center' }, { count: 1, visible: true, align: 'center', direction: 'ltr' }).status, 'pass'); From f0686a44a856fc6535ddd4110c20e17da8f9caca Mon Sep 17 00:00:00 2001 From: leader Date: Wed, 28 Oct 2026 01:32:15 +0900 Subject: [PATCH 140/149] docs(skills): component-first workflow and guide updates --- .../server/skills/devup-implementation-plan/SKILL.md | 9 ++++++++- crates/devup-mcp/src/server/skills/devup/SKILL.md | 6 +++++- crates/devup-mcp/src/server/skills/manifest.json | 12 ++++++------ .../src/server/text/guide-GUIDE-c682b0c5.md | 4 ++++ 4 files changed, 23 insertions(+), 8 deletions(-) diff --git a/crates/devup-mcp/src/server/skills/devup-implementation-plan/SKILL.md b/crates/devup-mcp/src/server/skills/devup-implementation-plan/SKILL.md index 3626ea1a..92eddf45 100644 --- a/crates/devup-mcp/src/server/skills/devup-implementation-plan/SKILL.md +++ b/crates/devup-mcp/src/server/skills/devup-implementation-plan/SKILL.md @@ -22,7 +22,11 @@ description: | screens, routes, URL, screen plan, devup_screen_plan, /devup --- -## Project intake comes first +## Project intake comes first + +When replacing an inlined instance with a shared component, carry over every `instanceOverrides` entry through a new variant or prop, or explicitly report it as unresolved. Never discard `// @devup-override` annotations or the required `DEVUP_CODEGEN_INSTANCE_OVERRIDE` review item silently. + +For a confirmed multi-width URL, `responsiveTsx` is the deliverable. Export all sibling frameIds in width order using screen-plan status's next export action. Never start from single-width `tsx`/`componentTsx` for its page or sections. Only wire links/events/data; never change generated layout values, collapse arrays, or add hard-coded sizes/layout wrappers. Review `generatedExport.generatedDrift` in status; its confidence-labelled findings are heuristic and missing mappings remain unverified. `/devup` is the entry point for new projects, existing features, bug fixes, Figma page implementation and refactors. Figma screen planning is a branch @@ -588,3 +592,6 @@ These rules apply to every plan this workflow produces. - Done means real data, real handlers and no leftovers: no hard-coded fixture arrays standing in for an endpoint, no `TODO` or "replace when" comments, no inputs without a handler, no form submit without a network call, every referenced public asset present. A "wire to the API now" answer needs an `operationId` that exists in `openapi.json`; otherwise the screen is reported unfinished. - Ask every user-owned decision with the runtime question tool, before or after confirm: recommended option first, labelled "(Recommended)", two to four options and a header up to 30 characters. Show evidence in prose, never a numbered chat question ending with "tell me the numbers to change". Keep table approval separate from individual decisions in the same question call. Workers escalate to the supervisor. Record the chosen answer, decision id, affected model/endpoint/file and rationale with existing `defer`/`assume` actions, never an invented `ask` action. Model changes require source model updates and re-confirmation before dependent work. If no question tool exists, report the limitation and leave the user-owned decision unresolved; continue independent work. - After editing code, run `bun run lint:fix` first and fix by hand only what it cannot fix; do not re-sort JSX props or reformat by hand, and do not run a bare `prettier`. +## Wrong-Width Component Masters + +Treat `DEVUP_DESIGN_COMPONENT_WIDTH_MISMATCH` as an automatically resolved design placement error. The screen plan records the strict same-width majority, the expected master and a correct sibling instance, plus native-node PNG comparisons when available (otherwise `render=unavailable`). Follow that assumption without another question. Header/Footer/Nav remains one responsive component across widths, not a second variant. Audit planning substitutes the correct sibling's height and children at screen width and the suspect's original y position. The original whole-page PNG still depicts the design error and must not be accepted as a corrected-page visual reference. diff --git a/crates/devup-mcp/src/server/skills/devup/SKILL.md b/crates/devup-mcp/src/server/skills/devup/SKILL.md index a3c97bb0..9964aee6 100644 --- a/crates/devup-mcp/src/server/skills/devup/SKILL.md +++ b/crates/devup-mcp/src/server/skills/devup/SKILL.md @@ -11,7 +11,11 @@ description: | build pages or a site from a design, even without an explicit /devup. --- -## Project intake comes first +## Project intake comes first + +When replacing an inlined instance with a shared component, carry over every `instanceOverrides` entry through a new variant or prop, or explicitly report it as unresolved. Never discard `// @devup-override` annotations or the required `DEVUP_CODEGEN_INSTANCE_OVERRIDE` review item silently. + +For a confirmed multi-width URL, `responsiveTsx` is the deliverable. Export all sibling frameIds in width order using screen-plan status's next export action. Never start from single-width `tsx`/`componentTsx` for its page or sections. Only wire links/events/data; never change generated layout values, collapse arrays, or add hard-coded sizes/layout wrappers. Review `generatedExport.generatedDrift` in status; its confidence-labelled findings are heuristic and missing mappings remain unverified. `/devup` is the entry point for new projects, existing features, bug fixes, Figma page implementation and refactors. Figma screen planning is a branch diff --git a/crates/devup-mcp/src/server/skills/manifest.json b/crates/devup-mcp/src/server/skills/manifest.json index 1fec446f..9e6f8201 100644 --- a/crates/devup-mcp/src/server/skills/manifest.json +++ b/crates/devup-mcp/src/server/skills/manifest.json @@ -13,8 +13,8 @@ "documents": [ { "path": "SKILL.md", - "bytes": 3559, - "sha256": "9919baab3af27236c5bdb02b0b60d02570dd818111edca6c6dadcdfc07404dca" + "bytes": 5866, + "sha256": "e16941efe87d21f466868fd7112c0298728f80360b5aa1b80c97a6cd8c29f649" } ], "sourceUrl": "https://github.com/dev-five-git/devup-mcp/blob/HEAD/crates/devup-mcp/src/server/skills/figma-bridge-setup", @@ -89,8 +89,8 @@ "documents": [ { "path": "SKILL.md", - "bytes": 41192, - "sha256": "75acf47ca65662fbad0cbbc7713593e0e532f57d3ce2807f143137270af0598d" + "bytes": 42690, + "sha256": "75b574c93e738f9d3d6b2b39d09bfb66f5e29ed34263afadfc945c37d064dcd8" } ], "sourceUrl": "https://github.com/dev-five-git/devup-mcp/blob/HEAD/crates/devup-mcp/src/server/skills/devup-implementation-plan", @@ -107,8 +107,8 @@ "documents": [ { "path": "SKILL.md", - "bytes": 20857, - "sha256": "b97af364c5883d289837db47faf1e26bdb342261b1352e643062c4f949d75abb" + "bytes": 21649, + "sha256": "6231e3f3c366b0c6e15bb589ee71246710458ba6ac404980ab6890af1845404c" } ], "sourceUrl": "https://github.com/dev-five-git/devup-mcp/blob/HEAD/crates/devup-mcp/src/server/skills/devup", diff --git a/crates/devup-mcp/src/server/text/guide-GUIDE-c682b0c5.md b/crates/devup-mcp/src/server/text/guide-GUIDE-c682b0c5.md index 64a00740..b5df6425 100644 --- a/crates/devup-mcp/src/server/text/guide-GUIDE-c682b0c5.md +++ b/crates/devup-mcp/src/server/text/guide-GUIDE-c682b0c5.md @@ -1,5 +1,9 @@ # Using devup-mcp +When replacing an inlined instance with a shared component, carry over every `instanceOverrides` entry through a new variant or prop, or explicitly report it as unresolved. `DEVUP_CODEGEN_INSTANCE_OVERRIDE` is a required review item; `// @devup-override` annotations retain its evidence in generated TSX. A missing captured value or master is unresolved, never evidence that an override is safe to discard. + +For a confirmed URL with multiple drawn widths, `responsiveTsx` is the deliverable. Use all sibling frameIds in width order from screen-plan status or `responsiveRequired.nextAction`. Never start from single-width `tsx` or `componentTsx` for that page or its sections. Only wire links, events and data; never change layout values, collapse responsive arrays, add fixed heights or layout wrappers. `generatedExport.generatedDrift` reports heuristic layout changes against recorded generated styles with source paths and confidence; missing mappings are unverified, not a pass. + ## Project intake comes first `/devup` is the entry point for new projects, existing features, bug fixes, From 9f7417680254ab69488d38f1913773291218f2e6 Mon Sep 17 00:00:00 2001 From: leader Date: Wed, 28 Oct 2026 01:32:16 +0900 Subject: [PATCH 141/149] feat(codegen,export): instance override reporting, default code paths and component references --- .../changepack_log_HdZ8AinyXi7pJg5Pc9ffy.json | 7 + crates/devup-mcp/src/server/compact.rs | 12 +- crates/devup-mcp/src/server/delivery.rs | 1 + crates/devup-mcp/src/server/export_record.rs | 19 +- .../server/export_record/generated_drift.rs | 84 +++++++++ .../src/server/feedback_interactions_tests.rs | 67 ++++++- .../src/server/handlers/devup_figma_export.rs | 93 +++++++--- crates/devup-mcp/src/server/interactions.rs | 40 ++++- .../src/server/interactions/marquee.rs | 109 +++++++++++ crates/devup-mcp/src/server/mod.rs | 1 + .../mod_parts/exploration_and_responses.rs | 5 +- crates/devup-mcp/src/server/navigation.rs | 135 ++++++-------- .../src/server/navigation_suggestion_tests.rs | 27 +++ crates/devup-mcp/src/server/page_scaffold.rs | 7 +- .../project_context/context_and_inventory.rs | 3 +- .../src/server/project_context_export.rs | 77 +++++++- crates/devup-mcp/src/server/projection.rs | 1 + .../src/server/projection/completion.rs | 15 +- .../server/projection/default_code_paths.rs | 121 +++++++++++++ .../projection/feedback_codegen_tests.rs | 169 +++++++++++++++++- .../server/projection/feedback_theme_tests.rs | 13 ++ .../projection/finalize_export_delivery.rs | 26 ++- .../server/projection/instance_overrides.rs | 57 ++++++ .../server/projection/project_operation.rs | 2 + .../projection/project_responsive_output.rs | 6 + .../projection/project_section_frames.rs | 2 +- .../devup-mcp/src/server/projection/setup.rs | 2 + .../summarize_projection_diagnostics.rs | 10 +- .../src/server/projection/theme_export.rs | 9 +- .../src/server/projection/w1_regressions.rs | 9 +- ...variant_and_css_map_resources_recover_w.rs | 2 +- ...s_placement_contract_without_diagnostic.rs | 7 +- crates/devup-mcp/src/server/review.rs | 36 +++- .../src/server/text/devup-figma-export.md | 5 + crates/devup-mcp/src/server/tools.rs | 16 ++ crates/devup-mcp/tests/cli.rs | 17 +- crates/devup-mcp/tests/export_defaults.rs | 24 ++- .../devup-mcp/tests/export_project_theme.rs | 72 +++++++- crates/devup-mcp/tests/ground_truth_tools.rs | 54 +++++- .../devup-mcp/tests/source_orchestration.rs | 70 ++++++-- crates/devup-mcp/tests/width_policy_export.rs | 2 +- fixtures/devup-figma-plugin/README.md | 11 ++ fixtures/devup-figma-plugin/manifest.json | 102 +++++------ .../upstream-codegen-001-4410624749.snap | 3 +- .../upstream-codegen-002-c1ac954cb6.snap | 3 +- .../upstream-codegen-021-6cae93b843.snap | 2 +- .../upstream-codegen-022-8dce1fe15f.snap | 2 +- .../upstream-codegen-023-040bff1c77.snap | 2 +- .../upstream-codegen-024-eb534f354c.snap | 2 +- .../upstream-codegen-025-74cf3c3463.snap | 2 +- .../upstream-codegen-026-e6df42a1c0.snap | 4 +- .../upstream-codegen-031-5d6f0a82e3.snap | 1 - .../upstream-codegen-032-be85ad69d0.snap | 1 - .../upstream-codegen-041-7c9175487c.snap | 3 +- .../upstream-codegen-046-82a07e3ec3.snap | 3 +- .../upstream-codegen-050-fda399bfe0.snap | 2 +- .../upstream-codegen-051-53df7b9474.snap | 2 +- .../upstream-codegen-052-e6195ba5a2.snap | 2 +- .../upstream-codegen-053-5fbe283840.snap | 2 +- .../upstream-codegen-055-1539142ffd.snap | 3 +- .../upstream-codegen-058-a5bed8edb7.snap | 3 +- .../upstream-codegen-059-0b07052901.snap | 8 +- .../upstream-codegen-070-a01830c33f.snap | 1 - .../upstream-codegen-071-3a132a6b4d.snap | 1 - .../upstream-codegen-072-df70a74728.snap | 1 - .../upstream-codegen-073-ef0cdd87cf.snap | 1 - .../upstream-codegen-074-57a24a2690.snap | 1 - .../upstream-codegen-075-fb8db22bb9.snap | 1 - .../upstream-codegen-076-3d2d4543bb.snap | 1 - .../upstream-codegen-077-a65991a745.snap | 1 - .../upstream-codegen-078-d6b1085274.snap | 1 - .../upstream-codegen-079-9e7f3ee9e4.snap | 1 - .../upstream-codegen-080-f6233dd6a0.snap | 1 - .../upstream-codegen-105-0137b6735e.snap | 1 - .../upstream-codegen-109-e2824ad5be.snap | 2 +- .../upstream-codegen-114-855686da78.snap | 1 + .../upstream-codegen-122-7ddad74cc1.snap | 2 +- .../upstream-codegen-124-bdedf14335.snap | 2 +- .../upstream-codegen-126-14073e1999.snap | 2 +- .../upstream-codegen-128-2222304a14.snap | 2 +- .../upstream-codegen-132-9a7b0243b6.snap | 2 +- .../upstream-codegen-134-f80fffb8b4.snap | 3 +- .../upstream-codegen-139-63f7b1f175.snap | 2 +- .../upstream-codegen-142-ac82229282.snap | 2 +- .../upstream-codegen-144-042a6de403.snap | 2 +- .../upstream-codegen-145-f7001b5648.snap | 2 +- .../upstream-codegen-181-5afa626d49.snap | 23 ++- .../upstream-codegen-193-bd488a5710.snap | 29 ++- .../upstream-codegen-194-17922977f6.snap | 29 ++- .../upstream-codegen-195-1a6efe22a8.snap | 23 ++- .../upstream-codegen-196-d14f03afcd.snap | 23 ++- .../upstream-codegen-235-8a3992468b.snap | 15 +- .../codegen/upstream-root-257-f23261dd4f.snap | 8 +- .../codegen/upstream-root-258-5cfe8baba5.snap | 4 +- 94 files changed, 1481 insertions(+), 306 deletions(-) create mode 100644 .changepacks/changepack_log_HdZ8AinyXi7pJg5Pc9ffy.json create mode 100644 crates/devup-mcp/src/server/export_record/generated_drift.rs create mode 100644 crates/devup-mcp/src/server/interactions/marquee.rs create mode 100644 crates/devup-mcp/src/server/navigation_suggestion_tests.rs create mode 100644 crates/devup-mcp/src/server/projection/default_code_paths.rs create mode 100644 crates/devup-mcp/src/server/projection/instance_overrides.rs diff --git a/.changepacks/changepack_log_HdZ8AinyXi7pJg5Pc9ffy.json b/.changepacks/changepack_log_HdZ8AinyXi7pJg5Pc9ffy.json new file mode 100644 index 00000000..3ff8d95f --- /dev/null +++ b/.changepacks/changepack_log_HdZ8AinyXi7pJg5Pc9ffy.json @@ -0,0 +1,7 @@ +{ + "changes": { + "Cargo.toml": "Minor" + }, + "note": "devup_ui_validate answers a compact summary by default and reads paths from disk; devup_figma_export writes code outputs under .devup/exports by default when projectRoot is given", + "date": "2026-10-24T11:43:31.269250300Z" +} \ No newline at end of file diff --git a/crates/devup-mcp/src/server/compact.rs b/crates/devup-mcp/src/server/compact.rs index 157cb401..d949924e 100644 --- a/crates/devup-mcp/src/server/compact.rs +++ b/crates/devup-mcp/src/server/compact.rs @@ -306,6 +306,7 @@ pub fn select( } let evidence_fields = [ "projectionIssues", + "instanceOverrides", "projectionEvidence", "completenessReport", "fidelity", @@ -339,7 +340,7 @@ pub fn select( .get("projectionIssues") .and_then(Value::as_array) .map_or(0, Vec::len); - let issues=result.get("projectionIssues").and_then(Value::as_array).map(|items|items.iter().take(10).map(|v|json!({"code":v["code"],"nodeId":v["nodeId"],"property":v["property"],"fidelityImpact":v["fidelityImpact"],"message":v["message"].as_str().map(|s|s.chars().take(240).collect::())})).collect::>()).unwrap_or_default(); + let issues=result.get("projectionIssues").and_then(Value::as_array).map(|items|items.iter().take(10).map(|v|json!({"code":v["code"],"nodeId":v["nodeId"],"property":v["property"],"fidelityImpact":v["fidelityImpact"],"how":v["how"].as_str().or_else(|| v["details"]["nextAction"].as_str()).or_else(|| v["details"]["nextAction"]["how"].as_str()),"message":v["message"].as_str().map(|s|s.chars().take(240).collect::())})).collect::>()).unwrap_or_default(); let output_results = result.get("outputResults").and_then(Value::as_object).map(|items| items.iter().map(|(name,v)|(name.clone(),json!({"state":v["state"],"projection":v["projection"],"bodyDelivery":"omitted-by-selection"}))).collect::>()); result.retain(|key, _| { matches!( @@ -428,6 +429,15 @@ pub fn refresh_reply_selection(reply: &mut Value, options: &ResponseSelection) - } else { reply["selected"] = json!({"selections":picked["selections"]}); } + if let Some(object) = reply.as_object_mut() { + object.retain(|key, _| matches!(key.as_str(), + "status" | "source" | "cache" | "quality" | "completeness" | "collection" | + "verificationScope" | "outputPaths" | "outputPathResults" | "resume" | + "responsiveEvidence" | "mappingComplete" | "projectionIssues" | "selectionEvidence" | + "deliverable" | "outputResults" | "selected" | "resources" | "delivery" | + "sizeEstimate" | "metadataDelivery" | "responseTruncated" | "artifactId" | + "exportJob" | "assetJob" | "summary" | "reviewChecklist" | "warnings")); + } Ok(()) } diff --git a/crates/devup-mcp/src/server/delivery.rs b/crates/devup-mcp/src/server/delivery.rs index 6f6835c3..c5c5b006 100644 --- a/crates/devup-mcp/src/server/delivery.rs +++ b/crates/devup-mcp/src/server/delivery.rs @@ -327,6 +327,7 @@ pub(crate) fn server_identity() -> Value { serde_json::json!({"version":env!("CARGO_PKG_VERSION"),"buildId":crate::build_id(), "commit":option_env!("DEVUP_MCP_GIT_COMMIT").filter(|s|!s.is_empty()), "displayVersion":format!("{}+{}", env!("CARGO_PKG_VERSION"), crate::build_id()), + "builtAt":env!("DEVUP_MCP_BUILD_TIME").parse::().ok(), "identityGuidance":include_str!("text/delivery-server_identity-f8e03915.md"), // Cache-only. The lookup lives in a background task, so adding this to // the identity every response carries costs no network on the call path diff --git a/crates/devup-mcp/src/server/export_record.rs b/crates/devup-mcp/src/server/export_record.rs index c0ef5fc2..55664fe9 100644 --- a/crates/devup-mcp/src/server/export_record.rs +++ b/crates/devup-mcp/src/server/export_record.rs @@ -16,6 +16,7 @@ use serde_json::{Value, json}; use super::output::{OutputPolicy, OutputTransaction}; + pub(super) const EXPORTS_DIR: &str = ".devup/exports"; pub(super) const CONVERTER_BUGS_DIR: &str = ".devup/converter-bugs"; const RECORD_KIND: &str = "devup-export-record"; @@ -191,9 +192,18 @@ pub(super) fn write( } } } - if nodes.is_empty() { + let generated = response["responsiveTsx"].as_str().or_else(|| response["tsx"].as_str()).or_else(|| response["componentTsx"].as_str()); + if generated.is_none() && nodes.is_empty() && response["frames"].as_array().is_none_or(|frames| frames.is_empty()) { return Ok(None); } + let output = if response["responsiveTsx"].is_string() { Some("responsiveTsx") } + else if response["tsx"].is_string() { Some("tsx") } + else if response["componentTsx"].is_string() { Some("componentTsx") } else { output }; + let mut frame_ids = response["responsiveEvidence"]["frames"].as_array().into_iter().flatten() + .chain(response["frames"].as_array().into_iter().flatten()) + .filter_map(|frame| frame["nodeId"].as_str()).map(str::to_owned) + .collect::>(); + if frame_ids.is_empty() && !root_node_id.is_empty() { frame_ids.insert(root_node_id.to_owned()); } let mut source_map = BTreeMap::>::new(); for entry in response["outputResults"]["tsx"]["_sourceMap"] .as_array() @@ -222,10 +232,13 @@ pub(super) fn write( "output": output, "nodes": nodes, "responsive": response["figmaNodeIds"]["responsive"], + "frameIds": frame_ids, + "artifactId": response["artifactId"], + "generatedStyles": generated.map(style_elements), }); let path = Path::new(project_root) .join(EXPORTS_DIR) - .join(format!("{}.json", file_name(root_node_id))); + .join(format!("{}.json", file_name(frame_ids.iter().next().map_or(root_node_id, String::as_str)))); let target = policy.resolve(&path.to_string_lossy())?; let display = target.display_path().display().to_string(); let bytes = serde_json::to_vec_pretty(&record).map_err(|error| { @@ -404,3 +417,5 @@ pub(super) fn write_repro( transaction.commit()?; Ok((display, repro)) } + +include!("export_record/generated_drift.rs"); diff --git a/crates/devup-mcp/src/server/export_record/generated_drift.rs b/crates/devup-mcp/src/server/export_record/generated_drift.rs new file mode 100644 index 00000000..6993cb84 --- /dev/null +++ b/crates/devup-mcp/src/server/export_record/generated_drift.rs @@ -0,0 +1,84 @@ +fn style_prop(name: &str) -> bool { + LAYOUT_PROPS.contains(&name) || matches!(name, + "bg" | "background" | "backgroundColor" | "backgroundImage" | "color" | "border" | + "borderColor" | "borderWidth" | "borderRadius" | "rounded" | "boxShadow" | "opacity" | + "textAlign" | "whiteSpace" | "wordBreak" | "zIndex" | "maskImage" | "objectPosition") +} + +fn style_elements(tsx: &str) -> Vec { + let mut elements = BTreeMap::)>::new(); + for attribute in generated_attributes(tsx).into_iter().filter(|attribute| style_prop(&attribute.name)) { + let element = elements.entry(attribute.element).or_insert_with(|| (attribute.tag, BTreeMap::new())); + element.1.insert(attribute.name, normalized(&attribute.source)); + } + elements.into_values().map(|(tag, props)| json!({"tag": tag, "props": props})).collect() +} + +fn style_value(source: &str) -> String { + source.split_once('=').map_or(source, |(_, value)| value).chars() + .filter(|character| !character.is_whitespace()).map(|character| if character == '\'' { '"' } else { character }).collect() +} + +pub(super) fn generated_drift(record: &Value, root: &Path, sources: &[String]) -> Value { + let Some(expected) = record["generatedStyles"].as_array() else { + return json!({"state": "unverified", "confidence": "none", "reason": "Export predates the style baseline; re-export responsiveTsx."}); + }; + if expected.is_empty() { + return json!({"state": "unverified", "confidence": "none", "reason": "The export has no comparable literal style props."}); + } + let mut actual = Vec::new(); + let mut files = Vec::new(); + for source in sources { + let path = root.join(source); + if !path.starts_with(root) || source.split(['/', '\\']).any(|part| part == "..") { continue; } + if !dunce::canonicalize(&path).ok().zip(dunce::canonicalize(root).ok()).is_some_and(|(path, root)| path.starts_with(root)) { continue; } + if std::fs::metadata(&path).map_or(true, |metadata| metadata.len() > MAX_RECORD_BYTES) { continue; } + let Ok(text) = std::fs::read_to_string(&path) else { continue; }; + files.push(source.clone()); + actual.extend(style_elements(&text).into_iter().map(|element| (source, element))); + } + if files.is_empty() { + return json!({"state": "unverified", "confidence": "none", "reason": "No mapped URL source files were readable."}); + } + let mut findings = Vec::new(); + for (index, element) in expected.iter().enumerate() { + let Some((file, current)) = actual.get(index) else { + findings.push(json!({"kind": "element-removed", "element": index, "files": files, "generated": element})); + continue; + }; + let Some(props) = element["props"].as_object() else { continue; }; + for (prop, generated) in props { + let value = ¤t["props"][prop]; + if value.as_str().map(style_value) != generated.as_str().map(style_value) { + let kind = if value.is_null() { "prop-removed" } + else if generated.as_str().is_some_and(|value| value.contains("{[")) && !value.as_str().is_some_and(|value| value.contains("{[")) { "responsive-array-collapsed" } + else { "prop-changed" }; + findings.push(json!({"kind": kind, "file": file, "element": index, "prop": prop, "generated": generated, "actual": value})); + } + } + for (prop, value) in current["props"].as_object().into_iter().flatten() { + if !props.contains_key(prop) { + findings.push(json!({"kind": "style-added", "file": file, "element": index, "prop": prop, "actual": value})); + } + } + } + for (file, element) in actual.iter().skip(expected.len()) { + findings.push(json!({"kind": "styled-element-added", "file": file, "actual": element})); + } + json!({"state": if findings.is_empty() { "matched-heuristic" } else { "drift" }, "confidence": "medium", + "method": "Ordered style-bearing JSX elements across mapped URL files; wiring-only wrappers and attributes are ignored. Component extraction or reordering can require mapping review; not a runtime proof.", + "files": files, "findings": findings}) +} + +#[cfg(test)] +mod generated_drift_tests { + use super::*; + + #[test] + fn style_baseline_ignores_wiring_but_preserves_responsive_arrays() { + let generated = style_elements(""); + let wired = style_elements(""); + assert_eq!(generated, wired); + assert_ne!(generated, style_elements("")); + } +} diff --git a/crates/devup-mcp/src/server/feedback_interactions_tests.rs b/crates/devup-mcp/src/server/feedback_interactions_tests.rs index 333704ed..0ed46006 100644 --- a/crates/devup-mcp/src/server/feedback_interactions_tests.rs +++ b/crates/devup-mcp/src/server/feedback_interactions_tests.rs @@ -69,7 +69,7 @@ fn feedback_interactions_preserve_targets_roles_states_and_unresolved_data() { ); assert_eq!( schema["$defs"]["Role"]["enum"], - json!(["link", "button", "tab", "nav", "checkbox", "radio"]) + json!(["link", "button", "tab", "nav", "checkbox", "radio", "marquee"]) ); assert_eq!( schema["$defs"]["InteractiveCandidate"]["properties"]["targetResolved"]["type"], @@ -311,6 +311,30 @@ fn feedback_validation_catches_missing_routes_unmapped_actions_and_active_state( assert_eq!(report["coverage"]["runtimeVerified"], false); } +#[tokio::test] +async fn export_navigation_reads_existing_code_link_evidence() { + let p = Project::new(); + p.write("src/app/company/location/page.tsx", "export default function Location(){return
}"); + p.write("src/app/layout.tsx", "import Link from 'next/link'; export default function Layout(){return 오시는길}"); + let result = project_context::checked_export(json!({"interactionCoverage":{},"interactiveCandidates":[{"nodeId":"location","text":"오시는 길"}]}), Some(p.root()), &skills::Lookup::project_only(p.0.clone(), skills::Runtime::Codex)).await; + assert_eq!(result["routeSuggestions"][0]["targetResolved"], true); + assert_eq!(result["routeSuggestions"][0]["suggestions"][0]["route"], "/company/location"); + assert!(result["routeSuggestions"][0]["suggestions"][0]["evidence"].as_str().unwrap().contains("layout.tsx:1")); + assert_eq!(result["projectContext"]["ui"]["routes"][0]["route"], "/company/location"); +} + +#[test] +fn export_context_lists_only_referenced_assets_even_past_inventory_cap() { + let p = Project::new(); + p.write("src/app/page.tsx", "export default function Home(){return
}"); + for index in 0..300 { p.write(&format!("public/images/image-{index:03}.png"), "image"); } + let mut result = json!({"tsx":"<>","interactiveCandidates":[]}); + project_context::attach_export_context(&mut result, p.root()); + assert_eq!(result["projectContext"]["counts"]["publicFiles"], 300); + assert_eq!(result["projectContext"]["ui"]["nonPageDestinations"], json!(["/images/image-299.png"])); + assert_eq!(result["projectContext"]["referencedAssets"], json!([{"url":"/images/image-299.png","exists":true},{"url":"/images/missing.png","exists":false}])); +} + #[test] fn a_primitive_rendered_as_link_with_props_href_is_wired() { let inventory = json!({"found":true,"routes":[{"route":"/company","appliesTo":"."}]}); @@ -716,6 +740,7 @@ fn feedback_context_budget_caps_or_defers_explicitly() { ); assert!( result["projectContext"]["exportContextLimits"]["truncated"] == true + || result["projectContext"]["state"] == "summary" || result["projectContext"]["state"] == "deferred" ); } @@ -755,3 +780,43 @@ fn feedback_read_failures_and_direct_selection_preserve_review_and_source_maps() assert_eq!(result["deliverable"]["isFinal"], false); assert!(result["summary"]["omittedReviewItems"].as_u64().unwrap() > 0); } + +#[test] +fn broken_component_sets_do_not_hide_readable_reactions() { + for reason in ["in get_componentProperties: Component set for node has existing errors", "Component set is invalid"] { + let mut snapshot = design(); + let node = snapshot.nodes.get_mut("nav").unwrap(); + node.fields.insert("mainComponentSetId".into(), json!("set:1")); + node.fields.insert("mainComponentSetName".into(), json!("Navigation")); + for field in ["componentProperties", "variantProperties"] { + node.field_errors.insert(field.into(), reason.into()); + } + let mut result = json!({"source":{"nodeId":"screen"},"tsx":"","deliverable":{"isFinal":true}}); + interactions::attach(&mut result, &snapshot); + let issues = result["projectionIssues"].as_array().unwrap(); + let issue = issues.iter().find(|issue| issue["code"] == "DEVUP_DESIGN_COMPONENT_SET_ERROR").unwrap(); + assert_eq!(issue["nodeId"], "nav"); + assert_eq!(issue["details"]["componentSetId"], "set:1"); + assert_eq!(issue["details"]["componentSetName"], "Navigation"); + assert_eq!(issue["fidelityImpact"], "none"); + assert!(!issues.iter().any(|issue| issue["code"] == "DEVUP_CODEGEN_INTERACTION_UNVERIFIED")); + let candidate = result["interactiveCandidates"].as_array().unwrap().iter().find(|candidate| candidate["nodeId"] == "nav").unwrap(); + assert_eq!(candidate["prototypeActions"][0]["destinationId"], "portfolio-screen"); + } +} + +#[test] +fn interaction_read_failure_names_node_and_field_but_state_only_failure_does_not_block() { + let mut snapshot = design(); + snapshot.nodes.get_mut("nav").unwrap().field_errors.insert("reactions".into(), "permission denied".into()); + let mut result = json!({"tsx":"","deliverable":{"isFinal":true}}); + interactions::attach(&mut result, &snapshot); + let issue = result["projectionIssues"].as_array().unwrap().iter().find(|issue| issue["code"] == "DEVUP_CODEGEN_INTERACTION_UNVERIFIED").unwrap(); + let message = issue["message"].as_str().unwrap(); + assert!(message.contains("nav") && message.contains("reactions")); + snapshot.nodes.get_mut("nav").unwrap().field_errors.clear(); + snapshot.nodes.get_mut("timer").unwrap().field_errors.insert("variantProperties".into(), "not available".into()); + let mut result = json!({"source":{"nodeId":"timer"},"tsx":"","deliverable":{"isFinal":true}}); + interactions::attach(&mut result, &snapshot); + assert_eq!(result["deliverable"]["isFinal"], true); +} diff --git a/crates/devup-mcp/src/server/handlers/devup_figma_export.rs b/crates/devup-mcp/src/server/handlers/devup_figma_export.rs index 3848fad4..b7eb56a3 100644 --- a/crates/devup-mcp/src/server/handlers/devup_figma_export.rs +++ b/crates/devup-mcp/src/server/handlers/devup_figma_export.rs @@ -46,6 +46,9 @@ impl DevupServer { }; frames.extend(node); } + if let Some(response) = screen_plan::responsive_required(&project_root, &frames, &input.outputs).map_err(to_mcp_error)? { + return Ok(tool_result(response)); + } let request = screen_plan::ExportRequest { project_root: &project_root, url: input.url.as_deref(), @@ -127,7 +130,7 @@ impl DevupServer { .wait_briefly(selection.active()) .await .map_err(to_mcp_error)?; - let result = with_project_checks( + let result = project_context::checked_export( result, project_root.as_deref(), &skills::Lookup::new( @@ -135,7 +138,7 @@ impl DevupServer { self.output_policy.primary_root(), self.client_name(), ), - ); + ).await; if selection.active() { return compact::pick(&result, selection) .map(tool_result) @@ -245,6 +248,8 @@ impl DevupServer { input.url = Some(target.link(target.node_id.as_deref())); } if reference_png_requested && (!input.frame_ids.is_empty() || input.all_screens) { + // No url and no artifact is the file the attached bridge plugin + // has open, so its frames are linked as figma-bridge://current. let target = if let Some(url) = &input.url { FigmaTarget::parse(url).ok() } else if let Some(id) = &input.artifact_id { @@ -253,29 +258,65 @@ impl DevupServer { .await .map(|a| a.payload.target.clone()) } else { - None + Some(FigmaTarget::bridge_current()) + }; + // One referencePng-only call per frame. The other outputs stay on + // the original multi-frame call: repeating responsiveTsx on a + // single frame would only be refused as responsive_required. + let reference_call = |url: &str| { + let mut arguments = json!({"url": url, "outputs": ["referencePng"], "scope": "node"}); + if let Some(root) = &input.project_root { + arguments["projectRoot"] = json!(root); + } + json!({"tool": "devup_figma_export", "arguments": arguments}) }; - let url = target + let reference_calls = target .as_ref() - .zip(input.frame_ids.first()) - .map(|(target, id)| target.link(Some(id))); - let mut arguments = json!(input); - for key in ["artifactId", "frameIds", "allScreens", "url"] { - arguments.as_object_mut().unwrap().remove(key); - } - arguments["scope"] = json!("node"); - if let Some(url) = &url { - arguments["url"] = json!(url); - } + .map(|target| { + input + .frame_ids + .iter() + .map(|id| reference_call(&target.link(Some(id)))) + .collect::>() + }) + .unwrap_or_default(); + let remaining_outputs = input + .outputs + .iter() + .filter(|output| *output != "referencePng") + .cloned() + .collect::>(); + let remaining_call = (!remaining_outputs.is_empty()).then(|| { + let mut arguments = json!(input); + arguments["outputs"] = json!(remaining_outputs); + json!({"tool": "devup_figma_export", "arguments": arguments}) + }); + let available = !reference_calls.is_empty(); + let next_action = if let Some(first) = reference_calls.first() { + let mut next = first.clone(); + next["omitArguments"] = json!(["artifactId", "frameIds", "allScreens"]); + next["how"] = json!(include_str!("../text/mod-devup_figma_export-1fe13d5b.md")); + next + } else { + let mut arguments = json!(input); + for key in ["artifactId", "frameIds", "allScreens", "url"] { + arguments.as_object_mut().unwrap().remove(key); + } + arguments["scope"] = json!("node"); + arguments["outputs"] = json!(["referencePng"]); + json!({"tool": "devup_figma_export", "arguments": arguments, + "requiredArguments": ["url"], + "omitArguments": ["artifactId", "frameIds", "allScreens"], + "how": include_str!("../text/mod-devup_figma_export-1fe13d5b.md")}) + }; return Err(to_mcp_error(DevupError::with_details( ErrorCode::DevupSnapshotUnsupported, "referencePng can only be collected for a single Figma link target.", false, - json!({"recoveryState":if url.is_some() {"available"} else {"manual-fix-required"}, - "nextAction":{"tool":"devup_figma_export","arguments":arguments, - "requiredArguments":if url.is_some() {Vec::<&str>::new()} else {vec!["url"]}, - "omitArguments":["artifactId","frameIds","allScreens"], - "how":include_str!("../text/mod-devup_figma_export-1fe13d5b.md")}}), + json!({"recoveryState": if available {"available"} else {"manual-fix-required"}, + "nextAction": next_action, + "referenceCalls": reference_calls, + "remainingCall": remaining_call}), ))); } let root_layout = parse_root_layout(&input.root_layout).map_err(to_mcp_error)?; @@ -430,7 +471,7 @@ impl DevupServer { ) .await .map_err(to_mcp_error)?; - return Ok(tool_result(with_project_checks( + let mut result = project_context::checked_export( result, input.project_root.as_deref(), &skills::Lookup::new( @@ -438,7 +479,9 @@ impl DevupServer { self.output_policy.primary_root(), self.client_name(), ), - ))); + ).await; + compact::refresh_reply_selection(&mut result, &input.response_selection).map_err(to_mcp_error)?; + return Ok(tool_result(result)); } validate_artifact_projection( &artifact, @@ -483,7 +526,7 @@ impl DevupServer { ) .await .map_err(to_mcp_error)?; - let mut result = with_project_checks( + let mut result = project_context::checked_export( result, input.project_root.as_deref(), &skills::Lookup::new( @@ -491,7 +534,7 @@ impl DevupServer { self.output_policy.primary_root(), self.client_name(), ), - ); + ).await; if manifest_added { let warning = json!({"code":"DEVUP_ASSET_MANIFEST_ADDED","message":"assetManifest was automatically added to outputs for assetRequests."}); if let Some(warnings) = result["warnings"].as_array_mut() { warnings.push(warning); } @@ -613,7 +656,7 @@ impl DevupServer { ok => ok, } .map_err(to_mcp_error)?; - let mut result = with_project_checks( + let mut result = project_context::checked_export( result, input.project_root.as_deref(), &skills::Lookup::new( @@ -621,7 +664,7 @@ impl DevupServer { self.output_policy.primary_root(), self.client_name(), ), - ); + ).await; if manifest_added { let warning = json!({"code":"DEVUP_ASSET_MANIFEST_ADDED","message":"assetManifest was automatically added to outputs for assetRequests."}); if let Some(warnings) = result["warnings"].as_array_mut() { warnings.push(warning); } diff --git a/crates/devup-mcp/src/server/interactions.rs b/crates/devup-mcp/src/server/interactions.rs index 7b86ef41..9d221bbb 100644 --- a/crates/devup-mcp/src/server/interactions.rs +++ b/crates/devup-mcp/src/server/interactions.rs @@ -7,6 +7,14 @@ use serde_json::{Value, json}; const MAX_CANDIDATES: usize = 128; const MAX_DETAILS: usize = 64; +#[path = "interactions/marquee.rs"] +mod marquee; +fn component_set_error(field: &str, error: &str) -> bool { + matches!(field, "componentProperties" | "variantProperties") + && (error.contains("Component set for node has existing errors") + || error.contains("Component set is invalid")) +} + fn bounded(value: &Value, depth: usize, truncated: &mut bool) -> Value { if depth == 0 && (value.is_object() || value.is_array()) { *truncated = true; @@ -52,6 +60,7 @@ pub(super) enum Role { Nav, Checkbox, Radio, + Marquee, } #[derive(Debug, Clone, Serialize, schemars::JsonSchema)] @@ -235,6 +244,10 @@ pub(super) fn candidates(snapshot: &Snapshot, roots: &[String]) -> Vec Value { ] .contains(&field.as_str()) { - read_errors.push(json!({"nodeId":id,"field":field,"reason":error})); + read_errors.push(json!({"nodeId":id,"field":field,"reason":error, + "classification":if component_set_error(field,error){"design-component-set-error"}else{"read-error"}, + "componentSetId":view.string("mainComponentSetId"), + "componentSetName":view.string("mainComponentSetName")})); } } if let Some(master) = view @@ -513,7 +529,7 @@ pub(super) fn evidence(snapshot: &Snapshot, roots: &[String]) -> Value { "shapeAndStateVerified":false})); } } - let mut shared = masters.into_iter().filter(|(_,members)| members.len()>1).map(|(master,mut members)| { + let mut shared = masters.into_iter().map(|(master,mut members)| { let signatures: BTreeSet<_> = members.iter().map(|m| json!([m["text"],m["variantProperties"],m["componentProperties"]]).to_string()).collect(); let total=members.len(); members.truncate(24); json!({"componentId":master,"instances":members,"instanceCount":total,"instancesOmitted":total-members.len(),"variantDifferences":signatures.len()>1, @@ -593,11 +609,20 @@ pub(super) fn attach(result: &mut Value, snapshot: &Snapshot) { ); } } - if data["interactionReadErrors"] - .as_array() - .is_some_and(|a| !a.is_empty()) - { - result.as_object_mut().unwrap().entry("projectionIssues").or_insert(json!([])).as_array_mut().unwrap().push(json!({"code":"DEVUP_CODEGEN_INTERACTION_UNVERIFIED","severity":"warning","fidelityImpact":"none","message":"Interaction/state fields could not be read; candidate coverage is incomplete.","details":{"classification":"interaction-unverified","nextAction":"Review interactionReadErrors and recollect failed fields before claiming interaction coverage."}})); + let read_errors = data["interactionReadErrors"].as_array().unwrap(); + for error in read_errors.iter().filter(|error| error["classification"] == "design-component-set-error") { + result.as_object_mut().unwrap().entry("projectionIssues").or_insert(json!([])).as_array_mut().unwrap().push(json!({ + "code":"DEVUP_DESIGN_COMPONENT_SET_ERROR","nodeId":error["nodeId"],"property":error["field"], + "severity":"warning","fidelityImpact":"none", + "message":"the Figma component set is broken; open it in Figma and fix its error (Figma shows a warning on the set)", + "details":error})); + } + let unreadable = read_errors.iter().filter(|error| error["classification"] != "design-component-set-error" + && matches!(error["field"].as_str(),Some("reactions" | "mainComponentId" | "mainComponentSetId"))) + .collect::>(); + if !unreadable.is_empty() { + let fields = unreadable.iter().map(|error| format!("{} on node {}",error["field"].as_str().unwrap(),error["nodeId"].as_str().unwrap())).collect::>().join(", "); + result.as_object_mut().unwrap().entry("projectionIssues").or_insert(json!([])).as_array_mut().unwrap().push(json!({"code":"DEVUP_CODEGEN_INTERACTION_UNVERIFIED","severity":"warning","fidelityImpact":"none","message":format!("Interaction fields could not be read: {fields}; candidate coverage is incomplete."),"details":{"classification":"interaction-unverified","readErrors":unreadable,"nextAction":"Review interactionReadErrors and recollect failed fields before claiming interaction coverage."}})); if result["deliverable"].is_object() { result["deliverable"]["isFinal"] = json!(false); result["deliverable"]["note"] = @@ -636,6 +661,7 @@ pub(super) fn attach(result: &mut Value, snapshot: &Snapshot) { let unknown_targets = result_candidates(&data) .iter() .filter(|c| { + if c["role"] == "marquee" { return false; } c["prototypeActions"] .as_array() .is_none_or(|a| a.is_empty() || a.iter().any(|a| !a["url"].is_string())) diff --git a/crates/devup-mcp/src/server/interactions/marquee.rs b/crates/devup-mcp/src/server/interactions/marquee.rs new file mode 100644 index 00000000..a4ea664a --- /dev/null +++ b/crates/devup-mcp/src/server/interactions/marquee.rs @@ -0,0 +1,109 @@ +use super::*; +use devup_mcp_devup_ui::ui_validate::visual_behavior::{MARQUEE_RECIPE, repeated_separator}; + +pub(super) fn candidate(snapshot: &Snapshot, node: &RawNode) -> Option { + let view = node.typed_view(); + let children = view.child_ids().filter_map(|id| snapshot.nodes.get(id)) + .filter(|child| child.typed_view().bool("visible") != Some(false)).collect::>(); + let row = view.string("layoutMode") == Some("HORIZONTAL") + && view.string("layoutWrap") != Some("WRAP") + && children.len() >= 3 + && children.iter().all(|child| child.node_type == "TEXT") + && children.iter().filter_map(|child| child.typed_view().string("characters")) + .collect::>().len() < children.len(); + if node.node_type != "TEXT" && !row { return None } + let text = if row { + children.iter().filter_map(|child| child.typed_view().string("characters")).collect::>().join(" ") + } else { str_field(node, "characters").to_owned() }; + if text.trim().is_empty() || text.contains(['\n', '\r']) { return None } + let single_line = row || view.string("textAutoResize") == Some("WIDTH_AND_HEIGHT") + || view.number("maxLines") == Some(1.0) + || matches!((view.number("height"), view.number("fontSize")), (Some(height), Some(size)) if height <= size * 1.6); + let width = view.number("width"); + let mut current = node; + let mut visited = BTreeSet::new(); + let mut clip = None; + while visited.insert(current.id.as_str()) { + let parent = current.typed_view().string("parentId") + .and_then(|id| snapshot.nodes.get(id)) + .or_else(|| snapshot.nodes.values().find(|parent| parent.typed_view().child_ids().any(|id| id == current.id))); + let Some(parent) = parent else { break }; + let parent_view = parent.typed_view(); + if parent_view.bool("clipsContent") == Some(true) + && matches!((width, parent_view.number("width")), (Some(width), Some(clip_width)) if width > clip_width + 1.0) + { + clip = Some(parent.id.clone()); + break; + } + current = parent; + } + let separators = repeated_separator(&text); + if !(separators || single_line && clip.is_some()) { return None } + Some(InteractiveCandidate { + node_id: node.id.clone(), name: str_field(node, "name").into(), + text: text.chars().take(500).collect(), role: Role::Marquee, + reason: [separators.then_some("repeated-separator-pattern"), + (single_line && clip.is_some()).then_some("single-line-text-exceeds-clipping-ancestor")] + .into_iter().flatten().map(str::to_owned).collect(), + has_prototype_target: false, target_resolved: false, + unresolved_reasons: vec!["animation-intent-is-heuristic-not-figma-animation-data".into()], + prototype_actions: Vec::new(), + states: json!({"confidence":"heuristic", "clippingAncestorId":clip, + "runtimeStateVerified":false,"recipe":MARQUEE_RECIPE, + "animation":{"from":"translateX(0)","to":"translateX(-50%)","iterationCount":"infinite","timingFunction":"linear","reducedMotion":"none","duplicateAriaHidden":true}}), + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn snapshot(nodes: Value) -> Snapshot { + Snapshot { file_key: "fixture".into(), version: None, roots: vec!["clip".into()], + nodes: nodes.as_array().unwrap().iter().map(|node| { + let node: RawNode = serde_json::from_value(node.clone()).unwrap(); + (node.id.clone(), node) + }).collect(), diagnostics: Vec::new() } + } + + #[test] + fn marquee_candidates_report_overflow_through_intermediate_ancestors() { + let snapshot = snapshot(json!([ + {"id":"clip","type":"FRAME","fields":{"width":400,"clipsContent":true,"childrenIds":["track"]}}, + {"id":"track","type":"FRAME","fields":{"childrenIds":["text"]}}, + {"id":"text","type":"TEXT","fields":{"width":1200,"textAutoResize":"WIDTH_AND_HEIGHT","characters":"An overflowing research strip without separator glyphs"}} + ])); + let output = evidence(&snapshot, &snapshot.roots); + let candidate = &output["interactiveCandidates"][0]; + assert_eq!(candidate["role"], "marquee"); + assert_eq!(candidate["states"]["confidence"], "heuristic"); + assert_eq!(candidate["states"]["clippingAncestorId"], "clip"); + assert_eq!(candidate["states"]["animation"]["duplicateAriaHidden"], true); + } + + #[test] + fn marquee_candidates_detect_separators_and_repeated_rows() { + let snapshot = snapshot(json!([ + {"id":"clip","type":"FRAME","fields":{"width":400,"clipsContent":true,"childrenIds":["text","row"]}}, + {"id":"text","type":"TEXT","fields":{"characters":"Research ✦ Repair ✦ Renew ✦ Research"}}, + {"id":"row","type":"FRAME","fields":{"width":600,"layoutMode":"HORIZONTAL","childrenIds":["a","b","c"]}}, + {"id":"a","type":"TEXT","fields":{"characters":"Research","width":200}}, + {"id":"b","type":"TEXT","fields":{"characters":"Research","width":200}}, + {"id":"c","type":"TEXT","fields":{"characters":"Research","width":200}} + ])); + assert!(candidate(&snapshot, &snapshot.nodes["text"]).is_some()); + assert!(candidate(&snapshot, &snapshot.nodes["row"]).is_some()); + let mut wrapped = snapshot; + wrapped.nodes.get_mut("row").unwrap().fields.insert("layoutWrap".into(), json!("WRAP")); + assert!(candidate(&wrapped, &wrapped.nodes["row"]).is_none()); + } + + #[test] + fn marquee_candidates_ignore_wrapped_unclipped_and_short_text() { + let snapshot = snapshot(json!([ + {"id":"clip","type":"FRAME","fields":{"width":400,"childrenIds":["text"]}}, + {"id":"text","type":"TEXT","fields":{"width":1200,"textAutoResize":"HEIGHT","characters":"Research ✦ Repair\n✦ Renew ✦ Research"}} + ])); + assert!(candidate(&snapshot, &snapshot.nodes["text"]).is_none()); + } +} diff --git a/crates/devup-mcp/src/server/mod.rs b/crates/devup-mcp/src/server/mod.rs index db0144e5..ca3f4fd0 100644 --- a/crates/devup-mcp/src/server/mod.rs +++ b/crates/devup-mcp/src/server/mod.rs @@ -1,5 +1,6 @@ // Source sections share this module to preserve privacy and test names. include!("mod_parts/state_and_services.rs"); include!("mod_parts/acquisition.rs"); +include!("mod_parts/component_width_acquisition.rs"); include!("mod_parts/exploration_and_responses.rs"); include!("mod_parts/p2_recovery_tests.rs"); diff --git a/crates/devup-mcp/src/server/mod_parts/exploration_and_responses.rs b/crates/devup-mcp/src/server/mod_parts/exploration_and_responses.rs index cf608bf1..162e5600 100644 --- a/crates/devup-mcp/src/server/mod_parts/exploration_and_responses.rs +++ b/crates/devup-mcp/src/server/mod_parts/exploration_and_responses.rs @@ -432,9 +432,8 @@ fn with_project_checks( } } } - // Before the theme half, and before its `project_root` gate: a response - // that handed back code has to name the conventions that code is written - // in whether or not the caller told us where the project is. + generated.extend(projection::default_code_paths::written_sources(&result)); + // Code delivery must name its conventions, independently of project_root. let produced_code = !generated.is_empty() || result.get("deliverable").is_some(); if produced_code { result = with_skill_gap(result, lookup); diff --git a/crates/devup-mcp/src/server/navigation.rs b/crates/devup-mcp/src/server/navigation.rs index fca5e819..d38916c8 100644 --- a/crates/devup-mcp/src/server/navigation.rs +++ b/crates/devup-mcp/src/server/navigation.rs @@ -1,5 +1,6 @@ //! Static navigation checks share the UI inventory; they never guess destinations. use std::collections::{BTreeMap, BTreeSet}; +use devup_mcp_devup_ui::effective_attributes::AttributeContext; use oxc_allocator::Allocator; use oxc_ast::ast::*; @@ -33,6 +34,7 @@ struct Collector<'s> { link_names: BTreeSet, elements: Vec, diagnostics: Vec, + attributes: AttributeContext, } // Only literal visual markers on a navigation candidate warrant this hint. @@ -89,22 +91,6 @@ fn fixed_visual_marker(attrs: &BTreeMap<&str, &JSXAttribute<'_>>) -> bool { .is_some_and(|v| !v.is_empty() && v != "transparent")) } -fn boolean(a: &JSXAttribute<'_>) -> Option { - match &a.value { - None => Some(true), - Some(JSXAttributeValue::ExpressionContainer(c)) => match &c.expression { - JSXExpression::BooleanLiteral(b) => Some(b.value), - _ => None, - }, - Some(JSXAttributeValue::StringLiteral(s)) => match s.value.as_str() { - "true" => Some(true), - "false" => Some(false), - _ => None, - }, - _ => None, - } -} - impl<'a> Visit<'a> for Collector<'_> { fn visit_import_declaration(&mut self, declaration: &ImportDeclaration<'a>) { if declaration.source.value == "next/link" { @@ -131,74 +117,31 @@ impl<'a> Visit<'a> for Collector<'_> { } }) .collect(); - let rendered_as = attrs.get("as").and_then(|a| match &a.value { - Some(JSXAttributeValue::StringLiteral(s)) => Some(s.value.to_string()), - Some(JSXAttributeValue::ExpressionContainer(c)) => match &c.expression { - JSXExpression::StringLiteral(s) => Some(s.value.to_string()), - JSXExpression::Identifier(i) => Some(i.name.to_string()), - _ => None, - }, - _ => None, - }); + let effective = self.attributes.read(&element.opening_element); + let rendered_as = effective.rendered_as(); let name = rendered_as.as_deref().unwrap_or(name); - let props_href = attrs.get("props").is_some_and(|a| match &a.value { - Some(JSXAttributeValue::ExpressionContainer(c)) => match &c.expression { - JSXExpression::ObjectExpression(o) => o.properties.iter().any(|p| { - matches!(p, ObjectPropertyKind::ObjectProperty(p) - if p.key.static_name().as_deref() == Some("href")) - }), - _ => false, - }, - _ => false, - }); - let has_href = attrs.contains_key("href") || props_href; - let href = attrs.get("href").and_then(|a| literal(a)).or_else(|| { - attrs.get("props").and_then(|a| match &a.value { - Some(JSXAttributeValue::ExpressionContainer(c)) => match &c.expression { - JSXExpression::ObjectExpression(o) => o.properties.iter().find_map(|p| match p { - ObjectPropertyKind::ObjectProperty(p) - if p.key.static_name().as_deref() == Some("href") => - { - match &p.value { - Expression::StringLiteral(s) => Some(s.value.to_string()), - _ => None, - } - } - _ => None, - }), - _ => None, - }, - _ => None, - }) - }); + let has_href = effective.contains("href"); + let href = effective.literal("href"); let is_link = name == "a" || self.link_names.contains(name); let native = matches!(name, "button" | "input" | "select" | "textarea") - && !attrs - .get("type") - .and_then(|a| literal(a)) + && !effective.literal("type") .is_some_and(|s| s == "hidden"); - let handler = attrs - .keys() - .any(|k| matches!(*k, "onClick" | "onChange" | "onSubmit")); + let handler = ["onClick", "onChange", "onSubmit"].iter().any(|name| effective.contains(name)); let static_target = href .as_deref() .is_some_and(|h| !h.trim().is_empty() && h != "#" && !h.starts_with("javascript:")); - let disabled = attrs.get("disabled").and_then(|a| boolean(a)) == Some(true); + let disabled = effective.boolean("disabled") == Some(true); let native_toggle = name == "input" - && attrs - .get("type") - .and_then(|a| literal(a)) + && effective.literal("type") .is_some_and(|s| ["checkbox", "radio"].contains(&s.as_str())); let form_control = native && self.form_ancestors > 0 && (name != "button" - || attrs - .get("type") - .and_then(|a| literal(a)) + || effective.literal("type") .is_none_or(|t| t == "submit")); let wired = (is_link && static_target) || (native && handler) || form_control; let form_action = - name == "form" && (attrs.contains_key("action") || attrs.contains_key("onSubmit")); + name == "form" && (effective.contains("action") || effective.contains("onSubmit")); let own_text = element .children .iter() @@ -211,11 +154,9 @@ impl<'a> Visit<'a> for Collector<'_> { }) .collect::>() .join(" "); - let accessible_name = attrs - .get("aria-label") - .and_then(|a| literal(a)) + let accessible_name = effective.literal("aria-label") .unwrap_or(own_text); - let role = attrs.get("role").and_then(|a| literal(a)); + let role = effective.literal("role"); let nav = name == "nav" || role.as_deref() == Some("navigation") || matches!( @@ -241,15 +182,11 @@ impl<'a> Visit<'a> for Collector<'_> { .filter(|b| *b == b'\n') .count() + 1; - let active = attrs - .get("aria-current") - .is_some_and(|a| literal(a).is_some_and(|s| s != "false")) + let active = effective.literal("aria-current").is_some_and(|s| s != "false") || ["data-active", "aria-selected"] .iter() - .any(|k| attrs.get(k).is_some_and(|a| boolean(a) == Some(true))) - || attrs.get("data-state").is_some_and(|a| { - literal(a).is_some_and(|s| ["active", "selected"].contains(&s.as_str())) - }); + .any(|k| effective.boolean(k) == Some(true)) + || effective.literal("data-state").is_some_and(|s| ["active", "selected"].contains(&s.as_str())); let visual_marker = !active && ((candidate && (self.nav_ancestors > 0 || nav || is_link || role.as_deref() == Some("tab"))) @@ -289,6 +226,7 @@ pub(super) fn source_evidence(source: &str) -> Value { } let mut collector = Collector { source, + attributes: AttributeContext::from_program(&parsed.program), ..Default::default() }; // Imports may follow declarations; collect aliases before traversing JSX. @@ -594,17 +532,46 @@ pub(super) fn suggestions(candidates: &Value, inventory: &Value) -> Value { ("reservation", vec!["reservation", "견적", "문의하기"]), ("about", vec!["about", "소개"]), ("services", vec!["services", "서비스"]), + ("location", vec!["location", "오시는 길", "찾아오시는 길", "map"]), + ("contact", vec!["contact", "문의", "연락"]), + ("company", vec!["company", "회사", "기업"]), + ("news", vec!["news", "뉴스", "소식"]), + ("notice", vec!["notice", "공지"]), + ("products", vec!["products", "제품"]), + ("technology", vec!["technology", "기술"]), + ("research", vec!["research", "연구"]), + ("patents", vec!["patents", "특허"]), + ("vision", vec!["vision", "비전"]), + ("history", vec!["history", "연혁"]), + ("ceo", vec!["ceo", "인사말"]), + ("careers", vec!["careers", "채용"]), + ("partners", vec!["partners", "파트너"]), ]; - let options=array(&inventory["routes"]).iter().filter(|r| { + let compact = |value: &str| value.split_whitespace().collect::().to_lowercase(); + let matches_label = |value: &Value| value.as_str().is_some_and(|value| !value.is_empty() && ["text", "name"].iter().any(|key| candidate[*key].as_str().is_some_and(|text| compact(text) == compact(value)))); + let mut options = array(&inventory["codeLinks"]).iter().filter(|link| matches_label(&link["label"])) + .map(|link| json!({"route":link["href"],"evidence":link["evidence"],"kind":"existing-code-link","requiresRecordId":link["href"].as_str().is_some_and(|s|s.contains('['))})).collect::>(); + let resolved = !options.is_empty(); + if options.is_empty() { + options = array(&inventory["confirmedRoutes"]).iter().filter(|route| matches_label(&route["name"])) + .map(|route| json!({"route":route["route"],"evidence":"confirmed-screen-plan","requiresRecordId":route["route"].as_str().is_some_and(|s|s.contains('['))})).collect(); + } + if options.is_empty() { + options=array(&inventory["routes"]).iter().filter(|r| { let route=r["route"].as_str().unwrap_or("").to_lowercase(); - concepts.iter().any(|(segment,labels)|route.split('/').any(|s|s==*segment) && labels.iter().any(|s|label.contains(s))) + concepts.iter().any(|(segment,labels)|route.split('/').any(|s|s==*segment) && labels.iter().any(|s|compact(&label).contains(&compact(s)))) }).map(|r|json!({"route":r["route"],"page":r["page"],"appliesTo":r["appliesTo"],"requiresRecordId":r["route"].as_str().is_some_and(|s|s.contains('['))})).collect::>(); - result.push(json!({"nodeId":candidate["nodeId"],"suggestions":options,"targetResolved":false, - "reason":if options.is_empty(){"No matching route evidence; ask for destination/action."}else{"Label/path heuristic only; confirm authority and record IDs before wiring. Suggestions never resolve a target automatically."}})); + } + result.push(json!({"nodeId":candidate["nodeId"],"suggestions":options,"targetResolved":resolved, + "reason":if resolved {"Matching existing code link provides a literal destination and file:line evidence; runtime wiring remains unverified."} else if options.is_empty(){"No matching route evidence; ask for destination/action."}else{"Plan/label/path suggestion only; confirm authority and record IDs before wiring."}})); } json!(result) } +#[cfg(test)] +#[path = "navigation_suggestion_tests.rs"] +mod suggestion_tests; + /// Compare only explicitly bound URLs. Figma destination node IDs remain unverified. pub(super) fn graph( design: &Value, diff --git a/crates/devup-mcp/src/server/navigation_suggestion_tests.rs b/crates/devup-mcp/src/server/navigation_suggestion_tests.rs new file mode 100644 index 00000000..c9ac0048 --- /dev/null +++ b/crates/devup-mcp/src/server/navigation_suggestion_tests.rs @@ -0,0 +1,27 @@ +use super::*; + +#[test] +fn nested_korean_location_uses_any_route_segment() { + let result = suggestions(&json!([{"nodeId":"location","text":"오시는 길"}]), &json!({"routes":[{"route":"/company/location"}]})); + assert_eq!(result[0]["suggestions"][0]["route"], "/company/location"); + assert_eq!(result[0]["targetResolved"], false); +} + +#[test] +fn existing_links_precede_confirmed_names_and_concepts() { + let result = suggestions(&json!([{"nodeId":"location","text":"오시는 길"}]), &json!({ + "routes":[{"route":"/company/location"}], + "codeLinks":[{"label":"오시는길","href":"/visit","evidence":"src/components/Header.tsx:7"}], + "confirmedRoutes":[{"name":"오시는 길","route":"/directions"}] + })); + assert_eq!(result[0]["suggestions"][0]["route"], "/visit"); + assert_eq!(result[0]["suggestions"][0]["evidence"], "src/components/Header.tsx:7"); + assert_eq!(result[0]["targetResolved"], true); +} + +#[test] +fn confirmed_names_are_suggestions_not_resolved_targets() { + let result = suggestions(&json!([{"nodeId":"location","text":"오시는 길"}]), &json!({"confirmedRoutes":[{"name":"오시는길","route":"/directions"}]})); + assert_eq!(result[0]["suggestions"][0]["route"], "/directions"); + assert_eq!(result[0]["targetResolved"], false); +} diff --git a/crates/devup-mcp/src/server/page_scaffold.rs b/crates/devup-mcp/src/server/page_scaffold.rs index 41e9570b..c399ca13 100644 --- a/crates/devup-mcp/src/server/page_scaffold.rs +++ b/crates/devup-mcp/src/server/page_scaffold.rs @@ -70,6 +70,9 @@ pub(crate) fn validate( } fn key(result: &Map) -> Result { + if result.get("responsiveTsx").is_some() { + return Ok("pageScaffold".into()); + } if let Some(frames) = result.get("frames").and_then(Value::as_array) { if frames.len() != 1 { return Err(invalid( @@ -161,7 +164,7 @@ pub(crate) fn attach( .as_object_mut() .unwrap() }; - let code = carrier.get("tsx").and_then(Value::as_str).ok_or_else(|| { + let code = carrier.get("responsiveTsx").or_else(|| carrier.get("tsx")).and_then(Value::as_str).ok_or_else(|| { invalid("pageScaffold has no generated screen; inspect projection failures.") })?; // The existing generator emits one named function. Do not split its JSX or infer hooks. @@ -252,7 +255,7 @@ pub(crate) fn attach( .get_mut("outputResults") .and_then(Value::as_object_mut) { - if let Some(tsx) = output.get("tsx").cloned() { + if let Some(tsx) = output.get("responsiveTsx").or_else(|| output.get("tsx")).cloned() { output.insert("pageScaffold".into(), tsx); } if !keep_tsx { diff --git a/crates/devup-mcp/src/server/project_context/context_and_inventory.rs b/crates/devup-mcp/src/server/project_context/context_and_inventory.rs index 59493f45..1d0fa33a 100644 --- a/crates/devup-mcp/src/server/project_context/context_and_inventory.rs +++ b/crates/devup-mcp/src/server/project_context/context_and_inventory.rs @@ -17,6 +17,7 @@ mod ui; mod export; pub(super) use export::attach as attach_export_context; pub(super) use export::export_context; +pub(super) use export::checked_export; pub(super) fn routes_for_validation(project_root: Option<&str>) -> Value { project_root.map(export_context).map(|v|v["ui"].clone()) @@ -721,4 +722,4 @@ pub(super) fn relative_display(root: &Path, file: &Path) -> String { file.strip_prefix(root) .map(|relative| relative.to_string_lossy().replace('\\', "/")) .unwrap_or_else(|_| display_path(file)) -} \ No newline at end of file +} diff --git a/crates/devup-mcp/src/server/project_context_export.rs b/crates/devup-mcp/src/server/project_context_export.rs index 66d5b516..3fc8cf37 100644 --- a/crates/devup-mcp/src/server/project_context_export.rs +++ b/crates/devup-mcp/src/server/project_context_export.rs @@ -115,13 +115,18 @@ pub(crate) fn export_context(project_root: &str) -> Value { /// Added context is bounded independently and respects remaining inline space. pub(crate) fn attach(result: &mut Value, root: &str) { - let context = export_context(root); + let mut context = export_context(root); + if let Some(evidence) = result.as_object_mut().unwrap().remove("_navigationEvidence") { + context["ui"]["codeLinks"] = evidence["codeLinks"].clone(); + context["ui"]["confirmedRoutes"] = evidence["confirmedRoutes"].clone(); + } let suggestions = super::super::navigation::suggestions(&result["interactiveCandidates"], &context["ui"]); + summarize(&mut context, result, &suggestions, root); let mut added = json!({"projectContext":context,"routeSuggestions":suggestions}); let wire_bytes = serde_json::to_vec(&super::super::tool_result(result.clone())) .map_or(usize::MAX, |v| v.len()); - let budget = 65_536usize.min( + let budget = 4_096usize.min( super::super::delivery::MAX_INLINE_TOTAL_BYTES // Review, skills, theme-validation and job metadata are attached // around this boundary and are also duplicated in MCP content. @@ -139,6 +144,8 @@ pub(crate) fn attach(result: &mut Value, root: &str) { "/projectContext/ui/routes", "/projectContext/ui/diagnostics", "/projectContext/ui/excludedPaths", + "/projectContext/ui/nonPageDestinations", + "/projectContext/referencedAssets", "/routeSuggestions", ]; let path = pointers @@ -173,3 +180,69 @@ pub(crate) fn attach(result: &mut Value, root: &str) { .unwrap() .extend(added.as_object().unwrap().clone()); } + +fn summarize(context: &mut Value, result: &Value, suggestions: &Value, root: &str) { + let ui = &context["ui"]; + let routes = ui["routes"].as_array().into_iter().flatten().filter(|route| suggestions.as_array().into_iter().flatten().any(|suggestion| suggestion["suggestions"].as_array().into_iter().flatten().any(|target| target["route"] == route["route"]))) + .map(|route| json!({"route":route["route"],"page":route["page"],"appliesTo":route["appliesTo"],"components":route["components"]})).collect::>(); + let components = ui["components"].as_array().into_iter().flatten().filter(|component| routes.iter().any(|route| route["components"].as_array().is_some_and(|paths| paths.contains(&component["path"])))) + .map(|component| json!({"path":component["path"],"exports":component["exports"],"appliesTo":component["appliesTo"]})).collect::>(); + let mut urls = BTreeSet::new(); + let mut sources = super::super::projection::default_code_paths::written_sources(result); + for scope in std::iter::once(result).chain(result["frames"].as_array().into_iter().flatten()) { + for output in ["tsx", "componentTsx", "responsiveTsx"] { + if let Some(source) = scope[output].as_str() { sources.push((output.into(), source.into())); } + } + for asset in scope["assetManifest"]["assets"].as_array().into_iter().flatten() { + if let Some(path) = asset["path"].as_str() { urls.insert(path.to_owned()); } + } + } + for (_, source) in sources { urls.extend(super::super::asset_guard::asset_urls(&source).into_iter().map(|(url, _)| url)); } + let destinations = ui["nonPageDestinations"].as_array().into_iter().flatten().filter(|destination| destination.as_str().is_some_and(|url| urls.contains(url) || suggestions.as_array().into_iter().flatten().any(|suggestion| suggestion["suggestions"].as_array().into_iter().flatten().any(|target| target["route"] == url)))).cloned().collect::>(); + let (public_files, _) = find_matching_files(Path::new(root), |path| path.components().any(|component| component.as_os_str() == "public"), usize::MAX); + let public_urls = public_files.iter().filter_map(|path| super::relative_display(Path::new(root), path).split_once("public/").map(|(_, asset)|format!("/{asset}"))).collect::>(); + let assets = urls.into_iter().map(|url| json!({"exists":public_urls.contains(url.split(['?', '#']).next().unwrap_or(&url)),"url":url})).collect::>(); + let counts = json!({"routes":ui["totalRoutes"],"components":ui["totalComponents"],"publicFiles":public_files.len()}); + let mut destinations = destinations; + destinations.extend(assets.iter().filter(|asset| asset["exists"] == true && !destinations.contains(&asset["url"])).map(|asset|asset["url"].clone()).collect::>()); + let summary = json!({"found":ui["found"],"routes":routes,"components":components,"nonPageDestinations":destinations,"customRoutingUnverified":ui["customRoutingUnverified"],"truncation":ui["truncation"]}); + *context = json!({"state":"summary","counts":counts,"ui":summary,"referencedAssets":assets, + "nextAction":{"tool":"devup_project_context","arguments":{"projectRoot":root,"scope":"ui"},"alternativeScope":"routes","how":"Read ui for the full component inventory, or routes for page destinations."}}); +} + +pub(crate) async fn navigation_evidence(result: &mut Value, root: &str) { + let code_links = match super::super::screen_plan::project_evidence(root).await { + Ok(evidence) => evidence.nav_links.into_iter().map(|(label, href, evidence)|json!({"label":label,"href":href,"evidence":evidence})).collect::>(), + Err(_) => vec![], + }; + let plan = super::super::screen_plan::read_plan(root).ok().flatten().unwrap_or(Value::Null); + let mut confirmed = vec![]; + if plan["status"] == "confirmed" { + for route in plan["routes"].as_array().into_iter().flatten() { + confirmed.push(json!({"name":route["name"],"route":route["url"]})); + } + for frame in plan["frameInventory"]["frames"].as_array().into_iter().flatten() { + if frame["route"].as_str().is_some_and(|route| route.starts_with('/')) { + confirmed.push(json!({"name":frame["name"],"route":frame["route"]})); + } + } + for label in plan["linkLabelCheck"]["labels"].as_array().into_iter().flatten().filter(|label|label["state"] == "route-bound") { + if let Some(url) = label["resolved"]["url"].as_str().filter(|url|url.starts_with('/')) { + confirmed.push(json!({"name":label["label"],"route":url})); + } + } + } + result["_navigationEvidence"] = json!({"codeLinks":code_links,"confirmedRoutes":confirmed}); +} + +pub(crate) async fn checked_export(mut result: Value, root: Option<&str>, lookup: &super::super::skills::Lookup) -> Value { + if result.get("interactionCoverage").is_some() && let Some(root) = root { + navigation_evidence(&mut result, root).await; + } + let mut result = super::super::with_project_checks(result, root, lookup); + if result["metadataDelivery"]["name"] == "written-export-details.json" && result["skillGap"].is_object() { + let gap = &result["skillGap"]; + result["skillGap"] = json!({"missing":gap["missing"].as_array().into_iter().flatten().map(|item|json!({"skill":item["skill"],"whyYouNeedIt":item["whyYouNeedIt"]})).collect::>(),"outdated":gap["outdated"],"runtime":gap["runtime"],"workspace":gap["workspace"],"nextAction":{"tool":"devup_skills","arguments":{"action":"status","projectRoot":root},"how":"Read skill installation details, install missing skills, then load them before editing generated code."}}); + } + result +} diff --git a/crates/devup-mcp/src/server/projection.rs b/crates/devup-mcp/src/server/projection.rs index f8344b03..4b5d265d 100644 --- a/crates/devup-mcp/src/server/projection.rs +++ b/crates/devup-mcp/src/server/projection.rs @@ -13,6 +13,7 @@ include!("projection/artifact_metadata.rs"); include!("projection/verification_scope.rs"); include!("projection/diagnostics.rs"); include!("projection/component_projection.rs"); +include!("projection/instance_overrides.rs"); include!("projection/completion.rs"); include!("projection/diagnostic_screens.rs"); include!("projection/regression_tests.rs"); diff --git a/crates/devup-mcp/src/server/projection/completion.rs b/crates/devup-mcp/src/server/projection/completion.rs index 697d939f..d6122a79 100644 --- a/crates/devup-mcp/src/server/projection/completion.rs +++ b/crates/devup-mcp/src/server/projection/completion.rs @@ -29,16 +29,23 @@ pub(super) async fn complete_operation( } // What this export wrote for each marked node, so the audit can tell // code drift from a converter bug later. - if let Some(root) = project_root.as_deref() - && response.get("figmaNodeIds").is_some() - { + if let Some(root) = project_root.as_deref() { let root_node = payload.target.node_id.clone().unwrap_or_default(); + let mut record_source = response.clone(); + for (key, code) in default_code_paths::written_sources(&response) { + if let Some(frame_key) = key.strip_prefix("frame:") { + if let Some((id, output)) = frame_key.rsplit_once(':') + && let Some(frames) = record_source["frames"].as_array_mut() { + for frame in frames.iter_mut().filter(|frame| frame["nodeId"] == id) { frame[output] = json!(code); } + } + } else { record_source[&key] = json!(code); } + } match super::export_record::write( output_policy, root, &payload.target.file_key, &root_node, - &response, + &record_source, ) { Ok(path) => response["exportRecord"] = json!(path), Err(error) => response["exportRecordError"] = json!(error.message), diff --git a/crates/devup-mcp/src/server/projection/default_code_paths.rs b/crates/devup-mcp/src/server/projection/default_code_paths.rs new file mode 100644 index 00000000..3cca1761 --- /dev/null +++ b/crates/devup-mcp/src/server/projection/default_code_paths.rs @@ -0,0 +1,121 @@ +use super::*; + +pub(crate) fn written_sources(result: &Value) -> Vec<(String, String)> { + result["outputPaths"].as_object().into_iter().flat_map(|paths| paths.iter()) + .filter(|(key, _)| matches!(key.rsplit(':').next(), Some("tsx" | "componentTsx" | "responsiveTsx"))) + .filter_map(|(key, path)| Some((key.clone(), std::fs::read_to_string(path.as_str()?).ok()?))) + .collect() +} + +pub(super) fn apply(result: &mut Map, paths: &mut BTreeMap, root: Option<&str>, policy: &OutputPolicy) { + let Some(root) = root else { return }; + let mut defaults = Map::new(); + for (output, extension) in [("tsx", "tsx"), ("componentTsx", "component.tsx"), ("responsiveTsx", "responsive.tsx"), ("sourceMap", "sourcemap.json")] { + if paths.contains_key(output) { continue; } + let targets: Vec<(String, String)> = if let Some(frames) = result.get("frames").and_then(Value::as_array) { + frames.iter().filter(|frame| frame.get(output).is_some()).filter_map(|frame| { + let id = frame["nodeId"].as_str()?; + Some((format!("frame:{id}:{output}"), id.into())) + }).collect() + } else if result.contains_key(output) { + let id = result.get("_defaultNodeId").and_then(Value::as_str).unwrap_or("export"); + vec![(output.into(), id.into())] + } else { vec![] }; + for (key, id) in targets { + if paths.contains_key(&key) { continue; } + let path = std::path::Path::new(root).join(".devup/exports").join(format!("{}.{extension}", id.replace([':', ';'], "-"))); + let path = path.to_string_lossy().into_owned(); + if policy.resolve(&path).is_ok() { + paths.insert(key.clone(), path.clone()); + defaults.insert(key, json!(path)); + } else if let Some(warnings) = result.entry("warnings").or_insert_with(|| json!([])).as_array_mut() { + warnings.push(json!({"code":"DEVUP_DEFAULT_OUTPUT_PATH_SKIPPED","path":path,"message":"Default output is not allowed; keeping inline delivery."})); + } + } + } + if !defaults.is_empty() { result.insert("defaultOutputPaths".into(), json!(defaults)); } +} + +pub(super) fn omit_written(result: &mut Map) { + let paths = result.get("outputPaths").and_then(Value::as_object).cloned().unwrap_or_default(); + for (key, path) in paths { + if let Some(frame_key) = key.strip_prefix("frame:") { + if let Some((id, output)) = frame_key.rsplit_once(':') + && matches!(output, "tsx" | "componentTsx" | "responsiveTsx") + && let Some(frames) = result.get_mut("frames").and_then(Value::as_array_mut) { + for frame in frames.iter_mut().filter(|frame| frame["nodeId"] == id) { + if let Some(object) = frame.as_object_mut() { object.remove(output); } + frame["outputResults"][output]["bodyDelivery"] = json!("written-to-file"); + frame["outputResults"][output]["path"] = path.clone(); + } + } + } else if matches!(key.as_str(), "tsx" | "componentTsx" | "responsiveTsx") { + result.remove(&key); + let outputs = result.entry("outputResults").or_insert_with(|| json!({})); + outputs[&key]["bodyDelivery"] = json!("written-to-file"); + outputs[&key]["path"] = path; + } + } +} + +pub(super) fn compact_written(result: &mut Map, outputs: &mut Vec, artifact_id: &str) -> Result<(), DevupError> { + if !result.get("outputPaths").and_then(Value::as_object).is_some_and(|paths| paths.keys().any(|key| matches!(key.rsplit(':').next(), Some("tsx" | "componentTsx" | "responsiveTsx")))) { + return Ok(()); + } + let details = ProjectedOutput::text("written-export-details.json", "application/json", serde_json::to_vec(result).map_err(|error| DevupError::new(ErrorCode::DevupInvalidInput, error.to_string(), false))?); + let uri = details.manifest_uri(artifact_id); + let issues = result.get("projectionIssues").cloned().unwrap_or_else(|| json!([])); + let preview = issues.as_array().into_iter().flatten().take(10).map(|issue|json!({"code":issue["code"],"nodeId":issue["nodeId"],"property":issue["property"],"message":issue["message"],"fidelityImpact":issue["fidelityImpact"],"how":issue["details"]["nextAction"].as_str().or_else(|| issue["details"]["nextAction"]["how"].as_str())})).collect::>(); + result.retain(|key, _| !matches!(key.as_str(), "sectionChecklist" | "imageFillAudit" | "placementContracts" | "projectionReview" | "verdictScope" | "widthBehavior" | "fidelity" | "completionEvidence")); + result.insert("projectionIssues".into(), json!(preview)); + result.insert("metadataDelivery".into(), json!({"mode":"resource-details","uri":uri,"name":"written-export-details.json","requiresFullDetailRead":true,"projectionIssueCount":issues.as_array().map_or(0, Vec::len)})); + outputs.push(details); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn defaults_fill_each_frame_without_replacing_an_explicit_path() -> anyhow::Result<()> { + let root = std::env::temp_dir(); + let policy = OutputPolicy::from_roots(vec![root.clone()])?; + let mut result = json!({"frames":[ + {"nodeId":"1:2","tsx":"first","responsiveTsx":"responsive"}, + {"nodeId":"1:3;4","tsx":"second"} + ]}).as_object().unwrap().clone(); + let mut paths = BTreeMap::from([("frame:1:2:tsx".into(), "explicit.tsx".into())]); + apply(&mut result, &mut paths, root.to_str(), &policy); + assert_eq!(paths["frame:1:2:tsx"], "explicit.tsx"); + assert!(paths["frame:1:3;4:tsx"].ends_with("1-3-4.tsx")); + assert!(paths["frame:1:2:responsiveTsx"].ends_with("1-2.responsive.tsx")); + assert_eq!(result["defaultOutputPaths"].as_object().unwrap().len(), 2); + Ok(()) + } + + #[test] + fn default_extensions_match_the_output_format() -> anyhow::Result<()> { + let root = std::env::temp_dir(); + let policy = OutputPolicy::from_roots(vec![root.clone()])?; + let mut result = json!({"_defaultNodeId":"51:6723","componentTsx":"component","sourceMap":{}}).as_object().unwrap().clone(); + let mut paths = BTreeMap::new(); + apply(&mut result, &mut paths, root.to_str(), &policy); + assert!(paths["componentTsx"].ends_with("51-6723.component.tsx")); + assert!(paths["sourceMap"].ends_with("51-6723.sourcemap.json")); + Ok(()) + } + + #[test] + fn disallowed_defaults_keep_inline_code() -> anyhow::Result<()> { + let root = std::env::temp_dir(); + let policy = OutputPolicy::from_roots(vec![root.join(".")])?; + let mut result = json!({"tsx":"code","_defaultNodeId":"1:2"}).as_object().unwrap().clone(); + let mut paths = BTreeMap::new(); + apply(&mut result, &mut paths, root.parent().and_then(std::path::Path::to_str), &policy); + assert!(paths.is_empty()); + assert_eq!(result["tsx"], "code"); + assert_eq!(result["warnings"][0]["code"], "DEVUP_DEFAULT_OUTPUT_PATH_SKIPPED"); + Ok(()) + } +} diff --git a/crates/devup-mcp/src/server/projection/feedback_codegen_tests.rs b/crates/devup-mcp/src/server/projection/feedback_codegen_tests.rs index d1b1825a..615381e0 100644 --- a/crates/devup-mcp/src/server/projection/feedback_codegen_tests.rs +++ b/crates/devup-mcp/src/server/projection/feedback_codegen_tests.rs @@ -1,3 +1,165 @@ +#[test] +fn responsive_merge_diagnostics_survive_export_summary_without_opt_in() { + let data = responsive_payload(); + let outputs = vec!["responsiveTsx".into()]; + let codes = [ + ("DEVUP_CODEGEN_RESPONSIVE_UNMATCHED", devup_mcp_figma::FidelityImpact::Approximated), + ("DEVUP_CODEGEN_RESPONSIVE_DRAWN_WIDTH_EMPTY", devup_mcp_figma::FidelityImpact::Lossy), + ]; + let diagnostics = codes.iter().map(|(code, impact)| Diagnostic { + code: (*code).into(), + message: "Responsive merge requires review".into(), + node_id: Some("1:1".into()), + property: Some("w".into()), + details: Some(json!({"output":"responsiveTsx","slot":0,"width":390, + "originalValue":"390px","nextAction":"Inspect responsive merge provenance."})), + severity: Some(DiagnosticSeverity::Warning), + fidelity_impact: Some(*impact), + ..Diagnostic::default() + }).collect(); + let (result,) = summarize_projection_diagnostics::summarize_projection_diagnostics( + summarize_projection_diagnostics::ProjectionDiagnostics { + payload: &data, + outputs: &outputs, + include_diagnostics: false, + root_layout: devup_mcp_devup_ui::codegen::RootLayout::Standalone, + asset_names_per_node: true, + scope: &"node".into(), + strict: false, + frame_ids: &Vec::new(), + all_screens: false, + asset_captures: &Vec::new(), + delivery: DeliveryMode::Inline, + result: json!({"responsiveTsx":"export default function Page() { return null }"}) + .as_object().unwrap().clone(), + failures: Vec::new(), + warnings: Vec::new(), + completeness_report: &data.completeness_report(), + section_tsx_projected: false, + projection_diagnostics: diagnostics, + fidelity_reports: Vec::new(), + theme_conflict_count: 0, + theme_unresolved_count: 0, + component_name_for_components: &None, + target_kind: TargetKind::Screen, + }, + ).unwrap(); + let mut result = Value::Object(result); + super::super::review::attach(&mut result); + for (code, impact) in codes { + let issue = result["projectionIssues"].as_array().unwrap().iter() + .find(|issue| issue["code"] == code).unwrap(); + assert_eq!(issue["details"]["output"], "responsiveTsx"); + assert_eq!(issue["fidelityImpact"], json!(impact)); + let review = result["reviewChecklist"].as_array().unwrap().iter() + .find(|item| item["code"] == code).unwrap(); + assert_eq!(review["nodeIds"], json!(["1:1"])); + assert_eq!(review["severity"], if impact == devup_mcp_figma::FidelityImpact::Lossy { + "required" + } else { + "recommended" + }); + } +} + +#[tokio::test] +async fn instance_overrides_reach_export_and_required_review_without_opt_in() { + for output in ["tsx", "componentTsx"] { + let mut data = payload(); + let root = data.snapshot.roots[0].clone(); + let mut master = data.snapshot.nodes[&root].clone(); + master.id = "override-master".into(); + master.node_type = "COMPONENT".into(); + data.snapshot.nodes.insert(master.id.clone(), master); + let instance = data.snapshot.nodes.get_mut(&root).unwrap(); + instance.node_type = "INSTANCE".into(); + instance.fields.insert("mainComponentId".into(), json!("override-master")); + instance.fields.insert("overrides".into(), json!([{"id":root,"overriddenFields":["fills"]}])); + instance.fields.insert("fills".into(), json!([{"type":"SOLID","color":{"r":0,"g":0.5,"b":0.5}}])); + let mut result = project(data, operation(&[output])).await.unwrap(); + super::super::review::attach(&mut result); + assert_eq!(result["instanceOverrides"][0]["instanceNodeId"], root, "{result}"); + assert_eq!(result["instanceOverrides"][0]["resolution"], "resolved"); + assert!(result[output].as_str().unwrap().contains("// @devup-override ")); + assert!(result["projectionIssues"].as_array().unwrap().iter().any(|item| + item["code"] == "DEVUP_CODEGEN_INSTANCE_OVERRIDE")); + assert!(result["reviewChecklist"].as_array().unwrap().iter().any(|item| + item["code"] == "DEVUP_CODEGEN_INSTANCE_OVERRIDE" && item["severity"] == "required")); + } +} + +#[tokio::test] +async fn responsive_unmatched_diagnostic_reaches_export_review_without_opt_in() { + let mut data = responsive_payload(); + for (root, children) in [ + ("1:1", vec!["small"]), + ("1:2", vec!["wide-a", "wide-b"]), + ] { + data.snapshot.nodes.get_mut(root).unwrap().fields.insert("childrenIds".into(), json!(children)); + for child in children { + data.snapshot.nodes.insert(child.into(), serde_json::from_value(json!({ + "id":child,"type":"FRAME","fields":{ + "name":"Logo","parentId":root,"width":100,"height":24 + } + })).unwrap()); + } + } + let mut op = operation(&["responsiveTsx"]); + if let PendingOperation::Export { frame_ids, include_diagnostics, .. } = &mut op { + *frame_ids = vec!["1:1".into(), "1:2".into()]; + *include_diagnostics = false; + } + let mut result = project(data, op).await.unwrap(); + super::super::review::attach(&mut result); + assert!(result["responsiveTsx"].is_string(), "{result}"); + let code = "DEVUP_CODEGEN_RESPONSIVE_UNMATCHED"; + for items in [ + &result["projectionIssues"], + &result["responsiveReviewChecklist"]["items"], + &result["reviewChecklist"], + ] { + assert!(items.as_array().unwrap().iter().any(|item| item["code"] == code), "{items}"); + } +} + +#[tokio::test] +async fn suspicious_design_values_are_published_for_recommended_review_without_loss() { + let mut data = payload(); + let root = data.snapshot.roots[0].clone(); + let fields = &mut data.snapshot.nodes.get_mut(&root).unwrap().fields; + fields.insert("width".into(), json!(171)); + fields.insert("height".into(), json!(80)); + fields.insert("layoutSizingHorizontal".into(), json!("FIXED")); + fields.insert("layoutSizingVertical".into(), json!("FIXED")); + fields.remove("parentType"); + fields.remove("childrenIds"); + let mut result = project(data, operation(&["tsx"])).await.unwrap(); + super::super::review::attach(&mut result); + let issue = result["projectionIssues"].as_array().unwrap().iter() + .find(|issue| issue["code"] == "DEVUP_CODEGEN_SUSPICIOUS_VALUE").expect("published advisory"); + assert_eq!(issue["fidelityImpact"], "none"); + assert_eq!(issue["details"]["appliedValue"], "171px"); + assert!(result["tsx"].as_str().unwrap().contains("w=\"171px\"")); + assert_ne!(result["quality"]["projection"], "lossy"); + assert!(result["reviewChecklist"].as_array().unwrap().iter().any(|item| + item["code"] == "DEVUP_CODEGEN_SUSPICIOUS_VALUE" && item["severity"] == "recommended")); +} + +#[tokio::test] +async fn stale_plugin_warning_is_visible_without_optional_diagnostics() { + let mut data = payload(); + data.snapshot.diagnostics.push(serde_json::from_value(json!({ + "code":"DEVUP_BRIDGE_PLUGIN_STALE", "message":"Plugin scripts are stale.", + "severity":"warning", "fidelityImpact":"none", + "details":{"pluginStale":true,"expected":"current","actual":"old"} + })).unwrap()); + let result = project(data, operation(&["tsx"])).await.unwrap(); + let issues = result["projectionIssues"].as_array().unwrap(); + let warning = issues.iter().find(|issue| issue["code"] == "DEVUP_BRIDGE_PLUGIN_STALE").unwrap(); + assert_eq!(warning["fidelityImpact"], "none"); + assert_eq!(warning["details"]["pluginStale"], true); +} + #[tokio::test] async fn feedback36_confirmed_pair_merges_despite_different_names() { let directory = std::env::temp_dir().join(format!("feedback36-{}", rand::random::())); @@ -15,7 +177,8 @@ async fn feedback36_confirmed_pair_merges_despite_different_names() { theme_options.project_root = Some(directory.to_string_lossy().into_owned()); } let result = project(data, op).await.unwrap(); - assert!(result["responsiveTsx"].is_string(), "{result}"); + assert!(!std::fs::read_to_string(result["outputPaths"]["responsiveTsx"].as_str().unwrap()).unwrap().is_empty()); + assert!(result.get("responsiveTsx").is_none()); assert_eq!(result["responsiveSlots"], json!([0,4])); std::fs::remove_dir_all(directory).unwrap(); } @@ -89,9 +252,9 @@ async fn feedback66_component_roots_export_despite_uncaptured_nested_master() { output_paths.insert("componentTsx".into(), path.to_string_lossy().into_owned()); } let result = project(data, op).await.unwrap(); - let code = result["componentTsx"].as_str().expect("component definition"); + let code = std::fs::read_to_string(&path).unwrap(); assert!(code.contains("export function Button"), "{root}: {result}"); - assert_eq!(std::fs::read_to_string(path).unwrap(), code); + assert!(result.get("componentTsx").is_none()); assert!(result["failures"].as_array().unwrap().is_empty(), "{root}: {result}"); std::fs::remove_dir_all(directory).unwrap(); } diff --git a/crates/devup-mcp/src/server/projection/feedback_theme_tests.rs b/crates/devup-mcp/src/server/projection/feedback_theme_tests.rs index c67e56f6..c660816a 100644 --- a/crates/devup-mcp/src/server/projection/feedback_theme_tests.rs +++ b/crates/devup-mcp/src/server/projection/feedback_theme_tests.rs @@ -19,6 +19,19 @@ fn feedback_theme_payload() -> CollectedPayload { data } +#[tokio::test] +async fn feedback_theme_reports_dropped_modes_without_optional_diagnostics() { + let mut data = feedback_theme_payload(); + data.variables.as_mut().unwrap().raw["collections"] = json!([ + {"id":"empty","name":"Empty","defaultModeId":"one","modes":[{"modeId":"one","name":"Mode 1"}]} + ]); + let mut op = operation(&["devupJson"]); + if let PendingOperation::Export { include_diagnostics, .. } = &mut op { *include_diagnostics = false; } + let result = project(data, op).await.unwrap(); + assert_eq!(result["themeModesDropped"][0]["collectionId"], "empty"); + assert_eq!(result["themeModesDropped"][0]["reason"], "no-defined-color-values"); +} + #[tokio::test] async fn feedback_theme_partial_export_keeps_code_and_valid_styles_with_named_issue() { let mut data = feedback_theme_payload(); diff --git a/crates/devup-mcp/src/server/projection/finalize_export_delivery.rs b/crates/devup-mcp/src/server/projection/finalize_export_delivery.rs index e0ca5a97..be2124cd 100644 --- a/crates/devup-mcp/src/server/projection/finalize_export_delivery.rs +++ b/crates/devup-mcp/src/server/projection/finalize_export_delivery.rs @@ -9,6 +9,8 @@ pub(super) struct ExportDelivery<'a> { pub(super) asset_names_per_node: bool, pub(super) strict: bool, pub(super) output_paths: BTreeMap, + pub(super) project_root: Option, + pub(super) default_node_id: String, pub(super) page_scaffold: Option, pub(super) asset_captures: Vec, pub(super) web_ready_images: bool, @@ -43,7 +45,9 @@ pub(super) async fn finalize_export_delivery( outputs, asset_names_per_node, strict, - output_paths, + mut output_paths, + project_root, + default_node_id, page_scaffold, asset_captures, web_ready_images, @@ -66,6 +70,15 @@ pub(super) async fn finalize_export_delivery( mut asset_resource_outputs, responsive_widths, } = input; + result.insert("_defaultNodeId".into(), json!(default_node_id)); + default_code_paths::apply(&mut result, &mut output_paths, project_root.as_deref(), output_policy); + result.remove("_defaultNodeId"); + for output in ["tsx", "componentTsx", "responsiveTsx"] { + if output_paths.contains_key(output) + && let Some(code) = result.get(output).and_then(Value::as_str) { + pending_text_outputs.insert(output.into(), code.into()); + } + } if scaffold_requested && let Some(refusal) = page_scaffold::directory_refusal(&result, &output_paths, output_policy)? @@ -163,6 +176,10 @@ pub(super) async fn finalize_export_delivery( super::super::asset_guard::refresh_export_asset_urls(&mut value); audit_delivered_properties(&mut value); super::super::interactions::attach(&mut value, &payload.snapshot); + attach_instance_overrides(&mut value, payload, &payload.snapshot.roots); + if let Some(root) = project_root.as_deref() { + super::super::screen_plan::attach_component_width_plan(&mut value, root)?; + } attach_diagnostic_screens(&mut value, &owners); super::super::verdict_scope::attach(&mut value); attach_verification_scope(&mut value, &responsive_widths); @@ -235,7 +252,7 @@ pub(super) async fn finalize_export_delivery( } if let Some(frames) = result.get("frames").and_then(Value::as_array) { for frame in frames { - for field in ["tsx", "componentTsx", "sourceMap"] { + for field in ["tsx", "componentTsx", "responsiveTsx", "sourceMap"] { let Some(contents) = frame.get(field) else { continue; }; @@ -494,6 +511,10 @@ pub(super) async fn finalize_export_delivery( let mut projected_outputs = projected_outputs; projected_outputs.extend(asset_resource_outputs); projected_outputs.extend(compact_details); + if !response_selection.active() && project_root.is_some() { + default_code_paths::omit_written(&mut result); + default_code_paths::compact_written(&mut result, &mut projected_outputs, &artifact.artifact_id)?; + } let mut result = Value::Object(result); let attachment = apply_delivery( &mut result, @@ -514,6 +535,7 @@ pub(super) async fn finalize_export_delivery( return Err(error); } commit_delivery(attachment); + if let Some(object) = result.as_object_mut() { default_code_paths::omit_written(object); } if let Some(path) = result["outputPaths"].get("designFingerprints").cloned() { result["outputPathResults"]["committed"] = json!({"designFingerprints":path}); } diff --git a/crates/devup-mcp/src/server/projection/instance_overrides.rs b/crates/devup-mcp/src/server/projection/instance_overrides.rs new file mode 100644 index 00000000..c6587738 --- /dev/null +++ b/crates/devup-mcp/src/server/projection/instance_overrides.rs @@ -0,0 +1,57 @@ +fn attach_instance_overrides( + result: &mut Value, + payload: &CollectedPayload, + roots: &[String], +) { + let options = CodegenOptions::default().with_payload_tokens(payload); + let mut records = Vec::new(); + if let Some(frames) = result.get_mut("frames").and_then(Value::as_array_mut) { + for frame in frames { + if let Some(id) = frame["nodeId"].as_str().map(str::to_owned) { + attach_override_records(frame, devup_mcp_devup_ui::codegen::instance_overrides::instance_overrides( + &payload.snapshot, &id, &options)); + if let Some(entries) = frame["instanceOverrides"].as_array() { + for entry in entries { + if !records.contains(entry) { records.push(entry.clone()); } + } + } + } + } + } + if ["tsx", "componentTsx", "responsiveTsx"].iter().any(|key| result[*key].is_string()) { + for record in roots.iter().flat_map(|id| { + devup_mcp_devup_ui::codegen::instance_overrides::instance_overrides(&payload.snapshot, id, &options) + }) { + if !records.contains(&record) { records.push(record); } + } + } + attach_override_records(result, records); +} + +fn attach_override_records(result: &mut Value, records: Vec) { + if records.is_empty() { return; } + for field in ["tsx", "componentTsx", "responsiveTsx"] { + if let Some(source) = result.get(field).and_then(Value::as_str).map(str::to_owned) { + let mut source = source; + for record in &records { + let hint = format!("// @devup-override {}", record.to_string() + .replace('\u{2028}', "\\u2028").replace('\u{2029}', "\\u2029")); + if !source.contains(&hint) { source.push_str(&format!("\n{hint}\n")); } + } + result[field] = json!(source); + } + } + let issues = result.as_object_mut().map(|object| object.entry("projectionIssues").or_insert_with(|| json!([]))); + if let Some(issues) = issues.and_then(Value::as_array_mut) { + for record in &records { + let issue = json!(devup_mcp_devup_ui::codegen::instance_overrides::diagnostic(record.clone())); + if !issues.iter().any(|existing| existing["code"] == issue["code"] && existing["details"] == issue["details"]) { + issues.push(issue); + } + } + } + result["instanceOverrides"] = json!(records); + if result["deliverable"].is_object() { + result["deliverable"]["isFinal"] = json!(false); + } +} diff --git a/crates/devup-mcp/src/server/projection/project_operation.rs b/crates/devup-mcp/src/server/projection/project_operation.rs index 93a44d42..3f4ddf7d 100644 --- a/crates/devup-mcp/src/server/projection/project_operation.rs +++ b/crates/devup-mcp/src/server/projection/project_operation.rs @@ -830,6 +830,8 @@ async fn project_operation( asset_names_per_node, strict, output_paths, + project_root: theme_options.project_root.clone(), + default_node_id: if frame_ids.len() == 1 { frame_ids[0].clone() } else { payload.target.node_id.clone().unwrap_or_else(|| "export".into()) }, page_scaffold, asset_captures, web_ready_images, diff --git a/crates/devup-mcp/src/server/projection/project_responsive_output.rs b/crates/devup-mcp/src/server/projection/project_responsive_output.rs index ef759bed..75a882be 100644 --- a/crates/devup-mcp/src/server/projection/project_responsive_output.rs +++ b/crates/devup-mcp/src/server/projection/project_responsive_output.rs @@ -177,6 +177,12 @@ pub(super) fn project_responsive_output(input: ResponsiveOutput<'_>) -> Responsi "responsiveReviewChecklist".into(), responsive_groups::review(&responsive_snapshot), ); + for diagnostic in &merged.diagnostics { + projection_diagnostics.push(diagnostic.clone()); + if let Some(review) = result.get_mut("responsiveReviewChecklist").and_then(|review| review.get_mut("items")).and_then(Value::as_array_mut) { + review.push(json!({"code":diagnostic.code,"nodeId":diagnostic.node_id,"details":diagnostic.details})); + } + } responsive_widths = responsive_snapshot .roots .iter() diff --git a/crates/devup-mcp/src/server/projection/project_section_frames.rs b/crates/devup-mcp/src/server/projection/project_section_frames.rs index 3054da40..0dd62ffb 100644 --- a/crates/devup-mcp/src/server/projection/project_section_frames.rs +++ b/crates/devup-mcp/src/server/projection/project_section_frames.rs @@ -248,7 +248,7 @@ pub(super) fn project_section_frames(input: SectionFrames<'_>) -> SectionFramePr .iter() .filter( |d| d.fidelity_impact() != devup_mcp_figma::FidelityImpact::None - || d.code == "DEVUP_CODEGEN_PROPERTY_UNMAPPED" + || matches!(d.code.as_str(), "DEVUP_CODEGEN_PROPERTY_UNMAPPED" | "DEVUP_CODEGEN_SUSPICIOUS_VALUE") ) .collect::>() ); diff --git a/crates/devup-mcp/src/server/projection/setup.rs b/crates/devup-mcp/src/server/projection/setup.rs index 15841971..9cf58a07 100644 --- a/crates/devup-mcp/src/server/projection/setup.rs +++ b/crates/devup-mcp/src/server/projection/setup.rs @@ -4,6 +4,8 @@ mod asset_policy; mod exclude_hidden_asset_outputs; #[path = "finalize_export_delivery.rs"] mod finalize_export_delivery; +#[path = "default_code_paths.rs"] +pub(super) mod default_code_paths; #[path = "image_fill_audit.rs"] mod image_fill_audit; #[path = "../page_scaffold.rs"] diff --git a/crates/devup-mcp/src/server/projection/summarize_projection_diagnostics.rs b/crates/devup-mcp/src/server/projection/summarize_projection_diagnostics.rs index 40719124..9bba18c2 100644 --- a/crates/devup-mcp/src/server/projection/summarize_projection_diagnostics.rs +++ b/crates/devup-mcp/src/server/projection/summarize_projection_diagnostics.rs @@ -52,6 +52,8 @@ pub(super) fn summarize_projection_diagnostics( component_name_for_components, target_kind, } = input; + projection_diagnostics.extend(payload.snapshot.diagnostics.iter() + .filter(|issue| issue.code == "DEVUP_BRIDGE_PLUGIN_STALE").cloned()); for failure in failures.iter().filter(|f| f.get("output").is_some()) { let issue = failure_issue(failure); projection_diagnostics.push(issue.clone()); @@ -84,7 +86,13 @@ pub(super) fn summarize_projection_diagnostics( .iter() .filter( |d| d.fidelity_impact() != devup_mcp_figma::FidelityImpact::None - || d.code == "DEVUP_CODEGEN_PROPERTY_UNMAPPED" + || matches!( + d.code.as_str(), + "DEVUP_CODEGEN_PROPERTY_UNMAPPED" + | "DEVUP_CODEGEN_INSTANCE_OVERRIDE" + | "DEVUP_CODEGEN_SUSPICIOUS_VALUE" + | "DEVUP_BRIDGE_PLUGIN_STALE" + ) ) .collect::>() ), diff --git a/crates/devup-mcp/src/server/projection/theme_export.rs b/crates/devup-mcp/src/server/projection/theme_export.rs index 983f72fb..e33f3783 100644 --- a/crates/devup-mcp/src/server/projection/theme_export.rs +++ b/crates/devup-mcp/src/server/projection/theme_export.rs @@ -75,7 +75,10 @@ fn all_tokens(json: &str) -> TokenUsage { let Ok(source) = serde_json::from_str::(json) else { return usage; }; - if let Some(typography) = source.pointer("/theme/typography").and_then(Value::as_object) { + if let Some(typography) = source + .pointer("/theme/typography") + .and_then(Value::as_object) + { usage.typography.extend(typography.keys().cloned()); } for category in ["colors", "length", "shadow"] { @@ -193,6 +196,10 @@ pub fn prepare( }), ); result.insert("skippedStyles".into(), json!(output.skipped_styles)); + result.insert( + "themeModesDropped".into(), + json!(output.theme_modes_dropped), + ); if let Some(root) = options.project_root.as_deref() { let lookup = super::super::project_context::theme_for_validation(Some(root))?; if let Some(existing) = lookup.theme { diff --git a/crates/devup-mcp/src/server/projection/w1_regressions.rs b/crates/devup-mcp/src/server/projection/w1_regressions.rs index bfb347e8..c2ab553b 100644 --- a/crates/devup-mcp/src/server/projection/w1_regressions.rs +++ b/crates/devup-mcp/src/server/projection/w1_regressions.rs @@ -449,10 +449,9 @@ async fn r13_frame_output_paths_report_unsupported_and_write_supported_keys() { .unwrap() .contains(&json!("frame:1:1:tsx")) ); - assert_eq!( - std::fs::read_to_string(&target).unwrap(), - result["frames"][0]["tsx"].as_str().unwrap() - ); + assert!(!std::fs::read_to_string(&target).unwrap().is_empty()); + assert!(result["frames"][0].get("tsx").is_none()); + assert_eq!(result["frames"][0]["outputResults"]["tsx"]["bodyDelivery"], "written-to-file"); assert_eq!( result["outputPaths"]["frame:1:1:tsx"], expected.to_string_lossy().as_ref() @@ -768,7 +767,7 @@ async fn r2_production_six_frames_return_valid_story_tsx() { "story pre-guard excluded path {path}; referenced={}", generated.tsx.contains(&path) ); - let mut op = operation(&["tsx", "assetManifest"]); + let mut op = operation(&["tsx"]); if let PendingOperation::Export { frame_ids, include_diagnostics, diff --git a/crates/devup-mcp/src/server/projection/w1_regressions_original_source_variant_and_css_map_resources_recover_w.rs b/crates/devup-mcp/src/server/projection/w1_regressions_original_source_variant_and_css_map_resources_recover_w.rs index 30919f46..7a0e3a0b 100644 --- a/crates/devup-mcp/src/server/projection/w1_regressions_original_source_variant_and_css_map_resources_recover_w.rs +++ b/crates/devup-mcp/src/server/projection/w1_regressions_original_source_variant_and_css_map_resources_recover_w.rs @@ -635,7 +635,7 @@ async fn p3_public_mapping_and_dedup_rewrite_inline_and_written_tsx() { output_paths.insert("tsx".into(), tsx_path.to_string_lossy().into()); } let result = project(p3_asset_payload(b"first image"), op).await.unwrap(); - let code = result["tsx"].as_str().unwrap(); + let code = std::fs::read_to_string(&tsx_path).unwrap(); assert!(code.contains("/icons/BI%20icon.png"), "{code}"); assert_eq!(std::fs::read_to_string(&tsx_path).unwrap(), code); assert_eq!(std::fs::read(&first_path).unwrap(), b"first image"); diff --git a/crates/devup-mcp/src/server/projection/w1_regressions_r4_export_exposes_placement_contract_without_diagnostic.rs b/crates/devup-mcp/src/server/projection/w1_regressions_r4_export_exposes_placement_contract_without_diagnostic.rs index d18e745c..12012bf4 100644 --- a/crates/devup-mcp/src/server/projection/w1_regressions_r4_export_exposes_placement_contract_without_diagnostic.rs +++ b/crates/devup-mcp/src/server/projection/w1_regressions_r4_export_exposes_placement_contract_without_diagnostic.rs @@ -409,8 +409,11 @@ async fn r1_approximation_is_explained_without_diagnostics() { } let result = project(data, op).await.unwrap(); let frame = &result["frames"][0]; - assert_eq!(frame["quality"]["projection"], "approximated"); - let issue = &frame["projectionIssues"][0]; + assert_eq!(frame["quality"]["projection"], "lossy"); + assert!(!frame["tsx"].as_str().unwrap().contains("pos=\"absolute\"")); + let issue = frame["projectionIssues"].as_array().unwrap().iter() + .find(|issue| issue["code"] == "DEVUP_CODEGEN_ABSOLUTE_FALLBACK").unwrap(); + assert_eq!(issue["fidelityImpact"], "approximated"); assert_eq!(issue["nodeId"], "1:1"); assert_eq!(issue["property"], "layoutPositioning"); assert_eq!( diff --git a/crates/devup-mcp/src/server/review.rs b/crates/devup-mcp/src/server/review.rs index b1dad115..2d4e54c5 100644 --- a/crates/devup-mcp/src/server/review.rs +++ b/crates/devup-mcp/src/server/review.rs @@ -37,15 +37,21 @@ fn items_from_issues(scope: &str, issues: &[Value], out: &mut Vec) { }) .collect(); let first = members[0]; - let how = first["details"]["nextAction"] + let how = first["how"].as_str().map(str::to_owned).or_else(|| first["details"]["nextAction"] .as_str() .map(str::to_owned) - .or_else(|| first["details"]["nextAction"]["how"].as_str().map(str::to_owned)) - .unwrap_or_else(|| { - "Compare the generated property with the raw Figma value for these nodes (export rawSnapshot with debug: true) before shipping.".into() - }); + .or_else(|| first["details"]["nextAction"]["how"].as_str().map(str::to_owned))) + .unwrap_or_else(|| match code.as_str() { + "DEVUP_CODEGEN_INTERACTION_UNMAPPED" => "Wire href/onClick using linkSuggestions and routeSuggestions; test the destination/action.", + "DEVUP_CODEGEN_TEXT_WHITESPACE_COLLAPSE" => "Use whiteSpace=\"pre-wrap\" or explicit spacing to preserve the intended text.", + "DEVUP_CODEGEN_LAYOUT_UNCOVERED" => "Check layoutSizingHorizontal/layoutSizingVertical against emitted w/h.", + "DEVUP_CODEGEN_INTERACTION_UNVERIFIED" => "Read interactionReadErrors before wiring or claiming interaction coverage.", + "DEVUP_CODEGEN_PROPERTY_UNMAPPED" => "Read projectionEvidence for the unmapped property and generated output.", + "DEVUP_CODEGEN_INSTANCE_OVERRIDE" => "Carry every instanceOverrides entry into a shared-component prop or variant, or explicitly report it as unresolved before replacing the inline instance.", + _ => "Compare the generated property with the raw Figma value for these nodes (export rawSnapshot with debug: true) before shipping.", + }.into()); out.push(json!({ - "severity": if lossy || code.starts_with("DEVUP_CODEGEN_INTERACTION_") { "required" } else { "recommended" }, + "severity": if lossy || code.starts_with("DEVUP_CODEGEN_INTERACTION_") || code == "DEVUP_CODEGEN_INSTANCE_OVERRIDE" { "required" } else { "recommended" }, "scope": scope, "code": code, "count": members.len(), @@ -200,6 +206,24 @@ pub(super) fn attach(result: &mut Value) { mod tests { use super::*; + #[test] + fn selected_issue_preview_keeps_specific_guidance() { + let mut result = json!({"tsx":"code","projectionIssues":[{"code":"DEVUP_CODEGEN_LAYOUT_UNCOVERED","details":{"nextAction":{"how":"Check layoutSizingHorizontal against emitted w."}}}]}); + super::super::compact::select(result.as_object_mut().unwrap(), &json!({}), &super::super::compact::ResponseSelection {response_fields:vec!["tsx".into()], ..Default::default()}).unwrap(); + attach(&mut result); + let item = result["reviewChecklist"].as_array().unwrap().iter().find(|item| item["code"] == "DEVUP_CODEGEN_LAYOUT_UNCOVERED").unwrap(); + assert_eq!(item["how"], "Check layoutSizingHorizontal against emitted w."); + } + + #[test] + fn issue_defaults_name_the_evidence_to_use() { + for (code, guidance) in [("INTERACTION_UNMAPPED", "linkSuggestions"), ("TEXT_WHITESPACE_COLLAPSE", "pre-wrap"), ("LAYOUT_UNCOVERED", "layoutSizing"), ("INTERACTION_UNVERIFIED", "interactionReadErrors"), ("PROPERTY_UNMAPPED", "projectionEvidence")] { + let mut result = json!({"projectionIssues":[{"code":format!("DEVUP_CODEGEN_{code}")}]}); + attach(&mut result); + assert!(result["reviewChecklist"][0]["how"].as_str().unwrap().contains(guidance), "{result}"); + } + } + #[test] fn clean_response_has_summary_and_no_checklist() { let mut value = diff --git a/crates/devup-mcp/src/server/text/devup-figma-export.md b/crates/devup-mcp/src/server/text/devup-figma-export.md index 930d32bf..dfee10df 100644 --- a/crates/devup-mcp/src/server/text/devup-figma-export.md +++ b/crates/devup-mcp/src/server/text/devup-figma-export.md @@ -1,4 +1,9 @@ Export a small Figma selection. outputs:[outline] returns a bounded captured tree with logical layout metrics and responseOffset/responseLimit pagination. responseFields or path use a validated field/index/exact-name-or-id grammar (e.g. nodes[name="Cards"].layout), retaining node identity and linking full omitted evidence as resources. For only explicit node dimensions, prefer devup_figma_measure, which bypasses snapshot collection and code generation; the Figma-to-code entry point. url is optional while exactly one Devup Bridge plugin is attached (devup_figma_auth status shows what is attached): omitted, or given as figma-bridge://current, the export reads the file that plugin has open and the node selected in Figma; frameIds without url name frames in it - one id is that frame, several are screens of the selected Section. Never invent a Figma file key to fill url. Asset requests: recommend 1–3 per call, maximum 6; split larger assetRequests before calling. All fresh exports return exportJob (assetJob compatibility alias) within a one-second initial wait when collection is still running, with per-call frame/root IDs, pagination and elapsed time. Slow asset calls also retain per-asset progress. Poll with jobId; use jobAction=resume when paused. Polls return compact asset rows (status, bytes, sha256, fileState); responseFields on a poll selects from its reply, e.g. exportJob.assets for full rows; assetJob only references exportJob. Generated code uses maskImage only for single-colour icons; multi-colour icons stay . Never convert an icon to a mask or recolour it by hand. Jobs retain accepted reads/bytes across client timeouts for 30 minutes in this server process, not across restart. Identical arguments recover a lost job reply. Completed results are retained for 5 minutes. Recommend 1–3 frames per call; allow at most 6 frames and 12 frame-times-output units. Budget roughly 5–20 seconds per frame-output unit (15–60 seconds per frame for three outputs) as a planning heuristic, not a guarantee: paging, complexity and throttling can exceed it and clients commonly time out at 300 seconds. Oversized selections are refused before screen collection; split frameIds into one-frame calls when isolating latency, or poll jobId. Partial per-frame projection failures retain successful frame outputs. projectionIssues always explains reported approximations, unclassified layout loss and missing generated-property provenance, even without includeDiagnostics. mappingComplete=false identifies mapping gaps; mapping-incomplete is not value loss and cannot be exact. projectionEvidence includes source fields and calculations for generated attributes. outputPathResults lists supported keys and diagnostics; frame file keys are frame::, while outputPaths reports actual committed writes. Resource responses offer nextAction.tool/arguments for same-artifact body comparison and sizeEstimate with explicit unmeasured wire overhead; coupled asset/path arguments remain together. quality.assets grades binary collection; assetSummary.description explains collection state. Merge saved batch responses offline with devup-mcp --merge-asset-batches batch1.json batch2.json for cumulative collection, unrequested, failed and conflict counts. SECTION links use two stages: receive selection_required, then run nextAction.example to export a selected screen. Asked to build pages or a site from Figma, follow the devup-implementation-plan workflow (/devup) even if not asked: it plans the URLs, updates dependencies to the latest, and adds interaction states, transitions and metadata; App Router apps may run on vinext with no next dependency. Building pages: with projectRoot, a page write (pageScaffold write, outputPaths under app/, or several frames with outputPaths) needs a confirmed devup_screen_plan and otherwise answers plan_required; pageScaffold then takes its route from the plan. Ask the plan's questions up front only; after confirm never ask the user, record assumptions instead. Excluded and design-fix frames are refused (skill devup-implementation-plan). The `tsx` this returns is devup-ui code, not plain React: its components are compile-time placeholders, `$token` names an entry in the project's devup.json, and a style prop takes a responsive array. Call devup_skills before you write or edit it - it reports which of those conventions this workspace is missing and installs them where your runtime loads skills from. An agent that skips this does not know it is guessing, and this server cannot see the guesses; the gap is repeated as `skillGap` on the response that carries the code. This output is the design, already read. Do not rewrite it from a screenshot or from `get_design_context` - pictures and visual reasoning verify, they do not author. Do not read the node tree and write devup-ui by hand, and do not infer layout from coordinates. Never guess a colour, spacing, radius or typography value: if a value did not come back, say so and stop, because an invented one is indistinguishable from a real one in the code and is the single failure this server exists to prevent. A failed call is a fact to report, not something to route around. Ask only for what you will read: `tsx` is the deliverable, and the response always carries `status`, `quality`, `cache.artifactId`, `collection` and `source` beside it. `outputs` defaults to `["tsx"]`. Add `devupJson` only when the project has no `devup.json` yet, or when you are introducing tokens it does not define - if it already has one, that file is what the code must match, and `devup_project_context` is what reads it. If you already know the node ids you want - a brief named them, or an earlier call did - pass them straight to `frameIds` and skip `devup_figma_explore`; exploring to rediscover ids you are already holding spends a Figma call for nothing. Explore is for when a Section link is all you have. Each output adds its own keys and nothing else - tsx adds `tsx`; componentTsx adds `componentTsx`; responsiveTsx adds `responsiveTsx`, `responsiveSlots` and, where a width asked for something one tree cannot say, `responsiveUnrepresented`; devupJson adds `devupJson`, `themeCounts`, `themeCompleteness`, `conflicts` and `unresolvedVariables`; sourceMap and assetManifest each add the key they name; referencePng adds `referencePng` and, when the Devup Bridge plugin's picture may hold layers get_screenshot leaves out, `referencePngNote` naming them. `rawSnapshot` and `rawPayload` are the collected design in raw form and need `debug: true`. Use them for one question only - a screen looks wrong and you must decide whether the generator is at fault or the design says so - never to implement, since the tsx already carries what they carry. `fidelity` and `completenessReport` appear only when the result is not exact or complete, or when includeDiagnostics is set. tsx expands every instance into primitives while componentTsx keeps them as references, so requesting both gives the same screen twice and the difference between them is each component's body. responsiveTsx merges every width the capture carries into one module whose differing values are devup-ui responsive arrays, and is produced whenever there is more than one width. `assetManifest` lists the assets and their ids and writes nothing. To get files, call again with `assetRequests`, giving each entry an `outputPath` under an allowed write root - and make that call with the original `url` rather than `artifactId`, because an acquisition made without asset capture cannot serve asset requests. By default manifest `path` and TSX references remain placeholders. Opt in with `assetPublicRoot`, an existing absolute local directory served at URL /, together with assetRequests.outputPath in this call (saved output mappings are not part of artifacts): files below that root map to percent-encoded relative URLs in manifest and all TSX outputs. Identical exported bytes share one canonical path/outputPath; inspect the returned paths. assetNamesPerNode remains true by default; matching names alone never trigger deduplication. Reuse a previous acquisition with `artifactId` from `cache` to project further outputs without calling Figma again. Omitted frameIds/allScreens reuse the stored Section selection; explicit selection overrides it. Debug outputs can use resource delivery in auto mode: read the linked resources or follow nextAction to reproject inline. sourceMap is a node/field/generatedProperty/resolution map without offsets; source.generatedOutput identifies the annotated output. Even when reusing artifactId, sourceMap requires tsx, componentTsx or devupJson in outputs in the same call. Example: {"artifactId":"","outputs":["tsx","rawSnapshot","sourceMap"],"debug":true} An asset request targeting a frame by nodeId defaults to the original IMAGE fill when one is available. This keeps hero headings and buttons live instead of baking them into the background image. Explicit renderMode node retains whole-node capture; its renderInfo warns when child text would be baked into the asset. + +interactiveCandidates may include role="marquee" with states.confidence="heuristic" for single-line text or repeated text rows exceeding a clipsContent ancestor, or long repeated-separator strips. Figma does not prove animation intent. Review the supplied recipe: duplicate content in a nowrap track, keyframes translateX(0) to translateX(-50%) with infinite linear animation, _motionReduce animation none, and aria-hidden on the duplicate. Export remains static until this behavior is implemented and tested. +For a confirmed multi-width URL, responsiveTsx is the deliverable. Never start from single-width tsx/componentTsx for its page or sections. Export all sibling frameIds in width order using the exact nextAction returned by responsiveRequired; single-width page writes are refused before collection. Only wire links, events and data; never change generated layout values, collapse responsive arrays, add fixed heights or invent layout wrappers. Screen-plan status reports recorded export mode and confidence-labelled generatedDrift against mapped URL sources, independently of optional Figma identity attributes. Exports report captured sections/assets with no generated element as DEVUP_CODEGEN_SECTION_COVERAGE projection issues, including without includeDiagnostics. With projectRoot and Figma node identities enabled, each TSX outputResults entry also returns sectionCoverage: pass this original contract to devup_ui_validate to detect deleted sections and asset references. Hidden subtrees, flattened asset internals and component-reference internals are excluded. sectionCoverage is null when identity attributes are disabled or no projectRoot is supplied; static coverage never proves runtime visibility. responseLimit and responseOffset page the already-captured result (outline nodes, selected paths); they do not limit how much Figma is read. Collection cost follows the size of the selected node, so for a very large node pick a smaller inner frame (devup_figma_explore) or read only dimensions with devup_figma_measure instead of raising or lowering responseLimit. To poll a job cheaply pass responseFields such as ["exportJob.state","exportJob.elapsedMs","exportJob.completedCalls","exportJob.nextAction"]. +With projectRoot, generated outputs default to guarded writes under .devup/exports/: tsx uses .tsx, componentTsx uses .component.tsx, responsiveTsx uses .responsive.tsx, and sourceMap uses .sourcemap.json, unless outputPaths already names that output. Section frames use frame:: keys. defaultOutputPaths lists applied defaults; disallowed defaults warn DEVUP_DEFAULT_OUTPUT_PATH_SKIPPED and retain inline delivery. Written code is omitted from the response body; outputResults reports bodyDelivery:"written-to-file" and path. Job polls retain the original destinations. Without projectRoot no default write is made. Export projectContext summarizes counts and relevant routes, components and referenced assets only; use its nextAction with devup_project_context scope ui/routes for the full inventory. responseFields keeps late project metadata in selected values only, alongside the bounded delivery/review envelope. +Screen-plan component substitutions: when projectRoot has a plan, `componentWidthMismatches` and `componentWidthAssumptions` expose automatic `DEVUP_DESIGN_COMPONENT_WIDTH_MISMATCH` corrections. Use the expected master/correct sibling for the affected width in one responsive shared component, never a second variant. Native-node PNG evidence compares the suspect and correct sibling; unavailable renders are explicit and the strict width majority decides. Audit references use the substituted subtree and retain the suspect's y placement; the original whole-page render cannot verify the corrected page. diff --git a/crates/devup-mcp/src/server/tools.rs b/crates/devup-mcp/src/server/tools.rs index 54a8450a..318e7ff4 100644 --- a/crates/devup-mcp/src/server/tools.rs +++ b/crates/devup-mcp/src/server/tools.rs @@ -357,6 +357,14 @@ pub struct ProjectContextInput { #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase")] pub struct UiValidateInput { + /// Summary (default) returns compact, deduplicated findings; full preserves all diagnostics. + #[serde(default)] + pub detail: UiValidateDetail, + /// Files or recursive directories inside projectRoot. Exclusive with tsx/files; + /// at most 512 source files and 8 MiB read. Uses the project's disk theme. + #[serde(default)] + #[schemars(length(max = 512))] + pub paths: Vec, /// Copy outputResults.tsx.sectionCoverage (or a Section frame's equivalent) /// from devup_figma_export to check that captured sections/assets remain. /// This is static JSX identity coverage, not proof of runtime visibility. @@ -416,6 +424,14 @@ pub struct UiValidateInput { pub reference_width: Option, } +#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub enum UiValidateDetail { + #[default] + Summary, + Full, +} + #[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase")] pub struct SectionCoverageInput { diff --git a/crates/devup-mcp/tests/cli.rs b/crates/devup-mcp/tests/cli.rs index fefa5c7d..f96dc68c 100644 --- a/crates/devup-mcp/tests/cli.rs +++ b/crates/devup-mcp/tests/cli.rs @@ -20,6 +20,19 @@ fn build_identity_composes_clean_dirty_and_safe_fallbacks() { None ); assert_eq!(build_identity::git_identity(None, false), None); + assert_eq!( + build_identity::dirty_identity(Some("0123456789ab"), Some("1a2b3c4d")).as_deref(), + Some("0123456789ab-dirty.1a2b3c4d") + ); + assert_ne!( + build_identity::dirty_identity(Some("0123456789ab"), Some("1a2b3c4d")), + build_identity::dirty_identity(Some("0123456789ab"), Some("5e6f7a8b")), + "two different uncommitted edits of one commit get different ids" + ); + assert_eq!( + build_identity::dirty_identity(Some("0123456789ab"), Some("bad hash")).as_deref(), + Some("0123456789ab-dirty") + ); } #[test] @@ -74,11 +87,11 @@ fn version_build_id_reports_the_repository_dirty_state() { env!("DEVUP_MCP_GIT_DIRTY"), ) { ("git", "true") => assert!( - build_id.ends_with("-dirty"), + build_id.contains("-dirty"), "the build script saw a dirty tree, so --version must say so: {build_id}" ), ("git", "false") => assert!( - !build_id.ends_with("-dirty"), + !build_id.contains("-dirty"), "the build script saw a clean tree, so --version must not claim dirty: {build_id}" ), // An injected DEVUP_MCP_BUILD_ID carries whatever suffix its caller diff --git a/crates/devup-mcp/tests/export_defaults.rs b/crates/devup-mcp/tests/export_defaults.rs index 91f7dea7..0497b26f 100644 --- a/crates/devup-mcp/tests/export_defaults.rs +++ b/crates/devup-mcp/tests/export_defaults.rs @@ -79,7 +79,10 @@ impl FigmaUpstream for Fixture { } async fn export(arguments: Value) -> anyhow::Result { - let server = DevupServer::new(Services::new(Arc::new(Auth), Arc::new(Fixture::default()))); + let server = DevupServer::with_output_roots( + Services::new(Arc::new(Auth), Arc::new(Fixture::default())), + vec![std::env::temp_dir()], + )?; let (server_transport, client_transport) = tokio::io::duplex(256 * 1024); let task = tokio::spawn(async move { server.serve(server_transport).await?.waiting().await?; @@ -103,6 +106,25 @@ async fn export(arguments: Value) -> anyhow::Result { Ok(result.structured_content.unwrap()) } +#[tokio::test] +async fn project_root_writes_default_code_without_inlining() -> anyhow::Result<()> { + let root = std::env::temp_dir().join(format!("devup-default-export-{}", std::process::id())); + std::fs::create_dir_all(&root)?; + let output = export(json!({"url":"https://www.figma.com/design/FileKey123/Fixture?node-id=1-2","projectRoot":root})).await?; + let path = output["defaultOutputPaths"]["tsx"] + .as_str() + .expect("default code path"); + assert!(path.ends_with("1-2.tsx"), "{output}"); + assert!(!std::fs::read_to_string(path)?.is_empty()); + assert!(output.get("tsx").is_none(), "{output}"); + assert_eq!( + output["outputResults"]["tsx"]["bodyDelivery"], + "written-to-file" + ); + std::fs::remove_dir_all(root)?; + Ok(()) +} + async fn export_tool_schema() -> anyhow::Result<(Value, String)> { let server = DevupServer::new(Services::new(Arc::new(Auth), Arc::new(Fixture::default()))); let (server_transport, client_transport) = tokio::io::duplex(256 * 1024); diff --git a/crates/devup-mcp/tests/export_project_theme.rs b/crates/devup-mcp/tests/export_project_theme.rs index 732bc12f..c04f2b05 100644 --- a/crates/devup-mcp/tests/export_project_theme.rs +++ b/crates/devup-mcp/tests/export_project_theme.rs @@ -55,7 +55,8 @@ impl FigmaUpstream for PrimaryFillUpstream { "id": "3879:35481", "type": "FRAME", "fields": { "name": "Proofread", "childrenIds": [], "layoutMode": "VERTICAL", - "width": 320, "height": 240, + "width": 320, "height": 240, + "layoutSizingHorizontal":"FIXED", "layoutSizingVertical":"FIXED", "fills": [{ "type": "SOLID", "color": {"r": 0, "g": 0.4, "b": 1, "a": 1}, "boundVariables": {"color": {"type": "VARIABLE_ALIAS", "id": "v"}} @@ -147,6 +148,73 @@ fn project_naming_it_brand() -> anyhow::Result { const URL: &str = "https://www.figma.com/design/85CgSws3o5XsLv7aAwWJyS/Fixture?node-id=3879-35481"; +#[tokio::test] +async fn export_context_summarizes_a_large_project_under_fifteen_kib() -> anyhow::Result<()> { + let root = std::env::temp_dir().join(format!("devup-context-size-{}", std::process::id())); + fs::create_dir_all(root.join("src/app"))?; + fs::create_dir_all(root.join("src/components"))?; + fs::create_dir_all(root.join("public/images"))?; + fs::write(root.join("package.json"), r#"{"name":"fixture"}"#)?; + fs::write( + root.join("src/app/page.tsx"), + "export default function Home(){return
}", + )?; + for index in 0..180 { + fs::write( + root.join(format!("src/components/Component{index}.tsx")), + format!("export function Component{index}(){{return
}}"), + )?; + fs::write( + root.join(format!("public/images/unreferenced-image-{index}.png")), + "image", + )?; + } + let result = export(json!({"url":URL,"projectRoot":root})).await?; + let bytes = serde_json::to_vec(&result)?.len(); + eprintln!("projectRoot export structured reply: {bytes} bytes"); + assert!(bytes < 15 * 1024, "reply is {bytes} bytes"); + assert_eq!(result["projectContext"]["counts"]["publicFiles"], 180); + assert_eq!(result["projectContext"]["ui"]["components"], json!([])); + assert_eq!( + result["projectContext"]["ui"]["nonPageDestinations"], + json!([]) + ); + assert_eq!( + result["projectContext"]["nextAction"]["tool"], + "devup_project_context" + ); + fs::remove_dir_all(root)?; + Ok(()) +} + +#[tokio::test] +async fn response_fields_do_not_leak_late_project_metadata() -> anyhow::Result<()> { + let root = project_naming_it_brand()?; + let result = export(json!({"url":URL,"projectRoot":root,"responseFields":["quality"]})).await?; + for field in [ + "projectContext", + "routeSuggestions", + "projectThemeValidation", + "planProgress", + "skillGap", + "formatting", + ] { + assert!( + result.get(field).is_none(), + "late metadata {field} leaked: {result}" + ); + } + let selected = + export(json!({"url":URL,"projectRoot":root,"responseFields":["projectThemeValidation"]})) + .await?; + assert_eq!( + selected["selected"]["selections"][0]["values"][0]["state"], + "checked" + ); + assert!(selected.get("projectThemeValidation").is_none()); + Ok(()) +} + #[tokio::test] async fn an_export_without_a_project_root_reads_nothing_from_disk() -> anyhow::Result<()> { let result = export(json!({"url": URL, "outputs": ["tsx"]})).await?; @@ -235,7 +303,7 @@ async fn an_export_into_a_project_marks_its_elements_with_the_figma_node() -> an export(json!({"url": URL, "outputs": ["tsx"], "projectRoot": root.to_string_lossy(), "figmaNodeIds": true})) .await?; assert!( - result["tsx"].as_str().unwrap_or_default().contains(marked), + fs::read_to_string(result["outputPaths"]["tsx"].as_str().unwrap())?.contains(marked), "{}", result["tsx"] ); diff --git a/crates/devup-mcp/tests/ground_truth_tools.rs b/crates/devup-mcp/tests/ground_truth_tools.rs index 389ddc5e..7650d810 100644 --- a/crates/devup-mcp/tests/ground_truth_tools.rs +++ b/crates/devup-mcp/tests/ground_truth_tools.rs @@ -64,7 +64,12 @@ async fn call_tool(tool: &str, arguments: Value) -> anyhow::Result = arguments.as_object().cloned().unwrap_or_default(); + let mut arguments: Map = arguments.as_object().cloned().unwrap_or_default(); + if tool == "devup_ui_validate" { + arguments + .entry("detail".to_owned()) + .or_insert(json!("full")); + } let result = client .call_tool(CallToolRequestParams::new(tool.to_owned()).with_arguments(arguments)) .await?; @@ -84,6 +89,53 @@ fn fixture_project_root() -> String { ) } +#[tokio::test] +async fn link_underline_paths_checks_project_src_reset_outside_selected_file() -> anyhow::Result<()> +{ + let root = std::env::temp_dir().join(format!( + "link-underline-{}-{}", + std::process::id(), + rand::random::() + )); + std::fs::create_dir_all(root.join("src/components"))?; + std::fs::write(root.join("package.json"), "{}")?; + std::fs::write( + root.join("src/components/View.tsx"), + "export const View = () => Go;", + )?; + let arguments = + json!({"projectRoot":root.to_string_lossy(),"paths":["src/components/View.tsx"]}); + let bare = call_tool("devup_ui_validate", arguments.clone()) + .await? + .structured_content + .unwrap(); + assert!( + bare["violations"] + .as_array() + .unwrap() + .iter() + .any(|finding| finding["rule"] == "link-underline") + ); + std::fs::write( + root.join("src/Reset.ts"), + "globalCss({ a: { textDecoration: 'none', color: 'inherit' } });", + )?; + let reset = call_tool("devup_ui_validate", arguments) + .await? + .structured_content + .unwrap(); + assert!( + !reset["violations"] + .as_array() + .unwrap() + .iter() + .any(|finding| finding["rule"] == "link-underline") + ); + assert_eq!(reset["pathsRead"], 1); + std::fs::remove_dir_all(root)?; + Ok(()) +} + #[tokio::test] async fn feedback_navigation_rules_and_routes_scope_reach_mcp_clients() -> anyhow::Result<()> { let root = std::env::temp_dir().join(format!( diff --git a/crates/devup-mcp/tests/source_orchestration.rs b/crates/devup-mcp/tests/source_orchestration.rs index f4c40743..b02c057a 100644 --- a/crates/devup-mcp/tests/source_orchestration.rs +++ b/crates/devup-mcp/tests/source_orchestration.rs @@ -274,7 +274,12 @@ async fn ui_output_path_writes_the_generated_artifact_only_when_requested() -> a .await?; let output = result.structured_content.unwrap(); let written = std::fs::read_to_string(&path)?; - assert_eq!(written, output["tsx"].as_str().unwrap()); + assert!(!written.is_empty()); + assert!(output.get("tsx").is_none()); + assert_eq!( + output["outputResults"]["tsx"]["bodyDelivery"], + "written-to-file" + ); assert!(output["outputPaths"]["tsx"].as_str().is_some()); std::fs::remove_file(path)?; std::fs::remove_dir(root)?; @@ -288,6 +293,47 @@ fn input() -> Value { }) } +#[tokio::test] +async fn confirmed_multi_width_url_refuses_single_width_page_write_before_upstream() +-> anyhow::Result<()> { + let root = + std::env::temp_dir().join(format!("devup-responsive-gate-{}", rand::random::())); + std::fs::create_dir_all(root.join(".devup"))?; + std::fs::write(root.join(".devup/screen-plan.json"), json!({ + "kind": "devup-screen-plan", "schemaVersion": 1, "status": "confirmed", + "source": {"url": "https://www.figma.com/design/FileKey123/Fixture?node-id=1-0"}, + "routes": [{"url": "/", "implementation": "page", "nodeIds": ["1:2", "1:3"], "drawnWidths": [1440, 360]}] + }).to_string())?; + let upstream = Arc::new(UpstreamProbe::unavailable()); + let result = call_named_tool_with_roots( + Arc::new(AuthProbe { + status: AuthStatus::Disconnected, + logins: AtomicUsize::new(0), + }), + upstream.clone(), + "devup_figma_export", + json!({"projectRoot": root, "frameIds": ["1:2"], "outputs": ["tsx"], + "outputPaths": {"tsx": root.join("src/app/page.tsx")}}), + vec![root.clone()], + ) + .await?; + let output = result.structured_content.unwrap(); + assert_eq!(output["status"], "responsive_required"); + assert_eq!(output["written"], false); + assert_eq!( + output["nextAction"]["arguments"]["frameIds"], + json!(["1:3", "1:2"]) + ); + assert_eq!( + output["nextAction"]["arguments"]["outputs"], + json!(["responsiveTsx"]) + ); + assert_eq!(upstream.calls.load(Ordering::SeqCst), 0); + assert!(!root.join("src/app/page.tsx").exists()); + std::fs::remove_dir_all(root)?; + Ok(()) +} + fn metadata_result() -> Value { json!({ "structuredContent": { @@ -347,14 +393,9 @@ async fn connected_auto_completes_through_the_direct_collector() -> anyhow::Resu let result = call_tool(auth.clone(), upstream.clone(), input()).await?; let output = result.structured_content.unwrap(); - assert_eq!(output["status"], "partial"); - assert!( - output["projectionIssues"] - .as_array() - .unwrap() - .iter() - .any(|issue| issue["code"] == "DEVUP_CODEGEN_LAYOUT_UNCOVERED") - ); + assert_eq!(output["status"], "complete"); + assert_eq!(output["quality"]["projection"], "exact"); + assert!(output["projectionIssues"].as_array().unwrap().is_empty()); assert_eq!(output["source"]["kind"], "direct"); assert!(output["tsx"].as_str().unwrap().contains("SyntheticFrame")); assert_eq!(output["collection"]["figmaToolCalls"], 3); @@ -430,14 +471,9 @@ async fn direct_fast_call_error_restarts_the_legacy_collector() -> anyhow::Resul .await?; let output = result.structured_content.unwrap(); - assert_eq!(output["status"], "partial"); - assert!( - output["projectionIssues"] - .as_array() - .unwrap() - .iter() - .any(|issue| issue["code"] == "DEVUP_CODEGEN_LAYOUT_UNCOVERED") - ); + assert_eq!(output["status"], "complete"); + assert_eq!(output["quality"]["projection"], "exact"); + assert!(output["projectionIssues"].as_array().unwrap().is_empty()); assert_eq!(output["collection"]["figmaToolCalls"], 3); assert_eq!(output["collection"]["fallbackUsed"], true); assert_eq!( diff --git a/crates/devup-mcp/tests/width_policy_export.rs b/crates/devup-mcp/tests/width_policy_export.rs index 44498de6..01614797 100644 --- a/crates/devup-mcp/tests/width_policy_export.rs +++ b/crates/devup-mcp/tests/width_policy_export.rs @@ -138,7 +138,7 @@ async fn violations_link_to_the_sourcemap_the_export_returns() -> anyhow::Result .await?; let validated = call( "devup_ui_validate", - json!({"tsx": exported["tsx"], "sourceMap": exported["sourceMap"]}), + json!({"detail": "full", "tsx": exported["tsx"], "sourceMap": exported["sourceMap"]}), ) .await?; let link = &validated["sourceMapLink"]; diff --git a/fixtures/devup-figma-plugin/README.md b/fixtures/devup-figma-plugin/README.md index b29d6414..bb9d805f 100644 --- a/fixtures/devup-figma-plugin/README.md +++ b/fixtures/devup-figma-plugin/README.md @@ -21,6 +21,17 @@ cargo test -p devup-mcp-devup-ui --test compat_fixtures `manifest.json`은 원본 54개 test file, 978 pass/0 fail, 268 snapshot이라는 upstream 기준 실행 정보와 536개 case/snapshot 파일의 LF-normalized SHA-256을 보존합니다. `ledger.json`은 이 upstream 결과의 inventory/coverage 분류이며 그 자체가 Rust 실행 결과를 뜻하지 않습니다. +## Codegen Defect Corrections + +The reviewed Rust goldens intentionally differ from the pinned plugin output: +multi-child auto-layout containers retain their individual assets; captured roots +do not inherit absolute placement without a collected parent; root FILL sizing +is axis-specific rather than implicitly filling both axes; and typography keeps +real letter-spacing overrides while removing redundant resolved metrics. +The 50 affected snapshot checksums are synchronized in `manifest.json`; original +case inputs and upstream inventory remain unchanged. These are static codegen +regressions, not new browser pixel measurements. + ## R4 배치 계약 변경 `upstream-codegen-191`, `251`–`254`의 골든은 absolute 자식의 기준 요소를 생성 코드가 소유하도록 갱신했습니다. PAGE/SECTION 루트에 relative와 FIXED 축의 크기를 보존하고, 앞에 그린 absolute 배경에는 루트 stacking context 안의 z-index를 적용합니다. 원본 case JSON은 유지했고 변경한 5개 snapshot의 LF-normalized checksum만 동기화했습니다. 이는 생성 코드 배치 계약의 의도적인 변경이며 신규 브라우저 픽셀 측정 결과를 뜻하지 않습니다. diff --git a/fixtures/devup-figma-plugin/manifest.json b/fixtures/devup-figma-plugin/manifest.json index 5ea549c7..43995856 100644 --- a/fixtures/devup-figma-plugin/manifest.json +++ b/fixtures/devup-figma-plugin/manifest.json @@ -1147,11 +1147,11 @@ }, { "path": "snapshots/codegen/upstream-codegen-001-4410624749.snap", - "sha256": "3c6ca1e1b36a6081845a40c97744b6a6261367a0c23f1c98f8ef81c08a1121bf" + "sha256": "9e70ad8c83568784a3fe0e6c4a78d91ce033778ac5929514f9af0a3f22f7bfa7" }, { "path": "snapshots/codegen/upstream-codegen-002-c1ac954cb6.snap", - "sha256": "1035f2a6b9c728ccfaa56a4a799eceed7376a3eac9a091cd2f3db38a10a6476a" + "sha256": "4cbdf86aceb0c2bf9a0aa7bacb1e3c0504b208b86919560b68e2af2b4049425b" }, { "path": "snapshots/codegen/upstream-codegen-003-c855738695.snap", @@ -1227,27 +1227,27 @@ }, { "path": "snapshots/codegen/upstream-codegen-021-6cae93b843.snap", - "sha256": "04fd0a4b60c0c45c384d291ad44f99134104065435ed6d03977ccf473bbbdf91" + "sha256": "9c4b069f3f88ce0ce17d3a868fead4673a2b73acdf5252e406556e5075aa70c4" }, { "path": "snapshots/codegen/upstream-codegen-022-8dce1fe15f.snap", - "sha256": "fd1c9794cf78d13aa5107ad8ba4400f1b6b7343de669db3760f251fbcfda8b44" + "sha256": "1e9cf4f1ed045a2e2411279abb83315028201abcbaaa2eeb2df8a1d35bf4bebd" }, { "path": "snapshots/codegen/upstream-codegen-023-040bff1c77.snap", - "sha256": "ffc152d7496bb46549d990d470de30c3594a8ba2f63fa03053a83e8d038b05be" + "sha256": "980347130fb753feac9278651bba8bfc002b4b1b588997b9665d0089fba05192" }, { "path": "snapshots/codegen/upstream-codegen-024-eb534f354c.snap", - "sha256": "6c9dd6391356eee543b278d9d3cd220cf9ab3142c3bdf7cdedb016abe6cb315b" + "sha256": "738294a3c5fd591d2dc56a88d3204b04e37dbf2600ebfe223f06a17608b086f0" }, { "path": "snapshots/codegen/upstream-codegen-025-74cf3c3463.snap", - "sha256": "622fc07d794e81404cd6671b6fe99fbb0d7911192494e4caa0b755cd947e4c00" + "sha256": "23250ae95d18e95ca234104942591abe21aacd42810a2052fb2829439b37ebd7" }, { "path": "snapshots/codegen/upstream-codegen-026-e6df42a1c0.snap", - "sha256": "60d3855fb7a3470bcb1347731c0a926294f659765e2df8f4c4fa39f562552175" + "sha256": "872e304eedcc761bb101c78c218c6473aeadd913c3f003a3f4b01c2facfa76ee" }, { "path": "snapshots/codegen/upstream-codegen-027-2373d0fb1b.snap", @@ -1267,11 +1267,11 @@ }, { "path": "snapshots/codegen/upstream-codegen-031-5d6f0a82e3.snap", - "sha256": "89940a0a9afb628cb80a48355e5e8bad1b579d58246ee6d09ece3ac190e21f42" + "sha256": "9f9d8a5968ac1f3616199fce93236029b7956a57e25949a687812d8b9358dfda" }, { "path": "snapshots/codegen/upstream-codegen-032-be85ad69d0.snap", - "sha256": "14d64e376c583934f528603427c9cde9669e55a3834c3c99deda335f066cc03a" + "sha256": "409c51e71a1429cd7e976a8a9fcc69eacac549f5ee8648cd9ca29751cd5fba57" }, { "path": "snapshots/codegen/upstream-codegen-033-4f42f0cabb.snap", @@ -1307,7 +1307,7 @@ }, { "path": "snapshots/codegen/upstream-codegen-041-7c9175487c.snap", - "sha256": "27131ed7ed4c084e0c0b8a38a4966c517f0beb967a5ceda54829c553d1769405" + "sha256": "c99c14e4a33e8ed0414e0eff0ed0622e1d25ad0358ab6c894c67aa64ada1b9a5" }, { "path": "snapshots/codegen/upstream-codegen-042-246e72e17a.snap", @@ -1327,7 +1327,7 @@ }, { "path": "snapshots/codegen/upstream-codegen-046-82a07e3ec3.snap", - "sha256": "c79565fb53c8c0602beba8c588c7c05e0b2a0b715dbd2ff7218c1a72314f54b5" + "sha256": "7f7318b7b72e383e4e0d4a002a1ee8fd75143260989f261fb3c81538dd0cb8a6" }, { "path": "snapshots/codegen/upstream-codegen-047-d524fcd6c3.snap", @@ -1343,19 +1343,19 @@ }, { "path": "snapshots/codegen/upstream-codegen-050-fda399bfe0.snap", - "sha256": "d9593d89e3dcde0e16d8bd81e07fe5c66d7f32b4e748aec8281beef6342afe12" + "sha256": "3b562c51d104a60aa4822d37a65aee7551c6fc7e4cc804be056e82b39d13d093" }, { "path": "snapshots/codegen/upstream-codegen-051-53df7b9474.snap", - "sha256": "04eefa2a7f108074e9390806bd32dc85668b4c554c9d410fa04640ae8627cc61" + "sha256": "120ec07a0f6fadb70fd1d40012f9b4754d68d6a35df6404bedb75e0ecfec8874" }, { "path": "snapshots/codegen/upstream-codegen-052-e6195ba5a2.snap", - "sha256": "e3981c71e1f20c9c2fb2e526cc5701d1e2d4f63dc30797dd242f5d83c33047fa" + "sha256": "9dedc44d07d512eb9476a40d8296b045e7d36eda1842a1024053a67d03241164" }, { "path": "snapshots/codegen/upstream-codegen-053-5fbe283840.snap", - "sha256": "cd12ccced864665cdcdb9f82d5de44171c058d1bb2984a337ef537a4a4d4d6f5" + "sha256": "fac1f2fe1a6e8f0ce554fba06a83e455b41e77e181c2d52bd8438738f8739610" }, { "path": "snapshots/codegen/upstream-codegen-054-e536c17528.snap", @@ -1363,7 +1363,7 @@ }, { "path": "snapshots/codegen/upstream-codegen-055-1539142ffd.snap", - "sha256": "3c6ca1e1b36a6081845a40c97744b6a6261367a0c23f1c98f8ef81c08a1121bf" + "sha256": "9e70ad8c83568784a3fe0e6c4a78d91ce033778ac5929514f9af0a3f22f7bfa7" }, { "path": "snapshots/codegen/upstream-codegen-056-834fa66895.snap", @@ -1375,11 +1375,11 @@ }, { "path": "snapshots/codegen/upstream-codegen-058-a5bed8edb7.snap", - "sha256": "15a1afe6ead2abf21b119f60d50aa2845815315b9b16568733d98826a5ac0368" + "sha256": "2df120f6661ff523d8705176bec929d2e35d7aaaba21bef891f786bf22b2decf" }, { "path": "snapshots/codegen/upstream-codegen-059-0b07052901.snap", - "sha256": "b15a5e2ff810fcbc268daa65fbf9d28cf84bd7ecfa91b4a1b7a662b066de0b50" + "sha256": "6ba30c9cfcd7ccad1298291974c666423667e3c20390696b004c930524a0c0af" }, { "path": "snapshots/codegen/upstream-codegen-060-b58a7535c7.snap", @@ -1423,47 +1423,47 @@ }, { "path": "snapshots/codegen/upstream-codegen-070-a01830c33f.snap", - "sha256": "c89b9d7d44226f484516611697a623144d6dcb607aac57aa1db1731e4a2d5c97" + "sha256": "9620b86be419a1072e5b9de746d7105b785ffdb6f257e1d316b1111eb9f95553" }, { "path": "snapshots/codegen/upstream-codegen-071-3a132a6b4d.snap", - "sha256": "2c42f25dd2cd3c2c510066356a64048bb29e9bdc1b4f0b2906361b6d3d48ed35" + "sha256": "75edb2cf4415ccd825df5b9833b685762810c6d6c9dc6c695e93c3eb234e26f4" }, { "path": "snapshots/codegen/upstream-codegen-072-df70a74728.snap", - "sha256": "80e2781f988d17d41112f820022ffef354a43e30118ed13238d71612f9ca8de6" + "sha256": "85f713e2b342c6bd5dd6883eccaa79d083154d84a0dcabde3e183318c6742471" }, { "path": "snapshots/codegen/upstream-codegen-073-ef0cdd87cf.snap", - "sha256": "5dbc6f46638b981fde91bdec886d786858d0e53e61712d48e2b103b5a4c250d2" + "sha256": "eaa16be2d46bb66171885a040c493992c7af6648529008b858e833928240d2a8" }, { "path": "snapshots/codegen/upstream-codegen-074-57a24a2690.snap", - "sha256": "a365f4630d7a0c57b3d5d5a86f7353970e866b388e8d079586d092a2ee5cf224" + "sha256": "09a32e78a6def879c9f64b5bb577519f477ccce4faa5c13ba8c26a5366ceeff1" }, { "path": "snapshots/codegen/upstream-codegen-075-fb8db22bb9.snap", - "sha256": "541e97ba17117d34792af36989cce3c5e9a27d33b7cbaa870722bf41d7382f87" + "sha256": "1737d7a374fa174684eb55a9a5f226e7d2598451f2ac3c6a36934e83eba39cc3" }, { "path": "snapshots/codegen/upstream-codegen-076-3d2d4543bb.snap", - "sha256": "e99419787f9f80359815bbd4656379e568a2162338bc7c74bceca2c79fb68333" + "sha256": "5f2c1ac791ccd758ccb0b8ffba5be8906ac58a4b9887b84e07fe75317aa2c68c" }, { "path": "snapshots/codegen/upstream-codegen-077-a65991a745.snap", - "sha256": "958dde8497a4dc970a6177bbadcdc4c1e1dc85ff6774eb1a7d273991b5083342" + "sha256": "1cb0e65c7808d9be902e1c614d29777c69f8207389fce3fad007a2f66145607b" }, { "path": "snapshots/codegen/upstream-codegen-078-d6b1085274.snap", - "sha256": "a873dd75640d408551a78c0cbee5bc3fecce586d96d96d2f2a91879fd9e9cd99" + "sha256": "24471bc5abc27e2fda36bb42862e64be0654beb8a885a91dee4a7cd6f8220ae6" }, { "path": "snapshots/codegen/upstream-codegen-079-9e7f3ee9e4.snap", - "sha256": "36104a68a8fadb2f293f3174fa75ab4259329c11defac8b64d58f29fdd70d5c9" + "sha256": "13289f198cc9481df72453811c6dac6240d22cd3ebaf101a16502169208fc632" }, { "path": "snapshots/codegen/upstream-codegen-080-f6233dd6a0.snap", - "sha256": "8fa99f0fe20f07891c75572d82fd86c9d0af901e5a51966e92a6907e14bbbdf9" + "sha256": "fb5a70f116a9d2f0319a67f364bf26e9a51da7dcce8e7931310166edc92ae482" }, { "path": "snapshots/codegen/upstream-codegen-081-ed3fa48802.snap", @@ -1563,7 +1563,7 @@ }, { "path": "snapshots/codegen/upstream-codegen-105-0137b6735e.snap", - "sha256": "4d6d8590b353e565bf88c67e4eb0df8035b9b7f03016cb68386466797e0bcf40" + "sha256": "36c3d66bfa18d70946a4c9a2a018886aabc265c33d890c87251a0af2c6c4ca4e" }, { "path": "snapshots/codegen/upstream-codegen-106-6d687083cc.snap", @@ -1579,7 +1579,7 @@ }, { "path": "snapshots/codegen/upstream-codegen-109-e2824ad5be.snap", - "sha256": "a91faf7ec1cfcc54c0418c362c74446dc25b49995471d36ce498bb9bcc679524" + "sha256": "bd3337b0e781209e46e14855cf16148fcef3073c8ecfae324c36a7f46e86144a" }, { "path": "snapshots/codegen/upstream-codegen-110-f2505cce4b.snap", @@ -1599,7 +1599,7 @@ }, { "path": "snapshots/codegen/upstream-codegen-114-855686da78.snap", - "sha256": "b2164313aceeea5a60940c521ac6870e66e42b9ea555653aa22cb18485c2072b" + "sha256": "244c70671418abfa4b2d57508a2543954c9a7178eed9a6ee98a61bebf810e2a8" }, { "path": "snapshots/codegen/upstream-codegen-115-45d3f116b5.snap", @@ -1631,7 +1631,7 @@ }, { "path": "snapshots/codegen/upstream-codegen-122-7ddad74cc1.snap", - "sha256": "d3752dc22fd731f727869c6b8f7bea9bdbf21d1f17b9c581a03d5d5eac6aa55a" + "sha256": "7d306e1feae002a8501083cc9a4f3967453523d34ef6750be2999f7eaaf7d87f" }, { "path": "snapshots/codegen/upstream-codegen-123-179129a02c.snap", @@ -1639,7 +1639,7 @@ }, { "path": "snapshots/codegen/upstream-codegen-124-bdedf14335.snap", - "sha256": "d3752dc22fd731f727869c6b8f7bea9bdbf21d1f17b9c581a03d5d5eac6aa55a" + "sha256": "7d306e1feae002a8501083cc9a4f3967453523d34ef6750be2999f7eaaf7d87f" }, { "path": "snapshots/codegen/upstream-codegen-125-4a4a93c4c9.snap", @@ -1647,7 +1647,7 @@ }, { "path": "snapshots/codegen/upstream-codegen-126-14073e1999.snap", - "sha256": "d3752dc22fd731f727869c6b8f7bea9bdbf21d1f17b9c581a03d5d5eac6aa55a" + "sha256": "7d306e1feae002a8501083cc9a4f3967453523d34ef6750be2999f7eaaf7d87f" }, { "path": "snapshots/codegen/upstream-codegen-127-1d5ead088d.snap", @@ -1655,7 +1655,7 @@ }, { "path": "snapshots/codegen/upstream-codegen-128-2222304a14.snap", - "sha256": "1217bc3380420e81983d14fa4567cbf310563a594878f6331e4523228ba4d2cc" + "sha256": "4495e28347cb7b463135eaf6a9239fef706b3a1d1adadee94922d6baa435bfeb" }, { "path": "snapshots/codegen/upstream-codegen-129-7c97156f7f.snap", @@ -1671,7 +1671,7 @@ }, { "path": "snapshots/codegen/upstream-codegen-132-9a7b0243b6.snap", - "sha256": "d3752dc22fd731f727869c6b8f7bea9bdbf21d1f17b9c581a03d5d5eac6aa55a" + "sha256": "7d306e1feae002a8501083cc9a4f3967453523d34ef6750be2999f7eaaf7d87f" }, { "path": "snapshots/codegen/upstream-codegen-133-f425fa006c.snap", @@ -1679,7 +1679,7 @@ }, { "path": "snapshots/codegen/upstream-codegen-134-f80fffb8b4.snap", - "sha256": "3c6ca1e1b36a6081845a40c97744b6a6261367a0c23f1c98f8ef81c08a1121bf" + "sha256": "9e70ad8c83568784a3fe0e6c4a78d91ce033778ac5929514f9af0a3f22f7bfa7" }, { "path": "snapshots/codegen/upstream-codegen-135-e00e826755.snap", @@ -1699,7 +1699,7 @@ }, { "path": "snapshots/codegen/upstream-codegen-139-63f7b1f175.snap", - "sha256": "95d287efdbf6ea1df14d68a6550f845ccda8eb05957172fb4362fc18c551d5dc" + "sha256": "9a273a5577d457ec7bd3b7378d36e95ddf0ff78f64ed259f54e474c9629b260d" }, { "path": "snapshots/codegen/upstream-codegen-140-e577df9ad5.snap", @@ -1711,7 +1711,7 @@ }, { "path": "snapshots/codegen/upstream-codegen-142-ac82229282.snap", - "sha256": "bdfc7d7b71563b12675756e301fbfda67699f3a2ab6000ba07b945f78460036b" + "sha256": "18e8945eaecca6d062bbd034df354e01e56bd6ec3bf2f3edeccdf3af6e3dfcd6" }, { "path": "snapshots/codegen/upstream-codegen-143-52b441f9f4.snap", @@ -1719,11 +1719,11 @@ }, { "path": "snapshots/codegen/upstream-codegen-144-042a6de403.snap", - "sha256": "95d287efdbf6ea1df14d68a6550f845ccda8eb05957172fb4362fc18c551d5dc" + "sha256": "9a273a5577d457ec7bd3b7378d36e95ddf0ff78f64ed259f54e474c9629b260d" }, { "path": "snapshots/codegen/upstream-codegen-145-f7001b5648.snap", - "sha256": "c72e7fa29de3f8a34afb2317af0578dbfa61f5bca610af8d3ec75587936695b6" + "sha256": "0f6848b4f28e7b6eb373cd61f3529888fb55927c9b642210a6659fde2991aae8" }, { "path": "snapshots/codegen/upstream-codegen-146-56be99e864.snap", @@ -1867,7 +1867,7 @@ }, { "path": "snapshots/codegen/upstream-codegen-181-5afa626d49.snap", - "sha256": "cdd86d5dc0becc9fce97bd9169c210c4310822bc94e59549d342d68a9bbd3d5e" + "sha256": "b7127d487b772bcfaf5151d5f437f40f5439aea6f8ebb2027c712e1db46623c9" }, { "path": "snapshots/codegen/upstream-codegen-182-c45d19171b.snap", @@ -1915,19 +1915,19 @@ }, { "path": "snapshots/codegen/upstream-codegen-193-bd488a5710.snap", - "sha256": "485ea5bbf1e093b29d00090120cc8fcfc7e95a671d4328a8e93094c11938575e" + "sha256": "ff913f1e601b8bad6114a3ea95ae6f79459a5904fa6eaad3cf7784b4ae913979" }, { "path": "snapshots/codegen/upstream-codegen-194-17922977f6.snap", - "sha256": "485ea5bbf1e093b29d00090120cc8fcfc7e95a671d4328a8e93094c11938575e" + "sha256": "986188d03130c1c0515b2d0925fa374c5c67b7504ec4e23d07c741ce19705824" }, { "path": "snapshots/codegen/upstream-codegen-195-1a6efe22a8.snap", - "sha256": "922e56218783cc63e2a7e17a7d8ea701a27d806fb4f1cc5e31ed46c317299403" + "sha256": "9e00d9af3e785eba11704a2e01c2ab8e361d0b492ccbe043881415d8f38d74dc" }, { "path": "snapshots/codegen/upstream-codegen-196-d14f03afcd.snap", - "sha256": "922e56218783cc63e2a7e17a7d8ea701a27d806fb4f1cc5e31ed46c317299403" + "sha256": "5ade484c7e55951217a8959aade09f18e74c1a0c50e631f0524bd7e53cc5121e" }, { "path": "snapshots/codegen/upstream-codegen-197-026402bf2e.snap", @@ -2083,7 +2083,7 @@ }, { "path": "snapshots/codegen/upstream-codegen-235-8a3992468b.snap", - "sha256": "85f92c032e9fe4c9420fd8873b1650226b18b88d5a2606d637076144fdb71b9e" + "sha256": "ed3506bc5e94b81f18766512d749279f10e45c1cce2db2d687f2d21ab3a47d93" }, { "path": "snapshots/codegen/upstream-codegen-236-621c6d6a9e.snap", @@ -2171,11 +2171,11 @@ }, { "path": "snapshots/codegen/upstream-root-257-f23261dd4f.snap", - "sha256": "86b89161d10b076a122269d6e4f428b4981f2e7163e18ad45c38a153e889b530" + "sha256": "4c51b42f35f67cd7e6360c7ccb3613f820692f4b064b4884ac171dede97227db" }, { "path": "snapshots/codegen/upstream-root-258-5cfe8baba5.snap", - "sha256": "c6d9f1d632bf12bde7eebd2ca331bfe39c3fcab6949c71e3a70cdcbcf07b0c33" + "sha256": "293903516d85b3316eba5e5c4e0f761589ad992aca2387d4cf278bf492bca459" }, { "path": "snapshots/codegen/upstream-root-259-0ae0aa9528.snap", diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-001-4410624749.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-001-4410624749.snap index 3fe43e64..2bbe4641 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-001-4410624749.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-001-4410624749.snap @@ -2,4 +2,5 @@ source: crates/devup-mcp-devup-ui/tests/compat_fixtures.rs expression: actual --- -"" + +"" diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-002-c1ac954cb6.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-002-c1ac954cb6.snap index f2fe1e64..05968abe 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-002-c1ac954cb6.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-002-c1ac954cb6.snap @@ -2,4 +2,5 @@ source: crates/devup-mcp-devup-ui/tests/compat_fixtures.rs expression: actual --- -"" + +"" diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-021-6cae93b843.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-021-6cae93b843.snap index b3840546..4bedae0e 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-021-6cae93b843.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-021-6cae93b843.snap @@ -3,6 +3,6 @@ source: crates/devup-mcp-devup-ui/tests/compat_fixtures.rs expression: actual --- -" +" " diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-022-8dce1fe15f.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-022-8dce1fe15f.snap index 722b5b1f..64d578a4 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-022-8dce1fe15f.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-022-8dce1fe15f.snap @@ -3,6 +3,6 @@ source: crates/devup-mcp-devup-ui/tests/compat_fixtures.rs expression: actual --- -" +" " diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-023-040bff1c77.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-023-040bff1c77.snap index e66565aa..253540df 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-023-040bff1c77.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-023-040bff1c77.snap @@ -3,7 +3,7 @@ source: crates/devup-mcp-devup-ui/tests/compat_fixtures.rs expression: actual --- -" +" +" " diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-025-74cf3c3463.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-025-74cf3c3463.snap index 22256512..5bc85737 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-025-74cf3c3463.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-025-74cf3c3463.snap @@ -3,7 +3,7 @@ source: crates/devup-mcp-devup-ui/tests/compat_fixtures.rs expression: actual --- -" +" +" -" +" diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-031-5d6f0a82e3.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-031-5d6f0a82e3.snap index 8423b7b0..47a97903 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-031-5d6f0a82e3.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-031-5d6f0a82e3.snap @@ -4,7 +4,6 @@ expression: actual --- "" + +"" diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-046-82a07e3ec3.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-046-82a07e3ec3.snap index 0e503934..8bee2238 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-046-82a07e3ec3.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-046-82a07e3ec3.snap @@ -2,4 +2,5 @@ source: crates/devup-mcp-devup-ui/tests/compat_fixtures.rs expression: actual --- -"" + +"" diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-050-fda399bfe0.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-050-fda399bfe0.snap index 907e7c83..8703bc25 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-050-fda399bfe0.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-050-fda399bfe0.snap @@ -3,6 +3,6 @@ source: crates/devup-mcp-devup-ui/tests/compat_fixtures.rs expression: actual --- -" +" " diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-051-53df7b9474.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-051-53df7b9474.snap index 9d9c667b..689fc45b 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-051-53df7b9474.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-051-53df7b9474.snap @@ -3,6 +3,6 @@ source: crates/devup-mcp-devup-ui/tests/compat_fixtures.rs expression: actual --- -" +" " diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-052-e6195ba5a2.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-052-e6195ba5a2.snap index 1f16c346..0356846b 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-052-e6195ba5a2.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-052-e6195ba5a2.snap @@ -3,6 +3,6 @@ source: crates/devup-mcp-devup-ui/tests/compat_fixtures.rs expression: actual --- -" +" " diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-053-5fbe283840.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-053-5fbe283840.snap index 21eb3e00..13da9de8 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-053-5fbe283840.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-053-5fbe283840.snap @@ -3,6 +3,6 @@ source: crates/devup-mcp-devup-ui/tests/compat_fixtures.rs expression: actual --- -" +" " diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-055-1539142ffd.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-055-1539142ffd.snap index 3fe43e64..2bbe4641 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-055-1539142ffd.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-055-1539142ffd.snap @@ -2,4 +2,5 @@ source: crates/devup-mcp-devup-ui/tests/compat_fixtures.rs expression: actual --- -"" + +"" diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-058-a5bed8edb7.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-058-a5bed8edb7.snap index 82298945..48f3b2fd 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-058-a5bed8edb7.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-058-a5bed8edb7.snap @@ -2,4 +2,5 @@ source: crates/devup-mcp-devup-ui/tests/compat_fixtures.rs expression: actual --- -"" + +"" diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-059-0b07052901.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-059-0b07052901.snap index ee21a346..a8675ddd 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-059-0b07052901.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-059-0b07052901.snap @@ -3,10 +3,4 @@ source: crates/devup-mcp-devup-ui/tests/compat_fixtures.rs expression: actual --- -"" +"" diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-070-a01830c33f.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-070-a01830c33f.snap index de7d3007..df01f8fd 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-070-a01830c33f.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-070-a01830c33f.snap @@ -4,7 +4,6 @@ expression: actual --- " diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-114-855686da78.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-114-855686da78.snap index a063712b..0419d595 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-114-855686da78.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-114-855686da78.snap @@ -5,6 +5,7 @@ expression: actual "