From 3e66cf951bc6872dc4b1afad5d32ab67417c0f01 Mon Sep 17 00:00:00 2001 From: leader Date: Thu, 22 Oct 2026 17:15:33 +0900 Subject: [PATCH 001/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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/134] 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 = <>