diff --git a/.changepacks/changepack_log_5W-DuiAzT_VrJ42YiG7t9.json b/.changepacks/changepack_log_5W-DuiAzT_VrJ42YiG7t9.json new file mode 100644 index 00000000..5999e023 --- /dev/null +++ b/.changepacks/changepack_log_5W-DuiAzT_VrJ42YiG7t9.json @@ -0,0 +1,7 @@ +{ + "changes": { + "Cargo.toml": "Patch" + }, + "note": "Runtime audit scrolls through scroll reveals and waits for them to settle; a vector-only variant of a component set with text variants stays a component reference", + "date": "2026-10-24T08:31:15.877717600Z" +} \ No newline at end of file diff --git a/.changepacks/changepack_log_8kyvms1No9ruHAr5hCy8h.json b/.changepacks/changepack_log_8kyvms1No9ruHAr5hCy8h.json new file mode 100644 index 00000000..91030d13 --- /dev/null +++ b/.changepacks/changepack_log_8kyvms1No9ruHAr5hCy8h.json @@ -0,0 +1,7 @@ +{ + "changes": { + "Cargo.toml": "Minor" + }, + "note": "Report instance overrides that shared component properties cannot preserve, with generated prop evidence and required export review.", + "date": "2026-10-27T12:59:41.181270100Z" +} \ No newline at end of file diff --git a/.changepacks/changepack_log_HdZ8AinyXi7pJg5Pc9ffy.json b/.changepacks/changepack_log_HdZ8AinyXi7pJg5Pc9ffy.json new file mode 100644 index 00000000..3ff8d95f --- /dev/null +++ b/.changepacks/changepack_log_HdZ8AinyXi7pJg5Pc9ffy.json @@ -0,0 +1,7 @@ +{ + "changes": { + "Cargo.toml": "Minor" + }, + "note": "devup_ui_validate answers a compact summary by default and reads paths from disk; devup_figma_export writes code outputs under .devup/exports by default when projectRoot is given", + "date": "2026-10-24T11:43:31.269250300Z" +} \ No newline at end of file diff --git a/.changepacks/changepack_log_eRTXLDvy0yMJ3YwboDGWu.json b/.changepacks/changepack_log_eRTXLDvy0yMJ3YwboDGWu.json new file mode 100644 index 00000000..1531f126 --- /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 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/.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/.changepacks/changepack_log_tjwFHFshfYAmyGP0g_Yjs.json b/.changepacks/changepack_log_tjwFHFshfYAmyGP0g_Yjs.json new file mode 100644 index 00000000..9c348d01 --- /dev/null +++ b/.changepacks/changepack_log_tjwFHFshfYAmyGP0g_Yjs.json @@ -0,0 +1,7 @@ +{ + "changes": { + "Cargo.toml": "Minor" + }, + "note": "Devup Bridge sends its current scripts to an open plugin with older scripts (scripts-update); the plugin swaps them in and reports the new hash without being closed and re-run", + "date": "2026-10-27T16:24:15.116555200Z" +} \ No newline at end of file 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 diff --git a/.gitattributes b/.gitattributes index 94b0d254..071dac1a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -13,3 +13,7 @@ crates/devup-mcp/src/server/skills/manifest.json -text crates/devup-mcp/src/server/text/** -text crates/devup-mcp-figma/src/text/** -text crates/devup-mcp-devup-ui/src/text/** -text + +# The live plugin scripts bundle is fingerprinted byte for byte in +# plugin_scripts.rs; a CRLF checkout would fail that check on Windows alone. +crates/devup-mcp-figma/src/plugin_scripts_bundle.js -text 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/README.md b/README.md index 8733c2a7..c6caf0ed 100644 --- a/README.md +++ b/README.md @@ -471,7 +471,7 @@ devup-mcp가 Figma에 붙는 경로는 **둘**이고, 대등하지 않습니다. **1. Figma 데스크톱 앱에서 불러옵니다** -Figma의 플러그인 등록은 데스크톱 앱의 메뉴 동작이라 스크립트로 대신할 수 없습니다. 이 세 단계만 직접 하시면 됩니다. +에이전트는 `devup_skills install`로 배포되는 `figma-bridge-setup` 스킬을 읽고 computer use로 아래 메뉴를 직접 조작합니다. 먼저 status에서 host/server commit·buildId를 비교하고, `inView:false`이면 Figma 창을 앞으로 가져와 해당 파일 탭을 누릅니다. Figma가 없으면 question 도구로 데스크톱 설치와 metered direct OAuth 중에서 묻습니다. 오래된 host는 PID·경로·소유 클라이언트를 확인한 뒤 그 연결만 재시작하며, 다른 세션을 중단하기 전에는 승인을 받습니다. 마지막에 status와 작은 노드 읽기로 연결을 확인합니다. 1. **Figma 데스크톱 앱**을 엽니다 (브라우저판은 로컬 플러그인을 못 불러옵니다) 2. 메뉴에서 **Plugins → Development → Import plugin from manifest...** @@ -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-devup-ui/src/codegen/component/component_generation.rs b/crates/devup-mcp-devup-ui/src/codegen/component/component_generation.rs index c15adac0..a04c36a7 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); } @@ -376,11 +387,7 @@ pub fn generate_component_set_target( target_id.to_owned(), ) }; - let code = if !main && generated.tsx.contains("") { - generated.tsx.replace("", "") - } else { - generated.tsx.clone() - }; + let code = generated.tsx.clone(); let variants = if main { component_variants( snapshot, @@ -919,4 +926,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/component/component_variants.rs b/crates/devup-mcp-devup-ui/src/codegen/component/component_variants.rs index ed506132..72939529 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 @@ -223,6 +223,11 @@ fn finalize_codegen_output( ); output.tsx = tsx; output.source_map = source_map; + for record in super::instance_overrides::instance_overrides(snapshot, root_id, options) { + output.tsx.push_str(&format!("\n// @devup-override {}\n", record.to_string() + .replace('\u{2028}', "\\u2028").replace('\u{2029}', "\\u2029"))); + output.diagnostics.push(super::instance_overrides::diagnostic(record)); + } super::image::attach_evidence(snapshot, options, &mut output); crate::provenance::account_for_sizing(snapshot, &mut output); validate_tsx(&output.tsx)?; @@ -269,6 +274,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 +573,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/component/render_node.rs b/crates/devup-mcp-devup-ui/src/codegen/component/render_node.rs index b33c7146..6fd90c21 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")) { @@ -191,7 +213,7 @@ fn render_node( } else if layout::centres_its_only_child(snapshot, node) { "Center" } else { - match inferred_mode { + match inferred_mode.or_else(|| view.string("layoutMode")) { Some("GRID") => "Grid", Some("HORIZONTAL" | "VERTICAL") if inferred_align("primaryAxisAlignItems") == Some("CENTER") @@ -217,6 +239,7 @@ fn render_node( context.width_policy.as_ref(), super::image::original_image(node, &context.asset_policies), ); + context.diagnostics.extend(layout::suspicious_values(snapshot, node, &props)); // A frame with no auto-layout places its children itself, and this keeps // them resolvable. Once the gap around them is measurable it is emitted as // padding instead, which puts them where they belong on its own — so the @@ -285,6 +308,7 @@ fn render_node( &context.length_tokens, &mut context.used_tokens, ); + text::elide_tokenized_typography(&mut props, &context.typography_values, &context.length_tokens); // Last, as the plugin merges `getReactionProps` last: what a timed Smart // Animate changes becomes keyframes on the child it changes, or on the // frame itself. 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/src/codegen/image/image_projection.rs b/crates/devup-mcp-devup-ui/src/codegen/image/image_projection.rs index 4e015422..0c8bb75c 100644 --- a/crates/devup-mcp-devup-ui/src/codegen/image/image_projection.rs +++ b/crates/devup-mcp-devup-ui/src/codegen/image/image_projection.rs @@ -827,6 +827,7 @@ pub(super) fn render_paint_layers( { let mut layer = Vec::new(); for (name, value) in [ + ("aria-hidden", "true"), ("pos", "absolute"), ("inset", "0"), ("zIndex", "-1"), 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..e24eb0ef 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" { @@ -145,6 +146,7 @@ pub(super) fn project_tree( .filter(|(_, p)| p["visible"] != false && p["opacity"] != 0) { let mut props: BTreeMap = [ + ("aria-hidden", "true"), ("pos", "absolute"), ("inset", "0"), ("zIndex", "-1"), diff --git a/crates/devup-mcp-devup-ui/src/codegen/instance_overrides.rs b/crates/devup-mcp-devup-ui/src/codegen/instance_overrides.rs new file mode 100644 index 00000000..ca1b51ad --- /dev/null +++ b/crates/devup-mcp-devup-ui/src/codegen/instance_overrides.rs @@ -0,0 +1,211 @@ +use std::collections::{BTreeSet, HashSet}; + +use devup_mcp_figma::{Diagnostic, FidelityImpact, RawNode, Snapshot}; +use serde_json::{Value, json}; + +use super::{CodegenOptions, component, layout, style, text}; + +pub fn instance_overrides( + snapshot: &Snapshot, + root_id: &str, + options: &CodegenOptions, +) -> Vec { + let mut pending = vec![root_id]; + let mut visited = HashSet::new(); + let mut records = Vec::new(); + while let Some(id) = pending.pop() { + if !visited.insert(id) { + continue; + } + let Some(node) = snapshot.nodes.get(id) else { + continue; + }; + pending.extend(node.typed_view().child_ids()); + if node.node_type == "INSTANCE" { + records.extend(for_instance(snapshot, node, options)); + } + } + records +} + +pub(crate) fn for_instance( + snapshot: &Snapshot, + instance: &RawNode, + options: &CodegenOptions, +) -> Vec { + let view = instance.typed_view(); + let main_id = view + .string("mainComponentId") + .or_else(|| view.string("componentId")); + let main = main_id.and_then(|id| snapshot.nodes.get(id)); + let set_id = view + .string("mainComponentSetId") + .or_else(|| main.and_then(|node| node.typed_view().string("parentId"))); + let mut records = Vec::new(); + for entry in view + .value("overrides") + .and_then(Value::as_array) + .into_iter() + .flatten() + { + let target_id = entry.get("id").and_then(Value::as_str); + let target = target_id.and_then(|id| snapshot.nodes.get(id)); + let master = if target_id == Some(instance.id.as_str()) { + main + } else { + target_id + .and_then(|id| id.rsplit(';').next()) + .and_then(|id| snapshot.nodes.get(id)) + .filter(|node| Some(node.id.as_str()) != target_id) + }; + for field in entry + .get("overriddenFields") + .and_then(Value::as_array) + .into_iter() + .flatten() + .filter_map(Value::as_str) + { + if target.is_some_and(|node| expressible(instance, node, field)) { + continue; + } + let resolved = target.and_then(|node| node.typed_view().value(field)); + let master_value = master.and_then(|node| node.typed_view().value(field)); + records.push(json!({ + "instanceNodeId": instance.id, + "mainComponentId": main_id, + "mainComponentSetId": set_id, + "mainComponentSetName": set_id.and_then(|id| snapshot.nodes.get(id)).and_then(|node| node.typed_view().name()), + "variantProperties": view.value("variantProperties"), + "overriddenNodeId": target_id, + "overriddenFields": [field], + "classification": "not-expressible", + "resolvedValue": resolved, + "resolution": if resolved.is_some() { "resolved" } else { "unresolved" }, + "generatedProps": target.map(|node| generated_props(snapshot, node, options)).unwrap_or_default(), + "generatedPropsScope": "resolved-node", + "masterValue": master_value, + "masterResolution": if master_value.is_some() { "resolved" } else { "unresolved" }, + "differsFromMaster": resolved.zip(master_value).map(|(value, master)| value != master), + })); + } + } + if let Some(error) = instance.field_errors.get("overrides") { + records.push( + json!({"instanceNodeId":instance.id,"mainComponentId":main_id, + "classification":"not-expressible","resolution":"unresolved", + "overriddenFields":[],"readError":error,"generatedProps":[]}), + ); + } + records +} + +fn expressible(instance: &RawNode, target: &RawNode, field: &str) -> bool { + let expected = match field { + "characters" => "TEXT", + "visible" => "BOOLEAN", + "mainComponent" => "INSTANCE_SWAP", + "variantProperties" => { + return instance + .typed_view() + .value("componentProperties") + .and_then(Value::as_object) + .is_some_and(|properties| { + instance + .typed_view() + .value("variantProperties") + .and_then(Value::as_object) + .is_some_and(|variants| { + !variants.is_empty() + && variants.iter().all(|(key, value)| { + properties.get(key).is_some_and(|prop| { + prop["type"] == "VARIANT" + && prop.get("value") == Some(value) + }) + }) + }) + }); + } + _ => return false, + }; + target + .typed_view() + .value("componentPropertyReferences") + .and_then(|refs| refs.get(field)) + .and_then(Value::as_str) + .and_then(|key| { + instance + .typed_view() + .value("componentProperties") + .and_then(|props| props.get(key)) + }) + .is_some_and(|property| property["type"] == expected) +} + +fn generated_props(snapshot: &Snapshot, node: &RawNode, options: &CodegenOptions) -> Vec { + let component = if node.node_type == "TEXT" { + "Text" + } else { + "Box" + }; + let mut props = Vec::new(); + let mut tokens = BTreeSet::new(); + layout::push_layout_props( + snapshot, + node, + component, + &mut props, + options.root_layout, + false, + options.width_policy.as_ref(), + super::image::original_image(node, &options.asset_policies), + ); + style::push_style_props( + snapshot, + node, + component, + super::image::asset_kind(snapshot, node, &options.asset_policies), + &mut props, + &mut tokens, + style::StyleOptions { + reserve_image_content: true, + asset_policies: &options.asset_policies, + variable_tokens: &options.variable_tokens, + length_tokens: &options.length_tokens, + asset_names_per_node: options.asset_names_per_node, + width_policy: options.width_policy.as_ref(), + }, + ); + text::push_text_props( + &node.typed_view(), + &options.text_style_tokens, + &options.typography_values, + &options.variable_tokens, + &mut tokens, + &mut props, + ); + style::tokenize_lengths( + snapshot, + node, + &mut props, + &options.length_tokens, + &mut tokens, + ); + props + .iter() + .map(|(name, component::PropValue::String(value))| { + component::render_static_attribute(name, value) + }) + .collect() +} + +pub fn diagnostic(record: Value) -> Diagnostic { + Diagnostic { + code: "DEVUP_CODEGEN_INSTANCE_OVERRIDE".into(), + message: "Instance override is not represented by component properties; preserve it through a new prop/variant or report it before replacing the inline instance.".into(), + node_id: record["instanceNodeId"].as_str().map(str::to_owned), + property: Some("overrides".into()), + fidelity_impact: Some(FidelityImpact::None), + details: Some(record), + ..Diagnostic::default() + } +} diff --git a/crates/devup-mcp-devup-ui/src/codegen/layout.rs b/crates/devup-mcp-devup-ui/src/codegen/layout.rs index 3529839a..0589b13b 100644 --- a/crates/devup-mcp-devup-ui/src/codegen/layout.rs +++ b/crates/devup-mcp-devup-ui/src/codegen/layout.rs @@ -5,3 +5,4 @@ include!("layout/grid_template.rs"); include!("layout/reading_order.rs"); include!("layout/fixed_rows.rs"); +include!("layout/suspicious_values.rs"); diff --git a/crates/devup-mcp-devup-ui/src/codegen/layout/grid_template.rs b/crates/devup-mcp-devup-ui/src/codegen/layout/grid_template.rs index 630ac912..c8e7676a 100644 --- a/crates/devup-mcp-devup-ui/src/codegen/layout/grid_template.rs +++ b/crates/devup-mcp-devup-ui/src/codegen/layout/grid_template.rs @@ -640,9 +640,9 @@ fn push_absolute( placed_by: Option, policy: Option<&WidthPolicy>, ) -> Option { + let parent_node = parent?; string_prop(props, "pos", "absolute"); let view = node.typed_view(); - let parent_node = parent?; // The box to place: the node's own, or the one handed in - an asset's // export. A group's children carry their `x` and `y` in the group's // parent's space, not the group's: the join-us panel's outermost circle, 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..8648f190 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,13 +104,13 @@ 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"); let fill_h = view.string("layoutSizingVertical") == Some("FILL"); - let absolute = view.string("layoutPositioning") == Some("ABSOLUTE") - || placed_by_a_free_layout(snapshot, node, parent, is_page_root); + let absolute = parent.is_some() && (view.string("layoutPositioning") == Some("ABSOLUTE") + || placed_by_a_free_layout(snapshot, node, parent, is_page_root)); let embedded_root = is_render_root && root_layout == RootLayout::Embedded; let containing_block = holds_positioned_children(snapshot, node); let mut width = None; @@ -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); @@ -344,24 +343,10 @@ pub(super) fn push_layout_props( height = Some("100%".to_owned()); } } else if is_root { - let no_dimensions = view.number("width").is_none() && view.number("height").is_none(); - let has_children = view.child_ids().next().is_some(); - let implicit_text_fill = view.node_type() == "TEXT" - && view.value("layoutSizingHorizontal").is_none() - && view.value("layoutSizingVertical").is_none(); - let standalone_component = view.node_type() == "COMPONENT" - && !snapshot - .nodes - .values() - .any(|node| node.typed_view().node_type() == "COMPONENT_SET"); - if (no_dimensions && (view.node_type() != "COMPONENT" || standalone_component)) - || has_children - || fill_w - || fill_h - || view.node_type() == "GROUP" - || implicit_text_fill - { + if fill_w || view.string("layoutAlign") == Some("STRETCH") { width = Some("100%".to_owned()); + } + if fill_h { height = Some("100%".to_owned()); } } else { @@ -965,4 +950,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/layout/suspicious_values.rs b/crates/devup-mcp-devup-ui/src/codegen/layout/suspicious_values.rs new file mode 100644 index 00000000..43e9df39 --- /dev/null +++ b/crates/devup-mcp-devup-ui/src/codegen/layout/suspicious_values.rs @@ -0,0 +1,85 @@ +pub(super) fn suspicious_values( + snapshot: &Snapshot, + node: &RawNode, + props: &[Prop], +) -> Vec { + let view = node.typed_view(); + if super::style::asset_kind(snapshot, node).is_some() { + return Vec::new(); + } + let parent = snapshot.nodes.values().find(|candidate| { + candidate.typed_view().child_ids().any(|id| id == node.id) + }); + if parent.is_some_and(|parent| { + let parent = parent.typed_view(); + parent.string("counterAxisAlignItems") == Some("CENTER") + || parent.string("primaryAxisAlignItems") == Some("CENTER") + }) || view.string("layoutAlign") == Some("CENTER") { + return Vec::new(); + } + let mut diagnostics = Vec::new(); + for (property, PropValue::String(value)) in props { + let field = match property.as_str() { + "w" => "width", + "h" => "height", + "boxSize" => "width", + "aspectRatio" => "targetAspectRatio", + _ => continue, + }; + if field == "width" && view.node_type() == "TEXT" + && (view.string("layoutSizingHorizontal") == Some("HUG") + || view.string("textAutoResize") == Some("WIDTH_AND_HEIGHT")) { + continue; + } + let advice = if property == "aspectRatio" { + let ratio = value.split_once('/').and_then(|(x,y)| Some(x.trim().parse::().ok()? / y.trim().parse::().ok()?)) + .or_else(|| value.parse::().ok()); + let Some(ratio) = ratio.filter(|ratio| ratio.is_finite() && *ratio > 0.0) else { continue; }; + let simple = [1.0, 2.0, 0.5, 3.0, 1.0/3.0, 1.5, 2.0/3.0, 4.0/3.0, 0.75, 16.0/9.0, 9.0/16.0, 21.0/9.0, 9.0/21.0]; + if simple.iter().any(|simple| (ratio / simple - 1.0).abs() <= 0.01) { continue; } + format!("Aspect ratio {value} is not within 1% of a common simple ratio; confirm the intended ratio with the designer in Figma's aspect-ratio control.") + } else { + let Some(number) = value.strip_suffix("px").and_then(|value| value.parse::().ok()) else { continue; }; + if !number.is_finite() || number <= 0.0 || (number.fract()).abs() > 0.001 + || (number.rem_euclid(2.0) - 1.0).abs() > 0.001 + || parent.is_some_and(|parent| explained_by_layout(snapshot, parent, field, number)) { + continue; + } + format!("{value} is odd; nearest even {}/{}px: confirm with the designer or round the {field} in Figma, then re-export.", format_number(number-1.0), format_number(number+1.0)) + }; + diagnostics.push(devup_mcp_figma::Diagnostic { + code: "DEVUP_CODEGEN_SUSPICIOUS_VALUE".into(), + node_id: Some(node.id.clone()), + property: Some(field.into()), + severity: Some(devup_mcp_figma::DiagnosticSeverity::Warning), + fidelity_impact: Some(devup_mcp_figma::FidelityImpact::None), + message: format!("Suspicious design value for {field}: {value}; the captured value is preserved."), + details: Some(serde_json::json!({"originalValue":view.value(field),"appliedValue":value,"generatedProperty":property,"nextAction":advice})), + ..Default::default() + }); + } + diagnostics +} + +fn explained_by_layout(snapshot: &Snapshot, parent: &RawNode, field: &str, number: f64) -> bool { + let view = parent.typed_view(); + let Some(size) = view.number(field) else { return false; }; + let (start, end, main_axis) = match field { + "width" => ("paddingLeft", "paddingRight", "HORIZONTAL"), + "height" => ("paddingTop", "paddingBottom", "VERTICAL"), + _ => return false, + }; + if !matches!(view.string("layoutMode"), Some("HORIZONTAL" | "VERTICAL")) { return false; } + let available = size - view.number(start).unwrap_or(0.0) - view.number(end).unwrap_or(0.0); + let children = view.child_ids().filter_map(|id| snapshot.nodes.get(id)) + .filter(|child| child.typed_view().bool("visible") != Some(false) + && child.typed_view().string("layoutPositioning") != Some("ABSOLUTE")) + .collect::>(); + if view.string("layoutMode") != Some(main_axis) { + return (available-number).abs() <= 0.01; + } + let Some(sizes) = children.iter().map(|child| child.typed_view().number(field)).collect::>>() else { return false; }; + let gaps = sizes.iter().skip(1).map(|_| view.number("itemSpacing").unwrap_or(0.0)).sum::(); + (available - sizes.iter().sum::() - gaps).abs() <= 0.01 + && sizes.iter().any(|size| (size-number).abs() <= 0.01) +} diff --git a/crates/devup-mcp-devup-ui/src/codegen/mod.rs b/crates/devup-mcp-devup-ui/src/codegen/mod.rs index e1c10b4c..b307d216 100644 --- a/crates/devup-mcp-devup-ui/src/codegen/mod.rs +++ b/crates/devup-mcp-devup-ui/src/codegen/mod.rs @@ -3,6 +3,7 @@ mod compat; mod component; mod evidence; mod image; +pub mod instance_overrides; mod layout; pub mod node_ids; mod property_derivations; diff --git a/crates/devup-mcp-devup-ui/src/codegen/property_derivations.rs b/crates/devup-mcp-devup-ui/src/codegen/property_derivations.rs index 032a2b12..bb57f876 100644 --- a/crates/devup-mcp-devup-ui/src/codegen/property_derivations.rs +++ b/crates/devup-mcp-devup-ui/src/codegen/property_derivations.rs @@ -85,6 +85,11 @@ pub(crate) fn property_derivations( &options.length_tokens, &mut tokens, ); + text::elide_tokenized_typography( + &mut props, + &options.typography_values, + &options.length_tokens, + ); let in_reading_grid = layout::in_reading_grid(snapshot, node); let in_wrapping_row = layout::in_wrapping_row(snapshot, node); props.into_iter().filter_map(|(name, PropValue::String(value))| { diff --git a/crates/devup-mcp-devup-ui/src/codegen/responsive.rs b/crates/devup-mcp-devup-ui/src/codegen/responsive.rs index 14e3f86b..7d2dc09f 100644 --- a/crates/devup-mcp-devup-ui/src/codegen/responsive.rs +++ b/crates/devup-mcp-devup-ui/src/codegen/responsive.rs @@ -24,5 +24,7 @@ // Source sections share this module to preserve privacy and test names. include!("responsive/breakpoint_merge.rs"); +include!("responsive/child_alignment.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..bf85c234 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 @@ -8,7 +8,7 @@ use super::{ component::{CodegenOptions, generate_component}, variant::{ POSITION_PROPS, Tree, instance_variant_props, project_tree_keeping_instances, - referenced_component_name, structure_signature, + referenced_component_name, }, }; use crate::provenance::attributes::{ @@ -93,23 +93,6 @@ const CLEARED_WHEN_DROPPED: [&str; 42] = [ /// length rustfmt likes to fight over. const CLEARED_WHEN_DROPPED_MARGINS: [&str; 7] = ["m", "mt", "mr", "mb", "ml", "mx", "my"]; -/// A value that need not be written because it is what the prop already is, -/// from the plugin's `DEFAULT_PROPS_MAP`. Its padding and margin entries are -/// commented out upstream, so they are absent here too. -fn is_default(prop: &str, value: &str) -> bool { - match prop { - "alignItems" | "justifyContent" => value == "flex-start", - "flexDir" => value == "row", - "gap" => value == "0" || value == "0px", - "textDecorationSkipInk" - | "textDecorationThickness" - | "textDecorationColor" - | "textUnderlineOffset" => value == "auto", - "textDecorationStyle" => value == "solid", - _ => false, - } -} - fn cleared_when_dropped(prop: &str) -> bool { CLEARED_WHEN_DROPPED.contains(&prop) || CLEARED_WHEN_DROPPED_MARGINS.contains(&prop) } @@ -200,13 +183,6 @@ pub fn merge_slots(prop: &str, widths: &[Drawn<'_>; SLOTS]) -> Merged { carried = Some(value); } } - if slots - .first() - .and_then(Option::as_deref) - .is_some_and(|value| is_default(prop, value)) - { - slots[0] = None; - } while slots.last().is_some_and(Option::is_none) { slots.pop(); } @@ -281,7 +257,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(); } @@ -597,52 +573,6 @@ fn first_slot(by_slot: &BySlot, precedence: &Precedence) -> Option .min_by_key(|slot| precedence[*slot]) } -/// A node's children, grouped under the key its counterparts will be found by. -/// -/// A child whose shape is unique among its siblings is keyed by that shape, so -/// renaming it in one width does not lose it. Where several siblings share a -/// shape the shape cannot tell them apart, so their name is used instead and -/// they are paired in order: the second `quote` with the second `quote`. Each -/// keeps a key of its own, so siblings drawn between them stay between them; -/// one key for all of them emitted `“ ” title` for `“ title ”`. Insertion -/// order is kept, because it is what the ordering below reads. -fn children_to_map(tree: &Tree) -> Vec<(String, Vec<&Tree>)> { - let mut counts: BTreeMap = BTreeMap::new(); - for child in &tree.children { - *counts.entry(structure_signature(child)).or_default() += 1; - } - let mut grouped: Vec<(String, Vec<&Tree>)> = Vec::new(); - let mut seen: BTreeMap = BTreeMap::new(); - for child in &tree.children { - let signature = structure_signature(child); - let mut key = if counts.get(&signature) == Some(&1) { - format!("sig:{signature}") - } else { - child.node_name.clone() - }; - // `src` is an HTML attribute, not a responsive CSS property. Keep - // distinct sources as distinct children; the existing missing-child - // merge supplies their visibility slots without array-valued URLs. - if child.component == "Image" - && let Some(source) = child.props.get("src") - { - key = serde_json::to_string(&( - key, - source, - child.props.get("srcSet"), - child.props.get("sizes"), - child.props.get("width"), - child.props.get("height"), - )) - .expect("image identity is serializable"); - } - let occurrence = seen.entry(key.clone()).or_insert(0usize); - grouped.push((format!("{key}#{occurrence}"), vec![child])); - *occurrence += 1; - } - grouped -} - /// One order for children that every width agrees with. /// /// Each width gives an order over the children it has, and no width need have @@ -752,6 +682,7 @@ fn render_attribute(merged: &Merged) -> Option { /// What a layout element does by itself besides its `display`. fn implied_layout(component: &str) -> &'static [(&'static str, &'static str)] { match component { + "Flex" => &[("flexDir", "row")], "VStack" => &[("flexDir", "column")], "Center" => &[("alignItems", "center"), ("justifyContent", "center")], _ => &[], @@ -818,7 +749,7 @@ fn restore_implied_display(tree: &mut Tree) { fn merge_trees( by_slot: &BySlot, precedence: &Precedence, - notes: &mut Vec, + context: &mut MergeContext<'_>, ) -> Option { // The merged element is the first width's. Where another width draws it as // a different layout element, what each element does by itself has to be @@ -874,7 +805,7 @@ fn merge_trees( } if let Some(widest) = seen.last().cloned() { if seen.iter().any(|value| *value != widest) { - notes.push(Unrepresented { + context.notes.push(Unrepresented { node_id: first.node_id.clone(), detail: format!( "{} takes {key} {} at different widths; a component prop cannot be responsive, so {widest} is used.", @@ -895,6 +826,28 @@ fn merge_trees( .map_or(Drawn::Unset, |value| Drawn::Set(value)), }); let mut merged = merge_slots(&key, &widths); + for (slot, width) in widths.iter().enumerate() { + if let Drawn::Set(expected) = width + && cleared_when_dropped(&key) + { + let has_value = match &merged { + Merged::Same(value) => value.is_some(), + Merged::Array(values) => values.iter().take(slot + 1).any(Option::is_some), + }; + if !has_value { + context.diagnostics.push(devup_mcp_figma::Diagnostic { + code: "DEVUP_CODEGEN_RESPONSIVE_DRAWN_WIDTH_EMPTY".into(), + message: "A drawn width lost its source layout value during responsive merging.".into(), + node_id: by_slot[slot].as_ref().map(|tree| tree.node_id.clone()), + property: Some(key.clone()), + details: Some(json!({"slot":slot,"width":context.slot_widths[slot],"originalValue":expected,"reason":"no effective layout value at drawn slot","nextAction":"Inspect responsive merge provenance for this node and property.","output":"responsiveTsx"})), + severity: Some(devup_mcp_figma::DiagnosticSeverity::Warning), + fidelity_impact: Some(devup_mcp_figma::FidelityImpact::Lossy), + ..devup_mcp_figma::Diagnostic::default() + }); + } + } + } if key == "display" && let Merged::Array(slots) = &mut merged { @@ -941,7 +894,7 @@ fn merge_trees( }) .collect::>(); - let content = merge_content(by_slot, &first, notes); + let content = merge_content(by_slot, &first, &mut context.notes); // A component reference has nothing below it to line up: each width picks // its own variant, and the component answers for its own widths. @@ -977,10 +930,10 @@ fn merge_trees( Some(Tree { props, - children: merge_children(by_slot, precedence, notes), + children: merge_children(by_slot, precedence, context), source_node_ids, content, slots, ..first }) -} \ No newline at end of file +} diff --git a/crates/devup-mcp-devup-ui/src/codegen/responsive/child_alignment.rs b/crates/devup-mcp-devup-ui/src/codegen/responsive/child_alignment.rs new file mode 100644 index 00000000..7e610467 --- /dev/null +++ b/crates/devup-mcp-devup-ui/src/codegen/responsive/child_alignment.rs @@ -0,0 +1,209 @@ +struct MergeContext<'a> { + snapshot: &'a Snapshot, + notes: Vec, + diagnostics: Vec, + slot_widths: BySlot, +} + +fn role_kind(tree: &Tree) -> &str { + if tree.is_component { + "component" + } else if LAYOUT_ELEMENTS.contains(&tree.component.as_str()) { + "layout" + } else { + &tree.component + } +} + +fn role(tree: &Tree) -> String { + let kind = role_kind(tree); + format!( + "{kind}:{:?}:{:?}:{:?}:{:?}:{:?}:{:?}", + tree.content + .as_deref() + .map(|text| text.replace("
", "")), + tree.props + .get("src") + .or_else(|| tree.props.get("maskImage")), + tree.props.get("srcSet"), + tree.props.get("sizes"), + tree.props.get("width"), + tree.props.get("height") + ) +} + +fn responsive_signature(tree: &Tree) -> String { + format!( + "{}:{}:{:?}:{:?}:{:?}:{:?}:{:?}:{:?}:[{}]", + role_kind(tree), + tree.content.is_some(), + tree.props.get("src"), + tree.props.get("srcSet"), + tree.props.get("sizes"), + tree.props.get("width"), + tree.props.get("height"), + tree.props.get("maskImage"), + tree.children + .iter() + .map(responsive_signature) + .collect::>() + .join("|") + ) +} + +fn component_identity<'a>(snapshot: &'a Snapshot, tree: &Tree, set: bool) -> Option<&'a str> { + let node = snapshot.nodes.get(&tree.node_id)?; + let view = node.typed_view(); + if set { + view.string("mainComponentSetId").or_else(|| { + let master = view + .string("mainComponentId") + .or_else(|| view.string("componentId"))?; + let parent = snapshot + .nodes + .get(master)? + .typed_view() + .string("parentId")?; + (snapshot.nodes.get(parent)?.node_type == "COMPONENT_SET").then_some(parent) + }) + } else { + view.string("mainComponentId") + .or_else(|| view.string("componentId")) + } +} + +fn matches_at(snapshot: &Snapshot, left: &Tree, right: &Tree, pass: usize) -> bool { + if ["src", "srcSet", "sizes", "width", "height", "maskImage"] + .iter() + .any(|key| left.props.get(*key) != right.props.get(*key)) + { + return false; + } + if pass == 2 && role(left) != role(right) { + return false; + } + match pass { + 0 | 1 => component_identity(snapshot, left, pass == 1) + .is_some_and(|id| component_identity(snapshot, right, pass == 1) == Some(id)), + 2 => left.node_name == right.node_name, + _ => { + (left.node_name == right.node_name || role(left) == role(right)) + && responsive_signature(left) == responsive_signature(right) + } + } +} + +type ChildMaps<'a> = BySlot)>>; + +fn align_children<'a>(by_slot: &'a BySlot, context: &mut MergeContext<'_>) -> ChildMaps<'a> { + let mut groups: Vec> = Vec::new(); + let mut maps: ChildMaps<'a> = + std::array::from_fn(|slot| by_slot[slot].as_ref().map(|_| Vec::new())); + for slot in 0..SLOTS { + let Some(parent) = &by_slot[slot] else { + continue; + }; + let mut assigned = vec![None; parent.children.len()]; + for pass in 0..4 { + let candidates = parent + .children + .iter() + .enumerate() + .map(|(index, child)| { + if assigned[index].is_some() { + return Vec::new(); + } + groups + .iter() + .enumerate() + .filter(|(_, group)| { + group[slot].is_none() + && group + .iter() + .flatten() + .any(|other| matches_at(context.snapshot, child, other, pass)) + }) + .map(|(index, _)| index) + .collect::>() + }) + .collect::>(); + for (index, choices) in candidates.iter().enumerate() { + if let [group] = choices.as_slice() + && candidates + .iter() + .filter(|choices| choices.contains(group)) + .count() + == 1 + { + assigned[index] = Some(*group); + groups[*group][slot] = Some(&parent.children[index]); + } + } + if pass == 2 { + for (index, choices) in candidates.iter().enumerate() { + if assigned[index].is_some() || choices.is_empty() { + continue; + } + let peers = candidates + .iter() + .enumerate() + .filter(|(other, candidate)| { + assigned[*other].is_none() && *candidate == choices + }) + .map(|(index, _)| index) + .collect::>(); + let available = choices + .iter() + .filter(|group| groups[**group][slot].is_none()) + .copied() + .collect::>(); + if peers.len() == available.len() { + for (child, group) in peers.into_iter().zip(available) { + assigned[child] = Some(group); + groups[group][slot] = Some(&parent.children[child]); + } + } + } + } + } + for (index, child) in parent.children.iter().enumerate() { + let group = match assigned[index] { + Some(group) => group, + None => { + let candidates = groups + .iter() + .filter(|group| { + group[slot].is_none() + && group.iter().flatten().any(|other| { + (0..4).any(|pass| { + matches_at(context.snapshot, child, other, pass) + }) + }) + }) + .collect::>(); + if !candidates.is_empty() { + let ids = candidates.iter().flat_map(|group| group.iter().enumerate().filter_map(|(slot, tree)| tree.map(|tree| json!({"slot":slot,"nodeId":tree.node_id,"width":context.slot_widths[slot]})))).chain(std::iter::once(json!({"slot":slot,"nodeId":child.node_id,"width":context.slot_widths[slot]}))).collect::>(); + context.diagnostics.push(devup_mcp_figma::Diagnostic { + code: "DEVUP_CODEGEN_RESPONSIVE_UNMATCHED".into(), + message: "Responsive child identity is ambiguous; separate width-specific nodes were retained.".into(), + node_id: Some(child.node_id.clone()), + details: Some(json!({"nodesByWidth":ids,"reason":"multiple semantic counterparts","nextAction":"Give ambiguous siblings distinct names or collect their mainComponentId/componentSet identity.","output":"responsiveTsx"})), + severity: Some(devup_mcp_figma::DiagnosticSeverity::Warning), + fidelity_impact: Some(devup_mcp_figma::FidelityImpact::Approximated), + ..devup_mcp_figma::Diagnostic::default() + }); + } + let group = groups.len(); + let mut members = [None; SLOTS]; + members[slot] = Some(child); + groups.push(members); + group + } + }; + if let Some(map) = &mut maps[slot] { + map.push((group.to_string(), vec![child])); + } + } + } + maps +} diff --git a/crates/devup-mcp-devup-ui/src/codegen/responsive/impl.rs b/crates/devup-mcp-devup-ui/src/codegen/responsive/impl.rs index e30110e8..31823813 100644 --- a/crates/devup-mcp-devup-ui/src/codegen/responsive/impl.rs +++ b/crates/devup-mcp-devup-ui/src/codegen/responsive/impl.rs @@ -227,6 +227,7 @@ impl WidthCheck<'_> { } for (name, (value, _)) in &theirs { if !names.contains(name) + && !(self.root_fluid && element == 0 && *name == "w") && settled(name, Some(value.as_str()), their_tag) != settled(name, None, tag) { gaps.push( 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..bfa8d9a8 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 @@ -144,10 +144,9 @@ fn is_placement_prop(name: &str) -> bool { fn merge_children( by_slot: &BySlot, precedence: &Precedence, - notes: &mut Vec, + context: &mut MergeContext<'_>, ) -> Vec { - let maps: BySlot)>> = - std::array::from_fn(|slot| by_slot[slot].as_ref().map(children_to_map)); + let maps = align_children(by_slot, context); let orders = maps .iter() .flatten() @@ -212,7 +211,7 @@ fn merge_children( } } } - merged.extend(merge_trees(&children, precedence, notes)); + merged.extend(merge_trees(&children, precedence, context)); } } merged @@ -300,6 +299,7 @@ pub struct Unrepresented { /// A screen's widths, folded into one tree. #[derive(Debug, Clone, PartialEq, Eq)] pub struct MergedScreen { + pub diagnostics: Vec, /// The merged JSX, indented for a component body. pub tsx: String, /// Every element name the JSX mentions, primitives and components alike. @@ -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); } @@ -440,22 +464,40 @@ pub fn merge_breakpoints( } } slots.sort_unstable(); - let mut unrepresented = Vec::new(); - let Some(tree) = merge_trees(&by_slot, &precedence, &mut unrepresented) else { + let mut context = MergeContext { snapshot, notes: Vec::new(), diagnostics: Vec::new(), slot_widths }; + let Some(mut tree) = merge_trees(&by_slot, &precedence, &mut context) else { return Ok(None); }; + // Each width's root is as wide as its frame, so the merged root would + // say `w={['360px', ..., '1920px']}`: the frame widths, not a layout. A + // page or section is a block that already takes its container's width. + let root_fluid = tree + .props + .get("w") + .and_then(|value| slot_values(value)) + .is_some_and(|values| { + values.len() > 1 + && values + .iter() + .flatten() + .all(|value| value.ends_with("px")) + }); + if root_fluid { + tree.props.remove("w"); + } let mut components = BTreeSet::new(); collect_components(&tree, &mut components); let tsx = render_merged(&tree, 2); - let (contract, gaps) = verify_merge(snapshot, options, &roots, &tree, &tsx); + let (contract, gaps) = verify_merge(snapshot, options, &roots, &tree, &tsx, root_fluid); let elements = merged_elements(&tree, &slot_widths); Ok(Some(MergedScreen { + diagnostics: context.diagnostics, elements, verified: gaps.is_empty() && contract.iter().all(|entry| entry["verified"] == true), tsx, components, slots, - unrepresented, + unrepresented: context.notes, contract, gaps, })) @@ -725,6 +767,7 @@ fn verify_merge( roots: &BySlot, tree: &Tree, tsx: &str, + root_fluid: bool, ) -> (Vec, Vec) { let attributes = generated_attributes(tsx); let tags = element_tags(tsx); @@ -754,6 +797,7 @@ fn verify_merge( tags: &tags, attributes: &attributes, by_element: &by_element, + root_fluid, }; check.run(&owned_elements(&output), &mut ok, &mut gaps); } @@ -801,4 +845,5 @@ struct WidthCheck<'a> { tags: &'a [String], attributes: &'a [GeneratedAttribute], by_element: &'a [Vec], -} \ No newline at end of file + root_fluid: bool, +} 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/codegen/style.rs b/crates/devup-mcp-devup-ui/src/codegen/style.rs index 490c6ae5..ab58d29d 100644 --- a/crates/devup-mcp-devup-ui/src/codegen/style.rs +++ b/crates/devup-mcp-devup-ui/src/codegen/style.rs @@ -1,4 +1,5 @@ // Source sections share this module to preserve privacy and test names. include!("style/style_properties.rs"); +include!("style/asset_kind.rs"); include!("style/image_fill_source.rs"); include!("style/push_effects.rs"); diff --git a/crates/devup-mcp-devup-ui/src/codegen/style/asset_kind.rs b/crates/devup-mcp-devup-ui/src/codegen/style/asset_kind.rs new file mode 100644 index 00000000..4f700e91 --- /dev/null +++ b/crates/devup-mcp-devup-ui/src/codegen/style/asset_kind.rs @@ -0,0 +1,10 @@ +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum AssetKind { + Svg, + SvgMask, + Png, +} + +pub(crate) fn asset_kind(snapshot: &Snapshot, node: &RawNode) -> Option { + asset_kind_nested(snapshot, node, false) +} 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..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,56 +91,121 @@ 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 = view.name().unwrap_or("Asset"); - 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) - && (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, } - 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([':', ';'], "-")) +} + +/// 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('-'); } - Some(variant) => format!("{name}={variant}"), - None => format!("{name}-{}", node.id.replace([':', ';'], "-")), } + slug.truncate(48); + slug.trim_end_matches('-').to_owned() } -/// 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(",")) +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; + } + 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}"); + } + 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 +/// `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(['(', ')'], "") } pub(super) fn paint_css( @@ -196,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. @@ -925,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/src/codegen/style/style_properties.rs b/crates/devup-mcp-devup-ui/src/codegen/style/style_properties.rs index 832f8e1f..fe056a0f 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,24 +1,11 @@ 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}, }; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum AssetKind { - Svg, - 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,11 +74,23 @@ 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); } } + if visible_children.len() >= 2 + && matches!(view.node_type(), "FRAME" | "INSTANCE" | "COMPONENT") + && matches!(view.string("layoutMode"), Some("HORIZONTAL" | "VERTICAL" | "GRID")) + { + return None; + } + visible_children .into_iter() .all(|child| { diff --git a/crates/devup-mcp-devup-ui/src/codegen/text.rs b/crates/devup-mcp-devup-ui/src/codegen/text.rs index d0e14e42..cc265e16 100644 --- a/crates/devup-mcp-devup-ui/src/codegen/text.rs +++ b/crates/devup-mcp-devup-ui/src/codegen/text.rs @@ -63,9 +63,7 @@ pub(super) fn push_text_props( if let Some(weight) = value("fontWeight").and_then(Value::as_f64) { string_prop(props, "fontWeight", format_number(weight)); } - if typography.is_none() - && let Some(letter_spacing) = letter_spacing(value("letterSpacing")) - { + if let Some(letter_spacing) = letter_spacing(value("letterSpacing")) { string_prop(props, "letterSpacing", letter_spacing); } if let Some(line_height) = line_height( @@ -168,39 +166,146 @@ pub(super) fn push_text_props( } fn elide_typography_duplicates(props: &mut Vec, typography: &Value) { - // A responsive property can be omitted only when every effective slot - // defines that same value, including the first width. Never select an - // active viewport from a text node's own width. - let values = match typography { - Value::Object(value) => vec![value], - Value::Array(slots) if slots.first().is_some_and(Value::is_object) => { - let Some(values) = slots - .iter() - .filter(|slot| !slot.is_null()) - .map(Value::as_object) - .collect::>>() - else { - return; - }; - values - } - _ => return, + elide_metrics(props, typography, &BTreeMap::new()); +} + +/// The metrics a `typography` token already sets. +/// +/// A text with a text style applied reports the style's own metrics as they +/// resolve in the mode of the frame it is drawn in: a desktop frame says +/// `40px` for an `h2` that is `28px` on mobile. Figma detaches the style +/// when one of its metrics is edited, so a value the token defines at any +/// width is the token speaking, not an override. Writing it beside +/// `typography` pins every width to the drawn one and breaks the token's +/// responsive values; it is left out. A value the token defines at no width +/// is a real override and stays. A responsive value (a length token) stays +/// only where it differs from the token width by width. +fn elide_metrics( + props: &mut Vec, + typography: &Value, + lengths: &BTreeMap, +) { + const METRICS: [&str; 4] = ["fontSize", "fontWeight", "lineHeight", "letterSpacing"]; + let Some(slots) = typography_slots(typography) else { + return; }; props.retain(|(name, PropValue::String(value))| { - if !matches!(name.as_str(), "fontSize" | "fontWeight" | "lineHeight") { + if !METRICS.contains(&name.as_str()) { return true; } - !values.iter().all(|values| { - values.get(name).is_some_and(|expected| { - expected.as_str().is_some_and(|expected| expected == value) - || expected - .as_f64() - .is_some_and(|expected| value.parse::().ok() == Some(expected)) + let expected = (0..SERIES) + .map(|slot| { + slots[slot] + .and_then(|slot_values| slot_values.get(name)) + .map(|value| series(&resolve_length(value, lengths))[slot].clone()) + .unwrap_or(Value::Null) }) - }) + .collect::>(); + if expected.iter().all(Value::is_null) { + return true; + } + let written = resolve_length(&Value::String(value.clone()), lengths); + let elided = if written.is_array() { + series(&written) + .iter() + .zip(&expected) + .all(|(written, expected)| same_metric(written, expected)) + } else { + expected + .iter() + .any(|expected| same_metric(&written, expected)) + }; + !elided }); } +const SERIES: usize = 5; + +/// A token's slot objects, the last one carried forward over `null`. +fn typography_slots( + typography: &Value, +) -> Option<[Option<&serde_json::Map>; SERIES]> { + match typography { + Value::Object(value) => Some([Some(value); SERIES]), + Value::Array(slots) if slots.first().is_some_and(Value::is_object) => { + let mut carried = None; + let mut out = [None; SERIES]; + for (slot, entry) in out.iter_mut().enumerate() { + match slots.get(slot) { + Some(Value::Object(value)) => carried = Some(value), + Some(Value::Null) | None => {} + Some(_) => return None, + } + *entry = carried; + } + Some(out) + } + _ => None, + } +} + +/// A value as each slot reads it: a scalar everywhere, an array carried +/// forward over `null`. +fn series(value: &Value) -> Vec { + match value { + Value::Array(values) => { + let mut carried = Value::Null; + (0..SERIES) + .map(|slot| { + if let Some(value) = values.get(slot).filter(|value| !value.is_null()) { + carried = value.clone(); + } + carried.clone() + }) + .collect() + } + value => vec![value.clone(); SERIES], + } +} + +fn resolve_length( + value: &Value, + lengths: &BTreeMap, +) -> Value { + match value { + Value::String(name) if name.starts_with('$') => lengths + .values() + .find(|token| token.token == name[1..]) + .map_or_else(|| value.clone(), |token| token.value.clone()), + _ => value.clone(), + } +} + +fn same_metric(left: &Value, right: &Value) -> bool { + fn text(value: &Value) -> Option { + match value { + Value::String(value) => Some(value.clone()), + Value::Number(value) => Some(value.to_string()), + _ => None, + } + } + let (Some(left), Some(right)) = (text(left), text(right)) else { + return false; + }; + left == right || matches!((left.parse::(), right.parse::()), (Ok(l), Ok(r)) if l == r) +} + +pub(super) fn elide_tokenized_typography( + props: &mut Vec, + typography_values: &BTreeMap, + lengths: &BTreeMap, +) { + let Some(typography) = props.iter().find_map(|(name, PropValue::String(value))| { + (name == "typography") + .then(|| typography_values.get(value)) + .flatten() + }) else { + return; + }; + let typography = typography.clone(); + elide_metrics(props, &typography, lengths); +} + fn is_resolved_value(value: &Value) -> bool { !value.as_object().is_some_and(|object| { ["$unsupported", "$undefined", "$truncated", "$error"] @@ -468,9 +573,7 @@ fn typography_props( if let Some(value) = segment.get("fontWeight").and_then(Value::as_f64) { string_prop(&mut props, "fontWeight", format_number(value)); } - if typography.is_none() - && let Some(value) = letter_spacing(segment.get("letterSpacing")) - { + if let Some(value) = letter_spacing(segment.get("letterSpacing")) { string_prop(&mut props, "letterSpacing", value); } if let Some(value) = line_height( diff --git a/crates/devup-mcp-devup-ui/src/codegen/variant.rs b/crates/devup-mcp-devup-ui/src/codegen/variant.rs index d59663cf..5038f065 100644 --- a/crates/devup-mcp-devup-ui/src/codegen/variant.rs +++ b/crates/devup-mcp-devup-ui/src/codegen/variant.rs @@ -1,3 +1,6 @@ // Source sections share this module to preserve privacy and test names. include!("variant/variant_projection.rs"); +include!("variant/child_keys.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/child_keys.rs b/crates/devup-mcp-devup-ui/src/codegen/variant/child_keys.rs new file mode 100644 index 00000000..6346abd9 --- /dev/null +++ b/crates/devup-mcp-devup-ui/src/codegen/variant/child_keys.rs @@ -0,0 +1,45 @@ +/// How each child is found again in another variant's tree. +/// +/// Addressing a child by its position breaks the moment a variant leaves one +/// out: a `tag` button has no icon, so its text sits where the icon sat and +/// every lookup after that reads the wrong node. A child is identified by its +/// shape where that is unique among its siblings, and by its name where it is +/// not, with the occurrence to tell repeats apart. This is the plugin's +/// `treeChildrenToMap`. +fn child_keys(tree: &Tree) -> Vec<(String, usize)> { + let mut counts: BTreeMap = BTreeMap::new(); + for child in &tree.children { + *counts.entry(structure_signature(child)).or_default() += 1; + } + let mut seen: BTreeMap = BTreeMap::new(); + tree.children + .iter() + .map(|child| { + let signature = structure_signature(child); + let key = if counts.get(&signature) == Some(&1) { + format!("sig:{signature}") + } else { + child.node_name.clone() + }; + let occurrence = seen.entry(key.clone()).or_default(); + let at = *occurrence; + *occurrence += 1; + (key, at) + }) + .collect() +} + +/// Where a node sits, said in a way another variant can follow. +type NodePath = [(String, usize)]; + +/// The name under which a text node's own words are folded like any other +/// value. No CSS property can be called this, so it cannot collide with one. +const CONTENT_FIELD: &str = "\u{1}content"; + +/// What a node says for a field, whether that is a prop or its own words. +fn tree_field(tree: &Tree, field: &str) -> Option { + if field == CONTENT_FIELD { + return tree.content.clone(); + } + tree.props.get(field).cloned() +} 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..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 @@ -104,6 +104,9 @@ fn merge_child_props( Some(&child_path), &child.props, ); + if child.component == "Image" { + child.scalar_sources = scalar_sources(records, definitions, dimensions, viewport, effect_value, &child_path); + } merge_child_props( child, records, @@ -126,8 +129,8 @@ fn changed_effect_props( ) -> BTreeMap { let effect_name = definitions .iter() - .find(|definition| definition.name.eq_ignore_ascii_case("effect")) - .map(|definition| definition.name.clone()); + .find(|definition| definition.name.eq_ignore_ascii_case("effect") && !dimensions.iter().any(|dimension| dimension.name == definition.name)) + .map(|definition| definition.source.clone()); let keys = records .iter() .filter(|record| { @@ -183,8 +186,8 @@ fn expression_for_prop( ) -> Option { let effect_name = definitions .iter() - .find(|definition| definition.name.eq_ignore_ascii_case("effect")) - .map(|definition| definition.name.as_str()); + .find(|definition| definition.name.eq_ignore_ascii_case("effect") && !dimensions.iter().any(|dimension| dimension.name == definition.name)) + .map(|definition| definition.source.as_str()); let dependencies = dimensions .iter() .filter(|dimension| !dimension.boolean) @@ -278,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) }) @@ -359,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() @@ -395,13 +398,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 +429,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(), )); } @@ -482,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) @@ -491,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) @@ -577,11 +581,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 +607,21 @@ fn render_component( }) .collect::>() .join("\n"); + for name in text_properties.keys() { + if !fields.is_empty() { + fields.push('\n'); + } + fields.push_str(&format!(" {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| if definition.boolean { format!("{} = {}", definition.name, definition.default) } else { definition.name.clone() }) + .chain(text_properties.iter().map(|(name, default)| format!("{name} = {default}"))) .collect::>() .join(", "); format!("export function {name}({{ {props} }}: {name}Props)") @@ -624,6 +636,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(); @@ -639,9 +662,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!( @@ -733,10 +761,8 @@ 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::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 @@ -772,23 +798,38 @@ 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::Responsive(slots) => format!( + Expression::Undefined => "undefined".into(), + Expression::Literal(value) => serde_json::to_string(value).unwrap_or_default(), + 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| format!("\"{value}\""))) + .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); 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) @@ -799,7 +840,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/scalar_properties.rs b/crates/devup-mcp-devup-ui/src/codegen/variant/scalar_properties.rs new file mode 100644 index 00000000..dd046c1c --- /dev/null +++ b/crates/devup-mcp-devup-ui/src/codegen/variant/scalar_properties.rs @@ -0,0 +1,103 @@ +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.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| { + 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 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("&"), + "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 new file mode 100644 index 00000000..ad4c65f1 --- /dev/null +++ b/crates/devup-mcp-devup-ui/src/codegen/variant/text_properties.rs @@ -0,0 +1,91 @@ +fn bind_text_properties( + snapshot: &Snapshot, + set: &RawNode, + tree: &mut Tree, + dimensions: &[Definition], +) -> BTreeMap { + let mut used = dimensions.iter().map(|definition| definition.name.clone()) + .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") + { + 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 fallback_index = 0; + 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}}}")); + } 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().rev()); + } + 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 2b3e099b..4bec688a 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, @@ -37,15 +35,14 @@ 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)] struct Record { values: BTreeMap, tree: Tree, node_id: String, } - #[derive(Clone, Debug)] struct Definition { /// The name as it is written in code. @@ -59,9 +56,9 @@ struct Definition { /// which may carry a `#id` suffix that `name` has dropped. source: String, } - #[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 @@ -71,11 +68,11 @@ enum Expression { Variant(String, Vec<(String, Expression)>), Conditional(String, String, String), } - 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,37 +81,72 @@ 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 + let mut records = set .typed_view() .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)| { + 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(), }) }) .collect::, DevupError>>()?; + 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 @@ -131,7 +163,7 @@ pub(super) fn generate_variant_component_set( false, ) })?; - let dimensions = definitions + let mut dimensions = definitions .iter() .filter(|definition| { !matches!( @@ -143,7 +175,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")); @@ -155,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( @@ -167,6 +204,9 @@ pub(super) fn generate_variant_component_set( None, &default.tree.props, ); + 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, @@ -176,6 +216,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(); @@ -194,11 +235,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, &dimensions); let component_name = legacy_component_name(set.typed_view().name().unwrap_or("Component")); let code = mark_node( &set.id, @@ -208,6 +252,7 @@ pub(super) fn generate_variant_component_set( &root, &selectors, transition.as_ref(), + &text_properties, ), ); let mut imports = BTreeSet::new(); @@ -324,6 +369,42 @@ fn is_asset_leaf(tree: &Tree) -> bool { || (tree.component == "Box" && tree.props.contains_key("maskImage"))) } +/// Whether a sibling variant in the set is more than a shape. A header whose +/// mobile variant is vectors only is still a component: flattening it to an +/// `` loses the `
` reference the other widths keep. +fn set_has_non_asset_variant(snapshot: &Snapshot, node: &RawNode) -> bool { + let view = node.typed_view(); + let master = view + .string("mainComponentId") + .or_else(|| view.string("componentId")) + .and_then(|id| snapshot.nodes.get(id)); + let set_id = view + .string("mainComponentSetId") + .or_else(|| master.and_then(|master| master.typed_view().string("parentId"))); + let Some(set) = set_id + .and_then(|id| snapshot.nodes.get(id)) + .filter(|set| set.typed_view().node_type() == "COMPONENT_SET") + else { + // A screen export carries the frames, not the page the set lives on. + // Another width's instance of the same set is the evidence left. + return set_id.is_some_and(|set_id| { + snapshot.nodes.values().any(|other| { + let other_view = other.typed_view(); + other.id != node.id + && other_view.node_type() == "INSTANCE" + && other_view.string("mainComponentSetId") == Some(set_id) + && other_view.string("mainComponentId") != view.string("mainComponentId") + && style::asset_kind(snapshot, other).is_none() + }) + }); + }; + set.typed_view() + .child_ids() + .filter_map(|id| snapshot.nodes.get(id)) + .filter(|variant| master.is_none_or(|master| master.id != variant.id)) + .any(|variant| style::asset_kind(snapshot, variant).is_none()) +} + /// Variant properties a component set uses to describe itself rather than to /// be told something, so an instance must not pass them. /// @@ -359,7 +440,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)| { @@ -441,7 +522,9 @@ fn project_tree_inner( // the spot with a `{/* */}` comment. let mut props = instance_variant_props(&view); let mut expanded = project_tree_inner(snapshot, node, options, is_render_root, false)?; - if is_asset_leaf(&expanded) || style::non_rendering_asset_reason(snapshot, node).is_some() { + if (is_asset_leaf(&expanded) && !set_has_non_asset_variant(snapshot, node)) + || style::non_rendering_asset_reason(snapshot, node).is_some() + { // Spelling the shape out loses which component it came from, and // that is the one thing a reader needs to change it in the right // place. The reference leaves the call it declined to write. @@ -477,6 +560,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); @@ -562,6 +646,10 @@ fn project_tree_inner( &options.length_tokens, &mut used_tokens, ); + text::elide_tokenized_typography(&mut props, &options.typography_values, &options.length_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!( @@ -620,6 +708,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)) } @@ -780,6 +869,7 @@ fn merged_props( } } keys.into_iter() + .filter(|prop| prop != "src" || defaults.contains_key("src")) .filter_map(|prop| { let expression = expression_for_prop( records, @@ -793,6 +883,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() @@ -898,49 +991,3 @@ pub(super) fn structure_signature(tree: &Tree) -> String { }; format!("{}::{kind}::{text}::{props}::{children}", tree.component) } - -/// How each child is found again in another variant's tree. -/// -/// Addressing a child by its position breaks the moment a variant leaves one -/// out: a `tag` button has no icon, so its text sits where the icon sat and -/// every lookup after that reads the wrong node. A child is identified by its -/// shape where that is unique among its siblings, and by its name where it is -/// not, with the occurrence to tell repeats apart. This is the plugin's -/// `treeChildrenToMap`. -fn child_keys(tree: &Tree) -> Vec<(String, usize)> { - let mut counts: BTreeMap = BTreeMap::new(); - for child in &tree.children { - *counts.entry(structure_signature(child)).or_default() += 1; - } - let mut seen: BTreeMap = BTreeMap::new(); - tree.children - .iter() - .map(|child| { - let signature = structure_signature(child); - let key = if counts.get(&signature) == Some(&1) { - format!("sig:{signature}") - } else { - child.node_name.clone() - }; - let occurrence = seen.entry(key.clone()).or_default(); - let at = *occurrence; - *occurrence += 1; - (key, at) - }) - .collect() -} - -/// Where a node sits, said in a way another variant can follow. -type NodePath = [(String, usize)]; - -/// The name under which a text node's own words are folded like any other -/// value. No CSS property can be called this, so it cannot collide with one. -const CONTENT_FIELD: &str = "\u{1}content"; - -/// What a node says for a field, whether that is a prop or its own words. -fn tree_field(tree: &Tree, field: &str) -> Option { - if field == CONTENT_FIELD { - return tree.content.clone(); - } - tree.props.get(field).cloned() -} \ No newline at end of file diff --git a/crates/devup-mcp-devup-ui/src/effective_attributes.rs b/crates/devup-mcp-devup-ui/src/effective_attributes.rs new file mode 100644 index 00000000..d5799aeb --- /dev/null +++ b/crates/devup-mcp-devup-ui/src/effective_attributes.rs @@ -0,0 +1,236 @@ +use std::collections::{BTreeMap, BTreeSet}; + +use oxc_ast::ast::*; +use oxc_ast_visit::{Visit, walk}; + +#[derive(Default, PartialEq, Eq)] +pub struct AttributeValue { + pub literal: Option, + pub boolean: Option, + pub identifier: Option, +} + +#[derive(Default)] +pub struct EffectiveAttributes { + pub values: BTreeMap, + pub transition: bool, + pub unresolved_spread: bool, +} + +impl EffectiveAttributes { + pub fn contains(&self, name: &str) -> bool { + self.values.contains_key(name) + } + + pub fn literal(&self, name: &str) -> Option { + self.values.get(name)?.literal.clone() + } + + pub fn boolean(&self, name: &str) -> Option { + self.values.get(name)?.boolean + } + + pub fn rendered_as(&self) -> Option { + let value = self.values.get("as")?; + value.literal.clone().or_else(|| value.identifier.clone()) + } + + fn expression(&mut self, expression: &Expression<'_>) { + match expression.get_inner_expression() { + Expression::ObjectExpression(object) => { + for property in &object.properties { + match property { + ObjectPropertyKind::ObjectProperty(property) => { + if let Some(name) = property.key.static_name() { + let name = name.into_owned(); + if name == "props" { + self.expression(&property.value); + } else { + self.values.insert(name, expression_value(&property.value)); + } + } + } + ObjectPropertyKind::SpreadProperty(spread) => { + self.expression(&spread.argument) + } + } + } + } + Expression::ConditionalExpression(conditional) => { + let mut consequent = Self::default(); + consequent.expression(&conditional.consequent); + let mut alternate = Self::default(); + alternate.expression(&conditional.alternate); + self.unresolved_spread |= + consequent.unresolved_spread || alternate.unresolved_spread; + for (name, value) in consequent.values.iter_mut() { + if alternate.values.get(name) != Some(value) { + *value = AttributeValue::default(); + } + } + for name in alternate.values.into_keys() { + consequent.values.entry(name).or_default(); + } + self.values.extend(consequent.values); + } + Expression::LogicalExpression(logical) => { + self.expression(&logical.left); + self.expression(&logical.right); + } + _ => self.unresolved_spread = true, + } + } +} + +fn expression_value(expression: &Expression<'_>) -> AttributeValue { + match expression.get_inner_expression() { + Expression::StringLiteral(value) => AttributeValue { + literal: Some(value.value.to_string()), + boolean: match value.value.as_str() { + "true" => Some(true), + "false" => Some(false), + _ => None, + }, + ..Default::default() + }, + Expression::BooleanLiteral(value) => AttributeValue { + boolean: Some(value.value), + ..Default::default() + }, + Expression::Identifier(value) => AttributeValue { + identifier: Some(value.name.to_string()), + ..Default::default() + }, + _ => AttributeValue::default(), + } +} + +#[derive(Default)] +struct TransitionReader { + found: bool, +} + +impl<'a> Visit<'a> for TransitionReader { + fn visit_object_property(&mut self, property: &ObjectProperty<'a>) { + self.found |= property + .key + .static_name() + .is_some_and(|name| name.starts_with("transition")); + walk::walk_object_property(self, property); + } +} + +#[derive(Default)] +pub struct AttributeContext { + pub link_names: BTreeSet, + transition_classes: BTreeSet, +} + +impl AttributeContext { + pub fn from_program(program: &Program<'_>) -> Self { + let mut context = Self::default(); + for statement in &program.body { + if let Statement::ImportDeclaration(import) = statement + && import.source.value == "next/link" + { + for specifier in import.specifiers.iter().flatten() { + if let ImportDeclarationSpecifier::ImportDefaultSpecifier(specifier) = specifier + { + context.link_names.insert(specifier.local.name.to_string()); + } + } + } + if let Statement::VariableDeclaration(declaration) = statement { + for variable in &declaration.declarations { + if let BindingPattern::BindingIdentifier(id) = &variable.id + && let Some(expression) = &variable.init + && css_transition(expression) + { + context.transition_classes.insert(id.name.to_string()); + } + } + } + } + context + } + + pub fn read(&self, opening: &JSXOpeningElement<'_>) -> EffectiveAttributes { + let mut result = EffectiveAttributes::default(); + for item in &opening.attributes { + match item { + JSXAttributeItem::SpreadAttribute(spread) => result.expression(&spread.argument), + JSXAttributeItem::Attribute(attribute) => { + let JSXAttributeName::Identifier(name) = &attribute.name else { + continue; + }; + let name = name.name.as_str(); + result.transition |= name.starts_with("transition"); + let value = match &attribute.value { + None => AttributeValue { + boolean: Some(true), + ..Default::default() + }, + Some(JSXAttributeValue::StringLiteral(value)) => AttributeValue { + literal: Some(value.value.to_string()), + boolean: match value.value.as_str() { + "true" => Some(true), + "false" => Some(false), + _ => None, + }, + ..Default::default() + }, + Some(JSXAttributeValue::ExpressionContainer(container)) => { + let Some(expression) = container.expression.as_expression() else { + continue; + }; + if name == "props" { + result.expression(expression); + } + if name == "className" { + result.transition |= css_transition(expression) + || matches!(expression.get_inner_expression(), Expression::Identifier(id) if self.transition_classes.contains(id.name.as_str())); + } else { + let mut reader = TransitionReader::default(); + reader.visit_expression(expression); + result.transition |= reader.found; + } + expression_value(expression) + } + _ => AttributeValue::default(), + }; + result.values.insert(name.to_string(), value); + } + } + } + result.transition |= result + .values + .keys() + .any(|name| name.starts_with("transition")); + for item in &opening.attributes { + if let JSXAttributeItem::SpreadAttribute(spread) = item { + let mut reader = TransitionReader::default(); + reader.visit_expression(&spread.argument); + result.transition |= reader.found; + } + } + result + } +} + +fn css_transition(expression: &Expression<'_>) -> bool { + match expression.get_inner_expression() { + Expression::CallExpression(call) if matches!(&call.callee, Expression::Identifier(id) if id.name == "css") => + { + let mut reader = TransitionReader::default(); + reader.visit_call_expression(call); + reader.found + } + Expression::ConditionalExpression(value) => { + css_transition(&value.consequent) || css_transition(&value.alternate) + } + Expression::LogicalExpression(value) => { + css_transition(&value.left) || css_transition(&value.right) + } + _ => false, + } +} diff --git a/crates/devup-mcp-devup-ui/src/lib.rs b/crates/devup-mcp-devup-ui/src/lib.rs index 3cea9257..f6fa1ac9 100644 --- a/crates/devup-mcp-devup-ui/src/lib.rs +++ b/crates/devup-mcp-devup-ui/src/lib.rs @@ -1,4 +1,5 @@ pub mod codegen; +pub mod effective_attributes; mod html_props; pub mod provenance; pub mod style_props; 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/text/ui_validate-report_hardcoded_value_Violation-63c589c5.md b/crates/devup-mcp-devup-ui/src/text/ui_validate-report_hardcoded_value_Violation-63c589c5.md index 6d06ae77..e6f40d13 100644 --- a/crates/devup-mcp-devup-ui/src/text/ui_validate-report_hardcoded_value_Violation-63c589c5.md +++ b/crates/devup-mcp-devup-ui/src/text/ui_validate-report_hardcoded_value_Violation-63c589c5.md @@ -1 +1 @@ -{prop_name} uses hardcoded {kind} {text}, which this project's devup.json already defines as {named}. Check the matching modes before replacement; a partial-mode match may change this value in another mode. \ No newline at end of file +{prop_name} uses hardcoded {kind} {text}, which this project's devup.json already defines as {named}. \ No newline at end of file diff --git a/crates/devup-mcp-devup-ui/src/theme/devup_json.rs b/crates/devup-mcp-devup-ui/src/theme/devup_json.rs index 8937c39b..a6cbb71b 100644 --- a/crates/devup-mcp-devup-ui/src/theme/devup_json.rs +++ b/crates/devup-mcp-devup-ui/src/theme/devup_json.rs @@ -1,3 +1,5 @@ // Source sections share this module to preserve privacy and test names. include!("devup_json/theme_generation.rs"); +include!("devup_json/typography_values.rs"); include!("devup_json/responsive_entry.rs"); +include!("devup_json/mode_projection.rs"); diff --git a/crates/devup-mcp-devup-ui/src/theme/devup_json/mode_projection.rs b/crates/devup-mcp-devup-ui/src/theme/devup_json/mode_projection.rs new file mode 100644 index 00000000..a6fa63f4 --- /dev/null +++ b/crates/devup-mcp-devup-ui/src/theme/devup_json/mode_projection.rs @@ -0,0 +1,156 @@ +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ThemeModeDropped { + pub collection_id: String, + pub mode_id: String, + pub mode_name: String, + pub reason: String, + pub mapped_to: Option, +} + +fn mode_meaning(name: &str) -> String { + let normalized = normalize_token(name); + let lower = normalized.to_ascii_lowercase(); + if lower.ends_with("dark") { + "dark".into() + } else if lower.ends_with("light") { + "light".into() + } else { + normalized + } +} + +fn mode_projection( + snapshot: &VariableSnapshot, + candidates: &[VariableCandidate<'_>], +) -> (BTreeMap<(String, String), String>, Vec) { + let remote_ids = snapshot + .used_remote_variables + .iter() + .map(|variable| variable.id.as_str()) + .collect::>(); + let variables = snapshot + .variables + .iter() + .chain(&snapshot.used_remote_variables) + .map(|variable| (variable.id.as_str(), variable)) + .collect(); + let mut reference_snapshot = snapshot.clone(); + reference_snapshot.used_variable_ids.extend( + candidates + .iter() + .filter(|candidate| !remote_ids.contains(candidate.variable.id.as_str())) + .map(|candidate| candidate.variable.id.clone()), + ); + let referenced = selected_variable_ids(&reference_snapshot, ThemeScope::Node, &variables) + .unwrap_or_default(); + let eligible = |collection: &VariableCollection| { + let local = candidates.iter().any(|candidate| { + candidate.variable.variable_collection_id == collection.id + && !remote_ids.contains(candidate.variable.id.as_str()) + }); + local + || candidates.iter().any(|candidate| { + candidate.variable.variable_collection_id == collection.id + && referenced.contains(&candidate.variable.id) + }) + }; + let default = if snapshot + .collections + .iter() + .filter(|collection| eligible(collection)) + .flat_map(|collection| &collection.modes) + .any(|mode| mode_meaning(&mode.name) == "light") + { + "light" + } else { + "default" + }; + let mut mapping = BTreeMap::new(); + let mut dropped = Vec::new(); + for collection in &snapshot.collections { + for mode in &collection.modes { + let name = mode_meaning(&mode.name); + let single_default = collection.modes.len() == 1 + && mode.mode_id == collection.default_mode_id + && matches!( + name.to_ascii_lowercase().as_str(), + "mode1" | "value" | "default" + ); + let (target, reason) = if !candidates + .iter() + .any(|candidate| candidate.variable.variable_collection_id == collection.id) + { + (None, Some("no-defined-color-values")) + } else if !eligible(collection) { + (None, Some("unreferenced-remote-collection")) + } else if single_default { + ( + Some(default.to_owned()), + (name != default).then_some("single-default-mode"), + ) + } else { + ( + Some(name.clone()), + (name != normalize_token(&mode.name)).then_some("merged-by-mode-meaning"), + ) + }; + if let Some(reason) = reason { + dropped.push(ThemeModeDropped { + collection_id: collection.id.clone(), + mode_id: mode.mode_id.clone(), + mode_name: mode.name.clone(), + reason: reason.into(), + mapped_to: target.clone(), + }); + } + if let Some(target) = target { + mapping.insert((collection.id.clone(), mode.mode_id.clone()), target); + } + } + } + (mapping, dropped) +} + +fn record_empty_color_modes( + snapshot: &VariableSnapshot, + mapping: &BTreeMap<(String, String), String>, + color_modes: &BTreeSet<(String, String)>, +) -> Vec { + snapshot + .collections + .iter() + .flat_map(|collection| { + collection.modes.iter().filter_map(|mode| { + let key = (collection.id.clone(), mode.mode_id.clone()); + (mapping.contains_key(&key) && !color_modes.contains(&key)).then(|| { + ThemeModeDropped { + collection_id: collection.id.clone(), + mode_id: mode.mode_id.clone(), + mode_name: mode.name.clone(), + reason: "no-defined-color-values".into(), + mapped_to: None, + } + }) + }) + }) + .collect() +} + +fn color_schemes(theme_names: &[String]) -> Value { + Value::Object( + theme_names + .iter() + .map(|name| { + ( + name.clone(), + json!(if name.to_ascii_lowercase().contains("dark") { + "dark" + } else { + "light" + }), + ) + }) + .collect(), + ) +} diff --git a/crates/devup-mcp-devup-ui/src/theme/devup_json/theme_generation.rs b/crates/devup-mcp-devup-ui/src/theme/devup_json/theme_generation.rs index 25fcf366..68c8301c 100644 --- a/crates/devup-mcp-devup-ui/src/theme/devup_json/theme_generation.rs +++ b/crates/devup-mcp-devup-ui/src/theme/devup_json/theme_generation.rs @@ -104,6 +104,8 @@ pub struct ThemeOutput { pub unresolved_variables: Vec, #[serde(default)] pub skipped_styles: Vec, + #[serde(default)] + pub theme_modes_dropped: Vec, pub source_map: SourceMap, } @@ -232,9 +234,11 @@ pub fn generate_devup_json( variable_candidates.retain(|candidate| selected.contains(&candidate.variable.id)); } let variable_count = variable_candidates.len(); + let (mode_mapping, mut theme_modes_dropped) = mode_projection(snapshot, &variable_candidates); let mut projected = BTreeMap::<(String, String, String), Vec>::new(); let mut colors: BTreeMap> = BTreeMap::new(); let mut lengths: BTreeMap> = BTreeMap::new(); + let mut color_modes = BTreeSet::new(); for candidate in variable_candidates { let variable = candidate.variable; @@ -256,6 +260,13 @@ pub fn generate_devup_json( }); continue; }; + if !collection + .modes + .iter() + .any(|mode| mode_mapping.contains_key(&(collection.id.clone(), mode.mode_id.clone()))) + { + continue; + } let token = variable_token( &variable.name, variable.code_syntax.get("WEB").map(String::as_str), @@ -319,7 +330,12 @@ pub fn generate_devup_json( diagnostics.push(alias_diagnostic(variable)); continue; }; - let mode_name = normalize_token(&mode.name); + let Some(mode_name) = mode_mapping + .get(&(collection.id.clone(), mode.mode_id.clone())) + .cloned() + else { + continue; + }; let projection = match variable.resolved_type.as_str() { "COLOR" => color_value(value).map(|color| ("color", Value::String(color))), "FLOAT" => value @@ -328,6 +344,9 @@ pub fn generate_devup_json( _ => None, }; if let Some((kind, value)) = projection { + if kind == "color" { + color_modes.insert((collection.id.clone(), mode.mode_id.clone())); + } projected .entry((kind.to_owned(), mode_name, token.clone())) .or_default() @@ -573,13 +592,18 @@ pub fn generate_devup_json( .modes .iter() .find(|mode| mode.mode_id == collection.default_mode_id) + && let Some(name) = + mode_mapping.get(&(collection.id.clone(), default.mode_id.clone())) { - push(normalize_token(&default.name)); + push(name.clone()); } } for collection in collections.values() { for mode in &collection.modes { - push(normalize_token(&mode.name)); + if let Some(name) = mode_mapping.get(&(collection.id.clone(), mode.mode_id.clone())) + { + push(name.clone()); + } } } order @@ -598,6 +622,11 @@ pub fn generate_devup_json( } Value::Object(ordered) }; + theme_modes_dropped.extend(record_empty_color_modes( + snapshot, + &mode_mapping, + &color_modes, + )); let colors = in_mode_order(colors); // Lengths and shadows do not vary by colour theme, but devup-ui keys them // by one, so each colour theme gets a copy — the plugin replicates them @@ -695,6 +724,7 @@ pub fn generate_devup_json( theme.insert("typography".to_owned(), Value::Object(typography)); theme.insert("length".to_owned(), lengths); theme.insert("shadow".to_owned(), Value::Object(shadows)); + theme.insert("colorScheme".to_owned(), color_schemes(&theme_names)); let mut root = Map::new(); root.insert("theme".to_owned(), Value::Object(theme)); let mut output = serde_json::to_string_pretty(&Value::Object(root)).map_err(|_| { @@ -740,6 +770,7 @@ pub fn generate_devup_json( conflicts, unresolved_variables, skipped_styles, + theme_modes_dropped, source_map: SourceMap { version: 1, entries: source_entries, @@ -747,225 +778,3 @@ pub fn generate_devup_json( }, }) } - -// A missing size is representable (including percentage line height). An -// unreadable style or malformed metric is not; isolate it rather than invent -// a default typography entry or reject all the other resources. -fn typography_problem(style: &Value) -> Option { - let Some(object) = style.as_object() else { - return Some("style value is not an object".into()); - }; - fn unreadable(value: &Value) -> bool { - match value { - Value::Object(object) => { - ["$unsupported", "$undefined", "$truncated", "$error"] - .iter() - .any(|marker| object.contains_key(*marker)) - || object.values().any(unreadable) - } - Value::Array(values) => values.iter().any(unreadable), - _ => false, - } - } - if unreadable(style) || object.values().any(unreadable) { - return Some("style contains an unreadable field; recollect the style".into()); - } - for metric in ["lineHeight", "letterSpacing"] { - if let Some(value) = object.get(metric).filter(|v| !v.is_null()) { - let unit = value.get("unit").and_then(Value::as_str); - if unit != Some("AUTO") - && (value.get("value").and_then(Value::as_f64).is_none() - || !matches!(unit, Some("PERCENT" | "PIXELS"))) - { - return Some(format!("{metric} has no representable unit/value")); - } - } - } - None -} - -/// A text style's value as devup-ui reads it — the plugin's -/// `textStyleToTypography`. -/// -/// `fontSize` is in pixels; `lineHeight` from a percentage is the whole-pixel -/// advance Figma draws when the size is fixed, and the percentage itself -/// (`150%`) when the size is bound to a variable or missing - CSS scales it -/// with whatever size applies -, pixels as they are, and `normal` when Figma -/// sets it automatically; `letterSpacing` is `em` from a percentage and pixels as -/// they are; the weight is read off the font style's name; and a field the -/// style binds to a variable is that variable's token. Two departures from -/// the plugin, on purpose: `Bold Italic` is `700` and italic, where the -/// plugin reads the weight off the whole style name and gets `400`; and a -/// text case is the CSS `text-transform` value — `uppercase`, not `upper`. -fn typography_value(style: &Value, variable_names: &BTreeMap<&str, String>) -> Value { - let mut entry = Map::new(); - let font = style.get("fontName"); - let family = font - .and_then(|font| font.get("family")) - .and_then(Value::as_str); - let face = font - .and_then(|font| font.get("style")) - .and_then(Value::as_str) - .unwrap_or("Regular"); - if let Some(family) = family { - entry.insert("fontFamily".to_owned(), Value::String(family.to_owned())); - } - if face.contains("Italic") { - entry.insert("fontStyle".to_owned(), Value::String("italic".to_owned())); - } - entry.insert("fontWeight".to_owned(), Value::from(font_weight(face))); - if let Some(size) = style.get("fontSize").and_then(Value::as_f64) { - entry.insert("fontSize".to_owned(), Value::String(format_px(size))); - } - match style.get("textDecoration").and_then(Value::as_str) { - Some("UNDERLINE") => { - entry.insert( - "textDecoration".to_owned(), - Value::String("underline".to_owned()), - ); - } - Some("STRIKETHROUGH") => { - entry.insert( - "textDecoration".to_owned(), - Value::String("line-through".to_owned()), - ); - } - _ => {} - } - let transform = match style.get("textCase").and_then(Value::as_str) { - Some("UPPER") => Some("uppercase"), - Some("LOWER") => Some("lowercase"), - Some("TITLE") => Some("capitalize"), - _ => None, - }; - if let Some(transform) = transform { - entry.insert( - "textTransform".to_owned(), - Value::String(transform.to_owned()), - ); - } - if let Some(line_height) = style.get("lineHeight") { - let unit = line_height.get("unit").and_then(Value::as_str); - let value = line_height.get("value").and_then(Value::as_f64); - let written = match (unit, value) { - (Some("AUTO"), _) => Some(Value::String("normal".to_owned())), - (Some("PERCENT"), Some(percent)) => Some(Value::String( - match style - .get("fontSize") - .and_then(Value::as_f64) - .filter(|_| style.pointer("/boundVariables/fontSize").is_none()) - { - Some(size) => format_px((size * percent / 100.0).round()), - None => format_percent(percent), - }, - )), - (Some(_), Some(pixels)) => Some(Value::String(format_px(pixels))), - _ => None, - }; - if let Some(written) = written { - entry.insert("lineHeight".to_owned(), written); - } - } - if let Some(spacing) = style.get("letterSpacing") { - let unit = spacing.get("unit").and_then(Value::as_str); - let value = spacing.get("value").and_then(Value::as_f64); - let written = match (unit, value) { - (Some("PERCENT"), Some(percent)) => Some(format!("{}em", percent.round() / 100.0)), - (Some(_), Some(pixels)) => Some(format_px(pixels)), - _ => None, - }; - if let Some(written) = written { - entry.insert("letterSpacing".to_owned(), Value::String(written)); - } - } - if let Some(bound) = style.get("boundVariables").and_then(Value::as_object) { - for field in [ - "fontFamily", - "fontSize", - "fontStyle", - "fontWeight", - "letterSpacing", - "lineHeight", - ] { - if let Some(token) = bound - .get(field) - .and_then(|alias| alias.get("id")) - .and_then(Value::as_str) - .and_then(|id| variable_names.get(id)) - { - entry.insert(field.to_owned(), Value::String(format!("${token}"))); - } - } - } - Value::Object(entry) -} - -/// The weight a font style's name means — the plugin's `getFontWeight`, -/// with `Italic` set aside first so `Bold Italic` is still bold. -fn font_weight(face: &str) -> u32 { - let name = face - .replace("Italic", "") - .replace([' ', '-', '_'], "") - .to_ascii_lowercase(); - match name.as_str() { - "thin" | "hairline" => 100, - "extralight" | "ultralight" => 200, - "light" => 300, - "" | "normal" | "regular" | "book" => 400, - "medium" => 500, - "semibold" | "demibold" => 600, - "bold" => 700, - "extrabold" | "ultrabold" => 800, - "black" | "heavy" => 900, - other => match other.parse::() { - Ok(number) if (1..=9).contains(&number) => number * 100, - Ok(number) => number, - Err(_) => 400, - }, - } -} - -/// An effect style's visible shadows as one CSS `box-shadow` — the plugin's -/// `effectStyleToCssShadow`. `None` when it casts none. -fn shadow_value(effects: &Value) -> Option { - let parts = effects - .as_array()? - .iter() - .filter(|effect| effect.get("visible").and_then(Value::as_bool) != Some(false)) - .filter_map(|effect| { - let kind = effect.get("type").and_then(Value::as_str)?; - let inset = match kind { - "DROP_SHADOW" => "", - "INNER_SHADOW" => "inset ", - _ => return None, - }; - let offset = effect.get("offset"); - let x = offset - .and_then(|offset| offset.get("x")) - .and_then(Value::as_f64) - .unwrap_or(0.0); - let y = offset - .and_then(|offset| offset.get("y")) - .and_then(Value::as_f64) - .unwrap_or(0.0); - let radius = effect.get("radius").and_then(Value::as_f64).unwrap_or(0.0); - let spread = effect.get("spread").and_then(Value::as_f64).unwrap_or(0.0); - let color = color_value(effect.get("color")?)?; - let length = |value: f64| { - if value == 0.0 { - "0".to_owned() - } else { - format_px(value) - } - }; - Some(format!( - "{inset}{} {} {} {} {color}", - length(x), - length(y), - length(radius), - length(spread) - )) - }) - .collect::>(); - (!parts.is_empty()).then(|| parts.join(", ")) -} \ No newline at end of file diff --git a/crates/devup-mcp-devup-ui/src/theme/devup_json/typography_values.rs b/crates/devup-mcp-devup-ui/src/theme/devup_json/typography_values.rs new file mode 100644 index 00000000..a16364ef --- /dev/null +++ b/crates/devup-mcp-devup-ui/src/theme/devup_json/typography_values.rs @@ -0,0 +1,221 @@ +// A missing size is representable (including percentage line height). An +// unreadable style or malformed metric is not; isolate it rather than invent +// a default typography entry or reject all the other resources. +fn typography_problem(style: &Value) -> Option { + let Some(object) = style.as_object() else { + return Some("style value is not an object".into()); + }; + fn unreadable(value: &Value) -> bool { + match value { + Value::Object(object) => { + ["$unsupported", "$undefined", "$truncated", "$error"] + .iter() + .any(|marker| object.contains_key(*marker)) + || object.values().any(unreadable) + } + Value::Array(values) => values.iter().any(unreadable), + _ => false, + } + } + if unreadable(style) || object.values().any(unreadable) { + return Some("style contains an unreadable field; recollect the style".into()); + } + for metric in ["lineHeight", "letterSpacing"] { + if let Some(value) = object.get(metric).filter(|v| !v.is_null()) { + let unit = value.get("unit").and_then(Value::as_str); + if unit != Some("AUTO") + && (value.get("value").and_then(Value::as_f64).is_none() + || !matches!(unit, Some("PERCENT" | "PIXELS"))) + { + return Some(format!("{metric} has no representable unit/value")); + } + } + } + None +} + +/// A text style's value as devup-ui reads it — the plugin's +/// `textStyleToTypography`. +/// +/// `fontSize` is in pixels; `lineHeight` from a percentage is the whole-pixel +/// advance Figma draws when the size is fixed, and the percentage itself +/// (`150%`) when the size is bound to a variable or missing - CSS scales it +/// with whatever size applies -, pixels as they are, and `normal` when Figma +/// sets it automatically; `letterSpacing` is `em` from a percentage and pixels as +/// they are; the weight is read off the font style's name; and a field the +/// style binds to a variable is that variable's token. Two departures from +/// the plugin, on purpose: `Bold Italic` is `700` and italic, where the +/// plugin reads the weight off the whole style name and gets `400`; and a +/// text case is the CSS `text-transform` value — `uppercase`, not `upper`. +fn typography_value(style: &Value, variable_names: &BTreeMap<&str, String>) -> Value { + let mut entry = Map::new(); + let font = style.get("fontName"); + let family = font + .and_then(|font| font.get("family")) + .and_then(Value::as_str); + let face = font + .and_then(|font| font.get("style")) + .and_then(Value::as_str) + .unwrap_or("Regular"); + if let Some(family) = family { + entry.insert("fontFamily".to_owned(), Value::String(family.to_owned())); + } + if face.contains("Italic") { + entry.insert("fontStyle".to_owned(), Value::String("italic".to_owned())); + } + entry.insert("fontWeight".to_owned(), Value::from(font_weight(face))); + if let Some(size) = style.get("fontSize").and_then(Value::as_f64) { + entry.insert("fontSize".to_owned(), Value::String(format_px(size))); + } + match style.get("textDecoration").and_then(Value::as_str) { + Some("UNDERLINE") => { + entry.insert( + "textDecoration".to_owned(), + Value::String("underline".to_owned()), + ); + } + Some("STRIKETHROUGH") => { + entry.insert( + "textDecoration".to_owned(), + Value::String("line-through".to_owned()), + ); + } + _ => {} + } + let transform = match style.get("textCase").and_then(Value::as_str) { + Some("UPPER") => Some("uppercase"), + Some("LOWER") => Some("lowercase"), + Some("TITLE") => Some("capitalize"), + _ => None, + }; + if let Some(transform) = transform { + entry.insert( + "textTransform".to_owned(), + Value::String(transform.to_owned()), + ); + } + if let Some(line_height) = style.get("lineHeight") { + let unit = line_height.get("unit").and_then(Value::as_str); + let value = line_height.get("value").and_then(Value::as_f64); + let written = match (unit, value) { + (Some("AUTO"), _) => Some(Value::String("normal".to_owned())), + (Some("PERCENT"), Some(percent)) => Some(Value::String( + match style + .get("fontSize") + .and_then(Value::as_f64) + .filter(|_| style.pointer("/boundVariables/fontSize").is_none()) + { + Some(size) => format_px((size * percent / 100.0).round()), + None => format_percent(percent), + }, + )), + (Some(_), Some(pixels)) => Some(Value::String(format_px(pixels))), + _ => None, + }; + if let Some(written) = written { + entry.insert("lineHeight".to_owned(), written); + } + } + if let Some(spacing) = style.get("letterSpacing") { + let unit = spacing.get("unit").and_then(Value::as_str); + let value = spacing.get("value").and_then(Value::as_f64); + let written = match (unit, value) { + (Some("PERCENT"), Some(percent)) => Some(format!("{}em", percent.round() / 100.0)), + (Some(_), Some(pixels)) => Some(format_px(pixels)), + _ => None, + }; + if let Some(written) = written { + entry.insert("letterSpacing".to_owned(), Value::String(written)); + } + } + if let Some(bound) = style.get("boundVariables").and_then(Value::as_object) { + for field in [ + "fontFamily", + "fontSize", + "fontStyle", + "fontWeight", + "letterSpacing", + "lineHeight", + ] { + if let Some(token) = bound + .get(field) + .and_then(|alias| alias.get("id")) + .and_then(Value::as_str) + .and_then(|id| variable_names.get(id)) + { + entry.insert(field.to_owned(), Value::String(format!("${token}"))); + } + } + } + Value::Object(entry) +} + +/// The weight a font style's name means — the plugin's `getFontWeight`, +/// with `Italic` set aside first so `Bold Italic` is still bold. +fn font_weight(face: &str) -> u32 { + let name = face + .replace("Italic", "") + .replace([' ', '-', '_'], "") + .to_ascii_lowercase(); + match name.as_str() { + "thin" | "hairline" => 100, + "extralight" | "ultralight" => 200, + "light" => 300, + "" | "normal" | "regular" | "book" => 400, + "medium" => 500, + "semibold" | "demibold" => 600, + "bold" => 700, + "extrabold" | "ultrabold" => 800, + "black" | "heavy" => 900, + other => match other.parse::() { + Ok(number) if (1..=9).contains(&number) => number * 100, + Ok(number) => number, + Err(_) => 400, + }, + } +} + +/// An effect style's visible shadows as one CSS `box-shadow` — the plugin's +/// `effectStyleToCssShadow`. `None` when it casts none. +fn shadow_value(effects: &Value) -> Option { + let parts = effects + .as_array()? + .iter() + .filter(|effect| effect.get("visible").and_then(Value::as_bool) != Some(false)) + .filter_map(|effect| { + let kind = effect.get("type").and_then(Value::as_str)?; + let inset = match kind { + "DROP_SHADOW" => "", + "INNER_SHADOW" => "inset ", + _ => return None, + }; + let offset = effect.get("offset"); + let x = offset + .and_then(|offset| offset.get("x")) + .and_then(Value::as_f64) + .unwrap_or(0.0); + let y = offset + .and_then(|offset| offset.get("y")) + .and_then(Value::as_f64) + .unwrap_or(0.0); + let radius = effect.get("radius").and_then(Value::as_f64).unwrap_or(0.0); + let spread = effect.get("spread").and_then(Value::as_f64).unwrap_or(0.0); + let color = color_value(effect.get("color")?)?; + let length = |value: f64| { + if value == 0.0 { + "0".to_owned() + } else { + format_px(value) + } + }; + Some(format!( + "{inset}{} {} {} {} {color}", + length(x), + length(y), + length(radius), + length(spread) + )) + }) + .collect::>(); + (!parts.is_empty()).then(|| parts.join(", ")) +} diff --git a/crates/devup-mcp-devup-ui/src/theme/usage.rs b/crates/devup-mcp-devup-ui/src/theme/usage.rs index 577a70c8..07486a90 100644 --- a/crates/devup-mcp-devup-ui/src/theme/usage.rs +++ b/crates/devup-mcp-devup-ui/src/theme/usage.rs @@ -149,6 +149,7 @@ pub fn generate_used_devup_json( selected .used_remote_variables .retain(|variable| ids.contains(&variable.id)); + selected.used_variable_ids = ids.into_iter().collect(); let mut output = super::generate_devup_json(&selected, ThemeScope::File)?; restrict_devup_json(&mut output, usage)?; Ok(output) diff --git a/crates/devup-mcp-devup-ui/src/ui_layout.rs b/crates/devup-mcp-devup-ui/src/ui_layout.rs index 2d7e65ab..49ee24a3 100644 --- a/crates/devup-mcp-devup-ui/src/ui_layout.rs +++ b/crates/devup-mcp-devup-ui/src/ui_layout.rs @@ -172,6 +172,25 @@ fn pixels(value: Option<&String>) -> Option { .ok() } +fn narrow_value(value: &str) -> &str { + value + .trim() + .strip_prefix('[') + .unwrap_or(value) + .split(',') + .next() + .unwrap_or_default() + .trim() + .trim_matches(['\'', '"']) +} + +fn hidden_narrow(element: &Element) -> bool { + element + .attrs + .get("display") + .is_some_and(|value| narrow_value(value) == "none") +} + fn fluid_width(attrs: &BTreeMap) -> bool { !attrs.contains_key("boxSize") && attrs.get("w").is_none_or(|w| w == "100%") } @@ -240,7 +259,7 @@ impl Collector<'_> { .count() >= 2 { - self.risks.push(json!({"rule":"column-reading-order", "confidence":"medium", "elementIndex":parent.ordinal, + self.risks.push(json!({"rule":"column-reading-order", "confidence":"medium", "elementIndex":parent.ordinal, "byteStart":parent.start, "message":"Column wrappers can collapse cards in column order (01,03,02,04). Use one Grid or flex-wrap in design reading order, or explicit breakpoint order; run sequence-order at every width."})); } let row = parent.tag == "Flex" @@ -274,37 +293,38 @@ impl Collector<'_> { + pixels(parent.attrs.get("gap")).unwrap_or(0.0) * children.len().saturating_sub(1) as f64 }); - let mobile_alternative = parent - .attrs - .get("display") - .is_some_and(|value| value.contains("none")) + let mobile_alternative = hidden_narrow(parent) || parent .attrs .get("flexWrap") .is_some_and(|value| value.contains("wrap")) - || direction(parent).is_some_and(|value| value.starts_with("[\"column\"")); + || direction(parent).is_some_and(|value| narrow_value(value).starts_with("column")); if navigation && measured_navigation > 390.0 && !mobile_alternative { - self.risks.push(json!({"rule":"navigation-fit", "confidence":"high", "elementIndex":parent.ordinal, + self.risks.push(json!({"rule":"navigation-fit", "confidence":"high", "elementIndex":parent.ordinal, "byteStart":parent.start, "contentWidth":measured_navigation, "narrowViewport":390, "message":"Header navigation cannot fit a narrow container. With no mobile menu frame, propose a drawer behind a menu button below the content-fit breakpoint; verify no-clipped-content at every width."})); } - let wraps = parent - .attrs - .get("flexWrap") - .is_some_and(|value| value.contains("wrap")); let fixed_children = children .iter() - .filter(|child| pixels(child.attrs.get("w")).is_some()) + .filter(|child| !hidden_narrow(child) && pixels(child.attrs.get("w")).is_some()) .count(); let wide_only = std::iter::once(parent).chain(children).any(|element| { - element.attrs.values().any(|value| { - (value.trim_start().starts_with("[null") - || value.trim_start().starts_with("[undefined")) - && value.contains("px") - }) + !hidden_narrow(element) + && element.attrs.iter().any(|(name, value)| { + if matches!(name.as_str(), "w" | "width" | "boxSize") + && value.trim_start().starts_with('[') + && !matches!(narrow_value(value), "null" | "undefined" | "") + && !narrow_value(value).ends_with("px") + { + return false; + } + (value.trim_start().starts_with("[null") + || value.trim_start().starts_with("[undefined")) + && value.contains("px") + }) }); - if row && !wraps && (fixed_children >= 2 || wide_only) { - self.risks.push(json!({"rule":"narrow-fixed-columns", "confidence":"medium", "elementIndex":parent.ordinal, + if row && !mobile_alternative && (fixed_children >= 2 || wide_only) { + self.risks.push(json!({"rule":"narrow-fixed-columns", "confidence":"medium", "elementIndex":parent.ordinal, "byteStart":parent.start, "message":"A multi-column row keeps fixed child widths without a narrow column or wrap rule. Supply narrow responsive layout props and verify section-layout, overflow and clipped text at 390/858px."})); } let width = pixels(parent.attrs.get("w")) diff --git a/crates/devup-mcp-devup-ui/src/ui_structure.rs b/crates/devup-mcp-devup-ui/src/ui_structure.rs index 44920094..1f6fa747 100644 --- a/crates/devup-mcp-devup-ui/src/ui_structure.rs +++ b/crates/devup-mcp-devup-ui/src/ui_structure.rs @@ -64,6 +64,15 @@ pub fn validate_bundle( strict: bool, ) -> Result { check_bundle(files)?; + Ok(validate_sources(files, theme, strict)) +} + +/// Validate source files whose envelope has already been checked by the caller. +pub fn validate_sources( + files: &[SourceFile], + theme: Option<&ProjectTheme>, + strict: bool, +) -> UiValidation { let boundary = files.iter().filter(|f| is_source(&f.path)).any(|file| { let allocator = Allocator::default(); let parsed = Parser::new(&allocator, &file.content, source_type(&file.path)).parse(); @@ -72,16 +81,25 @@ 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; + let global_link_decoration = files.iter().filter(|file| is_source(&file.path)) + .any(|file| super::visual_behavior::has_global_link_decoration(&file.content)); for file in files.iter().filter(|f| is_source(&f.path)) { let mut report = validate_source(&file.content, theme, strict, source_type(&file.path)); + if global_link_decoration { + report.violations.retain(|finding| finding.rule != "link-underline"); + } + 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)); report .violations .extend(super::site::interaction_findings(&file.path, &file.content)); @@ -99,12 +117,13 @@ 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 .iter() .all(|v| v.severity != Severity::Error && !(strict && v.severity == Severity::Warning)); - Ok(result) + result } fn is_source(path: &str) -> bool { path.ends_with(".tsx") 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..a310bc1f 100644 --- a/crates/devup-mcp-devup-ui/src/ui_validate/site.rs +++ b/crates/devup-mcp-devup-ui/src/ui_validate/site.rs @@ -24,6 +24,7 @@ //! neither `metadata` nor `generateMetadata`. use super::{Severity, Violation}; +use crate::effective_attributes::AttributeContext; use oxc_allocator::Allocator; use oxc_ast::ast::*; use oxc_ast_visit::{Visit, walk}; @@ -98,6 +99,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, } @@ -118,6 +121,7 @@ struct Collector<'s> { source: &'s str, nav_depth: usize, elements: Vec, + attributes: AttributeContext, } fn attribute_name<'a>(item: &'a JSXAttributeItem<'a>) -> Option<&'a str> { @@ -130,28 +134,6 @@ fn attribute_name<'a>(item: &'a JSXAttributeItem<'a>) -> Option<&'a str> { } } -/// The literal or identifier `as` names, if any: `as="a"`, `as={Link}`. -fn as_value(node: &JSXOpeningElement<'_>) -> Option { - node.attributes.iter().find_map(|item| { - let JSXAttributeItem::Attribute(attribute) = item else { - return None; - }; - if !matches!(&attribute.name, JSXAttributeName::Identifier(name) if name.name == "as") { - return None; - } - match &attribute.value { - Some(JSXAttributeValue::StringLiteral(value)) => Some(value.value.to_string()), - Some(JSXAttributeValue::ExpressionContainer(container)) => { - match &container.expression { - JSXExpression::Identifier(id) => Some(id.name.to_string()), - _ => None, - } - } - _ => None, - } - }) -} - fn element_name(node: &JSXOpeningElement<'_>) -> String { match &node.name { JSXElementName::Identifier(id) => id.name.to_string(), @@ -174,6 +156,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 { @@ -197,26 +196,14 @@ impl<'a> Visit<'a> for Collector<'_> { fn visit_jsx_element(&mut self, node: &JSXElement<'a>) { let opening = &node.opening_element; let tag = element_name(opening); - let as_tag = as_value(opening); - let names = opening - .attributes - .iter() - .filter_map(attribute_name) - .collect::>(); - let has = |name: &str| names.contains(&name); + let attributes = self.attributes.read(opening); + let as_tag = attributes.rendered_as(); + let has = |name: &str| attributes.contains(name); let effective = as_tag.clone().unwrap_or_else(|| tag.clone()); - let nav = effective == "nav" - || opening.attributes.iter().any(|item| { - matches!(item, JSXAttributeItem::Attribute(attribute) - if matches!(&attribute.name, JSXAttributeName::Identifier(name) if name.name == "role") - && matches!(&attribute.value, Some(JSXAttributeValue::StringLiteral(value)) if value.value == "navigation")) - }); + let nav = effective == "nav" || attributes.literal("role").as_deref() == Some("navigation"); // A spread may carry the states; nothing here can see into it. - let spread = opening - .attributes - .iter() - .any(|item| matches!(item, JSXAttributeItem::SpreadAttribute(_))); - let link = matches!(effective.as_str(), "a" | "Link"); + let spread = attributes.unresolved_spread; + let link = matches!(effective.as_str(), "a" | "Link") || self.attributes.link_names.contains(&effective); let button = effective == "button"; let input = matches!(effective.as_str(), "input" | "select" | "textarea"); let role = if input { @@ -251,10 +238,13 @@ impl<'a> Visit<'a> for Collector<'_> { hover: has("_hover"), focus: has("_focusVisible") || has("_focus") || has("_focusWithin"), active: has("_active"), - transition: names.iter().any(|name| name.starts_with("transition")), + transition: attributes.transition, 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) @@ -286,6 +276,7 @@ fn collect(path: &str, source: &str) -> Option> { source, nav_depth: 0, elements: Vec::new(), + attributes: AttributeContext::from_program(&parsed.program), }; collector.visit_program(&parsed.program); Some(collector.elements) @@ -391,7 +382,7 @@ fn scales(text: &str) -> Vec { .collect() } -fn violation( +pub(super) fn violation( source: &str, span: Span, rule: &'static str, @@ -453,7 +444,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 +473,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 +713,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/src/ui_validate/tests.rs b/crates/devup-mcp-devup-ui/src/ui_validate/tests.rs index 6e621e20..0e6cda38 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,59 @@ mod tests { use super::*; use crate::theme::parse_project_theme; + #[test] + fn effective_navigation_attributes_and_transitions_are_recognized() { + for attrs in [ + r#"props={{ href: '/x', 'aria-current': active ? 'page' : undefined }} transition="color .2s""#, + r#"{...{ props: { href: '/x', ['aria-current']: active ? 'page' : undefined }, _hover: { transition: 'color .2s' } }}"#, + r#"props={active ? { href: '/x', 'aria-current': 'page' } : { href: '/x', 'aria-current': undefined }} selectors={{ '&:hover': { transitionDuration: '200ms' } }}"#, + r#"props={{ href: '/x', 'aria-current': active ? 'page' : undefined }} className={css({ transition: 'color .2s' })}"#, + r#"props={{ href: '/x', 'aria-current': active ? 'page' : undefined }} className={linkStyle}"#, + ] { + let source = format!(r#"import RouteLink from 'next/link'; const linkStyle = css({{ transition: 'color .2s' }}); const Header = () => "#); + let findings = site::interaction_findings("Header.tsx", &source); + assert!(!findings.iter().any(|v| matches!(v.rule, "nav-current" | "hover-transition")), "{source}: {findings:?}"); + assert_eq!(site::interaction_coverage("Header.tsx", &source).interactive, 1); + } + } + + #[test] + fn narrow_safe_rows_do_not_report_fixed_columns() { + for source in [ + r#""#, + r#""#, + r#""#, + r#""#, + ] { + let report = validate_devup_ui_tsx(source, None, false); + assert!(!report.violations.iter().any(|v| v.rule == "narrow-fixed-columns"), "{source}: {:?}", report.violations); + } + } + + #[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": { @@ -501,10 +554,86 @@ mod tests { assert!(lenient.ok); assert!(!strict.ok); } + + fn purpose_named_length_theme() -> ProjectTheme { + parse_project_theme( + r##"{ "theme": { "length": { "default": { + "space0": "0px", "spacing24": "24px", "sizeStrokeBorder": "1px", + "radius12": "12px", "space12": "12px" + } } } }"##, + ) + .unwrap() + } + + #[test] + fn zero_lengths_are_never_reported() { + let tsx = r##"export const X = () => ;"##; + let report = validate_devup_ui_tsx(tsx, Some(&purpose_named_length_theme()), true); + assert!(report.ok); + assert!( + report.violations.iter().all(|v| v.rule != "hardcoded-length"), + "{:?}", + report.violations + ); + } + + #[test] + fn a_token_named_for_another_purpose_is_info_without_advice() { + let tsx = r##"export const X = () => ;"##; + let report = validate_devup_ui_tsx(tsx, Some(&purpose_named_length_theme()), true); + assert!(report.ok, "{:?}", report.violations); + let lengths = report + .violations + .iter() + .filter(|v| v.rule == "hardcoded-length") + .collect::>(); + assert_eq!(lengths.len(), 2); + for finding in lengths { + assert_eq!(finding.severity, Severity::Info); + assert!(finding.suggestion.is_none()); + assert!(finding.context.contains_key("otherPurposeTokens")); + assert!(finding.message.contains("different purpose")); + } + } + + #[test] + fn a_token_named_for_the_same_purpose_is_preferred() { + let theme = purpose_named_length_theme(); + let radius = validate_devup_ui_tsx( + r##"export const X = () => ;"##, + Some(&theme), + false, + ); + let gap = validate_devup_ui_tsx( + r##"export const X = () => ;"##, + Some(&theme), + false, + ); + let radius = &radius.violations[0]; + let gap = &gap.violations[0]; + assert_eq!(radius.severity, Severity::Warning); + assert_eq!(gap.severity, Severity::Warning); + assert!(radius.message.contains("$radius12") && !radius.message.contains("$space12")); + assert!(gap.message.contains("$space12") && !gap.message.contains("$radius12")); + } + + #[test] + fn mode_advice_is_stated_once() { + let report = validate_devup_ui_tsx( + r##"export const X = () => ;"##, + Some(&fixture_theme()), + false, + ); + let finding = &report.violations[0]; + assert!(!finding.message.contains("mode")); + assert!(finding.suggestion.as_deref().unwrap().contains("allModesMatch")); + assert_eq!(finding.context["value"], "16px"); + assert_eq!(finding.context["prop"], "borderRadius"); + } } #[path = "../ui_structure.rs"] mod structure; pub use structure::{ - MAX_FILES, MAX_TOTAL_BYTES, SourceFile, bundle_theme, check_bundle, validate_bundle, + MAX_FILES, MAX_TOTAL_BYTES, SourceFile, bundle_theme, check_bundle, validate_bundle, validate_sources, }; 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..8518ba78 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,13 @@ use std::collections::BTreeSet; mod blur_layers; #[path = "site.rs"] pub mod site; +#[path = "wiring.rs"] +pub mod wiring; +#[path = "visual_behavior.rs"] +pub mod visual_behavior; +#[cfg(test)] +#[path = "visual_behavior_tests.rs"] +mod visual_behavior_tests; use oxc_allocator::Allocator; use oxc_ast::ast::{ @@ -57,6 +64,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 @@ -139,6 +148,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(), @@ -147,6 +157,8 @@ fn validate_source( }; visitor.visit_program(&parsed.program); violations.extend(visitor.violations); + violations.extend(wiring::findings(tsx)); + violations.extend(visual_behavior::findings(tsx)); let checked_tokens = visitor.checked_tokens; let attributes = crate::provenance::attributes::generated_attributes(tsx); violations.extend(blur_layers::check(tsx, &attributes)); @@ -157,7 +169,8 @@ fn validate_source( Some("navigation-fit") => "navigation-fit", _ => continue, }; - violations.push(Violation { rule, severity: Severity::Warning, byte_range: [0, 0], + let start = risk["byteStart"].as_u64().and_then(|value| usize::try_from(value).ok()).unwrap_or(0); + violations.push(Violation { rule, severity: Severity::Warning, byte_range: [start, start], context: risk.as_object().cloned().unwrap_or_default(), message: risk["message"].as_str().unwrap_or_default().to_owned(), suggestion: Some("Use reading-order DOM and explicit narrow layout props, then run the runtime audit at every required width.".into()) }); @@ -234,6 +247,7 @@ fn validate_source( .unwrap_or_default(); UiValidation { + next_action: lint_fix_action(visitor.fixable), ok, violations, theme_notes, @@ -243,6 +257,7 @@ fn validate_source( } struct TsxVisitor<'t> { + fixable: usize, theme: Option<&'t ProjectTheme>, checked_tokens: usize, violations: Vec, @@ -340,21 +355,63 @@ impl<'t> TsxVisitor<'t> { return; } if is_length_like_prop(prop_name) && is_px_length(text) { + if is_zero_length(text) { + return; + } let tokens = self .theme .map(|theme| theme.length_tokens_matching_px(text)) .unwrap_or_default(); + let prop_kind = LengthKind::of_prop(prop_name); + let (fitting, other): (Vec<_>, Vec<_>) = tokens + .into_iter() + .partition(|name| prop_kind.accepts(LengthKind::of_token(name))); + if fitting.is_empty() && !other.is_empty() { + self.report_other_purpose_length(prop_name, prop_kind, text, &other, span); + return; + } self.report_hardcoded_value( "hardcoded-length", "length", prop_name, text, - &tokens, + &fitting, span, ); } } + /// The value equals a token, but every such token is named for another + /// purpose (`$spacing24` for a radius, a stroke width for a height). + /// Replacing would couple unrelated design decisions, so this is info + /// with no replacement advice. + fn report_other_purpose_length( + &mut self, + prop_name: &str, + prop_kind: LengthKind, + text: &str, + tokens: &[String], + span: Span, + ) { + let named = tokens.iter().map(|name| format!("${name}")).collect::>().join(", "); + self.violations.push(Violation { + rule: "hardcoded-length", + severity: Severity::Info, + context: serde_json::Map::from_iter([ + ("value".into(), serde_json::json!(text)), + ("prop".into(), serde_json::json!(prop_name)), + ("lengthKind".into(), serde_json::json!(prop_kind.name())), + ("otherPurposeTokens".into(), serde_json::json!(tokens.iter().map(|name| format!("${name}")).collect::>())), + ]), + byte_range: [span.start as usize, span.end as usize], + message: format!( + "{prop_name} uses hardcoded length {text}; only {named} has this value, and it is named for a different purpose than {}.", + prop_kind.label() + ), + suggestion: None, + }); + } + /// Every literal remains visible. Only exact matches justify a warning /// and token advice; unmatched values carry factual info with no suggestion. fn report_hardcoded_value( @@ -375,11 +432,15 @@ impl<'t> TsxVisitor<'t> { self.violations.push(Violation { rule, severity: Severity::Info, - context: Default::default(), + context: serde_json::Map::from_iter([ + ("value".into(), serde_json::json!(text)), + ("prop".into(), serde_json::json!(prop_name)), + ]), byte_range: [span.start as usize, span.end as usize], message: format!("{prop_name} uses hardcoded {kind} {text}; {context}."), suggestion: None, }); + self.tag_length_kind(rule, prop_name); return; } let named = tokens @@ -395,13 +456,30 @@ impl<'t> TsxVisitor<'t> { self.violations.push(Violation { rule, severity: Severity::Warning, - context: serde_json::Map::from_iter([("tokenMatches".into(), serde_json::json!(token_matches))]), + context: serde_json::Map::from_iter([ + ("tokenMatches".into(), serde_json::json!(token_matches)), + ("value".into(), serde_json::json!(text)), + ("prop".into(), serde_json::json!(prop_name)), + ]), byte_range: [span.start as usize, span.end as usize], message: format!( include_str!("../text/ui_validate-report_hardcoded_value_Violation-63c589c5.md") , kind = kind, named = named, prop_name = prop_name, text = text), - suggestion: Some(format!("matching tokens: {named}. Check tokenMatches.matchingModes and allModesMatch before replacing; a partial-mode match can change colors in other modes.")), + suggestion: Some(format!("Use {named} where tokenMatches[].allModesMatch is true; otherwise the {kind} changes in the modes it does not match.")), }); + self.tag_length_kind(rule, prop_name); + } + + fn tag_length_kind(&mut self, rule: &str, prop_name: &str) { + if rule != "hardcoded-length" { + return; + } + if let Some(last) = self.violations.last_mut() { + last.context.insert( + "lengthKind".into(), + serde_json::json!(LengthKind::of_prop(prop_name).name()), + ); + } } fn check_unknown_prop(&mut self, prop_name: &str, span: Span) { @@ -518,6 +596,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. @@ -667,6 +762,103 @@ fn is_hex_color(text: &str) -> bool { matches!(hex.len(), 3 | 4 | 6 | 8) && hex.chars().all(|character| character.is_ascii_hexdigit()) } +fn is_zero_length(text: &str) -> bool { + text.strip_suffix("px") + .map(|number| number.strip_prefix('-').unwrap_or(number)) + .and_then(|number| number.parse::().ok()) + .is_some_and(|number| number == 0.0) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum LengthKind { + Radius, + Stroke, + Space, + Size, + Text, + Unknown, +} + +impl LengthKind { + fn of_prop(prop: &str) -> Self { + if prop.contains("Radius") { + return Self::Radius; + } + if prop.starts_with("border") && prop.ends_with("Width") + || matches!(prop, "outlineWidth" | "strokeWidth") + { + return Self::Stroke; + } + if matches!(prop, "fontSize" | "lineHeight" | "letterSpacing" | "wordSpacing") { + return Self::Text; + } + if matches!( + prop, + "gap" | "columnGap" | "rowGap" | "gridGap" | "gridColumnGap" | "gridRowGap" + | "borderSpacing" | "top" | "right" | "bottom" | "left" | "outlineOffset" + ) || prop.starts_with("padding") + || prop.starts_with("margin") + || matches!( + prop, + "p" | "px" | "py" | "pt" | "pr" | "pb" | "pl" | "m" | "mx" | "my" | "mt" | "mr" | "mb" | "ml" + ) + { + return Self::Space; + } + if matches!(prop, "w" | "h" | "boxSize" | "width" | "height" | "flexBasis" | "bgSize") + || prop.starts_with("min") + || prop.starts_with("max") + { + return Self::Size; + } + Self::Unknown + } + + fn of_token(name: &str) -> Self { + let name = name.to_ascii_lowercase(); + let has = |words: &[&str]| words.iter().any(|word| name.contains(word)); + if has(&["radius", "rounded", "corner"]) { + Self::Radius + } else if has(&["stroke", "border", "outline"]) { + Self::Stroke + } else if has(&["space", "spacing", "gap", "gutter", "padding", "margin", "inset"]) { + Self::Space + } else if has(&["font", "leading", "tracking", "lineheight"]) { + Self::Text + } else if has(&["size", "width", "height", "icon", "avatar"]) { + Self::Size + } else { + Self::Unknown + } + } + + fn accepts(self, token: Self) -> bool { + self == Self::Unknown || token == Self::Unknown || self == token + } + + fn name(self) -> &'static str { + match self { + Self::Radius => "radius", + Self::Stroke => "stroke", + Self::Space => "space", + Self::Size => "size", + Self::Text => "text", + Self::Unknown => "other", + } + } + + fn label(self) -> &'static str { + match self { + Self::Radius => "a corner radius", + Self::Stroke => "a stroke width", + Self::Space => "spacing", + Self::Size => "a size", + Self::Text => "a text metric", + Self::Unknown => "this prop", + } + } +} + fn is_px_length(text: &str) -> bool { let Some(number) = text.strip_suffix("px") else { return false; @@ -693,4 +885,14 @@ pub fn rule_names() -> BTreeSet<&'static str> { ] .into_iter() .collect() -} \ No newline at end of file +} + +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-devup-ui/src/ui_validate/visual_behavior.rs b/crates/devup-mcp-devup-ui/src/ui_validate/visual_behavior.rs new file mode 100644 index 00000000..188f64ce --- /dev/null +++ b/crates/devup-mcp-devup-ui/src/ui_validate/visual_behavior.rs @@ -0,0 +1,151 @@ +use super::{Severity, Violation, site}; +use crate::effective_attributes::AttributeContext; +use oxc_allocator::Allocator; +use oxc_ast::ast::*; +use oxc_ast_visit::{Visit, walk}; +use oxc_parser::Parser; +use oxc_span::SourceType; +use std::collections::BTreeSet; + +pub const MARQUEE_RECIPE: &str = "Review this heuristic marquee candidate; Figma does not prove animation intent. Duplicate the content in one nowrap track; use keyframes from translateX(0) to translateX(-50%) with infinite linear animation, _motionReduce={{ animation: 'none' }}, and aria-hidden on the duplicate."; + +pub fn repeated_separator(text: &str) -> bool { + if text.contains(['\n', '\r']) || text.chars().count() < 24 { + return false; + } + ['✦', '★', '•', '·', '|', '/', '✧', '●', '◆'] + .into_iter() + .any(|separator| { + (!matches!(separator, '/' | '|') || text.contains(&format!(" {separator} "))) + && text.chars().filter(|character| *character == separator).count() >= 3 + && text.split(separator).filter(|part| { + let part = part.trim(); + !part.is_empty() + && part.chars().any(char::is_alphanumeric) + }).count() >= 3 + }) +} + +#[derive(Default)] +struct Globals { + names: BTreeSet, + namespaces: BTreeSet, + decoration: bool, + motion: bool, +} + +impl<'a> Visit<'a> for Globals { + fn visit_import_declaration(&mut self, node: &ImportDeclaration<'a>) { + if node.source.value == "@devup-ui/react" { + for specifier in node.specifiers.iter().flatten() { + match specifier { + ImportDeclarationSpecifier::ImportSpecifier(specifier) + if specifier.imported.to_string() == "globalCss" => { + self.names.insert(specifier.local.name.to_string()); + } + ImportDeclarationSpecifier::ImportNamespaceSpecifier(specifier) => { + self.namespaces.insert(specifier.local.name.to_string()); + } + _ => {} + } + } + } + } + + fn visit_call_expression(&mut self, call: &CallExpression<'a>) { + let global = match call.callee.get_inner_expression() { + Expression::Identifier(id) => self.names.contains(id.name.as_str()), + Expression::StaticMemberExpression(member) => { + member.property.name == "globalCss" + && matches!(&member.object, Expression::Identifier(id) if self.namespaces.contains(id.name.as_str())) + } + _ => false, + }; + if global { + for argument in &call.arguments { + if let Some(Expression::ObjectExpression(object)) = argument.as_expression() { + self.decoration |= object.properties.iter().any(|property| { + let ObjectPropertyKind::ObjectProperty(property) = property else { return false }; + property.key.static_name().is_some_and(|selector| { + selector.split(',').any(|selector| selector.trim() == "a") + }) && matches!(&property.value, Expression::ObjectExpression(styles) if styles.properties.iter().any(|style| { + matches!(style, ObjectPropertyKind::ObjectProperty(style) if style.key.static_name().is_some_and(|key| matches!(key.as_ref(), "textDecoration" | "text-decoration"))) + })) + }); + } + } + } + self.motion |= matches!(&call.callee, Expression::Identifier(id) if id.name == "keyframes"); + walk::walk_call_expression(self, call); + } + + fn visit_jsx_attribute(&mut self, attribute: &JSXAttribute<'a>) { + self.motion |= matches!(&attribute.name, JSXAttributeName::Identifier(id) if id.name.starts_with("animation")); + walk::walk_jsx_attribute(self, attribute); + } + + fn visit_object_property(&mut self, property: &ObjectProperty<'a>) { + self.motion |= property.key.static_name().is_some_and(|name| name.starts_with("animation")); + walk::walk_object_property(self, property); + } +} + +pub fn has_global_link_decoration(source: &str) -> bool { + let allocator = Allocator::default(); + let parsed = Parser::new(&allocator, source, SourceType::tsx()).parse(); + let mut globals = Globals::default(); + globals.names.insert("globalCss".into()); + globals.visit_program(&parsed.program); + !parsed.panicked && parsed.diagnostics.is_empty() && globals.decoration +} + +struct Collector<'s> { + source: &'s str, + attributes: AttributeContext, + globals: Globals, + findings: Vec, +} + +impl<'a> Visit<'a> for Collector<'_> { + fn visit_jsx_opening_element(&mut self, opening: &JSXOpeningElement<'a>) { + let name = opening.name.get_identifier_name().map(|name| name.as_str()).unwrap_or(""); + let attributes = self.attributes.read(opening); + let effective = attributes.rendered_as().unwrap_or_else(|| name.to_owned()); + if (effective == "a" || self.attributes.link_names.contains(&effective)) + && !attributes.contains("textDecoration") + && !attributes.unresolved_spread + && !self.globals.decoration + { + self.findings.push(site::violation(self.source, opening.span, "link-underline", Severity::Warning, + format!("<{name}> renders a link without an explicit textDecoration; the browser can underline its button label. @devup-ui/reset-css does not reset anchor decoration."), + "Set textDecoration=\"none\" on the link, or use globalCss({ a: { textDecoration: 'none', color: 'inherit' } }).")); + } + walk::walk_jsx_opening_element(self, opening); + } + + fn visit_jsx_text(&mut self, text: &JSXText<'a>) { + if !self.globals.motion && repeated_separator(text.value.trim()) { + let mut finding = site::violation(self.source, text.span, "marquee-static", Severity::Info, + "Long single-line text repeats separator glyphs but this file declares no keyframes or animation; review whether a scrolling strip was shipped as static text.".into(), MARQUEE_RECIPE); + finding.context.insert("confidence".into(), serde_json::json!("heuristic")); + self.findings.push(finding); + } + } +} + +pub(super) fn findings(source: &str) -> Vec { + let allocator = Allocator::default(); + let parsed = Parser::new(&allocator, source, SourceType::tsx()).parse(); + if parsed.panicked || !parsed.diagnostics.is_empty() { return Vec::new() } + let mut globals = Globals::default(); + globals.names.insert("globalCss".into()); + globals.visit_program(&parsed.program); + let mut collector = Collector { + source, + attributes: AttributeContext::from_program(&parsed.program), + globals, + findings: Vec::new(), + }; + collector.visit_program(&parsed.program); + collector.findings +} diff --git a/crates/devup-mcp-devup-ui/src/ui_validate/visual_behavior_tests.rs b/crates/devup-mcp-devup-ui/src/ui_validate/visual_behavior_tests.rs new file mode 100644 index 00000000..7fb625b0 --- /dev/null +++ b/crates/devup-mcp-devup-ui/src/ui_validate/visual_behavior_tests.rs @@ -0,0 +1,70 @@ +use super::*; + +fn count(source: &str, rule: &str) -> usize { + validate_devup_ui_tsx(source, None, false) + .violations + .iter() + .filter(|finding| finding.rule == rule) + .count() +} + +#[test] +fn link_underline_warns_for_native_polymorphic_and_aliased_links() { + let source = r#"import RouteLink from 'next/link'; + const View = () => <>AB + CD;"#; + assert_eq!(count(source, "link-underline"), 4); +} + +#[test] +fn link_underline_accepts_explicit_decoration_but_not_hover_only() { + let source = r#"const View = () => <>A + B + C;"#; + assert_eq!(count(source, "link-underline"), 1); +} + +#[test] +fn link_underline_accepts_a_bundle_global_reset() { + let files = [ + SourceFile { path: "src/Reset.ts".into(), content: "import { globalCss as reset } from '@devup-ui/react'; reset({ a: { textDecoration: 'none', color: 'inherit' } });".into() }, + SourceFile { path: "src/components/View.tsx".into(), content: "export const View = () => Go;".into() }, + ]; + let report = validate_bundle(&files, None, false).unwrap(); + assert!(!report.violations.iter().any(|finding| finding.rule == "link-underline")); +} + +#[test] +fn link_underline_does_not_accept_unrelated_or_hover_global_styles() { + for reset in [ + "const styles = { a: { textDecoration: 'none' } };", + "globalCss({ button: { textDecoration: 'none' } });", + "globalCss({ 'a:hover': { textDecoration: 'none' } });", + "globalCss({ a: { _hover: { textDecoration: 'none' } } });", + "// globalCss({ a: { textDecoration: 'none' } });", + ] { + assert_eq!(count(&format!("{reset}\nconst View = () => Go;"), "link-underline"), 1); + } +} + +#[test] +fn marquee_static_reports_repeated_single_line_text_without_motion() { + let source = "const View = () => Research ✦ Repair ✦ Renew ✦ Research;"; + let report = validate_devup_ui_tsx(source, None, false); + let finding = report.violations.iter().find(|finding| finding.rule == "marquee-static").unwrap(); + assert_eq!(finding.severity, Severity::Info); + assert_eq!(finding.context["confidence"], "heuristic"); +} + +#[test] +fn marquee_static_ignores_short_multiline_and_animated_content() { + for source in [ + "const View = () => A / B / C;", + "const View = () => Research ✦ Repair\n✦ Renew ✦ Research;", + "const View = () => Research ✦ Repair ✦ Renew ✦ Research;", + "const strip = keyframes({ from: { transform: 'translateX(0)' }, to: { transform: 'translateX(-50%)' } }); const View = () => Research ✦ Repair ✦ Renew ✦ Research;", + "const View = () => https://example.com/a/b/c/d/e;", + ] { + assert_eq!(count(source, "marquee-static"), 0, "{source}"); + } +} diff --git a/crates/devup-mcp-devup-ui/src/ui_validate/wiring.rs b/crates/devup-mcp-devup-ui/src/ui_validate/wiring.rs new file mode 100644 index 00000000..285d1a7a --- /dev/null +++ b/crates/devup-mcp-devup-ui/src/ui_validate/wiring.rs @@ -0,0 +1,213 @@ +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, 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); + 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, + link_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)); + } + 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; + 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); + self.link_depth += usize::from(links); + walk::walk_jsx_element(self, node); + self.link_depth -= usize::from(links); + 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 a_control_rendered_inside_a_link_takes_the_link_destination() { + let source = r#"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-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/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(" 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(" 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}"); +} + +#[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}"); + } + } + } +} + +#[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/codegen_defect_regressions.rs b/crates/devup-mcp-devup-ui/tests/codegen_defect_regressions.rs new file mode 100644 index 00000000..cfe7209a --- /dev/null +++ b/crates/devup-mcp-devup-ui/tests/codegen_defect_regressions.rs @@ -0,0 +1,174 @@ +use devup_mcp_devup_ui::codegen::{CodegenOptions, LengthToken, generate_component}; +use devup_mcp_figma::{Snapshot, discover_asset_manifest}; +use serde_json::{Value, json}; + +fn snapshot(nodes: Value) -> Snapshot { + serde_json::from_value( + json!({"fileKey":"regression","roots":["root"],"nodes":nodes,"diagnostics":[]}), + ) + .unwrap() +} + +#[test] +fn auto_layout_assets_preserve_container_and_individual_icons() { + for kind in ["FRAME", "INSTANCE", "COMPONENT"] { + for mode in ["HORIZONTAL", "VERTICAL", "GRID"] { + let input = snapshot(json!({ + "root":{"id":"root","type":kind,"fields":{"name":"Header","layoutMode":mode,"width":360,"height":64,"paddingLeft":12,"itemSpacing":29,"childrenIds":["logo","menu"]}}, + "logo":{"id":"logo","type":"VECTOR","fields":{"name":"Logo","parentId":"root","width":120,"height":24}}, + "menu":{"id":"menu","type":"VECTOR","fields":{"name":"Menu","parentId":"root","width":24,"height":24}} + })); + let output = generate_component(&input, "root", &CodegenOptions::default()).unwrap(); + assert!( + !output.tsx.contains("/icons/header.svg"), + "{kind}/{mode}: {}", + output.tsx + ); + let manifest = discover_asset_manifest(&input); + assert_eq!(manifest.assets.len(), 2, "{kind}/{mode}: {manifest:?}"); + } + } +} + +#[test] +fn exported_root_never_inherits_absolute_placement_from_uncollected_parent() { + let input = snapshot( + json!({"root":{"id":"root","type":"FRAME","fields":{"layoutPositioning":"ABSOLUTE","parentId":"outside","width":360,"height":64,"layoutSizingHorizontal":"FIXED","layoutSizingVertical":"FIXED"}}}), + ); + let output = generate_component(&input, "root", &CodegenOptions::default()).unwrap(); + assert!(!output.tsx.contains("pos=\"absolute\""), "{}", output.tsx); +} + +#[test] +fn root_fill_and_hug_axes_are_independent() { + for (horizontal, vertical, width, height) in [ + ("FILL", "HUG", true, false), + ("HUG", "FILL", false, true), + ("HUG", "HUG", false, false), + ("FILL", "FILL", true, true), + ] { + let input = snapshot( + json!({"root":{"id":"root","type":"FRAME","fields":{"layoutMode":"VERTICAL","layoutSizingHorizontal":horizontal,"layoutSizingVertical":vertical,"width":360,"height":80}}}), + ); + let output = generate_component(&input, "root", &CodegenOptions::default()).unwrap(); + assert_eq!( + output.tsx.contains("w=\"100%\"") || output.tsx.contains("boxSize=\"100%\""), + width, + "{}", + output.tsx + ); + assert_eq!( + output.tsx.contains("h=\"100%\"") || output.tsx.contains("boxSize=\"100%\""), + height, + "{}", + output.tsx + ); + } +} + +#[test] +fn typography_deduplicates_resolved_tokens_and_retains_real_overrides() { + for size in [16, 18] { + let input = snapshot( + json!({"root":{"id":"root","type":"TEXT","fields":{"width":360,"characters":"Body","textStyleId":"style","fontSize":size,"fontWeight":400,"lineHeight":{"unit":"PIXELS","value":24},"letterSpacing":{"unit":"PIXELS","value":1},"boundVariables":{"fontSize":{"id":"size","type":"VARIABLE_ALIAS"}}}}}), + ); + let options = CodegenOptions { + text_style_tokens: [("style".into(), "body".into())].into(), + typography_values: [("body".into(), json!({"fontSize":"$body","fontWeight":400,"lineHeight":"24px","letterSpacing":"0px"}))].into(), + length_tokens: [("size".into(), LengthToken {token:"body".into(),value:json!("16px")})].into(), + ..Default::default() + }; + let output = generate_component(&input, "root", &options).unwrap(); + assert_eq!( + output.tsx.contains("fontSize="), + size != 16, + "{}", + output.tsx + ); + assert!(!output.tsx.contains("fontWeight="), "{}", output.tsx); + assert!(!output.tsx.contains("lineHeight="), "{}", output.tsx); + assert!( + output.tsx.contains("letterSpacing=\"1px\""), + "{}", + output.tsx + ); + } +} + +#[test] +fn suspicious_values_are_advisory_and_preserve_original_dimensions() { + let input = snapshot( + json!({"root":{"id":"root","type":"FRAME","fields":{"width":171,"height":80,"layoutSizingHorizontal":"FIXED","layoutSizingVertical":"FIXED","targetAspectRatio":{"x":6.19,"y":1}}}}), + ); + let output = generate_component(&input, "root", &CodegenOptions::default()).unwrap(); + let issues = output + .diagnostics + .iter() + .filter(|d| d.code == "DEVUP_CODEGEN_SUSPICIOUS_VALUE") + .collect::>(); + assert_eq!(issues.len(), 2, "{:?}", output.diagnostics); + assert!( + issues + .iter() + .all(|issue| issue.fidelity_impact() == devup_mcp_figma::FidelityImpact::None) + ); + assert!( + issues + .iter() + .all(|issue| issue.details.as_ref().unwrap()["nextAction"].is_string()) + ); + assert!(output.tsx.contains("w=\"171px\""), "{}", output.tsx); + assert!( + output.tsx.contains("aspectRatio=\"6.19\""), + "{}", + output.tsx + ); +} + +#[test] +fn suspicious_values_exclude_centered_arithmetic_text_and_simple_ratios() { + for mode in ["centered", "arithmetic", "text", "asset", "simple"] { + let mut input = snapshot(json!({ + "root":{"id":"root","type":"FRAME","fields":{"width":360,"height":80,"layoutMode":"HORIZONTAL","paddingLeft":9,"paddingRight":9,"itemSpacing":0,"childrenIds":["child","sibling"]}}, + "child":{"id":"child","type":"FRAME","fields":{"parentId":"root","width":171,"height":80,"layoutSizingHorizontal":"FIXED","layoutSizingVertical":"FIXED"}}, + "sibling":{"id":"sibling","type":"TEXT","fields":{"parentId":"root","characters":"Sibling","width":171}} + })); + match mode { + "centered" => { + input + .nodes + .get_mut("root") + .unwrap() + .fields + .insert("counterAxisAlignItems".into(), json!("CENTER")); + } + "arithmetic" => {} + "text" => { + let node = input.nodes.get_mut("child").unwrap(); + node.node_type = "TEXT".into(); + node.fields + .insert("layoutSizingHorizontal".into(), json!("HUG")); + } + "asset" => { + input.nodes.get_mut("child").unwrap().node_type = "VECTOR".into(); + } + "simple" => { + let node = input.nodes.get_mut("child").unwrap(); + node.fields.insert("width".into(), json!(160)); + node.fields + .insert("targetAspectRatio".into(), json!({"x":16,"y":9})); + } + _ => unreachable!(), + } + let output = generate_component(&input, "root", &CodegenOptions::default()).unwrap(); + assert!( + !output + .diagnostics + .iter() + .any(|d| d.code == "DEVUP_CODEGEN_SUSPICIOUS_VALUE" + && d.node_id.as_deref() == Some("child")), + "{mode}: {:?}", + output.diagnostics + ); + } +} diff --git a/crates/devup-mcp-devup-ui/tests/cropped_export_background.rs b/crates/devup-mcp-devup-ui/tests/cropped_export_background.rs index f171ac57..a1fbf867 100644 --- a/crates/devup-mcp-devup-ui/tests/cropped_export_background.rs +++ b/crates/devup-mcp-devup-ui/tests/cropped_export_background.rs @@ -93,7 +93,7 @@ fn cropped_export_background_maps_to_its_actual_fill_and_asset() { let mut wrong = output.clone(); wrong.tsx = wrong .tsx - .replace("/images/Portrait-1.png", "/images/unrelated.png"); + .replace("/images/portrait-1.png", "/images/unrelated.png"); let report = devup_mcp_devup_ui::provenance::validate_fidelity(&snapshot, "picture", &wrong).unwrap(); assert!(!report.assets.complete()); 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-devup-ui/tests/feedback_theme.rs b/crates/devup-mcp-devup-ui/tests/feedback_theme.rs index 1f3160ad..e45ae9d1 100644 --- a/crates/devup-mcp-devup-ui/tests/feedback_theme.rs +++ b/crates/devup-mcp-devup-ui/tests/feedback_theme.rs @@ -217,7 +217,7 @@ fn collected_equal_typography_properties_are_elided_but_local_overrides_stay() { } #[test] -fn responsive_typography_elides_only_properties_equal_at_every_slot() { +fn responsive_typography_elides_values_the_token_defines_at_any_slot() { let options = CodegenOptions { text_style_tokens: [("title".into(), "heading".into())].into(), typography_values: [( @@ -229,7 +229,20 @@ fn responsive_typography_elides_only_properties_equal_at_every_slot() { }; let output = generate_component(&text(700), "t", &options).unwrap(); assert!(!output.tsx.contains("fontWeight="), "{}", output.tsx); - assert!(output.tsx.contains("fontSize=\"32px\""), "{}", output.tsx); + assert!(!output.tsx.contains("fontSize="), "{}", output.tsx); + let mut overridden = text(700); + overridden + .nodes + .get_mut("t") + .unwrap() + .fields + .insert("fontSize".into(), json!(24)); + let overridden = generate_component(&overridden, "t", &options).unwrap(); + assert!( + overridden.tsx.contains("fontSize=\"24px\""), + "{}", + overridden.tsx + ); assert!( generate_component(&text(400), "t", &options) .unwrap() diff --git a/crates/devup-mcp-devup-ui/tests/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_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_button_set.json b/crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_button_set.json new file mode 100644 index 00000000..7c5905dc --- /dev/null +++ b/crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_button_set.json @@ -0,0 +1,12500 @@ +{ + "fileKey": "K2kUnxGgvooAJwCQiVTb7g", + "version": null, + "roots": [ + "23:1613" + ], + "nodes": { + "23:1610": { + "id": "23:1610", + "type": "TEXT", + "fields": { + "parentId": "23:1612", + "absoluteBoundingBox": { + "height": 21, + "width": 36, + "x": 8808, + "y": 1931 + }, + "absoluteRenderBounds": { + "height": 12.501953125, + "width": 33.572265625, + "x": 8808.9384765625, + "y": 1935.64599609375 + }, + "boundVariables": { + "fontSize": [ + { + "id": "VariableID:18:956", + "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": 14, + "height": 21, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -3 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "버튼명", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:1649d6614e863b73049b737254cc0e54ab553c77,", + "textTruncation": "DISABLED", + "visible": true, + "width": 36, + "x": 12, + "y": 6, + "styledTextSegments": [ + { + "characters": "버튼명", + "end": 3, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:1649d6614e863b73049b737254cc0e54ab553c77," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "23:1612": { + "id": "23:1612", + "type": "COMPONENT", + "fields": { + "parentId": "23:1613", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "23:1610", + "23:1620" + ], + "absoluteBoundingBox": { + "height": 33, + "width": 84, + "x": 8796, + "y": 1925 + }, + "absoluteRenderBounds": { + "height": 33, + "width": 84, + "x": 8796, + "y": 1925 + }, + "bottomLeftRadius": 8, + "bottomRightRadius": 8, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:345", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 8, + "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 + } + ], + "gridColumnAnchorIndex": 0, + "gridRowAnchorIndex": 4, + "height": 33, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 8, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 6, + "paddingLeft": 12, + "paddingRight": 12, + "paddingTop": 6, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 8, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "FILL", + "maxHeight": null, + "maxWidth": null, + "name": "effect=default, type=primary, size=sm", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 6, + "paddingLeft": 12, + "paddingRight": 12, + "paddingTop": 6, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 8, + "topRightRadius": 8, + "variantProperties": { + "effect": "default", + "size": "sm", + "type": "primary" + }, + "visible": true, + "width": 84, + "x": 21, + "y": 281 + }, + "extra": {}, + "fieldErrors": {} + }, + "23:1613": { + "id": "23:1613", + "type": "COMPONENT_SET", + "fields": { + "parentId": "19:963", + "parentType": "SECTION", + "parentName": "Components", + "childrenIds": [ + "23:1612", + "23:1634", + "23:1642", + "24:1665", + "24:1659", + "24:1662", + "24:1853", + "24:1856", + "24:1859", + "24:1865", + "24:1862", + "24:1868", + "24:1871", + "24:1933", + "24:1940", + "30:368", + "30:371", + "30:374", + "30:377", + "30:383", + "30:399", + "30:402", + "30:415", + "30:421", + "30:508", + "30:511", + "30:514", + "30:517", + "30:496", + "30:499", + "30:505", + "30:502" + ], + "absoluteBoundingBox": { + "height": 486, + "width": 554, + "x": 8775, + "y": 1644 + }, + "absoluteRenderBounds": { + "height": 486, + "width": 554, + "x": 8775, + "y": 1644 + }, + "bottomLeftRadius": 5, + "bottomRightRadius": 5, + "clipsContent": true, + "componentPropertyDefinitions": { + "effect": { + "defaultValue": "default", + "type": "VARIANT", + "variantOptions": [ + "default", + "hover", + "active", + "disabled" + ] + }, + "rightIcon#23:2": { + "defaultValue": true, + "type": "BOOLEAN" + }, + "size": { + "defaultValue": "md", + "type": "VARIANT", + "variantOptions": [ + "sm", + "md" + ] + }, + "type": { + "defaultValue": "primary", + "type": "VARIANT", + "variantOptions": [ + "white", + "primary", + "white-line", + "ghost" + ] + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 5, + "dashPattern": [ + 10, + 5 + ], + "defaultVariant": { + "$nodeId": "24:1853", + "$nodeType": "COMPONENT" + }, + "gridColumnCount": 4, + "gridColumnGap": 16, + "gridColumnSizes": [ + { + "type": "HUG", + "value": 1 + }, + { + "type": "HUG", + "value": 1 + }, + { + "type": "HUG", + "value": 1 + }, + { + "type": "HUG", + "value": 1 + } + ], + "gridRowCount": 8, + "gridRowGap": 16, + "gridRowSizes": [ + { + "type": "HUG", + "value": 1 + }, + { + "type": "HUG", + "value": 1 + }, + { + "type": "HUG", + "value": 1 + }, + { + "type": "HUG", + "value": 1 + }, + { + "type": "HUG", + "value": 1 + }, + { + "type": "HUG", + "value": 1 + }, + { + "type": "HUG", + "value": 1 + }, + { + "type": "HUG", + "value": 1 + } + ], + "height": 486, + "inferredAutoLayout": { + "counterAxisAlignItems": "MIN", + "counterAxisSizingMode": "AUTO", + "itemSpacing": 0, + "layoutAlign": "INHERIT", + "layoutGrow": 0, + "layoutMode": "GRID", + "layoutPositioning": "AUTO", + "paddingBottom": 20, + "paddingLeft": 20, + "paddingRight": 20, + "paddingTop": 20, + "primaryAxisAlignItems": "MIN", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "GRID", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "button", + "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": 554, + "x": 105, + "y": 606 + }, + "extra": {}, + "fieldErrors": {} + }, + "23:1620": { + "id": "23:1620", + "type": "INSTANCE", + "fields": { + "parentId": "23:1612", + "childrenIds": [ + "I23:1620;23:1618" + ], + "absoluteBoundingBox": { + "height": 16, + "width": 16, + "x": 8852, + "y": 1933.5 + }, + "absoluteRenderBounds": { + "height": 16, + "width": 16, + "x": 8852, + "y": 1933.5 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "componentProperties": { + "속성 1": { + "boundVariables": {}, + "type": "VARIANT", + "value": "Right" + } + }, + "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.666666269302368, + "paddingLeft": 6, + "paddingRight": 4.666666507720947, + "paddingTop": 2.6666667461395264, + "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": 16, + "x": 56, + "y": 8.5, + "mainComponentId": "23:1619", + "mainComponentName": "속성 1=Right", + "mainComponentSetId": "20:1312" + }, + "extra": {}, + "fieldErrors": {} + }, + "23:1634": { + "id": "23:1634", + "type": "COMPONENT", + "fields": { + "parentId": "23:1613", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "23:1635", + "23:1636" + ], + "absoluteBoundingBox": { + "height": 33, + "width": 84, + "x": 8928, + "y": 1925 + }, + "absoluteRenderBounds": { + "height": 33, + "width": 84, + "x": 8928, + "y": 1925 + }, + "bottomLeftRadius": 8, + "bottomRightRadius": 8, + "boundVariables": { + "fills": [ + { + "id": "VariableID:23:1651", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 8, + "counterAxisAlignItems": "CENTER", + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:23:1651", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.6078431606292725, + "g": 0.2823529541492462, + "r": 0.003921568859368563 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "gridColumnAnchorIndex": 1, + "gridRowAnchorIndex": 4, + "height": 33, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 8, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 6, + "paddingLeft": 12, + "paddingRight": 12, + "paddingTop": 6, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 8, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "FILL", + "maxHeight": null, + "maxWidth": null, + "name": "effect=hover, type=primary, size=sm", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 6, + "paddingLeft": 12, + "paddingRight": 12, + "paddingTop": 6, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 8, + "topRightRadius": 8, + "variantProperties": { + "effect": "hover", + "size": "sm", + "type": "primary" + }, + "visible": true, + "width": 84, + "x": 153, + "y": 281 + }, + "extra": {}, + "fieldErrors": {} + }, + "23:1635": { + "id": "23:1635", + "type": "TEXT", + "fields": { + "parentId": "23:1634", + "absoluteBoundingBox": { + "height": 21, + "width": 36, + "x": 8940, + "y": 1931 + }, + "absoluteRenderBounds": { + "height": 12.501953125, + "width": 33.572265625, + "x": 8940.9384765625, + "y": 1935.64599609375 + }, + "boundVariables": { + "fontSize": [ + { + "id": "VariableID:18:956", + "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": 14, + "height": 21, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -3 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "버튼명", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:1649d6614e863b73049b737254cc0e54ab553c77,", + "textTruncation": "DISABLED", + "visible": true, + "width": 36, + "x": 12, + "y": 6, + "styledTextSegments": [ + { + "characters": "버튼명", + "end": 3, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:1649d6614e863b73049b737254cc0e54ab553c77," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "23:1636": { + "id": "23:1636", + "type": "INSTANCE", + "fields": { + "parentId": "23:1634", + "childrenIds": [ + "I23:1636;23:1618" + ], + "absoluteBoundingBox": { + "height": 16, + "width": 16, + "x": 8984, + "y": 1933.5 + }, + "absoluteRenderBounds": { + "height": 16, + "width": 16, + "x": 8984, + "y": 1933.5 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "componentProperties": { + "속성 1": { + "boundVariables": {}, + "type": "VARIANT", + "value": "Right" + } + }, + "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.666666269302368, + "paddingLeft": 6, + "paddingRight": 4.666666507720947, + "paddingTop": 2.6666667461395264, + "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": 16, + "x": 56, + "y": 8.5, + "mainComponentId": "23:1619", + "mainComponentName": "속성 1=Right", + "mainComponentSetId": "20:1312" + }, + "extra": {}, + "fieldErrors": {} + }, + "23:1642": { + "id": "23:1642", + "type": "COMPONENT", + "fields": { + "parentId": "23:1613", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "23:1643", + "23:1644" + ], + "absoluteBoundingBox": { + "height": 33, + "width": 84, + "x": 9060, + "y": 1925 + }, + "absoluteRenderBounds": { + "height": 33, + "width": 84, + "x": 9060, + "y": 1925 + }, + "bottomLeftRadius": 8, + "bottomRightRadius": 8, + "boundVariables": { + "fills": [ + { + "id": "VariableID:11:13", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 8, + "counterAxisAlignItems": "CENTER", + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:11:13", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.5137255191802979, + "g": 0.239215686917305, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "gridColumnAnchorIndex": 2, + "gridRowAnchorIndex": 4, + "height": 33, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 8, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 6, + "paddingLeft": 12, + "paddingRight": 12, + "paddingTop": 6, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 8, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "FILL", + "maxHeight": null, + "maxWidth": null, + "name": "effect=active, type=primary, size=sm", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 6, + "paddingLeft": 12, + "paddingRight": 12, + "paddingTop": 6, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 8, + "topRightRadius": 8, + "variantProperties": { + "effect": "active", + "size": "sm", + "type": "primary" + }, + "visible": true, + "width": 84, + "x": 285, + "y": 281 + }, + "extra": {}, + "fieldErrors": {} + }, + "23:1643": { + "id": "23:1643", + "type": "TEXT", + "fields": { + "parentId": "23:1642", + "absoluteBoundingBox": { + "height": 21, + "width": 36, + "x": 9072, + "y": 1931 + }, + "absoluteRenderBounds": { + "height": 12.501953125, + "width": 33.572265625, + "x": 9072.9384765625, + "y": 1935.64599609375 + }, + "boundVariables": { + "fontSize": [ + { + "id": "VariableID:18:956", + "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": 14, + "height": 21, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -3 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "버튼명", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:1649d6614e863b73049b737254cc0e54ab553c77,", + "textTruncation": "DISABLED", + "visible": true, + "width": 36, + "x": 12, + "y": 6, + "styledTextSegments": [ + { + "characters": "버튼명", + "end": 3, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:1649d6614e863b73049b737254cc0e54ab553c77," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "23:1644": { + "id": "23:1644", + "type": "INSTANCE", + "fields": { + "parentId": "23:1642", + "childrenIds": [ + "I23:1644;23:1618" + ], + "absoluteBoundingBox": { + "height": 16, + "width": 16, + "x": 9116, + "y": 1933.5 + }, + "absoluteRenderBounds": { + "height": 16, + "width": 16, + "x": 9116, + "y": 1933.5 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "componentProperties": { + "속성 1": { + "boundVariables": {}, + "type": "VARIANT", + "value": "Right" + } + }, + "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.666666269302368, + "paddingLeft": 6, + "paddingRight": 4.666666507720947, + "paddingTop": 2.6666667461395264, + "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": 16, + "x": 56, + "y": 8.5, + "mainComponentId": "23:1619", + "mainComponentName": "속성 1=Right", + "mainComponentSetId": "20:1312" + }, + "extra": {}, + "fieldErrors": {} + }, + "24:1659": { + "id": "24:1659", + "type": "COMPONENT", + "fields": { + "parentId": "23:1613", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "24:1660", + "24:1661" + ], + "absoluteBoundingBox": { + "height": 33, + "width": 84, + "x": 8928, + "y": 1974 + }, + "absoluteRenderBounds": { + "height": 33, + "width": 84, + "x": 8928, + "y": 1974 + }, + "bottomLeftRadius": 8, + "bottomRightRadius": 8, + "boundVariables": { + "fills": [ + { + "id": "VariableID:24:1672", + "type": "VARIABLE_ALIAS" + } + ], + "strokes": [ + { + "id": "VariableID:9:359", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 8, + "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 + } + ], + "gridColumnAnchorIndex": 1, + "gridRowAnchorIndex": 5, + "height": 33, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 8, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 6, + "paddingLeft": 12, + "paddingRight": 12, + "paddingTop": 6, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 8, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "FILL", + "maxHeight": null, + "maxWidth": null, + "name": "effect=hover, type=ghost, size=sm", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 6, + "paddingLeft": 12, + "paddingRight": 12, + "paddingTop": 6, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 0, + "strokeLeftWeight": 0, + "strokeRightWeight": 0, + "strokeTopWeight": 0, + "strokeWeight": 0, + "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 + } + ], + "topLeftRadius": 8, + "topRightRadius": 8, + "variantProperties": { + "effect": "hover", + "size": "sm", + "type": "ghost" + }, + "visible": true, + "width": 84, + "x": 153, + "y": 330 + }, + "extra": {}, + "fieldErrors": {} + }, + "24:1660": { + "id": "24:1660", + "type": "TEXT", + "fields": { + "parentId": "24:1659", + "absoluteBoundingBox": { + "height": 21, + "width": 36, + "x": 8940, + "y": 1980 + }, + "absoluteRenderBounds": { + "height": 12.501953125, + "width": 33.572265625, + "x": 8940.9384765625, + "y": 1984.64599609375 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:348", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:956", + "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": 14, + "height": 21, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -3 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "버튼명", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:1649d6614e863b73049b737254cc0e54ab553c77,", + "textTruncation": "DISABLED", + "visible": true, + "width": 36, + "x": 12, + "y": 6, + "styledTextSegments": [ + { + "characters": "버튼명", + "end": 3, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:1649d6614e863b73049b737254cc0e54ab553c77," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "24:1661": { + "id": "24:1661", + "type": "INSTANCE", + "fields": { + "parentId": "24:1659", + "childrenIds": [ + "I24:1661;23:1618" + ], + "absoluteBoundingBox": { + "height": 16, + "width": 16, + "x": 8984, + "y": 1982.5 + }, + "absoluteRenderBounds": { + "height": 16, + "width": 16, + "x": 8984, + "y": 1982.5 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "componentProperties": { + "속성 1": { + "boundVariables": {}, + "type": "VARIANT", + "value": "Right" + } + }, + "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.666666269302368, + "paddingLeft": 6, + "paddingRight": 4.666666507720947, + "paddingTop": 2.6666667461395264, + "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": 16, + "x": 56, + "y": 8.5, + "mainComponentId": "23:1619", + "mainComponentName": "속성 1=Right", + "mainComponentSetId": "20:1312" + }, + "extra": {}, + "fieldErrors": {} + }, + "24:1662": { + "id": "24:1662", + "type": "COMPONENT", + "fields": { + "parentId": "23:1613", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "24:1663", + "24:1664" + ], + "absoluteBoundingBox": { + "height": 33, + "width": 84, + "x": 9060, + "y": 1974 + }, + "absoluteRenderBounds": { + "height": 33, + "width": 84, + "x": 9060, + "y": 1974 + }, + "bottomLeftRadius": 8, + "bottomRightRadius": 8, + "boundVariables": { + "fills": [ + { + "id": "VariableID:24:1680", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 8, + "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 + } + ], + "gridColumnAnchorIndex": 2, + "gridRowAnchorIndex": 5, + "height": 33, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 8, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 6, + "paddingLeft": 12, + "paddingRight": 12, + "paddingTop": 6, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 8, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "FILL", + "maxHeight": null, + "maxWidth": null, + "name": "effect=active, type=ghost, size=sm", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 6, + "paddingLeft": 12, + "paddingRight": 12, + "paddingTop": 6, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 8, + "topRightRadius": 8, + "variantProperties": { + "effect": "active", + "size": "sm", + "type": "ghost" + }, + "visible": true, + "width": 84, + "x": 285, + "y": 330 + }, + "extra": {}, + "fieldErrors": {} + }, + "24:1663": { + "id": "24:1663", + "type": "TEXT", + "fields": { + "parentId": "24:1662", + "absoluteBoundingBox": { + "height": 21, + "width": 36, + "x": 9072, + "y": 1980 + }, + "absoluteRenderBounds": { + "height": 12.501953125, + "width": 33.572265625, + "x": 9072.9384765625, + "y": 1984.64599609375 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:345", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:956", + "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": 14, + "height": 21, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -3 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "버튼명", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:1649d6614e863b73049b737254cc0e54ab553c77,", + "textTruncation": "DISABLED", + "visible": true, + "width": 36, + "x": 12, + "y": 6, + "styledTextSegments": [ + { + "characters": "버튼명", + "end": 3, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:1649d6614e863b73049b737254cc0e54ab553c77," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "24:1664": { + "id": "24:1664", + "type": "INSTANCE", + "fields": { + "parentId": "24:1662", + "childrenIds": [ + "I24:1664;23:1618" + ], + "absoluteBoundingBox": { + "height": 16, + "width": 16, + "x": 9116, + "y": 1982.5 + }, + "absoluteRenderBounds": { + "height": 16, + "width": 16, + "x": 9116, + "y": 1982.5 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "componentProperties": { + "속성 1": { + "boundVariables": {}, + "type": "VARIANT", + "value": "Right" + } + }, + "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.666666269302368, + "paddingLeft": 6, + "paddingRight": 4.666666507720947, + "paddingTop": 2.6666667461395264, + "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": 16, + "x": 56, + "y": 8.5, + "mainComponentId": "23:1619", + "mainComponentName": "속성 1=Right", + "mainComponentSetId": "20:1312" + }, + "extra": {}, + "fieldErrors": {} + }, + "24:1665": { + "id": "24:1665", + "type": "COMPONENT", + "fields": { + "parentId": "23:1613", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "24:1666", + "24:1667" + ], + "absoluteBoundingBox": { + "height": 33, + "width": 84, + "x": 9192, + "y": 1925 + }, + "absoluteRenderBounds": { + "height": 33, + "width": 84, + "x": 9192, + "y": 1925 + }, + "bottomLeftRadius": 8, + "bottomRightRadius": 8, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:345", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 8, + "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 + } + ], + "gridColumnAnchorIndex": 3, + "gridRowAnchorIndex": 4, + "height": 33, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 8, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 6, + "paddingLeft": 12, + "paddingRight": 12, + "paddingTop": 6, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 8, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "FILL", + "maxHeight": null, + "maxWidth": null, + "name": "effect=disabled, type=primary, size=sm", + "opacity": 0.4000000059604645, + "overflowDirection": "NONE", + "paddingBottom": 6, + "paddingLeft": 12, + "paddingRight": 12, + "paddingTop": 6, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 8, + "topRightRadius": 8, + "variantProperties": { + "effect": "disabled", + "size": "sm", + "type": "primary" + }, + "visible": true, + "width": 84, + "x": 417, + "y": 281 + }, + "extra": {}, + "fieldErrors": {} + }, + "24:1666": { + "id": "24:1666", + "type": "TEXT", + "fields": { + "parentId": "24:1665", + "absoluteBoundingBox": { + "height": 21, + "width": 36, + "x": 9204, + "y": 1931 + }, + "absoluteRenderBounds": { + "height": 12.501953125, + "width": 33.572265625, + "x": 9204.9384765625, + "y": 1935.64599609375 + }, + "boundVariables": { + "fontSize": [ + { + "id": "VariableID:18:956", + "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": 14, + "height": 21, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -3 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "버튼명", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:1649d6614e863b73049b737254cc0e54ab553c77,", + "textTruncation": "DISABLED", + "visible": true, + "width": 36, + "x": 12, + "y": 6, + "styledTextSegments": [ + { + "characters": "버튼명", + "end": 3, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:1649d6614e863b73049b737254cc0e54ab553c77," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "24:1667": { + "id": "24:1667", + "type": "INSTANCE", + "fields": { + "parentId": "24:1665", + "childrenIds": [ + "I24:1667;23:1618" + ], + "absoluteBoundingBox": { + "height": 16, + "width": 16, + "x": 9248, + "y": 1933.5 + }, + "absoluteRenderBounds": { + "height": 16, + "width": 16, + "x": 9248, + "y": 1933.5 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "componentProperties": { + "속성 1": { + "boundVariables": {}, + "type": "VARIANT", + "value": "Right" + } + }, + "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.666666269302368, + "paddingLeft": 6, + "paddingRight": 4.666666507720947, + "paddingTop": 2.6666667461395264, + "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": 16, + "x": 56, + "y": 8.5, + "mainComponentId": "23:1619", + "mainComponentName": "속성 1=Right", + "mainComponentSetId": "20:1312" + }, + "extra": {}, + "fieldErrors": {} + }, + "24:1853": { + "id": "24:1853", + "type": "COMPONENT", + "fields": { + "parentId": "23:1613", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "24:1854", + "24:1855" + ], + "absoluteBoundingBox": { + "height": 48, + "width": 114, + "x": 8796, + "y": 1665 + }, + "absoluteRenderBounds": { + "height": 48, + "width": 114, + "x": 8796, + "y": 1665 + }, + "bottomLeftRadius": 12, + "bottomRightRadius": 12, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:345", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 12, + "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 + } + ], + "gridColumnAnchorIndex": 0, + "gridRowAnchorIndex": 0, + "height": 48, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 8, + "layoutAlign": "STRETCH", + "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": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "FILL", + "maxHeight": null, + "maxWidth": null, + "name": "effect=default, type=primary, size=md", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 12, + "paddingLeft": 24, + "paddingRight": 24, + "paddingTop": 12, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 12, + "topRightRadius": 12, + "variantProperties": { + "effect": "default", + "size": "md", + "type": "primary" + }, + "visible": true, + "width": 114, + "x": 21, + "y": 21 + }, + "extra": {}, + "fieldErrors": {} + }, + "24:1854": { + "id": "24:1854", + "type": "TEXT", + "fields": { + "parentId": "24:1853", + "absoluteBoundingBox": { + "height": 24, + "width": 42, + "x": 8820, + "y": 1677 + }, + "absoluteRenderBounds": { + "height": 14.2879638671875, + "width": 38.6884765625, + "x": 8821.072265625, + "y": 1682.0240478515625 + }, + "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": 42, + "x": 24, + "y": 12, + "styledTextSegments": [ + { + "characters": "버튼명", + "end": 3, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:cbc02b7831f02c3b00e92d3114823bdda982464e," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "24:1855": { + "id": "24:1855", + "type": "INSTANCE", + "fields": { + "parentId": "24:1853", + "childrenIds": [ + "I24:1855;23:1618" + ], + "absoluteBoundingBox": { + "height": 16, + "width": 16, + "x": 8870, + "y": 1681 + }, + "absoluteRenderBounds": { + "height": 16, + "width": 16, + "x": 8870, + "y": 1681 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "componentProperties": { + "속성 1": { + "boundVariables": {}, + "type": "VARIANT", + "value": "Right" + } + }, + "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.666666269302368, + "paddingLeft": 6, + "paddingRight": 4.666666507720947, + "paddingTop": 2.6666667461395264, + "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": 16, + "x": 74, + "y": 16, + "mainComponentId": "23:1619", + "mainComponentName": "속성 1=Right", + "mainComponentSetId": "20:1312" + }, + "extra": {}, + "fieldErrors": {} + }, + "24:1856": { + "id": "24:1856", + "type": "COMPONENT", + "fields": { + "parentId": "23:1613", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "24:1857", + "24:1858" + ], + "absoluteBoundingBox": { + "height": 48, + "width": 114, + "x": 8928, + "y": 1665 + }, + "absoluteRenderBounds": { + "height": 48, + "width": 114, + "x": 8928, + "y": 1665 + }, + "bottomLeftRadius": 12, + "bottomRightRadius": 12, + "boundVariables": { + "fills": [ + { + "id": "VariableID:23:1651", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 12, + "counterAxisAlignItems": "CENTER", + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:23:1651", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.6078431606292725, + "g": 0.2823529541492462, + "r": 0.003921568859368563 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "gridColumnAnchorIndex": 1, + "gridRowAnchorIndex": 0, + "height": 48, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 8, + "layoutAlign": "STRETCH", + "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": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "FILL", + "maxHeight": null, + "maxWidth": null, + "name": "effect=hover, type=primary, size=md", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 12, + "paddingLeft": 24, + "paddingRight": 24, + "paddingTop": 12, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 12, + "topRightRadius": 12, + "variantProperties": { + "effect": "hover", + "size": "md", + "type": "primary" + }, + "visible": true, + "width": 114, + "x": 153, + "y": 21 + }, + "extra": {}, + "fieldErrors": {} + }, + "24:1857": { + "id": "24:1857", + "type": "TEXT", + "fields": { + "parentId": "24:1856", + "absoluteBoundingBox": { + "height": 24, + "width": 42, + "x": 8952, + "y": 1677 + }, + "absoluteRenderBounds": { + "height": 14.2879638671875, + "width": 38.6884765625, + "x": 8953.072265625, + "y": 1682.0240478515625 + }, + "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": 42, + "x": 24, + "y": 12, + "styledTextSegments": [ + { + "characters": "버튼명", + "end": 3, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:cbc02b7831f02c3b00e92d3114823bdda982464e," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "24:1858": { + "id": "24:1858", + "type": "INSTANCE", + "fields": { + "parentId": "24:1856", + "childrenIds": [ + "I24:1858;23:1618" + ], + "absoluteBoundingBox": { + "height": 16, + "width": 16, + "x": 9002, + "y": 1681 + }, + "absoluteRenderBounds": { + "height": 16, + "width": 16, + "x": 9002, + "y": 1681 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "componentProperties": { + "속성 1": { + "boundVariables": {}, + "type": "VARIANT", + "value": "Right" + } + }, + "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.666666269302368, + "paddingLeft": 6, + "paddingRight": 4.666666507720947, + "paddingTop": 2.6666667461395264, + "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": 16, + "x": 74, + "y": 16, + "mainComponentId": "23:1619", + "mainComponentName": "속성 1=Right", + "mainComponentSetId": "20:1312" + }, + "extra": {}, + "fieldErrors": {} + }, + "24:1859": { + "id": "24:1859", + "type": "COMPONENT", + "fields": { + "parentId": "23:1613", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "24:1860", + "24:1861" + ], + "absoluteBoundingBox": { + "height": 48, + "width": 114, + "x": 9060, + "y": 1665 + }, + "absoluteRenderBounds": { + "height": 48, + "width": 114, + "x": 9060, + "y": 1665 + }, + "bottomLeftRadius": 12, + "bottomRightRadius": 12, + "boundVariables": { + "fills": [ + { + "id": "VariableID:11:13", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 12, + "counterAxisAlignItems": "CENTER", + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:11:13", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.5137255191802979, + "g": 0.239215686917305, + "r": 0 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "gridColumnAnchorIndex": 2, + "gridRowAnchorIndex": 0, + "height": 48, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 8, + "layoutAlign": "STRETCH", + "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": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "FILL", + "maxHeight": null, + "maxWidth": null, + "name": "effect=active, type=primary, size=md", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 12, + "paddingLeft": 24, + "paddingRight": 24, + "paddingTop": 12, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 12, + "topRightRadius": 12, + "variantProperties": { + "effect": "active", + "size": "md", + "type": "primary" + }, + "visible": true, + "width": 114, + "x": 285, + "y": 21 + }, + "extra": {}, + "fieldErrors": {} + }, + "24:1860": { + "id": "24:1860", + "type": "TEXT", + "fields": { + "parentId": "24:1859", + "absoluteBoundingBox": { + "height": 24, + "width": 42, + "x": 9084, + "y": 1677 + }, + "absoluteRenderBounds": { + "height": 14.2879638671875, + "width": 38.6884765625, + "x": 9085.072265625, + "y": 1682.0240478515625 + }, + "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": 42, + "x": 24, + "y": 12, + "styledTextSegments": [ + { + "characters": "버튼명", + "end": 3, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:cbc02b7831f02c3b00e92d3114823bdda982464e," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "24:1861": { + "id": "24:1861", + "type": "INSTANCE", + "fields": { + "parentId": "24:1859", + "childrenIds": [ + "I24:1861;23:1618" + ], + "absoluteBoundingBox": { + "height": 16, + "width": 16, + "x": 9134, + "y": 1681 + }, + "absoluteRenderBounds": { + "height": 16, + "width": 16, + "x": 9134, + "y": 1681 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "componentProperties": { + "속성 1": { + "boundVariables": {}, + "type": "VARIANT", + "value": "Right" + } + }, + "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.666666269302368, + "paddingLeft": 6, + "paddingRight": 4.666666507720947, + "paddingTop": 2.6666667461395264, + "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": 16, + "x": 74, + "y": 16, + "mainComponentId": "23:1619", + "mainComponentName": "속성 1=Right", + "mainComponentSetId": "20:1312" + }, + "extra": {}, + "fieldErrors": {} + }, + "24:1862": { + "id": "24:1862", + "type": "COMPONENT", + "fields": { + "parentId": "23:1613", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "24:1863", + "24:1864" + ], + "absoluteBoundingBox": { + "height": 48, + "width": 114, + "x": 9192, + "y": 1665 + }, + "absoluteRenderBounds": { + "height": 48, + "width": 114, + "x": 9192, + "y": 1665 + }, + "bottomLeftRadius": 12, + "bottomRightRadius": 12, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:345", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 12, + "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 + } + ], + "gridColumnAnchorIndex": 3, + "gridRowAnchorIndex": 0, + "height": 48, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 8, + "layoutAlign": "STRETCH", + "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": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "FILL", + "maxHeight": null, + "maxWidth": null, + "name": "effect=disabled, type=primary, size=md", + "opacity": 0.4000000059604645, + "overflowDirection": "NONE", + "paddingBottom": 12, + "paddingLeft": 24, + "paddingRight": 24, + "paddingTop": 12, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 12, + "topRightRadius": 12, + "variantProperties": { + "effect": "disabled", + "size": "md", + "type": "primary" + }, + "visible": true, + "width": 114, + "x": 417, + "y": 21 + }, + "extra": {}, + "fieldErrors": {} + }, + "24:1863": { + "id": "24:1863", + "type": "TEXT", + "fields": { + "parentId": "24:1862", + "absoluteBoundingBox": { + "height": 24, + "width": 42, + "x": 9216, + "y": 1677 + }, + "absoluteRenderBounds": { + "height": 14.2879638671875, + "width": 38.6884765625, + "x": 9217.072265625, + "y": 1682.0240478515625 + }, + "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": 42, + "x": 24, + "y": 12, + "styledTextSegments": [ + { + "characters": "버튼명", + "end": 3, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:cbc02b7831f02c3b00e92d3114823bdda982464e," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "24:1864": { + "id": "24:1864", + "type": "INSTANCE", + "fields": { + "parentId": "24:1862", + "childrenIds": [ + "I24:1864;23:1618" + ], + "absoluteBoundingBox": { + "height": 16, + "width": 16, + "x": 9266, + "y": 1681 + }, + "absoluteRenderBounds": { + "height": 16, + "width": 16, + "x": 9266, + "y": 1681 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "componentProperties": { + "속성 1": { + "boundVariables": {}, + "type": "VARIANT", + "value": "Right" + } + }, + "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.666666269302368, + "paddingLeft": 6, + "paddingRight": 4.666666507720947, + "paddingTop": 2.6666667461395264, + "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": 16, + "x": 74, + "y": 16, + "mainComponentId": "23:1619", + "mainComponentName": "속성 1=Right", + "mainComponentSetId": "20:1312" + }, + "extra": {}, + "fieldErrors": {} + }, + "24:1865": { + "id": "24:1865", + "type": "COMPONENT", + "fields": { + "parentId": "23:1613", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "24:1866", + "24:1867" + ], + "absoluteBoundingBox": { + "height": 50, + "width": 116, + "x": 8796, + "y": 1859 + }, + "absoluteRenderBounds": { + "height": 50, + "width": 116, + "x": 8796, + "y": 1859 + }, + "bottomLeftRadius": 12, + "bottomRightRadius": 12, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 12, + "counterAxisAlignItems": "CENTER", + "gridColumnAnchorIndex": 0, + "gridRowAnchorIndex": 3, + "height": 50, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 8, + "layoutAlign": "STRETCH", + "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": "FILL", + "maxHeight": null, + "maxWidth": null, + "name": "effect=default, type=white-line, size=md", + "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": 116, + "x": 21, + "y": 215 + }, + "extra": {}, + "fieldErrors": {} + }, + "24:1866": { + "id": "24:1866", + "type": "TEXT", + "fields": { + "parentId": "24:1865", + "absoluteBoundingBox": { + "height": 24, + "width": 42, + "x": 8821, + "y": 1872 + }, + "absoluteRenderBounds": { + "height": 14.2879638671875, + "width": 38.6884765625, + "x": 8822.072265625, + "y": 1877.0240478515625 + }, + "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": 42, + "x": 25, + "y": 13, + "styledTextSegments": [ + { + "characters": "버튼명", + "end": 3, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:cbc02b7831f02c3b00e92d3114823bdda982464e," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "24:1867": { + "id": "24:1867", + "type": "INSTANCE", + "fields": { + "parentId": "24:1865", + "childrenIds": [ + "I24:1867;23:1618" + ], + "absoluteBoundingBox": { + "height": 16, + "width": 16, + "x": 8871, + "y": 1876 + }, + "absoluteRenderBounds": { + "height": 16, + "width": 16, + "x": 8871, + "y": 1876 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "componentProperties": { + "속성 1": { + "boundVariables": {}, + "type": "VARIANT", + "value": "Right" + } + }, + "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.666666269302368, + "paddingLeft": 6, + "paddingRight": 4.666666507720947, + "paddingTop": 2.6666667461395264, + "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": 16, + "x": 75, + "y": 17, + "mainComponentId": "23:1619", + "mainComponentName": "속성 1=Right", + "mainComponentSetId": "20:1312" + }, + "extra": {}, + "fieldErrors": {} + }, + "24:1868": { + "id": "24:1868", + "type": "COMPONENT", + "fields": { + "parentId": "23:1613", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "24:1869", + "24:1870" + ], + "absoluteBoundingBox": { + "height": 50, + "width": 116, + "x": 8928, + "y": 1859 + }, + "absoluteRenderBounds": { + "height": 50, + "width": 116, + "x": 8928, + "y": 1859 + }, + "bottomLeftRadius": 12, + "bottomRightRadius": 12, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:345", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 12, + "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 + } + ], + "gridColumnAnchorIndex": 1, + "gridRowAnchorIndex": 3, + "height": 50, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 8, + "layoutAlign": "STRETCH", + "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": "FILL", + "maxHeight": null, + "maxWidth": null, + "name": "effect=hover, type=white-line, size=md", + "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": "hover", + "size": "md", + "type": "white-line" + }, + "visible": true, + "width": 116, + "x": 153, + "y": 215 + }, + "extra": {}, + "fieldErrors": {} + }, + "24:1869": { + "id": "24:1869", + "type": "TEXT", + "fields": { + "parentId": "24:1868", + "absoluteBoundingBox": { + "height": 24, + "width": 42, + "x": 8953, + "y": 1872 + }, + "absoluteRenderBounds": { + "height": 14.2879638671875, + "width": 38.6884765625, + "x": 8954.072265625, + "y": 1877.0240478515625 + }, + "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": 42, + "x": 25, + "y": 13, + "styledTextSegments": [ + { + "characters": "버튼명", + "end": 3, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:cbc02b7831f02c3b00e92d3114823bdda982464e," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "24:1870": { + "id": "24:1870", + "type": "INSTANCE", + "fields": { + "parentId": "24:1868", + "childrenIds": [ + "I24:1870;23:1618" + ], + "absoluteBoundingBox": { + "height": 16, + "width": 16, + "x": 9003, + "y": 1876 + }, + "absoluteRenderBounds": { + "height": 16, + "width": 16, + "x": 9003, + "y": 1876 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "componentProperties": { + "속성 1": { + "boundVariables": {}, + "type": "VARIANT", + "value": "Right" + } + }, + "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.666666269302368, + "paddingLeft": 6, + "paddingRight": 4.666666507720947, + "paddingTop": 2.6666667461395264, + "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": 16, + "x": 75, + "y": 17, + "mainComponentId": "23:1619", + "mainComponentName": "속성 1=Right", + "mainComponentSetId": "20:1312" + }, + "extra": {}, + "fieldErrors": {} + }, + "24:1871": { + "id": "24:1871", + "type": "COMPONENT", + "fields": { + "parentId": "23:1613", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "24:1872", + "24:1873" + ], + "absoluteBoundingBox": { + "height": 50, + "width": 116, + "x": 9060, + "y": 1859 + }, + "absoluteRenderBounds": { + "height": 50, + "width": 116, + "x": 9060, + "y": 1859 + }, + "bottomLeftRadius": 12, + "bottomRightRadius": 12, + "boundVariables": { + "fills": [ + { + "id": "VariableID:23:1651", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 12, + "counterAxisAlignItems": "CENTER", + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:23:1651", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.6078431606292725, + "g": 0.2823529541492462, + "r": 0.003921568859368563 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "gridColumnAnchorIndex": 2, + "gridRowAnchorIndex": 3, + "height": 50, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 8, + "layoutAlign": "STRETCH", + "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": "FILL", + "maxHeight": null, + "maxWidth": null, + "name": "effect=active, type=white-line, size=md", + "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": "active", + "size": "md", + "type": "white-line" + }, + "visible": true, + "width": 116, + "x": 285, + "y": 215 + }, + "extra": {}, + "fieldErrors": {} + }, + "24:1872": { + "id": "24:1872", + "type": "TEXT", + "fields": { + "parentId": "24:1871", + "absoluteBoundingBox": { + "height": 24, + "width": 42, + "x": 9085, + "y": 1872 + }, + "absoluteRenderBounds": { + "height": 14.2879638671875, + "width": 38.6884765625, + "x": 9086.072265625, + "y": 1877.0240478515625 + }, + "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": 42, + "x": 25, + "y": 13, + "styledTextSegments": [ + { + "characters": "버튼명", + "end": 3, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:cbc02b7831f02c3b00e92d3114823bdda982464e," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "24:1873": { + "id": "24:1873", + "type": "INSTANCE", + "fields": { + "parentId": "24:1871", + "childrenIds": [ + "I24:1873;23:1618" + ], + "absoluteBoundingBox": { + "height": 16, + "width": 16, + "x": 9135, + "y": 1876 + }, + "absoluteRenderBounds": { + "height": 16, + "width": 16, + "x": 9135, + "y": 1876 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "componentProperties": { + "속성 1": { + "boundVariables": {}, + "type": "VARIANT", + "value": "Right" + } + }, + "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.666666269302368, + "paddingLeft": 6, + "paddingRight": 4.666666507720947, + "paddingTop": 2.6666667461395264, + "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": 16, + "x": 75, + "y": 17, + "mainComponentId": "23:1619", + "mainComponentName": "속성 1=Right", + "mainComponentSetId": "20:1312" + }, + "extra": {}, + "fieldErrors": {} + }, + "24:1933": { + "id": "24:1933", + "type": "COMPONENT", + "fields": { + "parentId": "23:1613", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "24:1934", + "24:1935" + ], + "absoluteBoundingBox": { + "height": 33, + "width": 84, + "x": 8796, + "y": 1974 + }, + "absoluteRenderBounds": { + "height": 33, + "width": 84, + "x": 8796, + "y": 1974 + }, + "bottomLeftRadius": 8, + "bottomRightRadius": 8, + "boundVariables": { + "strokes": [ + { + "id": "VariableID:9:351", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 8, + "counterAxisAlignItems": "CENTER", + "gridColumnAnchorIndex": 0, + "gridRowAnchorIndex": 5, + "height": 33, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 8, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 6, + "paddingLeft": 12, + "paddingRight": 12, + "paddingTop": 6, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 8, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "FILL", + "maxHeight": null, + "maxWidth": null, + "name": "effect=default, type=ghost, size=sm", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 6, + "paddingLeft": 12, + "paddingRight": 12, + "paddingTop": 6, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 0, + "strokeLeftWeight": 0, + "strokeRightWeight": 0, + "strokeTopWeight": 0, + "strokeWeight": 0, + "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": 8, + "topRightRadius": 8, + "variantProperties": { + "effect": "default", + "size": "sm", + "type": "ghost" + }, + "visible": true, + "width": 84, + "x": 21, + "y": 330 + }, + "extra": {}, + "fieldErrors": {} + }, + "24:1934": { + "id": "24:1934", + "type": "TEXT", + "fields": { + "parentId": "24:1933", + "absoluteBoundingBox": { + "height": 21, + "width": 36, + "x": 8808, + "y": 1980 + }, + "absoluteRenderBounds": { + "height": 12.501953125, + "width": 33.572265625, + "x": 8808.9384765625, + "y": 1984.64599609375 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:13:153", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:956", + "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": 14, + "height": 21, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -3 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "버튼명", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:1649d6614e863b73049b737254cc0e54ab553c77,", + "textTruncation": "DISABLED", + "visible": true, + "width": 36, + "x": 12, + "y": 6, + "styledTextSegments": [ + { + "characters": "버튼명", + "end": 3, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:1649d6614e863b73049b737254cc0e54ab553c77," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "24:1935": { + "id": "24:1935", + "type": "INSTANCE", + "fields": { + "parentId": "24:1933", + "childrenIds": [ + "I24:1935;23:1618" + ], + "absoluteBoundingBox": { + "height": 16, + "width": 16, + "x": 8852, + "y": 1982.5 + }, + "absoluteRenderBounds": { + "height": 16, + "width": 16, + "x": 8852, + "y": 1982.5 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "componentProperties": { + "속성 1": { + "boundVariables": {}, + "type": "VARIANT", + "value": "Right" + } + }, + "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.666666269302368, + "paddingLeft": 6, + "paddingRight": 4.666666507720947, + "paddingTop": 2.6666667461395264, + "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": 16, + "x": 56, + "y": 8.5, + "mainComponentId": "23:1619", + "mainComponentName": "속성 1=Right", + "mainComponentSetId": "20:1312" + }, + "extra": {}, + "fieldErrors": {} + }, + "24:1940": { + "id": "24:1940", + "type": "COMPONENT", + "fields": { + "parentId": "23:1613", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "24:1941", + "24:1942" + ], + "absoluteBoundingBox": { + "height": 33, + "width": 84, + "x": 9192, + "y": 1974 + }, + "absoluteRenderBounds": { + "height": 33, + "width": 84, + "x": 9192, + "y": 1974 + }, + "bottomLeftRadius": 8, + "bottomRightRadius": 8, + "boundVariables": { + "strokes": [ + { + "id": "VariableID:9:351", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 8, + "counterAxisAlignItems": "CENTER", + "gridColumnAnchorIndex": 3, + "gridRowAnchorIndex": 5, + "height": 33, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 8, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 6, + "paddingLeft": 12, + "paddingRight": 12, + "paddingTop": 6, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 8, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "FILL", + "maxHeight": null, + "maxWidth": null, + "name": "effect=disabled, type=ghost, size=sm", + "opacity": 0.4000000059604645, + "overflowDirection": "NONE", + "paddingBottom": 6, + "paddingLeft": 12, + "paddingRight": 12, + "paddingTop": 6, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 0, + "strokeLeftWeight": 0, + "strokeRightWeight": 0, + "strokeTopWeight": 0, + "strokeWeight": 0, + "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": 8, + "topRightRadius": 8, + "variantProperties": { + "effect": "disabled", + "size": "sm", + "type": "ghost" + }, + "visible": true, + "width": 84, + "x": 417, + "y": 330 + }, + "extra": {}, + "fieldErrors": {} + }, + "24:1941": { + "id": "24:1941", + "type": "TEXT", + "fields": { + "parentId": "24:1940", + "absoluteBoundingBox": { + "height": 21, + "width": 36, + "x": 9204, + "y": 1980 + }, + "absoluteRenderBounds": { + "height": 12.501953125, + "width": 33.572265625, + "x": 9204.9384765625, + "y": 1984.64599609375 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:351", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:956", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "버튼명", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "fills": [ + { + "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 + } + ], + "fontName": { + "family": "SUIT", + "style": "Bold" + }, + "fontSize": 14, + "height": 21, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -3 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "버튼명", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:1649d6614e863b73049b737254cc0e54ab553c77,", + "textTruncation": "DISABLED", + "visible": true, + "width": 36, + "x": 12, + "y": 6, + "styledTextSegments": [ + { + "characters": "버튼명", + "end": 3, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:1649d6614e863b73049b737254cc0e54ab553c77," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "24:1942": { + "id": "24:1942", + "type": "INSTANCE", + "fields": { + "parentId": "24:1940", + "childrenIds": [ + "I24:1942;23:1618" + ], + "absoluteBoundingBox": { + "height": 16, + "width": 16, + "x": 9248, + "y": 1982.5 + }, + "absoluteRenderBounds": { + "height": 16, + "width": 16, + "x": 9248, + "y": 1982.5 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "componentProperties": { + "속성 1": { + "boundVariables": {}, + "type": "VARIANT", + "value": "Right" + } + }, + "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.666666269302368, + "paddingLeft": 6, + "paddingRight": 4.666666507720947, + "paddingTop": 2.6666667461395264, + "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": 16, + "x": 56, + "y": 8.5, + "mainComponentId": "23:1619", + "mainComponentName": "속성 1=Right", + "mainComponentSetId": "20:1312" + }, + "extra": {}, + "fieldErrors": {} + }, + "30:368": { + "id": "30:368", + "type": "COMPONENT", + "fields": { + "parentId": "23:1613", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "30:369", + "30:370" + ], + "absoluteBoundingBox": { + "height": 48, + "width": 114, + "x": 8796, + "y": 1729 + }, + "absoluteRenderBounds": { + "height": 48, + "width": 114, + "x": 8796, + "y": 1729 + }, + "bottomLeftRadius": 12, + "bottomRightRadius": 12, + "boundVariables": { + "strokes": [ + { + "id": "VariableID:9:351", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 12, + "counterAxisAlignItems": "CENTER", + "gridColumnAnchorIndex": 0, + "gridRowAnchorIndex": 1, + "height": 48, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 8, + "layoutAlign": "STRETCH", + "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": "FILL", + "maxHeight": null, + "maxWidth": null, + "name": "effect=default, type=ghost, size=md", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 12, + "paddingLeft": 24, + "paddingRight": 24, + "paddingTop": 12, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 0, + "strokeLeftWeight": 0, + "strokeRightWeight": 0, + "strokeTopWeight": 0, + "strokeWeight": 0, + "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": 12, + "topRightRadius": 12, + "variantProperties": { + "effect": "default", + "size": "md", + "type": "ghost" + }, + "visible": true, + "width": 114, + "x": 21, + "y": 85 + }, + "extra": {}, + "fieldErrors": {} + }, + "30:369": { + "id": "30:369", + "type": "TEXT", + "fields": { + "parentId": "30:368", + "absoluteBoundingBox": { + "height": 24, + "width": 42, + "x": 8820, + "y": 1741 + }, + "absoluteRenderBounds": { + "height": 14.2879638671875, + "width": 38.6884765625, + "x": 8821.072265625, + "y": 1746.0240478515625 + }, + "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": 42, + "x": 24, + "y": 12, + "styledTextSegments": [ + { + "characters": "버튼명", + "end": 3, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:cbc02b7831f02c3b00e92d3114823bdda982464e," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "30:370": { + "id": "30:370", + "type": "INSTANCE", + "fields": { + "parentId": "30:368", + "childrenIds": [ + "I30:370;23:1618" + ], + "absoluteBoundingBox": { + "height": 16, + "width": 16, + "x": 8870, + "y": 1745 + }, + "absoluteRenderBounds": { + "height": 16, + "width": 16, + "x": 8870, + "y": 1745 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "componentProperties": { + "속성 1": { + "boundVariables": {}, + "type": "VARIANT", + "value": "Right" + } + }, + "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.666666269302368, + "paddingLeft": 6, + "paddingRight": 4.666666507720947, + "paddingTop": 2.6666667461395264, + "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": 16, + "x": 74, + "y": 16, + "mainComponentId": "23:1619", + "mainComponentName": "속성 1=Right", + "mainComponentSetId": "20:1312" + }, + "extra": {}, + "fieldErrors": {} + }, + "30:371": { + "id": "30:371", + "type": "COMPONENT", + "fields": { + "parentId": "23:1613", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "30:372", + "30:373" + ], + "absoluteBoundingBox": { + "height": 48, + "width": 114, + "x": 8928, + "y": 1729 + }, + "absoluteRenderBounds": { + "height": 48, + "width": 114, + "x": 8928, + "y": 1729 + }, + "bottomLeftRadius": 12, + "bottomRightRadius": 12, + "boundVariables": { + "fills": [ + { + "id": "VariableID:24:1672", + "type": "VARIABLE_ALIAS" + } + ], + "strokes": [ + { + "id": "VariableID:9:359", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 12, + "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 + } + ], + "gridColumnAnchorIndex": 1, + "gridRowAnchorIndex": 1, + "height": 48, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 8, + "layoutAlign": "STRETCH", + "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": "FILL", + "maxHeight": null, + "maxWidth": null, + "name": "effect=hover, type=ghost, size=md", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 12, + "paddingLeft": 24, + "paddingRight": 24, + "paddingTop": 12, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 0, + "strokeLeftWeight": 0, + "strokeRightWeight": 0, + "strokeTopWeight": 0, + "strokeWeight": 0, + "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 + } + ], + "topLeftRadius": 12, + "topRightRadius": 12, + "variantProperties": { + "effect": "hover", + "size": "md", + "type": "ghost" + }, + "visible": true, + "width": 114, + "x": 153, + "y": 85 + }, + "extra": {}, + "fieldErrors": {} + }, + "30:372": { + "id": "30:372", + "type": "TEXT", + "fields": { + "parentId": "30:371", + "absoluteBoundingBox": { + "height": 24, + "width": 42, + "x": 8952, + "y": 1741 + }, + "absoluteRenderBounds": { + "height": 14.2879638671875, + "width": 38.6884765625, + "x": 8953.072265625, + "y": 1746.0240478515625 + }, + "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": 24, + "y": 12, + "styledTextSegments": [ + { + "characters": "버튼명", + "end": 3, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:cbc02b7831f02c3b00e92d3114823bdda982464e," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "30:373": { + "id": "30:373", + "type": "INSTANCE", + "fields": { + "parentId": "30:371", + "childrenIds": [ + "I30:373;23:1618" + ], + "absoluteBoundingBox": { + "height": 16, + "width": 16, + "x": 9002, + "y": 1745 + }, + "absoluteRenderBounds": { + "height": 16, + "width": 16, + "x": 9002, + "y": 1745 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "componentProperties": { + "속성 1": { + "boundVariables": {}, + "type": "VARIANT", + "value": "Right" + } + }, + "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.666666269302368, + "paddingLeft": 6, + "paddingRight": 4.666666507720947, + "paddingTop": 2.6666667461395264, + "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": 16, + "x": 74, + "y": 16, + "mainComponentId": "23:1619", + "mainComponentName": "속성 1=Right", + "mainComponentSetId": "20:1312" + }, + "extra": {}, + "fieldErrors": {} + }, + "30:374": { + "id": "30:374", + "type": "COMPONENT", + "fields": { + "parentId": "23:1613", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "30:375", + "30:376" + ], + "absoluteBoundingBox": { + "height": 48, + "width": 114, + "x": 9060, + "y": 1729 + }, + "absoluteRenderBounds": { + "height": 48, + "width": 114, + "x": 9060, + "y": 1729 + }, + "bottomLeftRadius": 12, + "bottomRightRadius": 12, + "boundVariables": { + "fills": [ + { + "id": "VariableID:24:1680", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 12, + "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 + } + ], + "gridColumnAnchorIndex": 2, + "gridRowAnchorIndex": 1, + "height": 48, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 8, + "layoutAlign": "STRETCH", + "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": "FILL", + "maxHeight": null, + "maxWidth": null, + "name": "effect=active, type=ghost, size=md", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 12, + "paddingLeft": 24, + "paddingRight": 24, + "paddingTop": 12, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "topLeftRadius": 12, + "topRightRadius": 12, + "variantProperties": { + "effect": "active", + "size": "md", + "type": "ghost" + }, + "visible": true, + "width": 114, + "x": 285, + "y": 85 + }, + "extra": {}, + "fieldErrors": {} + }, + "30:375": { + "id": "30:375", + "type": "TEXT", + "fields": { + "parentId": "30:374", + "absoluteBoundingBox": { + "height": 24, + "width": 42, + "x": 9084, + "y": 1741 + }, + "absoluteRenderBounds": { + "height": 14.2879638671875, + "width": 38.6884765625, + "x": 9085.072265625, + "y": 1746.0240478515625 + }, + "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": 24, + "y": 12, + "styledTextSegments": [ + { + "characters": "버튼명", + "end": 3, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:cbc02b7831f02c3b00e92d3114823bdda982464e," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "30:376": { + "id": "30:376", + "type": "INSTANCE", + "fields": { + "parentId": "30:374", + "childrenIds": [ + "I30:376;23:1618" + ], + "absoluteBoundingBox": { + "height": 16, + "width": 16, + "x": 9134, + "y": 1745 + }, + "absoluteRenderBounds": { + "height": 16, + "width": 16, + "x": 9134, + "y": 1745 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "componentProperties": { + "속성 1": { + "boundVariables": {}, + "type": "VARIANT", + "value": "Right" + } + }, + "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.666666269302368, + "paddingLeft": 6, + "paddingRight": 4.666666507720947, + "paddingTop": 2.6666667461395264, + "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": 16, + "x": 74, + "y": 16, + "mainComponentId": "23:1619", + "mainComponentName": "속성 1=Right", + "mainComponentSetId": "20:1312" + }, + "extra": {}, + "fieldErrors": {} + }, + "30:377": { + "id": "30:377", + "type": "COMPONENT", + "fields": { + "parentId": "23:1613", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "30:378", + "30:379" + ], + "absoluteBoundingBox": { + "height": 48, + "width": 114, + "x": 9192, + "y": 1729 + }, + "absoluteRenderBounds": { + "height": 48, + "width": 114, + "x": 9192, + "y": 1729 + }, + "bottomLeftRadius": 12, + "bottomRightRadius": 12, + "boundVariables": { + "strokes": [ + { + "id": "VariableID:9:351", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 12, + "counterAxisAlignItems": "CENTER", + "gridColumnAnchorIndex": 3, + "gridRowAnchorIndex": 1, + "height": 48, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 8, + "layoutAlign": "STRETCH", + "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": "FILL", + "maxHeight": null, + "maxWidth": null, + "name": "effect=disabled, type=ghost, size=md", + "opacity": 0.4000000059604645, + "overflowDirection": "NONE", + "paddingBottom": 12, + "paddingLeft": 24, + "paddingRight": 24, + "paddingTop": 12, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 0, + "strokeLeftWeight": 0, + "strokeRightWeight": 0, + "strokeTopWeight": 0, + "strokeWeight": 0, + "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": 12, + "topRightRadius": 12, + "variantProperties": { + "effect": "disabled", + "size": "md", + "type": "ghost" + }, + "visible": true, + "width": 114, + "x": 417, + "y": 85 + }, + "extra": {}, + "fieldErrors": {} + }, + "30:378": { + "id": "30:378", + "type": "TEXT", + "fields": { + "parentId": "30:377", + "absoluteBoundingBox": { + "height": 24, + "width": 42, + "x": 9216, + "y": 1741 + }, + "absoluteRenderBounds": { + "height": 14.2879638671875, + "width": 38.6884765625, + "x": 9217.072265625, + "y": 1746.0240478515625 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:351", + "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:351", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.9215686321258544, + "g": 0.9058823585510254, + "r": 0.8980392217636108 + }, + "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": 24, + "y": 12, + "styledTextSegments": [ + { + "characters": "버튼명", + "end": 3, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:cbc02b7831f02c3b00e92d3114823bdda982464e," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "30:379": { + "id": "30:379", + "type": "INSTANCE", + "fields": { + "parentId": "30:377", + "childrenIds": [ + "I30:379;23:1618" + ], + "absoluteBoundingBox": { + "height": 16, + "width": 16, + "x": 9266, + "y": 1745 + }, + "absoluteRenderBounds": { + "height": 16, + "width": 16, + "x": 9266, + "y": 1745 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "componentProperties": { + "속성 1": { + "boundVariables": {}, + "type": "VARIANT", + "value": "Right" + } + }, + "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.666666269302368, + "paddingLeft": 6, + "paddingRight": 4.666666507720947, + "paddingTop": 2.6666667461395264, + "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": 16, + "x": 74, + "y": 16, + "mainComponentId": "23:1619", + "mainComponentName": "속성 1=Right", + "mainComponentSetId": "20:1312" + }, + "extra": {}, + "fieldErrors": {} + }, + "30:383": { + "id": "30:383", + "type": "COMPONENT", + "fields": { + "parentId": "23:1613", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "30:384", + "30:385" + ], + "absoluteBoundingBox": { + "height": 50, + "width": 116, + "x": 9192, + "y": 1859 + }, + "absoluteRenderBounds": { + "height": 50, + "width": 116, + "x": 9192, + "y": 1859 + }, + "bottomLeftRadius": 12, + "bottomRightRadius": 12, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 12, + "counterAxisAlignItems": "CENTER", + "gridColumnAnchorIndex": 3, + "gridRowAnchorIndex": 3, + "height": 50, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 8, + "layoutAlign": "STRETCH", + "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": "FILL", + "maxHeight": null, + "maxWidth": null, + "name": "effect=disabled, type=white-line, size=md", + "opacity": 0.4000000059604645, + "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": "disabled", + "size": "md", + "type": "white-line" + }, + "visible": true, + "width": 116, + "x": 417, + "y": 215 + }, + "extra": {}, + "fieldErrors": {} + }, + "30:384": { + "id": "30:384", + "type": "TEXT", + "fields": { + "parentId": "30:383", + "absoluteBoundingBox": { + "height": 24, + "width": 42, + "x": 9217, + "y": 1872 + }, + "absoluteRenderBounds": { + "height": 14.2879638671875, + "width": 38.6884765625, + "x": 9218.072265625, + "y": 1877.0240478515625 + }, + "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": 42, + "x": 25, + "y": 13, + "styledTextSegments": [ + { + "characters": "버튼명", + "end": 3, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:cbc02b7831f02c3b00e92d3114823bdda982464e," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "30:385": { + "id": "30:385", + "type": "INSTANCE", + "fields": { + "parentId": "30:383", + "childrenIds": [ + "I30:385;23:1618" + ], + "absoluteBoundingBox": { + "height": 16, + "width": 16, + "x": 9267, + "y": 1876 + }, + "absoluteRenderBounds": { + "height": 16, + "width": 16, + "x": 9267, + "y": 1876 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "componentProperties": { + "속성 1": { + "boundVariables": {}, + "type": "VARIANT", + "value": "Right" + } + }, + "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.666666269302368, + "paddingLeft": 6, + "paddingRight": 4.666666507720947, + "paddingTop": 2.6666667461395264, + "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": 16, + "x": 75, + "y": 17, + "mainComponentId": "23:1619", + "mainComponentName": "속성 1=Right", + "mainComponentSetId": "20:1312" + }, + "extra": {}, + "fieldErrors": {} + }, + "30:399": { + "id": "30:399", + "type": "COMPONENT", + "fields": { + "parentId": "23:1613", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "30:400", + "30:401" + ], + "absoluteBoundingBox": { + "height": 50, + "width": 116, + "x": 8796, + "y": 1793 + }, + "absoluteRenderBounds": { + "height": 50, + "width": 116, + "x": 8796, + "y": 1793 + }, + "bottomLeftRadius": 12, + "bottomRightRadius": 12, + "boundVariables": { + "fills": [ + { + "id": "VariableID:13:161", + "type": "VARIABLE_ALIAS" + } + ], + "strokes": [ + { + "id": "VariableID:9:351", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 12, + "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 + } + ], + "gridColumnAnchorIndex": 0, + "gridRowAnchorIndex": 2, + "height": 50, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 8, + "layoutAlign": "STRETCH", + "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": "FILL", + "maxHeight": null, + "maxWidth": null, + "name": "effect=default, type=white, size=md", + "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": { + "id": "VariableID:9:351", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.9215686321258544, + "g": 0.9058823585510254, + "r": 0.8980392217636108 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "topLeftRadius": 12, + "topRightRadius": 12, + "variantProperties": { + "effect": "default", + "size": "md", + "type": "white" + }, + "visible": true, + "width": 116, + "x": 21, + "y": 149 + }, + "extra": {}, + "fieldErrors": {} + }, + "30:400": { + "id": "30:400", + "type": "TEXT", + "fields": { + "parentId": "30:399", + "absoluteBoundingBox": { + "height": 24, + "width": 42, + "x": 8821, + "y": 1806 + }, + "absoluteRenderBounds": { + "height": 14.2879638671875, + "width": 38.6884765625, + "x": 8822.072265625, + "y": 1811.0240478515625 + }, + "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": 25, + "y": 13, + "styledTextSegments": [ + { + "characters": "버튼명", + "end": 3, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:cbc02b7831f02c3b00e92d3114823bdda982464e," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "30:401": { + "id": "30:401", + "type": "INSTANCE", + "fields": { + "parentId": "30:399", + "childrenIds": [ + "I30:401;23:1618" + ], + "absoluteBoundingBox": { + "height": 16, + "width": 16, + "x": 8871, + "y": 1810 + }, + "absoluteRenderBounds": { + "height": 16, + "width": 16, + "x": 8871, + "y": 1810 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "componentProperties": { + "속성 1": { + "boundVariables": {}, + "type": "VARIANT", + "value": "Right" + } + }, + "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.666666269302368, + "paddingLeft": 6, + "paddingRight": 4.666666507720947, + "paddingTop": 2.6666667461395264, + "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": 16, + "x": 75, + "y": 17, + "mainComponentId": "23:1619", + "mainComponentName": "속성 1=Right", + "mainComponentSetId": "20:1312" + }, + "extra": {}, + "fieldErrors": {} + }, + "30:402": { + "id": "30:402", + "type": "COMPONENT", + "fields": { + "parentId": "23:1613", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "30:403", + "30:404" + ], + "absoluteBoundingBox": { + "height": 50, + "width": 116, + "x": 8928, + "y": 1793 + }, + "absoluteRenderBounds": { + "height": 50, + "width": 116, + "x": 8928, + "y": 1793 + }, + "bottomLeftRadius": 12, + "bottomRightRadius": 12, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:349", + "type": "VARIABLE_ALIAS" + } + ], + "strokes": [ + { + "id": "VariableID:9:359", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 12, + "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 + } + ], + "gridColumnAnchorIndex": 1, + "gridRowAnchorIndex": 2, + "height": 50, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 8, + "layoutAlign": "STRETCH", + "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": "FILL", + "maxHeight": null, + "maxWidth": null, + "name": "effect=hover, type=white, size=md", + "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": { + "id": "VariableID:9:359", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.6682692170143127, + "g": 0.6104382276535034, + "r": 0.5815227627754211 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "topLeftRadius": 12, + "topRightRadius": 12, + "variantProperties": { + "effect": "hover", + "size": "md", + "type": "white" + }, + "visible": true, + "width": 116, + "x": 153, + "y": 149 + }, + "extra": {}, + "fieldErrors": {} + }, + "30:403": { + "id": "30:403", + "type": "TEXT", + "fields": { + "parentId": "30:402", + "absoluteBoundingBox": { + "height": 24, + "width": 42, + "x": 8953, + "y": 1806 + }, + "absoluteRenderBounds": { + "height": 14.2879638671875, + "width": 38.6884765625, + "x": 8954.072265625, + "y": 1811.0240478515625 + }, + "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": 25, + "y": 13, + "styledTextSegments": [ + { + "characters": "버튼명", + "end": 3, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:cbc02b7831f02c3b00e92d3114823bdda982464e," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "30:404": { + "id": "30:404", + "type": "INSTANCE", + "fields": { + "parentId": "30:402", + "childrenIds": [ + "I30:404;23:1618" + ], + "absoluteBoundingBox": { + "height": 16, + "width": 16, + "x": 9003, + "y": 1810 + }, + "absoluteRenderBounds": { + "height": 16, + "width": 16, + "x": 9003, + "y": 1810 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "componentProperties": { + "속성 1": { + "boundVariables": {}, + "type": "VARIANT", + "value": "Right" + } + }, + "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.666666269302368, + "paddingLeft": 6, + "paddingRight": 4.666666507720947, + "paddingTop": 2.6666667461395264, + "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": 16, + "x": 75, + "y": 17, + "mainComponentId": "23:1619", + "mainComponentName": "속성 1=Right", + "mainComponentSetId": "20:1312" + }, + "extra": {}, + "fieldErrors": {} + }, + "30:415": { + "id": "30:415", + "type": "COMPONENT", + "fields": { + "parentId": "23:1613", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "30:416", + "30:417" + ], + "absoluteBoundingBox": { + "height": 50, + "width": 116, + "x": 9192, + "y": 1793 + }, + "absoluteRenderBounds": { + "height": 50, + "width": 116, + "x": 9192, + "y": 1793 + }, + "bottomLeftRadius": 12, + "bottomRightRadius": 12, + "boundVariables": { + "fills": [ + { + "id": "VariableID:13:161", + "type": "VARIABLE_ALIAS" + } + ], + "strokes": [ + { + "id": "VariableID:9:351", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 12, + "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 + } + ], + "gridColumnAnchorIndex": 3, + "gridRowAnchorIndex": 2, + "height": 50, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 8, + "layoutAlign": "STRETCH", + "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": "FILL", + "maxHeight": null, + "maxWidth": null, + "name": "effect=disabled, type=white, size=md", + "opacity": 0.4000000059604645, + "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": { + "id": "VariableID:9:351", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.9215686321258544, + "g": 0.9058823585510254, + "r": 0.8980392217636108 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "topLeftRadius": 12, + "topRightRadius": 12, + "variantProperties": { + "effect": "disabled", + "size": "md", + "type": "white" + }, + "visible": true, + "width": 116, + "x": 417, + "y": 149 + }, + "extra": {}, + "fieldErrors": {} + }, + "30:416": { + "id": "30:416", + "type": "TEXT", + "fields": { + "parentId": "30:415", + "absoluteBoundingBox": { + "height": 24, + "width": 42, + "x": 9217, + "y": 1806 + }, + "absoluteRenderBounds": { + "height": 14.2879638671875, + "width": 38.6884765625, + "x": 9218.072265625, + "y": 1811.0240478515625 + }, + "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": 25, + "y": 13, + "styledTextSegments": [ + { + "characters": "버튼명", + "end": 3, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:cbc02b7831f02c3b00e92d3114823bdda982464e," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "30:417": { + "id": "30:417", + "type": "INSTANCE", + "fields": { + "parentId": "30:415", + "childrenIds": [ + "I30:417;23:1618" + ], + "absoluteBoundingBox": { + "height": 16, + "width": 16, + "x": 9267, + "y": 1810 + }, + "absoluteRenderBounds": { + "height": 16, + "width": 16, + "x": 9267, + "y": 1810 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "componentProperties": { + "속성 1": { + "boundVariables": {}, + "type": "VARIANT", + "value": "Right" + } + }, + "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.666666269302368, + "paddingLeft": 6, + "paddingRight": 4.666666507720947, + "paddingTop": 2.6666667461395264, + "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": 16, + "x": 75, + "y": 17, + "mainComponentId": "23:1619", + "mainComponentName": "속성 1=Right", + "mainComponentSetId": "20:1312" + }, + "extra": {}, + "fieldErrors": {} + }, + "30:421": { + "id": "30:421", + "type": "COMPONENT", + "fields": { + "parentId": "23:1613", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "30:422", + "30:423" + ], + "absoluteBoundingBox": { + "height": 50, + "width": 116, + "x": 9060, + "y": 1793 + }, + "absoluteRenderBounds": { + "height": 50, + "width": 116, + "x": 9060, + "y": 1793 + }, + "bottomLeftRadius": 12, + "bottomRightRadius": 12, + "boundVariables": { + "fills": [ + { + "id": "VariableID:13:157", + "type": "VARIABLE_ALIAS" + } + ], + "strokes": [ + { + "id": "VariableID:30:411", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 12, + "counterAxisAlignItems": "CENTER", + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:13:157", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.9647058844566344, + "g": 0.95686274766922, + "r": 0.9529411792755128 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "gridColumnAnchorIndex": 2, + "gridRowAnchorIndex": 2, + "height": 50, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 8, + "layoutAlign": "STRETCH", + "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": "FILL", + "maxHeight": null, + "maxWidth": null, + "name": "effect=active, type=white, size=md", + "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": { + "id": "VariableID:30:411", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.8313725590705872, + "g": 0.7882353067398071, + "r": 0.772549033164978 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "topLeftRadius": 12, + "topRightRadius": 12, + "variantProperties": { + "effect": "active", + "size": "md", + "type": "white" + }, + "visible": true, + "width": 116, + "x": 285, + "y": 149 + }, + "extra": {}, + "fieldErrors": {} + }, + "30:422": { + "id": "30:422", + "type": "TEXT", + "fields": { + "parentId": "30:421", + "absoluteBoundingBox": { + "height": 24, + "width": 42, + "x": 9085, + "y": 1806 + }, + "absoluteRenderBounds": { + "height": 14.2879638671875, + "width": 38.6884765625, + "x": 9086.072265625, + "y": 1811.0240478515625 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:358", + "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: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": 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": 25, + "y": 13, + "styledTextSegments": [ + { + "characters": "버튼명", + "end": 3, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:cbc02b7831f02c3b00e92d3114823bdda982464e," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "30:423": { + "id": "30:423", + "type": "INSTANCE", + "fields": { + "parentId": "30:421", + "childrenIds": [ + "I30:423;23:1618" + ], + "absoluteBoundingBox": { + "height": 16, + "width": 16, + "x": 9135, + "y": 1810 + }, + "absoluteRenderBounds": { + "height": 16, + "width": 16, + "x": 9135, + "y": 1810 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "componentProperties": { + "속성 1": { + "boundVariables": {}, + "type": "VARIANT", + "value": "Right" + } + }, + "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.666666269302368, + "paddingLeft": 6, + "paddingRight": 4.666666507720947, + "paddingTop": 2.6666667461395264, + "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": 16, + "x": 75, + "y": 17, + "mainComponentId": "23:1619", + "mainComponentName": "속성 1=Right", + "mainComponentSetId": "20:1312" + }, + "extra": {}, + "fieldErrors": {} + }, + "30:496": { + "id": "30:496", + "type": "COMPONENT", + "fields": { + "parentId": "23:1613", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "30:497", + "30:498" + ], + "absoluteBoundingBox": { + "height": 35, + "width": 86, + "x": 8796, + "y": 2023 + }, + "absoluteRenderBounds": { + "height": 35, + "width": 86, + "x": 8796, + "y": 2023 + }, + "bottomLeftRadius": 8, + "bottomRightRadius": 8, + "boundVariables": { + "fills": [ + { + "id": "VariableID:13:161", + "type": "VARIABLE_ALIAS" + } + ], + "strokes": [ + { + "id": "VariableID:9:351", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 8, + "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 + } + ], + "gridColumnAnchorIndex": 0, + "gridRowAnchorIndex": 6, + "height": 35, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 8, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 6, + "paddingLeft": 12, + "paddingRight": 12, + "paddingTop": 6, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 8, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "FILL", + "maxHeight": null, + "maxWidth": null, + "name": "effect=default, type=white, size=sm", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 6, + "paddingLeft": 12, + "paddingRight": 12, + "paddingTop": 6, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "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": 8, + "topRightRadius": 8, + "variantProperties": { + "effect": "default", + "size": "sm", + "type": "white" + }, + "visible": true, + "width": 86, + "x": 21, + "y": 379 + }, + "extra": {}, + "fieldErrors": {} + }, + "30:497": { + "id": "30:497", + "type": "TEXT", + "fields": { + "parentId": "30:496", + "absoluteBoundingBox": { + "height": 21, + "width": 36, + "x": 8809, + "y": 2030 + }, + "absoluteRenderBounds": { + "height": 12.501953125, + "width": 33.572265625, + "x": 8809.9384765625, + "y": 2034.64599609375 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:348", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:956", + "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": 14, + "height": 21, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -3 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "버튼명", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:1649d6614e863b73049b737254cc0e54ab553c77,", + "textTruncation": "DISABLED", + "visible": true, + "width": 36, + "x": 13, + "y": 7, + "styledTextSegments": [ + { + "characters": "버튼명", + "end": 3, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:1649d6614e863b73049b737254cc0e54ab553c77," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "30:498": { + "id": "30:498", + "type": "INSTANCE", + "fields": { + "parentId": "30:496", + "childrenIds": [ + "I30:498;23:1618" + ], + "absoluteBoundingBox": { + "height": 16, + "width": 16, + "x": 8853, + "y": 2032.5 + }, + "absoluteRenderBounds": { + "height": 16, + "width": 16, + "x": 8853, + "y": 2032.5 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "componentProperties": { + "속성 1": { + "boundVariables": {}, + "type": "VARIANT", + "value": "Right" + } + }, + "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.666666269302368, + "paddingLeft": 6, + "paddingRight": 4.666666507720947, + "paddingTop": 2.6666667461395264, + "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": 16, + "x": 57, + "y": 9.5, + "mainComponentId": "23:1619", + "mainComponentName": "속성 1=Right", + "mainComponentSetId": "20:1312" + }, + "extra": {}, + "fieldErrors": {} + }, + "30:499": { + "id": "30:499", + "type": "COMPONENT", + "fields": { + "parentId": "23:1613", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "30:500", + "30:501" + ], + "absoluteBoundingBox": { + "height": 35, + "width": 86, + "x": 8928, + "y": 2023 + }, + "absoluteRenderBounds": { + "height": 35, + "width": 86, + "x": 8928, + "y": 2023 + }, + "bottomLeftRadius": 8, + "bottomRightRadius": 8, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:349", + "type": "VARIABLE_ALIAS" + } + ], + "strokes": [ + { + "id": "VariableID:9:359", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 8, + "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 + } + ], + "gridColumnAnchorIndex": 1, + "gridRowAnchorIndex": 6, + "height": 35, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 8, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 6, + "paddingLeft": 12, + "paddingRight": 12, + "paddingTop": 6, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 8, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "FILL", + "maxHeight": null, + "maxWidth": null, + "name": "effect=hover, type=white, size=sm", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 6, + "paddingLeft": 12, + "paddingRight": 12, + "paddingTop": 6, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "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 + } + ], + "topLeftRadius": 8, + "topRightRadius": 8, + "variantProperties": { + "effect": "hover", + "size": "sm", + "type": "white" + }, + "visible": true, + "width": 86, + "x": 153, + "y": 379 + }, + "extra": {}, + "fieldErrors": {} + }, + "30:500": { + "id": "30:500", + "type": "TEXT", + "fields": { + "parentId": "30:499", + "absoluteBoundingBox": { + "height": 21, + "width": 36, + "x": 8941, + "y": 2030 + }, + "absoluteRenderBounds": { + "height": 12.501953125, + "width": 33.572265625, + "x": 8941.9384765625, + "y": 2034.64599609375 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:348", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:956", + "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": 14, + "height": 21, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -3 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "버튼명", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:1649d6614e863b73049b737254cc0e54ab553c77,", + "textTruncation": "DISABLED", + "visible": true, + "width": 36, + "x": 13, + "y": 7, + "styledTextSegments": [ + { + "characters": "버튼명", + "end": 3, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:1649d6614e863b73049b737254cc0e54ab553c77," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "30:501": { + "id": "30:501", + "type": "INSTANCE", + "fields": { + "parentId": "30:499", + "childrenIds": [ + "I30:501;23:1618" + ], + "absoluteBoundingBox": { + "height": 16, + "width": 16, + "x": 8985, + "y": 2032.5 + }, + "absoluteRenderBounds": { + "height": 16, + "width": 16, + "x": 8985, + "y": 2032.5 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "componentProperties": { + "속성 1": { + "boundVariables": {}, + "type": "VARIANT", + "value": "Right" + } + }, + "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.666666269302368, + "paddingLeft": 6, + "paddingRight": 4.666666507720947, + "paddingTop": 2.6666667461395264, + "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": 16, + "x": 57, + "y": 9.5, + "mainComponentId": "23:1619", + "mainComponentName": "속성 1=Right", + "mainComponentSetId": "20:1312" + }, + "extra": {}, + "fieldErrors": {} + }, + "30:502": { + "id": "30:502", + "type": "COMPONENT", + "fields": { + "parentId": "23:1613", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "30:503", + "30:504" + ], + "absoluteBoundingBox": { + "height": 35, + "width": 86, + "x": 9060, + "y": 2023 + }, + "absoluteRenderBounds": { + "height": 35, + "width": 86, + "x": 9060, + "y": 2023 + }, + "bottomLeftRadius": 8, + "bottomRightRadius": 8, + "boundVariables": { + "fills": [ + { + "id": "VariableID:13:157", + "type": "VARIABLE_ALIAS" + } + ], + "strokes": [ + { + "id": "VariableID:30:411", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 8, + "counterAxisAlignItems": "CENTER", + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:13:157", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.9647058844566344, + "g": 0.95686274766922, + "r": 0.9529411792755128 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "gridColumnAnchorIndex": 2, + "gridRowAnchorIndex": 6, + "height": 35, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 8, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 6, + "paddingLeft": 12, + "paddingRight": 12, + "paddingTop": 6, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 8, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "FILL", + "maxHeight": null, + "maxWidth": null, + "name": "effect=active, type=white, size=sm", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 6, + "paddingLeft": 12, + "paddingRight": 12, + "paddingTop": 6, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:30:411", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.8313725590705872, + "g": 0.7882353067398071, + "r": 0.772549033164978 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "topLeftRadius": 8, + "topRightRadius": 8, + "variantProperties": { + "effect": "active", + "size": "sm", + "type": "white" + }, + "visible": true, + "width": 86, + "x": 285, + "y": 379 + }, + "extra": {}, + "fieldErrors": {} + }, + "30:503": { + "id": "30:503", + "type": "TEXT", + "fields": { + "parentId": "30:502", + "absoluteBoundingBox": { + "height": 21, + "width": 36, + "x": 9073, + "y": 2030 + }, + "absoluteRenderBounds": { + "height": 12.501953125, + "width": 33.572265625, + "x": 9073.9384765625, + "y": 2034.64599609375 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:956", + "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": 14, + "height": 21, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -3 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "버튼명", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:1649d6614e863b73049b737254cc0e54ab553c77,", + "textTruncation": "DISABLED", + "visible": true, + "width": 36, + "x": 13, + "y": 7, + "styledTextSegments": [ + { + "characters": "버튼명", + "end": 3, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:1649d6614e863b73049b737254cc0e54ab553c77," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "30:504": { + "id": "30:504", + "type": "INSTANCE", + "fields": { + "parentId": "30:502", + "childrenIds": [ + "I30:504;23:1618" + ], + "absoluteBoundingBox": { + "height": 16, + "width": 16, + "x": 9117, + "y": 2032.5 + }, + "absoluteRenderBounds": { + "height": 16, + "width": 16, + "x": 9117, + "y": 2032.5 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "componentProperties": { + "속성 1": { + "boundVariables": {}, + "type": "VARIANT", + "value": "Right" + } + }, + "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.666666269302368, + "paddingLeft": 6, + "paddingRight": 4.666666507720947, + "paddingTop": 2.6666667461395264, + "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": 16, + "x": 57, + "y": 9.5, + "mainComponentId": "23:1619", + "mainComponentName": "속성 1=Right", + "mainComponentSetId": "20:1312" + }, + "extra": {}, + "fieldErrors": {} + }, + "30:505": { + "id": "30:505", + "type": "COMPONENT", + "fields": { + "parentId": "23:1613", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "30:506", + "30:507" + ], + "absoluteBoundingBox": { + "height": 35, + "width": 86, + "x": 9192, + "y": 2023 + }, + "absoluteRenderBounds": { + "height": 35, + "width": 86, + "x": 9192, + "y": 2023 + }, + "bottomLeftRadius": 8, + "bottomRightRadius": 8, + "boundVariables": { + "fills": [ + { + "id": "VariableID:13:161", + "type": "VARIABLE_ALIAS" + } + ], + "strokes": [ + { + "id": "VariableID:9:351", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 8, + "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 + } + ], + "gridColumnAnchorIndex": 3, + "gridRowAnchorIndex": 6, + "height": 35, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 8, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 6, + "paddingLeft": 12, + "paddingRight": 12, + "paddingTop": 6, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 8, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "FILL", + "maxHeight": null, + "maxWidth": null, + "name": "effect=disabled, type=white, size=sm", + "opacity": 0.4000000059604645, + "overflowDirection": "NONE", + "paddingBottom": 6, + "paddingLeft": 12, + "paddingRight": 12, + "paddingTop": 6, + "primaryAxisAlignItems": "CENTER", + "strokeBottomWeight": 1, + "strokeLeftWeight": 1, + "strokeRightWeight": 1, + "strokeTopWeight": 1, + "strokeWeight": 1, + "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": 8, + "topRightRadius": 8, + "variantProperties": { + "effect": "disabled", + "size": "sm", + "type": "white" + }, + "visible": true, + "width": 86, + "x": 417, + "y": 379 + }, + "extra": {}, + "fieldErrors": {} + }, + "30:506": { + "id": "30:506", + "type": "TEXT", + "fields": { + "parentId": "30:505", + "absoluteBoundingBox": { + "height": 21, + "width": 36, + "x": 9205, + "y": 2030 + }, + "absoluteRenderBounds": { + "height": 12.501953125, + "width": 33.572265625, + "x": 9205.9384765625, + "y": 2034.64599609375 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:348", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:956", + "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": 14, + "height": 21, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -3 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "버튼명", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:1649d6614e863b73049b737254cc0e54ab553c77,", + "textTruncation": "DISABLED", + "visible": true, + "width": 36, + "x": 13, + "y": 7, + "styledTextSegments": [ + { + "characters": "버튼명", + "end": 3, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:1649d6614e863b73049b737254cc0e54ab553c77," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "30:507": { + "id": "30:507", + "type": "INSTANCE", + "fields": { + "parentId": "30:505", + "childrenIds": [ + "I30:507;23:1618" + ], + "absoluteBoundingBox": { + "height": 16, + "width": 16, + "x": 9249, + "y": 2032.5 + }, + "absoluteRenderBounds": { + "height": 16, + "width": 16, + "x": 9249, + "y": 2032.5 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "componentProperties": { + "속성 1": { + "boundVariables": {}, + "type": "VARIANT", + "value": "Right" + } + }, + "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.666666269302368, + "paddingLeft": 6, + "paddingRight": 4.666666507720947, + "paddingTop": 2.6666667461395264, + "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": 16, + "x": 57, + "y": 9.5, + "mainComponentId": "23:1619", + "mainComponentName": "속성 1=Right", + "mainComponentSetId": "20:1312" + }, + "extra": {}, + "fieldErrors": {} + }, + "30:508": { + "id": "30:508", + "type": "COMPONENT", + "fields": { + "parentId": "23:1613", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "30:509", + "30:510" + ], + "absoluteBoundingBox": { + "height": 35, + "width": 86, + "x": 8796, + "y": 2074 + }, + "absoluteRenderBounds": { + "height": 35, + "width": 86, + "x": 8796, + "y": 2074 + }, + "bottomLeftRadius": 8, + "bottomRightRadius": 8, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 8, + "counterAxisAlignItems": "CENTER", + "gridColumnAnchorIndex": 0, + "gridRowAnchorIndex": 7, + "height": 35, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 8, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 6, + "paddingLeft": 12, + "paddingRight": 12, + "paddingTop": 6, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 8, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "FILL", + "maxHeight": null, + "maxWidth": null, + "name": "effect=default, type=white-line, size=sm", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 6, + "paddingLeft": 12, + "paddingRight": 12, + "paddingTop": 6, + "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": 8, + "topRightRadius": 8, + "variantProperties": { + "effect": "default", + "size": "sm", + "type": "white-line" + }, + "visible": true, + "width": 86, + "x": 21, + "y": 430 + }, + "extra": {}, + "fieldErrors": {} + }, + "30:509": { + "id": "30:509", + "type": "TEXT", + "fields": { + "parentId": "30:508", + "absoluteBoundingBox": { + "height": 21, + "width": 36, + "x": 8809, + "y": 2081 + }, + "absoluteRenderBounds": { + "height": 12.501953125, + "width": 33.572265625, + "x": 8809.9384765625, + "y": 2085.64599609375 + }, + "boundVariables": { + "fontSize": [ + { + "id": "VariableID:18:956", + "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": 14, + "height": 21, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -3 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "버튼명", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:1649d6614e863b73049b737254cc0e54ab553c77,", + "textTruncation": "DISABLED", + "visible": true, + "width": 36, + "x": 13, + "y": 7, + "styledTextSegments": [ + { + "characters": "버튼명", + "end": 3, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:1649d6614e863b73049b737254cc0e54ab553c77," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "30:510": { + "id": "30:510", + "type": "INSTANCE", + "fields": { + "parentId": "30:508", + "childrenIds": [ + "I30:510;23:1618" + ], + "absoluteBoundingBox": { + "height": 16, + "width": 16, + "x": 8853, + "y": 2083.5 + }, + "absoluteRenderBounds": { + "height": 16, + "width": 16, + "x": 8853, + "y": 2083.5 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "componentProperties": { + "속성 1": { + "boundVariables": {}, + "type": "VARIANT", + "value": "Right" + } + }, + "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.666666269302368, + "paddingLeft": 6, + "paddingRight": 4.666666507720947, + "paddingTop": 2.6666667461395264, + "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": 16, + "x": 57, + "y": 9.5, + "mainComponentId": "23:1619", + "mainComponentName": "속성 1=Right", + "mainComponentSetId": "20:1312" + }, + "extra": {}, + "fieldErrors": {} + }, + "30:511": { + "id": "30:511", + "type": "COMPONENT", + "fields": { + "parentId": "23:1613", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "30:512", + "30:513" + ], + "absoluteBoundingBox": { + "height": 35, + "width": 86, + "x": 8928, + "y": 2074 + }, + "absoluteRenderBounds": { + "height": 35, + "width": 86, + "x": 8928, + "y": 2074 + }, + "bottomLeftRadius": 8, + "bottomRightRadius": 8, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:345", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 8, + "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 + } + ], + "gridColumnAnchorIndex": 1, + "gridRowAnchorIndex": 7, + "height": 35, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 8, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 6, + "paddingLeft": 12, + "paddingRight": 12, + "paddingTop": 6, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 8, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "FILL", + "maxHeight": null, + "maxWidth": null, + "name": "effect=hover, type=white-line, size=sm", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 6, + "paddingLeft": 12, + "paddingRight": 12, + "paddingTop": 6, + "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": 8, + "topRightRadius": 8, + "variantProperties": { + "effect": "hover", + "size": "sm", + "type": "white-line" + }, + "visible": true, + "width": 86, + "x": 153, + "y": 430 + }, + "extra": {}, + "fieldErrors": {} + }, + "30:512": { + "id": "30:512", + "type": "TEXT", + "fields": { + "parentId": "30:511", + "absoluteBoundingBox": { + "height": 21, + "width": 36, + "x": 8941, + "y": 2081 + }, + "absoluteRenderBounds": { + "height": 12.501953125, + "width": 33.572265625, + "x": 8941.9384765625, + "y": 2085.64599609375 + }, + "boundVariables": { + "fontSize": [ + { + "id": "VariableID:18:956", + "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": 14, + "height": 21, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -3 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "버튼명", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:1649d6614e863b73049b737254cc0e54ab553c77,", + "textTruncation": "DISABLED", + "visible": true, + "width": 36, + "x": 13, + "y": 7, + "styledTextSegments": [ + { + "characters": "버튼명", + "end": 3, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:1649d6614e863b73049b737254cc0e54ab553c77," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "30:513": { + "id": "30:513", + "type": "INSTANCE", + "fields": { + "parentId": "30:511", + "childrenIds": [ + "I30:513;23:1618" + ], + "absoluteBoundingBox": { + "height": 16, + "width": 16, + "x": 8985, + "y": 2083.5 + }, + "absoluteRenderBounds": { + "height": 16, + "width": 16, + "x": 8985, + "y": 2083.5 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "componentProperties": { + "속성 1": { + "boundVariables": {}, + "type": "VARIANT", + "value": "Right" + } + }, + "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.666666269302368, + "paddingLeft": 6, + "paddingRight": 4.666666507720947, + "paddingTop": 2.6666667461395264, + "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": 16, + "x": 57, + "y": 9.5, + "mainComponentId": "23:1619", + "mainComponentName": "속성 1=Right", + "mainComponentSetId": "20:1312" + }, + "extra": {}, + "fieldErrors": {} + }, + "30:514": { + "id": "30:514", + "type": "COMPONENT", + "fields": { + "parentId": "23:1613", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "30:515", + "30:516" + ], + "absoluteBoundingBox": { + "height": 35, + "width": 86, + "x": 9060, + "y": 2074 + }, + "absoluteRenderBounds": { + "height": 35, + "width": 86, + "x": 9060, + "y": 2074 + }, + "bottomLeftRadius": 8, + "bottomRightRadius": 8, + "boundVariables": { + "fills": [ + { + "id": "VariableID:23:1651", + "type": "VARIABLE_ALIAS" + } + ] + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 8, + "counterAxisAlignItems": "CENTER", + "fills": [ + { + "blendMode": "NORMAL", + "boundVariables": { + "color": { + "id": "VariableID:23:1651", + "type": "VARIABLE_ALIAS" + } + }, + "color": { + "b": 0.6078431606292725, + "g": 0.2823529541492462, + "r": 0.003921568859368563 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "gridColumnAnchorIndex": 2, + "gridRowAnchorIndex": 7, + "height": 35, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 8, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 6, + "paddingLeft": 12, + "paddingRight": 12, + "paddingTop": 6, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 8, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "FILL", + "maxHeight": null, + "maxWidth": null, + "name": "effect=active, type=white-line, size=sm", + "opacity": 1, + "overflowDirection": "NONE", + "paddingBottom": 6, + "paddingLeft": 12, + "paddingRight": 12, + "paddingTop": 6, + "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": 8, + "topRightRadius": 8, + "variantProperties": { + "effect": "active", + "size": "sm", + "type": "white-line" + }, + "visible": true, + "width": 86, + "x": 285, + "y": 430 + }, + "extra": {}, + "fieldErrors": {} + }, + "30:515": { + "id": "30:515", + "type": "TEXT", + "fields": { + "parentId": "30:514", + "absoluteBoundingBox": { + "height": 21, + "width": 36, + "x": 9073, + "y": 2081 + }, + "absoluteRenderBounds": { + "height": 12.501953125, + "width": 33.572265625, + "x": 9073.9384765625, + "y": 2085.64599609375 + }, + "boundVariables": { + "fontSize": [ + { + "id": "VariableID:18:956", + "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": 14, + "height": 21, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -3 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "버튼명", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:1649d6614e863b73049b737254cc0e54ab553c77,", + "textTruncation": "DISABLED", + "visible": true, + "width": 36, + "x": 13, + "y": 7, + "styledTextSegments": [ + { + "characters": "버튼명", + "end": 3, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:1649d6614e863b73049b737254cc0e54ab553c77," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "30:516": { + "id": "30:516", + "type": "INSTANCE", + "fields": { + "parentId": "30:514", + "childrenIds": [ + "I30:516;23:1618" + ], + "absoluteBoundingBox": { + "height": 16, + "width": 16, + "x": 9117, + "y": 2083.5 + }, + "absoluteRenderBounds": { + "height": 16, + "width": 16, + "x": 9117, + "y": 2083.5 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "componentProperties": { + "속성 1": { + "boundVariables": {}, + "type": "VARIANT", + "value": "Right" + } + }, + "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.666666269302368, + "paddingLeft": 6, + "paddingRight": 4.666666507720947, + "paddingTop": 2.6666667461395264, + "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": 16, + "x": 57, + "y": 9.5, + "mainComponentId": "23:1619", + "mainComponentName": "속성 1=Right", + "mainComponentSetId": "20:1312" + }, + "extra": {}, + "fieldErrors": {} + }, + "30:517": { + "id": "30:517", + "type": "COMPONENT", + "fields": { + "parentId": "23:1613", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "30:518", + "30:519" + ], + "absoluteBoundingBox": { + "height": 35, + "width": 86, + "x": 9192, + "y": 2074 + }, + "absoluteRenderBounds": { + "height": 35, + "width": 86, + "x": 9192, + "y": 2074 + }, + "bottomLeftRadius": 8, + "bottomRightRadius": 8, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 8, + "counterAxisAlignItems": "CENTER", + "gridColumnAnchorIndex": 3, + "gridRowAnchorIndex": 7, + "height": 35, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "counterAxisSizingMode": "FIXED", + "itemSpacing": 8, + "layoutAlign": "STRETCH", + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutPositioning": "AUTO", + "paddingBottom": 6, + "paddingLeft": 12, + "paddingRight": 12, + "paddingTop": 6, + "primaryAxisAlignItems": "CENTER", + "primaryAxisSizingMode": "AUTO" + }, + "itemSpacing": 8, + "layoutGrow": 0, + "layoutMode": "HORIZONTAL", + "layoutWrap": "WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "FILL", + "maxHeight": null, + "maxWidth": null, + "name": "effect=disabled, type=white-line, size=sm", + "opacity": 0.4000000059604645, + "overflowDirection": "NONE", + "paddingBottom": 6, + "paddingLeft": 12, + "paddingRight": 12, + "paddingTop": 6, + "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": 8, + "topRightRadius": 8, + "variantProperties": { + "effect": "disabled", + "size": "sm", + "type": "white-line" + }, + "visible": true, + "width": 86, + "x": 417, + "y": 430 + }, + "extra": {}, + "fieldErrors": {} + }, + "30:518": { + "id": "30:518", + "type": "TEXT", + "fields": { + "parentId": "30:517", + "absoluteBoundingBox": { + "height": 21, + "width": 36, + "x": 9205, + "y": 2081 + }, + "absoluteRenderBounds": { + "height": 12.501953125, + "width": 33.572265625, + "x": 9205.9384765625, + "y": 2085.64599609375 + }, + "boundVariables": { + "fontSize": [ + { + "id": "VariableID:18:956", + "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": 14, + "height": 21, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "HUG", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": -3 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 150 + }, + "maxHeight": null, + "maxWidth": null, + "name": "버튼명", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "WIDTH_AND_HEIGHT", + "textStyleId": "S:1649d6614e863b73049b737254cc0e54ab553c77,", + "textTruncation": "DISABLED", + "visible": true, + "width": 36, + "x": 13, + "y": 7, + "styledTextSegments": [ + { + "characters": "버튼명", + "end": 3, + "fillStyleId": "", + "fontWeight": 700, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:1649d6614e863b73049b737254cc0e54ab553c77," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "30:519": { + "id": "30:519", + "type": "INSTANCE", + "fields": { + "parentId": "30:517", + "childrenIds": [ + "I30:519;23:1618" + ], + "absoluteBoundingBox": { + "height": 16, + "width": 16, + "x": 9249, + "y": 2083.5 + }, + "absoluteRenderBounds": { + "height": 16, + "width": 16, + "x": 9249, + "y": 2083.5 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "clipsContent": true, + "componentProperties": { + "속성 1": { + "boundVariables": {}, + "type": "VARIANT", + "value": "Right" + } + }, + "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.666666269302368, + "paddingLeft": 6, + "paddingRight": 4.666666507720947, + "paddingTop": 2.6666667461395264, + "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": 16, + "x": 57, + "y": 9.5, + "mainComponentId": "23:1619", + "mainComponentName": "속성 1=Right", + "mainComponentSetId": "20:1312" + }, + "extra": {}, + "fieldErrors": {} + }, + "I23:1620;23:1618": { + "id": "I23:1620;23:1618", + "type": "VECTOR", + "fields": { + "parentId": "23:1620", + "absoluteBoundingBox": { + "height": 10.666666984558104, + "width": 5.333333492279053, + "x": 8858, + "y": 1936.1666259765625 + }, + "absoluteRenderBounds": { + "height": 12.2667236328125, + "width": 6.93359375, + "x": 8857.2001953125, + "y": 1935.3665771484375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "height": 10.666666984558104, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "CENTER", + "strokeWeight": 1.600000023841858, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "visible": true, + "width": 5.333333492279053, + "x": 6, + "y": 2.6666667461395264 + }, + "extra": {}, + "fieldErrors": {} + }, + "I23:1636;23:1618": { + "id": "I23:1636;23:1618", + "type": "VECTOR", + "fields": { + "parentId": "23:1636", + "absoluteBoundingBox": { + "height": 10.666666984558104, + "width": 5.333333492279053, + "x": 8990, + "y": 1936.1666259765625 + }, + "absoluteRenderBounds": { + "height": 12.2667236328125, + "width": 6.93359375, + "x": 8989.2001953125, + "y": 1935.3665771484375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "height": 10.666666984558104, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "CENTER", + "strokeWeight": 1.600000023841858, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "visible": true, + "width": 5.333333492279053, + "x": 6, + "y": 2.6666667461395264 + }, + "extra": {}, + "fieldErrors": {} + }, + "I23:1644;23:1618": { + "id": "I23:1644;23:1618", + "type": "VECTOR", + "fields": { + "parentId": "23:1644", + "absoluteBoundingBox": { + "height": 10.666666984558104, + "width": 5.333333492279053, + "x": 9122, + "y": 1936.1666259765625 + }, + "absoluteRenderBounds": { + "height": 12.2667236328125, + "width": 6.93359375, + "x": 9121.2001953125, + "y": 1935.3665771484375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "height": 10.666666984558104, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "CENTER", + "strokeWeight": 1.600000023841858, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "visible": true, + "width": 5.333333492279053, + "x": 6, + "y": 2.6666667461395264 + }, + "extra": {}, + "fieldErrors": {} + }, + "I24:1661;23:1618": { + "id": "I24:1661;23:1618", + "type": "VECTOR", + "fields": { + "parentId": "24:1661", + "absoluteBoundingBox": { + "height": 10.666666984558104, + "width": 5.333333492279053, + "x": 8990, + "y": 1985.1666259765625 + }, + "absoluteRenderBounds": { + "height": 12.2667236328125, + "width": 6.93359375, + "x": 8989.2001953125, + "y": 1984.3665771484375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "height": 10.666666984558104, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "CENTER", + "strokeWeight": 1.600000023841858, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "visible": true, + "width": 5.333333492279053, + "x": 6, + "y": 2.6666667461395264 + }, + "extra": {}, + "fieldErrors": {} + }, + "I24:1664;23:1618": { + "id": "I24:1664;23:1618", + "type": "VECTOR", + "fields": { + "parentId": "24:1664", + "absoluteBoundingBox": { + "height": 10.666666984558104, + "width": 5.333333492279053, + "x": 9122, + "y": 1985.1666259765625 + }, + "absoluteRenderBounds": { + "height": 12.2667236328125, + "width": 6.93359375, + "x": 9121.2001953125, + "y": 1984.3665771484375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "height": 10.666666984558104, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "CENTER", + "strokeWeight": 1.600000023841858, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "visible": true, + "width": 5.333333492279053, + "x": 6, + "y": 2.6666667461395264 + }, + "extra": {}, + "fieldErrors": {} + }, + "I24:1667;23:1618": { + "id": "I24:1667;23:1618", + "type": "VECTOR", + "fields": { + "parentId": "24:1667", + "absoluteBoundingBox": { + "height": 10.666666984558104, + "width": 5.333333492279053, + "x": 9254, + "y": 1936.1666259765625 + }, + "absoluteRenderBounds": { + "height": 12.2667236328125, + "width": 6.93359375, + "x": 9253.2001953125, + "y": 1935.3665771484375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "height": 10.666666984558104, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "CENTER", + "strokeWeight": 1.600000023841858, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "visible": true, + "width": 5.333333492279053, + "x": 6, + "y": 2.6666667461395264 + }, + "extra": {}, + "fieldErrors": {} + }, + "I24:1855;23:1618": { + "id": "I24:1855;23:1618", + "type": "VECTOR", + "fields": { + "parentId": "24:1855", + "absoluteBoundingBox": { + "height": 10.666666984558104, + "width": 5.333333492279053, + "x": 8876, + "y": 1683.6666259765625 + }, + "absoluteRenderBounds": { + "height": 12.2667236328125, + "width": 6.93359375, + "x": 8875.2001953125, + "y": 1682.8665771484375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "height": 10.666666984558104, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "CENTER", + "strokeWeight": 1.600000023841858, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "visible": true, + "width": 5.333333492279053, + "x": 6, + "y": 2.6666667461395264 + }, + "extra": {}, + "fieldErrors": {} + }, + "I24:1858;23:1618": { + "id": "I24:1858;23:1618", + "type": "VECTOR", + "fields": { + "parentId": "24:1858", + "absoluteBoundingBox": { + "height": 10.666666984558104, + "width": 5.333333492279053, + "x": 9008, + "y": 1683.6666259765625 + }, + "absoluteRenderBounds": { + "height": 12.2667236328125, + "width": 6.93359375, + "x": 9007.2001953125, + "y": 1682.8665771484375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "height": 10.666666984558104, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "CENTER", + "strokeWeight": 1.600000023841858, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "visible": true, + "width": 5.333333492279053, + "x": 6, + "y": 2.6666667461395264 + }, + "extra": {}, + "fieldErrors": {} + }, + "I24:1861;23:1618": { + "id": "I24:1861;23:1618", + "type": "VECTOR", + "fields": { + "parentId": "24:1861", + "absoluteBoundingBox": { + "height": 10.666666984558104, + "width": 5.333333492279053, + "x": 9140, + "y": 1683.6666259765625 + }, + "absoluteRenderBounds": { + "height": 12.2667236328125, + "width": 6.93359375, + "x": 9139.2001953125, + "y": 1682.8665771484375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "height": 10.666666984558104, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "CENTER", + "strokeWeight": 1.600000023841858, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "visible": true, + "width": 5.333333492279053, + "x": 6, + "y": 2.6666667461395264 + }, + "extra": {}, + "fieldErrors": {} + }, + "I24:1864;23:1618": { + "id": "I24:1864;23:1618", + "type": "VECTOR", + "fields": { + "parentId": "24:1864", + "absoluteBoundingBox": { + "height": 10.666666984558104, + "width": 5.333333492279053, + "x": 9272, + "y": 1683.6666259765625 + }, + "absoluteRenderBounds": { + "height": 12.2667236328125, + "width": 6.93359375, + "x": 9271.2001953125, + "y": 1682.8665771484375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "height": 10.666666984558104, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "CENTER", + "strokeWeight": 1.600000023841858, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "visible": true, + "width": 5.333333492279053, + "x": 6, + "y": 2.6666667461395264 + }, + "extra": {}, + "fieldErrors": {} + }, + "I24:1867;23:1618": { + "id": "I24:1867;23:1618", + "type": "VECTOR", + "fields": { + "parentId": "24:1867", + "absoluteBoundingBox": { + "height": 10.666666984558104, + "width": 5.333333492279053, + "x": 8877, + "y": 1878.6666259765625 + }, + "absoluteRenderBounds": { + "height": 12.2667236328125, + "width": 6.93359375, + "x": 8876.2001953125, + "y": 1877.8665771484375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "height": 10.666666984558104, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "CENTER", + "strokeWeight": 1.600000023841858, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "visible": true, + "width": 5.333333492279053, + "x": 6, + "y": 2.6666667461395264 + }, + "extra": {}, + "fieldErrors": {} + }, + "I24:1870;23:1618": { + "id": "I24:1870;23:1618", + "type": "VECTOR", + "fields": { + "parentId": "24:1870", + "absoluteBoundingBox": { + "height": 10.666666984558104, + "width": 5.333333492279053, + "x": 9009, + "y": 1878.6666259765625 + }, + "absoluteRenderBounds": { + "height": 12.2667236328125, + "width": 6.93359375, + "x": 9008.2001953125, + "y": 1877.8665771484375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "height": 10.666666984558104, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "CENTER", + "strokeWeight": 1.600000023841858, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "visible": true, + "width": 5.333333492279053, + "x": 6, + "y": 2.6666667461395264 + }, + "extra": {}, + "fieldErrors": {} + }, + "I24:1873;23:1618": { + "id": "I24:1873;23:1618", + "type": "VECTOR", + "fields": { + "parentId": "24:1873", + "absoluteBoundingBox": { + "height": 10.666666984558104, + "width": 5.333333492279053, + "x": 9141, + "y": 1878.6666259765625 + }, + "absoluteRenderBounds": { + "height": 12.2667236328125, + "width": 6.93359375, + "x": 9140.2001953125, + "y": 1877.8665771484375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "height": 10.666666984558104, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "CENTER", + "strokeWeight": 1.600000023841858, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "visible": true, + "width": 5.333333492279053, + "x": 6, + "y": 2.6666667461395264 + }, + "extra": {}, + "fieldErrors": {} + }, + "I24:1935;23:1618": { + "id": "I24:1935;23:1618", + "type": "VECTOR", + "fields": { + "parentId": "24:1935", + "absoluteBoundingBox": { + "height": 10.666666984558104, + "width": 5.333333492279053, + "x": 8858, + "y": 1985.1666259765625 + }, + "absoluteRenderBounds": { + "height": 12.2667236328125, + "width": 6.93359375, + "x": 8857.2001953125, + "y": 1984.3665771484375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "height": 10.666666984558104, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "CENTER", + "strokeWeight": 1.600000023841858, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "visible": true, + "width": 5.333333492279053, + "x": 6, + "y": 2.6666667461395264 + }, + "extra": {}, + "fieldErrors": {} + }, + "I24:1942;23:1618": { + "id": "I24:1942;23:1618", + "type": "VECTOR", + "fields": { + "parentId": "24:1942", + "absoluteBoundingBox": { + "height": 10.666666984558104, + "width": 5.333333492279053, + "x": 9254, + "y": 1985.1666259765625 + }, + "absoluteRenderBounds": { + "height": 12.2667236328125, + "width": 6.93359375, + "x": 9253.2001953125, + "y": 1984.3665771484375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "height": 10.666666984558104, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "CENTER", + "strokeWeight": 1.600000023841858, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "visible": true, + "width": 5.333333492279053, + "x": 6, + "y": 2.6666667461395264 + }, + "extra": {}, + "fieldErrors": {} + }, + "I30:370;23:1618": { + "id": "I30:370;23:1618", + "type": "VECTOR", + "fields": { + "parentId": "30:370", + "absoluteBoundingBox": { + "height": 10.666666984558104, + "width": 5.333333492279053, + "x": 8876, + "y": 1747.6666259765625 + }, + "absoluteRenderBounds": { + "height": 12.2667236328125, + "width": 6.93359375, + "x": 8875.2001953125, + "y": 1746.8665771484375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "height": 10.666666984558104, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "CENTER", + "strokeWeight": 1.600000023841858, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "visible": true, + "width": 5.333333492279053, + "x": 6, + "y": 2.6666667461395264 + }, + "extra": {}, + "fieldErrors": {} + }, + "I30:373;23:1618": { + "id": "I30:373;23:1618", + "type": "VECTOR", + "fields": { + "parentId": "30:373", + "absoluteBoundingBox": { + "height": 10.666666984558104, + "width": 5.333333492279053, + "x": 9008, + "y": 1747.6666259765625 + }, + "absoluteRenderBounds": { + "height": 12.2667236328125, + "width": 6.93359375, + "x": 9007.2001953125, + "y": 1746.8665771484375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "height": 10.666666984558104, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "CENTER", + "strokeWeight": 1.600000023841858, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "visible": true, + "width": 5.333333492279053, + "x": 6, + "y": 2.6666667461395264 + }, + "extra": {}, + "fieldErrors": {} + }, + "I30:376;23:1618": { + "id": "I30:376;23:1618", + "type": "VECTOR", + "fields": { + "parentId": "30:376", + "absoluteBoundingBox": { + "height": 10.666666984558104, + "width": 5.333333492279053, + "x": 9140, + "y": 1747.6666259765625 + }, + "absoluteRenderBounds": { + "height": 12.2667236328125, + "width": 6.93359375, + "x": 9139.2001953125, + "y": 1746.8665771484375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "height": 10.666666984558104, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "CENTER", + "strokeWeight": 1.600000023841858, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "visible": true, + "width": 5.333333492279053, + "x": 6, + "y": 2.6666667461395264 + }, + "extra": {}, + "fieldErrors": {} + }, + "I30:379;23:1618": { + "id": "I30:379;23:1618", + "type": "VECTOR", + "fields": { + "parentId": "30:379", + "absoluteBoundingBox": { + "height": 10.666666984558104, + "width": 5.333333492279053, + "x": 9272, + "y": 1747.6666259765625 + }, + "absoluteRenderBounds": { + "height": 12.2667236328125, + "width": 6.93359375, + "x": 9271.2001953125, + "y": 1746.8665771484375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "height": 10.666666984558104, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "CENTER", + "strokeWeight": 1.600000023841858, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "visible": true, + "width": 5.333333492279053, + "x": 6, + "y": 2.6666667461395264 + }, + "extra": {}, + "fieldErrors": {} + }, + "I30:385;23:1618": { + "id": "I30:385;23:1618", + "type": "VECTOR", + "fields": { + "parentId": "30:385", + "absoluteBoundingBox": { + "height": 10.666666984558104, + "width": 5.333333492279053, + "x": 9273, + "y": 1878.6666259765625 + }, + "absoluteRenderBounds": { + "height": 12.2667236328125, + "width": 6.93359375, + "x": 9272.2001953125, + "y": 1877.8665771484375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "height": 10.666666984558104, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "CENTER", + "strokeWeight": 1.600000023841858, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "visible": true, + "width": 5.333333492279053, + "x": 6, + "y": 2.6666667461395264 + }, + "extra": {}, + "fieldErrors": {} + }, + "I30:401;23:1618": { + "id": "I30:401;23:1618", + "type": "VECTOR", + "fields": { + "parentId": "30:401", + "absoluteBoundingBox": { + "height": 10.666666984558104, + "width": 5.333333492279053, + "x": 8877, + "y": 1812.6666259765625 + }, + "absoluteRenderBounds": { + "height": 12.2667236328125, + "width": 6.93359375, + "x": 8876.2001953125, + "y": 1811.8665771484375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "height": 10.666666984558104, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "CENTER", + "strokeWeight": 1.600000023841858, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "visible": true, + "width": 5.333333492279053, + "x": 6, + "y": 2.6666667461395264 + }, + "extra": {}, + "fieldErrors": {} + }, + "I30:404;23:1618": { + "id": "I30:404;23:1618", + "type": "VECTOR", + "fields": { + "parentId": "30:404", + "absoluteBoundingBox": { + "height": 10.666666984558104, + "width": 5.333333492279053, + "x": 9009, + "y": 1812.6666259765625 + }, + "absoluteRenderBounds": { + "height": 12.2667236328125, + "width": 6.93359375, + "x": 9008.2001953125, + "y": 1811.8665771484375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "height": 10.666666984558104, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "CENTER", + "strokeWeight": 1.600000023841858, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "visible": true, + "width": 5.333333492279053, + "x": 6, + "y": 2.6666667461395264 + }, + "extra": {}, + "fieldErrors": {} + }, + "I30:417;23:1618": { + "id": "I30:417;23:1618", + "type": "VECTOR", + "fields": { + "parentId": "30:417", + "absoluteBoundingBox": { + "height": 10.666666984558104, + "width": 5.333333492279053, + "x": 9273, + "y": 1812.6666259765625 + }, + "absoluteRenderBounds": { + "height": 12.2667236328125, + "width": 6.93359375, + "x": 9272.2001953125, + "y": 1811.8665771484375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "height": 10.666666984558104, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "CENTER", + "strokeWeight": 1.600000023841858, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "visible": true, + "width": 5.333333492279053, + "x": 6, + "y": 2.6666667461395264 + }, + "extra": {}, + "fieldErrors": {} + }, + "I30:423;23:1618": { + "id": "I30:423;23:1618", + "type": "VECTOR", + "fields": { + "parentId": "30:423", + "absoluteBoundingBox": { + "height": 10.666666984558104, + "width": 5.333333492279053, + "x": 9141, + "y": 1812.6666259765625 + }, + "absoluteRenderBounds": { + "height": 12.2667236328125, + "width": 6.93359375, + "x": 9140.2001953125, + "y": 1811.8665771484375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "height": 10.666666984558104, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "CENTER", + "strokeWeight": 1.600000023841858, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "visible": true, + "width": 5.333333492279053, + "x": 6, + "y": 2.6666667461395264 + }, + "extra": {}, + "fieldErrors": {} + }, + "I30:498;23:1618": { + "id": "I30:498;23:1618", + "type": "VECTOR", + "fields": { + "parentId": "30:498", + "absoluteBoundingBox": { + "height": 10.666666984558104, + "width": 5.333333492279053, + "x": 8859, + "y": 2035.1666259765625 + }, + "absoluteRenderBounds": { + "height": 12.2667236328125, + "width": 6.93359375, + "x": 8858.2001953125, + "y": 2034.3665771484375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "height": 10.666666984558104, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "CENTER", + "strokeWeight": 1.600000023841858, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "visible": true, + "width": 5.333333492279053, + "x": 6, + "y": 2.6666667461395264 + }, + "extra": {}, + "fieldErrors": {} + }, + "I30:501;23:1618": { + "id": "I30:501;23:1618", + "type": "VECTOR", + "fields": { + "parentId": "30:501", + "absoluteBoundingBox": { + "height": 10.666666984558104, + "width": 5.333333492279053, + "x": 8991, + "y": 2035.1666259765625 + }, + "absoluteRenderBounds": { + "height": 12.2667236328125, + "width": 6.93359375, + "x": 8990.2001953125, + "y": 2034.3665771484375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "height": 10.666666984558104, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "CENTER", + "strokeWeight": 1.600000023841858, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "visible": true, + "width": 5.333333492279053, + "x": 6, + "y": 2.6666667461395264 + }, + "extra": {}, + "fieldErrors": {} + }, + "I30:504;23:1618": { + "id": "I30:504;23:1618", + "type": "VECTOR", + "fields": { + "parentId": "30:504", + "absoluteBoundingBox": { + "height": 10.666666984558104, + "width": 5.333333492279053, + "x": 9123, + "y": 2035.1666259765625 + }, + "absoluteRenderBounds": { + "height": 12.2667236328125, + "width": 6.93359375, + "x": 9122.2001953125, + "y": 2034.3665771484375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "height": 10.666666984558104, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "CENTER", + "strokeWeight": 1.600000023841858, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "visible": true, + "width": 5.333333492279053, + "x": 6, + "y": 2.6666667461395264 + }, + "extra": {}, + "fieldErrors": {} + }, + "I30:507;23:1618": { + "id": "I30:507;23:1618", + "type": "VECTOR", + "fields": { + "parentId": "30:507", + "absoluteBoundingBox": { + "height": 10.666666984558104, + "width": 5.333333492279053, + "x": 9255, + "y": 2035.1666259765625 + }, + "absoluteRenderBounds": { + "height": 12.2667236328125, + "width": 6.93359375, + "x": 9254.2001953125, + "y": 2034.3665771484375 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "height": 10.666666984558104, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "CENTER", + "strokeWeight": 1.600000023841858, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "visible": true, + "width": 5.333333492279053, + "x": 6, + "y": 2.6666667461395264 + }, + "extra": {}, + "fieldErrors": {} + }, + "I30:510;23:1618": { + "id": "I30:510;23:1618", + "type": "VECTOR", + "fields": { + "parentId": "30:510", + "absoluteBoundingBox": { + "height": 10.666666984558104, + "width": 5.333333492279053, + "x": 8859, + "y": 2086.166748046875 + }, + "absoluteRenderBounds": { + "height": 12.2666015625, + "width": 6.93359375, + "x": 8858.2001953125, + "y": 2085.36669921875 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "height": 10.666666984558104, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "CENTER", + "strokeWeight": 1.600000023841858, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "visible": true, + "width": 5.333333492279053, + "x": 6, + "y": 2.6666667461395264 + }, + "extra": {}, + "fieldErrors": {} + }, + "I30:513;23:1618": { + "id": "I30:513;23:1618", + "type": "VECTOR", + "fields": { + "parentId": "30:513", + "absoluteBoundingBox": { + "height": 10.666666984558104, + "width": 5.333333492279053, + "x": 8991, + "y": 2086.166748046875 + }, + "absoluteRenderBounds": { + "height": 12.2666015625, + "width": 6.93359375, + "x": 8990.2001953125, + "y": 2085.36669921875 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "height": 10.666666984558104, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "CENTER", + "strokeWeight": 1.600000023841858, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "visible": true, + "width": 5.333333492279053, + "x": 6, + "y": 2.6666667461395264 + }, + "extra": {}, + "fieldErrors": {} + }, + "I30:516;23:1618": { + "id": "I30:516;23:1618", + "type": "VECTOR", + "fields": { + "parentId": "30:516", + "absoluteBoundingBox": { + "height": 10.666666984558104, + "width": 5.333333492279053, + "x": 9123, + "y": 2086.166748046875 + }, + "absoluteRenderBounds": { + "height": 12.2666015625, + "width": 6.93359375, + "x": 9122.2001953125, + "y": 2085.36669921875 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "height": 10.666666984558104, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "CENTER", + "strokeWeight": 1.600000023841858, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "visible": true, + "width": 5.333333492279053, + "x": 6, + "y": 2.6666667461395264 + }, + "extra": {}, + "fieldErrors": {} + }, + "I30:519;23:1618": { + "id": "I30:519;23:1618", + "type": "VECTOR", + "fields": { + "parentId": "30:519", + "absoluteBoundingBox": { + "height": 10.666666984558104, + "width": 5.333333492279053, + "x": 9255, + "y": 2086.166748046875 + }, + "absoluteRenderBounds": { + "height": 12.2666015625, + "width": 6.93359375, + "x": 9254.2001953125, + "y": 2085.36669921875 + }, + "constraints": { + "horizontal": "SCALE", + "vertical": "SCALE" + }, + "height": 10.666666984558104, + "isAsset": true, + "layoutGrow": 0, + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "Vector", + "opacity": 1, + "strokeAlign": "CENTER", + "strokeWeight": 1.600000023841858, + "strokes": [ + { + "blendMode": "NORMAL", + "boundVariables": {}, + "color": { + "b": 1, + "g": 1, + "r": 1 + }, + "opacity": 1, + "type": "SOLID", + "visible": true + } + ], + "visible": true, + "width": 5.333333492279053, + "x": 6, + "y": 2.6666667461395264 + }, + "extra": {}, + "fieldErrors": {} + } + }, + "diagnostics": [] +} \ No newline at end of file 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_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_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_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": [] +} 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_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": [] +} 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": [] +} 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_section_title_set.json b/crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_section_title_set.json new file mode 100644 index 00000000..ef32d316 --- /dev/null +++ b/crates/devup-mcp-devup-ui/tests/fixtures/chondrogen_section_title_set.json @@ -0,0 +1,684 @@ +{ + "fileKey": "K2kUnxGgvooAJwCQiVTb7g", + "version": null, + "roots": [ + "44:1949" + ], + "nodes": { + "41:706": { + "id": "41:706", + "type": "TEXT", + "fields": { + "parentId": "41:708", + "absoluteBoundingBox": { + "height": 20, + "width": 476, + "x": 11141, + "y": 4329 + }, + "absoluteRenderBounds": { + "height": 11.625, + "width": 128.025390625, + "x": 11141.900390625, + "y": 4332.77978515625 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:359", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "CEO MESSEAGE", + "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": "FILL", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": 4 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 132.00000524520874 + }, + "maxHeight": null, + "maxWidth": null, + "name": "CEO MESSEAGE", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAutoResize": "HEIGHT", + "textStyleId": "S:18adf49c362ab9c0a968104533e0a31e663f3222,", + "textTruncation": "DISABLED", + "visible": true, + "width": 476, + "x": 0, + "y": 0, + "styledTextSegments": [ + { + "characters": "CEO MESSEAGE", + "end": 12, + "fillStyleId": "", + "fontWeight": 900, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:18adf49c362ab9c0a968104533e0a31e663f3222," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "41:707": { + "id": "41:707", + "type": "TEXT", + "fields": { + "parentId": "41:708", + "absoluteBoundingBox": { + "height": 53, + "width": 476, + "x": 11141, + "y": 4361 + }, + "absoluteRenderBounds": { + "height": 36.0400390625, + "width": 391.52734375, + "x": 11141.6396484375, + "y": 4369.39990234375 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:938", + "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": 40, + "height": 53, + "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, + "textAutoResize": "HEIGHT", + "textStyleId": "S:7f649150dd922eb61a2b92d74e277e4a43e48ec4,", + "textTruncation": "DISABLED", + "visible": true, + "width": 476, + "x": 0, + "y": 32, + "styledTextSegments": [ + { + "characters": "관절재생으로 새로운 삶을", + "end": 13, + "fillStyleId": "", + "fontWeight": 800, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:7f649150dd922eb61a2b92d74e277e4a43e48ec4," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "41:708": { + "id": "41:708", + "type": "COMPONENT", + "fields": { + "parentId": "44:1949", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "41:706", + "41:707" + ], + "absoluteBoundingBox": { + "height": 85, + "width": 476, + "x": 11141, + "y": 4329 + }, + "absoluteRenderBounds": { + "height": 85, + "width": 476, + "x": 11141, + "y": 4329 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "boundVariables": { + "itemSpacing": { + "id": "VariableID:30:615", + "type": "VARIABLE_ALIAS" + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "height": 85, + "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": "FIXED", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "align=left", + "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": { + "align": "left" + }, + "visible": true, + "width": 476, + "x": 20, + "y": 20 + }, + "extra": {}, + "fieldErrors": {} + }, + "44:1949": { + "id": "44:1949", + "type": "COMPONENT_SET", + "fields": { + "parentId": "19:963", + "parentType": "SECTION", + "parentName": "Components", + "childrenIds": [ + "41:708", + "44:1950" + ], + "absoluteBoundingBox": { + "height": 230, + "width": 516, + "x": 11121, + "y": 4309 + }, + "absoluteRenderBounds": { + "height": 230, + "width": 516, + "x": 11121, + "y": 4309 + }, + "bottomLeftRadius": 5, + "bottomRightRadius": 5, + "clipsContent": true, + "componentPropertyDefinitions": { + "align": { + "defaultValue": "left", + "type": "VARIANT", + "variantOptions": [ + "left", + "center" + ] + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 5, + "dashPattern": [ + 10, + 5 + ], + "defaultVariant": { + "$nodeId": "41:708", + "$nodeType": "COMPONENT" + }, + "height": 230, + "itemSpacing": 0, + "layoutGrow": 0, + "layoutMode": "NONE", + "layoutWrap": "NO_WRAP", + "layoutPositioning": "AUTO", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "maxHeight": null, + "maxWidth": null, + "name": "SectionTitle", + "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": 516, + "x": 2451, + "y": 3271 + }, + "extra": {}, + "fieldErrors": {} + }, + "44:1950": { + "id": "44:1950", + "type": "COMPONENT", + "fields": { + "parentId": "44:1949", + "parentType": "COMPONENT_SET", + "childrenIds": [ + "44:1951", + "44:1952" + ], + "absoluteBoundingBox": { + "height": 85, + "width": 476, + "x": 11141, + "y": 4434 + }, + "absoluteRenderBounds": { + "height": 85, + "width": 476, + "x": 11141, + "y": 4434 + }, + "bottomLeftRadius": 0, + "bottomRightRadius": 0, + "boundVariables": { + "itemSpacing": { + "id": "VariableID:30:615", + "type": "VARIABLE_ALIAS" + } + }, + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "counterAxisAlignItems": "CENTER", + "height": 85, + "inferredAutoLayout": { + "counterAxisAlignItems": "CENTER", + "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": "FIXED", + "layoutSizingVertical": "HUG", + "maxHeight": null, + "maxWidth": null, + "name": "align=center", + "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": { + "align": "center" + }, + "visible": true, + "width": 476, + "x": 20, + "y": 125 + }, + "extra": {}, + "fieldErrors": {} + }, + "44:1951": { + "id": "44:1951", + "type": "TEXT", + "fields": { + "parentId": "44:1950", + "absoluteBoundingBox": { + "height": 20, + "width": 476, + "x": 11141, + "y": 4434 + }, + "absoluteRenderBounds": { + "height": 11.625, + "width": 128.0263671875, + "x": 11315.0693359375, + "y": 4437.77978515625 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:359", + "type": "VARIABLE_ALIAS" + } + ] + }, + "characters": "CEO MESSEAGE", + "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": "FILL", + "layoutSizingVertical": "HUG", + "letterSpacing": { + "unit": "PERCENT", + "value": 4 + }, + "lineHeight": { + "unit": "PERCENT", + "value": 132.00000524520874 + }, + "maxHeight": null, + "maxWidth": null, + "name": "CEO MESSEAGE", + "opacity": 1, + "strokeAlign": "OUTSIDE", + "strokeWeight": 1, + "textAlignHorizontal": "CENTER", + "textAutoResize": "HEIGHT", + "textStyleId": "S:18adf49c362ab9c0a968104533e0a31e663f3222,", + "textTruncation": "DISABLED", + "visible": true, + "width": 476, + "x": 0, + "y": 0, + "styledTextSegments": [ + { + "characters": "CEO MESSEAGE", + "end": 12, + "fillStyleId": "", + "fontWeight": 900, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:18adf49c362ab9c0a968104533e0a31e663f3222," + } + ] + }, + "extra": {}, + "fieldErrors": {} + }, + "44:1952": { + "id": "44:1952", + "type": "TEXT", + "fields": { + "parentId": "44:1950", + "absoluteBoundingBox": { + "height": 53, + "width": 476, + "x": 11141, + "y": 4466 + }, + "absoluteRenderBounds": { + "height": 36.0400390625, + "width": 391.5263671875, + "x": 11182.9365234375, + "y": 4474.39990234375 + }, + "boundVariables": { + "fills": [ + { + "id": "VariableID:9:358", + "type": "VARIABLE_ALIAS" + } + ], + "fontSize": [ + { + "id": "VariableID:18:938", + "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": 40, + "height": 53, + "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:7f649150dd922eb61a2b92d74e277e4a43e48ec4,", + "textTruncation": "DISABLED", + "visible": true, + "width": 476, + "x": 0, + "y": 32, + "styledTextSegments": [ + { + "characters": "관절재생으로 새로운 삶을", + "end": 13, + "fillStyleId": "", + "fontWeight": 800, + "hyperlink": null, + "indentation": 0, + "listOptions": { + "type": "NONE" + }, + "start": 0, + "textStyleId": "S:7f649150dd922eb61a2b92d74e277e4a43e48ec4," + } + ] + }, + "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 diff --git a/crates/devup-mcp-devup-ui/tests/folded_asset_size.rs b/crates/devup-mcp-devup-ui/tests/folded_asset_size.rs index 406eda17..018afc5b 100644 --- a/crates/devup-mcp-devup-ui/tests/folded_asset_size.rs +++ b/crates/devup-mcp-devup-ui/tests/folded_asset_size.rs @@ -53,7 +53,10 @@ fn fixed_non_square_frame_folded_into_mask_keeps_its_size() { .expect("codegen") .tsx; - assert!(tsx.contains("maskImage=\"url('/icons/BI Logo.svg')\"")); + assert!( + tsx.contains("maskImage=\"url(/icons/bi-logo.svg)\""), + "{tsx}" + ); assert!( tsx.contains("h=\"9px\"") && tsx.contains("w=\"24px\""), "folded mask lost its fixed dimensions:\n{tsx}" diff --git a/crates/devup-mcp-devup-ui/tests/free_placement_anchor.rs b/crates/devup-mcp-devup-ui/tests/free_placement_anchor.rs index 8f57b682..50b83a4d 100644 --- a/crates/devup-mcp-devup-ui/tests/free_placement_anchor.rs +++ b/crates/devup-mcp-devup-ui/tests/free_placement_anchor.rs @@ -405,7 +405,7 @@ fn an_asset_is_not_given_the_opacity_its_export_already_carries() { ); let picture = tsx .lines() - .find(|line| line.contains("/images/picture")) + .find(|line| line.contains("/images/card-picture")) .expect("the picture is an image"); assert!( !picture.contains("opacity="), 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..cfa857ab 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,45 @@ 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")); +} + +#[test] +fn readable_names_reuse_image_hash_across_roles_and_number_only_collisions() { + let snapshot: Snapshot = serde_json::from_value(json!({"fileKey":"fixture","roots":["root"],"diagnostics":[],"nodes":{ + "root":{"id":"root","type":"FRAME","fields":{"name":"Main","childrenIds":["1:1","1:2","1:3"]}}, + "1:1":{"id":"1:1","type":"RECTANGLE","fields":{"name":"R&D Lab","parentId":"root","isAsset":true,"fills":[{"type":"IMAGE","imageHash":"same"}]}}, + "1:2":{"id":"1:2","type":"RECTANGLE","fields":{"name":"Mobile Photo","parentId":"root","isAsset":true,"fills":[{"type":"IMAGE","imageHash":"same"}]}}, + "1:3":{"id":"1:3","type":"RECTANGLE","fields":{"name":"R&D Lab","parentId":"root","isAsset":true,"fills":[{"type":"IMAGE","imageHash":"different"}]}} + }})).unwrap(); + let paths = ["1:1", "1:2", "1:3"].map(|id| image_fill_path(&snapshot, id, 0, true).unwrap()); + assert_eq!( + paths, + [ + "/images/r-d-lab.png", + "/images/r-d-lab.png", + "/images/r-d-lab-2.png" + ] + ); +} + +#[test] +fn distinct_svg_variants_do_not_share_a_filename_when_per_node_is_off() { + let snapshot: Snapshot = serde_json::from_value(json!({"fileKey":"fixture","roots":["a","b"],"diagnostics":[],"nodes":{ + "a":{"id":"a","type":"VECTOR","fields":{"name":"Icon","width":24,"height":24,"isAsset":true,"variantProperties":{"Kind":"clock"}}}, + "b":{"id":"b","type":"VECTOR","fields":{"name":"Icon","width":24,"height":24,"isAsset":true,"variantProperties":{"Kind":"chart"}}} + }})).unwrap(); + assert_ne!( + asset_path(&snapshot, "a", false), + asset_path(&snapshot, "b", false) + ); +} diff --git a/crates/devup-mcp-devup-ui/tests/image_card_layout.rs b/crates/devup-mcp-devup-ui/tests/image_card_layout.rs index e1f1131e..4ec8f4b1 100644 --- a/crates/devup-mcp-devup-ui/tests/image_card_layout.rs +++ b/crates/devup-mcp-devup-ui/tests/image_card_layout.rs @@ -428,6 +428,12 @@ fn omitted_or_absolute_images_and_non_flex_boxes_do_not_claim_intrinsic_flex_ris .unwrap() .fields .remove("inferredAutoLayout"); + snapshot + .nodes + .get_mut("root") + .unwrap() + .fields + .remove("layoutMode"); } let output = generate(&snapshot); assert!( diff --git a/crates/devup-mcp-devup-ui/tests/instance_overrides.rs b/crates/devup-mcp-devup-ui/tests/instance_overrides.rs new file mode 100644 index 00000000..7cc02192 --- /dev/null +++ b/crates/devup-mcp-devup-ui/tests/instance_overrides.rs @@ -0,0 +1,98 @@ +use devup_mcp_devup_ui::codegen::{ + CodegenOptions, generate_component, instance_overrides::instance_overrides, +}; +use devup_mcp_figma::{RawNode, Snapshot}; +use serde_json::json; + +fn button() -> (Snapshot, CodegenOptions) { + let mut snapshot: Snapshot = + serde_json::from_str(include_str!("fixtures/chondrogen_button_set.json")).unwrap(); + let mut instance = snapshot.nodes["24:1853"].clone(); + instance.id = "51:5705".into(); + instance.node_type = "INSTANCE".into(); + instance + .fields + .insert("mainComponentId".into(), json!("24:1853")); + instance + .fields + .insert("mainComponentSetId".into(), json!("23:1613")); + instance.fields.insert( + "variantProperties".into(), + json!({"effect":"default","size":"md","type":"primary"}), + ); + instance.fields.insert("childrenIds".into(), json!([])); + instance.fields.insert( + "overrides".into(), + json!([{"id":"51:5705","overriddenFields":["fills"]}]), + ); + instance.fields.insert("fills".into(), json!([{"type":"SOLID","color":{"r":0.0235294118,"g":0.51372549,"b":0.56862745},"boundVariables":{"color":{"type":"VARIABLE_ALIAS","id":"VariableID:9:346"}}}])); + snapshot.nodes.insert(instance.id.clone(), instance); + snapshot.roots = vec!["51:5705".into()]; + let options = CodegenOptions { + variable_tokens: [("VariableID:9:346".into(), "secondary".into())].into(), + inline_instances: true, + ..CodegenOptions::default() + }; + (snapshot, options) +} + +#[test] +fn chondrogen_primary_button_fill_override_requires_preservation() { + let (snapshot, options) = button(); + let records = instance_overrides(&snapshot, "51:5705", &options); + assert_eq!(records.len(), 1); + assert_eq!(records[0]["classification"], "not-expressible"); + assert_eq!(records[0]["mainComponentId"], "24:1853"); + assert_eq!(records[0]["mainComponentSetId"], "23:1613"); + assert_eq!(records[0]["differsFromMaster"], true); + assert!( + records[0]["generatedProps"] + .as_array() + .unwrap() + .contains(&json!("bg=\"$secondary\"")) + ); + let output = generate_component(&snapshot, "51:5705", &options).unwrap(); + assert!(output.tsx.contains("// @devup-override ")); + assert!( + output + .diagnostics + .iter() + .any(|issue| issue.code == "DEVUP_CODEGEN_INSTANCE_OVERRIDE") + ); +} + +#[test] +fn text_characters_bound_to_text_component_property_is_not_loss() { + let (mut snapshot, options) = button(); + let child: RawNode = + serde_json::from_value(json!({"id":"I51:5705;24:1852","type":"TEXT","fields":{ + "characters":"Custom label","componentPropertyReferences":{"characters":"Label#1:2"}}})) + .unwrap(); + snapshot.nodes.insert(child.id.clone(), child); + let instance = snapshot.nodes.get_mut("51:5705").unwrap(); + instance + .fields + .insert("childrenIds".into(), json!(["I51:5705;24:1852"])); + instance.fields.insert( + "componentProperties".into(), + json!({"Label#1:2":{"type":"TEXT","value":"Custom label"}}), + ); + instance.fields.insert( + "overrides".into(), + json!([{"id":"I51:5705;24:1852","overriddenFields":["characters"]}]), + ); + assert!(instance_overrides(&snapshot, "51:5705", &options).is_empty()); +} + +#[test] +fn unbound_characters_and_missing_descendants_are_reported() { + let (mut snapshot, options) = button(); + snapshot.nodes.get_mut("51:5705").unwrap().fields.insert( + "overrides".into(), + json!([ + {"id":"missing","overriddenFields":["characters"]}]), + ); + let records = instance_overrides(&snapshot, "51:5705", &options); + assert_eq!(records[0]["resolution"], "unresolved"); + assert_eq!(records[0]["generatedProps"], json!([])); +} diff --git a/crates/devup-mcp-devup-ui/tests/placement_contract.rs b/crates/devup-mcp-devup-ui/tests/placement_contract.rs index eaa763fa..7caf93c6 100644 --- a/crates/devup-mcp-devup-ui/tests/placement_contract.rs +++ b/crates/devup-mcp-devup-ui/tests/placement_contract.rs @@ -100,7 +100,7 @@ fn r4_embedded_root_anchors_children_and_discloses_host_size() { } #[test] -fn r4_absolute_capture_root_discloses_external_containing_block() { +fn r4_absolute_capture_root_uses_normal_flow_without_collected_parent() { let mut snapshot = snapshot(); snapshot.nodes.remove("3997:46582"); snapshot.roots = vec!["3997:46621".into()]; @@ -112,11 +112,11 @@ fn r4_absolute_capture_root_discloses_external_containing_block() { .unwrap(); assert_eq!( contract.details.as_ref().unwrap()["containingBlock"], - "external-host" + "normal-flow-host" ); assert_eq!( contract.fidelity_impact(), - devup_mcp_figma::FidelityImpact::Approximated + devup_mcp_figma::FidelityImpact::None ); } @@ -254,7 +254,7 @@ fn r4_absolute_root_isolates_its_background_child() { ) .unwrap(); let tag = root_tag(&output.tsx); - assert!(tag.contains("pos=\"absolute\""), "{tag}"); + assert!(tag.contains("pos=\"relative\""), "{tag}"); assert!(tag.contains("zIndex=\"0\""), "{tag}"); assert!(output.tsx.contains("zIndex=\"-1\""), "{}", output.tsx); } diff --git a/crates/devup-mcp-devup-ui/tests/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#""#, diff --git a/crates/devup-mcp-devup-ui/tests/provenance.rs b/crates/devup-mcp-devup-ui/tests/provenance.rs index 4383150c..c33effb7 100644 --- a/crates/devup-mcp-devup-ui/tests/provenance.rs +++ b/crates/devup-mcp-devup-ui/tests/provenance.rs @@ -37,6 +37,7 @@ fn fidelity_snapshot(characters: &str) -> Snapshot { "name": "Card", "childrenIds": ["1:2"], "layoutMode": "VERTICAL", "inferredAutoLayout": {"layoutMode": "VERTICAL"}, "width": 320, "height": 200, "clipsContent": true, + "layoutSizingHorizontal": "FIXED", "layoutSizingVertical": "FIXED", "fills": [{ "type": "SOLID", "color": {"r": 1, "g": 0, "b": 0}, "boundVariables": {"color": {"type": "VARIABLE_ALIAS", "id": "v"}} @@ -347,7 +348,7 @@ fn asset_boundaries_exclude_internal_and_descendant_layout_fields() { json!({ "name": "Folded icon", "parentId": "root", "childrenIds": ["glyph-left", "glyph-right"], - "layoutMode": "HORIZONTAL", "layoutPositioning": "ABSOLUTE", + "layoutMode": "NONE", "layoutPositioning": "ABSOLUTE", "layoutSizingHorizontal": "FIXED", "layoutSizingVertical": "FIXED", "itemSpacing": 4, "paddingTop": 1, "paddingRight": 2, "paddingBottom": 3, "paddingLeft": 4, diff --git a/crates/devup-mcp-devup-ui/tests/responsive_contract.rs b/crates/devup-mcp-devup-ui/tests/responsive_contract.rs index 790dd216..95ddcbb1 100644 --- a/crates/devup-mcp-devup-ui/tests/responsive_contract.rs +++ b/crates/devup-mcp-devup-ui/tests/responsive_contract.rs @@ -279,7 +279,7 @@ fn a_frame_laid_out_differently_by_width_keeps_each_direction() { }); let compact = merged.tsx.split_whitespace().collect::(); assert!( - compact.contains(r#"flexDir={["column",null,null,null,"initial"]}"#), + compact.contains(r#"flexDir={["column",null,null,null,"row"]}"#), "{}", merged.tsx ); diff --git a/crates/devup-mcp-devup-ui/tests/responsive_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-devup-ui/tests/responsive_merge.rs b/crates/devup-mcp-devup-ui/tests/responsive_merge.rs index eb9f6425..65fb3e04 100644 --- a/crates/devup-mcp-devup-ui/tests/responsive_merge.rs +++ b/crates/devup-mcp-devup-ui/tests/responsive_merge.rs @@ -354,24 +354,22 @@ fn a_prop_outside_the_cleared_set_is_left_to_inherit() { ); } -/// A first value that is simply what the prop already is need not be written. -/// `flexDir="row"` and `alignItems="flex-start"` are the two this hits in -/// practice. +/// A drawn value must override any layout implied by the merged primitive. #[test] -fn a_leading_default_is_dropped() { +fn a_leading_drawn_default_is_preserved() { assert_eq!( slots( "flexDir", [Set("row"), Absent, Absent, Absent, Set("column")] ), - array(&[None, None, None, None, Some("column")]) + array(&[Some("row"), None, None, None, Some("column")]) ); assert_eq!( slots( "alignItems", [Set("flex-start"), Absent, Absent, Absent, Set("center")] ), - array(&[None, None, None, None, Some("center")]) + array(&[Some("flex-start"), None, None, None, Some("center")]) ); // The same value on a prop with no registered default stays put. assert_eq!( diff --git a/crates/devup-mcp-devup-ui/tests/responsive_root_width.rs b/crates/devup-mcp-devup-ui/tests/responsive_root_width.rs new file mode 100644 index 00000000..6f5edba8 --- /dev/null +++ b/crates/devup-mcp-devup-ui/tests/responsive_root_width.rs @@ -0,0 +1,49 @@ +//! A screen's widths are its frame widths, not a layout: the merged root +//! takes its container's width instead of `w={['360px', ..., '1440px']}`. + +use devup_mcp_devup_ui::codegen::{CodegenOptions, responsive::merge_breakpoints}; +use devup_mcp_figma::Snapshot; +use serde_json::json; + +fn snapshot() -> Snapshot { + let frame = |id: &str, name: &str, width: u32, child: &str| { + json!({"id":id,"type":"FRAME","fields":{"name":name,"width":width,"height":600, + "layoutMode":"VERTICAL","inferredAutoLayout":{"layoutMode":"VERTICAL"}, + "layoutSizingHorizontal":"FIXED","layoutSizingVertical":"FIXED", + "childrenIds":[child]}}) + }; + let card = |id: &str, parent: &str, width: u32| { + json!({"id":id,"type":"FRAME","fields":{"name":"card","parentId":parent, + "width":width,"height":120,"layoutSizingHorizontal":"FIXED", + "layoutSizingVertical":"FIXED","visible":true}}) + }; + serde_json::from_value(json!({ + "fileKey":"root-width","version":null,"roots":["m","d"],"diagnostics":[], + "nodes":{ + "m": frame("m", "mobile", 360, "mc"), + "mc": card("mc", "m", 320), + "d": frame("d", "desktop", 1440, "dc"), + "dc": card("dc", "d", 1200), + } + })) + .unwrap() +} + +#[test] +fn the_merged_root_does_not_carry_its_frame_widths() { + let merged = merge_breakpoints(&snapshot(), &CodegenOptions::default()) + .unwrap() + .unwrap(); + let root = merged.tsx.split('>').next().unwrap(); + assert!(!root.contains("w="), "{}", merged.tsx); + assert!(!merged.tsx.contains("\"1440px\""), "{}", merged.tsx); + assert!(merged.tsx.contains("\"1200px\""), "{}", merged.tsx); + assert!( + !merged + .gaps + .iter() + .any(|gap| gap["reason"] == "attribute-only-at-width" && gap["property"] == "w"), + "{:?}", + merged.gaps + ); +} diff --git a/crates/devup-mcp-devup-ui/tests/responsive_structural.rs b/crates/devup-mcp-devup-ui/tests/responsive_structural.rs new file mode 100644 index 00000000..a102d7b2 --- /dev/null +++ b/crates/devup-mcp-devup-ui/tests/responsive_structural.rs @@ -0,0 +1,192 @@ +use devup_mcp_devup_ui::codegen::{CodegenOptions, responsive::merge_breakpoints}; +use devup_mcp_figma::Snapshot; +use serde_json::{Value, json}; + +fn merge( + changes: impl Fn(usize, &mut Value), +) -> devup_mcp_devup_ui::codegen::responsive::MergedScreen { + let mut nodes = serde_json::Map::new(); + for (index, width) in [1440, 360].into_iter().enumerate() { + let frame = json!({"id":format!("root:{index}"),"type":"FRAME","fields":{ + "name":"screen","width":width,"height":200,"layoutMode":"VERTICAL", + "childrenIds":[format!("section:{index}")] + }}); + let mut section = json!({"id":format!("section:{index}"),"type":"FRAME","fields":{ + "name":"Footer","parentId":format!("root:{index}"),"width":width,"height":120, + "layoutMode":"HORIZONTAL","layoutSizingHorizontal":"FIXED","layoutSizingVertical":"FIXED", + "paddingTop":0,"paddingRight":0,"paddingBottom":0,"paddingLeft":0, + "childrenIds":[format!("logo:{index}"),format!("links:{index}")] + }}); + changes(index, &mut section); + let layout = section["fields"].clone(); + section["fields"]["inferredAutoLayout"] = layout; + let logo = json!({"id":format!("logo:{index}"),"type":"FRAME","fields":{ + "name":"Logo","parentId":format!("section:{index}"),"width":if index == 0 {240} else {173}, + "height":28,"layoutSizingHorizontal":"FIXED","layoutSizingVertical":"FIXED" + }}); + let links = json!({"id":format!("links:{index}"),"type":"TEXT","fields":{ + "name":"Links","parentId":format!("section:{index}"),"characters":"Company", + "fontSize":16,"width":100,"height":24,"textAutoResize":"WIDTH_AND_HEIGHT" + }}); + for node in [frame, section, logo, links] { + nodes.insert(node["id"].as_str().unwrap().to_owned(), node.clone()); + } + for name in ["search", "menu", "locale"] { + let id = format!("{name}:{index}"); + nodes.insert( + id.clone(), + json!({"id":id,"type":"TEXT","fields":{ + "name":name,"parentId":format!("section:{index}"),"characters":name, + "fontSize":16,"width":36,"height":24,"textAutoResize":"WIDTH_AND_HEIGHT" + }}), + ); + } + } + let snapshot: Snapshot = serde_json::from_value(json!({ + "fileKey":"structural","version":null,"roots":["root:0","root:1"],"nodes":nodes,"diagnostics":[] + })).unwrap(); + merge_breakpoints(&snapshot, &CodegenOptions::default()) + .unwrap() + .unwrap() +} + +#[test] +fn component_identity_pairs_renamed_containers_before_shape_matching() { + let merged = merge(|index, section| { + section["fields"]["mainComponentId"] = json!("footer-master"); + section["fields"]["name"] = json!(if index == 0 { + "wide footer" + } else { + "compact footer" + }); + section["fields"]["layoutMode"] = json!(if index == 0 { "HORIZONTAL" } else { "VERTICAL" }); + }); + let sections = merged + .elements + .iter() + .filter(|element| element.ids.iter().any(|(_, id)| id.starts_with("section:"))) + .collect::>(); + assert_eq!(sections.len(), 1, "{}", merged.tsx); + assert_eq!(sections[0].ids.len(), 2, "{}", merged.tsx); + assert!(sections[0].hidden_at.is_empty()); + assert!(merged.diagnostics.is_empty(), "{:?}", merged.diagnostics); +} + +#[test] +fn footer_direction_and_padding_merge_despite_different_container_shapes() { + let merged = merge(|index, section| { + section["fields"]["layoutMode"] = json!(if index == 0 { "HORIZONTAL" } else { "VERTICAL" }); + section["fields"]["paddingTop"] = json!(if index == 0 { 60 } else { 36 }); + section["fields"]["paddingBottom"] = json!(if index == 0 { 60 } else { 36 }); + }); + let compact = merged.tsx.split_whitespace().collect::(); + assert!( + compact.contains("flexDir={[\"column\",null,null,null,\"row\"]}"), + "{}", + merged.tsx + ); + assert!( + compact.contains("py={[\"36px\",null,null,null,\"60px\"]}"), + "{}", + merged.tsx + ); + assert!(!compact.contains("\"none\""), "{}", merged.tsx); + assert_eq!( + merged + .elements + .iter() + .filter(|element| element.ids.iter().any(|(_, id)| id.starts_with("logo:"))) + .count(), + 1 + ); +} + +#[test] +fn prop_names_do_not_split_the_same_child_between_widths() { + let merged = merge(|index, section| { + if index == 1 { + section["fields"]["paddingLeft"] = json!(12); + section["fields"]["paddingRight"] = json!(16); + } + }); + let compact = merged.tsx.split_whitespace().collect::(); + assert!(!compact.contains("\"none\""), "{}", merged.tsx); + assert!(compact.contains("pl={[\"12px\""), "{}", merged.tsx); + assert!(compact.contains("pr={[\"16px\""), "{}", merged.tsx); +} + +#[test] +fn header_shared_logo_survives_different_sibling_sets() { + let merged = merge(|index, section| { + section["fields"]["name"] = json!("Header"); + section["fields"]["childrenIds"] = if index == 1 { + json!(["logo:1", "search:1", "menu:1"]) + } else { + json!(["logo:0", "links:0", "locale:0"]) + }; + }); + let logos = merged + .elements + .iter() + .filter(|element| element.ids.iter().any(|(_, id)| id.starts_with("logo:"))) + .collect::>(); + assert_eq!(logos.len(), 1, "{}", merged.tsx); + assert_eq!(logos[0].ids.len(), 2, "{}", merged.tsx); + assert!(logos[0].hidden_at.is_empty(), "{}", merged.tsx); + for (id, hidden_width) in [ + ("links:0", 360), + ("locale:0", 360), + ("search:1", 1440), + ("menu:1", 1440), + ] { + assert!( + merged + .elements + .iter() + .any(|element| element.ids.iter().any(|(_, node)| node == id) + && element.hidden_at == [hidden_width]), + "{id}: {}", + merged.tsx + ); + } + assert!( + merged + .tsx + .split_whitespace() + .collect::() + .contains("w={[\"173px\",null,null,null,\"240px\"]}"), + "{}", + merged.tsx + ); +} + +#[test] +fn ambiguous_counterparts_report_node_ids_instead_of_silently_hiding() { + let merged = merge(|index, section| { + if index == 0 { + section["fields"]["childrenIds"] = json!(["logo:0", "logo:0", "links:0"]); + } + }); + let issue = merged + .diagnostics + .iter() + .find(|issue| issue.code == "DEVUP_CODEGEN_RESPONSIVE_UNMATCHED") + .expect("ambiguous pairing must be reported"); + let details = issue.details.as_ref().unwrap(); + assert!( + details["nodesByWidth"] + .as_array() + .unwrap() + .iter() + .any(|node| node["nodeId"] == "logo:0") + ); + assert!( + details["nodesByWidth"] + .as_array() + .unwrap() + .iter() + .any(|node| node["nodeId"] == "logo:1") + ); + assert!(details.get("reason").is_some()); + assert!(details.get("nextAction").is_some()); +} diff --git a/crates/devup-mcp-devup-ui/tests/responsive_vector_variant.rs b/crates/devup-mcp-devup-ui/tests/responsive_vector_variant.rs new file mode 100644 index 00000000..f676372e --- /dev/null +++ b/crates/devup-mcp-devup-ui/tests/responsive_vector_variant.rs @@ -0,0 +1,83 @@ +//! A component set whose mobile variant is drawn with vectors only and whose +//! desktop variant carries text stays one component reference at every width. + +use devup_mcp_devup_ui::codegen::{CodegenOptions, responsive::merge_breakpoints}; +use devup_mcp_figma::Snapshot; +use serde_json::json; + +fn snapshot() -> Snapshot { + let vector = |id: &str, parent: &str| { + json!({"id":id,"type":"VECTOR","fields":{"name":"logo","parentId":parent, + "width":120,"height":24,"visible":true, + "fills":[{"type":"SOLID","visible":true,"color":{"r":0,"g":0,"b":0}}]}}) + }; + let text = |id: &str, parent: &str| { + json!({"id":id,"type":"TEXT","fields":{"name":"menu","parentId":parent, + "characters":"Company","fontSize":16,"width":80,"height":24, + "textAutoResize":"WIDTH_AND_HEIGHT","visible":true}}) + }; + let frame = |id: &str, name: &str, width: u32, child: &str| { + json!({"id":id,"type":"FRAME","fields":{"name":name,"width":width,"height":600, + "layoutMode":"VERTICAL","inferredAutoLayout":{"layoutMode":"VERTICAL"}, + "layoutSizingHorizontal":"FIXED","layoutSizingVertical":"FIXED", + "childrenIds":[child]}}) + }; + let instance = |id: &str, parent: &str, main: &str, width: u32, child: &str, value: &str| { + json!({"id":id,"type":"INSTANCE","fields":{"name":"Header","parentId":parent, + "mainComponentId":main,"mainComponentSetId":"set","width":width,"height":64, + "componentProperties":{"type":{"type":"VARIANT","value":value}}, + "childrenIds":[child]}}) + }; + let nodes = json!({ + "set": {"id":"set","type":"COMPONENT_SET","fields":{"name":"Header","childrenIds":["cm","cd"]}}, + "cm": {"id":"cm","type":"COMPONENT","fields":{"name":"type=mobile","parentId":"set", + "width":360,"height":64,"variantProperties":{"type":"mobile"},"childrenIds":["cmv"]}}, + "cmv": vector("cmv", "cm"), + "cd": {"id":"cd","type":"COMPONENT","fields":{"name":"type=desktop","parentId":"set", + "width":1440,"height":80,"layoutMode":"HORIZONTAL","variantProperties":{"type":"desktop"}, + "childrenIds":["cdt"]}}, + "cdt": text("cdt", "cd"), + "m": frame("m", "mobile", 360, "mi"), + "mi": instance("mi", "m", "cm", 360, "miv", "mobile"), + "miv": vector("miv", "mi"), + "d": frame("d", "desktop", 1440, "di"), + "di": instance("di", "d", "cd", 1440, "dit", "desktop"), + "dit": text("dit", "di"), + }); + serde_json::from_value(json!({ + "fileKey":"vector-variant","version":null,"roots":["m","d"],"nodes":nodes,"diagnostics":[] + })) + .unwrap() +} + +#[test] +fn a_vector_only_variant_of_a_text_component_is_still_referenced() { + let merged = merge_breakpoints(&snapshot(), &CodegenOptions::default()) + .unwrap() + .unwrap(); + assert!( + merged.components.contains("Header"), + "{:?}\n{}", + merged.components, + merged.tsx + ); + assert!(!merged.tsx.contains("{/* @@ -163,6 +171,7 @@ export function AStoryFProofread() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" whiteSpace="pre-wrap" @@ -182,7 +191,7 @@ export function AStoryFProofread() { aspectRatio="1" bg="$text" boxSize="20px" - maskImage="url(/icons/Icons.svg)" + maskImage="url(/icons/icons.svg)" maskPos="center" maskRepeat="no-repeat" maskSize="contain" @@ -205,6 +214,7 @@ export function AStoryFProofread() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -217,6 +227,7 @@ export function AStoryFProofread() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -231,6 +242,7 @@ export function AStoryFProofread() { flex="1" fontSize="22px" fontWeight="700" + letterSpacing="-0.02em" lineHeight="31px" typography="h5" wordBreak="keep-all" @@ -252,6 +264,7 @@ export function AStoryFProofread() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -264,6 +277,7 @@ export function AStoryFProofread() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -277,6 +291,7 @@ export function AStoryFProofread() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" typography="body" w="100%" @@ -299,6 +314,7 @@ export function AStoryFProofread() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -311,6 +327,7 @@ export function AStoryFProofread() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -324,6 +341,7 @@ export function AStoryFProofread() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" typography="body" w="100%" @@ -354,6 +372,7 @@ export function AStoryFProofread() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -366,6 +385,7 @@ export function AStoryFProofread() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -379,6 +399,7 @@ export function AStoryFProofread() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" typography="body" w="100%" @@ -401,6 +422,7 @@ export function AStoryFProofread() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -413,6 +435,7 @@ export function AStoryFProofread() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -426,6 +449,7 @@ export function AStoryFProofread() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" typography="body" w="100%" @@ -460,6 +484,7 @@ export function AStoryFProofread() { color="$text" fontSize="18px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="31px" typography="bodySemibold" wordBreak="keep-all" @@ -472,6 +497,7 @@ export function AStoryFProofread() { color="#FFF" fontSize="18px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="31px" typography="bodySemibold" wordBreak="keep-all" diff --git a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151__wquw_151_proofread_devup_ui_embedded.snap b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151__wquw_151_proofread_devup_ui_embedded.snap index ba6cd88f..a4e743cf 100644 --- a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151__wquw_151_proofread_devup_ui_embedded.snap +++ b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151__wquw_151_proofread_devup_ui_embedded.snap @@ -19,6 +19,7 @@ export function AStoryFProofread() { color="$text" fontSize="28px" fontWeight="700" + letterSpacing="-0.02em" lineHeight="39px" textAlign="center" typography="h3" @@ -34,6 +35,7 @@ export function AStoryFProofread() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -44,6 +46,7 @@ export function AStoryFProofread() { color="$primary" fontSize="17px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="29px" typography="bodySmSemibold" wordBreak="keep-all" @@ -58,6 +61,7 @@ export function AStoryFProofread() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -68,6 +72,7 @@ export function AStoryFProofread() { color="$primary" fontSize="17px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="29px" typography="bodySmSemibold" wordBreak="keep-all" @@ -78,7 +83,7 @@ export function AStoryFProofread() { aspectRatio="1" bg="$caption" boxSize="16px" - maskImage="url(/icons/Icons.svg)" + maskImage="url(/icons/icons.svg)" maskPos="center" maskRepeat="no-repeat" maskSize="contain" @@ -103,7 +108,7 @@ export function AStoryFProofread() { bg="$captionLight" boxSize="40px" flexShrink="0" - maskImage="url(/icons/icons.svg)" + maskImage="url(/icons/icons-2.svg)" maskPos="center" maskRepeat="no-repeat" maskSize="contain" @@ -112,6 +117,7 @@ export function AStoryFProofread() { color="$textLight" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" textAlign="center" typography="caption" @@ -129,6 +135,7 @@ export function AStoryFProofread() { color="$textLight" fontSize="14px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSm" wordBreak="keep-all" @@ -148,6 +155,7 @@ export function AStoryFProofread() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" > @@ -157,6 +165,7 @@ export function AStoryFProofread() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" whiteSpace="pre-wrap" @@ -176,7 +185,7 @@ export function AStoryFProofread() { aspectRatio="1" bg="$text" boxSize="20px" - maskImage="url(/icons/Icons.svg)" + maskImage="url(/icons/icons.svg)" maskPos="center" maskRepeat="no-repeat" maskSize="contain" @@ -199,6 +208,7 @@ export function AStoryFProofread() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -211,6 +221,7 @@ export function AStoryFProofread() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -225,6 +236,7 @@ export function AStoryFProofread() { flex="1" fontSize="22px" fontWeight="700" + letterSpacing="-0.02em" lineHeight="31px" typography="h5" wordBreak="keep-all" @@ -246,6 +258,7 @@ export function AStoryFProofread() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -258,6 +271,7 @@ export function AStoryFProofread() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -271,6 +285,7 @@ export function AStoryFProofread() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" typography="body" w="100%" @@ -293,6 +308,7 @@ export function AStoryFProofread() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -305,6 +321,7 @@ export function AStoryFProofread() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -318,6 +335,7 @@ export function AStoryFProofread() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" typography="body" w="100%" @@ -348,6 +366,7 @@ export function AStoryFProofread() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -360,6 +379,7 @@ export function AStoryFProofread() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -373,6 +393,7 @@ export function AStoryFProofread() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" typography="body" w="100%" @@ -395,6 +416,7 @@ export function AStoryFProofread() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -407,6 +429,7 @@ export function AStoryFProofread() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -420,6 +443,7 @@ export function AStoryFProofread() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" typography="body" w="100%" @@ -454,6 +478,7 @@ export function AStoryFProofread() { color="$text" fontSize="18px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="31px" typography="bodySemibold" wordBreak="keep-all" @@ -466,6 +491,7 @@ export function AStoryFProofread() { color="#FFF" fontSize="18px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="31px" typography="bodySemibold" wordBreak="keep-all" diff --git a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151__wquw_151_proofread_diagnostics.snap b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151__wquw_151_proofread_diagnostics.snap index 464a58f8..f59ac3b9 100644 --- a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151__wquw_151_proofread_diagnostics.snap +++ b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151__wquw_151_proofread_diagnostics.snap @@ -1,6 +1,5 @@ --- source: crates/devup-mcp-devup-ui/tests/wquw_151.rs -assertion_line: 231 expression: json!(output.diagnostics) --- [ @@ -54,7 +53,7 @@ expression: json!(output.diagnostics) "appliedValue": { "state": "emitted", "generatedNodeId": "3879:35547", - "generatedSource": " \n 작은 시장, 큰 사랑\n ", + "generatedSource": " \n 작은 시장, 큰 사랑\n ", "sourceTruncated": false, "propertyMappingVerified": false }, @@ -69,7 +68,7 @@ expression: json!(output.diagnostics) "parentLayoutMode": "HORIZONTAL", "childSizing": "FILL", "generatedParent": "
\n \n " + " \n \n " ], "fallback": null, "heightPreservation": { @@ -550,9 +549,9 @@ expression: json!(output.diagnostics) } }, "color": { - "b": 0.9586742520332336, - "g": 0.96465665102005, - "r": 0.9695513844490051 + "b": 0.958674252033, + "g": 0.96465665102, + "r": 0.969551384449 }, "opacity": 1, "type": "SOLID", @@ -717,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", @@ -738,7 +737,7 @@ expression: json!(output.diagnostics) "originalResourceId": null, "appliedValue": { "generatedSource": [ - " \n \n \n 편집 설정\n \n
\n
\n \n 검토 완료\n \n
\n " + " \n \n \n 편집 설정\n
\n \n
\n \n 검토 완료\n \n
\n " ], "fallback": null, "heightPreservation": { diff --git a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151__wquw_151_proofread_source_map.snap b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151__wquw_151_proofread_source_map.snap index cd412993..8bd68c00 100644 --- a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151__wquw_151_proofread_source_map.snap +++ b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151__wquw_151_proofread_source_map.snap @@ -156,6 +156,13 @@ expression: output.source_map "resolution": "raw-fallback", "elementIndex": 2 }, + { + "generatedProperty": "letterSpacing=\"-0.02em\"", + "nodeId": "3879:35520", + "property": "letterSpacing", + "resolution": "raw-fallback", + "elementIndex": 2 + }, { "calculation": "round(fontSize * percent / 100) = round(28 * 140 / 100) = 39px", "generatedProperty": "lineHeight=\"39px\"", @@ -335,6 +342,13 @@ expression: output.source_map "resolution": "raw-fallback", "elementIndex": 6 }, + { + "generatedProperty": "letterSpacing=\"-0.01em\"", + "nodeId": "3879:35523", + "property": "letterSpacing", + "resolution": "raw-fallback", + "elementIndex": 6 + }, { "calculation": "round(fontSize * percent / 100) = round(16 * 160 / 100) = 26px", "generatedProperty": "lineHeight=\"26px\"", @@ -409,6 +423,13 @@ expression: output.source_map "resolution": "raw-fallback", "elementIndex": 7 }, + { + "generatedProperty": "letterSpacing=\"-0.01em\"", + "nodeId": "3879:35524", + "property": "letterSpacing", + "resolution": "raw-fallback", + "elementIndex": 7 + }, { "calculation": "round(fontSize * percent / 100) = round(17 * 170 / 100) = 29px", "generatedProperty": "lineHeight=\"29px\"", @@ -611,6 +632,13 @@ expression: output.source_map "resolution": "raw-fallback", "elementIndex": 11 }, + { + "generatedProperty": "letterSpacing=\"-0.01em\"", + "nodeId": "3879:35528", + "property": "letterSpacing", + "resolution": "raw-fallback", + "elementIndex": 11 + }, { "calculation": "round(fontSize * percent / 100) = round(16 * 160 / 100) = 26px", "generatedProperty": "lineHeight=\"26px\"", @@ -685,6 +713,13 @@ expression: output.source_map "resolution": "raw-fallback", "elementIndex": 12 }, + { + "generatedProperty": "letterSpacing=\"-0.01em\"", + "nodeId": "3879:35529", + "property": "letterSpacing", + "resolution": "raw-fallback", + "elementIndex": 12 + }, { "calculation": "round(fontSize * percent / 100) = round(17 * 170 / 100) = 29px", "generatedProperty": "lineHeight=\"29px\"", @@ -767,7 +802,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 +968,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", @@ -977,6 +1012,13 @@ expression: output.source_map "resolution": "raw-fallback", "elementIndex": 17 }, + { + "generatedProperty": "letterSpacing=\"-0.01em\"", + "nodeId": "3879:35535", + "property": "letterSpacing", + "resolution": "raw-fallback", + "elementIndex": 17 + }, { "calculation": "round(fontSize * percent / 100) = round(15 * 160 / 100) = 24px", "generatedProperty": "lineHeight=\"24px\"", @@ -1101,6 +1143,13 @@ expression: output.source_map "resolution": "raw-fallback", "elementIndex": 22 }, + { + "generatedProperty": "letterSpacing=\"-0.01em\"", + "nodeId": "3879:35536", + "property": "letterSpacing", + "resolution": "raw-fallback", + "elementIndex": 22 + }, { "calculation": "round(fontSize * percent / 100) = round(14 * 160 / 100) = 22px", "generatedProperty": "lineHeight=\"22px\"", @@ -1253,6 +1302,13 @@ expression: output.source_map "resolution": "raw-fallback", "elementIndex": 24 }, + { + "generatedProperty": "letterSpacing=\"-0.01em\"", + "nodeId": "3879:35538", + "property": "letterSpacing", + "resolution": "raw-fallback", + "elementIndex": 24 + }, { "calculation": "round(fontSize * percent / 100) = round(16 * 160 / 100) = 26px", "generatedProperty": "lineHeight=\"26px\"", @@ -1320,6 +1376,13 @@ expression: output.source_map "resolution": "raw-fallback", "elementIndex": 25 }, + { + "generatedProperty": "letterSpacing=\"-0.01em\"", + "nodeId": "3879:35539", + "property": "letterSpacing", + "resolution": "raw-fallback", + "elementIndex": 25 + }, { "calculation": "round(fontSize * percent / 100) = round(16 * 160 / 100) = 26px", "generatedProperty": "lineHeight=\"26px\"", @@ -1486,7 +1549,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", @@ -1629,6 +1692,13 @@ expression: output.source_map "resolution": "raw-fallback", "elementIndex": 32 }, + { + "generatedProperty": "letterSpacing=\"-0.01em\"", + "nodeId": "I3879:35545;1690:32933", + "property": "letterSpacing", + "resolution": "raw-fallback", + "elementIndex": 32 + }, { "calculation": "round(fontSize * percent / 100) = round(16 * 160 / 100) = 26px", "generatedProperty": "lineHeight=\"26px\"", @@ -1767,6 +1837,13 @@ expression: output.source_map "resolution": "raw-fallback", "elementIndex": 35 }, + { + "generatedProperty": "letterSpacing=\"-0.01em\"", + "nodeId": "I3879:35545;1690:32947", + "property": "letterSpacing", + "resolution": "raw-fallback", + "elementIndex": 35 + }, { "calculation": "round(fontSize * percent / 100) = round(14 * 160 / 100) = 22px", "generatedProperty": "lineHeight=\"22px\"", @@ -1898,6 +1975,13 @@ expression: output.source_map "resolution": "raw-fallback", "elementIndex": 37 }, + { + "generatedProperty": "letterSpacing=\"-0.02em\"", + "nodeId": "3879:35547", + "property": "letterSpacing", + "resolution": "raw-fallback", + "elementIndex": 37 + }, { "calculation": "round(fontSize * percent / 100) = round(22 * 140 / 100) = 31px", "generatedProperty": "lineHeight=\"31px\"", @@ -2057,6 +2141,13 @@ expression: output.source_map "resolution": "raw-fallback", "elementIndex": 40 }, + { + "generatedProperty": "letterSpacing=\"-0.01em\"", + "nodeId": "I3879:35549;1690:32933", + "property": "letterSpacing", + "resolution": "raw-fallback", + "elementIndex": 40 + }, { "calculation": "round(fontSize * percent / 100) = round(16 * 160 / 100) = 26px", "generatedProperty": "lineHeight=\"26px\"", @@ -2195,6 +2286,13 @@ expression: output.source_map "resolution": "raw-fallback", "elementIndex": 43 }, + { + "generatedProperty": "letterSpacing=\"-0.01em\"", + "nodeId": "I3879:35549;1690:32947", + "property": "letterSpacing", + "resolution": "raw-fallback", + "elementIndex": 43 + }, { "calculation": "round(fontSize * percent / 100) = round(14 * 160 / 100) = 22px", "generatedProperty": "lineHeight=\"22px\"", @@ -2326,6 +2424,13 @@ expression: output.source_map "resolution": "raw-fallback", "elementIndex": 45 }, + { + "generatedProperty": "letterSpacing=\"-0.01em\"", + "nodeId": "3879:35551", + "property": "letterSpacing", + "resolution": "raw-fallback", + "elementIndex": 45 + }, { "calculation": "round(fontSize * percent / 100) = round(18 * 180 / 100) = 32px", "generatedProperty": "lineHeight=\"32px\"", @@ -2492,6 +2597,13 @@ expression: output.source_map "resolution": "raw-fallback", "elementIndex": 50 }, + { + "generatedProperty": "letterSpacing=\"-0.01em\"", + "nodeId": "I3879:35553;1690:32933", + "property": "letterSpacing", + "resolution": "raw-fallback", + "elementIndex": 50 + }, { "calculation": "round(fontSize * percent / 100) = round(16 * 160 / 100) = 26px", "generatedProperty": "lineHeight=\"26px\"", @@ -2630,6 +2742,13 @@ expression: output.source_map "resolution": "raw-fallback", "elementIndex": 53 }, + { + "generatedProperty": "letterSpacing=\"-0.01em\"", + "nodeId": "I3879:35553;1690:32947", + "property": "letterSpacing", + "resolution": "raw-fallback", + "elementIndex": 53 + }, { "calculation": "round(fontSize * percent / 100) = round(14 * 160 / 100) = 22px", "generatedProperty": "lineHeight=\"22px\"", @@ -2761,6 +2880,13 @@ expression: output.source_map "resolution": "raw-fallback", "elementIndex": 55 }, + { + "generatedProperty": "letterSpacing=\"-0.01em\"", + "nodeId": "3879:35555", + "property": "letterSpacing", + "resolution": "raw-fallback", + "elementIndex": 55 + }, { "generatedProperty": "lineHeight=\"32px\"", "nodeId": "3879:35555", @@ -3010,6 +3136,13 @@ expression: output.source_map "resolution": "raw-fallback", "elementIndex": 68 }, + { + "generatedProperty": "letterSpacing=\"-0.01em\"", + "nodeId": "I3879:35557;1690:32933", + "property": "letterSpacing", + "resolution": "raw-fallback", + "elementIndex": 68 + }, { "calculation": "round(fontSize * percent / 100) = round(16 * 160 / 100) = 26px", "generatedProperty": "lineHeight=\"26px\"", @@ -3148,6 +3281,13 @@ expression: output.source_map "resolution": "raw-fallback", "elementIndex": 71 }, + { + "generatedProperty": "letterSpacing=\"-0.01em\"", + "nodeId": "I3879:35557;1690:32947", + "property": "letterSpacing", + "resolution": "raw-fallback", + "elementIndex": 71 + }, { "calculation": "round(fontSize * percent / 100) = round(14 * 160 / 100) = 22px", "generatedProperty": "lineHeight=\"22px\"", @@ -3279,6 +3419,13 @@ expression: output.source_map "resolution": "raw-fallback", "elementIndex": 73 }, + { + "generatedProperty": "letterSpacing=\"-0.01em\"", + "nodeId": "3879:35559", + "property": "letterSpacing", + "resolution": "raw-fallback", + "elementIndex": 73 + }, { "calculation": "round(fontSize * percent / 100) = round(18 * 180 / 100) = 32px", "generatedProperty": "lineHeight=\"32px\"", @@ -3445,6 +3592,13 @@ expression: output.source_map "resolution": "raw-fallback", "elementIndex": 84 }, + { + "generatedProperty": "letterSpacing=\"-0.01em\"", + "nodeId": "I3879:35561;1690:32933", + "property": "letterSpacing", + "resolution": "raw-fallback", + "elementIndex": 84 + }, { "calculation": "round(fontSize * percent / 100) = round(16 * 160 / 100) = 26px", "generatedProperty": "lineHeight=\"26px\"", @@ -3583,6 +3737,13 @@ expression: output.source_map "resolution": "raw-fallback", "elementIndex": 87 }, + { + "generatedProperty": "letterSpacing=\"-0.01em\"", + "nodeId": "I3879:35561;1690:32947", + "property": "letterSpacing", + "resolution": "raw-fallback", + "elementIndex": 87 + }, { "calculation": "round(fontSize * percent / 100) = round(14 * 160 / 100) = 22px", "generatedProperty": "lineHeight=\"22px\"", @@ -3714,6 +3875,13 @@ expression: output.source_map "resolution": "raw-fallback", "elementIndex": 89 }, + { + "generatedProperty": "letterSpacing=\"-0.01em\"", + "nodeId": "3879:35563", + "property": "letterSpacing", + "resolution": "raw-fallback", + "elementIndex": 89 + }, { "calculation": "round(fontSize * percent / 100) = round(18 * 180 / 100) = 32px", "generatedProperty": "lineHeight=\"32px\"", @@ -3967,6 +4135,13 @@ expression: output.source_map "resolution": "raw-fallback", "elementIndex": 98 }, + { + "generatedProperty": "letterSpacing=\"-0.01em\"", + "nodeId": "3879:35566", + "property": "letterSpacing", + "resolution": "raw-fallback", + "elementIndex": 98 + }, { "calculation": "round(fontSize * percent / 100) = round(18 * 170 / 100) = 31px", "generatedProperty": "lineHeight=\"31px\"", @@ -4089,6 +4264,13 @@ expression: output.source_map "resolution": "raw-fallback", "elementIndex": 100 }, + { + "generatedProperty": "letterSpacing=\"-0.01em\"", + "nodeId": "3879:35568", + "property": "letterSpacing", + "resolution": "raw-fallback", + "elementIndex": 100 + }, { "calculation": "round(fontSize * percent / 100) = round(18 * 170 / 100) = 31px", "generatedProperty": "lineHeight=\"31px\"", diff --git a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35503.snap b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35503.snap index add00ae6..a8363672 100644 --- a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35503.snap +++ b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35503.snap @@ -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" @@ -35,6 +35,7 @@ export function Wquw151Frame387935503() { color="$text" fontSize="18px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="31px" textAlign="center" typography="bodySemibold" @@ -57,6 +58,7 @@ export function Wquw151Frame387935503() { flex="1" fontSize="15px" fontWeight="600" + letterSpacing="-0.05em" lineHeight="24px" overflow="hidden" textAlign="center" @@ -77,7 +79,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..24d6de14 100644 --- a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35518.snap +++ b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35518.snap @@ -25,6 +25,7 @@ export function Wquw151Frame387935518() { color="$text" fontSize="28px" fontWeight="700" + letterSpacing="-0.02em" lineHeight="39px" overflow="hidden" textAlign="center" @@ -42,6 +43,7 @@ export function Wquw151Frame387935518() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -52,6 +54,7 @@ export function Wquw151Frame387935518() { color="$primary" fontSize="17px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="29px" typography="bodySmSemibold" wordBreak="keep-all" @@ -66,6 +69,7 @@ export function Wquw151Frame387935518() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -76,6 +80,7 @@ export function Wquw151Frame387935518() { color="$primary" fontSize="17px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="29px" typography="bodySmSemibold" wordBreak="keep-all" @@ -86,7 +91,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 +116,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" @@ -120,6 +125,7 @@ export function Wquw151Frame387935518() { color="$textLight" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" overflow="hidden" textAlign="center" @@ -139,6 +145,7 @@ export function Wquw151Frame387935518() { color="$textLight" fontSize="14px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="22px" overflow="hidden" textOverflow="ellipsis" @@ -160,6 +167,7 @@ export function Wquw151Frame387935518() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" > @@ -169,6 +177,7 @@ export function Wquw151Frame387935518() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" overflow="hidden" textOverflow="ellipsis" @@ -190,7 +199,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" @@ -213,6 +222,7 @@ export function Wquw151Frame387935518() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -225,6 +235,7 @@ export function Wquw151Frame387935518() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -239,6 +250,7 @@ export function Wquw151Frame387935518() { flex="1" fontSize="22px" fontWeight="700" + letterSpacing="-0.02em" lineHeight="31px" overflow="hidden" textOverflow="ellipsis" @@ -262,6 +274,7 @@ export function Wquw151Frame387935518() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -274,6 +287,7 @@ export function Wquw151Frame387935518() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -287,6 +301,7 @@ export function Wquw151Frame387935518() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" overflow="hidden" textOverflow="ellipsis" @@ -311,6 +326,7 @@ export function Wquw151Frame387935518() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -323,6 +339,7 @@ export function Wquw151Frame387935518() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -336,6 +353,7 @@ export function Wquw151Frame387935518() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" overflow="hidden" textOverflow="ellipsis" @@ -368,6 +386,7 @@ export function Wquw151Frame387935518() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -380,6 +399,7 @@ export function Wquw151Frame387935518() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -393,6 +413,7 @@ export function Wquw151Frame387935518() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" overflow="hidden" textOverflow="ellipsis" @@ -417,6 +438,7 @@ export function Wquw151Frame387935518() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -429,6 +451,7 @@ export function Wquw151Frame387935518() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -442,6 +465,7 @@ export function Wquw151Frame387935518() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" overflow="hidden" textOverflow="ellipsis" @@ -478,6 +502,7 @@ export function Wquw151Frame387935518() { color="$text" fontSize="18px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="31px" typography="bodySemibold" wordBreak="keep-all" @@ -490,6 +515,7 @@ export function Wquw151Frame387935518() { color="#FFF" fontSize="18px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="31px" typography="bodySemibold" wordBreak="keep-all" diff --git a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35569.snap b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35569.snap index 5f8e74ab..52eeb53c 100644 --- a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35569.snap +++ b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35569.snap @@ -25,6 +25,7 @@ export function Wquw151Frame387935569() { color="$text" fontSize="28px" fontWeight="700" + letterSpacing="-0.02em" lineHeight="39px" overflow="hidden" textAlign="center" @@ -42,6 +43,7 @@ export function Wquw151Frame387935569() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -52,6 +54,7 @@ export function Wquw151Frame387935569() { color="$primary" fontSize="17px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="29px" typography="bodySmSemibold" wordBreak="keep-all" @@ -71,6 +74,7 @@ export function Wquw151Frame387935569() { color="$textLight" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" overflow="hidden" textOverflow="ellipsis" @@ -93,6 +97,7 @@ export function Wquw151Frame387935569() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -113,6 +118,7 @@ export function Wquw151Frame387935569() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -128,6 +134,7 @@ export function Wquw151Frame387935569() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -138,6 +145,7 @@ export function Wquw151Frame387935569() { color="$primary" fontSize="17px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="29px" typography="bodySmSemibold" wordBreak="keep-all" @@ -148,7 +156,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 +181,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" @@ -182,6 +190,7 @@ export function Wquw151Frame387935569() { color="$textLight" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" overflow="hidden" textAlign="center" @@ -201,6 +210,7 @@ export function Wquw151Frame387935569() { color="$textLight" fontSize="14px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="22px" overflow="hidden" textOverflow="ellipsis" @@ -222,6 +232,7 @@ export function Wquw151Frame387935569() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" > @@ -231,6 +242,7 @@ export function Wquw151Frame387935569() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" overflow="hidden" textOverflow="ellipsis" @@ -252,7 +264,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" @@ -275,6 +287,7 @@ export function Wquw151Frame387935569() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -287,6 +300,7 @@ export function Wquw151Frame387935569() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -301,6 +315,7 @@ export function Wquw151Frame387935569() { flex="1" fontSize="22px" fontWeight="700" + letterSpacing="-0.02em" lineHeight="31px" overflow="hidden" textOverflow="ellipsis" @@ -324,6 +339,7 @@ export function Wquw151Frame387935569() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -336,6 +352,7 @@ export function Wquw151Frame387935569() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -349,6 +366,7 @@ export function Wquw151Frame387935569() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" overflow="hidden" textOverflow="ellipsis" @@ -373,6 +391,7 @@ export function Wquw151Frame387935569() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -385,6 +404,7 @@ export function Wquw151Frame387935569() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -398,6 +418,7 @@ export function Wquw151Frame387935569() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" overflow="hidden" textOverflow="ellipsis" @@ -430,6 +451,7 @@ export function Wquw151Frame387935569() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -442,6 +464,7 @@ export function Wquw151Frame387935569() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -455,6 +478,7 @@ export function Wquw151Frame387935569() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" overflow="hidden" textOverflow="ellipsis" @@ -479,6 +503,7 @@ export function Wquw151Frame387935569() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -491,6 +516,7 @@ export function Wquw151Frame387935569() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -504,6 +530,7 @@ export function Wquw151Frame387935569() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" overflow="hidden" textOverflow="ellipsis" @@ -534,6 +561,7 @@ export function Wquw151Frame387935569() { color="$primary" fontSize="18px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="31px" typography="bodySemibold" wordBreak="keep-all" @@ -546,6 +574,7 @@ export function Wquw151Frame387935569() { color="#FFF" fontSize="18px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="31px" typography="bodySemibold" wordBreak="keep-all" @@ -582,6 +611,7 @@ export function Wquw151Frame387935569() { color="$text" fontSize="20px" fontWeight="700" + letterSpacing="-0.02em" lineHeight="28px" typography="h6" wordBreak="keep-all" @@ -592,6 +622,7 @@ export function Wquw151Frame387935569() { color="$textLight" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -614,7 +645,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" @@ -638,6 +669,7 @@ export function Wquw151Frame387935569() { color="$primary" fontSize="18px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="31px" typography="bodySemibold" wordBreak="keep-all" @@ -648,6 +680,7 @@ export function Wquw151Frame387935569() { color="$caption" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" typography="caption" wordBreak="keep-all" @@ -672,6 +705,7 @@ export function Wquw151Frame387935569() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" typography="body" wordBreak="keep-all" @@ -682,6 +716,7 @@ export function Wquw151Frame387935569() { color="$caption" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" typography="caption" wordBreak="keep-all" @@ -706,6 +741,7 @@ export function Wquw151Frame387935569() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" typography="body" wordBreak="keep-all" @@ -716,6 +752,7 @@ export function Wquw151Frame387935569() { color="$caption" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" typography="caption" wordBreak="keep-all" @@ -731,6 +768,7 @@ export function Wquw151Frame387935569() { color="$text" fontSize="20px" fontWeight="700" + letterSpacing="-0.02em" lineHeight="28px" typography="h6" wordBreak="keep-all" @@ -741,6 +779,7 @@ export function Wquw151Frame387935569() { color="$textLight" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -768,6 +807,7 @@ export function Wquw151Frame387935569() { color="$primary" fontSize="18px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="31px" typography="bodySemibold" wordBreak="keep-all" @@ -778,6 +818,7 @@ export function Wquw151Frame387935569() { color="$caption" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" typography="caption" wordBreak="keep-all" @@ -802,6 +843,7 @@ export function Wquw151Frame387935569() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" typography="body" wordBreak="keep-all" @@ -812,6 +854,7 @@ export function Wquw151Frame387935569() { color="$caption" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" typography="caption" wordBreak="keep-all" diff --git a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35652.snap b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35652.snap index b0da753f..69bfca9a 100644 --- a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35652.snap +++ b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35652.snap @@ -25,6 +25,7 @@ export function Wquw151Frame387935652() { color="$text" fontSize="28px" fontWeight="700" + letterSpacing="-0.02em" lineHeight="39px" overflow="hidden" textAlign="center" @@ -42,6 +43,7 @@ export function Wquw151Frame387935652() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -52,6 +54,7 @@ export function Wquw151Frame387935652() { color="$primary" fontSize="17px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="29px" typography="bodySmSemibold" wordBreak="keep-all" @@ -71,6 +74,7 @@ export function Wquw151Frame387935652() { color="$textLight" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" overflow="hidden" textOverflow="ellipsis" @@ -93,6 +97,7 @@ export function Wquw151Frame387935652() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -113,6 +118,7 @@ export function Wquw151Frame387935652() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -128,6 +134,7 @@ export function Wquw151Frame387935652() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -138,6 +145,7 @@ export function Wquw151Frame387935652() { color="$primary" fontSize="17px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="29px" typography="bodySmSemibold" wordBreak="keep-all" @@ -148,7 +156,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 +181,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" @@ -182,6 +190,7 @@ export function Wquw151Frame387935652() { color="$textLight" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" overflow="hidden" textAlign="center" @@ -201,6 +210,7 @@ export function Wquw151Frame387935652() { color="$textLight" fontSize="14px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="22px" overflow="hidden" textOverflow="ellipsis" @@ -222,6 +232,7 @@ export function Wquw151Frame387935652() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" > @@ -231,6 +242,7 @@ export function Wquw151Frame387935652() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" overflow="hidden" textOverflow="ellipsis" @@ -252,7 +264,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" @@ -275,6 +287,7 @@ export function Wquw151Frame387935652() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -287,6 +300,7 @@ export function Wquw151Frame387935652() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -301,6 +315,7 @@ export function Wquw151Frame387935652() { flex="1" fontSize="22px" fontWeight="700" + letterSpacing="-0.02em" lineHeight="31px" overflow="hidden" textOverflow="ellipsis" @@ -324,6 +339,7 @@ export function Wquw151Frame387935652() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -336,6 +352,7 @@ export function Wquw151Frame387935652() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -349,6 +366,7 @@ export function Wquw151Frame387935652() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" overflow="hidden" textOverflow="ellipsis" @@ -373,6 +391,7 @@ export function Wquw151Frame387935652() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -385,6 +404,7 @@ export function Wquw151Frame387935652() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -398,6 +418,7 @@ export function Wquw151Frame387935652() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" overflow="hidden" textOverflow="ellipsis" @@ -430,6 +451,7 @@ export function Wquw151Frame387935652() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -442,6 +464,7 @@ export function Wquw151Frame387935652() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -455,6 +478,7 @@ export function Wquw151Frame387935652() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" overflow="hidden" textOverflow="ellipsis" @@ -479,6 +503,7 @@ export function Wquw151Frame387935652() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -491,6 +516,7 @@ export function Wquw151Frame387935652() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -504,6 +530,7 @@ export function Wquw151Frame387935652() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" overflow="hidden" textOverflow="ellipsis" @@ -534,6 +561,7 @@ export function Wquw151Frame387935652() { color="$primary" fontSize="18px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="31px" typography="bodySemibold" wordBreak="keep-all" @@ -546,6 +574,7 @@ export function Wquw151Frame387935652() { color="#FFF" fontSize="18px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="31px" typography="bodySemibold" wordBreak="keep-all" @@ -582,6 +611,7 @@ export function Wquw151Frame387935652() { color="$text" fontSize="20px" fontWeight="700" + letterSpacing="-0.02em" lineHeight="28px" typography="h6" wordBreak="keep-all" @@ -592,6 +622,7 @@ export function Wquw151Frame387935652() { color="$textLight" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -614,7 +645,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" @@ -638,6 +669,7 @@ export function Wquw151Frame387935652() { color="$primary" fontSize="18px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="31px" typography="bodySemibold" wordBreak="keep-all" @@ -648,6 +680,7 @@ export function Wquw151Frame387935652() { color="$caption" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" typography="caption" wordBreak="keep-all" @@ -672,6 +705,7 @@ export function Wquw151Frame387935652() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" typography="body" wordBreak="keep-all" @@ -682,6 +716,7 @@ export function Wquw151Frame387935652() { color="$caption" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" typography="caption" wordBreak="keep-all" @@ -706,6 +741,7 @@ export function Wquw151Frame387935652() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" typography="body" wordBreak="keep-all" @@ -716,6 +752,7 @@ export function Wquw151Frame387935652() { color="$caption" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" typography="caption" wordBreak="keep-all" diff --git a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35729.snap b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35729.snap index cfae75d1..139c17b1 100644 --- a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35729.snap +++ b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35729.snap @@ -25,6 +25,7 @@ export function Wquw151Frame387935729() { color="$text" fontSize="28px" fontWeight="700" + letterSpacing="-0.02em" lineHeight="39px" overflow="hidden" textAlign="center" @@ -42,6 +43,7 @@ export function Wquw151Frame387935729() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -52,6 +54,7 @@ export function Wquw151Frame387935729() { color="$primary" fontSize="17px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="29px" typography="bodySmSemibold" wordBreak="keep-all" @@ -71,6 +74,7 @@ export function Wquw151Frame387935729() { color="$textLight" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" overflow="hidden" textOverflow="ellipsis" @@ -93,6 +97,7 @@ export function Wquw151Frame387935729() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -113,6 +118,7 @@ export function Wquw151Frame387935729() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -128,6 +134,7 @@ export function Wquw151Frame387935729() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -138,6 +145,7 @@ export function Wquw151Frame387935729() { color="$primary" fontSize="17px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="29px" typography="bodySmSemibold" wordBreak="keep-all" @@ -148,7 +156,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 +181,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" @@ -182,6 +190,7 @@ export function Wquw151Frame387935729() { color="$textLight" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" overflow="hidden" textAlign="center" @@ -201,6 +210,7 @@ export function Wquw151Frame387935729() { color="$textLight" fontSize="14px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="22px" overflow="hidden" textOverflow="ellipsis" @@ -222,6 +232,7 @@ export function Wquw151Frame387935729() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" > @@ -231,6 +242,7 @@ export function Wquw151Frame387935729() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" overflow="hidden" textOverflow="ellipsis" @@ -252,7 +264,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" @@ -275,6 +287,7 @@ export function Wquw151Frame387935729() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -287,6 +300,7 @@ export function Wquw151Frame387935729() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -301,6 +315,7 @@ export function Wquw151Frame387935729() { flex="1" fontSize="22px" fontWeight="700" + letterSpacing="-0.02em" lineHeight="31px" overflow="hidden" textOverflow="ellipsis" @@ -324,6 +339,7 @@ export function Wquw151Frame387935729() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -336,6 +352,7 @@ export function Wquw151Frame387935729() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -349,6 +366,7 @@ export function Wquw151Frame387935729() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" overflow="hidden" textOverflow="ellipsis" @@ -373,6 +391,7 @@ export function Wquw151Frame387935729() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -385,6 +404,7 @@ export function Wquw151Frame387935729() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -398,6 +418,7 @@ export function Wquw151Frame387935729() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" overflow="hidden" textOverflow="ellipsis" @@ -430,6 +451,7 @@ export function Wquw151Frame387935729() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -442,6 +464,7 @@ export function Wquw151Frame387935729() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -455,6 +478,7 @@ export function Wquw151Frame387935729() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" overflow="hidden" textOverflow="ellipsis" @@ -479,6 +503,7 @@ export function Wquw151Frame387935729() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -491,6 +516,7 @@ export function Wquw151Frame387935729() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -504,6 +530,7 @@ export function Wquw151Frame387935729() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" overflow="hidden" textOverflow="ellipsis" @@ -534,6 +561,7 @@ export function Wquw151Frame387935729() { color="$primary" fontSize="18px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="31px" typography="bodySemibold" wordBreak="keep-all" @@ -546,6 +574,7 @@ export function Wquw151Frame387935729() { color="#FFF" fontSize="18px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="31px" typography="bodySemibold" wordBreak="keep-all" @@ -582,6 +611,7 @@ export function Wquw151Frame387935729() { color="$text" fontSize="20px" fontWeight="700" + letterSpacing="-0.02em" lineHeight="28px" typography="h6" wordBreak="keep-all" @@ -592,6 +622,7 @@ export function Wquw151Frame387935729() { color="$textLight" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -614,7 +645,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" @@ -638,6 +669,7 @@ export function Wquw151Frame387935729() { color="$primary" fontSize="18px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="31px" typography="bodySemibold" wordBreak="keep-all" @@ -648,6 +680,7 @@ export function Wquw151Frame387935729() { color="$caption" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" typography="caption" wordBreak="keep-all" @@ -672,6 +705,7 @@ export function Wquw151Frame387935729() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" typography="body" wordBreak="keep-all" @@ -682,6 +716,7 @@ export function Wquw151Frame387935729() { color="$caption" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" typography="caption" wordBreak="keep-all" diff --git a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35887.snap b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35887.snap index 6fadaad5..87683d55 100644 --- a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35887.snap +++ b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35887.snap @@ -25,6 +25,7 @@ export function Wquw151Frame387935887() { color="$text" fontSize="28px" fontWeight="700" + letterSpacing="-0.02em" lineHeight="39px" overflow="hidden" textAlign="center" @@ -42,6 +43,7 @@ export function Wquw151Frame387935887() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -52,6 +54,7 @@ export function Wquw151Frame387935887() { color="$primary" fontSize="17px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="29px" typography="bodySmSemibold" wordBreak="keep-all" @@ -71,6 +74,7 @@ export function Wquw151Frame387935887() { color="$textLight" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" overflow="hidden" textOverflow="ellipsis" @@ -93,6 +97,7 @@ export function Wquw151Frame387935887() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -113,6 +118,7 @@ export function Wquw151Frame387935887() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -128,6 +134,7 @@ export function Wquw151Frame387935887() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -138,6 +145,7 @@ export function Wquw151Frame387935887() { color="$primary" fontSize="17px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="29px" typography="bodySmSemibold" wordBreak="keep-all" @@ -148,7 +156,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 +181,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" @@ -182,6 +190,7 @@ export function Wquw151Frame387935887() { color="$textLight" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" overflow="hidden" textAlign="center" @@ -201,6 +210,7 @@ export function Wquw151Frame387935887() { color="$textLight" fontSize="14px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="22px" overflow="hidden" textOverflow="ellipsis" @@ -222,6 +232,7 @@ export function Wquw151Frame387935887() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" > @@ -231,6 +242,7 @@ export function Wquw151Frame387935887() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" overflow="hidden" textOverflow="ellipsis" @@ -252,7 +264,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" @@ -275,6 +287,7 @@ export function Wquw151Frame387935887() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -287,6 +300,7 @@ export function Wquw151Frame387935887() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -301,6 +315,7 @@ export function Wquw151Frame387935887() { flex="1" fontSize="22px" fontWeight="700" + letterSpacing="-0.02em" lineHeight="31px" overflow="hidden" textOverflow="ellipsis" @@ -324,6 +339,7 @@ export function Wquw151Frame387935887() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -336,6 +352,7 @@ export function Wquw151Frame387935887() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -349,6 +366,7 @@ export function Wquw151Frame387935887() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" overflow="hidden" textOverflow="ellipsis" @@ -373,6 +391,7 @@ export function Wquw151Frame387935887() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -385,6 +404,7 @@ export function Wquw151Frame387935887() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -398,6 +418,7 @@ export function Wquw151Frame387935887() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" overflow="hidden" textOverflow="ellipsis" @@ -430,6 +451,7 @@ export function Wquw151Frame387935887() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -442,6 +464,7 @@ export function Wquw151Frame387935887() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -455,6 +478,7 @@ export function Wquw151Frame387935887() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" overflow="hidden" textOverflow="ellipsis" @@ -479,6 +503,7 @@ export function Wquw151Frame387935887() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -491,6 +516,7 @@ export function Wquw151Frame387935887() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -504,6 +530,7 @@ export function Wquw151Frame387935887() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" overflow="hidden" textOverflow="ellipsis" @@ -534,6 +561,7 @@ export function Wquw151Frame387935887() { color="$primary" fontSize="18px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="31px" typography="bodySemibold" wordBreak="keep-all" @@ -546,6 +574,7 @@ export function Wquw151Frame387935887() { color="#FFF" fontSize="18px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="31px" typography="bodySemibold" wordBreak="keep-all" @@ -582,6 +611,7 @@ export function Wquw151Frame387935887() { color="$text" fontSize="20px" fontWeight="700" + letterSpacing="-0.02em" lineHeight="28px" typography="h6" wordBreak="keep-all" @@ -592,6 +622,7 @@ export function Wquw151Frame387935887() { color="$textLight" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" typography="caption" wordBreak="keep-all" @@ -611,7 +642,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" @@ -635,6 +666,7 @@ export function Wquw151Frame387935887() { color="$primary" fontSize="18px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="31px" typography="bodySemibold" wordBreak="keep-all" @@ -645,6 +677,7 @@ export function Wquw151Frame387935887() { color="$caption" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" overflow="hidden" textOverflow="ellipsis" @@ -671,6 +704,7 @@ export function Wquw151Frame387935887() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" typography="body" wordBreak="keep-all" @@ -681,6 +715,7 @@ export function Wquw151Frame387935887() { color="$caption" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" overflow="hidden" textOverflow="ellipsis" @@ -698,6 +733,7 @@ export function Wquw151Frame387935887() { color="$text" fontSize="20px" fontWeight="700" + letterSpacing="-0.02em" lineHeight="28px" typography="h6" wordBreak="keep-all" @@ -708,6 +744,7 @@ export function Wquw151Frame387935887() { color="$textLight" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -735,6 +772,7 @@ export function Wquw151Frame387935887() { color="$primary" fontSize="18px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="31px" typography="bodySemibold" wordBreak="keep-all" @@ -745,6 +783,7 @@ export function Wquw151Frame387935887() { color="$caption" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" typography="caption" wordBreak="keep-all" @@ -769,6 +808,7 @@ export function Wquw151Frame387935887() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" typography="body" wordBreak="keep-all" @@ -779,6 +819,7 @@ export function Wquw151Frame387935887() { color="$caption" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" typography="caption" wordBreak="keep-all" @@ -815,6 +856,7 @@ export function Wquw151Frame387935887() { color="$text" fontSize="20px" fontWeight="700" + letterSpacing="-0.02em" lineHeight="28px" typography="h6" wordBreak="keep-all" @@ -826,6 +868,7 @@ export function Wquw151Frame387935887() { color="$text" fontSize="17px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="29px" overflow="hidden" textAlign="center" @@ -845,6 +888,7 @@ export function Wquw151Frame387935887() { color="$textLight" fontSize="17px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="29px" typography="bodySmSemibold" wordBreak="keep-all" @@ -863,6 +907,7 @@ export function Wquw151Frame387935887() { color="$primary" fontSize="17px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="29px" typography="bodySmSemibold" wordBreak="keep-all" diff --git a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35973.snap b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35973.snap index f90c41fd..ca4c98d8 100644 --- a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35973.snap +++ b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35973.snap @@ -25,6 +25,7 @@ export function Wquw151Frame387935973() { color="$text" fontSize="28px" fontWeight="700" + letterSpacing="-0.02em" lineHeight="39px" overflow="hidden" textAlign="center" @@ -42,6 +43,7 @@ export function Wquw151Frame387935973() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -52,6 +54,7 @@ export function Wquw151Frame387935973() { color="$primary" fontSize="17px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="29px" typography="bodySmSemibold" wordBreak="keep-all" @@ -71,6 +74,7 @@ export function Wquw151Frame387935973() { color="$textLight" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" overflow="hidden" textOverflow="ellipsis" @@ -93,6 +97,7 @@ export function Wquw151Frame387935973() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -113,6 +118,7 @@ export function Wquw151Frame387935973() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -128,6 +134,7 @@ export function Wquw151Frame387935973() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -138,6 +145,7 @@ export function Wquw151Frame387935973() { color="$primary" fontSize="17px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="29px" typography="bodySmSemibold" wordBreak="keep-all" @@ -148,7 +156,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 +181,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" @@ -182,6 +190,7 @@ export function Wquw151Frame387935973() { color="$textLight" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" overflow="hidden" textAlign="center" @@ -201,6 +210,7 @@ export function Wquw151Frame387935973() { color="$textLight" fontSize="14px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="22px" overflow="hidden" textOverflow="ellipsis" @@ -222,6 +232,7 @@ export function Wquw151Frame387935973() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" > @@ -231,6 +242,7 @@ export function Wquw151Frame387935973() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" overflow="hidden" textOverflow="ellipsis" @@ -252,7 +264,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" @@ -275,6 +287,7 @@ export function Wquw151Frame387935973() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -287,6 +300,7 @@ export function Wquw151Frame387935973() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -301,6 +315,7 @@ export function Wquw151Frame387935973() { flex="1" fontSize="22px" fontWeight="700" + letterSpacing="-0.02em" lineHeight="31px" overflow="hidden" textOverflow="ellipsis" @@ -324,6 +339,7 @@ export function Wquw151Frame387935973() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -336,6 +352,7 @@ export function Wquw151Frame387935973() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -349,6 +366,7 @@ export function Wquw151Frame387935973() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" overflow="hidden" textOverflow="ellipsis" @@ -373,6 +391,7 @@ export function Wquw151Frame387935973() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -385,6 +404,7 @@ export function Wquw151Frame387935973() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -398,6 +418,7 @@ export function Wquw151Frame387935973() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" overflow="hidden" textOverflow="ellipsis" @@ -430,6 +451,7 @@ export function Wquw151Frame387935973() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -442,6 +464,7 @@ export function Wquw151Frame387935973() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -455,6 +478,7 @@ export function Wquw151Frame387935973() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" overflow="hidden" textOverflow="ellipsis" @@ -479,6 +503,7 @@ export function Wquw151Frame387935973() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -491,6 +516,7 @@ export function Wquw151Frame387935973() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -504,6 +530,7 @@ export function Wquw151Frame387935973() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" overflow="hidden" textOverflow="ellipsis" @@ -534,6 +561,7 @@ export function Wquw151Frame387935973() { color="$primary" fontSize="18px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="31px" typography="bodySemibold" wordBreak="keep-all" @@ -546,6 +574,7 @@ export function Wquw151Frame387935973() { color="#FFF" fontSize="18px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="31px" typography="bodySemibold" wordBreak="keep-all" @@ -582,6 +611,7 @@ export function Wquw151Frame387935973() { color="$text" fontSize="20px" fontWeight="700" + letterSpacing="-0.02em" lineHeight="28px" typography="h6" wordBreak="keep-all" @@ -592,6 +622,7 @@ export function Wquw151Frame387935973() { color="$textLight" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" typography="caption" wordBreak="keep-all" @@ -611,7 +642,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" @@ -635,6 +666,7 @@ export function Wquw151Frame387935973() { color="$primary" fontSize="18px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="31px" typography="bodySemibold" wordBreak="keep-all" @@ -645,6 +677,7 @@ export function Wquw151Frame387935973() { color="$caption" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" overflow="hidden" textOverflow="ellipsis" @@ -671,6 +704,7 @@ export function Wquw151Frame387935973() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" typography="body" wordBreak="keep-all" @@ -681,6 +715,7 @@ export function Wquw151Frame387935973() { color="$caption" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" overflow="hidden" textOverflow="ellipsis" @@ -698,6 +733,7 @@ export function Wquw151Frame387935973() { color="$text" fontSize="20px" fontWeight="700" + letterSpacing="-0.02em" lineHeight="28px" typography="h6" wordBreak="keep-all" @@ -708,6 +744,7 @@ export function Wquw151Frame387935973() { color="$textLight" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXs" wordBreak="keep-all" @@ -735,6 +772,7 @@ export function Wquw151Frame387935973() { color="$primary" fontSize="18px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="31px" typography="bodySemibold" wordBreak="keep-all" @@ -745,6 +783,7 @@ export function Wquw151Frame387935973() { color="$caption" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" typography="caption" wordBreak="keep-all" @@ -769,6 +808,7 @@ export function Wquw151Frame387935973() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" typography="body" wordBreak="keep-all" @@ -779,6 +819,7 @@ export function Wquw151Frame387935973() { color="$caption" fontSize="15px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="24px" typography="caption" wordBreak="keep-all" @@ -816,6 +857,7 @@ export function Wquw151Frame387935973() { color="$text" fontSize="20px" fontWeight="700" + letterSpacing="-0.02em" lineHeight="28px" typography="h6" wordBreak="keep-all" @@ -827,6 +869,7 @@ export function Wquw151Frame387935973() { color="$text" fontSize="17px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="29px" overflow="hidden" textAlign="center" @@ -850,6 +893,7 @@ export function Wquw151Frame387935973() { color="$textLight" fontSize="17px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="29px" typography="bodySmSemibold" wordBreak="keep-all" @@ -868,6 +912,7 @@ export function Wquw151Frame387935973() { color="$primary" fontSize="17px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="29px" typography="bodySmSemibold" wordBreak="keep-all" diff --git a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_36059.snap b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_36059.snap index abd026e0..18c58e17 100644 --- a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_36059.snap +++ b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_36059.snap @@ -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" @@ -46,6 +46,7 @@ export function Wquw151Frame387936059() { color="$textLight" fontSize="15px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="24px" typography="captionSemibold" wordBreak="keep-all" @@ -66,6 +67,7 @@ export function Wquw151Frame387936059() { color="$text" fontSize="24px" fontWeight="700" + letterSpacing="-0.02em" lineHeight="34px" overflow="hidden" textOverflow="ellipsis" @@ -87,7 +89,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" @@ -96,6 +98,7 @@ export function Wquw151Frame387936059() { color="$secondary" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -108,6 +111,7 @@ export function Wquw151Frame387936059() { flex="1" fontSize="18px" fontWeight="500" + letterSpacing="-0.02em" lineHeight="31px" overflow="hidden" textOverflow="ellipsis" @@ -123,45 +127,49 @@ export function Wquw151Frame387936059() { color="$textLight" fontSize="15px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="24px" typography="captionSemibold" wordBreak="keep-all" > 직접 입력
- +
녹음하기 - +
작성 완료 - +
직접 입력 - +
녹음하기 - +
작성 완료 - +
@@ -146,6 +152,7 @@ export function Wquw151Frame387936144() { color="$text" fontSize="16px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="26px" overflow="hidden" textOverflow="ellipsis" @@ -167,7 +174,7 @@ export function Wquw151Frame387936144() { aspectRatio="1" bg="$text" boxSize="20px" - maskImage="url(/icons/Icons.svg)" + maskImage="url(/icons/icons.svg)" maskPos="center" maskRepeat="no-repeat" maskSize="contain" @@ -190,6 +197,7 @@ export function Wquw151Frame387936144() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -202,6 +210,7 @@ export function Wquw151Frame387936144() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -216,6 +225,7 @@ export function Wquw151Frame387936144() { flex="1" fontSize="22px" fontWeight="700" + letterSpacing="-0.02em" lineHeight="31px" overflow="hidden" textOverflow="ellipsis" @@ -239,6 +249,7 @@ export function Wquw151Frame387936144() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -251,6 +262,7 @@ export function Wquw151Frame387936144() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -264,6 +276,7 @@ export function Wquw151Frame387936144() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" overflow="hidden" textOverflow="ellipsis" @@ -288,6 +301,7 @@ export function Wquw151Frame387936144() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -300,6 +314,7 @@ export function Wquw151Frame387936144() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -313,6 +328,7 @@ export function Wquw151Frame387936144() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" overflow="hidden" textOverflow="ellipsis" @@ -345,6 +361,7 @@ export function Wquw151Frame387936144() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -357,6 +374,7 @@ export function Wquw151Frame387936144() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -370,6 +388,7 @@ export function Wquw151Frame387936144() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" overflow="hidden" textOverflow="ellipsis" @@ -394,6 +413,7 @@ export function Wquw151Frame387936144() { color="$text" fontSize="16px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="26px" typography="bodyXsSemibold" wordBreak="keep-all" @@ -406,6 +426,7 @@ export function Wquw151Frame387936144() { color="$textLight" fontSize="14px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="22px" typography="captionSmSemibold" wordBreak="keep-all" @@ -419,6 +440,7 @@ export function Wquw151Frame387936144() { color="$text" fontSize="18px" fontWeight="400" + letterSpacing="-0.01em" lineHeight="32px" overflow="hidden" textOverflow="ellipsis" @@ -455,6 +477,7 @@ export function Wquw151Frame387936144() { color="$text" fontSize="18px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="31px" typography="bodySemibold" wordBreak="keep-all" @@ -467,6 +490,7 @@ export function Wquw151Frame387936144() { color="#FFF" fontSize="18px" fontWeight="600" + letterSpacing="-0.01em" lineHeight="31px" typography="bodySemibold" wordBreak="keep-all" diff --git a/crates/devup-mcp-devup-ui/tests/theme.rs b/crates/devup-mcp-devup-ui/tests/theme.rs index 9de192dc..92216874 100644 --- a/crates/devup-mcp-devup-ui/tests/theme.rs +++ b/crates/devup-mcp-devup-ui/tests/theme.rs @@ -104,6 +104,10 @@ fn maps_variables_modes_aliases_and_styles_to_devup_json() { " \"darkMode\": {\n", " \"elevationCard\": \"0 4px 12px 0 #0000001a\"\n", " }\n", + " },\n", + " \"colorScheme\": {\n", + " \"light\": \"light\",\n", + " \"darkMode\": \"dark\"\n", " }\n", " }\n", "}\n" diff --git a/crates/devup-mcp-devup-ui/tests/theme_modes.rs b/crates/devup-mcp-devup-ui/tests/theme_modes.rs new file mode 100644 index 00000000..b846403d --- /dev/null +++ b/crates/devup-mcp-devup-ui/tests/theme_modes.rs @@ -0,0 +1,129 @@ +use devup_mcp_devup_ui::theme::{ThemeScope, VariableSnapshot, generate_devup_json}; +use serde_json::{Value, json}; + +fn incident() -> VariableSnapshot { + serde_json::from_value(json!({ + "collections": [ + {"id":"primitives","name":"Primitives","defaultModeId":"value","modes":[{"modeId":"value","name":"value"}]}, + {"id":"leftover","name":"Leftover","defaultModeId":"one","modes":[{"modeId":"one","name":"Mode 1"}]}, + {"id":"site","name":"Site","defaultModeId":"light","modes":[{"modeId":"light","name":"Light"},{"modeId":"dark","name":"Dark"}]}, + {"id":"sds","name":"Simple Design System","remote":true,"defaultModeId":"sl","modes":[{"modeId":"sl","name":"SDS Light"},{"modeId":"sd","name":"SDS Dark"}]}, + {"id":"empty","name":"Empty","defaultModeId":"empty","modes":[{"modeId":"empty","name":"Empty"}]} + ], + "variables": [ + {"id":"primitive","name":"white","resolvedType":"COLOR","variableCollectionId":"primitives","valuesByMode":{"value":{"r":1,"g":1,"b":1}}}, + {"id":"leftover","name":"accent","resolvedType":"COLOR","variableCollectionId":"leftover","valuesByMode":{"one":{"r":0,"g":0,"b":1}}}, + {"id":"bg","name":"background","resolvedType":"COLOR","variableCollectionId":"site","valuesByMode":{"light":{"type":"VARIABLE_ALIAS","id":"primitive"},"dark":{"r":0,"g":0,"b":0}}} + ], + "usedRemoteVariables":[ + {"id":"sds-icon","name":"sdsIcon","resolvedType":"COLOR","variableCollectionId":"sds","valuesByMode":{"sl":{"r":1,"g":1,"b":1},"sd":{"r":0,"g":0,"b":0}}} + ], + "usedVariableIds":["bg"],"styles":[],"localComplete":true,"usedRemoteComplete":true + })).unwrap() +} + +#[test] +fn incident_exports_only_site_themes_with_drop_reasons() { + let output = generate_devup_json(&incident(), ThemeScope::File).unwrap(); + let theme: Value = serde_json::from_str(&output.json).unwrap(); + assert_eq!( + theme["theme"]["colors"] + .as_object() + .unwrap() + .keys() + .collect::>(), + ["light", "dark"] + ); + assert_eq!(theme["theme"]["colors"]["light"]["accent"], "#0000ff"); + assert_eq!(theme["theme"]["colors"]["light"]["white"], "#ffffff"); + let output = serde_json::to_value(output).unwrap(); + let dropped = output["themeModesDropped"].as_array().unwrap(); + assert!( + dropped.iter().any(|mode| mode["collectionId"] == "sds" + && mode["reason"] == "unreferenced-remote-collection") + ); + assert!( + dropped + .iter() + .any(|mode| mode["collectionId"] == "empty" + && mode["reason"] == "no-defined-color-values") + ); + assert!( + dropped + .iter() + .any(|mode| mode["modeName"] == "Mode 1" && mode["reason"] == "single-default-mode") + ); +} + +#[test] +fn referenced_library_modes_merge_by_meaning() { + let mut snapshot = incident(); + snapshot.used_variable_ids.push("sds-icon".into()); + let output = generate_devup_json(&snapshot, ThemeScope::File).unwrap(); + let theme: Value = serde_json::from_str(&output.json).unwrap(); + assert_eq!( + theme["theme"]["colors"] + .as_object() + .unwrap() + .keys() + .collect::>(), + ["light", "dark"] + ); + assert_eq!(theme["theme"]["colors"]["dark"]["sdsIcon"], "#000000"); +} + +#[test] +fn used_only_drops_unused_modes_and_missing_only_preserves_existing_values() { + use devup_mcp_devup_ui::theme::{ + TokenUsage, generate_used_devup_json, missing_token_diff, parse_project_theme, + }; + let mut usage = TokenUsage::default(); + usage.add_tsx(r#""#).unwrap(); + let output = generate_used_devup_json(&incident(), &usage).unwrap(); + let theme: Value = serde_json::from_str(&output.json).unwrap(); + assert_eq!( + theme["theme"]["colors"], + json!({"light":{"background":"#ffffff"},"dark":{"background":"#000000"}}) + ); + assert_eq!( + theme["theme"]["colorScheme"], + json!({"light":"light","dark":"dark"}) + ); + let existing = + parse_project_theme(r##"{"theme":{"colors":{"light":{"background":"#123456"}}}}"##) + .unwrap(); + let diff = missing_token_diff(&output.json, &usage, &existing).unwrap(); + assert!( + diff["additions"]["theme"]["colors"]["light"] + .as_object() + .unwrap() + .is_empty() + ); + assert_eq!(existing.colors["light"]["background"], "#123456"); +} + +#[test] +fn incompatible_same_meaning_tokens_report_conflict() { + let mut snapshot = incident(); + snapshot.used_variable_ids.push("sds-icon".into()); + snapshot.used_remote_variables[0].name = "background".into(); + snapshot.used_remote_variables[0] + .values_by_mode + .insert("sd".into(), json!({"r":1,"g":0,"b":0})); + let output = generate_devup_json(&snapshot, ThemeScope::File).unwrap(); + assert!( + output + .conflicts + .iter() + .any(|conflict| conflict.mode == "dark" && conflict.token == "background") + ); +} + +#[test] +fn custom_dark_mode_has_explicit_color_scheme() { + let mut snapshot = incident(); + snapshot.collections[2].modes[1].name = "Dark Brand".into(); + let output = generate_devup_json(&snapshot, ThemeScope::File).unwrap(); + let theme: Value = serde_json::from_str(&output.json).unwrap(); + assert_eq!(theme["theme"]["colorScheme"]["darkBrand"], "dark"); +} diff --git a/crates/devup-mcp-devup-ui/tests/validation.rs b/crates/devup-mcp-devup-ui/tests/validation.rs index e48472b5..c5173176 100644 --- a/crates/devup-mcp-devup-ui/tests/validation.rs +++ b/crates/devup-mcp-devup-ui/tests/validation.rs @@ -33,3 +33,14 @@ fn syntax_rejects_invalid_tsx_without_echoing_source_text() { assert_eq!(error.details["errors"][0]["category"], "syntax"); } } + +#[test] +fn fatal_parse_errors_are_normal_validation_errors() { + let source = "export function Button() { return }"; + let error = std::panic::catch_unwind(|| validate_tsx(source)) + .expect("validation must not unwind") + .expect_err("a hyphenated object key requires quotes"); + assert_eq!(error.code, ErrorCode::DevupCodegenFailed); + assert_eq!(error.details["parserAborted"], true); + assert!(error.details.get("panicked").is_none()); +} diff --git a/crates/devup-mcp-devup-ui/tests/variant_nesting.rs b/crates/devup-mcp-devup-ui/tests/variant_nesting.rs index 37b98228..e210431f 100644 --- a/crates/devup-mcp-devup-ui/tests/variant_nesting.rs +++ b/crates/devup-mcp-devup-ui/tests/variant_nesting.rs @@ -123,7 +123,7 @@ fn a_combination_never_drawn_is_not_a_hole_but_a_refused_value_is() { "{tsx}" ); assert!( - tsx.contains("border={varient === 'white' && "), + tsx.contains("border={varient === \"white\" ? ") && tsx.contains(": undefined}"), "a value only one variant sets stays a condition: {tsx}" ); } 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/assets/asset_manifest.rs b/crates/devup-mcp-figma/src/assets/asset_manifest.rs index 4b1c2093..963e2a66 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)) { @@ -512,11 +517,22 @@ fn compute_asset_node(snapshot: &Snapshot, node: &RawNode, nested: bool) -> Opti 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); } } + if visible_children.len() >= 2 + && matches!(view.node_type(), "FRAME" | "INSTANCE" | "COMPONENT") + && matches!(view.string("layoutMode"), Some("HORIZONTAL" | "VERTICAL" | "GRID")) + { + return None; + } + visible_children .into_iter() .all(|child| compute_asset_node(snapshot, child, true) == Some(AssetNode::Svg)) @@ -922,7 +938,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 +966,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/bridge.rs b/crates/devup-mcp-figma/src/bridge.rs index fc63bd68..823ed026 100644 --- a/crates/devup-mcp-figma/src/bridge.rs +++ b/crates/devup-mcp-figma/src/bridge.rs @@ -19,5 +19,7 @@ // Source sections share this module to preserve privacy and test names. include!("bridge/transport_and_state.rs"); +include!("bridge/job_budgets.rs"); include!("bridge/stamp_file_key.rs"); +include!("bridge/fallback_reasons.rs"); include!("bridge/texts.rs"); diff --git a/crates/devup-mcp-figma/src/bridge/fallback_reasons.rs b/crates/devup-mcp-figma/src/bridge/fallback_reasons.rs new file mode 100644 index 00000000..fdddbea3 --- /dev/null +++ b/crates/devup-mcp-figma/src/bridge/fallback_reasons.rs @@ -0,0 +1,87 @@ +/// 플러그인이 답은 했지만 원격 경로가 더 낫게 답할 수 있는 경우와 그 이유. 원격이 +/// 어떤 이유로든 답하지 못하면 플러그인의 답을 그대로 쓴다 — 쓸 수 있는 답을 버리고 +/// 실패를 올리지 않는다. +/// +/// - Figma 가 화면에 보이지 않는 파일의 데이터를 가져오지 않아 읽기의 일부나 전부가 +/// "Unable to establish connection to Figma" 로 빠졌다(`figma_unreachable_hint`). +/// 원격 경로는 그 상태와 무관하다. +/// - 참조 PNG 에서 플러그인이 `get_screenshot` 과 같은 그림을 다 그리지 못했다. +fn remote_does_better(raw: &Value) -> Option<&'static str> { + if raw["_meta"][REFERENCE_PNG_APPROXIMATE_META_KEY].is_string() { + return Some("reference-png-approximate"); + } + texts(raw) + .any(|text| { + crate::figma_unreachable_hint(text).is_some() + && (raw["isError"] == true || reports_unreachable(text)) + }) + .then_some("file-out-of-sight") +} + +/// 플러그인 답의 JSON 에서 오류를 싣는 자리 — 값을 읽지 못한 `$error`, 필드 읽기 실패 +/// `fieldErrors`, 미해석 리소스의 `error`, 자산 export 의 `errorDetail` — 에 그 말이 있는지. +/// 디자인의 글(`characters`, 이름, 설명)에 같은 문장이 있어도 한도를 써서 다시 읽지 않는다. +fn reports_unreachable(text: &str) -> bool { + fn within(value: &Value, in_error: bool) -> bool { + match value { + Value::String(text) => in_error && crate::figma_unreachable_hint(text).is_some(), + Value::Array(items) => items.iter().any(|item| within(item, in_error)), + Value::Object(fields) => fields.iter().any(|(key, value)| { + let error_field = matches!( + key.as_str(), + "$error" | "error" | "errors" | "fieldErrors" | "errorDetail" | "lookupError" + ); + within(value, in_error || error_field) + }), + _ => false, + } + } + serde_json::from_str::(text).is_ok_and(|value| within(&value, false)) +} + +/// 플러그인이 답하지 못한 읽기를 원격으로 넘기려 했지만 정해진 시간 안에 한도 자리가 나지 +/// 않았다. 한도가 찬 것과 같으므로 그렇게 알려, 수집이 한도가 비기를 기다렸다 다시 하게 한다. +fn no_room_on_the_metered_path() -> DevupError { + DevupError::with_details( + ErrorCode::DevupFigmaRateLimited, + include_str!("../text/bridge-no_room_on_the_metered_path-41a0802f.md"), + true, + json!({ "source": "direct", "stage": "bridge-fallback" }), + ) +} + +/// 원격 경로라면 해낼 수 있는 플러그인의 거절과 그 이유: +/// +/// - 이 devup-mcp 보다 먼저 빌드돼 스크립트를 모른다 — 읽기는 시도되지 않았다. +/// - 참조 PNG 가 브리지 답 하나(8 MiB)를 넘는다 — 조각으로 내주기 전의 플러그인이다. +/// - 큰 값의 바이트가 읽기가 가리키는 해시와 다르다 — 그 값은 원격 경로의 렌더러가 +/// 만든 것이라(플러그인이 답하지 못해 원격이 읽은 자산의 이어 읽기), 거기서만 같은 +/// 바이트가 나온다. +fn remote_may_answer(raw: &Value) -> Option<&'static str> { + if raw["isError"] != true { + return None; + } + let text = raw["content"][0]["text"].as_str()?; + [ + ("DEVUP_BRIDGE_UNKNOWN_SCRIPT", "plugin-older-than-server"), + ("DEVUP_REFERENCE_PNG_TOO_LARGE", "plugin-answer-too-large"), + ("DEVUP_REFERENCE_PNG_BUSY", "plugin-busy"), + ("DEVUP_LARGE_VALUE_CHANGED", "value-from-direct-path"), + ] + .into_iter() + .find_map(|(code, reason)| text.contains(code).then_some(reason)) +} + +/// 원격의 답을 쓸 수 없는지. 그때는 플러그인의 답이 낫다. +/// +/// - 오류로 표시됐다. +/// - 한도에 걸렸다는 문장 하나다 — `isError` 없이 오기도 한다(`is_quota_refusal`). +/// - 공식 MCP 의 텍스트 상한(20 KB)에서 잘렸다. 플러그인의 크기로 묶은 읽기를 원격이 다시 +/// 읽으면 생기고, 그 답은 디코딩되지 않는다. +fn direct_unusable(raw: &Value) -> bool { + raw["isError"] == true + || texts(raw).any(|text| { + crate::is_quota_refusal(text, false) + || text.trim_end().ends_with("// truncated to 20kb") + }) +} diff --git a/crates/devup-mcp-figma/src/bridge/job_budgets.rs b/crates/devup-mcp-figma/src/bridge/job_budgets.rs new file mode 100644 index 00000000..4bc921b6 --- /dev/null +++ b/crates/devup-mcp-figma/src/bridge/job_budgets.rs @@ -0,0 +1,49 @@ +pub(super) fn job_timeout(script: &str, params: &Value) -> Duration { + if job_is_asset_read(script, params) { + let options = if script == "assets" { + ¶ms["asset"] + } else { + ¶ms["largeValue"] + }; + let millis = options["timeoutMs"] + .as_u64() + .filter(|v| (1000..=600000).contains(v)) + .unwrap_or(90000); + Duration::from_millis(millis) + Duration::from_secs(5) + } else { + JOB_TIMEOUT + } +} + +pub(super) fn job_is_asset_read(script: &str, params: &Value) -> bool { + script == "assets" + || (script == "largeValue" + && params["largeValue"]["field"] + .as_str() + .is_some_and(|field| field.starts_with("$export:"))) +} + +/// 한 번에 담을 수 있는 양. 로컬 소켓이라 공식 MCP 의 자름을 따를 이유가 없다. +/// +/// 기본값(봉투 19KB · 필드 4KB)은 공식 MCP 가 텍스트 결과를 20,480 바이트에서 +/// 자르기 때문에 생긴 것이다. 그 탓에 화면 하나가 30번 넘는 왕복으로 쪼개지고, +/// 창이 뒤로 가 있으면 왕복 하나가 60초까지 걸린다 — 실측한 값이다. +pub(crate) const BRIDGE_ENVELOPE_BYTES: usize = 8 * 1024 * 1024; +const BRIDGE_FIELD_BYTES: usize = 4 * 1024 * 1024; + +/// 스냅샷 읽기의 한도를 브리지 전송에 맞게 올린다. +/// +/// 값만 바꿀 뿐 무엇을 읽을지는 그대로다. 스크립트는 한 페이지에 다 담기면 +/// `complete` 로 표시하고, 큰 필드도 잘리지 않으므로 수집기가 그 필드를 따로 +/// 받으러 가지 않는다. 두 왕복이 한 왕복이 되는 것이 아니라 서른이 하나가 된다. +fn widen_budgets(params: &mut Value) { + let Some(object) = params.as_object_mut() else { + return; + }; + let Some(snapshot) = object.get_mut("snapshot").and_then(Value::as_object_mut) else { + return; + }; + snapshot.insert("maxEnvelopeBytes".to_owned(), BRIDGE_ENVELOPE_BYTES.into()); + snapshot.insert("maxPayloadBytes".to_owned(), BRIDGE_ENVELOPE_BYTES.into()); + snapshot.insert("maxFieldBytes".to_owned(), BRIDGE_FIELD_BYTES.into()); +} diff --git a/crates/devup-mcp-figma/src/bridge/stamp_file_key.rs b/crates/devup-mcp-figma/src/bridge/stamp_file_key.rs index 816f4c33..dfb61b22 100644 --- a/crates/devup-mcp-figma/src/bridge/stamp_file_key.rs +++ b/crates/devup-mcp-figma/src/bridge/stamp_file_key.rs @@ -281,6 +281,29 @@ fn a_plugin_origin(headers: &HeaderMap) -> bool { }) } +/// A plugin that can load scripts at run time (`liveScripts`) and speaks this +/// protocol but runs older scripts gets this build's scripts. It answers with a +/// new `hello` carrying the new hash, which matches and ends the exchange; a +/// plugin that fails to load them sends nothing, so nothing repeats. +fn offer_current_scripts(outbox: &mpsc::UnboundedSender, hello: &Value) { + if script_update_needed(hello) { + let _ = outbox.send( + json!({ + "kind": "scripts-update", + "scriptsHash": crate::PLUGIN_SCRIPTS_HASH, + "bundle": crate::PLUGIN_SCRIPTS_BUNDLE, + }) + .to_string(), + ); + } +} + +fn script_update_needed(hello: &Value) -> bool { + hello["liveScripts"] == true + && hello["protocol"].as_u64() == Some(u64::from(crate::PLUGIN_PROTOCOL)) + && hello["scriptsHash"].as_str() != Some(crate::PLUGIN_SCRIPTS_HASH) +} + /// 플러그인이 붙는 문. async fn plugin_socket( State(state): State, @@ -321,6 +344,7 @@ async fn handle_plugin(state: BridgeState, mut socket: WebSocket) { match value.get("kind").and_then(Value::as_str) { Some("hello") => { state.announce_server(&outbox); + offer_current_scripts(&outbox, &value); // 키가 없어도 등록한다. 건너뛰면 창은 "연결됨"이라고 하는데 // 어떤 읽기도 오지 않아 원인을 찾을 수 없다. 키 없는 연결을 // 어디까지 믿을지는 resolve 가 정한다. @@ -358,6 +382,8 @@ async fn handle_plugin(state: BridgeState, mut socket: WebSocket) { if let Some(plugin) = inner.plugins.get_mut(&id) { // 버전은 hello 에만 실려 온다. 맥락을 바꿔 끼워도 잃지 않는다. context.plugin_version = plugin.context.plugin_version.take(); + context.scripts_hash = plugin.context.scripts_hash.take(); + context.protocol = plugin.context.protocol; plugin.context = context; state.plugins_changed(&mut inner); } @@ -908,91 +934,3 @@ where !(is_bridge_only_key(call.file_key()) || self.preferred.can_serve(call).await) } } - -/// 플러그인이 답은 했지만 원격 경로가 더 낫게 답할 수 있는 경우와 그 이유. 원격이 -/// 어떤 이유로든 답하지 못하면 플러그인의 답을 그대로 쓴다 — 쓸 수 있는 답을 버리고 -/// 실패를 올리지 않는다. -/// -/// - Figma 가 화면에 보이지 않는 파일의 데이터를 가져오지 않아 읽기의 일부나 전부가 -/// "Unable to establish connection to Figma" 로 빠졌다(`figma_unreachable_hint`). -/// 원격 경로는 그 상태와 무관하다. -/// - 참조 PNG 에서 플러그인이 `get_screenshot` 과 같은 그림을 다 그리지 못했다. -fn remote_does_better(raw: &Value) -> Option<&'static str> { - if raw["_meta"][REFERENCE_PNG_APPROXIMATE_META_KEY].is_string() { - return Some("reference-png-approximate"); - } - texts(raw) - .any(|text| { - crate::figma_unreachable_hint(text).is_some() - && (raw["isError"] == true || reports_unreachable(text)) - }) - .then_some("file-out-of-sight") -} - -/// 플러그인 답의 JSON 에서 오류를 싣는 자리 — 값을 읽지 못한 `$error`, 필드 읽기 실패 -/// `fieldErrors`, 미해석 리소스의 `error`, 자산 export 의 `errorDetail` — 에 그 말이 있는지. -/// 디자인의 글(`characters`, 이름, 설명)에 같은 문장이 있어도 한도를 써서 다시 읽지 않는다. -fn reports_unreachable(text: &str) -> bool { - fn within(value: &Value, in_error: bool) -> bool { - match value { - Value::String(text) => in_error && crate::figma_unreachable_hint(text).is_some(), - Value::Array(items) => items.iter().any(|item| within(item, in_error)), - Value::Object(fields) => fields.iter().any(|(key, value)| { - let error_field = matches!( - key.as_str(), - "$error" | "error" | "errors" | "fieldErrors" | "errorDetail" | "lookupError" - ); - within(value, in_error || error_field) - }), - _ => false, - } - } - serde_json::from_str::(text).is_ok_and(|value| within(&value, false)) -} - -/// 플러그인이 답하지 못한 읽기를 원격으로 넘기려 했지만 정해진 시간 안에 한도 자리가 나지 -/// 않았다. 한도가 찬 것과 같으므로 그렇게 알려, 수집이 한도가 비기를 기다렸다 다시 하게 한다. -fn no_room_on_the_metered_path() -> DevupError { - DevupError::with_details( - ErrorCode::DevupFigmaRateLimited, - include_str!("../text/bridge-no_room_on_the_metered_path-41a0802f.md"), - true, - json!({ "source": "direct", "stage": "bridge-fallback" }), - ) -} - -/// 원격 경로라면 해낼 수 있는 플러그인의 거절과 그 이유: -/// -/// - 이 devup-mcp 보다 먼저 빌드돼 스크립트를 모른다 — 읽기는 시도되지 않았다. -/// - 참조 PNG 가 브리지 답 하나(8 MiB)를 넘는다 — 조각으로 내주기 전의 플러그인이다. -/// - 큰 값의 바이트가 읽기가 가리키는 해시와 다르다 — 그 값은 원격 경로의 렌더러가 -/// 만든 것이라(플러그인이 답하지 못해 원격이 읽은 자산의 이어 읽기), 거기서만 같은 -/// 바이트가 나온다. -fn remote_may_answer(raw: &Value) -> Option<&'static str> { - if raw["isError"] != true { - return None; - } - let text = raw["content"][0]["text"].as_str()?; - [ - ("DEVUP_BRIDGE_UNKNOWN_SCRIPT", "plugin-older-than-server"), - ("DEVUP_REFERENCE_PNG_TOO_LARGE", "plugin-answer-too-large"), - ("DEVUP_REFERENCE_PNG_BUSY", "plugin-busy"), - ("DEVUP_LARGE_VALUE_CHANGED", "value-from-direct-path"), - ] - .into_iter() - .find_map(|(code, reason)| text.contains(code).then_some(reason)) -} - -/// 원격의 답을 쓸 수 없는지. 그때는 플러그인의 답이 낫다. -/// -/// - 오류로 표시됐다. -/// - 한도에 걸렸다는 문장 하나다 — `isError` 없이 오기도 한다(`is_quota_refusal`). -/// - 공식 MCP 의 텍스트 상한(20 KB)에서 잘렸다. 플러그인의 크기로 묶은 읽기를 원격이 다시 -/// 읽으면 생기고, 그 답은 디코딩되지 않는다. -fn direct_unusable(raw: &Value) -> bool { - raw["isError"] == true - || texts(raw).any(|text| { - crate::is_quota_refusal(text, false) - || text.trim_end().ends_with("// truncated to 20kb") - }) -} \ No newline at end of file diff --git a/crates/devup-mcp-figma/src/bridge/transport_and_state.rs b/crates/devup-mcp-figma/src/bridge/transport_and_state.rs index df30e84e..5c98c3f6 100644 --- a/crates/devup-mcp-figma/src/bridge/transport_and_state.rs +++ b/crates/devup-mcp-figma/src/bridge/transport_and_state.rs @@ -118,9 +118,22 @@ pub struct PluginContext { /// 플러그인은 `None`. #[serde(default, skip_serializing_if = "Option::is_none")] pub plugin_version: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub scripts_hash: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub protocol: Option, } impl PluginContext { + pub fn plugin_build(&self) -> Value { + let unknown = self.scripts_hash.as_deref().is_none_or(str::is_empty) || self.protocol.is_none(); + let stale = self.scripts_hash.as_deref() != Some(crate::PLUGIN_SCRIPTS_HASH) + || self.protocol != Some(crate::PLUGIN_PROTOCOL); + json!({"pluginStale":stale,"state":if unknown {"stale-unknown"} else if stale {"stale"} else {"current"}, + "expected":crate::PLUGIN_SCRIPTS_HASH,"actual":self.scripts_hash, + "expectedProtocol":crate::PLUGIN_PROTOCOL,"actualProtocol":self.protocol}) + } + /// 정확히 하나가 선택돼 있으면 그 노드. pub fn single_selection(&self) -> Option<&NodeRef> { match self.selection.as_deref()? { @@ -158,6 +171,10 @@ pub struct BridgeServed { pub file_key: Option, pub file_name: Option, pub page_name: Option, + #[serde(default)] + pub scripts_hash: Option, + #[serde(default)] + pub protocol: Option, } impl BridgeServed { @@ -235,6 +252,8 @@ impl Connected { .current_page .as_ref() .map(|page| page.name.clone()), + scripts_hash: self.context.scripts_hash.clone(), + protocol: self.context.protocol, } } } @@ -930,53 +949,3 @@ impl BridgeState { self.plugins_changed(inner); } } - -pub(super) fn job_timeout(script: &str, params: &Value) -> Duration { - if job_is_asset_read(script, params) { - let options = if script == "assets" { - ¶ms["asset"] - } else { - ¶ms["largeValue"] - }; - let millis = options["timeoutMs"] - .as_u64() - .filter(|v| (1000..=600000).contains(v)) - .unwrap_or(90000); - Duration::from_millis(millis) + Duration::from_secs(5) - } else { - JOB_TIMEOUT - } -} - -pub(super) fn job_is_asset_read(script: &str, params: &Value) -> bool { - script == "assets" - || (script == "largeValue" - && params["largeValue"]["field"] - .as_str() - .is_some_and(|field| field.starts_with("$export:"))) -} - -/// 한 번에 담을 수 있는 양. 로컬 소켓이라 공식 MCP 의 자름을 따를 이유가 없다. -/// -/// 기본값(봉투 19KB · 필드 4KB)은 공식 MCP 가 텍스트 결과를 20,480 바이트에서 -/// 자르기 때문에 생긴 것이다. 그 탓에 화면 하나가 30번 넘는 왕복으로 쪼개지고, -/// 창이 뒤로 가 있으면 왕복 하나가 60초까지 걸린다 — 실측한 값이다. -pub(crate) const BRIDGE_ENVELOPE_BYTES: usize = 8 * 1024 * 1024; -const BRIDGE_FIELD_BYTES: usize = 4 * 1024 * 1024; - -/// 스냅샷 읽기의 한도를 브리지 전송에 맞게 올린다. -/// -/// 값만 바꿀 뿐 무엇을 읽을지는 그대로다. 스크립트는 한 페이지에 다 담기면 -/// `complete` 로 표시하고, 큰 필드도 잘리지 않으므로 수집기가 그 필드를 따로 -/// 받으러 가지 않는다. 두 왕복이 한 왕복이 되는 것이 아니라 서른이 하나가 된다. -fn widen_budgets(params: &mut Value) { - let Some(object) = params.as_object_mut() else { - return; - }; - let Some(snapshot) = object.get_mut("snapshot").and_then(Value::as_object_mut) else { - return; - }; - snapshot.insert("maxEnvelopeBytes".to_owned(), BRIDGE_ENVELOPE_BYTES.into()); - snapshot.insert("maxPayloadBytes".to_owned(), BRIDGE_ENVELOPE_BYTES.into()); - snapshot.insert("maxFieldBytes".to_owned(), BRIDGE_FIELD_BYTES.into()); -} \ No newline at end of file diff --git a/crates/devup-mcp-figma/src/collector/advance.rs b/crates/devup-mcp-figma/src/collector/advance.rs index 97a2b3c5..598aca54 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); } } @@ -374,7 +374,7 @@ impl CollectorSession { } if self.metadata.is_some() && self.queued.is_empty() { self.completed = true; - let snapshot_chunks = if self.request.metadata_only || self.request.variables_only { + let mut snapshot_chunks = if self.request.metadata_only || self.request.variables_only { vec![SnapshotChunk { file_key: self.request.target.file_key.clone(), version: self.source_version.clone(), @@ -391,6 +391,20 @@ impl CollectorSession { self.restore_section_visual_order(chunks)? }; self.finish_stats(&snapshot_chunks); + if let Some(served) = &self.stats.bridge { + let context = crate::PluginContext { scripts_hash: served.scripts_hash.clone(), protocol: served.protocol, ..Default::default() }; + let build = context.plugin_build(); + if build["pluginStale"] == true && let Some(chunk) = snapshot_chunks.first_mut() { + chunk.diagnostics.push(crate::Diagnostic { + code: "DEVUP_BRIDGE_PLUGIN_STALE".into(), + message: "The serving Devup Bridge plugin has stale or unknown scripts; load figma-bridge-setup, rebuild and re-run the plugin with computer use, then verify devup_figma_auth status.".into(), + severity: Some(crate::DiagnosticSeverity::Warning), + fidelity_impact: Some(crate::FidelityImpact::None), + details: Some(build), + ..Default::default() + }); + } + } return Ok(CollectorStep::Complete(Box::new(CollectedParts { target: self.request.target.clone(), scope: self.request.scope, @@ -407,4 +421,4 @@ impl CollectorSession { } Ok(CollectorStep::AwaitingResults) } -} \ No newline at end of file +} diff --git a/crates/devup-mcp-figma/src/collector/w5_tests.rs b/crates/devup-mcp-figma/src/collector/w5_tests.rs index 721d7e50..5a78ad1b 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 = @@ -235,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/lib.rs b/crates/devup-mcp-figma/src/lib.rs index 7a3f3bf5..461741b5 100644 --- a/crates/devup-mcp-figma/src/lib.rs +++ b/crates/devup-mcp-figma/src/lib.rs @@ -12,6 +12,8 @@ mod explore; mod large_values; mod oauth; mod payload; +mod plugin_scripts; +pub use plugin_scripts::{PLUGIN_PROTOCOL, PLUGIN_SCRIPTS_BUNDLE, PLUGIN_SCRIPTS_HASH}; mod resources; mod search; mod section; @@ -63,7 +65,9 @@ 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, 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/plugin_api_manifest.json b/crates/devup-mcp-figma/src/plugin_api_manifest.json index 1798f3eb..bda91500 100644 --- a/crates/devup-mcp-figma/src/plugin_api_manifest.json +++ b/crates/devup-mcp-figma/src/plugin_api_manifest.json @@ -5,9 +5,9 @@ "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", + "inferredAutoLayout", "isAsset", "isMask", "itemSpacing", "layoutAlign", "layoutGrow", "layoutMode", "layoutWrap", "layoutPositioning", "layoutSizingHorizontal", "layoutSizingVertical", "letterSpacing", "lineHeight", "maxHeight", - "maxLines", "maxWidth", "minHeight", "minWidth", "name", "opacity", "overflowDirection", + "maxLines", "maxWidth", "minHeight", "minWidth", "name", "opacity", "overflowDirection", "overrides", "paddingBottom", "paddingLeft", "paddingRight", "paddingTop", "primaryAxisAlignItems", "reactions", "rotation", "strokeAlign", "strokeBottomWeight", "strokeLeftWeight", "strokeRightWeight", "strokeStyleId", "strokeTopWeight", "strokeWeight", "strokes", "targetAspectRatio", "textAlignHorizontal", "textAlignVertical", diff --git a/crates/devup-mcp-figma/src/plugin_scripts.rs b/crates/devup-mcp-figma/src/plugin_scripts.rs new file mode 100644 index 00000000..74aa571a --- /dev/null +++ b/crates/devup-mcp-figma/src/plugin_scripts.rs @@ -0,0 +1,119 @@ +pub const PLUGIN_SCRIPTS_HASH: &str = + "7d82cf8c2104ac5bed3c88cc57352a95772d757da65e8e57e6322598e9bd8bf4"; +pub const PLUGIN_PROTOCOL: u32 = 1; + +/// The scripts of [`PLUGIN_SCRIPTS_HASH`] as one evaluable expression. The bridge +/// host sends it to an open plugin whose scripts are older (`scripts-update`). +pub const PLUGIN_SCRIPTS_BUNDLE: &str = include_str!("plugin_scripts_bundle.js"); + +#[cfg(test)] +mod tests { + use sha2::{Digest, Sha256}; + + #[test] + fn generated_plugin_identity_matches_shipped_sources() { + for (source, expected) in [ + ( + include_bytes!("scripts/asset_render_helpers.js").as_slice(), + "68473e139c9801808a056972c1e647ee5072d670440f6b5ea27866c67ed3ca66", + ), + ( + include_bytes!("scripts/assets.js").as_slice(), + "e5678e5c3df57ee803c633a13d4d827d50c7905edd545734cbdbf9a458bee878", + ), + ( + include_bytes!("scripts/bridge_context.js").as_slice(), + "626afebb51fa13f0861491cff3a6a1715a13051635ff383acdfe3944a7a3d722", + ), + ( + include_bytes!("scripts/component_identity_helpers.js").as_slice(), + "942af2370e69b55aaaee5fa709decea9e209057eeb25a17d03447a04bb35aff8", + ), + ( + include_bytes!("scripts/explore.js").as_slice(), + "ef323ef89972be4936e3ee5f2a559b04385d9ef5cb324c9928e9d5f4ff61f773", + ), + ( + include_bytes!("scripts/fast_snapshot.js").as_slice(), + "29bd5c5c18ad62b50163065467b3c90d2034db8a8fc3cee78e93b1130d7c1fce", + ), + ( + include_bytes!("scripts/fast_theme.js").as_slice(), + "4773dc0af3b7bf8d5e452efab3ea424fd84676043f5203708ed568c88d6cc42c", + ), + ( + include_bytes!("scripts/large_value.js").as_slice(), + "1f590feaed7244a19b291795194ec6dc1887961f4932d24c5ecd3a8f404d8282", + ), + ( + include_bytes!("scripts/large_value_helpers.js").as_slice(), + "adea569bb0b23f119c56aac675ebf0c2585583781cd7f4a1aecc09cdd15bff69", + ), + ( + include_bytes!("scripts/measure_nodes.js").as_slice(), + "99a79b6595b831085685cf87a4c6fc9ea6e85dfc3945f159775c7f70eac73551", + ), + ( + include_bytes!("scripts/metadata.js").as_slice(), + "d51d69c92904df6f67c678ff07fde0001f9151a1bb01be2efc588a5f8c68a7c0", + ), + ( + include_bytes!("scripts/page_catalog.js").as_slice(), + "9735441c1749edf73811fd5c73f76ec6ad157224f62f79d0ff7592088a80569c", + ), + ( + include_bytes!("scripts/reference_png.js").as_slice(), + "3b11a9179bc5b328880a8d5076605875e5c75e4631f6e0413c2f6dae24de48d7", + ), + ( + include_bytes!("scripts/reference_png_chunk.js").as_slice(), + "278ccbe14638e3028cfd75e285e94c1f7dd904aedd26d7574fecdc874d7f4f71", + ), + ( + include_bytes!("scripts/search.js").as_slice(), + "03eb7f622cf4877d59106d6cde65844b6013a464c3f2c499493dde6ed7b6a7a4", + ), + ( + include_bytes!("scripts/section_index.js").as_slice(), + "4e3e0749854ca2e73cd7dab635cc31c0a1f2449027b6800de954fec078d0d161", + ), + ( + include_bytes!("scripts/snapshot.js").as_slice(), + "74b582ca455a1a6be69fbe708f64fea9b2377dcc39ec65dc3428a17769be6ad3", + ), + ( + include_bytes!("scripts/used_resources.js").as_slice(), + "04fd9fb4c7e9cd82fe1f0ca88f2f47bc997e5a9ba2dc3fe157b978b591f95ae6", + ), + ( + include_bytes!("scripts/variable_catalog.js").as_slice(), + "367bc0f317bd988d694d8000f7405a8ec216be1b38e45604d7bdc6a01ef46972", + ), + ( + include_bytes!("scripts/variables.js").as_slice(), + "b78f48e63d2d60dc9f43739637bb90b6c77e7a266a2c07379d981d5d7a045afb", + ), + ( + include_bytes!("plugin_api_manifest.json").as_slice(), + "431e7c9eb6a244d486b4bebbdc60ce356fd202ea5b2661f03093b43f774c9ae6", + ), + ( + include_bytes!("text_segment_manifest.json").as_slice(), + "ae43d7546b12f312303e5e984cb48268aa2ea1c4ba92e529c028bc04d74dd888", + ), + ( + include_bytes!("plugin_scripts_bundle.js").as_slice(), + "70078f59c3e0eb4636c542b097e1bef21fcdb825b83e6f971672f3cef83c6533", + ), + ] { + let actual = Sha256::digest(source) + .iter() + .map(|byte| format!("{byte:02x}")) + .collect::(); + assert_eq!( + actual, expected, + "Rebuild plugin after changing script sources" + ); + } + } +} diff --git a/crates/devup-mcp-figma/src/plugin_scripts_bundle.js b/crates/devup-mcp-figma/src/plugin_scripts_bundle.js new file mode 100644 index 00000000..1e3e6b35 --- /dev/null +++ b/crates/devup-mcp-figma/src/plugin_scripts_bundle.js @@ -0,0 +1,4212 @@ +(function () { +"use strict"; +/** source: assets.js */ +async function assets(p) { +// Figma's own MCP runs scripts with hidden layers inside instances left out +// of `children`, `findAll` and `getNodeByIdAsync`; a plugin sees them. Set +// here so the direct path and the Devup Bridge read the same document. +figma.skipInvisibleInstanceChildren = false; +const options = p.asset; +const DEVUP_MAX_LARGE_VALUE_BYTES = 16 * 1024 * 1024; +// A fragment's text is the chunk in base64, four thirds of it, inside a +// wrapper of some three hundred bytes, and the Figma MCP cuts a text result +// at 20,480 UTF-8 bytes (measured; see fast_snapshot.js). Twelve KiB comes +// back as 16.7 KB of text, with room to spare; eight KiB was 11.2 KB, and +// took a third more calls for the same value. +const DEVUP_LARGE_VALUE_CHUNK_BYTES = 12288; + +function devupUtf8Encode(value) { + const bytes = []; + for (let index = 0; index < value.length; index += 1) { + let codePoint = value.charCodeAt(index); + if (codePoint >= 0xd800 && codePoint <= 0xdbff) { + const next = index + 1 < value.length ? value.charCodeAt(index + 1) : 0; + if (next >= 0xdc00 && next <= 0xdfff) { + codePoint = 0x10000 + ((codePoint - 0xd800) << 10) + (next - 0xdc00); + index += 1; + } else codePoint = 0xfffd; + } else if (codePoint >= 0xdc00 && codePoint <= 0xdfff) codePoint = 0xfffd; + if (codePoint < 0x80) bytes.push(codePoint); + else if (codePoint < 0x800) { + bytes.push(0xc0 | (codePoint >> 6), 0x80 | (codePoint & 0x3f)); + } else if (codePoint < 0x10000) { + bytes.push( + 0xe0 | (codePoint >> 12), + 0x80 | ((codePoint >> 6) & 0x3f), + 0x80 | (codePoint & 0x3f), + ); + } else { + bytes.push( + 0xf0 | (codePoint >> 18), + 0x80 | ((codePoint >> 12) & 0x3f), + 0x80 | ((codePoint >> 6) & 0x3f), + 0x80 | (codePoint & 0x3f), + ); + } + } + return new Uint8Array(bytes); +} + +function devupSha256(bytes) { + const constants = [ + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, + 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, + 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, + 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, + 0x06ca6351, 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, + ]; + const paddedLength = Math.ceil((bytes.length + 9) / 64) * 64; + const padded = new Uint8Array(paddedLength); + padded.set(bytes); + padded[bytes.length] = 0x80; + const bitLength = bytes.length * 8; + for (let index = 0; index < 8; index += 1) { + padded[paddedLength - 1 - index] = Math.floor(bitLength / 2 ** (index * 8)) & 0xff; + } + const hash = [ + 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, + 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19, + ]; + const rotate = (value, bits) => (value >>> bits) | (value << (32 - bits)); + for (let offset = 0; offset < padded.length; offset += 64) { + const words = new Uint32Array(64); + for (let index = 0; index < 16; index += 1) { + const start = offset + index * 4; + words[index] = + (padded[start] << 24) | + (padded[start + 1] << 16) | + (padded[start + 2] << 8) | + padded[start + 3]; + } + for (let index = 16; index < 64; index += 1) { + const s0 = rotate(words[index - 15], 7) ^ rotate(words[index - 15], 18) ^ (words[index - 15] >>> 3); + const s1 = rotate(words[index - 2], 17) ^ rotate(words[index - 2], 19) ^ (words[index - 2] >>> 10); + words[index] = (words[index - 16] + s0 + words[index - 7] + s1) >>> 0; + } + let [a, b, c, d, e, f, g, h] = hash; + for (let index = 0; index < 64; index += 1) { + const s1 = rotate(e, 6) ^ rotate(e, 11) ^ rotate(e, 25); + const choice = (e & f) ^ (~e & g); + const first = (h + s1 + choice + constants[index] + words[index]) >>> 0; + const s0 = rotate(a, 2) ^ rotate(a, 13) ^ rotate(a, 22); + const majority = (a & b) ^ (a & c) ^ (b & c); + const second = (s0 + majority) >>> 0; + h = g; g = f; f = e; e = (d + first) >>> 0; + d = c; c = b; b = a; a = (first + second) >>> 0; + } + for (const [index, value] of [a, b, c, d, e, f, g, h].entries()) { + hash[index] = (hash[index] + value) >>> 0; + } + } + return hash.map((value) => value.toString(16).padStart(8, "0")).join(""); +} + +function devupLargeValueDescriptor(nodeId, field, serialized) { + const bytes = devupUtf8Encode(JSON.stringify(serialized)); + if (bytes.length > DEVUP_MAX_LARGE_VALUE_BYTES) { + return { $truncated: "max-large-value-bytes", byteLength: bytes.length }; + } + return { + $largeValue: { + nodeId, + field, + byteLength: bytes.length, + sha256: devupSha256(bytes), + cursor: { nextOffset: 0, maxChunkBytes: DEVUP_LARGE_VALUE_CHUNK_BYTES }, + }, + }; +} +; +// Read-only recovery. Never clone, detach, crop a parent or manufacture a +// substitute: the returned geometry must belong to this exact subtree. +async function devupLookupAssetNode(id) { + try { + const node = await figma.getNodeByIdAsync(id); + if (node) return node; + } catch (error) { + if (!id.startsWith("I") || !id.includes(";")) throw error; + try { return await walk(); } + catch (fallback) { throw new Error(`${error.message || error}; nested lookup fallback: ${fallback.message || fallback}`); } + } + if (!id.startsWith("I") || !id.includes(";")) return null; + return walk(); + + async function walk() { + const root = await figma.getNodeByIdAsync(id.slice(1).split(";")[0]); + const pending = root ? [root] : []; + const seen = new Set(); + while (pending.length) { + const node = pending.pop(); + if (node.id === id) return node; + if (seen.has(node.id)) continue; + seen.add(node.id); + if (seen.size > 10000) throw new Error("nested lookup exceeds 10000 nodes"); + if (Array.isArray(node.children)) pending.push(...node.children); + } + throw new Error(`nested node ${id} not found in its instance`); + } +} + +function devupGeometrySvg(root) { + const escape = value => String(value).replace(/[&<>"']/g, c => ({"&":"&","<":"<",">":">",'"':""","'":"'"}[c])); + const finite = value => typeof value === "number" && Number.isFinite(value); + const fail = (node, reason) => { throw new Error(`geometry fallback unsupported: ${node.type || "unknown"} ${node.id || ""}: ${reason}`); }; + let geometryCount = 0, visited = 0; + function paintPaths(node, paths, paints) { + if ((paints !== undefined && !Array.isArray(paints)) || (paints === undefined && Array.isArray(paths) && paths.length)) fail(node, "unresolved paint requires native export"); + const visible = Array.isArray(paints) ? paints.filter(p => p.visible !== false) : []; + if (!visible.length) return ""; + if (!Array.isArray(paths) || !paths.length) fail(node, "paint has no rendered geometry"); + return visible.map(paint => { + if (paint.type !== "SOLID" || (paint.blendMode && paint.blendMode !== "NORMAL")) fail(node, "paint type or blending requires native export"); + const color = paint.color; + if (!color || ![color.r, color.g, color.b].every(c => finite(c) && c >= 0 && c <= 1)) fail(node, "invalid solid color"); + const opacity = paint.opacity === undefined ? 1 : paint.opacity; + if (!finite(opacity) || opacity < 0 || opacity > 1) fail(node, "invalid paint opacity"); + // Paths of one winding rule are painted together, preserving holes and + // overlap within the rendered geometry. Preserve floating-point colors. + return ["NONZERO", "EVENODD"].map(rule => { + const matching = paths.filter(path => path.windingRule === rule); + if (!matching.length) return ""; + for (const path of matching) { + if (typeof path.data !== "string" || !path.data.trim()) fail(node, "empty path"); + geometryCount++; + } + return ``; + }).join(""); + }).join(""); + } + function render(node, isRoot) { + if (++visited > 10000) fail(node, "subtree exceeds 10000 nodes"); + if (node.isMask) fail(node, "mask requires native export"); + if (node.visible === false || node.opacity === 0) return ""; + if (node.clipsContent) fail(node, "clipping requires native export"); + if (Array.isArray(node.effects) && node.effects.some(e => e.visible !== false)) fail(node, "effect requires native export"); + if (node.blendMode && !["NORMAL", "PASS_THROUGH"].includes(node.blendMode)) fail(node, "layer blending requires native export"); + const opacity = node.opacity === undefined ? 1 : node.opacity; + if (!finite(opacity) || opacity < 0 || opacity > 1) fail(node, "invalid layer opacity"); + let transform = ""; + if (!isRoot) { + const t = node.relativeTransform; + if (!Array.isArray(t) || t.length !== 2 || !t.every(row => Array.isArray(row) && row.length === 3 && row.every(finite))) fail(node, "missing relative transform"); + transform = ` transform="matrix(${t[0][0]} ${t[1][0]} ${t[0][1]} ${t[1][1]} ${t[0][2]} ${t[1][2]})"`; + } + let body = ""; + if (["GROUP", "FRAME", "INSTANCE", "COMPONENT"].includes(node.type)) { + if ((Array.isArray(node.fills) && node.fills.some(p => p.visible !== false)) || (Array.isArray(node.strokes) && node.strokes.some(p => p.visible !== false))) fail(node, "container paint requires native export"); + if (!Array.isArray(node.children)) fail(node, "missing children"); + const children = node.itemReverseZIndex === true ? [...node.children].reverse() : node.children; + body = children.map(child => render(child, false)).join(""); + } else { + if (!["VECTOR", "BOOLEAN_OPERATION", "RECTANGLE", "ELLIPSE", "POLYGON", "STAR", "LINE"].includes(node.type)) fail(node, "node type requires native export"); + // Plugin API strokeGeometry is centered even for INSIDE/OUTSIDE strokes. + // Using it there would silently change the shape. + if (Array.isArray(node.strokes) && node.strokes.some(p => p.visible !== false) && node.strokeAlign !== "CENTER") fail(node, "non-center stroke alignment requires native export"); + if (Array.isArray(node.dashPattern) && node.dashPattern.length) fail(node, "dashed stroke requires native export"); + for (const paths of [node.fillGeometry, node.strokeGeometry]) { + if (paths !== undefined && (!Array.isArray(paths) || paths.some(p => !["NONZERO", "EVENODD"].includes(p.windingRule)))) fail(node, "unknown winding rule"); + } + // Baked geometry includes boolean operations and centered stroke outlines. + // Never approximate those using a bounding box. + body = paintPaths(node, node.fillGeometry, node.fills) + paintPaths(node, node.strokeGeometry, node.strokes); + } + return `${body}`; + } + const bounds = root.absoluteRenderBounds, t = root.absoluteTransform; + // Native exports include stroke extents. Convert rendered bounds back to + // local space only when this is exact; rotated/sheared bounds lose that info. + if (!bounds || ![bounds.x, bounds.y, bounds.width, bounds.height].every(finite) || bounds.width <= 0 || bounds.height <= 0) fail(root, "missing render bounds"); + if (!Array.isArray(t) || t.length !== 2 || !t.every(row => Array.isArray(row) && row.length === 3 && row.every(finite)) || t[0][1] !== 0 || t[1][0] !== 0 || t[0][0] <= 0 || t[1][1] <= 0) fail(root, "root rotation, reflection or shear requires native export"); + const x = (bounds.x - t[0][2]) / t[0][0], y = (bounds.y - t[1][2]) / t[1][1]; + const width = bounds.width / t[0][0], height = bounds.height / t[1][1]; + const body = render(root, true); + if (!geometryCount) fail(root, "no painted geometry"); + return `${body}`; +} + +async function devupRenderAsset(node, settings, nativeBudgetMs = 85000, observe = () => {}) { + const began = Date.now(); + let timer; + try { + const result = await Promise.race([ + node.exportAsync(settings), + new Promise((_, reject) => { timer = setTimeout(() => reject(new Error(`DEVUP_ASSET_TIMEOUT: native export exceeded ${nativeBudgetMs} ms`)), nativeBudgetMs); }), + ]); + observe("native-export", Date.now() - began); + return result; + } + catch (nativeError) { + observe("native-export", Date.now() - began, String(nativeError.message || nativeError).slice(0, 500)); + if (settings.format !== "SVG_STRING") throw nativeError; + const fallbackBegan = Date.now(); + try { + const svg = devupGeometrySvg(node); + observe("geometry-fallback", Date.now() - fallbackBegan); + return svg; + } + catch (fallbackError) { throw new Error(`native export: ${nativeError.message || nativeError}; ${fallbackError.message || fallbackError}`); } + } finally { clearTimeout(timer); } +} +; + +const started = Date.now(); +let stage = "lookup"; +let nodeType = "unknown"; +const timeoutMs = Math.min(600000, Math.max(1000, Number(options.timeoutMs) || 90000)); +const exportDiagnostics = { method: "native", stages: [] }; +function observe(stage, elapsedMs, nativeError) { + exportDiagnostics.stages.push({ stage, elapsedMs }); + if (nativeError) exportDiagnostics.nativeError = nativeError; + if (stage === "geometry-fallback") exportDiagnostics.method = "geometry"; +} +async function timed(work) { + let timer; + try { + return await Promise.race([work(), new Promise((_, reject) => { + timer = setTimeout(() => reject(new Error(`DEVUP_ASSET_TIMEOUT: ${stage} exceeded ${timeoutMs} ms asset budget`)), Math.max(0, timeoutMs - (Date.now() - started))); + })]); + } finally { clearTimeout(timer); } +} + +function describe(error) { + const message = error && typeof error.message === "string" ? error.message : String(error); + const editor = typeof figma.editorType === "string" ? ` (editor: ${figma.editorType})` : ""; + return `${stage} failed after ${Date.now() - started} ms${editor} (node: ${options.nodeId}, type: ${nodeType}): ${message}`.slice(0, 500); +} + +function failed(errorCode, errorDetail) { + if (errorCode && exportDiagnostics.renderInfo) { + exportDiagnostics.renderInfo.attemptedRepresentation = exportDiagnostics.renderInfo.representation; + exportDiagnostics.renderInfo.representation = "not-exported"; + exportDiagnostics.renderInfo.renderedAs = "not-exported"; + exportDiagnostics.renderInfo.effectsBakedIntoAsset = false; + exportDiagnostics.renderInfo.fidelity = "unverified"; + } + return { + kind: "devupAssetExport", + fileKey: figma.fileKey || "", + version: options.version, + assetId: options.assetId, + nodeId: options.nodeId, + field: options.field, + imageHash: options.imageHash, + format: options.format, + scale: options.scale, + renderMode, + usageAspectRatio: options.usageAspectRatio ?? null, maxWidth: options.maxWidth ?? null, srcset: options.srcset === true, visibleScopeNodeId: options.visibleScopeNodeId ?? null, + status: "failed", + byteLength: null, + sha256: null, + errorCode, + exportDiagnostics, + ...(errorDetail ? { errorDetail } : {}), + }; +} + +const original = typeof options.field === "string" && options.field.startsWith("$original-image/fills/"); +const renderMode = options.renderMode === undefined ? (original ? "fill" : ["svg", "pdf"].includes(options.format) ? "node" : "original-source") : options.renderMode; +let effectiveMode = ["auto", "original-source"].includes(renderMode) ? (renderMode === "auto" ? "node" : "fill") : renderMode; +function renderInfo(node, paint) { + const finite = n => typeof n === "number" && Number.isFinite(n) && n > 0; + const effects = Array.isArray(node.effects) ? node.effects : null; + const ancestors = []; + const boundedName = name => [...name].slice(0, 128).join(""); + const layerHierarchy = [typeof node.name === "string" ? boundedName(node.name) : "layer"]; + const seen = new Set(); + let branch = node; + for (let parent = node.parent; parent && ancestors.length < 32 && !seen.has(parent); parent = parent.parent) { + seen.add(parent); + if (layerHierarchy.length < 4 && typeof parent.name === "string" && parent.name.length && !["PAGE", "DOCUMENT"].includes(parent.type)) { + layerHierarchy.push(boundedName(parent.name)); + } + ancestors.push({ type: parent.type || "unknown", clipsContent: parent.clipsContent ?? null, + isMask: parent.isMask ?? null, effects: Array.isArray(parent.effects) ? parent.effects : null, + opacity: parent.opacity ?? null, blendMode: parent.blendMode ?? null, visible: parent.visible ?? null, + siblingMaskPresent: Array.isArray(parent.children) ? parent.children.some(n => n !== branch && n.isMask === true) : null }); + branch = parent; + } + return { + sourceKind: paint ? "image-fill" : options.imageHash ? "image-node" : "vector-node", + renderedAs: effectiveMode === "fill" ? "raw-fill" : "node-render", + representation: effectiveMode === "fill" ? "original-fill" : "native-node", + layerHierarchy: layerHierarchy.reverse(), + scope: effectiveMode === "fill" ? "selected-paint-original-bytes" : "exact-node-subtree", + exportSettings: effectiveMode === "node" ? { contentsOnly: true, useAbsoluteBounds: false } : null, + requestedScale: options.scale, + scaleSemantics: effectiveMode === "fill" ? "intrinsic-original-bytes-no-resize" + : ["png", "jpg"].includes(options.format) ? "native-node-SCALE-constraint" : "not-applied-to-vector-document", + nodeSize: finite(node.width) && finite(node.height) ? {width:node.width, height:node.height, aspectRatio:node.width/node.height} : null, + outputSize: null, dimensionEvidence: "unknown-until-byte-inspection", aspectRatioComparison: "unknown", + scaleMode: paint ? paint.scaleMode ?? null : null, + imageTransform: paint ? paint.imageTransform ?? null : null, + // A transform matrix is not a visible-area percentage. Rotation, fit, + // clipping and source geometry are needed to determine visible coverage. + visibleSourceRatio: null, visibleSourceRatioEvidence: "not-computed", + effects, + extraFills: Array.isArray(node.fills) ? Math.max(0, node.fills.filter(p => p.visible !== false).length - (paint ? 1 : 0)) : null, + effectsBakedIntoAsset: effectiveMode === "node" && ["png", "jpg"].includes(options.format) && effects !== null, + bakedEffects: effectiveMode === "node" && ["png", "jpg"].includes(options.format) && effects ? effects.filter(e => e.visible !== false) : [], + instanceOverride: "unknown", instanceOverrideEvidence: "no-component-paint-comparison", + sourceProperties: { scaleMode: paint ? paint.scaleMode ?? null : null, + imageTransform: paint ? paint.imageTransform ?? null : null, + rotation: paint ? paint.rotation ?? null : null, filters: paint ? paint.filters ?? null : null, + opacity: paint ? paint.opacity ?? null : null, effects, + blurEffects: effects ? effects.filter(e => e.visible !== false && String(e.type).includes("BLUR")) : null, + isMask: node.isMask ?? null, clipsContent: node.clipsContent ?? null, ancestors }, + unappliedProperties: effectiveMode === "fill" ? ["scaleMode", "imageTransform", "rotation", "filters", "paintOpacity", "nodeGeometry", "nodeTransforms", "nodeEffectsIncludingBlur", "nodeAndAncestorMasks", "ancestorClipsAndEffects"] : [], + warnings: effectiveMode === "fill" ? ["original-fill-properties-unapplied", "aspect-ratio-unknown"] + : ["ancestor-canvas-appearance-unverified", "aspect-ratio-unknown"], + fidelity: "unverified", + }; +} + +// Unknown properties fail closed. Identity alone cannot establish canvas +// equality, and auto never infers source dimensions from the transform. +function rawSafety(node, paint, info) { + const reasons = []; + const require = (condition, reason) => { if (!condition) reasons.push(reason); }; + const identity = t => Array.isArray(t) && t.length === 2 && t.every((r,i) => Array.isArray(r) && r.length === 3 && r.every((v,j) => v === [[1,0,0],[0,1,0]][i][j])); + const unrotated = t => Array.isArray(t) && t.length === 2 && t.every(r => Array.isArray(r) && r.length === 3 && r.every(Number.isFinite)) && t[0][0] === 1 && t[1][1] === 1 && t[0][1] === 0 && t[1][0] === 0 && Number.isInteger(t[0][2]) && Number.isInteger(t[1][2]); + require(options.transport === "bridge", "raw-requires-bridge"); + require(options.scale === 1 && ["png","jpg"].includes(options.format), "raw-requires-unscaled-raster"); + require(!!paint && paint.visible === true && paint.opacity === 1 && paint.blendMode === "NORMAL", "paint-compositing-or-unknown"); + require(paint && paint.scaleMode === "FILL" && identity(paint.imageTransform) && paint.rotation === 0, "paint-transform-or-unknown"); + require(paint && paint.filters && Object.values(paint.filters).every(v => v === 0), "image-filters-or-unknown"); + require(Array.isArray(node.fills) && node.fills.length === 1, "multiple-fills-or-unknown"); + require(node.type === "RECTANGLE" && node.cornerRadius === 0 && Array.isArray(node.strokes) && node.strokes.length === 0 && (!node.children || node.children.length === 0), "node-geometry-or-unknown"); + // RectangleNode has no clipsContent in the Plugin API. Its absence is + // non-applicable for this geometry, rather than missing Frame evidence. + require(Array.isArray(node.effects) && node.effects.length === 0 && node.isMask === false && (node.clipsContent === undefined || node.clipsContent === false) && node.opacity === 1 && node.visible === true && node.blendMode === "NORMAL", "node-effects-mask-compositing-or-unknown"); + require(unrotated(node.absoluteTransform) && unrotated(node.relativeTransform), "node-transform-or-unknown"); + require(info.sourceProperties.ancestors.length > 0 && info.sourceProperties.ancestors.some(p => ["PAGE","DOCUMENT"].includes(p.type)), "ancestor-chain-incomplete"); + require(info.sourceProperties.ancestors.every(p => p.siblingMaskPresent === false), "ancestor-sibling-mask-or-unknown"); + for (const p of info.sourceProperties.ancestors.filter(p => !["PAGE","DOCUMENT"].includes(p.type))) { + require(p.clipsContent === false && p.isMask === false && p.siblingMaskPresent === false && Array.isArray(p.effects) && p.effects.length === 0 && p.opacity === 1 && p.visible === true && ["NORMAL","PASS_THROUGH"].includes(p.blendMode), "ancestor-dependency-or-unknown"); + } + return { eligible: reasons.length === 0, reasons: [...new Set(reasons)], evidence: "explicit-plugin-properties; intrinsic-dimensions-and-codec-required" }; +} + +function overrideEvidence(node, info) { + let count = 0; + for (let parent = node; parent && count++ < 32; parent = parent.parent) { + if (parent.type === "INSTANCE" && Array.isArray(parent.overrides)) { + const evidence = parent.overrides.find(o => o.id === node.id && Array.isArray(o.overriddenFields) && o.overriddenFields.includes("fills")); + if (evidence) { info.instanceOverride = true; info.instanceOverrideEvidence = { kind: "figma-instance-overrides", instanceId: parent.id, nodeId: node.id, overriddenFields: ["fills"] }; return; } + } + if (["PAGE","DOCUMENT"].includes(parent.type)) break; + } +} +try { + if (!["original-source", "fill", "node", "visible", "auto"].includes(renderMode)) return failed("DEVUP_ASSET_RENDER_MODE_UNSUPPORTED"); + if (!original && renderMode === "fill" && + (!String(options.field).startsWith("fills/") || !["png", "jpg", "webp"].includes(options.format) || options.scale !== 1)) { + return failed("DEVUP_ASSET_FILL_SETTINGS_UNSUPPORTED", "Original fill requires fills/N, png or jpg matching its codec, and scale 1; no resizing or transcoding."); + } + // Original uploads are a different representation from node renditions. + // Only the local bridge opts in: the remote MCP's bounded text envelope + // cannot carry arbitrary original bytes, and its file writer is not a + // general binary transport. Never silently return a node PNG instead. + if ((original || ["fill", "original-source"].includes(renderMode)) && options.transport !== "bridge") { + return failed(original ? "DEVUP_ORIGINAL_IMAGE_REQUIRES_BRIDGE" : "DEVUP_ASSET_FILL_REQUIRES_BRIDGE"); + } + const node = await timed(() => devupLookupAssetNode(options.nodeId)); + observe("lookup", Date.now() - started); + nodeType = node && node.type || "unknown"; + if (!node || (["node", "visible"].includes(renderMode) && typeof node.exportAsync !== "function")) { + return failed("DEVUP_ASSET_UNSUPPORTED_BY_UPSTREAM", describe(new Error(node ? "node has no exportAsync" : "node not found"))); + } + let selectedPaint = null; + if (original || (typeof options.field === "string" && options.field.startsWith("fills/"))) { + const index = Number(options.field.slice(original ? "$original-image/fills/".length : "fills/".length)); + const fills = "fills" in node && Array.isArray(node.fills) ? node.fills : []; + const paint = Number.isInteger(index) ? fills[index] : null; + const imageHash = paint && paint.type === "IMAGE" ? paint.imageHash || paint.imageRef : null; + if (!paint || imageHash !== options.imageHash) { + return failed("DEVUP_ASSET_SOURCE_CHANGED"); + } + selectedPaint = paint; + } else if (options.field !== "node") { + return failed("DEVUP_ASSET_FIELD_UNSUPPORTED"); + } + if (renderMode === "original-source" && (!selectedPaint || !options.imageHash)) return failed("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 captureNode = node; + if (renderMode === "visible") { + const scopeId = options.visibleScopeNodeId; + if (!scopeId) return failed("DEVUP_VISIBLE_CONTEXT_UNAVAILABLE", "Recollect the parent frame and provide visibleScopeNodeId."); + const seen = new Set(); + while (captureNode && captureNode.id !== scopeId && !seen.has(captureNode) && seen.size < 64) { + seen.add(captureNode); captureNode = captureNode.parent; + } + if (!captureNode || captureNode.id !== scopeId || ["PAGE","DOCUMENT"].includes(captureNode.type) || typeof captureNode.exportAsync !== "function") return failed("DEVUP_VISIBLE_SCOPE_UNSUPPORTED", "The selected capture scope is not an exportable ancestor of the requested node."); + } + exportDiagnostics.renderInfo = renderInfo(node, selectedPaint); + if (renderMode === "visible") { + const info = exportDiagnostics.renderInfo; + info.representation = "native-visible"; info.renderedAs = "visible-scoped-capture"; + info.captureNodeId = captureNode.id; + info.captureBox = {width:captureNode.width, height:captureNode.height, aspectRatio:captureNode.width/captureNode.height}; + info.contextScope = "selected-ancestor-subtree"; + info.captureEffects = Array.isArray(captureNode.effects) ? captureNode.effects : null; + info.requestedNodeEffects = info.effects; + info.effects = info.captureEffects; + info.sourceProperties.captureEffects = info.captureEffects; + info.visibleContext = {requestedNodeId:node.id,captureNodeId:captureNode.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:false}; + info.warnings.push("visible-scoped-capture", "outside-capture-context-unverified"); + } + overrideEvidence(node, exportDiagnostics.renderInfo); + let observedSourceSize = null; + let sizeTimer; + if (selectedPaint) { + try { + const image = figma.getImageByHash(options.imageHash); + const size = image && await Promise.race([image.getSizeAsync(), new Promise(resolve => { + sizeTimer = setTimeout(() => resolve(null), 250); + })]); + observedSourceSize = size && size.width > 0 && size.height > 0 && Number.isFinite(size.width) && Number.isFinite(size.height) + ? { ...size, aspectRatio: size.width / size.height } : null; + } catch (_error) { /* Optional diagnostics never consume the native export budget. */ } + finally { clearTimeout(sizeTimer); } + } + let autoBytes = null; + if (renderMode === "auto") { + const safety = rawSafety(node, selectedPaint, exportDiagnostics.renderInfo); + if (safety.eligible) { + try { + const image = figma.getImageByHash(options.imageHash); + const sourceSize = observedSourceSize; + if (!sourceSize || sourceSize.width !== node.width || sourceSize.height !== node.height) safety.reasons.push("intrinsic-dimensions-mismatch-or-unknown"); + else { + autoBytes = await timed(() => image.getBytesAsync()); + const codec = autoBytes[0] === 137 && autoBytes[1] === 80 && autoBytes[2] === 78 && autoBytes[3] === 71 ? "png" : autoBytes[0] === 255 && autoBytes[1] === 216 && autoBytes[2] === 255 ? "jpg" : "unknown"; + if (codec !== options.format) safety.reasons.push("source-codec-mismatch-or-unknown"); + } + } catch (_error) { safety.reasons.push("source-inspection-unavailable"); } + } + safety.eligible = safety.reasons.length === 0; + effectiveMode = safety.eligible ? "fill" : "node"; + exportDiagnostics.renderInfo = renderInfo(node, selectedPaint); + overrideEvidence(node, exportDiagnostics.renderInfo); + exportDiagnostics.renderInfo.autoDecision = { ...safety, selected: effectiveMode }; + } + exportDiagnostics.renderInfo.sourceSize = observedSourceSize; + exportDiagnostics.renderInfo.sourceSizeEvidence = observedSourceSize ? "Figma-image-getSizeAsync" : "unknown"; + + if (original || effectiveMode === "fill") { + exportDiagnostics.method = "original-fill"; + stage = "reading the original image"; + const image = figma.getImageByHash(options.imageHash); + if (!image) return failed("DEVUP_ORIGINAL_IMAGE_NOT_FOUND"); + const bytes = autoBytes || await timed(() => image.getBytesAsync()); + if (bytes.length === 0 || bytes.length > 80 * 1024 * 1024) { + return failed("DEVUP_ASSET_RESPONSE_TOO_LARGE", `${bytes.length} bytes; one answer carries 1 to 83886080`); + } + const starts = values => values.every((value, index) => bytes[index] === value); + const mimeType = starts([137,80,78,71,13,10,26,10]) ? "image/png" + : starts([255,216,255]) ? "image/jpeg" + : starts([71,73,70,56]) ? "image/gif" + : starts([82,73,70,70]) && bytes[8] === 87 && bytes[9] === 69 && bytes[10] === 66 && bytes[11] === 80 ? "image/webp" + : null; + if (!mimeType) return failed("DEVUP_ORIGINAL_IMAGE_CODEC_UNSUPPORTED"); + exportDiagnostics.renderInfo.originalMimeType = mimeType; + if (!original && renderMode === "fill" && options.format !== "webp" && mimeType !== { png: "image/png", jpg: "image/jpeg" }[options.format]) { + return failed("DEVUP_ASSET_FILL_CODEC_MISMATCH", `Original fill codec is ${mimeType}, but ${options.format} was requested; choose the matching format or renderMode node. No transcoding was performed.`); + } + if (!original) return { ...failed(null), status: "exported", mimeType, + byteLength: bytes.length, sha256: devupSha256(bytes), data: figma.base64Encode(bytes) }; + const size = await timed(() => image.getSizeAsync()); + return { + ...failed(null), status: "exported", kind: "devupOriginalImage", + representation: "original-image-v1", format: null, scale: null, + mimeType, width: size.width, height: size.height, + byteLength: bytes.length, sha256: devupSha256(bytes), data: figma.base64Encode(bytes), + }; + } + + // WebP is encoded by the server from this exact PNG; Figma has no native WebP export. + const format = options.format === "webp" ? "PNG" : String(options.format || "").toUpperCase(); + if (!["PNG", "JPG", "SVG", "PDF"].includes(format)) { + return failed("DEVUP_ASSET_FORMAT_UNSUPPORTED"); + } + const scale = Number(options.scale ?? 2); + if (!Number.isFinite(scale) || scale <= 0 || scale > 4) return failed("DEVUP_ASSET_SCALE_INVALID"); + // SVG is exported as a string and carried back inline. Figma's remote MCP + // does not return a written `.svg` as an attachment the way it does a PNG, + // so writing the file alone left the caller holding a descriptor and no + // bytes at all, and every SVG request failed. SVG is text and small, so an + // inline copy is bounded well under the text-response limit; anything + // larger is reported rather than silently truncated. + const inlineSvg = format === "SVG"; + const settings = { format: inlineSvg ? "SVG_STRING" : format, contentsOnly: true, useAbsoluteBounds: false }; + if (format === "PNG" || format === "JPG") { + settings.constraint = { type: "SCALE", value: scale }; + } + stage = "export"; + const remaining = Math.max(1, timeoutMs - (Date.now() - started)); + // Reserve up to five seconds for read-only geometry after a hung renderer. + // A late native answer has no file writer and cannot replace the fallback. + const nativeBudget = inlineSvg ? Math.max(1, remaining - Math.min(5000, Math.floor(remaining / 5))) : remaining; + const nativeBegan = Date.now(); + // Image-filled nodes must use Figma's renderer for crop, filters, effects, + // transforms and masks within this subtree. Never substitute raw fill bytes + // or geometry on renderer failure. The canvas outside this node is unverified. + const exported = await timed(() => renderMode === "visible" ? captureNode.exportAsync(settings) + : selectedPaint ? node.exportAsync(settings) : devupRenderAsset(node, settings, nativeBudget, observe)); + if (selectedPaint) observe("native", Date.now() - nativeBegan); + if (exportDiagnostics.method === "geometry") { + exportDiagnostics.renderInfo.representation = "geometry-fallback"; + exportDiagnostics.renderInfo.renderedAs = "geometry-fallback"; + exportDiagnostics.renderInfo.effectsBakedIntoAsset = false; + exportDiagnostics.renderInfo.warnings.push("geometry-fallback-not-native-render"); + } + if (Date.now() - started >= timeoutMs) throw new Error(`DEVUP_ASSET_TIMEOUT: export exceeded ${timeoutMs} ms asset budget`); + stage = "encode"; + const encodeBegan = Date.now(); + const svgText = inlineSvg && typeof exported === "string" ? exported : null; + const bytes = + svgText === null + ? exported instanceof Uint8Array + ? exported + : new Uint8Array(exported) + : devupUtf8Encode(svgText); + if (bytes.length === 0 || bytes.length > 80 * 1024 * 1024) { + return failed("DEVUP_ASSET_RESPONSE_TOO_LARGE", `${format} at scale ${scale} is ${bytes.length} bytes; one answer carries 1 to 83886080`); + } + const sha256 = devupSha256(bytes); + observe("encode", Date.now() - encodeBegan); + // figma.io is an extension of the remote MCP, not the native Plugin API. + // The bridge returns JSON over its local socket and has no attachment + // writer. Carry its bounded bytes inline, without exporting again for each + // fragment; the Rust decoder still validates length, MIME type and hash. + const hasFileWriter = figma.io && typeof figma.io.write === "function"; + // A PNG past what one attachment carries is not written here either. + // Figma's remote MCP returns a written PNG as an attachment only up to + // about a megabyte once base64-encoded: a 665 KB photograph came back, a + // 950 KB one was written, reported exported, and never arrived - the + // devup-ui landing page's hero. Past 768 KiB, which is exactly one MiB + // encoded, it is announced and read back in fragments like a large SVG. + const pngTooLargeToAttach = format === "PNG" && bytes.length > 768 * 1024; + if (hasFileWriter && ((svgText !== null && bytes.length > 12 * 1024) || pngTooLargeToAttach)) { + // An SVG past what one text response holds is not written here at all: + // it is announced with its length and hash, and read back in fragments + // through the large-value script, which re-exports it and slices — the + // same transport a large node field takes. An illustration of fifty + // vectors is 125 KB; before this it simply failed. + return { + kind: "devupAssetExport", + fileKey: figma.fileKey || "", + version: options.version, + assetId: options.assetId, + nodeId: options.nodeId, + field: options.field, + imageHash: options.imageHash, + format: options.format, + scale, + renderMode, + usageAspectRatio: options.usageAspectRatio ?? null, maxWidth: options.maxWidth ?? null, srcset: options.srcset === true, visibleScopeNodeId: options.visibleScopeNodeId ?? null, + status: "chunked", + byteLength: bytes.length, + sha256, + cursor: { nextOffset: 0, maxChunkBytes: DEVUP_LARGE_VALUE_CHUNK_BYTES }, + errorCode: null, + exportDiagnostics, + }; + } + if (hasFileWriter) { + figma.io.write(`devup-asset-${options.assetId.replace(/[^A-Za-z0-9_-]/g, "_")}.${format.toLowerCase()}`, bytes); + } + return { + kind: "devupAssetExport", + fileKey: figma.fileKey || "", + version: options.version, + assetId: options.assetId, + nodeId: options.nodeId, + field: options.field, + imageHash: options.imageHash, + format: options.format, + scale, + renderMode, + usageAspectRatio: options.usageAspectRatio ?? null, maxWidth: options.maxWidth ?? null, srcset: options.srcset === true, visibleScopeNodeId: options.visibleScopeNodeId ?? null, + status: "exported", + byteLength: bytes.length, + sha256, + // MIME identifies inline bridge bytes or remote SVG text as a payload. + mimeType: !hasFileWriter + ? { PNG: "image/png", JPG: "image/jpeg", SVG: "image/svg+xml", PDF: "application/pdf" }[format] + : svgText === null ? null : "image/svg+xml", + ...(!hasFileWriter && svgText === null ? { data: figma.base64Encode(bytes) } : {}), + text: svgText, + errorCode: null, + exportDiagnostics, + }; +} catch (error) { + return failed(String(error.message || error).includes("DEVUP_ASSET_TIMEOUT:") ? "DEVUP_ASSET_TIMEOUT" : original || ["fill", "original-source"].includes(renderMode) ? "DEVUP_ORIGINAL_IMAGE_READ_FAILED" : "DEVUP_ASSET_EXPORT_FAILED", describe(error)); +} + +} + +/** source: bridge_context.js */ +async function bridgeContext(p) { +// Operation-time read: independent of asynchronous selection event delivery. +const page = figma.currentPage; +const selected = page.selection; +const ref = node => ({id: node.id, name: String(node.name).slice(0, 256), type: node.type}); +return {kind: 'devup-bridge-context', currentPage: ref(page), + selection: selected.slice(0, 20).map(ref), selectionCount: selected.length, + zoom: Number.isFinite(figma.viewport?.zoom) ? figma.viewport.zoom : null, + observedAt: Date.now(), lastKnownSelection: globalThis.__devupLastSelection ?? null, + lastKnownPage: globalThis.__devupLastSelectionPage ?? null}; + +} + +/** source: explore.js */ +async function explore(p) { +// Figma's own MCP runs scripts with hidden layers inside instances left out +// of `children`, `findAll` and `getNodeByIdAsync`; a plugin sees them. Set +// here so the direct path and the Devup Bridge read the same document. +figma.skipInvisibleInstanceChildren = false; +const anchor = await figma.getNodeByIdAsync(p.nodeId); +if (!anchor) throw new Error("DEVUP_NODE_NOT_FOUND"); + +const options = p.explore; +const projectionLimit = Math.max(20, Math.min( + 400, + Number.isInteger(options.projectionLimit) ? options.projectionLimit : 200, +)); +const textPreviewLimit = Math.max(0, Math.min( + 500, + Number.isInteger(options.textPreviewLimit) ? options.textPreviewLimit : 160, +)); +const MAX_PROJECTION_JSON_CHARS = 14_000; +const MAX_NAME_CHARS = 240; +const MAX_BREADCRUMB_SEGMENTS = 12; + +function compactName(value) { + return typeof value === "string" ? value.slice(0, MAX_NAME_CHARS) : ""; +} + +const requestedAnchor = anchor; +const requestedPage = requestedAnchor.type === "PAGE"; +let pagePeer = requestedAnchor; +let nearestSection = requestedAnchor.type === "SECTION" ? requestedAnchor : null; +while (!requestedPage && pagePeer.parent && pagePeer.parent.type !== "PAGE") { + pagePeer = pagePeer.parent; + if (!nearestSection && pagePeer.type === "SECTION") nearestSection = pagePeer; +} +const page = requestedAnchor.type === "PAGE" ? requestedAnchor : pagePeer.parent; +if (!page || page.type !== "PAGE") throw new Error("DEVUP_PAGE_NOT_FOUND"); +await figma.setCurrentPageAsync(page); + +function bounds(node) { + const absolute = node.absoluteBoundingBox; + const value = absolute || { + x: typeof node.x === "number" ? node.x : 0, + y: typeof node.y === "number" ? node.y : 0, + width: typeof node.width === "number" ? node.width : 0, + height: typeof node.height === "number" ? node.height : 0, + }; + if (![value.x, value.y, value.width, value.height].every(Number.isFinite)) return null; + return { x: value.x, y: value.y, width: value.width, height: value.height }; +} + +// Carry a small measured navigation summary into screen planning, before a +// full page export. Missing measurements remain unknown rather than guessed. +function navigationContentWidth(root) { + const queue = [root]; + let visited = 0; + let width = null; + while (queue.length && visited++ < 400) { + const node = queue.shift(); + if (("visible" in node) && node.visible === false) continue; + const children = "children" in node ? node.children.filter((child) => !("visible" in child) || child.visible !== false) : []; + if (/header|nav|gnb/i.test(node.name || "") && children.length >= 2 + && ("layoutMode" in node) && node.layoutMode === "HORIZONTAL") { + const sizes = children.map(bounds); + if (sizes.every(Boolean)) { + const measured = sizes.reduce((sum, box) => sum + box.width, 0) + (Number(node.itemSpacing) || 0) * (sizes.length - 1); + width = Math.max(width || 0, measured); + } + } + queue.push(...children.slice(0, 80)); + } + return width; +} + +function navigationSummary(root) { + const width = navigationContentWidth(root); + return width === null ? {} : { navigationContentWidth: width }; +} + + +function horizontalOverlap(left, right) { + return Math.min(left.x + left.width, right.x + right.width) - Math.max(left.x, right.x); +} + +function isScreenLike(node) { + const box = bounds(node); + return box + && ["FRAME", "COMPONENT", "INSTANCE", "COMPONENT_SET"].includes(node.type) + && box.width >= 240 + && box.width <= 1800 + && box.height >= 300 + && box.height <= 2000 + && box.width / Math.max(1, box.height) >= 0.25 + && box.width / Math.max(1, box.height) <= 2.5; +} + +function textPreview(node) { + if (textPreviewLimit === 0) return ""; + const values = []; + const queue = [node]; + let visited = 0; + while (queue.length && visited < 80 && values.join(" ").length < textPreviewLimit) { + const candidate = queue.shift(); + visited += 1; + if (candidate.type === "TEXT" && typeof candidate.characters === "string") { + values.push(candidate.characters.replace(/\s+/gu, " ").trim()); + } + if ("children" in candidate) queue.push(...candidate.children.slice(0, 40)); + } + return values.filter(Boolean).join(" ").slice(0, textPreviewLimit); +} + +function breadcrumb(node) { + const names = []; + let current = node; + while (current && current.type !== "DOCUMENT" && names.length < MAX_BREADCRUMB_SEGMENTS) { + const name = compactName(current.name); + if (name) names.push(name); + current = current.parent; + } + return names.reverse(); +} + +const scopeAnchor = requestedPage ? page : !isScreenLike(requestedAnchor) && nearestSection + ? nearestSection + : pagePeer; +const anchorBounds = bounds(scopeAnchor) || bounds(requestedAnchor); +if (!anchorBounds) throw new Error("DEVUP_NODE_BOUNDS_UNAVAILABLE"); +const pageChildren = "children" in page ? page.children : []; +const eligible = pageChildren + .map((node, pageChildIndex) => ({ node, pageChildIndex, bounds: bounds(node) })) + .filter((entry) => entry.bounds) + .filter((entry) => { + if (requestedPage) return true; + if (entry.node.id === scopeAnchor.id) return true; + const verticallyNear = entry.bounds.y >= anchorBounds.y - 240 + && entry.bounds.y <= anchorBounds.y + 12000; + return verticallyNear && horizontalOverlap(entry.bounds, anchorBounds) > 0; + }) + .sort((left, right) => + left.bounds.y - right.bounds.y + || left.bounds.x - right.bounds.x + || left.node.id.localeCompare(right.node.id), + ); +const selected = scopeAnchor.type === "SECTION" + ? eligible.filter((entry) => entry.node.id === scopeAnchor.id) + : eligible.slice(0, projectionLimit); +const included = new Map(selected.map((entry) => [entry.node.id, entry])); +if (!included.has(pagePeer.id)) { + included.set(pagePeer.id, { node: pagePeer, pageChildIndex: -1, bounds: bounds(pagePeer) }); +} +if (!included.has(scopeAnchor.id)) { + included.set(scopeAnchor.id, { node: scopeAnchor, pageChildIndex: -1, bounds: bounds(scopeAnchor) }); +} +if (!included.has(requestedAnchor.id)) { + included.set(requestedAnchor.id, { + node: requestedAnchor, + pageChildIndex: -1, + bounds: bounds(requestedAnchor), + }); +} +let requiredAncestor = requestedPage ? null : requestedAnchor.parent; +while (requiredAncestor && requiredAncestor.type !== "PAGE") { + if (!included.has(requiredAncestor.id)) { + included.set(requiredAncestor.id, { + node: requiredAncestor, + pageChildIndex: -1, + bounds: bounds(requiredAncestor), + }); + } + if (requiredAncestor.id === scopeAnchor.id) break; + requiredAncestor = requiredAncestor.parent; +} +if ("children" in scopeAnchor) { + for (const child of scopeAnchor.children.slice(0, projectionLimit)) { + if (!included.has(child.id)) { + included.set(child.id, { node: child, pageChildIndex: -1, bounds: bounds(child) }); + } + } +} +const descendantScope = scopeAnchor.type === "SECTION" || requestedPage; +let sectionTraversalTruncated = descendantScope + && "children" in scopeAnchor + && scopeAnchor.children.length > projectionLimit; +if (descendantScope && "children" in scopeAnchor) { + const traversalLimit = projectionLimit * 8; + const nestedQueue = scopeAnchor.children + .filter((node) => !isScreenLike(node)) + .map((node) => ({ node, ancestors: [] })); + let visited = 0; + while (nestedQueue.length && visited < traversalLimit) { + const { node, ancestors } = nestedQueue.shift(); + visited += 1; + if (isScreenLike(node)) { + const missingChain = [...ancestors, node] + .filter((candidate) => !included.has(candidate.id)); + if (included.size + missingChain.length > projectionLimit + 2) { + sectionTraversalTruncated = true; + continue; + } + for (const candidate of missingChain) { + included.set(candidate.id, { + node: candidate, + pageChildIndex: -1, + bounds: bounds(candidate), + }); + } + continue; + } + if ("children" in node) { + for (const child of node.children) { + if (requestedPage && ["FRAME", "COMPONENT", "INSTANCE", "COMPONENT_SET"].includes(node.type)) continue; + nestedQueue.push({ node: child, ancestors: [...ancestors, node] }); + } + } + } + sectionTraversalTruncated ||= nestedQueue.length > 0; +} + +const compact = [...included.values()] + .filter((entry) => entry.bounds) + .slice(0, projectionLimit + 2) + .map(({ node, pageChildIndex, bounds: box }) => ({ + id: node.id, + type: node.type, + fields: { + name: compactName(node.name), + parentId: node.parent && node.parent.type !== "DOCUMENT" ? node.parent.id : null, + childrenIds: [], + x: box.x, + y: box.y, + width: box.width, + height: box.height, + childCount: "children" in node ? node.children.length : 0, + // Select the structural projection before spending space on previews. + textPreview: "", + ...(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 + // undefined — so exploring from a page id failed outright. + visible: !("visible" in node) || node.visible !== false, + breadcrumb: breadcrumb(node), + }, + extra: {}, + fieldErrors: {}, + })); + +const projectedBounds = compact.reduce((result, node) => { + const box = node.fields; + if (!result) return { x: box.x, y: box.y, right: box.x + box.width, bottom: box.y + box.height }; + return { + x: Math.min(result.x, box.x), + y: Math.min(result.y, box.y), + right: Math.max(result.right, box.x + box.width), + bottom: Math.max(result.bottom, box.y + box.height), + }; +}, null) || { x: 0, y: 0, right: 0, bottom: 0 }; +const projectionTruncated = (scopeAnchor.type !== "SECTION" && eligible.length > selected.length) + || included.size > projectionLimit + 2 + || sectionTraversalTruncated; +const pageNode = { + id: page.id, + type: page.type, + fields: { + name: compactName(page.name), + parentId: null, + childrenIds: [], + x: projectedBounds.x, + y: projectedBounds.y, + width: projectedBounds.right - projectedBounds.x, + height: projectedBounds.bottom - projectedBounds.y, + childCount: pageChildren.length, + textPreview: "", + projectionTruncated, + visible: true, + breadcrumb: breadcrumb(page), + pageChildIndex: null, + }, + extra: {}, + fieldErrors: {}, +}; + +const output = { + fileKey: figma.fileKey || "", + version: null, + rootIds: [page.id], + nodes: [pageNode, ...compact.filter((node) => node.id !== page.id)], + diagnostics: [], +}; +const requiredIds = new Set([ + page.id, + pagePeer.id, + scopeAnchor.id, + requestedAnchor.id, +]); +while (JSON.stringify(output).length > MAX_PROJECTION_JSON_CHARS) { + let removableIndex = output.nodes.length - 1; + while (removableIndex >= 0 && requiredIds.has(output.nodes[removableIndex].id)) { + removableIndex -= 1; + } + if (removableIndex < 0) break; + output.nodes.splice(removableIndex, 1); + pageNode.fields.projectionTruncated = true; +} + +if (JSON.stringify(output).length > MAX_PROJECTION_JSON_CHARS) { + output.nodes = output.nodes + .filter((node) => requiredIds.has(node.id)) + .map((node) => ({ + ...node, + fields: { + ...node.fields, + name: compactName(node.fields.name).slice(0, 80), + textPreview: "", + breadcrumb: node.fields.breadcrumb.slice(-4).map((name) => name.slice(0, 80)), + projectionTruncated: node.id === page.id ? true : node.fields.projectionTruncated, + }, + })); +} +if (JSON.stringify(output).length > MAX_PROJECTION_JSON_CHARS) { + throw new Error("DEVUP_EXPLORE_PROJECTION_TOO_LARGE"); +} + +// Optional previews may only consume space left by the structural projection. +// Page fallback can collect names across the page; bound UTF-8 bytes as well +// as the existing character budget on the metered text transport. +function utf8Bytes(value) { + let count = 0; + for (const character of value) { + const point = character.codePointAt(0); + count += point < 0x80 ? 1 : point < 0x800 ? 2 : point < 0x10000 ? 3 : 4; + } + return count; +} +const PAGE_TEXT_BYTES = 19 * 1024; +if (requestedPage) { + while (utf8Bytes(JSON.stringify(output)) > PAGE_TEXT_BYTES && output.nodes.length > 1) { + output.nodes.pop(); + pageNode.fields.projectionTruncated = true; + } + if (utf8Bytes(JSON.stringify(output)) > PAGE_TEXT_BYTES) throw new Error("DEVUP_EXPLORE_PROJECTION_TOO_LARGE"); +} +// Count JSON escaping as well, so quotes/control characters cannot push a +// preview over the envelope cap or evict a candidate. +let previewBudget = MAX_PROJECTION_JSON_CHARS - JSON.stringify(output).length; +if (requestedPage) previewBudget = Math.min(previewBudget, PAGE_TEXT_BYTES - utf8Bytes(JSON.stringify(output))); +if (textPreviewLimit > 0 && previewBudget > 0) { + // Each candidate takes an equal share of what is left rather than as much + // as it wants, so the last one is funded before the first is indulged. + // Spending the budget first-come left the tail with nothing at all, and a + // candidate with no text is one the caller cannot tell from its + // neighbours — which is the only thing a preview is for. What a candidate + // does not spend stays in the budget and widens the shares after it. + const fundable = output.nodes.filter( + (projected) => included.get(projected.id) && projected.id !== page.id, + ); + let unfunded = fundable.length; + for (const projected of fundable) { + const share = Math.floor(previewBudget / unfunded); + unfunded -= 1; + if (share <= 0) continue; + let preview = ""; + let spent = 0; + for (const character of textPreview(included.get(projected.id).node)) { + const cost = (requestedPage ? utf8Bytes(JSON.stringify(character)) : JSON.stringify(character).length) - 2; + if (spent + cost > share) break; + preview += character; + spent += cost; + } + projected.fields.textPreview = preview; + previewBudget -= spent; + } +} + +return output; + +} + +/** source: fast_snapshot.js */ +async function fastSnapshot(p) { +// Figma's own MCP runs scripts with hidden layers inside instances left out +// of `children`, `findAll` and `getNodeByIdAsync`; a plugin sees them. Set +// here so the direct path and the Devup Bridge read the same document. +figma.skipInvisibleInstanceChildren = false; +const requestedRootIds = p.rootIds; +if (!Array.isArray(requestedRootIds) || requestedRootIds.length === 0) { + throw new Error("DEVUP_ROOTS_INVALID"); +} +const roots = await Promise.all(requestedRootIds.map((id) => figma.getNodeByIdAsync(id))); +if (roots.some((root) => !root)) throw new Error("DEVUP_NODE_NOT_FOUND"); +if (roots.length === 1 && roots[0].type === "SECTION") { + throw new Error("DEVUP_TARGET_IS_SECTION"); +} + +// A screen drawn at three widths is three sibling frames in a Section, named +// for the width they are. Converting one of them alone can only describe that +// width, and the caller wanted the screen — so when the target is one of those +// frames, its siblings come along and the conversion can say how the screen +// changes rather than how it looks at one size. +// +// Narrow on purpose: the target must itself be named for a breakpoint, and only +// siblings that are. A Section is also how a file of unrelated cases is grouped, +// and pulling every neighbour in there would collect a catalogue to convert one +// square. +// +// The family keeps the Section's own order. It is not decoration: where one +// width does not draw a node, the plugin gives that width a hidden copy of the +// node from the first width that does, first in this order, and every value of +// the copy shows in the responsive array. Sorting the family by name here put +// tablet's values where the reference has desktop's. +const BREAKPOINT_NAMES = ["mobile", "tablet", "desktop"]; +const breakpointRank = (node) => { + const name = String(node.name || "").trim().toLowerCase(); + const known = BREAKPOINT_NAMES.indexOf(name); + if (known >= 0) return known; + const range = name.match(/^(\d+)\s*[~–]\s*(\d+)$/); + if (!range) return -1; + const start = Number(range[1]), end = Number(range[2]); + return Number.isSafeInteger(start) && Number.isSafeInteger(end) && start > 0 && end >= start && end <= 0xffffffff ? 1 : -1; +}; +if (roots.length === 1 && breakpointRank(roots[0]) >= 0) { + const parent = roots[0].parent; + if (parent && parent.type === "SECTION" && "children" in parent) { + const family = parent.children + .filter((child) => child.id === roots[0].id || breakpointRank(child) >= 0) + .filter((child) => child.visible !== false); + if (family.length > 1) { + roots.length = 0; + roots.push(...family); + } + } +} + +// A timed Smart Animate is a chain of frames: the target, after a timeout, +// becomes its destination, which after its own timeout becomes the next, and +// so on, often back to the start. The plugin reads those frames as it goes, +// with `getNodeByIdAsync`, and turns what changes between them into CSS +// keyframes. Those frames are top-level siblings the target's subtree does +// not contain, so they are gathered here as extra roots — with the same +// narrowness as the family above: only for a single requested root, so a +// multi-root request keeps its exact root list. Every node in the subtree is +// looked at, not only the root, because the animated thing is usually inside +// the screen; and every chain member's own chain is followed, until a frame +// repeats. +if (requestedRootIds.length === 1) { + const chainRootIds = []; + const seen = new Set(roots.map((root) => root.id)); + const pending = []; + const collectDestinations = (node) => { + if (!("reactions" in node) || !Array.isArray(node.reactions)) return; + for (const reaction of node.reactions) { + if (!reaction || !reaction.trigger || reaction.trigger.type !== "AFTER_TIMEOUT") continue; + for (const action of reaction.actions || []) { + if ( + action && + action.type === "NODE" && + action.transition && + action.transition.type === "SMART_ANIMATE" && + typeof action.destinationId === "string" && + !seen.has(action.destinationId) + ) { + seen.add(action.destinationId); + pending.push(action.destinationId); + } + } + } + if ("children" in node) for (const child of node.children) collectDestinations(child); + }; + for (const root of roots) collectDestinations(root); + while (pending.length > 0) { + const id = pending.shift(); + const node = await figma.getNodeByIdAsync(id); + if (!node || node.type === "DOCUMENT" || node.type === "PAGE") continue; + chainRootIds.push(node); + collectDestinations(node); + } + roots.push(...chainRootIds); +} +const envelopeRootId = p.nodeId; + +const manifest = [ + "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", "layoutAlign", "layoutGrow", "layoutMode", "layoutWrap", + "layoutPositioning", "layoutSizingHorizontal", "layoutSizingVertical", "letterSpacing", "lineHeight", "maxHeight", + "maxLines", "maxWidth", "minHeight", "minWidth", "name", "opacity", "overflowDirection", "overrides", + "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" +] +; +const textSegmentManifest = [ + "fontName", + "fontWeight", + "fontSize", + "textDecoration", + "textCase", + "lineHeight", + "letterSpacing", + "fills", + "textStyleId", + "fillStyleId", + "listOptions", + "indentation", + "hyperlink" +] +; +const pageOptions = p.snapshot; +const offset = Math.max(0, Math.floor(Number(pageOptions.offset) || 0)); +// Upper bound for one round's node payload, before the resources block and +// the envelope around it. The ceiling that matters is on the whole text +// result: the Figma MCP cuts one at 20,480 UTF-8 bytes and appends +// "// truncated to 20kb". Measured on the official server, 2026-09-07: a +// 20,480-character ASCII string came back whole and a 20,703-character one +// was cut at 20,480; a Hangul-heavy value of 12,003 characters and 24,003 +// bytes was cut at byte 20,480, mid-character, so the count is bytes and not +// characters; and 5,816 quote characters in a 20,357-character JSON array all +// survived, so JSON escaping is not counted either. The script measures +// exactly what is cut - `utf8ByteLength(JSON.stringify(envelope))` - and +// `integrity.utf8Bytes` matched the text that arrived on every one of 488 +// banked pages, so a 1 KiB margin under the cut is enough. There is no PNG +// fallback any more; a page has to survive as text. +// 위 측정은 공식 MCP 가 텍스트 결과를 자르는 지점에 관한 것이다. 로컬 소켓으로 +// 읽을 때는 그 자름이 없어 한 페이지에 전부 담을 수 있고, 그러면 이 스크립트를 +// 수십 번 다시 부르지 않아도 된다. 천장을 옵션으로 열어 두되, 값이 없으면 위에서 +// 잰 그대로 동작한다. +const envelopeCeiling = Math.max( + 8 * 1024, + Math.floor(Number(pageOptions.maxEnvelopeBytes) || 19 * 1024), +); +const payloadCeiling = pageOptions.maxEnvelopeBytes + ? envelopeCeiling - 1024 + : 18000; +const maxPayloadBytes = Math.min( + payloadCeiling, + Math.max(4096, Math.floor(Number(pageOptions.maxPayloadBytes) || 15000)), +); +const MAX_TEXT_ENVELOPE_BYTES = envelopeCeiling; + +// A field whose value equals its default carries no information the converter +// can't recover from the key being absent, so it is dropped from the envelope. +// Which fields qualify is NOT a judgement call: it is proven for every rule +// below by `devup-mcp-devup-ui/tests/default_omission_golden.rs`, which +// replays this exact omission over ten real screens (1,500+ nodes) and +// requires the generated TSX to stay byte-identical. Keep the two tables in +// sync with that test. + +// Figma reports an unbound style as `""`, and both readers of these fields +// already treat `""` and "absent" the same: `resources.rs::is_resource_id` +// rejects empty IDs, and `codegen/text.rs` looks the ID up in a token map +// where an empty key can never match. +const STYLE_ID_FIELDS = new Set([ + "backgroundStyleId", + "effectStyleId", + "fillStyleId", + "gridStyleId", + "strokeStyleId", + "textStyleId", +]); + +// `codegen/layout.rs` compares `view.value("maxWidth") != Some(&Value::Null)`, +// so for these two a present-null and an absent key take opposite branches. +// Their null must survive. +const NULL_SENSITIVE_FIELDS = new Set(["maxWidth", "maxHeight", "absoluteRenderBounds"]); + +// Deliberately absent from this table, each because the converter branches on +// the field's *presence* rather than its value: `opacity` (hover-variant +// detection), `visible` (component registration snapshot), `layoutPositioning` +// (compared against "AUTO"), and the per-corner radii / per-side stroke +// weights (read as a group by the shorthand builders). +const SCALAR_DEFAULTS = new Map([ + ["rotation", 0], + ["cornerRadius", 0], + ["isAsset", false], + ["isMask", false], + ["clipsContent", false], + ["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], + // A grid child spans one track unless it says otherwise. + ["gridColumnSpan", 1], + ["gridRowSpan", 1], +]); + +// Keys a styled text segment carries that the TEXT node itself does not, so +// they must survive even when the node has a single segment. +const SEGMENT_ONLY_KEYS = new Set([ + "start", + "end", + "characters", + "fontWeight", + "textStyleId", + "fillStyleId", + "listOptions", + "indentation", + "hyperlink", +]); + +function isOmittableDefault(value, name) { + if (value === null) return !NULL_SENSITIVE_FIELDS.has(name); + if (Array.isArray(value)) return value.length === 0; + if (typeof value === "object") return Object.keys(value).length === 0; + if (value === "" && STYLE_ID_FIELDS.has(name)) return true; + return SCALAR_DEFAULTS.has(name) && SCALAR_DEFAULTS.get(name) === value; +} + +// One serializer for both node fields and variable/style resources. Resources +// need the prototype chain walked (their data lives on accessors, not own +// keys) and a few structural keys skipped; node fields never do, because the +// manifest already names every property worth reading. +const RESOURCE_SKIPPED_KEYS = new Set(["parent", "children", "consumers"]); +function serialize(value, resource = false, seen = new WeakSet(), depth = 0) { + if (value === null || ["string", "number", "boolean"].includes(typeof value)) return value; + if (typeof value === "undefined") return { $undefined: true }; + if (typeof value === "bigint") return { $bigint: value.toString() }; + if (["function", "symbol"].includes(typeof value)) return { $unsupported: typeof value }; + if (depth > 12) return { $truncated: "max-depth" }; + if ( + typeof value === "object" && + "parent" in value && + typeof value.id === "string" && + typeof value.type === "string" + ) { + return { $nodeId: value.id, $nodeType: value.type }; + } + if (Array.isArray(value)) return value.map((item) => serialize(item, resource, seen, depth + 1)); + if (ArrayBuffer.isView(value)) { + return { $binary: value.constructor.name, byteLength: value.byteLength }; + } + if (value instanceof ArrayBuffer) return { $binary: "ArrayBuffer", byteLength: value.byteLength }; + if (seen.has(value)) return { $circular: true }; + seen.add(value); + + let keys; + if (resource) { + const names = new Set(Object.keys(value)); + let current = value; + while (current && current !== Object.prototype) { + for (const name of Object.getOwnPropertyNames(current)) names.add(name); + current = Object.getPrototypeOf(current); + } + keys = [...names].sort().filter((name) => !name.startsWith("_") && !RESOURCE_SKIPPED_KEYS.has(name)); + } else { + keys = Object.keys(value).sort(); + } + + const result = {}; + for (const key of keys) { + try { + const serialized = serialize(value[key], resource, seen, depth + 1); + if (!(serialized && serialized.$unsupported === "function")) result[key] = serialized; + } catch (error) { + result[key] = resource + ? { $error: "unavailable" } + : { $error: String(error && error.message ? error.message : error) }; + } + } + seen.delete(value); + return result; +} + +// Main-component identity is asynchronous under dynamic-page access. It is +// not a synchronous getter from the property manifest. Cache per collection +// so repacking the same page does not repeat its reads. +const devupComponentIdentity = new Map(); +async function devupReadComponentIdentity(node, fields, fieldErrors) { + if (node.type !== "INSTANCE" || typeof node.getMainComponentAsync !== "function") return; + if (!devupComponentIdentity.has(node.id)) { + devupComponentIdentity.set(node.id, (async () => { + try { + const main = await node.getMainComponentAsync(); + return { fields: { + mainComponentId: main ? main.id : null, + mainComponentName: main ? main.name : null, + mainComponentSetId: main && main.parent && main.parent.type === "COMPONENT_SET" ? main.parent.id : null, + } }; + } catch (error) { + return { error: String(error && error.message ? error.message : error) }; + } + })()); + } + const identity = await devupComponentIdentity.get(node.id); + if (identity.error) fieldErrors.mainComponentId = identity.error; + else Object.assign(fields, identity.fields); +} +; +async function snapshotNode(node) { + const fields = {}; + const fieldErrors = {}; + if (node.parent) fields.parentId = node.parent.id; + // Only a root needs this. Its parent lies outside the collected subtree, so + // the id alone says nothing, and the parent's type is what decides whether + // the root's width is a real constraint or merely the canvas the design was + // drawn on. Every other node's parent is collected and can be read directly, + // so recording it there would be repetition — and repeated across a whole + // screen it was enough to push the payload into chunked delivery. + // Only a frame sitting directly on a page, section or component set needs + // this: its parent is outside the collected subtree, so the id alone says + // nothing, and the type is what decides whether its width is a real + // constraint or the canvas it was drawn on. Keyed on the parent's type + // rather than on being a requested root, because a multi-root collection is + // split into batches with different root sets — the same node would then + // carry the field in one batch and not another, and merging rejects a node + // that arrives two different ways. + if ( + node.parent && + (node.parent.type === "PAGE" || + node.parent.type === "SECTION" || + node.parent.type === "COMPONENT_SET") + ) { + fields.parentType = node.parent.type; + // A screen's Section names the page component the plugin writes for it, + // `AboutPage` for a Section called `about`, and the Section itself is + // outside the collected subtree. + if (node.parent.type === "SECTION") fields.parentName = node.parent.name; + } + const childrenIds = "children" in node ? node.children.map((child) => child.id) : []; + if (childrenIds.length > 0) fields.childrenIds = childrenIds; + + // Only ever look at the checked-in manifest. No prototype-chain walk, no + // "extra" bucket: an unlisted Figma Plugin API property is never collected. + for (const name of manifest) { + let value; + // A variant's component properties are defined on its set, and Figma + // refuses to read them from the variant. + if ( + name === "componentPropertyDefinitions" && + node.type === "COMPONENT" && + node.parent && + node.parent.type === "COMPONENT_SET" + ) { + continue; + } + try { + if (!(name in node)) { + if (name === "overflowDirection" && ["FRAME", "COMPONENT", "INSTANCE", "COMPONENT_SET"].includes(node.type)) fields[name] = null; // Read attempted; absent in source. + continue; + } + value = node[name]; + if (typeof value === "function") continue; + const serialized = serialize(value); + if (name === "overflowDirection" && value == null) { fields[name] = null; continue; } + if (!isOmittableDefault(serialized, name)) fields[name] = serialized; + } catch (error) { + fieldErrors[name] = String(error && error.message ? error.message : error); + } + } + if (node.type === "TEXT" && typeof node.getStyledTextSegments === "function") { + try { + const segments = serialize(node.getStyledTextSegments(textSegmentManifest)); + // A single segment restates typography the node already carries at the + // top level, and `codegen/text.rs` reads the node field first and only + // falls back to the segment. Keep just the keys that exist nowhere else. + // Proven over 269 real single-segment text nodes by + // `devup-mcp-devup-ui/tests/default_omission_golden.rs`. + if (segments.length === 1) { + const only = segments[0]; + for (const key of Object.keys(only)) { + if (!SEGMENT_ONLY_KEYS.has(key)) delete only[key]; + } + } + if (segments.length > 0) fields.styledTextSegments = segments; + } catch (error) { + fieldErrors.styledTextSegments = String(error && error.message ? error.message : error); + } + } + await devupReadComponentIdentity(node, fields, fieldErrors); + // `extra` and `fieldErrors` are `#[serde(default)]` on the Rust `RawNode`, + // so an empty one is the same as an absent one on the wire. + const snapshotted = { id: node.id, type: node.type, fields }; + if (Object.keys(fieldErrors).length > 0) snapshotted.fieldErrors = fieldErrors; + return snapshotted; +} + +const allNodes = []; +const queue = [...roots]; +const visitedNodeIds = new Set(); +for (let index = 0; index < queue.length; index += 1) { + const node = queue[index]; + if (visitedNodeIds.has(node.id)) continue; + visitedNodeIds.add(node.id); + allNodes.push(node); + if ("children" in node) queue.push(...node.children); +} +if (offset >= allNodes.length && allNodes.length > 0) { + throw new Error("DEVUP_SNAPSHOT_RANGE_INVALID"); +} + +function utf8ByteLength(value) { + let bytes = 0; + for (let index = 0; index < value.length; index += 1) { + const code = value.charCodeAt(index); + if (code < 0x80) bytes += 1; + else if (code < 0x800) bytes += 2; + else if (code >= 0xd800 && code <= 0xdbff && index + 1 < value.length) { + bytes += 4; + index += 1; + } else bytes += 3; + } + return bytes; +} + +function jsonByteLength(value) { + return utf8ByteLength(JSON.stringify(value)); +} + +function styleTypeForField(field) { + if (field === "textStyleId") return "TEXT"; + if (["fillStyleId", "strokeStyleId", "backgroundStyleId"].includes(field)) return "PAINT"; + if (field === "effectStyleId") return "EFFECT"; + if (field === "gridStyleId") return "GRID"; + return null; +} + +function scanResources(value, variableIds, styleTypes) { + if (Array.isArray(value)) { + for (const child of value) scanResources(child, variableIds, styleTypes); + return; + } + if (!value || typeof value !== "object") return; + if ( + value.type === "VARIABLE_ALIAS" && + typeof value.id === "string" && + value.id && + value.id !== "figma.mixed" && + value.id !== "MIXED" + ) { + variableIds.add(value.id); + } + for (const [field, child] of Object.entries(value)) { + const styleType = styleTypeForField(field); + if ( + styleType && + typeof child === "string" && + child && + child !== "figma.mixed" && + child !== "MIXED" + ) { + if (!styleTypes.has(child)) styleTypes.set(child, styleType); + } + scanResources(child, variableIds, styleTypes); + } +} + +function aliasIdsOf(variable) { + return Object.values(variable.valuesByMode || {}) + .filter((value) => value && value.type === "VARIABLE_ALIAS" && typeof value.id === "string" && value.id) + .map((value) => value.id); +} + +// Resolves every variable/style the given page of nodes references. Only the +// nodes shipped in THIS page are scanned, so a page's resource block stays +// consistent with its own integrity counters; devup-mcp merges across pages. +async function collectResources(nodes) { + const variableIds = new Set(); + const styleTypes = new Map(); + scanResources(nodes, variableIds, styleTypes); + + const sortedVariableIds = [...variableIds].sort(); + const sortedStyles = [...styleTypes.entries()] + .map(([id, styleType]) => ({ id, styleType })) + .sort((left, right) => left.id.localeCompare(right.id)); + + async function lookupVariable(id) { + try { + const variable = await figma.variables.getVariableByIdAsync(id); + return variable + ? { + kind: "variable", + value: serialize(variable, true), + collectionId: variable.variableCollectionId, + aliasIds: aliasIdsOf(variable), + } + : { kind: "unresolved", value: { id, kind: "variable", reason: "notFoundOrUnavailable" } }; + } catch (error) { + return { kind: "unresolved", value: { id, kind: "variable", reason: "notFoundOrUnavailable", error: String((error && error.message) || error).slice(0, 300) } }; + } + } + const results = await Promise.all([ + ...sortedVariableIds.map(lookupVariable), + ...sortedStyles.map(async ({ id, styleType }) => { + try { + const style = await figma.getStyleByIdAsync(id); + if (!style) { + return { kind: "unresolved", value: { id, kind: "style", reason: "notFoundOrUnavailable" } }; + } + return { + kind: "style", + value: { + ...serialize(style, true), + styleType, + value: serialize( + styleType === "PAINT" + ? style.paints + : styleType === "EFFECT" + ? style.effects + : styleType === "GRID" + ? style.layoutGrids + : style, + true, + ), + }, + }; + } catch (error) { + return { kind: "unresolved", value: { id, kind: "style", reason: "notFoundOrUnavailable", error: String((error && error.message) || error).slice(0, 300) } }; + } + }), + ]); + + // A variable can alias another that no node in this page binds - a local + // spacing pointing at a library's primitive. The theme needs that variable + // to give the alias a value, so it is read too, and so is what it aliases. + // The counts above stay the page's own references. + const requestedVariableIds = new Set(sortedVariableIds); + let pendingAliasIds = results.flatMap((result) => result.aliasIds || []); + while (pendingAliasIds.length > 0) { + const batch = [...new Set(pendingAliasIds)] + .filter((id) => !requestedVariableIds.has(id)) + .sort(); + for (const id of batch) requestedVariableIds.add(id); + const fetched = await Promise.all(batch.map(lookupVariable)); + results.push(...fetched); + pendingAliasIds = fetched.flatMap((result) => result.aliasIds || []); + } + + const collectionIds = [...new Set(results + .filter((result) => result.kind === "variable" && result.collectionId) + .map((result) => result.collectionId))].sort(); + const collections = (await Promise.all(collectionIds.map(async (id) => { + try { + const collection = await figma.variables.getVariableCollectionByIdAsync(id); + return collection ? serialize(collection, true) : null; + } catch (_) { + return null; + } + }))).filter((collection) => collection !== null); + + const variables = results.filter((result) => result.kind === "variable").map((result) => result.value); + const styles = results.filter((result) => result.kind === "style").map((result) => result.value); + const unresolved = results.filter((result) => result.kind === "unresolved").map((result) => result.value); + + return { + collections, + variables, + styles, + usedRemoteVariables: variables.filter((variable) => variable.remote === true), + usedVariableIds: sortedVariableIds, + usedStyleIds: sortedStyles.map((style) => style.id), + localComplete: false, + usedRemoteComplete: unresolved.length === 0, + unresolved, + $variableRefCount: sortedVariableIds.length, + $styleRefCount: sortedStyles.length, + }; +} + +// Packs as many nodes as fit under `budget`, starting at `offset`. Same +// dynamic, byte-budget-driven pagination the legacy cursor snapshot uses. +async function packPage(budget) { + const pageNodes = []; + let payloadBytes = 2; + for (let index = offset; index < allNodes.length; index += 1) { + const snapshotted = await snapshotNode(allNodes[index]); + const nodeBytes = jsonByteLength(snapshotted) + (pageNodes.length ? 1 : 0); + if (pageNodes.length && payloadBytes + nodeBytes > budget) break; + pageNodes.push(snapshotted); + payloadBytes += nodeBytes; + } + return { pageNodes, packedBytes: payloadBytes }; +} + +function buildEnvelope(pageNodes, resources) { + const nextOffset = Math.min(allNodes.length, offset + pageNodes.length); + const { $variableRefCount, $styleRefCount, ...resourceBlock } = resources; + const nodes = [ + ...pageNodes, + { + id: "__DEVUP_SNAPSHOT_CURSOR__", + type: "DEVUP_INTERNAL", + // `offset` is what lets the Rust decoder tell a first page from a + // continuation page, which decides whether the root must be present + // here. All four fields are read by the shared `read_snapshot_cursor`. + fields: { + offset, + nextOffset, + complete: nextOffset >= allNodes.length, + totalNodes: allNodes.length, + }, + extra: {}, + fieldErrors: {}, + }, + ]; + const envelope = { + kind: "devupFastSnapshotEnvelope", + schemaVersion: 1, + source: { fileKey: figma.fileKey || "", rootId: envelopeRootId }, + snapshot: { + fileKey: figma.fileKey || "", + version: null, + rootIds: roots.map((root) => root.id), + nodes, + diagnostics: [], + }, + resources: resourceBlock, + // No `pagination` mirror: the __DEVUP_SNAPSHOT_CURSOR__ marker node is the + // single source of truth for page state, and duplicating it is exactly how + // the two copies drifted apart before. + integrity: { + nodeCount: nodes.length, + variableRefCount: $variableRefCount, + styleRefCount: $styleRefCount, + utf8Bytes: 0, + }, + }; + // Writing the byte count into the envelope changes the envelope's own + // length, so iterate to the fixed point. `utf8ByteLength` measures without + // building a throwaway byte array. + let bytes = 0; + for (let attempt = 0; attempt < 8; attempt += 1) { + bytes = utf8ByteLength(JSON.stringify(envelope)); + if (envelope.integrity.utf8Bytes === bytes) break; + envelope.integrity.utf8Bytes = bytes; + } + if (envelope.integrity.utf8Bytes !== utf8ByteLength(JSON.stringify(envelope))) { + throw new Error("DEVUP_ENVELOPE_LENGTH_UNSTABLE"); + } + return { envelope, bytes }; +} + +// The node budget alone can't bound the envelope: a page also carries every +// variable/style its nodes reference, and that block is only sized once the +// nodes are chosen. So pack, build, and if the whole envelope overshoots the +// text limit, take the overshoot off the node budget and try again. Fewer +// nodes can only reference fewer resources, so the next envelope is at least +// the overshoot smaller and lands under the limit. Halving the budget +// instead, as this did, gave up half a page over a few hundred bytes: of 340 +// banked pages, 143 ended where the next node would still have fit. +let nodeBudget = maxPayloadBytes - 1024; +let built = null; +for (let attempt = 0; attempt < 5; attempt += 1) { + const { pageNodes, packedBytes } = await packPage(nodeBudget); + if (pageNodes.length === 0) throw new Error("DEVUP_SNAPSHOT_RANGE_INVALID"); + const candidate = buildEnvelope(pageNodes, await collectResources(pageNodes)); + if (candidate.bytes <= MAX_TEXT_ENVELOPE_BYTES) { + built = candidate; + break; + } + if (pageNodes.length === 1) { + // A single node whose own resources blow the limit; no smaller page + // exists and there is no binary transport to fall back to. + throw new Error("DEVUP_ENVELOPE_TOO_LARGE"); + } + const overshoot = candidate.bytes - MAX_TEXT_ENVELOPE_BYTES; + nodeBudget = Math.max(1, Math.min(nodeBudget - 1, packedBytes - overshoot - 256)); +} +if (!built) throw new Error("DEVUP_ENVELOPE_TOO_LARGE"); +return built.envelope; + +} + +/** source: fast_theme.js */ +async function fastTheme(p) { +// Figma's own MCP runs scripts with hidden layers inside instances left out +// of `children`, `findAll` and `getNodeByIdAsync`; a plugin sees them. Set +// here so the direct path and the Devup Bridge read the same document. +figma.skipInvisibleInstanceChildren = false; +const MAX_ENVELOPE_BYTES = 8 * 1024 * 1024; +// The Figma MCP cuts a text result at 20,480 UTF-8 bytes; the measurement is +// written up in fast_snapshot.js. +const MAX_TEXT_ENVELOPE_BYTES = 19 * 1024; +// A file's resources rarely fit one answer - forty variables with their +// modes are twice this - so they are paged: everything the theme is made of +// (the resources, and the scan of which ids are used and what could not be +// resolved) is one list in a fixed order, and a page is a run of it from +// `offset` that says where the next one starts. The theme used to be one +// envelope or nothing, and a file whose theme did not fit fell back to a +// path that knows only local styles. The scan is paged like the rest +// because it is not small: a file with three hundred used ids carries +// 20 KB of them, a whole page on its own. +const pageOptions = p.theme; +const pageOffset = Math.max(0, Math.floor(Number(pageOptions.offset) || 0)); + +function propertyNames(value) { + const names = new Set(Object.keys(value)); + let current = value; + while (current && current !== Object.prototype) { + for (const name of Object.getOwnPropertyNames(current)) names.add(name); + current = Object.getPrototypeOf(current); + } + return [...names].sort(); +} + +function serialize(value, seen = new WeakSet(), depth = 0) { + if (value === null || ["string", "number", "boolean"].includes(typeof value)) return value; + if (typeof value === "undefined") return { $undefined: true }; + if (typeof value === "bigint") return { $bigint: value.toString() }; + if (["function", "symbol"].includes(typeof value)) return { $unsupported: typeof value }; + if (depth > 12) return { $truncated: "max-depth" }; + if ( + typeof value === "object" && + "parent" in value && + typeof value.id === "string" && + typeof value.type === "string" + ) { + return { $nodeId: value.id, $nodeType: value.type }; + } + if (Array.isArray(value)) return value.map((item) => serialize(item, seen, depth + 1)); + if (ArrayBuffer.isView(value)) { + return { $binary: value.constructor.name, byteLength: value.byteLength }; + } + if (value instanceof ArrayBuffer) return { $binary: "ArrayBuffer", byteLength: value.byteLength }; + if (seen.has(value)) return { $circular: true }; + seen.add(value); + const output = {}; + for (const name of propertyNames(value)) { + if (name.startsWith("_") || ["parent", "children", "consumers"].includes(name)) continue; + try { + const serialized = serialize(value[name], seen, depth + 1); + if (!(serialized && serialized.$unsupported === "function")) output[name] = serialized; + } catch (_) { + output[name] = { $error: "unavailable" }; + } + } + seen.delete(value); + return output; +} + +function styleTypeForField(field) { + if (field === "textStyleId") return "TEXT"; + if (["fillStyleId", "strokeStyleId", "backgroundStyleId"].includes(field)) return "PAINT"; + if (field === "effectStyleId") return "EFFECT"; + if (field === "gridStyleId") return "GRID"; + return null; +} + +const usedVariableIds = new Set(); +const usedStyleTypes = new Map(); +function scanResources(value, fieldName = "", seen = new WeakSet(), depth = 0) { + if (depth > 16 || value === null || typeof value !== "object") return; + if (seen.has(value)) return; + seen.add(value); + if (Array.isArray(value)) { + for (const child of value) scanResources(child, fieldName, seen, depth + 1); + return; + } + if ( + value.type === "VARIABLE_ALIAS" && + typeof value.id === "string" && + value.id && + value.id !== "figma.mixed" && + value.id !== "MIXED" + ) { + usedVariableIds.add(value.id); + } + for (const [field, child] of Object.entries(value)) { + const styleType = styleTypeForField(field); + if ( + styleType && + typeof child === "string" && + child && + child !== "figma.mixed" && + child !== "MIXED" + ) { + if (!usedStyleTypes.has(child)) usedStyleTypes.set(child, styleType); + } + scanResources(child, field || fieldName, seen, depth + 1); + } +} + +// Under Figma's own MCP a page is not loaded until something loads it, and +// `findAll` sees nothing on it: the devup Test file's second page answered +// 0 nodes there and 70 to the plugin, which loads every page when it starts. +for (const page of figma.root.children) await page.loadAsync(); +const documentNodes = [figma.root, ...figma.root.findAll(() => true)]; +for (const node of documentNodes) { + for (const field of [ + "boundVariables", + "fills", + "strokes", + "effects", + "layoutGrids", + "textStyleId", + "fillStyleId", + "strokeStyleId", + "backgroundStyleId", + "effectStyleId", + "gridStyleId", + ]) { + try { + scanResources({ [field]: node[field] }); + } catch (_) {} + } +} + +const [localCollections, localVariables, paints, texts, effects, grids] = await Promise.all([ + figma.variables.getLocalVariableCollectionsAsync(), + figma.variables.getLocalVariablesAsync(), + figma.getLocalPaintStylesAsync(), + figma.getLocalTextStylesAsync(), + figma.getLocalEffectStylesAsync(), + figma.getLocalGridStylesAsync(), +]); +const localVariableIds = new Set(localVariables.map((variable) => variable.id)); +const localStyleIds = new Set([...paints, ...texts, ...effects, ...grids].map((style) => style.id)); + +const unresolved = []; +async function remoteVariable(id) { + try { + const variable = await figma.variables.getVariableByIdAsync(id); + return variable || null; + } catch (error) { + unresolved.push({ id, kind: "variable", reason: "notFoundOrUnavailable", error: String((error && error.message) || error).slice(0, 300) }); + return null; + } +} +// A variable can alias a library variable no node binds - a local spacing +// that points at a library's primitive. The theme needs that variable to give +// the alias a value, so it is read too, and so is whatever it aliases. +function aliasTargets(variable) { + return Object.values(variable.valuesByMode || {}) + .filter((value) => value && value.type === "VARIABLE_ALIAS" && typeof value.id === "string" && value.id) + .map((value) => value.id); +} +const remoteStyleJobs = [...usedStyleTypes.entries()] + .filter(([id]) => !localStyleIds.has(id)) + .sort(([left], [right]) => left.localeCompare(right)) + .map(async ([id, styleType]) => { + try { + const style = await figma.getStyleByIdAsync(id); + return style ? { style, styleType } : null; + } catch (error) { + unresolved.push({ id, kind: "style", reason: "notFoundOrUnavailable", error: String((error && error.message) || error).slice(0, 300) }); + return null; + } + }); +const requestedRemoteIds = new Set(); +const remoteVariables = []; +let pendingRemoteIds = [...usedVariableIds, ...localVariables.flatMap(aliasTargets)]; +while (pendingRemoteIds.length > 0) { + const batch = [...new Set(pendingRemoteIds)] + .filter((id) => !localVariableIds.has(id) && !requestedRemoteIds.has(id)) + .sort(); + for (const id of batch) requestedRemoteIds.add(id); + const fetched = (await Promise.all(batch.map(remoteVariable))).filter(Boolean); + remoteVariables.push(...fetched); + pendingRemoteIds = fetched.flatMap(aliasTargets); +} +const remoteStyles = (await Promise.all(remoteStyleJobs)).filter(Boolean); +for (const id of requestedRemoteIds) { + if (!remoteVariables.some((variable) => variable.id === id) && !unresolved.some((item) => item.id === id)) { + unresolved.push({ id, kind: "variable", reason: "notFoundOrUnavailable" }); + } +} +for (const [id] of [...usedStyleTypes.entries()].filter(([id]) => !localStyleIds.has(id))) { + if (!remoteStyles.some((item) => item.style.id === id) && !unresolved.some((item) => item.id === id)) { + unresolved.push({ id, kind: "style", reason: "notFoundOrUnavailable" }); + } +} + +const remoteCollectionIds = [...new Set(remoteVariables.map((variable) => variable.variableCollectionId))] + .filter((id) => !localCollections.some((collection) => collection.id === id)) + .sort(); +const remoteCollections = ( + await Promise.all( + remoteCollectionIds.map(async (id) => { + try { + return await figma.variables.getVariableCollectionByIdAsync(id); + } catch (_) { + return null; + } + }), + ) +).filter(Boolean); + +function serializeStyle(style, styleType) { + return { + ...serialize(style), + styleType, + value: serialize( + styleType === "PAINT" + ? style.paints + : styleType === "EFFECT" + ? style.effects + : styleType === "GRID" + ? style.layoutGrids + : style, + ), + }; +} + +const styles = [ + ...paints.map((style) => serializeStyle(style, "PAINT")), + ...texts.map((style) => serializeStyle(style, "TEXT")), + ...effects.map((style) => serializeStyle(style, "EFFECT")), + ...grids.map((style) => serializeStyle(style, "GRID")), + ...remoteStyles.map(({ style, styleType }) => serializeStyle(style, styleType)), +].sort((left, right) => left.id.localeCompare(right.id)); +const variables = [...localVariables, ...remoteVariables] + .map((variable) => serialize(variable)) + .sort((left, right) => left.id.localeCompare(right.id)); +const collections = [...localCollections, ...remoteCollections] + .map((collection) => serialize(collection)) + .sort((left, right) => left.id.localeCompare(right.id)); +unresolved.sort((left, right) => left.kind.localeCompare(right.kind) || left.id.localeCompare(right.id)); + +function utf8Encode(value) { + const bytes = []; + for (let index = 0; index < value.length; index += 1) { + let codePoint = value.charCodeAt(index); + if (codePoint >= 0xd800 && codePoint <= 0xdbff) { + const next = index + 1 < value.length ? value.charCodeAt(index + 1) : 0; + if (next >= 0xdc00 && next <= 0xdfff) { + codePoint = 0x10000 + ((codePoint - 0xd800) << 10) + (next - 0xdc00); + index += 1; + } else { + codePoint = 0xfffd; + } + } else if (codePoint >= 0xdc00 && codePoint <= 0xdfff) { + codePoint = 0xfffd; + } + if (codePoint < 0x80) bytes.push(codePoint); + else if (codePoint < 0x800) bytes.push(0xc0 | (codePoint >> 6), 0x80 | (codePoint & 0x3f)); + else if (codePoint < 0x10000) { + bytes.push(0xe0 | (codePoint >> 12), 0x80 | ((codePoint >> 6) & 0x3f), 0x80 | (codePoint & 0x3f)); + } else { + bytes.push( + 0xf0 | (codePoint >> 18), + 0x80 | ((codePoint >> 12) & 0x3f), + 0x80 | ((codePoint >> 6) & 0x3f), + 0x80 | (codePoint & 0x3f), + ); + } + } + return new Uint8Array(bytes); +} + +// Everything in one order: collections, variables, styles, then the scan - +// the used variable ids, the used style ids, the unresolved. A page is a run +// of this list. +const items = [ + ...collections.map((value) => ({ kind: "collection", value })), + ...variables.map((value) => ({ kind: "variable", value })), + ...styles.map((value) => ({ kind: "style", value })), + ...[...usedVariableIds].sort().map((value) => ({ kind: "usedVariableId", value })), + ...[...usedStyleTypes.keys()].sort().map((value) => ({ kind: "usedStyleId", value })), + ...unresolved.map((value) => ({ kind: "unresolved", value })), +]; +if (pageOffset > items.length) throw new Error("DEVUP_SNAPSHOT_RANGE_INVALID"); + +function buildEnvelope(pageItems, nextOffset) { + const of = (kind) => pageItems.filter((item) => item.kind === kind).map((item) => item.value); + const pageCollections = of("collection"); + const pageVariables = of("variable"); + const pageStyles = of("style"); + const pageUnresolved = of("unresolved"); + const envelope = { + kind: "devupFastThemeEnvelope", + schemaVersion: 1, + source: { fileKey: figma.fileKey || "", version: null }, + resources: { + collections: pageCollections, + variables: pageVariables, + styles: pageStyles, + // Derived by the collector from `variables` once the pages are merged; + // listing a page's remote variables twice halved what a page held. + usedRemoteVariables: [], + usedVariableIds: of("usedVariableId"), + usedStyleIds: of("usedStyleId"), + localComplete: true, + usedRemoteComplete: unresolved.length === 0, + unresolved: pageUnresolved, + }, + // Read by the Rust decoder: a page is one of several when `complete` is + // false, and the next one is asked for from `nextOffset`. + page: { + offset: pageOffset, + nextOffset, + complete: nextOffset >= items.length, + totalItems: items.length, + }, + integrity: { + collectionCount: pageCollections.length, + variableCount: pageVariables.length, + styleCount: pageStyles.length, + unresolvedCount: pageUnresolved.length, + utf8Bytes: 0, + }, + }; + let envelopeBytes = new Uint8Array(); + for (let attempt = 0; attempt < 8; attempt += 1) { + envelopeBytes = utf8Encode(JSON.stringify(envelope)); + if (envelope.integrity.utf8Bytes === envelopeBytes.length) break; + envelope.integrity.utf8Bytes = envelopeBytes.length; + } + envelopeBytes = utf8Encode(JSON.stringify(envelope)); + if (envelope.integrity.utf8Bytes !== envelopeBytes.length) { + throw new Error("DEVUP_ENVELOPE_LENGTH_UNSTABLE"); + } + return { envelope, bytes: envelopeBytes.length }; +} + +// Pack from the offset by the items' own sizes until the budget is spent, +// then build the page; the envelope around the items - and the remote +// variables a page lists twice - is only sized once built, so if the whole +// overshoots, take the overshoot off the budget and pack again. Fewer items +// can only make a smaller envelope, so this converges. One item alone that +// does not fit cannot be split, and is reported rather than cut. +const itemBytes = items.map((item) => utf8Encode(JSON.stringify(item.value)).length + 1); +function packFrom(budget) { + const pageItems = []; + let packed = 0; + for (let index = pageOffset; index < items.length; index += 1) { + if (pageItems.length > 0 && packed + itemBytes[index] > budget) break; + pageItems.push(items[index]); + packed += itemBytes[index]; + } + return { pageItems, packed }; +} +let budget = MAX_TEXT_ENVELOPE_BYTES - 1024; +let built = null; +for (let attempt = 0; attempt < 6; attempt += 1) { + const { pageItems, packed } = packFrom(budget); + const candidate = buildEnvelope(pageItems, pageOffset + pageItems.length); + if (candidate.bytes <= MAX_TEXT_ENVELOPE_BYTES) { + built = candidate; + break; + } + if (pageItems.length <= 1) break; + budget = Math.max(1, Math.min(budget - 1, packed - (candidate.bytes - MAX_TEXT_ENVELOPE_BYTES) - 256)); +} +if (built === null) throw new Error("DEVUP_ENVELOPE_TOO_LARGE"); +if (built.bytes > MAX_ENVELOPE_BYTES) throw new Error("DEVUP_ENVELOPE_TOO_LARGE"); +return built.envelope; + +} + +/** source: large_value.js */ +async function largeValue(p) { +// Figma's own MCP runs scripts with hidden layers inside instances left out +// of `children`, `findAll` and `getNodeByIdAsync`; a plugin sees them. Set +// here so the direct path and the Devup Bridge read the same document. +figma.skipInvisibleInstanceChildren = false; +const options = p.largeValue; +// Read-only recovery. Never clone, detach, crop a parent or manufacture a +// substitute: the returned geometry must belong to this exact subtree. +async function devupLookupAssetNode(id) { + try { + const node = await figma.getNodeByIdAsync(id); + if (node) return node; + } catch (error) { + if (!id.startsWith("I") || !id.includes(";")) throw error; + try { return await walk(); } + catch (fallback) { throw new Error(`${error.message || error}; nested lookup fallback: ${fallback.message || fallback}`); } + } + if (!id.startsWith("I") || !id.includes(";")) return null; + return walk(); + + async function walk() { + const root = await figma.getNodeByIdAsync(id.slice(1).split(";")[0]); + const pending = root ? [root] : []; + const seen = new Set(); + while (pending.length) { + const node = pending.pop(); + if (node.id === id) return node; + if (seen.has(node.id)) continue; + seen.add(node.id); + if (seen.size > 10000) throw new Error("nested lookup exceeds 10000 nodes"); + if (Array.isArray(node.children)) pending.push(...node.children); + } + throw new Error(`nested node ${id} not found in its instance`); + } +} + +function devupGeometrySvg(root) { + const escape = value => String(value).replace(/[&<>"']/g, c => ({"&":"&","<":"<",">":">",'"':""","'":"'"}[c])); + const finite = value => typeof value === "number" && Number.isFinite(value); + const fail = (node, reason) => { throw new Error(`geometry fallback unsupported: ${node.type || "unknown"} ${node.id || ""}: ${reason}`); }; + let geometryCount = 0, visited = 0; + function paintPaths(node, paths, paints) { + if ((paints !== undefined && !Array.isArray(paints)) || (paints === undefined && Array.isArray(paths) && paths.length)) fail(node, "unresolved paint requires native export"); + const visible = Array.isArray(paints) ? paints.filter(p => p.visible !== false) : []; + if (!visible.length) return ""; + if (!Array.isArray(paths) || !paths.length) fail(node, "paint has no rendered geometry"); + return visible.map(paint => { + if (paint.type !== "SOLID" || (paint.blendMode && paint.blendMode !== "NORMAL")) fail(node, "paint type or blending requires native export"); + const color = paint.color; + if (!color || ![color.r, color.g, color.b].every(c => finite(c) && c >= 0 && c <= 1)) fail(node, "invalid solid color"); + const opacity = paint.opacity === undefined ? 1 : paint.opacity; + if (!finite(opacity) || opacity < 0 || opacity > 1) fail(node, "invalid paint opacity"); + // Paths of one winding rule are painted together, preserving holes and + // overlap within the rendered geometry. Preserve floating-point colors. + return ["NONZERO", "EVENODD"].map(rule => { + const matching = paths.filter(path => path.windingRule === rule); + if (!matching.length) return ""; + for (const path of matching) { + if (typeof path.data !== "string" || !path.data.trim()) fail(node, "empty path"); + geometryCount++; + } + return ``; + }).join(""); + }).join(""); + } + function render(node, isRoot) { + if (++visited > 10000) fail(node, "subtree exceeds 10000 nodes"); + if (node.isMask) fail(node, "mask requires native export"); + if (node.visible === false || node.opacity === 0) return ""; + if (node.clipsContent) fail(node, "clipping requires native export"); + if (Array.isArray(node.effects) && node.effects.some(e => e.visible !== false)) fail(node, "effect requires native export"); + if (node.blendMode && !["NORMAL", "PASS_THROUGH"].includes(node.blendMode)) fail(node, "layer blending requires native export"); + const opacity = node.opacity === undefined ? 1 : node.opacity; + if (!finite(opacity) || opacity < 0 || opacity > 1) fail(node, "invalid layer opacity"); + let transform = ""; + if (!isRoot) { + const t = node.relativeTransform; + if (!Array.isArray(t) || t.length !== 2 || !t.every(row => Array.isArray(row) && row.length === 3 && row.every(finite))) fail(node, "missing relative transform"); + transform = ` transform="matrix(${t[0][0]} ${t[1][0]} ${t[0][1]} ${t[1][1]} ${t[0][2]} ${t[1][2]})"`; + } + let body = ""; + if (["GROUP", "FRAME", "INSTANCE", "COMPONENT"].includes(node.type)) { + if ((Array.isArray(node.fills) && node.fills.some(p => p.visible !== false)) || (Array.isArray(node.strokes) && node.strokes.some(p => p.visible !== false))) fail(node, "container paint requires native export"); + if (!Array.isArray(node.children)) fail(node, "missing children"); + const children = node.itemReverseZIndex === true ? [...node.children].reverse() : node.children; + body = children.map(child => render(child, false)).join(""); + } else { + if (!["VECTOR", "BOOLEAN_OPERATION", "RECTANGLE", "ELLIPSE", "POLYGON", "STAR", "LINE"].includes(node.type)) fail(node, "node type requires native export"); + // Plugin API strokeGeometry is centered even for INSIDE/OUTSIDE strokes. + // Using it there would silently change the shape. + if (Array.isArray(node.strokes) && node.strokes.some(p => p.visible !== false) && node.strokeAlign !== "CENTER") fail(node, "non-center stroke alignment requires native export"); + if (Array.isArray(node.dashPattern) && node.dashPattern.length) fail(node, "dashed stroke requires native export"); + for (const paths of [node.fillGeometry, node.strokeGeometry]) { + if (paths !== undefined && (!Array.isArray(paths) || paths.some(p => !["NONZERO", "EVENODD"].includes(p.windingRule)))) fail(node, "unknown winding rule"); + } + // Baked geometry includes boolean operations and centered stroke outlines. + // Never approximate those using a bounding box. + body = paintPaths(node, node.fillGeometry, node.fills) + paintPaths(node, node.strokeGeometry, node.strokes); + } + return `${body}`; + } + const bounds = root.absoluteRenderBounds, t = root.absoluteTransform; + // Native exports include stroke extents. Convert rendered bounds back to + // local space only when this is exact; rotated/sheared bounds lose that info. + if (!bounds || ![bounds.x, bounds.y, bounds.width, bounds.height].every(finite) || bounds.width <= 0 || bounds.height <= 0) fail(root, "missing render bounds"); + if (!Array.isArray(t) || t.length !== 2 || !t.every(row => Array.isArray(row) && row.length === 3 && row.every(finite)) || t[0][1] !== 0 || t[1][0] !== 0 || t[0][0] <= 0 || t[1][1] <= 0) fail(root, "root rotation, reflection or shear requires native export"); + const x = (bounds.x - t[0][2]) / t[0][0], y = (bounds.y - t[1][2]) / t[1][1]; + const width = bounds.width / t[0][0], height = bounds.height / t[1][1]; + const body = render(root, true); + if (!geometryCount) fail(root, "no painted geometry"); + return `${body}`; +} + +async function devupRenderAsset(node, settings, nativeBudgetMs = 85000, observe = () => {}) { + const began = Date.now(); + let timer; + try { + const result = await Promise.race([ + node.exportAsync(settings), + new Promise((_, reject) => { timer = setTimeout(() => reject(new Error(`DEVUP_ASSET_TIMEOUT: native export exceeded ${nativeBudgetMs} ms`)), nativeBudgetMs); }), + ]); + observe("native-export", Date.now() - began); + return result; + } + catch (nativeError) { + observe("native-export", Date.now() - began, String(nativeError.message || nativeError).slice(0, 500)); + if (settings.format !== "SVG_STRING") throw nativeError; + const fallbackBegan = Date.now(); + try { + const svg = devupGeometrySvg(node); + observe("geometry-fallback", Date.now() - fallbackBegan); + return svg; + } + catch (fallbackError) { throw new Error(`native export: ${nativeError.message || nativeError}; ${fallbackError.message || fallbackError}`); } + } finally { clearTimeout(timer); } +} +; +const node = await devupLookupAssetNode(options.nodeId); +if (!node) throw new Error("DEVUP_NODE_NOT_FOUND"); + +const textSegmentManifest = [ + "fontName", + "fontWeight", + "fontSize", + "textDecoration", + "textCase", + "lineHeight", + "letterSpacing", + "fills", + "textStyleId", + "fillStyleId", + "listOptions", + "indentation", + "hyperlink" +] +; + +function serialize(value, seen = new WeakSet(), depth = 0) { + if (value === null || ["string", "number", "boolean"].includes(typeof value)) return value; + if (typeof value === "undefined") return { $undefined: true }; + if (typeof value === "bigint") return { $bigint: value.toString() }; + if (["function", "symbol"].includes(typeof value)) return { $unsupported: typeof value }; + if (depth > 12) return { $truncated: "max-depth" }; + if ( + typeof value === "object" && + "parent" in value && + typeof value.id === "string" && + typeof value.type === "string" + ) { + return { $nodeId: value.id, $nodeType: value.type }; + } + if (Array.isArray(value)) return value.map((item) => serialize(item, seen, depth + 1)); + if (ArrayBuffer.isView(value)) { + return { $binary: value.constructor.name, byteLength: value.byteLength }; + } + if (value instanceof ArrayBuffer) return { $binary: "ArrayBuffer", byteLength: value.byteLength }; + if (seen.has(value)) return { $circular: true }; + seen.add(value); + const result = {}; + for (const key of Object.keys(value).sort()) { + try { + const serialized = serialize(value[key], seen, depth + 1); + if (!(serialized && serialized.$unsupported === "function")) result[key] = serialized; + } catch (error) { + result[key] = { $error: String(error && error.message ? error.message : error) }; + } + } + seen.delete(value); + return result; +} + +function utf8Encode(value) { + const bytes = []; + for (let index = 0; index < value.length; index += 1) { + let codePoint = value.charCodeAt(index); + if (codePoint >= 0xd800 && codePoint <= 0xdbff) { + const next = index + 1 < value.length ? value.charCodeAt(index + 1) : 0; + if (next >= 0xdc00 && next <= 0xdfff) { + codePoint = 0x10000 + ((codePoint - 0xd800) << 10) + (next - 0xdc00); + index += 1; + } else codePoint = 0xfffd; + } else if (codePoint >= 0xdc00 && codePoint <= 0xdfff) codePoint = 0xfffd; + if (codePoint < 0x80) bytes.push(codePoint); + else if (codePoint < 0x800) { + bytes.push(0xc0 | (codePoint >> 6), 0x80 | (codePoint & 0x3f)); + } else if (codePoint < 0x10000) { + bytes.push( + 0xe0 | (codePoint >> 12), + 0x80 | ((codePoint >> 6) & 0x3f), + 0x80 | (codePoint & 0x3f), + ); + } else { + bytes.push( + 0xf0 | (codePoint >> 18), + 0x80 | ((codePoint >> 12) & 0x3f), + 0x80 | ((codePoint >> 6) & 0x3f), + 0x80 | (codePoint & 0x3f), + ); + } + } + return new Uint8Array(bytes); +} + +function sha256(bytes) { + const constants = [ + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, + 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, + 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, + 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, + 0x06ca6351, 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, + ]; + const length = bytes.length; + const paddedLength = Math.ceil((length + 9) / 64) * 64; + const padded = new Uint8Array(paddedLength); + padded.set(bytes); + padded[length] = 0x80; + const bitLength = length * 8; + for (let index = 0; index < 8; index += 1) { + padded[paddedLength - 1 - index] = Math.floor(bitLength / 2 ** (index * 8)) & 0xff; + } + const hash = [ + 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, + 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19, + ]; + const rotate = (value, bits) => (value >>> bits) | (value << (32 - bits)); + for (let offset = 0; offset < padded.length; offset += 64) { + const words = new Uint32Array(64); + for (let index = 0; index < 16; index += 1) { + const start = offset + index * 4; + words[index] = + (padded[start] << 24) | + (padded[start + 1] << 16) | + (padded[start + 2] << 8) | + padded[start + 3]; + } + for (let index = 16; index < 64; index += 1) { + const s0 = rotate(words[index - 15], 7) ^ rotate(words[index - 15], 18) ^ (words[index - 15] >>> 3); + const s1 = rotate(words[index - 2], 17) ^ rotate(words[index - 2], 19) ^ (words[index - 2] >>> 10); + words[index] = (words[index - 16] + s0 + words[index - 7] + s1) >>> 0; + } + let [a, b, c, d, e, f, g, h] = hash; + for (let index = 0; index < 64; index += 1) { + const s1 = rotate(e, 6) ^ rotate(e, 11) ^ rotate(e, 25); + const choice = (e & f) ^ (~e & g); + const temp1 = (h + s1 + choice + constants[index] + words[index]) >>> 0; + const s0 = rotate(a, 2) ^ rotate(a, 13) ^ rotate(a, 22); + const majority = (a & b) ^ (a & c) ^ (b & c); + const temp2 = (s0 + majority) >>> 0; + h = g; g = f; f = e; e = (d + temp1) >>> 0; + d = c; c = b; b = a; a = (temp1 + temp2) >>> 0; + } + for (const [index, value] of [a, b, c, d, e, f, g, h].entries()) { + hash[index] = (hash[index] + value) >>> 0; + } + } + return hash.map((value) => value.toString(16).padStart(8, "0")).join(""); +} + +function base64(bytes) { + const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + let output = ""; + for (let index = 0; index < bytes.length; index += 3) { + const first = bytes[index]; + const second = index + 1 < bytes.length ? bytes[index + 1] : 0; + const third = index + 2 < bytes.length ? bytes[index + 2] : 0; + output += alphabet[first >> 2]; + output += alphabet[((first & 3) << 4) | (second >> 4)]; + output += index + 1 < bytes.length ? alphabet[((second & 15) << 2) | (third >> 6)] : "="; + output += index + 2 < bytes.length ? alphabet[third & 63] : "="; + } + return output; +} + +let rawValue; +// The bytes a fragment is cut from. A node field is its JSON; the virtual +// field `$export:svg` is the node's SVG export as text, which the asset +// script announced when it was too large to carry in one answer. +let exportedBytes = null; +try { + if (options.field === "$export:svg") { + if (typeof node.exportAsync !== "function") throw new Error("unsupported"); + const svg = await devupRenderAsset(node, { format: "SVG_STRING" }, Math.max(1, (Number(options.timeoutMs) || 90000) - 5000)); + if (typeof svg !== "string") throw new Error("unsupported"); + exportedBytes = utf8Encode(svg); + } else if (typeof options.field === "string" && options.field.startsWith("$export:png")) { + // A PNG too large for one attachment, re-exported a fragment at a time. + // The scale rides on the field - `$export:png@2` - so the re-export is + // the same bytes the asset script announced, hash for hash. + if (typeof node.exportAsync !== "function") throw new Error("unsupported"); + const scale = Number(options.field.split("@")[1] || 2); + if (!Number.isFinite(scale) || scale <= 0 || scale > 4) throw new Error("invalid scale"); + const parts = options.field.split("@"); + let capture = node; + if (parts[2] === "visible") { + const seen = new Set(); + while (capture && capture.id !== parts[3] && !seen.has(capture) && seen.size < 64) { seen.add(capture); capture = capture.parent; } + if (!capture || capture.id !== parts[3] || ["PAGE", "DOCUMENT"].includes(capture.type)) throw new Error("invalid visible scope"); + } else if (parts.length > 2) throw new Error("invalid export field"); + const png = await capture.exportAsync({ format: "PNG", contentsOnly: true, useAbsoluteBounds: false, constraint: { type: "SCALE", value: scale } }); + exportedBytes = png instanceof Uint8Array ? png : new Uint8Array(png); + } else if ( + options.field === "styledTextSegments" && + node.type === "TEXT" && + typeof node.getStyledTextSegments === "function" + ) { + rawValue = node.getStyledTextSegments(textSegmentManifest); + } else if (options.field in node) { + rawValue = node[options.field]; + } else { + throw new Error("unsupported"); + } +} catch (error) { + return { + kind: "devupLargeValueUnsupported", + fileKey: figma.fileKey || "", + version: options.version, + nodeId: options.nodeId, + field: options.field, + byteLength: options.byteLength, + sha256: options.sha256, + errorCode: "DEVUP_FIELD_UNSUPPORTED_BY_UPSTREAM", + errorDetail: `${options.field} failed: ${error.message || error}`.slice(0, 500), + }; +} + +const bytes = exportedBytes === null ? utf8Encode(JSON.stringify(serialize(rawValue))) : exportedBytes; +const observedHash = sha256(bytes); +if (bytes.length !== options.byteLength || observedHash !== options.sha256) { + throw new Error("DEVUP_LARGE_VALUE_CHANGED"); +} +const offset = Math.max(0, Math.floor(Number(options.offset) || 0)); +const maxChunkBytes = Math.min(65536, Math.max(1, Math.floor(Number(options.maxChunkBytes) || 8192))); +if (offset >= bytes.length) throw new Error("DEVUP_LARGE_VALUE_RANGE_INVALID"); +const nextOffset = Math.min(bytes.length, offset + maxChunkBytes); +return { + kind: "devupLargeValueFragment", + fileKey: figma.fileKey || "", + version: options.version, + nodeId: options.nodeId, + field: options.field, + offset, + nextOffset, + byteLength: bytes.length, + sha256: observedHash, + dataBase64: base64(bytes.slice(offset, nextOffset)), + complete: nextOffset === bytes.length, +}; + +} + +/** source: measure_nodes.js */ +async function measureNodes(p) { +// A fixed read of explicit IDs. Never walk children or acquire resources. +const ids = p.rootIds; +if (!Array.isArray(ids) || ids.length < 1 || ids.length > 10 || + ids.some(id => typeof id !== 'string' || id.length > 128 || !/^(?:[0-9]+:[0-9]+|I[0-9]+:[0-9]+(?:;[0-9]+:[0-9]+)+)$/.test(id)) || + new Set(ids).size !== ids.length) throw new Error('DEVUP_INVALID_INPUT: nodeIds must be 1..10 distinct Figma IDs'); +const number = (node, key) => { + try { const value = node[key]; return typeof value === 'number' && Number.isFinite(value) ? value : null; } + catch { return null; } +}; +const label = (node, key) => { + try { const value = node[key]; return typeof value === 'string' && value.length <= 32 && /^[A-Z_]+$/.test(value) ? value : null; } + catch { return null; } +}; +const nodes = []; +for (const nodeId of ids) { + try { + const node = await figma.getNodeByIdAsync(nodeId); + if (!node || node.type === 'DOCUMENT' || node.type === 'PAGE') { + nodes.push({nodeId, status: 'unavailable', reason: node ? 'not-scene-node' : 'not-found'}); + continue; + } + const layout = { + width: number(node, 'width'), height: number(node, 'height'), + padding: {top: number(node, 'paddingTop'), right: number(node, 'paddingRight'), bottom: number(node, 'paddingBottom'), left: number(node, 'paddingLeft')}, + gap: number(node, 'itemSpacing'), layoutMode: label(node, 'layoutMode'), + align: {primary: label(node, 'primaryAxisAlignItems'), counter: label(node, 'counterAxisAlignItems')} + }; + const known = layout.width !== null && layout.height !== null; + nodes.push({nodeId, status: known ? 'measured' : 'partial', ...layout}); + } catch { + nodes.push({nodeId, status: 'unavailable', reason: 'read-failed'}); + } +} +return {kind: 'devup-measure', schemaVersion: 1, coordinateSpace: 'figma-logical', nodes, + partial: nodes.some(node => node.status !== 'measured'), + evidenceLimit: 'Null means unavailable or not applicable; no screenshot dimensions, descendants, theme, assets or code were read.'}; + +} + +/** source: metadata.js */ +async function metadata(p) { +// Figma's own MCP runs scripts with hidden layers inside instances left out +// of `children`, `findAll` and `getNodeByIdAsync`; a plugin sees them. Set +// here so the direct path and the Devup Bridge read the same document. +figma.skipInvisibleInstanceChildren = false; +// `get_metadata` 가 주던 노드 계층을 플러그인 API 로 그대로 만든다. +// +// 공식 경로는 XML 을 주고 이 경로는 JSON 을 주지만, 받는 쪽은 한 곳이다. +// `metadata.rs` 의 `find_metadata` 가 첫 분기에서 `MetadataDocument` 를 그대로 +// 역직렬화하고, XML 은 그 다음 분기에서 같은 구조체로 환원된다. 그래서 여기서 +// 내보낼 것은 그 구조체의 camelCase 표현 하나뿐이다 — 필드를 더 얹으면 두 경로가 +// 갈라진다. +// +// { fileKey, version, rootId, nodes: [{ id, type, name, childrenIds, descendantCount }] } +// +// 좌표와 크기는 넣지 않는다. 디코더가 읽지 않고, 레이아웃은 snapshot 이 맡는다. + +const root = await figma.getNodeByIdAsync(p.nodeId); +if (!root) throw new Error("DEVUP_NODE_NOT_FOUND"); + +// 다이나믹 페이지 문서에서는 자식이 아직 메모리에 없을 수 있다. search.js 와 같은 +// 방법으로 대상이 속한 페이지를 먼저 올린다. +let page = root; +while (page && page.type !== "PAGE") page = page.parent; +if (page && page.type === "PAGE") await figma.setCurrentPageAsync(page); + +const nodes = []; + +// 자손 수를 돌려주면서 목록을 채운다. 깊이가 아주 깊은 문서에서 호출 스택이 +// 터지지 않도록 명시적 스택으로 후위 순회한다. +function collect(start) { + const stack = [{ node: start, visited: false }]; + const counts = new Map(); + while (stack.length > 0) { + const frame = stack[stack.length - 1]; + const node = frame.node; + const children = "children" in node ? node.children : []; + if (!frame.visited) { + frame.visited = true; + for (let index = children.length - 1; index >= 0; index -= 1) { + stack.push({ node: children[index], visited: false }); + } + continue; + } + stack.pop(); + let descendantCount = 0; + const childrenIds = []; + for (const child of children) { + childrenIds.push(child.id); + descendantCount += 1 + (counts.get(child.id) || 0); + } + counts.set(node.id, descendantCount); + nodes.push({ + id: node.id, + type: node.type, + name: node.name, + childrenIds, + descendantCount, + }); + } +} + +collect(root); + +return { + fileKey: figma.fileKey || "", + version: null, + rootId: root.id, + nodes, +}; + +} + +/** source: page_catalog.js */ +async function pageCatalog(p) { +const pages = figma.root.children; +return { + fileKey: figma.fileKey || "", + version: null, + rootIds: pages.map((page) => page.id), + nodes: pages.map((page) => ({ + id: page.id, + type: page.type, + fields: { + name: page.name, + parentId: null, + childrenIds: [], + }, + extra: {}, + fieldErrors: {}, + })), + diagnostics: [], +}; + +} + +/** source: reference_png.js */ +async function referencePng(p) { +// referencePng over the bridge. The direct path asks Figma's get_screenshot, +// which draws the node in its ancestors' context - their paints, clipping, +// opacity, blend modes and blur - with every other layer left out, across the +// node's render bounds (measured on the devup Test file's cases). Exports are +// at the node's size with the longer edge held to 8192px, as the collector +// holds get_screenshot's maxDimension, in sRGB. +// - No other layer draws into those bounds: the canvas export (contentsOnly: +// false) is that picture; devup-mcp flattens it onto the page background. +// - One does: a plugin cannot leave it out of an export, so this exports the +// node alone and describes its ancestors - and sends the images their fills +// use, and the masks that clip the chain - for devup-mcp to compose. +// - What devup-mcp cannot compose is named in `approximate`, over the canvas +// export; the read then goes to get_screenshot when the direct path can. +// An answer too large for one bridge answer is held here and read in pieces +// (referencePngChunk). A held answer stays until it is read or two minutes +// pass; while held answers fill the budget this answers BUSY, and devup-mcp +// asks again, rather than drop one another request is still reading. +const nodeId = p.nodeId; +const PIECE_THRESHOLD = 6 * 1024 * 1024; +const ANSWER_LIMIT = 90 * 1024 * 1024; // under the 96 MiB devup-mcp reads back +const HELD_BUDGET = 192 * 1024 * 1024; +const HELD_FOR = 2 * 60 * 1000; +if (!globalThis.__devupReferencePngAnswers) globalThis.__devupReferencePngAnswers = new Map(); +const store = globalThis.__devupReferencePngAnswers; +let heldLength = 0; +for (const [id, entry] of store) { + if (Date.now() - entry.at > HELD_FOR) store.delete(id); + else heldLength += entry.text.length; +} +if (heldLength >= HELD_BUDGET) { + throw new Error(`DEVUP_REFERENCE_PNG_BUSY ${heldLength} characters are held for other reads`); +} +const node = await figma.getNodeByIdAsync(nodeId); +if (!node || typeof node.exportAsync !== "function") { + throw new Error(`DEVUP_NODE_NOT_FOUND ${nodeId}`); +} +const area = node.absoluteRenderBounds || node.absoluteBoundingBox; +const longest = Math.max(area.width, area.height); +const scale = longest > 8192 ? 8192 / longest : 1; +const settings = (contentsOnly, own) => ({ + format: "PNG", + constraint: own && scale < 1 + ? { type: area.width >= area.height ? "WIDTH" : "HEIGHT", value: 8192 } + : { type: "SCALE", value: scale }, + contentsOnly, + useAbsoluteBounds: false, + colorProfile: "SRGB", +}); +const shown = (list) => (Array.isArray(list) ? list : []).filter((item) => item && item.visible !== false); +const overlaps = (a, b) => !!a && !!b + && a.x < b.x + b.width && b.x < a.x + a.width && a.y < b.y + b.height && b.y < a.y + a.height; +const covers = (box, inset) => !!box + && box.x + inset <= area.x && box.y + inset <= area.y + && box.x + box.width - inset >= area.x + area.width && box.y + box.height - inset >= area.y + area.height; +const label = (layer) => `${layer.type} ${JSON.stringify(layer.name)}`; +const plain = (value) => JSON.parse(JSON.stringify(value === figma.mixed ? null : value)); +const pngSize = (bytes) => [16, 20].map((at) => ((bytes[at] << 24) | (bytes[at + 1] << 16) | (bytes[at + 2] << 8) | bytes[at + 3]) >>> 0); +const normal = (layer) => (layer.opacity === undefined || layer.opacity === 1) + && (!layer.blendMode || ["PASS_THROUGH", "NORMAL"].includes(layer.blendMode)); + +const chain = []; +let page = node.parent; +while (page && page.type !== "PAGE") { + chain.unshift(page); + page = page.parent; +} +const pageColour = [255, 255, 255]; +for (const paint of shown(page && page.backgrounds)) { + if (paint.type !== "SOLID") continue; + const alpha = paint.opacity === undefined ? 1 : paint.opacity; + ["r", "g", "b"].forEach((channel, index) => { + pageColour[index] = alpha * paint.color[channel] * 255 + (1 - alpha) * pageColour[index]; + }); +} + +// What else draws into the area: a visible layer outside the node's chain with +// a paint, a stroke, an effect or text there, itself or below it. +let visited = 0; +const draws = (layer) => { + if (layer.visible === false || layer.isMask || !overlaps(layer.absoluteRenderBounds, area)) return false; + visited += 1; + if (visited > 5000) return true; + if (layer.type === "TEXT" || shown(layer.fills).length || shown(layer.strokes).length || shown(layer.effects).length) return true; + return "children" in layer && layer.children.some(draws); +}; +// An opaque plain paint over the whole area hides whatever lies under it. +const hidesUnder = (layer) => normal(layer) && covers(layer.absoluteBoundingBox, 0) + && Math.abs(layer.absoluteTransform[0][1]) < 1e-9 && Math.abs(layer.absoluteTransform[1][0]) < 1e-9 + && Math.max(0, ...[layer.topLeftRadius, layer.topRightRadius, layer.bottomRightRadius, layer.bottomLeftRadius] + .filter((radius) => typeof radius === "number")) === 0 + && shown(layer.fills).some((paint) => (paint.blendMode || "NORMAL") === "NORMAL" + && (paint.opacity === undefined || paint.opacity === 1) + && (paint.type === "SOLID" || (paint.type.startsWith("GRADIENT_") && paint.gradientStops.every((stop) => stop.color.a === 1)))); +const members = [...chain, node]; +const intruders = []; +const below = []; +const masks = new Map(); +for (let depth = 0; depth < members.length; depth += 1) { + const member = members[depth]; + const siblings = member.parent.children; + const index = siblings.indexOf(member); + const underneath = members.slice(depth, members.length - 1); + const hidden = underneath.some((layer, at) => hidesUnder(layer) && underneath.slice(0, at).every(normal)); + siblings.forEach((other, at) => { + if (other === member) return; + if (other.isMask && other.visible !== false && at < index) masks.set(member, other); + if (!draws(other) || (at < index && hidden)) return; + intruders.push(label(other)); + if (at < index) below.push(other); + }); +} + +const pageBackdrop = pageColour.map(Math.round); +const canvasAnswer = async (extra) => { + const bytes = await node.exportAsync(settings(false, true)); + return { kind: "devupReferencePng", nodeId, mimeType: "image/png", byteLength: bytes.length, data: figma.base64Encode(bytes), backdrop: pageBackdrop, ...extra }; +}; + +// The canvas picture stands in for what devup-mcp cannot compose, and says why. +const approximation = (reasons, reaching) => { + const unique = [...new Set(reasons)]; + return { + approximate: unique.join("; "), + note: `Drawn as the canvas shows this area: devup-mcp cannot yet compose what get_screenshot draws here (${unique.slice(0, 3).join("; ")}). ` + + `Layers reaching into it may show here: ${reaching}; get_screenshot leaves other layers out.`, + }; +}; + +const held = (answer) => { + // Escaped to ASCII, so an offset into it counts the same here (UTF-16 units) + // and in devup-mcp (bytes): layer names in a note are often not ASCII. + const text = JSON.stringify(answer) + .replace(/[\u0080-\uffff]/g, (unit) => `\\u${unit.charCodeAt(0).toString(16).padStart(4, "0")}`); + if (text.length > ANSWER_LIMIT) { + if (answer.compose) { + // What it takes to compose does not fit; the canvas picture alone may. + const { compose, reaching, ...canvas } = answer; + return held({ ...canvas, ...approximation([`the answer would be ${text.length} characters`], reaching) }); + } + throw new Error(`DEVUP_REFERENCE_PNG_TOO_LARGE ${text.length} characters, over the ${ANSWER_LIMIT} one answer is read back up to`); + } + if (text.length <= PIECE_THRESHOLD) return answer; + const id = `${nodeId}@${Date.now()}.${Math.random().toString(36).slice(2, 10)}`; + store.set(id, { text, at: Date.now() }); + return { kind: "devupReferencePngChunked", nodeId, id, length: text.length }; +}; + +if (intruders.length === 0) return held(await canvasAnswer({})); +// Inside a mask's own layers the node only shapes the mask: Figma draws none of +// it, and get_screenshot shows what lies under it. +const shapesMask = members.some((member) => member.isMask); + +const approximate = []; +const images = new Set(); +const radii = (layer) => [layer.topLeftRadius, layer.topRightRadius, layer.bottomRightRadius, layer.bottomLeftRadius] + .map((radius) => (typeof radius === "number" ? radius : 0)); +const strokeReaches = (layer) => { + const weights = shown(layer.strokes).length === 0 ? [] : [layer.strokeWeight, layer.strokeTopWeight, layer.strokeRightWeight, + layer.strokeBottomWeight, layer.strokeLeftWeight].filter((weight) => typeof weight === "number" && weight > 0); + if (weights.length === 0) return false; + const weight = Math.max(...weights); + const inner = layer.strokeAlign === "OUTSIDE" ? 0 : layer.strokeAlign === "CENTER" ? weight / 2 : weight; + const outer = layer.strokeAlign === "INSIDE" ? 0 : layer.strokeAlign === "CENTER" ? weight / 2 : weight; + const box = layer.absoluteBoundingBox; + const grown = box && { x: box.x - outer, y: box.y - outer, width: box.width + 2 * outer, height: box.height + 2 * outer }; + return overlaps(grown, area) && !covers(box, inner); +}; +const describe = (layer, own) => { + const owner = label(layer); + for (const paint of shown(layer.fills)) { + if (paint.type === "IMAGE") { + if (paint.rotation || shown([paint.filters]).some((filters) => Object.values(filters).some((value) => value))) { + approximate.push(`${owner} has an image fill with a rotation or filters`); + } + images.add(paint.imageHash); + } else if (paint.type !== "SOLID" && !paint.type.startsWith("GRADIENT_")) { + approximate.push(`${owner} is painted with ${paint.type}`); + } + } + for (const effect of shown(layer.effects)) { + if (!["LAYER_BLUR", "DROP_SHADOW", "INNER_SHADOW", "BACKGROUND_BLUR"].includes(effect.type)) { + approximate.push(`${owner} has a ${effect.type}`); + } + } + // A rectangle's stroke devup-mcp draws: solid or gradient, undashed, and + // mitred where it reaches past the corners. + const strokes = shown(layer.strokes); + if (strokeReaches(layer)) { + const rectangular = ["FRAME", "RECTANGLE", "COMPONENT", "COMPONENT_SET", "INSTANCE", "SECTION"].includes(layer.type); + const painted = strokes.every((paint) => paint.type === "SOLID" || paint.type.startsWith("GRADIENT_")); + const dashed = Array.isArray(layer.dashPattern) && layer.dashPattern.length > 0; + const mitred = layer.strokeAlign === "INSIDE" || !layer.strokeJoin || layer.strokeJoin === "MITER"; + if (!(rectangular && painted && !dashed && mitred)) approximate.push(`${owner}'s stroke reaches it`); + } + const weight = (side) => (typeof layer[side] === "number" ? layer[side] : typeof layer.strokeWeight === "number" ? layer.strokeWeight : 0); + return { + transform: plain(layer.absoluteTransform), width: layer.width, height: layer.height, radii: radii(layer), + fills: plain(shown(layer.fills)), opacity: layer.opacity === undefined ? 1 : layer.opacity, + blendMode: layer.blendMode || null, clips: !!layer.clipsContent, effects: plain(shown(layer.effects)), + strokes: plain(strokes), strokeAlign: layer.strokeAlign || null, + strokeWeights: ["strokeTopWeight", "strokeRightWeight", "strokeBottomWeight", "strokeLeftWeight"].map(weight), + }; +}; +const picture = async (layer) => { + const bytes = await layer.exportAsync(settings(true, layer === node)); + // A mask draws nothing itself, so Figma gives it no render bounds; its + // export covers its box. + return { data: figma.base64Encode(bytes), area: layer.absoluteRenderBounds || layer.absoluteBoundingBox, blendMode: layer.blendMode || null, size: pngSize(bytes) }; +}; +const maskOf = async (member) => { + const mask = masks.get(member); + if (!mask) return null; + // A mask draws nothing itself: Figma gives it no render bounds, and an export + // by them is empty. Exported by its box, it shows its shape. + const bytes = await mask.exportAsync({ ...settings(true, false), useAbsoluteBounds: true }); + return { data: figma.base64Encode(bytes), area: mask.absoluteBoundingBox, luminance: mask.maskType === "LUMINANCE" }; +}; + +const layers = []; +const content = {}; +const imageData = {}; +let size; +// A part's export or an image read can fail where the canvas export does not +// (Figma refuses data it has not loaded while the file is out of sight); the +// answer is then the canvas one, never a refusal. +try { + for (const layer of chain) { + const described = describe(layer, false); + const mask = await maskOf(layer); + if (mask) described.mask = mask; + layers.push(described); + } + // A background blur in the node's own export samples the layers behind it, + // which get_screenshot leaves out; draw the node from its own paints and its + // children's exports instead. + const blursBehind = (layer) => shown(layer.effects).some((effect) => effect.type === "BACKGROUND_BLUR"); + const sampled = below.length > 0 && (blursBehind(node) || ("findOne" in node && !!node.findOne(blursBehind))); + if (sampled) { + const childBlur = "children" in node && node.children.some((child) => blursBehind(child) || ("findOne" in child && !!child.findOne(blursBehind))); + if (!("children" in node) || childBlur || shown(node.strokes).length) { + approximate.push(`${label(node)} blurs what lies behind it`); + } else { + content.own = describe(node, true); + content.children = []; + for (const child of node.children) { + if (child.visible === false || child.isMask || !child.absoluteRenderBounds) continue; + const exported = await picture(child); + content.children.push({ data: exported.data, area: exported.area, blendMode: exported.blendMode }); + } + size = [Math.ceil(area.width * scale - 1e-6), Math.ceil(area.height * scale - 1e-6)]; + } + } else if (shapesMask) { + size = [Math.ceil(area.width * scale - 1e-6), Math.ceil(area.height * scale - 1e-6)]; + } else { + const exported = await picture(node); + content.picture = { data: exported.data, area: exported.area, blendMode: exported.blendMode }; + size = exported.size; + } + const mask = await maskOf(node); + if (mask) content.mask = mask; + if (size && size[0] * size[1] > 8e6) approximate.push(`the area is ${size[0]}x${size[1]} pixels`); + + // An image devup-mcp does not decode - not a PNG, JPEG or GIF, an edge past + // what the plugin exports, or mostly metadata - the plugin's UI decodes as a + // browser does and hands back as a PNG, laid out at the original's size. + // Others go as they are, so their pixels stay what they were. The UI decodes + // one image at a time and all of a scene's images share one allowance of + // time, so stalled conversions still leave time to send the canvas picture. + // A conversion is counted before it is asked for, every buffer it makes + // added up and none taken to be freed early: the image's bytes here, the + // message carrying them, the copy the UI receives, the Blob made from it + // and what the decoder reads; the decoded image, and a resampled copy when + // it is drawn smaller; the canvas; the PNG encoded from it (at most 5 bytes + // a pixel, each row's filter byte and deflate's overhead included), the + // ArrayBuffer read from it, the message back and the copy received here; + // and the encoder's working memory. An image over 512 MiB that way, or one + // left when the time is up, goes as it is. Images are counted as they are + // read: past 64 MiB together the rest are not encoded, as the answer will + // be the canvas picture. + const normalize = globalThis.__devupNormalizeImage; + const normalizeUntil = Date.now() + 20000; + let imageBytes = 0; + for (const hash of images) { + const image = hash && figma.getImageByHash(hash); + let bytes = image && await image.getBytesAsync(); + if (!bytes) { + approximate.push(`the image ${hash} could not be read`); + continue; + } + let laidOut = null; + if (typeof normalize === "function") { + const { width, height } = await image.getSizeAsync(); + const starts = (...values) => values.every((value, at) => bytes[at] === value); + const decoded = starts(0x89, 0x50, 0x4e, 0x47) || starts(0xff, 0xd8) || starts(0x47, 0x49, 0x46); + const fit = Math.min(1, 8192 / Math.max(width, height, 1)); + const drawn = Math.ceil(width * fit) * Math.ceil(height * fit); + const encoded = 5 * drawn + 64 * 1024; + const peak = 5 * bytes.length + 4 * width * height * (fit < 1 ? 2 : 1) + 4 * drawn + 4 * encoded + + 1024 * 1024; + const left = normalizeUntil - Date.now(); + if ((!decoded || Math.max(width, height) > 8192 || bytes.length > 4 * width * height + 1024 * 1024) + && peak <= 512 * 1024 * 1024 && left > 0) { + const png = await normalize(bytes, 8192, left, width, height); + if (png) { + bytes = png; + laidOut = { width, height }; + } + } + } + if (imageBytes + bytes.length > 64 * 1024 * 1024) { + approximate.push(`its ancestors' images are over ${64 * 1024 * 1024} bytes`); + break; + } + imageBytes += bytes.length; + imageData[hash] = laidOut ? { data: figma.base64Encode(bytes), ...laidOut } : figma.base64Encode(bytes); + } +} catch (error) { + approximate.push(`a part could not be exported: ${error instanceof Error ? error.message : String(error)}`); +} + +const reaching = `${intruders.slice(0, 5).join(", ")}${intruders.length > 5 ? ` and ${intruders.length - 5} more` : ""}`; +if (approximate.length > 0) return held(await canvasAnswer(approximation(approximate, reaching))); +// The canvas export rides along for devup-mcp to fall back on if composing fails. +return held(await canvasAnswer({ + reaching, + compose: { area, scale, width: size[0], height: size[1], page: pageColour, layers, node: content, images: imageData }, +})); + +} + +/** source: reference_png_chunk.js */ +async function referencePngChunk(p) { +// The next piece of a referencePng answer that referencePng held because it +// did not fit one bridge answer. Bridge only: the direct path never asks. +const options = p.largeValue; +const store = globalThis.__devupReferencePngAnswers; +const entry = store && store.get(options.id); +if (!entry || typeof entry.text !== "string") { + throw new Error(`DEVUP_REFERENCE_PNG_ANSWER_GONE ${options.id}`); +} +const text = entry.text; +const offset = Math.max(0, Math.floor(Number(options.offset) || 0)); +const end = Math.min(text.length, offset + 4 * 1024 * 1024); +if (end >= text.length) store.delete(options.id); +return { kind: "devupReferencePngChunk", id: options.id, offset, nextOffset: end, complete: end >= text.length, text: text.slice(offset, end) }; + +} + +/** source: search.js */ +async function search(p) { +// Figma's own MCP runs scripts with hidden layers inside instances left out +// of `children`, `findAll` and `getNodeByIdAsync`; a plugin sees them. Set +// here so the direct path and the Devup Bridge read the same document. +figma.skipInvisibleInstanceChildren = false; +const root = await figma.getNodeByIdAsync(p.nodeId); +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 page.loadAsync(); + +const options = p.search; +const defaultTypes = ["PAGE", "SECTION", "FRAME", "COMPONENT_SET", "COMPONENT"]; +const allowedTypes = new Set( + (options.nodeTypes.length ? options.nodeTypes : defaultTypes).map((value) => value.toUpperCase()), +); + +function normalize(value) { + return value.normalize("NFC").toLocaleLowerCase().replace(/\s/gu, ""); +} + +function levenshtein(left, right) { + let previous = Array.from({ length: right.length + 1 }, (_, index) => index); + for (let leftIndex = 0; leftIndex < left.length; leftIndex += 1) { + const current = [leftIndex + 1]; + for (let rightIndex = 0; rightIndex < right.length; rightIndex += 1) { + current.push( + Math.min( + current[rightIndex] + 1, + previous[rightIndex + 1] + 1, + previous[rightIndex] + (left[leftIndex] === right[rightIndex] ? 0 : 1), + ), + ); + } + previous = current; + } + return previous[right.length]; +} + +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); + const query = normalize(options.query); + if (candidate === query) return 300; + if (candidate.startsWith(query)) return 200; + if (candidate.includes(query)) return 100; + if (options.matchKind !== "fuzzy") return null; + const distance = levenshtein(Array.from(candidate), Array.from(query)); + const threshold = Math.min(4, Math.max(1, Math.ceil(Array.from(query).length / 4))); + return distance <= threshold ? Math.max(0, 50 - distance) : null; +} + +function excluded(node) { + for (let current = node; current && current.type !== "DOCUMENT"; current = current.parent) { + // SECTION nodes have no opacity; reading it throws in the plugin API. + if (current.visible === false || ("opacity" in current && 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) : []; +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) + .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), + ); +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 +// need the chain to resolve scope and distinguish empty from missing roots. +for (const node of [root, ...candidates.map((entry) => entry.node)]) { + let current = node; + while (current && current.type !== "DOCUMENT") { + included.set(current.id, current); + current = current.parent; + } +} + +return { + fileKey: figma.fileKey || "", + version: null, + rootIds: [page.id], + nodes: [...included.values()].map((node) => ({ + id: node.id, + type: node.type, + fields: { + name: node.name, + visible: node.visible !== false, + opacity: "opacity" in node && 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) : [], + }, + extra: {}, + fieldErrors: {}, + })), + diagnostics: [], +}; + +} + +/** source: section_index.js */ +async function sectionIndex(p) { +// Carry a small measured navigation summary into screen planning, before a +// full page export. Missing measurements remain unknown rather than guessed. +function navigationContentWidth(root) { + const queue = [root]; + let visited = 0; + let width = null; + while (queue.length && visited++ < 400) { + const node = queue.shift(); + if (("visible" in node) && node.visible === false) continue; + const children = "children" in node ? node.children.filter((child) => !("visible" in child) || child.visible !== false) : []; + if (/header|nav|gnb/i.test(node.name || "") && children.length >= 2 + && ("layoutMode" in node) && node.layoutMode === "HORIZONTAL") { + const sizes = children.map(bounds); + if (sizes.every(Boolean)) { + const measured = sizes.reduce((sum, box) => sum + box.width, 0) + (Number(node.itemSpacing) || 0) * (sizes.length - 1); + width = Math.max(width || 0, measured); + } + } + queue.push(...children.slice(0, 80)); + } + return width; +} + +function navigationSummary(root) { + const width = navigationContentWidth(root); + return width === null ? {} : { navigationContentWidth: width }; +} + + +// Figma's own MCP runs scripts with hidden layers inside instances left out +// of `children`, `findAll` and `getNodeByIdAsync`; a plugin sees them. Set +// here so the direct path and the Devup Bridge read the same document. +figma.skipInvisibleInstanceChildren = false; +const section = await figma.getNodeByIdAsync(p.nodeId); +if (!section) throw new Error("DEVUP_NODE_NOT_FOUND"); +if (section.type !== "SECTION") { + let ancestor = section.parent; + const seen = new Set(); + while (ancestor && ancestor.type !== "SECTION" && !seen.has(ancestor.id)) { + seen.add(ancestor.id); + ancestor = ancestor.parent; + } + const sectionId = ancestor && ancestor.type === "SECTION" ? ancestor.id : null; + const url = sectionId && figma.fileKey + ? `https://www.figma.com/design/${figma.fileKey}?node-id=${sectionId.replace(/:/g, "-")}` : null; + throw new Error("DEVUP_SECTION_REQUIRED " + JSON.stringify({ + pluginCode: "DEVUP_SECTION_REQUIRED", stage: "section-index", + nodeId: section.id, nodeType: section.type, sectionId, + nextAction: { + tool: "devup_figma_export", + how: sectionId ? `The url must point to a SECTION. This capture's SECTION is ${sectionId}; 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: url ? (section.type === "FRAME" && section.parent === ancestor + ? {url, frameIds: [section.id]} : {url}) : null, + requiredArguments: url ? [] : ["url"] + } + })); +} + +const MAX_CANDIDATES = 100; +const MAX_TRAVERSED_NODES = 20000; +const MAX_PREVIEW_CHARACTERS = 120; +const MAX_PREVIEW_NODES = 64; +let remainingPreviewBytes = 2048; + +// The menu needs enough copy to distinguish similarly named frames, without +// returning their descendants or letting previews dominate the compact index. +// +// `allowance` is this candidate's share of the budget rather than the whole +// of what is left. Reading the pool directly spent it first-come: on the +// Braillify Studio login Section the first seven candidates took their full +// 120 characters — Korean is three UTF-8 bytes each, so 360 bytes apiece +// against a 2,048-byte pool — and the remaining seven came back +// `budget-exhausted` with nothing to read. That file names four frames +// `AUTH-01` and four `Frame 3`, so a candidate without text is a candidate +// that cannot be identified, and the previews that did land were three +// strings repeated twice over. Screens differ in their opening characters, +// so a short preview for every candidate is worth more than a long one for +// the first few. +function textPreview(root, allowance) { + const queue = [root]; + let preview = ""; + let characters = 0; + let bytes = 0; + let scanTruncated = false; + if (allowance <= 0) return { text: "", state: "budget-exhausted", spent: 0 }; + for (let index = 0; index < queue.length; index += 1) { + const node = queue[index]; + if (node.visible === false) continue; + if (node.type === "TEXT" && typeof node.characters === "string") { + const text = node.characters.replace(/\s+/g, " ").trim(); + for (const character of (preview && text ? " " : "") + text) { + const size = utf8ByteLength(character); + if (characters >= MAX_PREVIEW_CHARACTERS || bytes + size > allowance) { + return { + text: preview.trimEnd(), + state: bytes + size > allowance ? "budget-exhausted" : "truncated", + spent: bytes, + }; + } + preview += character; + characters += 1; + bytes += size; + } + } + if ("children" in node) { + const room = Math.max(0, MAX_PREVIEW_NODES - queue.length); + if (node.children.length > room) scanTruncated = true; + queue.push(...node.children.slice(0, room)); + } + } + return { + text: preview, + state: scanTruncated ? "truncated" : (preview ? "available" : "no-text"), + spent: bytes, + }; +} + +function bounds(node) { + const value = node.absoluteBoundingBox || { + x: typeof node.x === "number" ? node.x : 0, + y: typeof node.y === "number" ? node.y : 0, + width: typeof node.width === "number" ? node.width : 0, + height: typeof node.height === "number" ? node.height : 0, + }; + if (![value.x, value.y, value.width, value.height].every(Number.isFinite)) return null; + return { x: value.x, y: value.y, width: value.width, height: value.height }; +} + +function isScreen(node, box) { + if (node.type !== "FRAME" || node.visible === false || !box) return false; + const aspect = box.width / Math.max(1, box.height); + return box.width >= 240 && box.width <= 1800 + && box.height >= 300 && box.height <= 2000 + && aspect >= 0.25 && aspect <= 2.5; +} + +function breadcrumb(node) { + const names = []; + let current = node; + while (current && current.type !== "DOCUMENT") { + if (typeof current.name === "string" && current.name) names.push(current.name); + current = current.parent; + } + return names.reverse(); +} + +function utf8ByteLength(value) { + let bytes = 0; + for (let index = 0; index < value.length; index += 1) { + const code = value.charCodeAt(index); + if (code < 0x80) bytes += 1; + else if (code < 0x800) bytes += 2; + else if (code >= 0xd800 && code <= 0xdbff && index + 1 < value.length) { + bytes += 4; + index += 1; + } else bytes += 3; + } + return bytes; +} + +function subtreeEstimate(root) { + const queue = [root]; + let count = 0; + let estimatedBytes = 0; + for (let index = 0; index < queue.length && count < MAX_TRAVERSED_NODES; index += 1) { + const node = queue[index]; + count += 1; + estimatedBytes += 512 + utf8ByteLength(typeof node.name === "string" ? node.name : ""); + if (node.type === "TEXT" && typeof node.characters === "string") { + estimatedBytes += utf8ByteLength(node.characters) * 2; + } + if ("children" in node) queue.push(...node.children); + } + return { + subtreeNodeCount: count, + estimatedSerializedBytes: estimatedBytes, + truncated: queue.length > count, + }; +} + +const queue = "children" in section ? [...section.children] : []; +// Cache edges during the existing traversal; ownership must not cross the +// plugin bridge once per ancestor of every descendant. +const traversalParents = new Map(queue.map(node => [node.id, section.id])); +const candidateNodes = []; +let traversalCount = 0; +for (let index = 0; index < queue.length && traversalCount < MAX_TRAVERSED_NODES; index += 1) { + const node = queue[index]; + traversalCount += 1; + const box = bounds(node); + if (isScreen(node, box)) { + let parent = node.parent; + let nestedInScreen = false; + while (parent && parent.id !== section.id) { + if (isScreen(parent, bounds(parent))) { + nestedInScreen = true; + break; + } + parent = parent.parent; + } + if (!nestedInScreen) candidateNodes.push({ node, box }); + } + if ("children" in node) { + const children = node.children; + for (const child of children) traversalParents.set(child.id, node.id); + queue.push(...children); + } +} +// Screen shape is a guess for finding screens on a page that has no grouping. +// A Section is grouping, already explicit, and the guess applied there answers +// with whatever happens to measure like a phone. A Section of small cases +// annotated with tall notes turns it upside down: the notes pass and the cases +// do not, so the index offered the notes and hid every case — an answer that +// looked complete, which is worse than the empty list a Section of cases used +// to give. What the Section holds is what it offers. +if ("children" in section) { + const chosen = new Set(candidateNodes.map(({ node }) => node.id)); + for (const node of section.children) { + if (chosen.has(node.id)) continue; + const box = bounds(node); + if (!box || node.visible === false) continue; + // Keep the whole child available for explicit selection alongside any + // automatic screens inside it. Rust cannot recover discarded descendants. + candidateNodes.push({ node, box }); + } +} +candidateNodes.sort((left, right) => + left.box.y - right.box.y + || left.box.x - right.box.x + || left.node.id.localeCompare(right.node.id), +); +const projectionTruncated = queue.length > traversalCount || candidateNodes.length > MAX_CANDIDATES; +const selected = candidateNodes.slice(0, MAX_CANDIDATES); +const selectedIds = new Set(selected.map(({ node }) => node.id)); +// Only selection queries need ownership in this response. Export diagnostics +// use the collected snapshot's parentId/childrenIds on the server instead. +const requestedIds = new Set(p.rootIds); +const nodeScreenIds = {}; +const owners = new Map([[section.id, null]]); +for (let index = 0; index < traversalCount; index += 1) { + const id = queue[index].id; + const owner = selectedIds.has(id) ? id : owners.get(traversalParents.get(id)) ?? null; + owners.set(id, owner); + if (requestedIds.has(id)) nodeScreenIds[id] = owner; +} +// Link to the nearest retained ancestor: intermediate layout groups are not +// included in this compact projection, but container/screen ancestry survives. +const parentIds = new Map(selected.map(({ node }) => { + let parent = node.parent; + while (parent && parent.id !== section.id && !selectedIds.has(parent.id)) { + parent = parent.parent; + } + return [node.id, parent && selectedIds.has(parent.id) ? parent.id : section.id]; +})); +const childrenIds = (id) => selected + .filter(({ node }) => parentIds.get(node.id) === id) + .map(({ node }) => node.id); +const sectionBox = bounds(section); +if (!sectionBox) throw new Error("DEVUP_NODE_BOUNDS_UNAVAILABLE"); + +const sectionNode = { + id: section.id, + type: section.type, + fields: { + name: section.name, + parentId: section.parent && section.parent.type !== "DOCUMENT" ? section.parent.id : null, + childrenIds: childrenIds(section.id), + absoluteBoundingBox: sectionBox, + visible: section.visible !== false, + projectionTruncated, + nodeScreenIds, + }, + extra: {}, + fieldErrors: {}, +}; +let unfunded = selected.length; +const candidates = selected.map(({ node, box }) => { + const estimate = subtreeEstimate(node); + // An equal share of what is left, so the last candidate is funded before + // the first is indulged. What a candidate does not spend stays in the pool + // and widens every share after it, so a menu of short texts still reads in + // full. + const allowance = Math.floor(remainingPreviewBytes / unfunded); + unfunded -= 1; + const preview = textPreview(node, allowance); + remainingPreviewBytes -= preview.spent; + return { + id: node.id, + type: node.type, + fields: { + name: typeof node.name === "string" ? node.name : "", + parentId: parentIds.get(node.id), + childrenIds: childrenIds(node.id), + absoluteBoundingBox: box, + visible: node.visible !== false, + breadcrumb: breadcrumb(node), + directChildCount: "children" in node ? node.children.length : 0, + textPreview: preview.text, + ...navigationSummary(node), + textPreviewState: preview.state, + subtreeNodeCount: estimate.subtreeNodeCount, + estimatedSerializedBytes: estimate.estimatedSerializedBytes, + selectionReasons: [isScreen(node, box) ? "screen-like" : "explicit-selection-only", "inside-section"], + estimateTruncated: estimate.truncated, + }, + extra: {}, + fieldErrors: {}, + }; +}); + +const result = { + fileKey: figma.fileKey || "", + version: null, + rootIds: [section.id], + nodes: [sectionNode, ...candidates], + diagnostics: [], +}; +// The measured upstream ceiling is 20,480 UTF-8 bytes. Never hand it JSON +// that it will *silently* cut; what crosses has to describe itself. +let responseBytes = utf8ByteLength(JSON.stringify(result)); +// Previews are optional menu hints. Yield their space first: a hint is never +// worth a selectable screen. +for (let index = candidates.length - 1; index >= 0 && responseBytes > 19 * 1024; index -= 1) { + candidates[index].fields.textPreview = ""; + candidates[index].fields.textPreviewState = "budget-exhausted"; + responseBytes = utf8ByteLength(JSON.stringify(result)); +} +// Still over. Refusing outright returned *nothing* for a Section whose menu +// was merely long, and left no way forward: the ceiling is a property of the +// Section's size, so the same request reproduces it forever. Drop from the +// end and declare it with the same `projectionTruncated` the traversal and +// candidate caps above already raise — the server refuses `allScreens` on a +// truncated index and answers `not-found-in-truncated-index` instead of +// claiming a node is absent. A short menu that admits it is short beats an +// empty one. Silence was the thing forbidden here, not truncation. +const retained = () => result.nodes.length - 1; +const discoveredCandidates = candidates.length; +while (responseBytes > 19 * 1024 && retained() > 1) { + const dropped = result.nodes.pop(); + sectionNode.fields.projectionTruncated = true; + sectionNode.fields.childrenIds = sectionNode.fields.childrenIds + .filter((id) => id !== dropped.id); + responseBytes = utf8ByteLength(JSON.stringify(result)); +} +if (responseBytes > 19 * 1024) { + throw new Error("DEVUP_SECTION_INDEX_TOO_LARGE " + JSON.stringify({ + pluginCode: "DEVUP_SECTION_INDEX_TOO_LARGE", stage: "section-index", + responseBytes, maxResponseBytes: 19 * 1024, + discoveredCandidates, retainedCandidates: retained(), + })); +} +return result; + +} + +/** source: snapshot.js */ +async function snapshot(p) { +// Figma's own MCP runs scripts with hidden layers inside instances left out +// of `children`, `findAll` and `getNodeByIdAsync`; a plugin sees them. Set +// here so the direct path and the Devup Bridge read the same document. +figma.skipInvisibleInstanceChildren = false; +const root = await figma.getNodeByIdAsync(p.nodeId); +if (!root) throw new Error("DEVUP_NODE_NOT_FOUND"); + +const manifest = [ + "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", "layoutAlign", "layoutGrow", "layoutMode", "layoutWrap", + "layoutPositioning", "layoutSizingHorizontal", "layoutSizingVertical", "letterSpacing", "lineHeight", "maxHeight", + "maxLines", "maxWidth", "minHeight", "minWidth", "name", "opacity", "overflowDirection", "overrides", + "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" +] +; +const manifestSet = new Set(manifest); +const textSegmentManifest = [ + "fontName", + "fontWeight", + "fontSize", + "textDecoration", + "textCase", + "lineHeight", + "letterSpacing", + "fills", + "textStyleId", + "fillStyleId", + "listOptions", + "indentation", + "hyperlink" +] +; +const snapshotOptions = p.snapshot; +const offset = Math.max(0, Math.floor(Number(snapshotOptions.offset) || 0)); +// 이 천장들은 공식 MCP 가 텍스트 결과를 자르는 지점에서 나왔다. 로컬 소켓으로 +// 읽을 때는 자름이 없으므로 한 번에 다 담을 수 있고, 그러면 큰 필드를 따로 +// 받아오는 chunk-read 왕복이 통째로 사라진다. 값이 없으면 기존 그대로다. +const payloadCeiling = snapshotOptions.maxEnvelopeBytes + ? Math.max(8 * 1024, Math.floor(Number(snapshotOptions.maxEnvelopeBytes))) + : 16000; +const maxPayloadBytes = Math.min( + payloadCeiling, + Math.max(4096, Math.floor(Number(snapshotOptions.maxPayloadBytes) || 15000)), +); +const maxFieldBytes = Math.min( + maxPayloadBytes - 1024, + Math.max(512, Math.floor(Number(snapshotOptions.maxFieldBytes) || 4096)), +); +// `mainComponent`, `masterComponent` and `instances` are the synchronous +// getters Figma refuses under dynamic-page document access ("Use +// node.getMainComponentAsync instead"). Nothing here reads them, and walking +// the prototype chain put one refusal per instance and component into +// `fieldErrors`: 593 on the devup Test file. +const skipped = new Set(["id", "type", "parent", "children", "mainComponent", "masterComponent", "instances"]); +const hardProtectedFields = new Set([ + "parentId", + "childrenIds", + "name", + "characters", + "styledTextSegments", + "boundVariables", + "overrides", +]); + +const DEVUP_MAX_LARGE_VALUE_BYTES = 16 * 1024 * 1024; +// A fragment's text is the chunk in base64, four thirds of it, inside a +// wrapper of some three hundred bytes, and the Figma MCP cuts a text result +// at 20,480 UTF-8 bytes (measured; see fast_snapshot.js). Twelve KiB comes +// back as 16.7 KB of text, with room to spare; eight KiB was 11.2 KB, and +// took a third more calls for the same value. +const DEVUP_LARGE_VALUE_CHUNK_BYTES = 12288; + +function devupUtf8Encode(value) { + const bytes = []; + for (let index = 0; index < value.length; index += 1) { + let codePoint = value.charCodeAt(index); + if (codePoint >= 0xd800 && codePoint <= 0xdbff) { + const next = index + 1 < value.length ? value.charCodeAt(index + 1) : 0; + if (next >= 0xdc00 && next <= 0xdfff) { + codePoint = 0x10000 + ((codePoint - 0xd800) << 10) + (next - 0xdc00); + index += 1; + } else codePoint = 0xfffd; + } else if (codePoint >= 0xdc00 && codePoint <= 0xdfff) codePoint = 0xfffd; + if (codePoint < 0x80) bytes.push(codePoint); + else if (codePoint < 0x800) { + bytes.push(0xc0 | (codePoint >> 6), 0x80 | (codePoint & 0x3f)); + } else if (codePoint < 0x10000) { + bytes.push( + 0xe0 | (codePoint >> 12), + 0x80 | ((codePoint >> 6) & 0x3f), + 0x80 | (codePoint & 0x3f), + ); + } else { + bytes.push( + 0xf0 | (codePoint >> 18), + 0x80 | ((codePoint >> 12) & 0x3f), + 0x80 | ((codePoint >> 6) & 0x3f), + 0x80 | (codePoint & 0x3f), + ); + } + } + return new Uint8Array(bytes); +} + +function devupSha256(bytes) { + const constants = [ + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, + 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, + 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, + 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, + 0x06ca6351, 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, + ]; + const paddedLength = Math.ceil((bytes.length + 9) / 64) * 64; + const padded = new Uint8Array(paddedLength); + padded.set(bytes); + padded[bytes.length] = 0x80; + const bitLength = bytes.length * 8; + for (let index = 0; index < 8; index += 1) { + padded[paddedLength - 1 - index] = Math.floor(bitLength / 2 ** (index * 8)) & 0xff; + } + const hash = [ + 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, + 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19, + ]; + const rotate = (value, bits) => (value >>> bits) | (value << (32 - bits)); + for (let offset = 0; offset < padded.length; offset += 64) { + const words = new Uint32Array(64); + for (let index = 0; index < 16; index += 1) { + const start = offset + index * 4; + words[index] = + (padded[start] << 24) | + (padded[start + 1] << 16) | + (padded[start + 2] << 8) | + padded[start + 3]; + } + for (let index = 16; index < 64; index += 1) { + const s0 = rotate(words[index - 15], 7) ^ rotate(words[index - 15], 18) ^ (words[index - 15] >>> 3); + const s1 = rotate(words[index - 2], 17) ^ rotate(words[index - 2], 19) ^ (words[index - 2] >>> 10); + words[index] = (words[index - 16] + s0 + words[index - 7] + s1) >>> 0; + } + let [a, b, c, d, e, f, g, h] = hash; + for (let index = 0; index < 64; index += 1) { + const s1 = rotate(e, 6) ^ rotate(e, 11) ^ rotate(e, 25); + const choice = (e & f) ^ (~e & g); + const first = (h + s1 + choice + constants[index] + words[index]) >>> 0; + const s0 = rotate(a, 2) ^ rotate(a, 13) ^ rotate(a, 22); + const majority = (a & b) ^ (a & c) ^ (b & c); + const second = (s0 + majority) >>> 0; + h = g; g = f; f = e; e = (d + first) >>> 0; + d = c; c = b; b = a; a = (first + second) >>> 0; + } + for (const [index, value] of [a, b, c, d, e, f, g, h].entries()) { + hash[index] = (hash[index] + value) >>> 0; + } + } + return hash.map((value) => value.toString(16).padStart(8, "0")).join(""); +} + +function devupLargeValueDescriptor(nodeId, field, serialized) { + const bytes = devupUtf8Encode(JSON.stringify(serialized)); + if (bytes.length > DEVUP_MAX_LARGE_VALUE_BYTES) { + return { $truncated: "max-large-value-bytes", byteLength: bytes.length }; + } + return { + $largeValue: { + nodeId, + field, + byteLength: bytes.length, + sha256: devupSha256(bytes), + cursor: { nextOffset: 0, maxChunkBytes: DEVUP_LARGE_VALUE_CHUNK_BYTES }, + }, + }; +} +; + +function utf8ByteLength(value) { + let bytes = 0; + for (let index = 0; index < value.length; index += 1) { + const code = value.charCodeAt(index); + if (code < 0x80) bytes += 1; + else if (code < 0x800) bytes += 2; + else if (code >= 0xd800 && code <= 0xdbff && index + 1 < value.length) { + bytes += 4; + index += 1; + } else bytes += 3; + } + return bytes; +} + +function jsonByteLength(value) { + return utf8ByteLength(JSON.stringify(value)); +} + +function propertyNames(value) { + const names = new Set(); + let current = value; + while (current && current !== Object.prototype) { + for (const name of Object.getOwnPropertyNames(current)) names.add(name); + current = Object.getPrototypeOf(current); + } + for (const name of manifest) { + try { + if (name in value) names.add(name); + } catch (_) {} + } + return [...names].sort(); +} + +function serialize(value, seen = new WeakSet(), depth = 0) { + if (value === null || ["string", "number", "boolean"].includes(typeof value)) return value; + if (typeof value === "undefined") return { $undefined: true }; + if (typeof value === "bigint") return { $bigint: value.toString() }; + if (["function", "symbol"].includes(typeof value)) return { $unsupported: typeof value }; + if (depth > 12) return { $truncated: "max-depth" }; + if ( + typeof value === "object" && + "parent" in value && + typeof value.id === "string" && + typeof value.type === "string" + ) { + return { $nodeId: value.id, $nodeType: value.type }; + } + if (Array.isArray(value)) return value.map((item) => serialize(item, seen, depth + 1)); + if (ArrayBuffer.isView(value)) { + return { $binary: value.constructor.name, byteLength: value.byteLength }; + } + if (value instanceof ArrayBuffer) return { $binary: "ArrayBuffer", byteLength: value.byteLength }; + if (seen.has(value)) return { $circular: true }; + seen.add(value); + const result = {}; + for (const key of Object.keys(value).sort()) { + try { + const serialized = serialize(value[key], seen, depth + 1); + if (!(serialized && serialized.$unsupported === "function")) result[key] = serialized; + } catch (error) { + result[key] = { $error: String(error && error.message ? error.message : error) }; + } + } + seen.delete(value); + return result; +} + +function truncatedValue(reason, byteLength) { + return { $truncated: reason, byteLength }; +} + +function serializeField(nodeId, name, value, fieldErrors) { + const serialized = serialize(value); + const byteLength = jsonByteLength(serialized); + if (byteLength <= maxFieldBytes) return serialized; + const descriptor = devupLargeValueDescriptor(nodeId, name, serialized); + if (descriptor.$truncated) { + fieldErrors[name] = `DEVUP_FIELD_VALUE_UNSUPPORTED:${byteLength}>${DEVUP_MAX_LARGE_VALUE_BYTES}`; + } + return descriptor; +} + +function isHardProtectedField(name) { + return hardProtectedFields.has(name) || name.endsWith("StyleId"); +} + +function shrinkNodeToBudget(node, budget) { + let currentBytes = jsonByteLength(node); + if (currentBytes <= budget) return node; + + const candidates = []; + for (const [sectionName, section] of [ + ["extra", node.extra], + ["fields", node.fields], + ]) { + for (const [name, value] of Object.entries(section)) { + if (value && typeof value === "object" && ("$truncated" in value || "$largeValue" in value)) { + continue; + } + candidates.push({ + sectionName, + name, + byteLength: jsonByteLength(value), + protected: sectionName === "fields" && isHardProtectedField(name), + }); + } + } + candidates.sort( + (left, right) => + Number(left.protected) - Number(right.protected) || right.byteLength - left.byteLength, + ); + + for (const candidate of candidates) { + if (currentBytes <= budget) break; + const descriptor = devupLargeValueDescriptor( + node.id, + candidate.name, + node[candidate.sectionName][candidate.name], + ); + node[candidate.sectionName][candidate.name] = descriptor; + if (descriptor.$truncated) { + node.fieldErrors[candidate.name] = + `DEVUP_FIELD_VALUE_UNSUPPORTED:${candidate.byteLength}>${DEVUP_MAX_LARGE_VALUE_BYTES}`; + } + currentBytes = jsonByteLength(node); + } + return node; +} + +// A variant's component properties are defined on its set, and Figma +// refuses to read them from the variant: "Can only get component property +// definitions of a component set or non-variant component". +function variantDefinitions(node, name) { + return ( + name === "componentPropertyDefinitions" && + node.type === "COMPONENT" && + Boolean(node.parent) && + node.parent.type === "COMPONENT_SET" + ); +} + +// Main-component identity is asynchronous under dynamic-page access. It is +// not a synchronous getter from the property manifest. Cache per collection +// so repacking the same page does not repeat its reads. +const devupComponentIdentity = new Map(); +async function devupReadComponentIdentity(node, fields, fieldErrors) { + if (node.type !== "INSTANCE" || typeof node.getMainComponentAsync !== "function") return; + if (!devupComponentIdentity.has(node.id)) { + devupComponentIdentity.set(node.id, (async () => { + try { + const main = await node.getMainComponentAsync(); + return { fields: { + mainComponentId: main ? main.id : null, + mainComponentName: main ? main.name : null, + mainComponentSetId: main && main.parent && main.parent.type === "COMPONENT_SET" ? main.parent.id : null, + } }; + } catch (error) { + return { error: String(error && error.message ? error.message : error) }; + } + })()); + } + const identity = await devupComponentIdentity.get(node.id); + if (identity.error) fieldErrors.mainComponentId = identity.error; + else Object.assign(fields, identity.fields); +} +; +async function snapshotNode(node) { + const fields = {}; + const extra = {}; + const fieldErrors = {}; + fields.parentId = node.parent ? node.parent.id : null; + // Only the root needs this. Its parent lies outside the collected subtree, + // so the id alone says nothing, and the parent's type is what decides + // whether the root's width is a real constraint or merely the canvas the + // design was drawn on. Every other node's parent is collected and can be + // read directly. + // Keyed on the parent's type rather than on being the requested root, so a + // node carries the same fields however it is reached. See fast_snapshot.js. + if ( + node.parent && + (node.parent.type === "PAGE" || + node.parent.type === "SECTION" || + node.parent.type === "COMPONENT_SET") + ) { + fields.parentType = node.parent.type; + // A screen's Section names the page component the plugin writes for it, + // `AboutPage` for a Section called `about`, and the Section itself is + // outside the collected subtree. + if (node.parent.type === "SECTION") fields.parentName = node.parent.name; + } + fields.childrenIds = "children" in node ? node.children.map((child) => child.id) : []; + if (!("overflowDirection" in node) && ["FRAME", "COMPONENT", "INSTANCE", "COMPONENT_SET"].includes(node.type)) fields.overflowDirection = null; + + for (const name of propertyNames(node)) { + if (skipped.has(name) || name.startsWith("_") || variantDefinitions(node, name)) continue; + try { + const value = node[name]; + if (typeof value === "function") continue; + const serialized = serializeField(node.id, name, value, fieldErrors); + (manifestSet.has(name) ? fields : extra)[name] = serialized; + } catch (error) { + fieldErrors[name] = String(error && error.message ? error.message : error); + } + } + if (node.type === "TEXT" && typeof node.getStyledTextSegments === "function") { + try { + fields.styledTextSegments = serializeField( + node.id, + "styledTextSegments", + node.getStyledTextSegments(textSegmentManifest), + fieldErrors, + ); + } catch (error) { + fieldErrors.styledTextSegments = String(error && error.message ? error.message : error); + } + } + await devupReadComponentIdentity(node, fields, fieldErrors); + return { id: node.id, type: node.type, fields, extra, fieldErrors }; +} + +const allNodes = []; +const queue = [root]; +while (queue.length) { + const node = queue.shift(); + allNodes.push(node); + if ("children" in node) queue.push(...node.children); +} + +const nodes = []; +const nodeBudget = maxPayloadBytes - 1024; +let payloadBytes = 2; +for (let index = offset; index < allNodes.length; index += 1) { + const node = shrinkNodeToBudget(await snapshotNode(allNodes[index]), nodeBudget); + const nodeBytes = jsonByteLength(node) + (nodes.length ? 1 : 0); + if (nodes.length && payloadBytes + nodeBytes > nodeBudget) break; + nodes.push(node); + payloadBytes += nodeBytes; +} + +const nextOffset = Math.min(allNodes.length, offset + nodes.length); +nodes.push({ + id: "__DEVUP_SNAPSHOT_CURSOR__", + type: "DEVUP_INTERNAL", + // Same marker shape as the fast snapshot so both paths go through the one + // `read_snapshot_cursor` reader in Rust. + fields: { + offset, + nextOffset, + complete: nextOffset >= allNodes.length, + totalNodes: allNodes.length, + }, + extra: {}, + fieldErrors: {}, +}); + +return { + fileKey: figma.fileKey || "", + version: null, + rootIds: [root.id], + nodes, + diagnostics: [], +}; + +} + +/** source: used_resources.js */ +async function usedResources(p) { +const resources = p.resources; + +function serialize(value, seen = new WeakSet(), depth = 0) { + if (value === null || ["string", "number", "boolean"].includes(typeof value)) return value; + if (typeof value === "undefined") return { $undefined: true }; + if (typeof value === "bigint") return { $bigint: value.toString() }; + if (["function", "symbol"].includes(typeof value)) return { $unsupported: typeof value }; + if (depth > 12) return { $truncated: "max-depth" }; + if (typeof value === "object" && "parent" in value && typeof value.id === "string" && typeof value.type === "string") { + return { $nodeId: value.id, $nodeType: value.type }; + } + if (Array.isArray(value)) return value.map((item) => serialize(item, seen, depth + 1)); + if (ArrayBuffer.isView(value)) return { $binary: value.constructor.name, byteLength: value.byteLength }; + if (value instanceof ArrayBuffer) return { $binary: "ArrayBuffer", byteLength: value.byteLength }; + if (seen.has(value)) return { $circular: true }; + seen.add(value); + const result = {}; + const names = new Set(Object.keys(value)); + let current = value; + while (current && current !== Object.prototype) { + for (const name of Object.getOwnPropertyNames(current)) names.add(name); + current = Object.getPrototypeOf(current); + } + for (const name of [...names].sort()) { + if (name.startsWith("_") || ["parent", "children", "consumers"].includes(name)) continue; + try { + const serialized = serialize(value[name], seen, depth + 1); + if (!(serialized && serialized.$unsupported === "function")) result[name] = serialized; + } catch (_error) { + result[name] = { $error: "unavailable" }; + } + } + seen.delete(value); + return result; +} + +// 파일에 있는 변수는 한 번에 다 받아 두고 id 로 찾는다. +// +// 하나씩 `getVariableByIdAsync` 로 묻는 것이 이 파일에서는 건당 21.5초를 쓰고 +// 끝내 null 을 냈다. 33개든 40개든 병렬이라 총 시간이 21.5초로 같았던 것이 +// 단서였다 ? 작업이 아니라 건당 대기였다. 같은 순간 지역 목록을 받는 호출은 +// 12ms 에 돌아온다. 그래서 그쪽을 먼저 쓴다. +const localVariableIndex = new Map(); +let localVariableListing = "ok"; +try { + for (const variable of await figma.variables.getLocalVariablesAsync()) { + localVariableIndex.set(variable.id, variable); + } +} catch (_error) { + localVariableListing = "unavailable"; +} + +function aliasIdsOf(variable) { + return Object.values(variable.valuesByMode || {}) + .filter((value) => value && value.type === "VARIABLE_ALIAS" && typeof value.id === "string" && value.id) + .map((value) => value.id); +} + +async function resolveVariable(id) { + const local = localVariableIndex.get(id); + if (local) { + return { value: serialize(local), collectionId: local.variableCollectionId, aliasIds: aliasIdsOf(local) }; + } + // 목록에 없으면 이 파일 것이 아니다(라이브러리 변수). 그때만 개별로 묻는다. + try { + const value = await figma.variables.getVariableByIdAsync(id); + return value + ? { value: serialize(value), collectionId: value.variableCollectionId, aliasIds: aliasIdsOf(value) } + : { unresolved: { id, kind: "variable", reason: "notInFileAndLookupEmpty" } }; + } catch (error) { + return { unresolved: { id, kind: "variable", reason: "notInFileAndLookupThrew", error: String((error && error.message) || error).slice(0, 300) } }; + } +} + +const variableResults = await Promise.all(resources.variableIds.map(resolveVariable)); +// 변수는 어떤 노드도 묶지 않은 변수를 alias 로 가리킬 수 있다 — 라이브러리 primitive 를 +// 가리키는 로컬 spacing 처럼. 테마가 그 alias 의 값을 내려면 그 변수가 있어야 하므로 +// 그것도, 그것이 가리키는 것도 읽는다. 노드가 쓴 id 목록에는 넣지 않는다. +const requestedVariableIds = new Set(resources.variableIds); +let pendingAliasIds = variableResults.flatMap((result) => result.aliasIds || []); +while (pendingAliasIds.length > 0) { + const batch = [...new Set(pendingAliasIds)] + .filter((id) => !requestedVariableIds.has(id)) + .sort(); + for (const id of batch) requestedVariableIds.add(id); + const fetched = await Promise.all(batch.map(resolveVariable)); + variableResults.push(...fetched); + pendingAliasIds = fetched.flatMap((result) => result.aliasIds || []); +} + +const collectionIds = [...new Set(variableResults + .flatMap((result) => result.collectionId ? [result.collectionId] : []))].sort(); +const collectionResults = await Promise.all(collectionIds.map(async (id) => { + try { + const collection = await figma.variables.getVariableCollectionByIdAsync(id); + return collection ? [serialize(collection)] : []; + } catch (_error) { + return []; + } +})); + +const styleResults = await Promise.all(resources.styles.map(async (styleRef) => { + try { + const style = await figma.getStyleByIdAsync(styleRef.id); + if (!style) { + return { unresolved: { id: styleRef.id, kind: "style", reason: "notFoundOrUnavailable" } }; + } + return { + value: { + ...serialize(style), + styleType: styleRef.styleType, + value: serialize( + styleRef.styleType === "PAINT" ? style.paints + : styleRef.styleType === "EFFECT" ? style.effects + : styleRef.styleType === "GRID" ? style.layoutGrids + : style + ) + } + }; + } catch (error) { + return { unresolved: { id: styleRef.id, kind: "style", reason: "notFoundOrUnavailable", error: String((error && error.message) || error).slice(0, 300) } }; + } +})); + +return { + collections: collectionResults.flat(), + variables: variableResults.flatMap((result) => result.value ? [result.value] : []), + styles: styleResults.flatMap((result) => result.value ? [result.value] : []), + usedVariableIds: resources.variableIds, + usedStyleIds: resources.styles.map((style) => style.id), + // 아무것도 해석되지 않았을 때 목록 자체를 못 받은 것인지 구분하기 위해 남긴다. + localVariableListing, + localVariableCount: localVariableIndex.size, + unresolved: [...variableResults, ...styleResults] + .flatMap((result) => result.unresolved ? [result.unresolved] : []) +}; + +} + +/** source: variable_catalog.js */ +async function variableCatalog(p) { +function serialize(value, seen = new WeakSet(), depth = 0) { + if (value === null || ["string", "number", "boolean"].includes(typeof value)) return value; + if (typeof value === "undefined") return { $undefined: true }; + if (typeof value === "bigint") return { $bigint: value.toString() }; + if (["function", "symbol"].includes(typeof value)) return { $unsupported: typeof value }; + if (depth > 12) return { $truncated: "max-depth" }; + if (Array.isArray(value)) return value.map((item) => serialize(item, seen, depth + 1)); + if (seen.has(value)) return { $circular: true }; + seen.add(value); + const result = {}; + const names = new Set(Object.keys(value)); + let current = value; + while (current && current !== Object.prototype) { + for (const name of Object.getOwnPropertyNames(current)) names.add(name); + current = Object.getPrototypeOf(current); + } + for (const name of [...names].sort()) { + if (name.startsWith("_")) continue; + try { + const serialized = serialize(value[name], seen, depth + 1); + if (!(serialized && serialized.$unsupported === "function")) result[name] = serialized; + } catch (error) { + result[name] = { $error: String(error && error.message ? error.message : error) }; + } + } + seen.delete(value); + return result; +} + +const [collections, paints, texts, effects, grids] = await Promise.all([ + figma.variables.getLocalVariableCollectionsAsync(), + figma.getLocalPaintStylesAsync(), + figma.getLocalTextStylesAsync(), + figma.getLocalEffectStylesAsync(), + figma.getLocalGridStylesAsync() +]); +const styleGroups = [paints, texts, effects, grids]; +const styleTypes = ["PAINT", "TEXT", "EFFECT", "GRID"]; + +return { + collections: collections.map((value) => serialize(value)), + variableIds: [...new Set(collections.flatMap((collection) => collection.variableIds))].sort(), + styles: styleGroups.flatMap((group, index) => group.map((style) => ({ + id: style.id, + styleType: styleTypes[index] + }))).sort((left, right) => left.id.localeCompare(right.id)), + localComplete: true, + usedRemoteComplete: false +}; + +} + +/** source: variables.js */ +async function variables(p) { +const resources = p.resources; + +function serialize(value, seen = new WeakSet(), depth = 0) { + if (value === null || ["string", "number", "boolean"].includes(typeof value)) return value; + if (typeof value === "undefined") return { $undefined: true }; + if (typeof value === "bigint") return { $bigint: value.toString() }; + if (["function", "symbol"].includes(typeof value)) return { $unsupported: typeof value }; + if (depth > 12) return { $truncated: "max-depth" }; + if (typeof value === "object" && "parent" in value && typeof value.id === "string" && typeof value.type === "string") { + return { $nodeId: value.id, $nodeType: value.type }; + } + if (Array.isArray(value)) return value.map((item) => serialize(item, seen, depth + 1)); + if (ArrayBuffer.isView(value)) return { $binary: value.constructor.name, byteLength: value.byteLength }; + if (value instanceof ArrayBuffer) return { $binary: "ArrayBuffer", byteLength: value.byteLength }; + if (seen.has(value)) return { $circular: true }; + seen.add(value); + const result = {}; + const names = new Set(Object.keys(value)); + let current = value; + while (current && current !== Object.prototype) { + for (const name of Object.getOwnPropertyNames(current)) names.add(name); + current = Object.getPrototypeOf(current); + } + for (const name of [...names].sort()) { + if (name.startsWith("_") || ["parent", "children", "consumers"].includes(name)) continue; + try { + const serialized = serialize(value[name], seen, depth + 1); + if (!(serialized && serialized.$unsupported === "function")) result[name] = serialized; + } catch (error) { + result[name] = { $error: String(error && error.message ? error.message : error) }; + } + } + seen.delete(value); + return result; +} + +const [variableValues, styleValues] = await Promise.all([ + Promise.all(resources.variableIds.map((id) => figma.variables.getVariableByIdAsync(id))), + Promise.all(resources.styles.map((style) => figma.getStyleByIdAsync(style.id))) +]); +const styleRefs = new Map(resources.styles.map((style) => [style.id, style])); +const styles = await Promise.all(styleValues.filter(Boolean).map(async (style) => { + const styleRef = styleRefs.get(style.id); + const styleType = styleRef.styleType; + if (Number.isInteger(styleRef.consumerStart) && Number.isInteger(styleRef.consumerEnd)) { + const consumers = await style.getStyleConsumersAsync(); + return { + id: style.id, + styleType, + $consumerStart: styleRef.consumerStart, + $consumerEntries: consumers.slice(styleRef.consumerStart, styleRef.consumerEnd).map((consumer) => [ + consumer.node.id, + consumer.node.type, + serialize(consumer.fields) + ]) + }; + } + const consumers = await style.getStyleConsumersAsync(); + return { + ...serialize(style), + styleType, + $consumerCount: consumers.length, + value: serialize( + styleType === "PAINT" ? style.paints + : styleType === "EFFECT" ? style.effects + : styleType === "GRID" ? style.layoutGrids + : style + ) + }; +})); + +return { + variables: variableValues.filter(Boolean).map((value) => serialize(value)), + styles +}; + +} + +return { + assets, + bridgeContext, + explore, + fastSnapshot, + fastTheme, + largeValue, + measureNodes, + metadata, + pageCatalog, + referencePng, + referencePngChunk, + search, + sectionIndex, + snapshot, + usedResources, + variableCatalog, + variables, +}; +}) diff --git a/crates/devup-mcp-figma/src/scripts/explore.js b/crates/devup-mcp-figma/src/scripts/explore.js index 0fc94bcc..3f44a79d 100644 --- a/crates/devup-mcp-figma/src/scripts/explore.js +++ b/crates/devup-mcp-figma/src/scripts/explore.js @@ -206,6 +206,7 @@ if (descendantScope && "children" in scopeAnchor) { } if ("children" in node) { for (const child of node.children) { + if (requestedPage && ["FRAME", "COMPONENT", "INSTANCE", "COMPONENT_SET"].includes(node.type)) continue; nestedQueue.push({ node: child, ancestors: [...ancestors, node] }); } } @@ -230,7 +231,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/crates/devup-mcp-figma/src/scripts/search.js b/crates/devup-mcp-figma/src/scripts/search.js index 34b5bd6f..5573147e 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"]; @@ -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); @@ -51,19 +53,30 @@ 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) { + // SECTION nodes have no opacity; reading it throws in the plugin API. + if (current.visible === false || ("opacity" in current && 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) : []; -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) .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), - ) - .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 @@ -85,6 +98,9 @@ return { type: node.type, fields: { name: node.name, + visible: node.visible !== false, + opacity: "opacity" in node && 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/scripts/snapshot.js b/crates/devup-mcp-figma/src/scripts/snapshot.js index 8c9e9828..19b8e634 100644 --- a/crates/devup-mcp-figma/src/scripts/snapshot.js +++ b/crates/devup-mcp-figma/src/scripts/snapshot.js @@ -37,6 +37,7 @@ const hardProtectedFields = new Set([ "characters", "styledTextSegments", "boundVariables", + "overrides", ]); "__DEVUP_LARGE_VALUE_HELPERS__"; diff --git a/crates/devup-mcp-figma/src/search.rs b/crates/devup-mcp-figma/src/search.rs index 198ce0e3..448d68c4 100644 --- a/crates/devup-mcp-figma/src/search.rs +++ b/crates/devup-mcp-figma/src/search.rs @@ -30,6 +30,66 @@ 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", + "hidden", + "휴지통", + "보관", + "초안", + "복사", + "pdf", + "저장용", + "export", + "출력", +]; + +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 node_excluded_reason(snapshot: &Snapshot, node_id: &str) -> 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( @@ -38,16 +98,17 @@ 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, )); } if options.limit == 0 || options.limit > 100 { return Err(DevupError::new( - ErrorCode::DevupFigmaResponseTooLarge, + ErrorCode::DevupInvalidInput, "Search limit must be between 1 and 100.", false, )); @@ -57,7 +118,7 @@ pub fn search_snapshot( "exact" | "normalized" | "fuzzy" ) { return Err(DevupError::new( - ErrorCode::DevupFigmaNodeNotFound, + ErrorCode::DevupInvalidInput, "match must be exact, normalized, or fuzzy.", false, )); @@ -82,11 +143,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 = node_excluded_reason(snapshot, &node.id); Some(SearchResult { + excluded, node_id: node.id.clone(), name: name.to_owned(), node_type: node.node_type.clone(), @@ -99,9 +165,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)) }); @@ -109,6 +176,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, @@ -292,6 +385,185 @@ 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 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", "출력본"] { + assert!(excluded_reason(&[], Some(page)).is_some(), "{page}"); + } + 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(); + let error = search_snapshot(&snapshot, &target, &options("", &[])).unwrap_err(); + 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()); 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-figma/src/webp.rs b/crates/devup-mcp-figma/src/webp.rs index 13de5076..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.", )); @@ -296,7 +298,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 +318,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/collector.rs b/crates/devup-mcp-figma/tests/collector.rs index 175f899f..38e196fa 100644 --- a/crates/devup-mcp-figma/tests/collector.rs +++ b/crates/devup-mcp-figma/tests/collector.rs @@ -1,4 +1,5 @@ // Source sections share this module to preserve privacy and test names. include!("collector/fixtures_and_cases.rs"); +include!("collector/plugin_identity.rs"); include!("collector/a_fast_theme_page_that_does_not_advance_is_refused.rs"); include!("collector/failed_section_batch_retries_only_its_root_and_preserves_fast_resource.rs"); diff --git a/crates/devup-mcp-figma/tests/collector/fixtures_and_cases.rs b/crates/devup-mcp-figma/tests/collector/fixtures_and_cases.rs index 10b13fd0..014c951f 100644 --- a/crates/devup-mcp-figma/tests/collector/fixtures_and_cases.rs +++ b/crates/devup-mcp-figma/tests/collector/fixtures_and_cases.rs @@ -990,4 +990,4 @@ fn a_paged_fast_theme_is_read_page_by_page_and_merged() { assert_eq!(parts.stats.variable_count, 2); assert_eq!(parts.stats.style_count, 1); assert_eq!(parts.metadata["pageCount"], 2); -} \ No newline at end of file +} diff --git a/crates/devup-mcp-figma/tests/collector/plugin_identity.rs b/crates/devup-mcp-figma/tests/collector/plugin_identity.rs new file mode 100644 index 00000000..8321326a --- /dev/null +++ b/crates/devup-mcp-figma/tests/collector/plugin_identity.rs @@ -0,0 +1,30 @@ +#[test] +fn stale_plugin_warning_survives_collection_without_affecting_fidelity() { + for hash in [Some(devup_mcp_figma::PLUGIN_SCRIPTS_HASH), Some("old-build"), None] { + let mut request = CollectionRequest::new(target("1:2"), CollectionScope::Node); + request.resource_scope = ResourceScope::Used; + let mut collector = CollectorSession::new(request); + let CollectorStep::Call(call) = collector.advance().unwrap() else { + panic!("fast snapshot call expected") + }; + let mut result = fast_envelope_result(); + result.raw["_meta"] = json!({"devup/bridge": { + "reads": 1, "scriptsHash": hash, "protocol": devup_mcp_figma::PLUGIN_PROTOCOL + }}); + collector.accept(&call.id, result).unwrap(); + let CollectorStep::Complete(parts) = collector.advance().unwrap() else { + panic!("fast snapshot should complete") + }; + let snapshot = merge_chunks(parts.snapshot_chunks).unwrap(); + let warnings = snapshot.diagnostics.iter() + .filter(|issue| issue.code == "DEVUP_BRIDGE_PLUGIN_STALE") + .collect::>(); + if hash == Some(devup_mcp_figma::PLUGIN_SCRIPTS_HASH) { + assert!(warnings.is_empty()); + } else { + assert_eq!(warnings.len(), 1); + assert_eq!(warnings[0].fidelity_impact(), devup_mcp_figma::FidelityImpact::None); + assert_eq!(warnings[0].details.as_ref().unwrap()["pluginStale"], true); + } + } +} diff --git a/crates/devup-mcp-figma/tests/component_identity_script.rs b/crates/devup-mcp-figma/tests/component_identity_script.rs index 37fda953..d969b4db 100644 --- a/crates/devup-mcp-figma/tests/component_identity_script.rs +++ b/crates/devup-mcp-figma/tests/component_identity_script.rs @@ -18,8 +18,8 @@ let calls=0; const root={id:'1:1',type:'FRAME',name:'screen',children:[]}; const main={id:'10:1',type:'COMPONENT',name:'Header / desktop',parent:{id:'10:0',type:'COMPONENT_SET'}}; root.children=[ - {id:'1:2',type:'INSTANCE',children:[],parent:root,getMainComponentAsync:async()=>{calls++;return main}}, - {id:'1:3',type:'INSTANCE',children:[],parent:root,getMainComponentAsync:async()=>{calls++;throw new Error('actual main-component lookup failure')}}, + {id:'1:2',type:'INSTANCE',children:[],parent:root,get overrides(){return [{id:'1:2',overriddenFields:['fills']}]},get mainComponent(){throw new Error('sync main getter forbidden')},getMainComponentAsync:async()=>{calls++;return main}}, + {id:'1:3',type:'INSTANCE',children:[],parent:root,get overrides(){throw new Error('override lookup failure')},getMainComponentAsync:async()=>{calls++;throw new Error('actual main-component lookup failure')}}, {id:'1:4',type:'INSTANCE',children:[],parent:root,getMainComponentAsync:async()=>{calls++;return null}}, ]; const figma={fileKey:'fixture',getNodeByIdAsync:async()=>root,variables:{}}; @@ -55,7 +55,16 @@ new AsyncFunction('figma',code)(figma).then(data=>process.stdout.write(JSON.stri assert_eq!(instance["fields"]["mainComponentId"], "10:1"); assert_eq!(instance["fields"]["mainComponentName"], "Header / desktop"); assert_eq!(instance["fields"]["mainComponentSetId"], "10:0"); + assert_eq!( + instance["fields"]["overrides"], + json!([{ "id": "1:2", "overriddenFields": ["fills"] }]) + ); + assert!(instance["fieldErrors"].get("mainComponent").is_none()); let failed = nodes.iter().find(|n| n["id"] == "1:3").unwrap(); + assert_eq!( + failed["fieldErrors"]["overrides"], + "override lookup failure" + ); assert_eq!( failed["fieldErrors"]["mainComponentId"], "actual main-component lookup failure" 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-figma/tests/live_scripts.rs b/crates/devup-mcp-figma/tests/live_scripts.rs new file mode 100644 index 00000000..54a3bfa2 --- /dev/null +++ b/crates/devup-mcp-figma/tests/live_scripts.rs @@ -0,0 +1,79 @@ +use devup_mcp_figma::{BridgeServer, PLUGIN_PROTOCOL, PLUGIN_SCRIPTS_BUNDLE, PLUGIN_SCRIPTS_HASH}; +use futures_util::{SinkExt, StreamExt}; +use serde_json::{Value, json}; +use std::time::Duration; +use tokio::net::TcpStream; +use tokio_tungstenite::{MaybeTlsStream, WebSocketStream, connect_async, tungstenite::Message}; + +type Plugin = WebSocketStream>; + +async fn say_hello(server: &BridgeServer, hello: Value) -> Plugin { + let (mut socket, _) = connect_async(format!("ws://127.0.0.1:{}/plugin", server.port())) + .await + .expect("bridge accepts a plugin"); + socket + .send(Message::Text(hello.to_string().into())) + .await + .expect("hello is sent"); + socket +} + +async fn messages_within(socket: &mut Plugin, wait: Duration) -> Vec { + let mut seen = Vec::new(); + while let Ok(Some(Ok(Message::Text(text)))) = tokio::time::timeout(wait, socket.next()).await { + seen.push(serde_json::from_str(&text).expect("the server sends JSON")); + } + seen +} + +fn hello(hash: &str, live: bool) -> Value { + json!({ + "kind": "hello", "fileKey": "FileKey123", + "scriptsHash": hash, "protocol": PLUGIN_PROTOCOL, "liveScripts": live, + }) +} + +fn updates(messages: &[Value]) -> Vec<&Value> { + messages + .iter() + .filter(|message| message["kind"] == "scripts-update") + .collect() +} + +#[tokio::test] +async fn a_live_plugin_with_older_scripts_is_sent_this_builds_scripts() { + let server = BridgeServer::start(0).expect("an ephemeral port is free"); + let mut plugin = say_hello(&server, hello("older-build", true)).await; + let messages = messages_within(&mut plugin, Duration::from_millis(500)).await; + let updates = updates(&messages); + assert_eq!(updates.len(), 1, "{messages:?}"); + assert_eq!(updates[0]["scriptsHash"], PLUGIN_SCRIPTS_HASH); + assert_eq!(updates[0]["bundle"], PLUGIN_SCRIPTS_BUNDLE); + + // The plugin loaded them and says hello again with the new hash: no repeat. + plugin + .send(Message::Text( + hello(PLUGIN_SCRIPTS_HASH, true).to_string().into(), + )) + .await + .unwrap(); + let after = messages_within(&mut plugin, Duration::from_millis(300)).await; + assert!(self::updates(&after).is_empty(), "{after:?}"); + let attached = server.state().attached_files().await; + assert_eq!(attached[0].context.plugin_build()["state"], "current"); +} + +#[tokio::test] +async fn current_or_non_live_plugins_are_not_sent_scripts() { + for hello in [ + hello(PLUGIN_SCRIPTS_HASH, true), + hello("older-build", false), + json!({ "kind": "hello", "fileKey": "FileKey123", "scriptsHash": "older-build", + "protocol": PLUGIN_PROTOCOL + 1, "liveScripts": true }), + ] { + let server = BridgeServer::start(0).expect("an ephemeral port is free"); + let mut plugin = say_hello(&server, hello.clone()).await; + let messages = messages_within(&mut plugin, Duration::from_millis(300)).await; + assert!(updates(&messages).is_empty(), "{hello}: {messages:?}"); + } +} diff --git a/crates/devup-mcp-figma/tests/manifest_covers_readers.rs b/crates/devup-mcp-figma/tests/manifest_covers_readers.rs index 8e9f02c3..43c02109 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-figma/tests/plugin_identity.rs b/crates/devup-mcp-figma/tests/plugin_identity.rs new file mode 100644 index 00000000..195e2419 --- /dev/null +++ b/crates/devup-mcp-figma/tests/plugin_identity.rs @@ -0,0 +1,24 @@ +use devup_mcp_figma::{PLUGIN_PROTOCOL, PLUGIN_SCRIPTS_HASH, PluginContext}; +use serde_json::json; + +#[test] +fn plugin_identity_distinguishes_matching_mismatched_and_unknown_builds() { + for (hash, protocol, state) in [ + (Some(PLUGIN_SCRIPTS_HASH), Some(PLUGIN_PROTOCOL), "current"), + (Some("old-build"), Some(PLUGIN_PROTOCOL), "stale"), + (None, None, "stale-unknown"), + (Some(PLUGIN_SCRIPTS_HASH), Some(999), "stale"), + ] { + let context: PluginContext = + serde_json::from_value(json!({"scriptsHash":hash,"protocol":protocol})).unwrap(); + let build = context.plugin_build(); + assert_eq!(build["state"], state); + assert_eq!(build["pluginStale"], state != "current"); + assert_eq!(build["expected"], PLUGIN_SCRIPTS_HASH); + assert_eq!(build["actual"], json!(hash)); + assert_eq!( + serde_json::to_value(context).unwrap()["scriptsHash"], + json!(hash) + ); + } +} diff --git a/crates/devup-mcp-figma/tests/upstream_contract.rs b/crates/devup-mcp-figma/tests/upstream_contract.rs index 441a40db..71826c07 100644 --- a/crates/devup-mcp-figma/tests/upstream_contract.rs +++ b/crates/devup-mcp-figma/tests/upstream_contract.rs @@ -214,7 +214,8 @@ fn search_uses_a_compiled_read_only_page_projection() { let code = call.arguments()["code"].as_str().unwrap().to_owned(); assert_eq!(call.tool_name(), "use_figma"); - assert!(code.contains("figma.setCurrentPageAsync(page)")); + assert!(code.contains("page.loadAsync()")); + assert!(!code.contains("figma.setCurrentPageAsync(page)")); assert!(code.contains("page.findAll")); assert!(code.contains("Essence")); assert!(!code.contains("eval(")); 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); 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/build.rs b/crates/devup-mcp/build.rs index aea21a32..77e69a0a 100644 --- a/crates/devup-mcp/build.rs +++ b/crates/devup-mcp/build.rs @@ -2,7 +2,7 @@ use std::{env, path::PathBuf, process::Command}; mod build_identity; -use build_identity::{git_identity, safe}; +use build_identity::{dirty_identity, git_identity, safe}; fn main() { let commit = Command::new("git") @@ -18,6 +18,21 @@ fn main() { println!("cargo:rerun-if-changed=build_identity.rs"); println!("cargo:rerun-if-changed=Cargo.toml"); println!("cargo:rerun-if-changed=src"); + // The binary embeds these too; an edit there must refresh the diff hash. + for path in [ + "../devup-mcp-devup-ui/src", + "../devup-mcp-figma/src", + "../devup-mcp-visual/src", + "../../harness/runtime-audit/run.mjs", + "../../harness/runtime-audit/checks.mjs", + "../../harness/runtime-audit/resource-checks.mjs", + ] { + println!("cargo:rerun-if-changed={path}"); + } + let built_at = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map_or(0, |elapsed| elapsed.as_secs()); + println!("cargo:rustc-env=DEVUP_MCP_BUILD_TIME={built_at}"); for path in [git_path("HEAD"), git_path("index")].into_iter().flatten() { println!("cargo:rerun-if-changed={}", path.display()); } @@ -85,7 +100,28 @@ fn git_build_id() -> Option { if !status.status.success() { return None; } - git_identity(Some(value.trim()), !status.stdout.is_empty()) + if status.stdout.is_empty() { + return git_identity(Some(value.trim()), false); + } + dirty_identity(Some(value.trim()), Some(&diff_hash(&status.stdout))) +} + +/// FNV-1a over the tracked diff and the status listing: deterministic across +/// toolchains, unlike `DefaultHasher`, and needs no build dependency. +fn diff_hash(status: &[u8]) -> String { + let diff = Command::new("git") + .args(["diff", "HEAD", "--binary"]) + .output() + .ok() + .filter(|o| o.status.success()) + .map(|o| o.stdout) + .unwrap_or_default(); + let mut hash: u64 = 0xcbf2_9ce4_8422_2325; + for byte in diff.iter().chain(status) { + hash ^= u64::from(*byte); + hash = hash.wrapping_mul(0x0100_0000_01b3); + } + format!("{:08x}", hash >> 32) } fn git_path(name: &str) -> Option { diff --git a/crates/devup-mcp/build_identity.rs b/crates/devup-mcp/build_identity.rs index b7a2368f..eb776233 100644 --- a/crates/devup-mcp/build_identity.rs +++ b/crates/devup-mcp/build_identity.rs @@ -1,7 +1,14 @@ pub fn git_identity(commit: Option<&str>, dirty: bool) -> Option { + dirty_identity(commit, dirty.then_some("")) +} + +pub fn dirty_identity(commit: Option<&str>, diff: Option<&str>) -> Option { let commit = commit.filter(|value| safe(value))?; - let suffix = if dirty { "-dirty" } else { "" }; - Some(format!("{commit}{suffix}")) + Some(match diff { + None => commit.to_owned(), + Some(hash) if safe(hash) => format!("{commit}-dirty.{hash}"), + Some(_) => format!("{commit}-dirty"), + }) } pub fn safe(value: &str) -> bool { diff --git a/crates/devup-mcp/src/server/asset_guard.rs b/crates/devup-mcp/src/server/asset_guard.rs index 5f0d9394..6fcd599c 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. @@ -290,6 +292,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)); } @@ -331,7 +339,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/asset_job_progress.rs b/crates/devup-mcp/src/server/asset_job_progress.rs new file mode 100644 index 00000000..67ee75de --- /dev/null +++ b/crates/devup-mcp/src/server/asset_job_progress.rs @@ -0,0 +1,31 @@ +use std::collections::BTreeMap; +use serde_json::Value; + +pub(super) 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..433656fe 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_key, 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", @@ -316,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", @@ -383,8 +396,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 +416,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 +454,31 @@ 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)) + .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)); asset["requestedOutputPath"] = json!(path); if asset.get("fileState").is_none() { @@ -474,7 +522,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 +547,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 +657,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..4275857b --- /dev/null +++ b/crates/devup-mcp/src/server/asset_jobs_tests.rs @@ -0,0 +1,453 @@ +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!(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(); +} diff --git a/crates/devup-mcp/src/server/audit_plan.rs b/crates/devup-mcp/src/server/audit_plan.rs index 8705e28e..2b559e95 100644 --- a/crates/devup-mcp/src/server/audit_plan.rs +++ b/crates/devup-mcp/src/server/audit_plan.rs @@ -11,3 +11,7 @@ // 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"); +include!("audit_plan/component_width.rs"); +include!("audit_plan/plan_document.rs"); +include!("audit_plan/dynamic_urls.rs"); diff --git a/crates/devup-mcp/src/server/audit_plan/component_width.rs b/crates/devup-mcp/src/server/audit_plan/component_width.rs new file mode 100644 index 00000000..9ad0fc47 --- /dev/null +++ b/crates/devup-mcp/src/server/audit_plan/component_width.rs @@ -0,0 +1,45 @@ +impl Sources { + fn effective(&self, frames: &[(u32, String, Option)]) -> (Self, Vec) { + let Some((_, first)) = self.payloads.first() else { + return (Self { payloads: Vec::new(), substitutions: Vec::new() }, Vec::new()); + }; + let mut snapshot = first.snapshot.clone(); + for (_, payload) in &self.payloads { + snapshot.nodes.extend(payload.snapshot.nodes.clone()); + } + let roots = snapshot.nodes.iter().filter(|(_, node)| node.node_type == "FRAME") + .filter(|(id, _)| !snapshot.nodes.values().any(|parent| parent.typed_view().child_ids().any(|child| child == id.as_str()))) + .map(|(id, _)| id.clone()).collect::>(); + let substitutions = if self.substitutions.is_empty() { + super::screen_plan::component_width_mismatches(&snapshot, &roots) + } else { self.substitutions.clone() }.into_iter().filter(|row| frames.iter().any(|(_,id,_)|row["screen"] == *id)).collect::>(); + let mut payloads = Vec::new(); + for (artifact, original) in &self.payloads { + let mut payload = original.as_ref().clone(); + for substitution in &substitutions { + let (Some(suspect), Some(correct), Some(screen)) = (substitution["nodeId"].as_str(),substitution["correctNodeId"].as_str(),substitution["screen"].as_str()) else { continue }; + if !payload.snapshot.nodes.contains_key(suspect) { continue; } + let Some(source) = self.payloads.iter().map(|(_,payload)|payload.as_ref()).find(|payload|payload.snapshot.nodes.contains_key(correct)) else { continue }; + let (Some(old_box),Some(new_box),Some(frame_box)) = (bounds(&payload,suspect),bounds(source,correct),bounds(&payload,screen)) else { continue }; + let mut stack = vec![correct.to_owned()]; + let mut seen = BTreeSet::new(); + while let Some(id) = stack.pop() { + if !seen.insert(id.clone()) { continue; } + let Some(node) = source.snapshot.nodes.get(&id) else { continue }; + stack.extend(node.typed_view().child_ids().map(str::to_owned)); + let mut replacement = node.clone(); + if let Some(position) = bounds(source,&id) { + replacement.fields.insert("absoluteBoundingBox".into(),json!({ + "x":frame_box.x + position.x - new_box.x,"y":old_box.y + position.y - new_box.y, + "width":if id == correct {frame_box.width} else {position.width},"height":position.height})); + } + let destination = if id == correct {suspect} else {&id}; + replacement.id = destination.into(); + payload.snapshot.nodes.insert(destination.into(),replacement); + } + } + payloads.push((artifact.clone(),Arc::new(payload))); + } + (Self {payloads,substitutions:substitutions.clone()},substitutions) + } +} diff --git a/crates/devup-mcp/src/server/audit_plan/dynamic_urls.rs b/crates/devup-mcp/src/server/audit_plan/dynamic_urls.rs new file mode 100644 index 00000000..7cf3eb3d --- /dev/null +++ b/crates/devup-mcp/src/server/audit_plan/dynamic_urls.rs @@ -0,0 +1,78 @@ +/// The concrete URLs a dynamic route is audited at, and where they came from. +pub(super) struct Dynamic { + pub urls: Vec, + pub source: &'static str, + pub why: String, + pub assumption: Option, +} + +/// `[slug]` -> ("slug", false); `[...slug]` and `[[...slug]]` -> ("slug", true). +fn params(pattern: &str) -> Vec<(String, bool)> { + pattern + .split('/') + .filter(|segment| segment.starts_with('[') && segment.ends_with(']')) + .map(|segment| { + let inner = segment.trim_matches(|c| c == '[' || c == ']'); + match inner.strip_prefix("...") { + Some(name) => (name.to_owned(), true), + None => (inner.to_owned(), false), + } + }) + .collect() +} + +fn fill(pattern: &str, values: &BTreeMap) -> Option { + let mut out = Vec::new(); + for segment in pattern.split('/') { + if segment.starts_with('[') && segment.ends_with(']') { + let inner = segment.trim_matches(|c| c == '[' || c == ']'); + out.push(values.get(inner.trim_start_matches("...")).cloned()?); + } else { + out.push(segment.to_owned()); + } + } + Some(out.join("/")) +} + +/// Every literal value of `key` in `text`: `key: 'a'`, `"key": "a"`, or +/// `key: ['a', 'b']` (joined with `/` for a catch-all). +pub(super) fn values_of(text: &str, key: &str) -> Vec { + let mut found = Vec::new(); + let bytes = text.as_bytes(); + let mut from = 0; + while let Some(at) = text[from..].find(key) { + let start = from + at; + from = start + key.len(); + let before = start.checked_sub(1).map(|i| bytes[i] as char); + if before.is_some_and(|c| c.is_alphanumeric() || c == '_' || c == '$' || c == '.') { + continue; + } + let mut rest = text[from..].trim_start_matches(['"', '\'']).trim_start(); + let Some(after) = rest.strip_prefix(':') else { + continue; + }; + rest = after.trim_start(); + let quoted = |rest: &str| -> Option<(String, usize)> { + let quote = rest + .chars() + .next() + .filter(|c| matches!(c, '"' | '\'' | '`'))?; + let end = rest[1..].find(quote)?; + Some((rest[1..=end].to_owned(), end + 2)) + }; + if let Some((value, _)) = quoted(rest) { + found.push(value); + } else if let Some(list) = rest.strip_prefix('[') { + let end = list.find(']').unwrap_or(list.len()); + let parts = list[..end] + .split(',') + .filter_map(|part| quoted(part.trim()).map(|(value, _)| value)) + .collect::>(); + if !parts.is_empty() { + found.push(parts.join("/")); + } + } + } + found.dedup(); + found +} diff --git a/crates/devup-mcp/src/server/audit_plan/plan_document.rs b/crates/devup-mcp/src/server/audit_plan/plan_document.rs new file mode 100644 index 00000000..9b65da64 --- /dev/null +++ b/crates/devup-mcp/src/server/audit_plan/plan_document.rs @@ -0,0 +1,89 @@ +fn write_component_substitutions( + plan_path: &Path, + policy: &OutputPolicy, + substitutions: &[Value], +) -> Result<(), DevupError> { + if !substitutions.is_empty() { + let bytes = std::fs::read(plan_path).map_err(|error| invalid(error.to_string()))?; + let mut document: Value = serde_json::from_slice(&bytes).map_err(|error| invalid(error.to_string()))?; + document["componentSubstitutions"] = json!(substitutions); + let target = policy.resolve(&plan_path.to_string_lossy())?; + let mut transaction = super::output::OutputTransaction::new(); + transaction.stage("componentSubstitutions", target, &serde_json::to_vec_pretty(&document).map_err(|error| invalid(error.to_string()))?)?; + transaction.commit()?; + } + Ok(()) +} + +const REFERENCES_DIR: &str = ".devup/runtime-audit/references"; + +/// Writes each frame's referencePng beside the audit plans, so a page +/// comparison can still run as `reference: {path}` after the in-memory +/// artifact has expired or the server restarted. +fn persist_references( + root: &str, + sources: &Sources, + policy: &OutputPolicy, +) -> Result, DevupError> { + use base64::Engine as _; + let mut transaction = super::output::OutputTransaction::new(); + let mut written = Vec::new(); + for (artifact_id, payload) in &sources.payloads { + let (Some(node_id), Some(reference)) = + (payload.target.node_id.as_deref(), payload.reference_png.as_ref()) + else { + continue; + }; + let Ok(bytes) = + base64::engine::general_purpose::STANDARD.decode(&reference.data_base64) + else { + continue; + }; + let path = Path::new(root) + .join(REFERENCES_DIR) + .join(format!("{}.png", slug(node_id))); + let target = policy.resolve(&path.to_string_lossy())?; + transaction.stage(format!("reference:{node_id}"), target, &bytes)?; + written.push(json!({"nodeId": node_id, "artifactId": artifact_id, + "path": path.display().to_string(), "sha256": reference.sha256, + "compareWith": {"reference": {"path": path.display().to_string()}}})); + } + if !written.is_empty() { + transaction.commit()?; + } + Ok(written) +} + +fn stored_references(root: &str) -> Vec { + let mut found = std::fs::read_dir(Path::new(root).join(REFERENCES_DIR)) + .into_iter() + .flatten() + .flatten() + .map(|entry| entry.path()) + .filter(|path| path.extension().is_some_and(|ext| ext == "png")) + .map(|path| path.display().to_string()) + .collect::>(); + found.sort(); + found +} + +fn local_imports(source: &str) -> Vec<(String, String)> { + let mut found = Vec::new(); + for line in source.lines() { + let line = line.trim(); + let Some(rest) = line.strip_prefix("import ") else { + continue; + }; + let Some((names, module)) = rest.rsplit_once(" from ") else { + continue; + }; + let module = module + .trim() + .trim_end_matches(';') + .trim_matches(['"', '\'']); + if module.starts_with("./") || module.starts_with("../") || module.starts_with("@/") { + found.push((names.to_owned(), module.to_owned())); + } + } + found +} diff --git a/crates/devup-mcp/src/server/audit_plan/planning_and_capture.rs b/crates/devup-mcp/src/server/audit_plan/planning_and_capture.rs index e50960d6..a6f26544 100644 --- a/crates/devup-mcp/src/server/audit_plan/planning_and_capture.rs +++ b/crates/devup-mcp/src/server/audit_plan/planning_and_capture.rs @@ -74,6 +74,7 @@ impl VisualThresholds { /// its referencePng and the native renders of its sections and assets. pub(super) struct Sources { payloads: Vec<(String, Arc)>, + substitutions: Vec, } impl Sources { @@ -87,12 +88,19 @@ impl Sources { "issue": "missing or expired; export the frame again and pass the new artifactId"})), } } - (Self { payloads }, missing) + ( + Self { + payloads, + substitutions: Vec::new(), + }, + missing, + ) } #[cfg(test)] pub(super) fn from_payloads(payloads: Vec<(String, CollectedPayload)>) -> Self { Self { + substitutions: Vec::new(), payloads: payloads .into_iter() .map(|(id, payload)| (id, Arc::new(payload))) @@ -128,6 +136,12 @@ impl Sources { /// A native-node PNG of the node at scale 1, the only render an element /// capture at deviceScaleFactor 1 can be compared with pixel for pixel. fn native_render(&self, node_id: &str) -> Option<(&str, &str)> { + let node_id = self + .substitutions + .iter() + .find(|row| row["nodeId"] == node_id) + .and_then(|row| row["correctNodeId"].as_str()) + .unwrap_or(node_id); self.payloads.iter().find_map(|(id, payload)| { payload .assets @@ -251,6 +265,7 @@ pub(super) struct UrlPlan { pub plan: PlanInput, pub missing: Vec, pub notes: Vec, + pub substitutions: Vec, } pub(super) fn url_plan( @@ -259,6 +274,8 @@ pub(super) fn url_plan( sources: &Sources, thresholds: &VisualThresholds, ) -> Result { + let (effective, substitutions) = sources.effective(&target.frames); + let sources = &effective; let origin = url::Url::parse(base_url) .map_err(|_| invalid(format!("baseUrl {base_url} is not a URL.")))? .origin() @@ -307,15 +324,21 @@ pub(super) fn url_plan( // Health and containment at every width. let mut checks = vec![ - check(json!({"kind":"links-resolve", "label":"all connected URLs resolve", "plannedTargets":[target.url]})), - check(json!({"kind": "no-clipped-content", "label": "navigation and card content not cut off"})), + check( + json!({"kind":"links-resolve", "label":"all connected URLs resolve", "plannedTargets":[target.url]}), + ), + check( + json!({"kind": "no-clipped-content", "label": "navigation and card content not cut off"}), + ), check(json!({"kind": "sequence-order", "label": "numbered cards in reading order"})), check(json!({"kind": "section-layout", "label": "responsive sections and text columns"})), check(json!({"kind": "page-health", "label": "page health"})), check(json!({"kind": "styles-applied", "label": "page styles delivered"})), check(json!({"kind": "no-low-contrast-text", "label": "visible text contrast"})), check(json!({"kind": "image-loaded", "label": "images and backgrounds loaded"})), - check(json!({"kind": "native-appearance", "label": "controls drawn without native browser chrome"})), + check( + json!({"kind": "native-appearance", "label": "controls drawn without native browser chrome"}), + ), check(json!({"kind": "stale-resources", "label": "no stale or missing build resources"})), check(json!({"kind": "no-horizontal-overflow", "label": "no horizontal overflow"})), check(json!({"kind": "no-clipped-text", "label": "no clipped text"})), @@ -325,22 +348,34 @@ pub(super) fn url_plan( let mut frame_refs = Vec::new(); let assets_everywhere = sources.asset_nodes(); let mut nav_checked = false; - for (width, frame_id, _) in &target.frames { + for (width, frame_id, target_height) in &target.frames { + let frame_height = target_height + .or_else(|| { + sources + .frame(frame_id) + .and_then(|(_, payload)| bounds(payload, frame_id).map(|bounds| bounds.height)) + }) + .map(|height| height.round() as u32); let Some((artifact_id, payload)) = sources.frame(frame_id) else { + checks.push(check(json!({"kind": "page-health", "label": "design reference unavailable", "widths": [width], "unavailableReason": "Design comparison inconclusive: frame artifact missing or expired; export the frame again."}))); missing.push(json!({"kind": "frame", "nodeId": frame_id, "width": width, "why": "No artifact passed holds this frame; section, asset and geometry checks for this width are missing.", "call": {"tool": "devup_figma_export", "arguments": {"url": format!(""), "outputs": ["tsx", "referencePng"], "projectRoot": ""}}})); - frame_refs.push(json!({"width": width, "nodeId": frame_id})); + frame_refs.push(json!({"width": width, "height": frame_height, "nodeId": frame_id})); continue; }; - let reference = sources.reference_png(frame_id); + let corrected = substitutions.iter().any(|row| row["screen"] == *frame_id); + let reference = (!corrected) + .then(|| sources.reference_png(frame_id)) + .flatten(); if reference.is_none() { + checks.push(check(json!({"kind": "page-health", "label": "design reference unavailable", "widths": [width], "unavailableReason": "Design comparison inconclusive: referencePng missing or expired; export the frame again."}))); missing.push(json!({"kind": "referencePng", "nodeId": frame_id, "width": width, "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"]}}})); } frame_refs.push( - json!({"width": width, "nodeId": frame_id, "artifactId": reference, + json!({"width": width, "height": frame_height, "nodeId": frame_id, "artifactId": reference, "threshold": thresholds.for_node(frame_id, "page")}), ); let _ = artifact_id; @@ -376,7 +411,10 @@ pub(super) fn url_plan( "subject": node(section), "container": node(frame_id), "nodeId": section}), )); } - if !is_asset && bounds(payload, section).is_some_and(|section_box| section_box.width >= frame_box.width * 0.5) { + if !is_asset + && bounds(payload, section) + .is_some_and(|section_box| section_box.width >= frame_box.width * 0.5) + { layout.push(check(json!({"kind": "section-layout", "label": format!("{name} remains a visible section"), "subject": node_at(*width, section), "container": node_at(*width, frame_id), "widths": viewports.iter().filter(|viewport| { target.frames.iter().filter(|(drawn, _, _)| *drawn <= viewport.width).max_by_key(|(drawn, _, _)| *drawn) @@ -422,7 +460,15 @@ pub(super) fn url_plan( *width, thresholds.for_node(section, scope), )), - None => renders_missing.push(section.clone()), + None => renders_missing.push( + sources + .substitutions + .iter() + .find(|row| row["nodeId"] == *section) + .and_then(|row| row["correctNodeId"].as_str()) + .unwrap_or(section) + .to_owned(), + ), } } // Assets below the sections, each against its own render and in @@ -553,6 +599,7 @@ pub(super) fn url_plan( plan, missing, notes, + substitutions, }) } @@ -651,18 +698,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<'_>, @@ -677,17 +721,37 @@ pub(super) async fn write( artifact_ids, thresholds, } = *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() + let mut ids = artifact_ids.to_vec(); + ids.extend( + plan["source"]["componentWidthArtifacts"] + .as_array() + .into_iter() + .flatten() + .filter_map(Value::as_str) + .map(str::to_owned), + ); + ids.sort(); + ids.dedup(); + let (mut sources, expired) = Sources::load(&ids, store).await; + let persisted_references = persist_references(root, &sources, policy)?; + let expired = expired .into_iter() - .flatten() + .map(|mut entry| { + entry["persistedReferences"] = json!(stored_references(root)); + entry + }) + .collect::>(); + sources.substitutions = plan["componentWidthMismatches"] + .as_array() + .cloned() + .unwrap_or_default(); + let evidence = super::screen_plan::project_evidence(root).await?; + let audit_routes = super::screen_plan::audit_routes(plan); + let routes = audit_routes + .iter() .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() { @@ -751,11 +815,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,10 +849,27 @@ 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 { + check.label = Some(format!( + "State {node}: {}", + check.label.as_deref().unwrap_or(&check.kind) + )); + } + } for check in &mut built.plan.screens[0].checks { if check.kind == "links-resolve" { - check.planned_targets = routes.iter().filter_map(|route| route["url"].as_str().map(str::to_owned)).collect(); - check.existing_routes = evidence.routes.iter().map(|(route, _)| route.clone()).collect(); + check.planned_targets = routes + .iter() + .filter_map(|route| route["url"].as_str().map(str::to_owned)) + .collect(); + check.existing_routes = evidence + .routes + .iter() + .map(|(route, _)| route.clone()) + .collect(); } } let plan_path = Path::new(root) @@ -814,9 +898,13 @@ pub(super) async fn write( store, ) .await?; + write_component_substitutions(&plan_path, policy, &built.substitutions)?; written.push(json!({ "url": url, "screen": screen, + "app": app, + "baseUrl": base, + "state": route["state"], "planPath": created["writtenTo"], "planId": created["planId"], "reportDirectory": report_dir.display().to_string(), @@ -824,8 +912,10 @@ pub(super) async fn write( "checks": counts, "missingReferences": built.missing, "notes": built.notes, + "componentSubstitutions": built.substitutions, "dynamic": dynamic, "run": ["node", "run.mjs", "--plan", plan_path.display().to_string(), "--out", report_dir.display().to_string()], + "startServer": dev_server_command(Path::new(root), app, base), })); } let missing = written @@ -844,6 +934,7 @@ pub(super) async fn write( "productionStrip": strip, "missingReferences": missing, "expiredArtifacts": expired, + "persistedReferences": persisted_references, "runner": {"directory": runner.display().to_string(), "state": template_state, "install": [["npm", "ci"], ["npx", "playwright", "install", "chromium"]], "ifInstallFails": "Record it with devup_screen_plan action status, auditPlanPath and toolingUnavailable: the URL's audit stays inconclusive and the work goes on."}, @@ -891,105 +982,3 @@ fn hidden_elements(records: &[Value], frames: &[(u32, String, Option)]) -> } hidden } - -/// The concrete URLs a dynamic route is audited at, and where they came from. -pub(super) struct Dynamic { - pub urls: Vec, - pub source: &'static str, - pub why: String, - pub assumption: Option, -} - -/// `[slug]` -> ("slug", false); `[...slug]` and `[[...slug]]` -> ("slug", true). -fn params(pattern: &str) -> Vec<(String, bool)> { - pattern - .split('/') - .filter(|segment| segment.starts_with('[') && segment.ends_with(']')) - .map(|segment| { - let inner = segment.trim_matches(|c| c == '[' || c == ']'); - match inner.strip_prefix("...") { - Some(name) => (name.to_owned(), true), - None => (inner.to_owned(), false), - } - }) - .collect() -} - -fn fill(pattern: &str, values: &BTreeMap) -> Option { - let mut out = Vec::new(); - for segment in pattern.split('/') { - if segment.starts_with('[') && segment.ends_with(']') { - let inner = segment.trim_matches(|c| c == '[' || c == ']'); - out.push(values.get(inner.trim_start_matches("...")).cloned()?); - } else { - out.push(segment.to_owned()); - } - } - Some(out.join("/")) -} - -/// Every literal value of `key` in `text`: `key: 'a'`, `"key": "a"`, or -/// `key: ['a', 'b']` (joined with `/` for a catch-all). -pub(super) fn values_of(text: &str, key: &str) -> Vec { - let mut found = Vec::new(); - let bytes = text.as_bytes(); - let mut from = 0; - while let Some(at) = text[from..].find(key) { - let start = from + at; - from = start + key.len(); - let before = start.checked_sub(1).map(|i| bytes[i] as char); - if before.is_some_and(|c| c.is_alphanumeric() || c == '_' || c == '$' || c == '.') { - continue; - } - let mut rest = text[from..].trim_start_matches(['"', '\'']).trim_start(); - let Some(after) = rest.strip_prefix(':') else { - continue; - }; - rest = after.trim_start(); - let quoted = |rest: &str| -> Option<(String, usize)> { - let quote = rest - .chars() - .next() - .filter(|c| matches!(c, '"' | '\'' | '`'))?; - let end = rest[1..].find(quote)?; - Some((rest[1..=end].to_owned(), end + 2)) - }; - if let Some((value, _)) = quoted(rest) { - found.push(value); - } else if let Some(list) = rest.strip_prefix('[') { - let end = list.find(']').unwrap_or(list.len()); - let parts = list[..end] - .split(',') - .filter_map(|part| quoted(part.trim()).map(|(value, _)| value)) - .collect::>(); - if !parts.is_empty() { - found.push(parts.join("/")); - } - } - } - found.dedup(); - found -} - -/// Local modules a page imports: `./x`, `../x` or `@/x`, with the names -/// imported from each. -fn local_imports(source: &str) -> Vec<(String, String)> { - let mut found = Vec::new(); - for line in source.lines() { - let line = line.trim(); - let Some(rest) = line.strip_prefix("import ") else { - continue; - }; - let Some((names, module)) = rest.rsplit_once(" from ") else { - continue; - }; - let module = module - .trim() - .trim_end_matches(';') - .trim_matches(['"', '\'']); - if module.starts_with("./") || module.starts_with("../") || module.starts_with("@/") { - found.push((names.to_owned(), module.to_owned())); - } - } - found -} diff --git a/crates/devup-mcp/src/server/audit_plan/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 28c83ff4..1aaedc69 100644 --- a/crates/devup-mcp/src/server/audit_plan_tests.rs +++ b/crates/devup-mcp/src/server/audit_plan_tests.rs @@ -1,5 +1,192 @@ use super::*; +#[test] +fn width_mismatch_audit_uses_mobile_subtree_at_suspect_position() { + let mut nodes = serde_json::Map::new(); + for (screen, width, master, y, height) in [ + ("mobile-a", 360.0, "mobile", 700.0, 400.0), + ("mobile-b", 360.0, "mobile", 600.0, 400.0), + ("mobile-c", 360.0, "mobile", 800.0, 400.0), + ("home", 360.0, "desktop", 1200.0, 200.0), + ("wide", 1440.0, "desktop", 900.0, 200.0), + ] { + let footer = format!("{screen}-footer"); + let child = format!("{screen}-text"); + let (id, node) = frame( + screen, + screen, + [0.0, 0.0, width, 1600.0], + &[&footer], + json!({}), + ); + nodes.insert(id, node); + let (id, mut node) = frame( + &footer, + "Footer", + [0.0, y, width, height], + &[&child], + json!({"mainComponentId":master,"mainComponentName":"Footer"}), + ); + node["type"] = json!("INSTANCE"); + nodes.insert(id, node); + let (id, node) = frame( + &child, + "Text", + [20.0, y + 30.0, 300.0, 50.0], + &[], + json!({}), + ); + nodes.insert(id, node); + } + let sources = Sources::from_payloads(vec![( + "all".into(), + payload( + "home", + Value::Object(nodes), + json!([native("mobile-a-footer", "mobile-render")]), + true, + ), + )]); + let built = url_plan( + &Target { + url: "/", + screen: "home".into(), + file: None, + frames: vec![(360, "home".into(), Some(1600.0))], + next_url: None, + hidden: vec![], + }, + "http://localhost:3000", + &sources, + &VisualThresholds::default(), + ) + .unwrap(); + let checks = &built.plan.screens[0].checks; + let footer = checks + .iter() + .find(|check| check.kind == "bounds" && check.node_id.as_deref() == Some("home-footer")) + .unwrap(); + let expected = serde_json::to_value(&footer.expected).unwrap(); + assert_eq!( + expected, + json!({"x":0.0,"y":1200.0,"width":360.0,"height":400.0}) + ); + let capture = checks + .iter() + .find(|check| check.kind == "capture" && check.node_id.as_deref() == Some("home-footer")) + .unwrap(); + assert_eq!( + serde_json::to_value(&capture.reference).unwrap()["assetId"], + "mobile-render" + ); + assert!( + checks.iter().any( + |check| check.kind == "bounds" && check.node_id.as_deref() == Some("mobile-a-text") + ) + ); + assert!( + !checks + .iter() + .any(|check| check.node_id.as_deref() == Some("home-text")) + ); + assert_eq!(built.substitutions[0]["correctNodeId"], "mobile-a-footer"); +} + +#[test] +fn dev_server_command_uses_the_route_app_and_its_package_manager() { + let project = Project::new("dev-command"); + project.write("bun.lock", ""); + project.write( + "apps/front/package.json", + r#"{"scripts":{"dev":"vinext dev"}}"#, + ); + let command = dev_server_command( + Path::new(project.root()), + "apps/front", + "http://localhost:3000", + ); + 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"); + 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}, @@ -145,6 +332,37 @@ fn home() -> Target<'static> { } } +#[test] +fn tall_audit_frames_keep_drawn_height_without_oversized_viewports() { + let mut target = home(); + target.frames = vec![(1440, "10:1".into(), Some(5953.0))]; + let built = url_plan( + &target, + "http://localhost:3000", + &sources(), + &VisualThresholds::default(), + ) + .unwrap(); + let document = serde_json::to_value(&built.plan).unwrap(); + assert_eq!(document["screens"][0]["figma"]["frames"][0]["height"], 5953); + assert_eq!( + built + .plan + .viewports + .iter() + .find(|viewport| viewport.width == 1440) + .unwrap() + .height, + 900 + ); + assert!( + !built + .notes + .iter() + .any(|note| note.contains("comparison stays inconclusive")) + ); +} + fn kinds(plan: &PlanInput) -> BTreeSet<&str> { plan.screens[0] .checks @@ -191,9 +409,16 @@ fn a_two_url_audit_plan_checks_the_page_its_sections_assets_geometry_overflow_an } let checks = &home.plan.screens[0].checks; for kind in ["no-low-contrast-text", "image-loaded"] { - assert!(checks.iter().any(|check| check.kind == kind && check.widths.is_empty())); + assert!( + checks + .iter() + .any(|check| check.kind == kind && check.widths.is_empty()) + ); } - let completeness = checks.iter().filter(|check| check.kind == "rendered-completeness").collect::>(); + let completeness = checks + .iter() + .filter(|check| check.kind == "rendered-completeness") + .collect::>(); assert_eq!(completeness.len(), 2); for check in completeness { assert_eq!(check.widths.len(), 1); @@ -598,252 +823,4 @@ impl Drop for Project { } } -fn blog_route() -> Value { - json!({"url": "/blog/[slug]", "file": "src/app/blog/[slug]/page.tsx", "app": ".", "nodeIds": ["20:1"], "drawnWidths": [1440]}) -} - -#[test] -fn a_dynamic_route_takes_its_literal_static_params() { - let project = Project::new("literal"); - project.write( - "src/app/blog/[slug]/page.tsx", - "export function generateStaticParams() {\n return [{ slug: 'hello' }, { slug: \"second-post\" }]\n}\nexport default function Page() { return null }\n", - ); - let found = dynamic_urls( - project.root(), - &blog_route(), - &Value::Null, - &Sources::from_payloads(Vec::new()), - ); - assert_eq!(found.source, "generateStaticParams"); - assert_eq!(found.urls, ["/blog/hello", "/blog/second-post"]); -} - -#[test] -fn a_dynamic_route_reads_the_local_data_its_static_params_map() { - let project = Project::new("data"); - project.write( - "src/app/blog/[slug]/page.tsx", - "import { posts } from '@/data/posts'\nexport async function generateStaticParams() {\n return posts.map((post) => ({ slug: post.slug }))\n}\nexport default function Page() { return null }\n", - ); - project.write( - "src/data/posts.ts", - "export const posts = [\n { slug: 'launch', title: 'Launch' },\n { slug: 'roadmap', title: 'Roadmap' },\n]\n", - ); - let found = dynamic_urls( - project.root(), - &blog_route(), - &Value::Null, - &Sources::from_payloads(Vec::new()), - ); - assert_eq!(found.urls, ["/blog/launch", "/blog/roadmap"]); -} - -#[test] -fn a_dynamic_route_without_params_takes_the_links_audited_pages_hold() { - let project = Project::new("links"); - let links = - json!({"/": ["/blog/from-home", "/about", "/blog/other"], "/about": ["/blog/from-home"]}); - let found = dynamic_urls( - project.root(), - &blog_route(), - &links, - &Sources::from_payloads(Vec::new()), - ); - assert_eq!(found.source, "links on audited pages"); - assert_eq!(found.urls, ["/blog/from-home", "/blog/other"]); - assert!(found.assumption.is_none()); -} - -#[test] -fn a_dynamic_route_falls_back_to_its_detail_frame_as_an_assumption() { - let project = Project::new("frame"); - let detail = [ - frame("20:1", "Blog detail", [0.0, 0.0, 1440.0, 900.0], &["20:2"], json!({})), - ("20:2".to_owned(), json!({"id": "20:2", "type": "TEXT", "fields": {"characters": "Our First Release!", "parentId": "20:1"}})), - ] - .into_iter() - .collect::>(); - let sources = Sources::from_payloads(vec![( - "art-blog".into(), - payload("20:1", Value::Object(detail), json!([]), true), - )]); - let found = dynamic_urls(project.root(), &blog_route(), &Value::Null, &sources); - assert_eq!(found.source, "Figma detail frame"); - assert_eq!(found.urls, ["/blog/our-first-release"]); - assert!(found.assumption.unwrap().contains("Our First Release!")); - - let nothing = dynamic_urls( - project.root(), - &blog_route(), - &Value::Null, - &Sources::from_payloads(Vec::new()), - ); - assert!(nothing.urls.is_empty()); - assert!(nothing.why.contains("generateStaticParams")); -} - -#[test] -fn a_catch_all_route_joins_its_segments() { - assert_eq!( - values_of("{ slug: ['docs', 'intro'] }", "slug"), - ["docs/intro"] - ); - assert_eq!(params("/docs/[...slug]"), [("slug".to_owned(), true)]); - assert_eq!( - fill( - "/docs/[...slug]", - &BTreeMap::from([("slug".to_owned(), "docs/intro".to_owned())]) - ) - .as_deref(), - Some("/docs/docs/intro") - ); -} - -const EXPORTED: &str = "export default function Page() {\n return (\n \n \n \n )\n}\n"; - -fn geometry_audit() -> Value { - json!({"findings": [{"id": "home|bounds|10:132|1440", "class": "layout-position", "kind": "bounds", - "nodeId": "10:132", "routePath": "/", "width": 1440, "reason": "position: x off by 1px", - "evidence": {"screenshot": "report/home.png", "measured": { - "expected": {"x": 560, "y": 40, "width": 400, "height": 300}, - "actual": {"x": 561, "y": 40, "width": 400, "height": 300}}}}]}) -} - -fn exported_project(name: &str) -> (Project, OutputPolicy) { - let project = Project::new(name); - let policy = OutputPolicy::from_roots(vec![project.0.clone()]).unwrap(); - let response = json!({"tsx": EXPORTED, "figmaNodeIds": {"root": "10:1"}, - "outputResults": {"tsx": {"_sourceMap": [ - {"nodeId": "10:132", "property": "width", "generatedProperty": "w=\"400px\"", "resolution": "exact"}]}}}); - let written = - super::super::export_record::write(&policy, project.root(), "FILEKEY", "10:1", &response) - .unwrap() - .unwrap(); - assert!(written.ends_with("10-1.json"), "{written}"); - (project, policy) -} - -#[test] -fn a_mismatch_with_the_code_still_equal_to_the_export_is_a_converter_bug_with_a_repro() { - let (project, policy) = exported_project("converter-bug"); - // The agent added behaviour, not layout: still the export's element. - project.write( - "src/app/page.tsx", - &EXPORTED.replace( - "", "w=\"401px\" ml=\"2px\" />"), - ); - let mut audit = geometry_audit(); - classify_geometry( - &mut audit, - project.root(), - &["src/app/page.tsx".to_owned()], - &policy, - ); - let finding = &audit["findings"][0]; - assert_eq!(finding["class"], "code-drift", "{finding}"); - assert_eq!( - finding["evidence"]["drift"]["missing"], - json!(["w=\"400px\""]) - ); - assert_eq!(finding["evidence"]["drift"]["added"], json!(["ml=\"2px\""])); - assert!( - finding["fixHint"] - .as_str() - .unwrap() - .contains("Restore the values the export wrote") - ); - assert!(!project.0.join(".devup/converter-bugs").exists()); -} - -#[test] -fn a_mismatch_without_an_export_record_keeps_its_class_and_says_why() { - let project = Project::new("no-record"); - let policy = OutputPolicy::from_roots(vec![project.0.clone()]).unwrap(); - let mut audit = geometry_audit(); - classify_geometry(&mut audit, project.root(), &[], &policy); - assert_eq!(audit["findings"][0]["class"], "layout-position"); - assert!( - audit["findings"][0]["evidence"]["cause"] - .as_str() - .unwrap() - .contains("No export record") - ); -} - -#[test] -fn an_asset_capture_waiting_for_comparison_blocks_completion() { - let mut plan = json!({"audit":{}}); - let audit = json!({"screenTable":[{"routePath":"/", "width":1440}], "visualComparisons":[ - {"id":"home|asset|icon|1440", "routePath":"/", "viewport":{"width":1440}, "blockers":[]} - ]}); - record_round(&mut plan, &audit, "run", &[], &BTreeMap::new(), 3); - assert_eq!(plan["audit"]["visualPending"].as_array().unwrap().len(), 1); - record_round(&mut plan, &audit, "run", &[VisualResultInput { id:"home|asset|icon|1440".into(), verdict:"pass".into(), changed_ratio:Some(0.0), diff_path:None, reason:None }], &BTreeMap::new(), 3); - assert!(plan["audit"]["visualPending"].as_array().unwrap().is_empty()); -} +include!("audit_plan_tests_routes_and_drift.rs"); diff --git a/crates/devup-mcp/src/server/audit_plan_tests_routes_and_drift.rs b/crates/devup-mcp/src/server/audit_plan_tests_routes_and_drift.rs new file mode 100644 index 00000000..482ed285 --- /dev/null +++ b/crates/devup-mcp/src/server/audit_plan_tests_routes_and_drift.rs @@ -0,0 +1,267 @@ +fn blog_route() -> Value { + json!({"url": "/blog/[slug]", "file": "src/app/blog/[slug]/page.tsx", "app": ".", "nodeIds": ["20:1"], "drawnWidths": [1440]}) +} + +#[test] +fn a_dynamic_route_takes_its_literal_static_params() { + let project = Project::new("literal"); + project.write( + "src/app/blog/[slug]/page.tsx", + "export function generateStaticParams() {\n return [{ slug: 'hello' }, { slug: \"second-post\" }]\n}\nexport default function Page() { return null }\n", + ); + let found = dynamic_urls( + project.root(), + &blog_route(), + &Value::Null, + &Sources::from_payloads(Vec::new()), + ); + assert_eq!(found.source, "generateStaticParams"); + assert_eq!(found.urls, ["/blog/hello", "/blog/second-post"]); +} + +#[test] +fn a_dynamic_route_reads_the_local_data_its_static_params_map() { + let project = Project::new("data"); + project.write( + "src/app/blog/[slug]/page.tsx", + "import { posts } from '@/data/posts'\nexport async function generateStaticParams() {\n return posts.map((post) => ({ slug: post.slug }))\n}\nexport default function Page() { return null }\n", + ); + project.write( + "src/data/posts.ts", + "export const posts = [\n { slug: 'launch', title: 'Launch' },\n { slug: 'roadmap', title: 'Roadmap' },\n]\n", + ); + let found = dynamic_urls( + project.root(), + &blog_route(), + &Value::Null, + &Sources::from_payloads(Vec::new()), + ); + assert_eq!(found.urls, ["/blog/launch", "/blog/roadmap"]); +} + +#[test] +fn a_dynamic_route_without_params_takes_the_links_audited_pages_hold() { + let project = Project::new("links"); + let links = + json!({"/": ["/blog/from-home", "/about", "/blog/other"], "/about": ["/blog/from-home"]}); + let found = dynamic_urls( + project.root(), + &blog_route(), + &links, + &Sources::from_payloads(Vec::new()), + ); + assert_eq!(found.source, "links on audited pages"); + assert_eq!(found.urls, ["/blog/from-home", "/blog/other"]); + assert!(found.assumption.is_none()); +} + +#[test] +fn a_dynamic_route_falls_back_to_its_detail_frame_as_an_assumption() { + let project = Project::new("frame"); + let detail = [ + frame("20:1", "Blog detail", [0.0, 0.0, 1440.0, 900.0], &["20:2"], json!({})), + ("20:2".to_owned(), json!({"id": "20:2", "type": "TEXT", "fields": {"characters": "Our First Release!", "parentId": "20:1"}})), + ] + .into_iter() + .collect::>(); + let sources = Sources::from_payloads(vec![( + "art-blog".into(), + payload("20:1", Value::Object(detail), json!([]), true), + )]); + let found = dynamic_urls(project.root(), &blog_route(), &Value::Null, &sources); + assert_eq!(found.source, "Figma detail frame"); + assert_eq!(found.urls, ["/blog/our-first-release"]); + assert!(found.assumption.unwrap().contains("Our First Release!")); + + let nothing = dynamic_urls( + project.root(), + &blog_route(), + &Value::Null, + &Sources::from_payloads(Vec::new()), + ); + assert!(nothing.urls.is_empty()); + assert!(nothing.why.contains("generateStaticParams")); +} + +#[test] +fn a_catch_all_route_joins_its_segments() { + assert_eq!( + values_of("{ slug: ['docs', 'intro'] }", "slug"), + ["docs/intro"] + ); + assert_eq!(params("/docs/[...slug]"), [("slug".to_owned(), true)]); + assert_eq!( + fill( + "/docs/[...slug]", + &BTreeMap::from([("slug".to_owned(), "docs/intro".to_owned())]) + ) + .as_deref(), + Some("/docs/docs/intro") + ); +} + +const EXPORTED: &str = "export default function Page() {\n return (\n \n \n \n )\n}\n"; + +fn geometry_audit() -> Value { + json!({"findings": [{"id": "home|bounds|10:132|1440", "class": "layout-position", "kind": "bounds", + "nodeId": "10:132", "routePath": "/", "width": 1440, "reason": "position: x off by 1px", + "evidence": {"screenshot": "report/home.png", "measured": { + "expected": {"x": 560, "y": 40, "width": 400, "height": 300}, + "actual": {"x": 561, "y": 40, "width": 400, "height": 300}}}}]}) +} + +fn exported_project(name: &str) -> (Project, OutputPolicy) { + let project = Project::new(name); + let policy = OutputPolicy::from_roots(vec![project.0.clone()]).unwrap(); + let response = json!({"tsx": EXPORTED, "figmaNodeIds": {"root": "10:1"}, + "outputResults": {"tsx": {"_sourceMap": [ + {"nodeId": "10:132", "property": "width", "generatedProperty": "w=\"400px\"", "resolution": "exact"}]}}}); + let written = + super::super::export_record::write(&policy, project.root(), "FILEKEY", "10:1", &response) + .unwrap() + .unwrap(); + assert!(written.ends_with("10-1.json"), "{written}"); + (project, policy) +} + +#[test] +fn a_mismatch_with_the_code_still_equal_to_the_export_is_a_converter_bug_with_a_repro() { + let (project, policy) = exported_project("converter-bug"); + // The agent added behaviour, not layout: still the export's element. + project.write( + "src/app/page.tsx", + &EXPORTED.replace( + "", "w=\"401px\" ml=\"2px\" />"), + ); + let mut audit = geometry_audit(); + classify_geometry( + &mut audit, + project.root(), + &["src/app/page.tsx".to_owned()], + &policy, + ); + let finding = &audit["findings"][0]; + assert_eq!(finding["class"], "code-drift", "{finding}"); + assert_eq!( + finding["evidence"]["drift"]["missing"], + json!(["w=\"400px\""]) + ); + assert_eq!(finding["evidence"]["drift"]["added"], json!(["ml=\"2px\""])); + assert!( + finding["fixHint"] + .as_str() + .unwrap() + .contains("Restore the values the export wrote") + ); + assert!(!project.0.join(".devup/converter-bugs").exists()); +} + +#[test] +fn a_mismatch_without_an_export_record_keeps_its_class_and_says_why() { + let project = Project::new("no-record"); + let policy = OutputPolicy::from_roots(vec![project.0.clone()]).unwrap(); + let mut audit = geometry_audit(); + classify_geometry(&mut audit, project.root(), &[], &policy); + assert_eq!(audit["findings"][0]["class"], "layout-position"); + assert!( + audit["findings"][0]["evidence"]["cause"] + .as_str() + .unwrap() + .contains("No export record") + ); +} + +#[test] +fn an_asset_capture_waiting_for_comparison_blocks_completion() { + let mut plan = json!({"audit":{}}); + let audit = json!({"screenTable":[{"routePath":"/", "width":1440}], "visualComparisons":[ + {"id":"home|asset|icon|1440", "routePath":"/", "viewport":{"width":1440}, "blockers":[]} + ]}); + record_round(&mut plan, &audit, "run", &[], &BTreeMap::new(), 3); + assert_eq!(plan["audit"]["visualPending"].as_array().unwrap().len(), 1); + record_round( + &mut plan, + &audit, + "run", + &[VisualResultInput { + id: "home|asset|icon|1440".into(), + verdict: "pass".into(), + changed_ratio: Some(0.0), + diff_path: None, + reason: None, + }], + &BTreeMap::new(), + 3, + ); + assert!( + plan["audit"]["visualPending"] + .as_array() + .unwrap() + .is_empty() + ); +} diff --git a/crates/devup-mcp/src/server/commands/opencode-devup.md b/crates/devup-mcp/src/server/commands/opencode-devup.md index 4c4be15d..149bebf8 100644 --- a/crates/devup-mcp/src/server/commands/opencode-devup.md +++ b/crates/devup-mcp/src/server/commands/opencode-devup.md @@ -100,13 +100,16 @@ items; never claim completion from split or command generation alone. For Figma work, continue with the following branch after project intake: -1. `devup_figma_auth` status. +1. `devup_figma_auth` status. If disconnected, out of view or on a mismatched + relay host, load `figma-bridge-setup`, perform its computer-use setup yourself, + then verify status and a small read. Don't ask the user to click routine menus. 2. `devup_skills` status, then install what is missing, and load the skills. 3. `devup_project_context` with scope theme and scope routes. 4. `devup_screen_plan` draft with projectRoot, the link, the notes and extraQuestions for anything you would otherwise guess later. 5. Ask every question now with the `question` tool: the flat `questions` list - in one call. Never in prose, never during implementation. + in one call. Never in prose. New user-owned decisions after confirm still + use the question tool; workers escalate to the supervisor. 6. `devup_screen_plan` confirm with every answer that applies. 7. Latest first, without asking: at the repository root run `bunx @dependency-check-updates/cli -d -u --rm` - it updates every @@ -116,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 without asking anything: `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 @@ -137,14 +148,26 @@ For Figma work, continue with the following branch after project intake: workQueue order, re-run. A geometry mismatch is code-drift (restore the export) or converter-bug (repro filed, never retried). After maxFixAttempts (default 3) a finding is unresolved with its evidence. No runner: toolingUnavailable, carry on. -11. Never stop to wait: a decision mid-run goes to `devup_screen_plan` defer +11. Continue independent work: an ordinary choice mid-run goes to `devup_screen_plan` defer with the recommended option; take it with assume at the end (a destructive one stays unbuilt). 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. 13. Finish with the status finalReport and summary. + +User-owned decisions, before or after confirm, use the runtime question tool. +Show the evidence table first. Keep table approval and individual decisions as +separate entries in the same call, with headers up to 30 characters and 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 with existing defer/assume actions, not an invented ask action. +Update source models and re-confirm ERD changes before dependent implementation. +Without a question tool, report the limitation and leave that decision unresolved. diff --git a/crates/devup-mcp/src/server/compact.rs b/crates/devup-mcp/src/server/compact.rs index 70933a35..d949924e 100644 --- a/crates/devup-mcp/src/server/compact.rs +++ b/crates/devup-mcp/src/server/compact.rs @@ -306,6 +306,7 @@ pub fn select( } let evidence_fields = [ "projectionIssues", + "instanceOverrides", "projectionEvidence", "completenessReport", "fidelity", @@ -339,7 +340,7 @@ pub fn select( .get("projectionIssues") .and_then(Value::as_array) .map_or(0, Vec::len); - let issues=result.get("projectionIssues").and_then(Value::as_array).map(|items|items.iter().take(10).map(|v|json!({"code":v["code"],"nodeId":v["nodeId"],"property":v["property"],"fidelityImpact":v["fidelityImpact"],"message":v["message"].as_str().map(|s|s.chars().take(240).collect::())})).collect::>()).unwrap_or_default(); + let issues=result.get("projectionIssues").and_then(Value::as_array).map(|items|items.iter().take(10).map(|v|json!({"code":v["code"],"nodeId":v["nodeId"],"property":v["property"],"fidelityImpact":v["fidelityImpact"],"how":v["how"].as_str().or_else(|| v["details"]["nextAction"].as_str()).or_else(|| v["details"]["nextAction"]["how"].as_str()),"message":v["message"].as_str().map(|s|s.chars().take(240).collect::())})).collect::>()).unwrap_or_default(); let output_results = result.get("outputResults").and_then(Value::as_object).map(|items| items.iter().map(|(name,v)|(name.clone(),json!({"state":v["state"],"projection":v["projection"],"bodyDelivery":"omitted-by-selection"}))).collect::>()); result.retain(|key, _| { matches!( @@ -415,6 +416,31 @@ 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"]}); + } + if let Some(object) = reply.as_object_mut() { + object.retain(|key, _| matches!(key.as_str(), + "status" | "source" | "cache" | "quality" | "completeness" | "collection" | + "verificationScope" | "outputPaths" | "outputPathResults" | "resume" | + "responsiveEvidence" | "mappingComplete" | "projectionIssues" | "selectionEvidence" | + "deliverable" | "outputResults" | "selected" | "resources" | "delivery" | + "sizeEstimate" | "metadataDelivery" | "responseTruncated" | "artifactId" | + "exportJob" | "assetJob" | "summary" | "reviewChecklist" | "warnings")); + } + Ok(()) +} + /// 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/delivery.rs b/crates/devup-mcp/src/server/delivery.rs index b83faacf..c5c5b006 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, @@ -275,6 +284,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() && split.is_empty() { + 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." @@ -297,6 +327,7 @@ pub(crate) fn server_identity() -> Value { serde_json::json!({"version":env!("CARGO_PKG_VERSION"),"buildId":crate::build_id(), "commit":option_env!("DEVUP_MCP_GIT_COMMIT").filter(|s|!s.is_empty()), "displayVersion":format!("{}+{}", env!("CARGO_PKG_VERSION"), crate::build_id()), + "builtAt":env!("DEVUP_MCP_BUILD_TIME").parse::().ok(), "identityGuidance":include_str!("text/delivery-server_identity-f8e03915.md"), // Cache-only. The lookup lives in a background task, so adding this to // the identity every response carries costs no network on the call path @@ -356,6 +387,46 @@ 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( + 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/diagnostics/response_diagnostics.rs b/crates/devup-mcp/src/server/diagnostics/response_diagnostics.rs index 162d2d64..587f0894 100644 --- a/crates/devup-mcp/src/server/diagnostics/response_diagnostics.rs +++ b/crates/devup-mcp/src/server/diagnostics/response_diagnostics.rs @@ -76,6 +76,7 @@ pub fn connection_report( "direct": direct_path(status, direct, usable.is_some()), }, "nextAction": match active_path { + Some(ActivePath::Bridge) if attached.iter().any(|file| file.context.plugin_build()["pluginStale"] == true) => stale_plugin_action(), Some(ActivePath::Bridge) => bridge_next_action(attached), Some(ActivePath::Direct) => json!({ "tool": "devup_figma_export", @@ -97,6 +98,10 @@ pub fn connection_report( } /// With plugins attached: the call to make, and whether it needs a url. +fn stale_plugin_action() -> Value { + json!({"skill":"figma-bridge-setup","action":"A plugin that reports liveScripts is sent this server's scripts automatically; call devup_figma_auth status again after a second. If it is still stale, load the figma-bridge-setup skill. In the plugin directory run npm ci, then npm run build. Use computer use to foreground Figma desktop; import plugin/manifest.json via Plugins > Development > Import plugin from manifest if needed, close and re-run Devup Bridge, then verify devup_figma_auth status.","then":{"tool":"devup_figma_auth","arguments":{"action":"status"}}}) +} + fn bridge_next_action(attached: &[AttachedFile]) -> Value { let [file] = attached else { return several_plugins( @@ -450,6 +455,8 @@ fn bridge_path(bridge: Option<&BridgePathSnapshot>) -> Value { "port": bridge.port, "host": bridge.host.as_ref().map(|host| peer(host, bridge.role == BridgeRole::Host)), "attachedFiles": bridge.attached_files.iter().map(attached_file).collect::>(), + "pluginStale": bridge.attached_files.iter().any(|file| file.context.plugin_build()["pluginStale"] == true), + "expectedScriptsHash":devup_mcp_figma::PLUGIN_SCRIPTS_HASH, "attachedFilesNote": include_str!("../text/diagnostics-bridge_path-10545dbe.md"), "reason": bridge_reason(bridge), }); @@ -634,6 +641,9 @@ fn attached_file(file: &AttachedFile) -> Value { "contextFreshness":"last-delivered-event","observedAt":file.context.observed_at, "lastKnownSelection":file.context.last_known_selection,"lastKnownPage":file.context.last_known_page, "pluginVersion":file.context.plugin_version, + "scriptsHash":file.context.scripts_hash,"protocol":file.context.protocol, + "pluginBuild":file.context.plugin_build(), + "pluginStale":file.context.plugin_build()["pluginStale"], "displayContextEvidenceLimit":"Zoom is Figma viewport zoom; DPR belongs to the plugin UI iframe and does not verify screenshot capture scale.", }); if let Some(in_view) = file.context.in_view { @@ -757,4 +767,4 @@ fn client_setup() -> Value { } } }) -} \ No newline at end of file +} diff --git a/crates/devup-mcp/src/server/diagnostics/tests.rs b/crates/devup-mcp/src/server/diagnostics/tests.rs index a97304f2..82a95f2e 100644 --- a/crates/devup-mcp/src/server/diagnostics/tests.rs +++ b/crates/devup-mcp/src/server/diagnostics/tests.rs @@ -14,6 +14,27 @@ mod tests { } } + #[test] + fn plugin_identity_status_distinguishes_current_stale_and_unknown() { + for (hash, protocol, state) in [ + (Some(devup_mcp_figma::PLUGIN_SCRIPTS_HASH), Some(devup_mcp_figma::PLUGIN_PROTOCOL), "current"), + (Some("old-build"), Some(devup_mcp_figma::PLUGIN_PROTOCOL), "stale"), + (None, None, "stale-unknown"), + ] { + let mut file = attached(None, Some(1)); + file.context.scripts_hash = hash.map(str::to_owned); + file.context.protocol = protocol; + let report = connection_report(AuthStatus::Disconnected, &absent_direct_snapshot(), Some(&bridge_with(vec![file]))); + assert_eq!(report["paths"]["bridge"]["pluginStale"], state != "current"); + assert_eq!(report["paths"]["bridge"]["attachedFiles"][0]["pluginBuild"]["state"], state); + if state == "current" { + assert_eq!(report["nextAction"]["tool"], "devup_figma_export"); + } else { + assert_eq!(report["nextAction"]["skill"], "figma-bridge-setup"); + } + } + } + /// `doctor` used to answer every connection question with "log in", /// because `direct` was the only path it knew. It now reports the cheaper /// one first, and distinguishes the three states that need three different @@ -87,6 +108,8 @@ mod tests { selection_count: selected, in_view: Some(false), plugin_version: Some("0.4.6".to_owned()), + scripts_hash: Some(devup_mcp_figma::PLUGIN_SCRIPTS_HASH.into()), + protocol: Some(devup_mcp_figma::PLUGIN_PROTOCOL), ..Default::default() }, } diff --git a/crates/devup-mcp/src/server/export_record.rs b/crates/devup-mcp/src/server/export_record.rs index c0ef5fc2..55664fe9 100644 --- a/crates/devup-mcp/src/server/export_record.rs +++ b/crates/devup-mcp/src/server/export_record.rs @@ -16,6 +16,7 @@ use serde_json::{Value, json}; use super::output::{OutputPolicy, OutputTransaction}; + pub(super) const EXPORTS_DIR: &str = ".devup/exports"; pub(super) const CONVERTER_BUGS_DIR: &str = ".devup/converter-bugs"; const RECORD_KIND: &str = "devup-export-record"; @@ -191,9 +192,18 @@ pub(super) fn write( } } } - if nodes.is_empty() { + let generated = response["responsiveTsx"].as_str().or_else(|| response["tsx"].as_str()).or_else(|| response["componentTsx"].as_str()); + if generated.is_none() && nodes.is_empty() && response["frames"].as_array().is_none_or(|frames| frames.is_empty()) { return Ok(None); } + let output = if response["responsiveTsx"].is_string() { Some("responsiveTsx") } + else if response["tsx"].is_string() { Some("tsx") } + else if response["componentTsx"].is_string() { Some("componentTsx") } else { output }; + let mut frame_ids = response["responsiveEvidence"]["frames"].as_array().into_iter().flatten() + .chain(response["frames"].as_array().into_iter().flatten()) + .filter_map(|frame| frame["nodeId"].as_str()).map(str::to_owned) + .collect::>(); + if frame_ids.is_empty() && !root_node_id.is_empty() { frame_ids.insert(root_node_id.to_owned()); } let mut source_map = BTreeMap::>::new(); for entry in response["outputResults"]["tsx"]["_sourceMap"] .as_array() @@ -222,10 +232,13 @@ pub(super) fn write( "output": output, "nodes": nodes, "responsive": response["figmaNodeIds"]["responsive"], + "frameIds": frame_ids, + "artifactId": response["artifactId"], + "generatedStyles": generated.map(style_elements), }); let path = Path::new(project_root) .join(EXPORTS_DIR) - .join(format!("{}.json", file_name(root_node_id))); + .join(format!("{}.json", file_name(frame_ids.iter().next().map_or(root_node_id, String::as_str)))); let target = policy.resolve(&path.to_string_lossy())?; let display = target.display_path().display().to_string(); let bytes = serde_json::to_vec_pretty(&record).map_err(|error| { @@ -404,3 +417,5 @@ pub(super) fn write_repro( transaction.commit()?; Ok((display, repro)) } + +include!("export_record/generated_drift.rs"); diff --git a/crates/devup-mcp/src/server/export_record/generated_drift.rs b/crates/devup-mcp/src/server/export_record/generated_drift.rs new file mode 100644 index 00000000..6993cb84 --- /dev/null +++ b/crates/devup-mcp/src/server/export_record/generated_drift.rs @@ -0,0 +1,84 @@ +fn style_prop(name: &str) -> bool { + LAYOUT_PROPS.contains(&name) || matches!(name, + "bg" | "background" | "backgroundColor" | "backgroundImage" | "color" | "border" | + "borderColor" | "borderWidth" | "borderRadius" | "rounded" | "boxShadow" | "opacity" | + "textAlign" | "whiteSpace" | "wordBreak" | "zIndex" | "maskImage" | "objectPosition") +} + +fn style_elements(tsx: &str) -> Vec { + let mut elements = BTreeMap::)>::new(); + for attribute in generated_attributes(tsx).into_iter().filter(|attribute| style_prop(&attribute.name)) { + let element = elements.entry(attribute.element).or_insert_with(|| (attribute.tag, BTreeMap::new())); + element.1.insert(attribute.name, normalized(&attribute.source)); + } + elements.into_values().map(|(tag, props)| json!({"tag": tag, "props": props})).collect() +} + +fn style_value(source: &str) -> String { + source.split_once('=').map_or(source, |(_, value)| value).chars() + .filter(|character| !character.is_whitespace()).map(|character| if character == '\'' { '"' } else { character }).collect() +} + +pub(super) fn generated_drift(record: &Value, root: &Path, sources: &[String]) -> Value { + let Some(expected) = record["generatedStyles"].as_array() else { + return json!({"state": "unverified", "confidence": "none", "reason": "Export predates the style baseline; re-export responsiveTsx."}); + }; + if expected.is_empty() { + return json!({"state": "unverified", "confidence": "none", "reason": "The export has no comparable literal style props."}); + } + let mut actual = Vec::new(); + let mut files = Vec::new(); + for source in sources { + let path = root.join(source); + if !path.starts_with(root) || source.split(['/', '\\']).any(|part| part == "..") { continue; } + if !dunce::canonicalize(&path).ok().zip(dunce::canonicalize(root).ok()).is_some_and(|(path, root)| path.starts_with(root)) { continue; } + if std::fs::metadata(&path).map_or(true, |metadata| metadata.len() > MAX_RECORD_BYTES) { continue; } + let Ok(text) = std::fs::read_to_string(&path) else { continue; }; + files.push(source.clone()); + actual.extend(style_elements(&text).into_iter().map(|element| (source, element))); + } + if files.is_empty() { + return json!({"state": "unverified", "confidence": "none", "reason": "No mapped URL source files were readable."}); + } + let mut findings = Vec::new(); + for (index, element) in expected.iter().enumerate() { + let Some((file, current)) = actual.get(index) else { + findings.push(json!({"kind": "element-removed", "element": index, "files": files, "generated": element})); + continue; + }; + let Some(props) = element["props"].as_object() else { continue; }; + for (prop, generated) in props { + let value = ¤t["props"][prop]; + if value.as_str().map(style_value) != generated.as_str().map(style_value) { + let kind = if value.is_null() { "prop-removed" } + else if generated.as_str().is_some_and(|value| value.contains("{[")) && !value.as_str().is_some_and(|value| value.contains("{[")) { "responsive-array-collapsed" } + else { "prop-changed" }; + findings.push(json!({"kind": kind, "file": file, "element": index, "prop": prop, "generated": generated, "actual": value})); + } + } + for (prop, value) in current["props"].as_object().into_iter().flatten() { + if !props.contains_key(prop) { + findings.push(json!({"kind": "style-added", "file": file, "element": index, "prop": prop, "actual": value})); + } + } + } + for (file, element) in actual.iter().skip(expected.len()) { + findings.push(json!({"kind": "styled-element-added", "file": file, "actual": element})); + } + json!({"state": if findings.is_empty() { "matched-heuristic" } else { "drift" }, "confidence": "medium", + "method": "Ordered style-bearing JSX elements across mapped URL files; wiring-only wrappers and attributes are ignored. Component extraction or reordering can require mapping review; not a runtime proof.", + "files": files, "findings": findings}) +} + +#[cfg(test)] +mod generated_drift_tests { + use super::*; + + #[test] + fn style_baseline_ignores_wiring_but_preserves_responsive_arrays() { + let generated = style_elements(""); + let wired = style_elements(""); + assert_eq!(generated, wired); + assert_ne!(generated, style_elements("")); + } +} diff --git a/crates/devup-mcp/src/server/export_skill_gap_tests.rs b/crates/devup-mcp/src/server/export_skill_gap_tests.rs index 352afc15..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}-{}-{}", @@ -229,3 +256,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/feature_trace/run.rs b/crates/devup-mcp/src/server/feature_trace/run.rs index 5a7de0e6..dc353120 100644 --- a/crates/devup-mcp/src/server/feature_trace/run.rs +++ b/crates/devup-mcp/src/server/feature_trace/run.rs @@ -151,6 +151,7 @@ pub(super) async fn run( chain.push(hop("component-api",json!({"operationId":input.operation_id,"apiPath":input.api_path,"method":input.method}),Value::Null,json!({}),Some("Explicit operation anchor was not found in project OpenAPI endpoints."))); } let mut navigation_sources = vec![]; + let known_clients = stack_diff::project_api_clients(&root); for file in &selected_files { let source = match read(&root.join(file)) { Ok(s) => s, @@ -166,7 +167,7 @@ pub(super) async fn run( } }; navigation_sources.push((file.clone(), source.clone())); - let (calls, configs) = stack_diff::parse::client_references(&source); + let (calls, configs) = stack_diff::parse::client_references_with(&source, &known_clients); if calls.is_empty() && configs.is_empty() { chain.push(hop("component-api",json!(file),Value::Null,json!({}),Some("No supported literal API reference in this file; helpers, wrappers and dynamic calls are not resolved."))); } diff --git a/crates/devup-mcp/src/server/feedback_interactions_tests.rs b/crates/devup-mcp/src/server/feedback_interactions_tests.rs index 0540eb15..0ed46006 100644 --- a/crates/devup-mcp/src/server/feedback_interactions_tests.rs +++ b/crates/devup-mcp/src/server/feedback_interactions_tests.rs @@ -69,7 +69,7 @@ fn feedback_interactions_preserve_targets_roles_states_and_unresolved_data() { ); assert_eq!( schema["$defs"]["Role"]["enum"], - json!(["link", "button", "tab", "nav", "checkbox", "radio"]) + json!(["link", "button", "tab", "nav", "checkbox", "radio", "marquee"]) ); assert_eq!( schema["$defs"]["InteractiveCandidate"]["properties"]["targetResolved"]["type"], @@ -174,6 +174,22 @@ fn feedback_shared_widgets_details_and_grouped_questions_require_review() { ); } +#[test] +fn feedback44_export_wiring_guidance_keeps_states_on_the_anchor() { + let mut result = json!({"tsx":"","deliverable":{"isFinal":true}}); + interactions::attach(&mut result, &design()); + let issue = result["projectionIssues"].as_array().unwrap().iter() + .find(|issue| issue["code"] == "DEVUP_CODEGEN_INTERACTION_UNMAPPED").unwrap(); + let guidance = issue["details"]["nextAction"].as_str().unwrap(); + assert!(guidance.contains("as={Link} props={{ href, 'aria-current'")); + for state in ["_hover={hoverStyles}", "_focusVisible={focusStyles}", "_active={activeStyles}"] { + assert!(guidance.contains(state), "{guidance}"); + } + assert!(guidance.contains("not on a child wrapped in Link")); + assert_eq!(result["interactionCoverage"]["wiredCount"], 0); + assert_eq!(result["deliverable"]["isFinal"], false); +} + #[tokio::test] async fn feedback_context_routes_reuse_env_names_and_no_secret_values() { let p = Project::new(); @@ -295,6 +311,55 @@ fn feedback_validation_catches_missing_routes_unmapped_actions_and_active_state( assert_eq!(report["coverage"]["runtimeVerified"], false); } +#[tokio::test] +async fn export_navigation_reads_existing_code_link_evidence() { + let p = Project::new(); + p.write("src/app/company/location/page.tsx", "export default function Location(){return
}"); + p.write("src/app/layout.tsx", "import Link from 'next/link'; export default function Layout(){return 오시는길}"); + let result = project_context::checked_export(json!({"interactionCoverage":{},"interactiveCandidates":[{"nodeId":"location","text":"오시는 길"}]}), Some(p.root()), &skills::Lookup::project_only(p.0.clone(), skills::Runtime::Codex)).await; + assert_eq!(result["routeSuggestions"][0]["targetResolved"], true); + assert_eq!(result["routeSuggestions"][0]["suggestions"][0]["route"], "/company/location"); + assert!(result["routeSuggestions"][0]["suggestions"][0]["evidence"].as_str().unwrap().contains("layout.tsx:1")); + assert_eq!(result["projectContext"]["ui"]["routes"][0]["route"], "/company/location"); +} + +#[test] +fn export_context_lists_only_referenced_assets_even_past_inventory_cap() { + let p = Project::new(); + p.write("src/app/page.tsx", "export default function Home(){return
}"); + for index in 0..300 { p.write(&format!("public/images/image-{index:03}.png"), "image"); } + let mut result = json!({"tsx":"<>","interactiveCandidates":[]}); + project_context::attach_export_context(&mut result, p.root()); + assert_eq!(result["projectContext"]["counts"]["publicFiles"], 300); + assert_eq!(result["projectContext"]["ui"]["nonPageDestinations"], json!(["/images/image-299.png"])); + assert_eq!(result["projectContext"]["referencedAssets"], json!([{"url":"/images/image-299.png","exists":true},{"url":"/images/missing.png","exists":false}])); +} + +#[test] +fn a_primitive_rendered_as_link_with_props_href_is_wired() { + let inventory = json!({"found":true,"routes":[{"route":"/company","appliesTo":"."}]}); + let source = r#"import Link from 'next/link'; export function Header(){return <>회사소개없음전체 보기}"#; + let report = navigation::report(source, &inventory, None); + let findings = report["findings"].as_array().unwrap(); + let unmapped = findings + .iter() + .filter(|f| f["rule"] == "interaction-unmapped") + .count(); + assert_eq!(unmapped, 1, "{findings:?}"); + assert!(findings.iter().any(|f| f["rule"] == "missing-static-route" && f["href"] == "/missing")); + assert_eq!(report["sourceHrefs"]["/company"], 1); + let dynamic = r#"import Link from 'next/link'; const Menu=({items})=> <>{items.map((item)=> {item.label})}"#; + let report = navigation::report(dynamic, &inventory, None); + assert!( + !report["findings"] + .as_array() + .unwrap() + .iter() + .any(|f| f["rule"] == "interaction-unmapped"), + "{report}" + ); +} + #[test] fn feedback_fixed_navigation_underline_is_an_unverified_info_candidate() { let source = r#"import Link from 'next/link'; const Header=()=> "#; @@ -675,6 +740,7 @@ fn feedback_context_budget_caps_or_defers_explicitly() { ); assert!( result["projectContext"]["exportContextLimits"]["truncated"] == true + || result["projectContext"]["state"] == "summary" || result["projectContext"]["state"] == "deferred" ); } @@ -714,3 +780,43 @@ fn feedback_read_failures_and_direct_selection_preserve_review_and_source_maps() assert_eq!(result["deliverable"]["isFinal"], false); assert!(result["summary"]["omittedReviewItems"].as_u64().unwrap() > 0); } + +#[test] +fn broken_component_sets_do_not_hide_readable_reactions() { + for reason in ["in get_componentProperties: Component set for node has existing errors", "Component set is invalid"] { + let mut snapshot = design(); + let node = snapshot.nodes.get_mut("nav").unwrap(); + node.fields.insert("mainComponentSetId".into(), json!("set:1")); + node.fields.insert("mainComponentSetName".into(), json!("Navigation")); + for field in ["componentProperties", "variantProperties"] { + node.field_errors.insert(field.into(), reason.into()); + } + let mut result = json!({"source":{"nodeId":"screen"},"tsx":"","deliverable":{"isFinal":true}}); + interactions::attach(&mut result, &snapshot); + let issues = result["projectionIssues"].as_array().unwrap(); + let issue = issues.iter().find(|issue| issue["code"] == "DEVUP_DESIGN_COMPONENT_SET_ERROR").unwrap(); + assert_eq!(issue["nodeId"], "nav"); + assert_eq!(issue["details"]["componentSetId"], "set:1"); + assert_eq!(issue["details"]["componentSetName"], "Navigation"); + assert_eq!(issue["fidelityImpact"], "none"); + assert!(!issues.iter().any(|issue| issue["code"] == "DEVUP_CODEGEN_INTERACTION_UNVERIFIED")); + let candidate = result["interactiveCandidates"].as_array().unwrap().iter().find(|candidate| candidate["nodeId"] == "nav").unwrap(); + assert_eq!(candidate["prototypeActions"][0]["destinationId"], "portfolio-screen"); + } +} + +#[test] +fn interaction_read_failure_names_node_and_field_but_state_only_failure_does_not_block() { + let mut snapshot = design(); + snapshot.nodes.get_mut("nav").unwrap().field_errors.insert("reactions".into(), "permission denied".into()); + let mut result = json!({"tsx":"","deliverable":{"isFinal":true}}); + interactions::attach(&mut result, &snapshot); + let issue = result["projectionIssues"].as_array().unwrap().iter().find(|issue| issue["code"] == "DEVUP_CODEGEN_INTERACTION_UNVERIFIED").unwrap(); + let message = issue["message"].as_str().unwrap(); + assert!(message.contains("nav") && message.contains("reactions")); + snapshot.nodes.get_mut("nav").unwrap().field_errors.clear(); + snapshot.nodes.get_mut("timer").unwrap().field_errors.insert("variantProperties".into(), "not available".into()); + let mut result = json!({"source":{"nodeId":"timer"},"tsx":"","deliverable":{"isFinal":true}}); + interactions::attach(&mut result, &snapshot); + assert_eq!(result["deliverable"]["isFinal"], true); +} diff --git a/crates/devup-mcp/src/server/handlers/devup_figma_export.rs b/crates/devup-mcp/src/server/handlers/devup_figma_export.rs index 4ccb4686..b7eb56a3 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,15 @@ impl DevupServer { Parameters(workflow): Parameters, ) -> Result { let mut input = workflow.input; + 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") { for output in ["assetManifest", "imageAudit"] { if !input.outputs.iter().any(|o| o == output) { @@ -37,6 +46,9 @@ impl DevupServer { }; frames.extend(node); } + if let Some(response) = screen_plan::responsive_required(&project_root, &frames, &input.outputs).map_err(to_mcp_error)? { + return Ok(tool_result(response)); + } let request = screen_plan::ExportRequest { project_root: &project_root, url: input.url.as_deref(), @@ -118,7 +130,7 @@ impl DevupServer { .wait_briefly(selection.active()) .await .map_err(to_mcp_error)?; - let result = with_project_checks( + let result = project_context::checked_export( result, project_root.as_deref(), &skills::Lookup::new( @@ -126,7 +138,7 @@ impl DevupServer { self.output_policy.primary_root(), self.client_name(), ), - ); + ).await; if selection.active() { return compact::pick(&result, selection) .map(tool_result) @@ -228,20 +240,16 @@ 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.len() == 1 && !input.all_screens && input.artifact_id.is_none() { + let mut target = parse_link(input.url.as_deref()).map_err(to_mcp_error)?; + target.node_id = input.frame_ids.pop(); + input.url = Some(target.link(target.node_id.as_deref())); + } if reference_png_requested && (!input.frame_ids.is_empty() || input.all_screens) { + // No url and no artifact is the file the attached bridge plugin + // has open, so its frames are linked as figma-bridge://current. let target = if let Some(url) = &input.url { FigmaTarget::parse(url).ok() } else if let Some(id) = &input.artifact_id { @@ -250,29 +258,65 @@ impl DevupServer { .await .map(|a| a.payload.target.clone()) } else { - None + Some(FigmaTarget::bridge_current()) }; - let url = target + // One referencePng-only call per frame. The other outputs stay on + // the original multi-frame call: repeating responsiveTsx on a + // single frame would only be refused as responsive_required. + let reference_call = |url: &str| { + let mut arguments = json!({"url": url, "outputs": ["referencePng"], "scope": "node"}); + if let Some(root) = &input.project_root { + arguments["projectRoot"] = json!(root); + } + json!({"tool": "devup_figma_export", "arguments": arguments}) + }; + let reference_calls = target .as_ref() - .zip(input.frame_ids.first()) - .map(|(target, id)| target.link(Some(id))); - let mut arguments = json!(input); - for key in ["artifactId", "frameIds", "allScreens", "url"] { - arguments.as_object_mut().unwrap().remove(key); - } - arguments["scope"] = json!("node"); - if let Some(url) = &url { - arguments["url"] = json!(url); - } + .map(|target| { + input + .frame_ids + .iter() + .map(|id| reference_call(&target.link(Some(id)))) + .collect::>() + }) + .unwrap_or_default(); + let remaining_outputs = input + .outputs + .iter() + .filter(|output| *output != "referencePng") + .cloned() + .collect::>(); + let remaining_call = (!remaining_outputs.is_empty()).then(|| { + let mut arguments = json!(input); + arguments["outputs"] = json!(remaining_outputs); + json!({"tool": "devup_figma_export", "arguments": arguments}) + }); + let available = !reference_calls.is_empty(); + let next_action = if let Some(first) = reference_calls.first() { + let mut next = first.clone(); + next["omitArguments"] = json!(["artifactId", "frameIds", "allScreens"]); + next["how"] = json!(include_str!("../text/mod-devup_figma_export-1fe13d5b.md")); + next + } else { + let mut arguments = json!(input); + for key in ["artifactId", "frameIds", "allScreens", "url"] { + arguments.as_object_mut().unwrap().remove(key); + } + arguments["scope"] = json!("node"); + arguments["outputs"] = json!(["referencePng"]); + json!({"tool": "devup_figma_export", "arguments": arguments, + "requiredArguments": ["url"], + "omitArguments": ["artifactId", "frameIds", "allScreens"], + "how": include_str!("../text/mod-devup_figma_export-1fe13d5b.md")}) + }; return Err(to_mcp_error(DevupError::with_details( ErrorCode::DevupSnapshotUnsupported, "referencePng can only be collected for a single Figma link target.", false, - json!({"recoveryState":if url.is_some() {"available"} else {"manual-fix-required"}, - "nextAction":{"tool":"devup_figma_export","arguments":arguments, - "requiredArguments":if url.is_some() {Vec::<&str>::new()} else {vec!["url"]}, - "omitArguments":["artifactId","frameIds","allScreens"], - "how":include_str!("../text/mod-devup_figma_export-1fe13d5b.md")}}), + json!({"recoveryState": if available {"available"} else {"manual-fix-required"}, + "nextAction": next_action, + "referenceCalls": reference_calls, + "remainingCall": remaining_call}), ))); } let root_layout = parse_root_layout(&input.root_layout).map_err(to_mcp_error)?; @@ -351,6 +395,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, ) @@ -402,6 +449,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, @@ -423,7 +471,7 @@ impl DevupServer { ) .await .map_err(to_mcp_error)?; - return Ok(tool_result(with_project_checks( + let mut result = project_context::checked_export( result, input.project_root.as_deref(), &skills::Lookup::new( @@ -431,7 +479,9 @@ impl DevupServer { self.output_policy.primary_root(), self.client_name(), ), - ))); + ).await; + compact::refresh_reply_selection(&mut result, &input.response_selection).map_err(to_mcp_error)?; + return Ok(tool_result(result)); } validate_artifact_projection( &artifact, @@ -451,6 +501,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, @@ -475,7 +526,7 @@ impl DevupServer { ) .await .map_err(to_mcp_error)?; - return Ok(tool_result(with_project_checks( + let mut result = project_context::checked_export( result, input.project_root.as_deref(), &skills::Lookup::new( @@ -483,7 +534,14 @@ impl DevupServer { self.output_policy.primary_root(), self.client_name(), ), - ))); + ).await; + if manifest_added { + let warning = json!({"code":"DEVUP_ASSET_MANIFEST_ADDED","message":"assetManifest was automatically added to outputs for assetRequests."}); + if let Some(warnings) = result["warnings"].as_array_mut() { warnings.push(warning); } + 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)?; @@ -558,6 +616,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, @@ -597,7 +656,7 @@ impl DevupServer { ok => ok, } .map_err(to_mcp_error)?; - Ok(tool_result(with_project_checks( + let mut result = project_context::checked_export( result, input.project_root.as_deref(), &skills::Lookup::new( @@ -605,6 +664,13 @@ impl DevupServer { self.output_policy.primary_root(), self.client_name(), ), - ))) + ).await; + if manifest_added { + let warning = json!({"code":"DEVUP_ASSET_MANIFEST_ADDED","message":"assetManifest was automatically added to outputs for assetRequests."}); + if let Some(warnings) = result["warnings"].as_array_mut() { warnings.push(warning); } + 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/handlers/devup_figma_search.rs b/crates/devup-mcp/src/server/handlers/devup_figma_search.rs index 6bff8160..012336e9 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, ))); @@ -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/handlers/devup_screen_plan.rs b/crates/devup-mcp/src/server/handlers/devup_screen_plan.rs index 99106c12..21139d77 100644 --- a/crates/devup-mcp/src/server/handlers/devup_screen_plan.rs +++ b/crates/devup-mcp/src/server/handlers/devup_screen_plan.rs @@ -10,9 +10,10 @@ impl DevupServer { &self, Parameters(input): Parameters, ) -> Result { - let result = match input.action.as_str() { + let mut result = match input.action.as_str() { "draft" => { - let (screens, source) = self.plan_screens(&input).await.map_err(to_mcp_error)?; + let (screens, mut source) = self.plan_screens(&input).await.map_err(to_mcp_error)?; + self.plan_component_widths(&input, &screens, &mut source).await.map_err(to_mcp_error)?; let mut assets = input.assets.clone(); assets.extend( source["assetCandidates"] @@ -40,6 +41,43 @@ impl DevupServer { )), } .map_err(to_mcp_error)?; + if input.action == "auditPlan" + && self.services.upstream.bridge_path_snapshot().await + .is_some_and(|bridge| bridge.attached_files.len() == 1) + { + prefer_bridge_reference_calls(&mut result); + } Ok(tool_result(result)) } } + +fn prefer_bridge_reference_calls(value: &mut Value) { + if value["tool"] == "devup_figma_export" + && let Some(url) = value["arguments"]["url"].as_str() + && let Some(node_id) = url.strip_prefix("')) + { + let node_id = node_id.to_owned(); + value["arguments"].as_object_mut().unwrap().remove("url"); + value["arguments"]["frameIds"] = json!([node_id]); + } + match value { + Value::Object(object) => object.values_mut().for_each(prefer_bridge_reference_calls), + Value::Array(array) => array.iter_mut().for_each(prefer_bridge_reference_calls), + _ => {} + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn feedback57_reference_recovery_prefers_explicit_bridge_frames() { + let mut result = json!({"missingReferences":[{"call":{"tool":"devup_figma_export", + "arguments":{"url":"","outputs":["referencePng"]}}}]}); + prefer_bridge_reference_calls(&mut result); + let arguments = &result["missingReferences"][0]["call"]["arguments"]; + assert_eq!(arguments["frameIds"], json!(["45:2066"])); + assert!(arguments.get("url").is_none()); + } +} 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..529952b8 100644 --- a/crates/devup-mcp/src/server/handlers/devup_ui_validate.rs +++ b/crates/devup-mcp/src/server/handlers/devup_ui_validate.rs @@ -1,4 +1,7 @@ use super::*; +use crate::server::tools::UiValidateDetail; +#[path = "ui_validate_sources.rs"] +mod sources; #[tool_router(router = devup_ui_validate_router, vis = "pub(super)")] impl DevupServer { @@ -8,10 +11,18 @@ impl DevupServer { #[doc = include_str!("../text/devup-ui-validate.md")] pub(super) async fn devup_ui_validate( &self, - Parameters(input): Parameters, + Parameters(mut input): Parameters, ) -> Result { use devup_mcp_devup_ui::ui_validate::{bundle_theme, check_bundle, validate_bundle}; let invalid = |message: String| ErrorData::invalid_params(message, None); + let paths_mode = !input.paths.is_empty(); + if paths_mode { + if !input.tsx.is_empty() || input.files.is_some() { + return Err(invalid("Supply paths or tsx/files, not both".into())); + } + let root = input.project_root.as_deref().ok_or_else(|| invalid("paths requires projectRoot".into()))?; + input.files = Some(sources::read_paths(root, &input.paths).map_err(invalid)?); + } // Neither source supplied. This used to validate the empty string and // answer `ok: true, okReason: "clean"` — which reads as "this code // passed" about code that was never sent. There is no verdict to give @@ -23,7 +34,9 @@ impl DevupServer { .into(), )); } - let theme_lookup = if let Some(files) = &input.files { + let theme_lookup = if paths_mode { + project_context::theme_for_validation(input.project_root.as_deref()).map_err(to_mcp_error)? + } else if let Some(files) = &input.files { check_bundle(files).map_err(invalid)?; if !input.tsx.is_empty() { return Err(invalid("Supply either tsx or files, not both".into())); @@ -37,7 +50,9 @@ impl DevupServer { project_context::theme_for_validation(input.project_root.as_deref()) .map_err(to_mcp_error)? }; - let mut report = if let Some(files) = &input.files { + let mut report = if paths_mode { + devup_mcp_devup_ui::ui_validate::validate_sources(input.files.as_deref().unwrap_or_default(), theme_lookup.theme.as_ref(), input.strict) + } else if let Some(files) = &input.files { validate_bundle(files, theme_lookup.theme.as_ref(), input.strict).map_err(invalid)? } else { devup_mcp_devup_ui::ui_validate::validate_devup_ui_tsx( @@ -46,6 +61,19 @@ impl DevupServer { input.strict, ) }; + if paths_mode + && let Some(root) = input.project_root.as_deref() + && std::path::Path::new(root).join("src").is_dir() + { + let project_sources = sources::read_paths(root, &["src".into()]).map_err(invalid)?; + if project_sources.iter().any(|file| devup_mcp_devup_ui::ui_validate::visual_behavior::has_global_link_decoration(&file.content)) { + report.violations.retain(|finding| finding.rule != "link-underline"); + report.ok = report.violations.iter().all(|finding| { + finding.severity != devup_mcp_devup_ui::ui_validate::Severity::Error + && !(input.strict && finding.severity == devup_mcp_devup_ui::ui_validate::Severity::Warning) + }); + } + } // Site completeness, for code checked against a project: feedback on // every interactive element (bundle mode checks it per file), and an // icon for the app. @@ -143,6 +171,12 @@ impl DevupServer { // And with no theme the token check is skipped rather than passed, // which a bare `checkedTokens` cannot distinguish. use devup_mcp_devup_ui::ui_validate::Severity; + relax_unbound_token_matches(&mut report.violations, &input); + if input.strict { + report.ok = report.violations.iter().all(|finding| { + !matches!(finding.severity, Severity::Error | Severity::Warning) + }); + } let count_severity = |severity| { report .violations @@ -200,6 +234,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: @@ -220,6 +257,8 @@ impl DevupServer { "install": skill.install_action(&lookup), }); } + if paths_mode { result["pathsRead"] = json!(sources.len()); } + if matches!(input.detail, UiValidateDetail::Summary) { sources::summarize(&mut result, &sources); } Ok(tool_result(result)) } } diff --git a/crates/devup-mcp/src/server/handlers/ui_validate_sources.rs b/crates/devup-mcp/src/server/handlers/ui_validate_sources.rs new file mode 100644 index 00000000..f03cacb9 --- /dev/null +++ b/crates/devup-mcp/src/server/handlers/ui_validate_sources.rs @@ -0,0 +1,139 @@ +use devup_mcp_devup_ui::ui_validate::SourceFile; +use std::{collections::BTreeSet, io::Read}; + +pub(super) fn read_paths(root: &str, paths: &[String]) -> Result, String> { + if paths.len() > 512 { return Err("paths exceeds 512 entries".into()); } + let root = std::fs::canonicalize(root).map_err(|error| format!("projectRoot: {error}"))?; + let mut pending = paths.iter().map(|path| root.join(path)).collect::>(); + let mut visited = BTreeSet::new(); + let mut files = Vec::new(); + let mut total: usize = 0; + while let Some(path) = pending.pop() { + let path = std::fs::canonicalize(&path) + .map_err(|error| format!("{}: {error}", path.display()))?; + if !path.starts_with(&root) { + return Err(format!("paths must stay inside projectRoot: {}", path.display())); + } + if !visited.insert(path.clone()) { + continue; + } + if path.is_dir() { + for entry in std::fs::read_dir(&path).map_err(|error| error.to_string())? { + let entry = entry.map_err(|error| error.to_string())?; + let name = entry.file_name(); + let name = name.to_string_lossy(); + if name.starts_with('.') || ["node_modules", "dist", "build", "target", "df"].contains(&name.as_ref()) { + continue; + } + pending.push(entry.path()); + } + } else if matches!(path.extension().and_then(|value| value.to_str()), Some("tsx" | "ts" | "jsx" | "js")) { + if files.len() == 512 { + return Err("paths exceeds 512 source files".into()); + } + let file = std::fs::File::open(&path).map_err(|error| error.to_string())?; + let mut bytes = Vec::new(); + file.take(u64::try_from(8 * 1024 * 1024 - total + 1).map_err(|error| error.to_string())?) + .read_to_end(&mut bytes).map_err(|error| error.to_string())?; + total += bytes.len(); + if total > 8 * 1024 * 1024 { + return Err("paths exceeds 8 MiB total read bytes".into()); + } + let relative = path.strip_prefix(&root).map_err(|error| error.to_string())?; + files.push(SourceFile { + path: relative.to_string_lossy().replace('\\', "/"), + content: String::from_utf8(bytes).map_err(|error| format!("{}: {error}", path.display()))?, + }); + } + } + files.sort_by(|left, right| left.path.cmp(&right.path)); + if files.is_empty() { + return Err("paths contains no .tsx, .ts, .jsx or .js sources".into()); + } + Ok(files) +} + +pub(super) fn summarize(result: &mut serde_json::Value, sources: &[( &str, &str)]) { + use serde_json::{Value, json}; + let mut rows: Vec = Vec::new(); + let mut groups = std::collections::BTreeMap::new(); + let mut value_groups: std::collections::BTreeMap = std::collections::BTreeMap::new(); + for finding in result["violations"].as_array().into_iter().flatten() { + let name = finding.get("sourceName").or_else(|| finding.get("path")).or_else(|| finding["context"].get("path")) + .or_else(|| finding["context"].get("sourceName")); + if let Some(value) = finding.get("value").and_then(Value::as_str) + && matches!(finding["rule"].as_str(), Some("hardcoded-length" | "hardcoded-color")) { + let tokens = finding["tokenMatches"].as_array().map(|matches| matches.iter().map(|m| m["token"].clone()).collect::>()) + .or_else(|| finding["otherPurposeTokens"].as_array().cloned()).unwrap_or_default(); + let binding = finding.get("designBinding").cloned().unwrap_or(Value::Null); + let length_kind = finding.get("lengthKind").cloned().unwrap_or(Value::Null); + let key = format!("{}|{}|{value}|{}|{}|{}", finding["rule"], finding["severity"], json!(tokens), binding["state"], length_kind); + let line = finding.get("line").cloned().unwrap_or(Value::Null); + let prop = finding.get("prop").cloned().unwrap_or(Value::Null); + let file = name.cloned().unwrap_or(Value::Null); + if let Some(&index) = value_groups.get(&key) { + let row: &mut Value = &mut rows[index]; + row["count"] = json!(row["count"].as_u64().unwrap_or(1) + 1); + if let Some(props) = row["props"].as_array_mut() && !props.contains(&prop) { props.push(prop); } + let files = row["files"].as_array_mut().expect("files array"); + match files.iter_mut().find(|entry| entry["sourceName"] == file) { + Some(entry) => entry["lines"].as_array_mut().expect("lines array").push(line), + None => files.push(json!({"sourceName":file,"lines":[line]})), + } + continue; + } + let kind = if finding["rule"] == "hardcoded-color" { "color" } else { "length" }; + let message = match (finding["severity"].as_str(), tokens.is_empty()) { + (_, false) if binding["state"] == "unbound" && finding["severity"] != "warning" => format!("hardcoded {kind} {value}; the Figma design hardcodes it too, so {} only shares the value", tokens.iter().filter_map(Value::as_str).collect::>().join(", ")), + (Some("warning"), false) => format!("hardcoded {kind} {value}; devup.json defines it as {}", tokens.iter().filter_map(Value::as_str).collect::>().join(", ")), + (_, false) => format!("hardcoded {kind} {value}; only tokens named for another purpose have this value"), + _ => format!("hardcoded {kind} {value}; no matching token"), + }; + let mut row = json!({"rule":finding["rule"],"severity":finding["severity"],"value":value,"message":message, + "count":1,"props":[prop],"files":[{"sourceName":file,"lines":[line]}]}); + if !tokens.is_empty() { row["tokens"] = json!(tokens); } + if !binding.is_null() { row["designBinding"] = binding; } + if !length_kind.is_null() { row["lengthKind"] = length_kind; } + if let Some(suggestion) = finding.get("suggestion").filter(|value| !value.is_null()) { row["suggestion"] = suggestion.clone(); } + value_groups.insert(key, rows.len()); + rows.push(row); + continue; + } + let key = (name.cloned().unwrap_or(Value::Null).to_string(), finding["rule"].to_string(), finding["message"].to_string()); + if let Some(&index) = groups.get(&key) { + let row: &mut Value = &mut rows[index]; + row["count"] = json!(row["count"].as_u64().unwrap_or(1) + 1); + continue; + } + let mut row = json!({"rule":finding["rule"],"severity":finding["severity"],"message":finding["message"]}); + if let Some(name) = name { row["sourceName"] = name.clone(); } + let source = sources.iter().find(|(path, _)| name.and_then(Value::as_str).is_none_or(|name| name == *path)); + if let Some(line) = finding.get("line").or_else(|| finding["context"].get("line")) { + row["line"] = line.clone(); + } else if let Some((_, source)) = source + && let Some(start) = finding["byteRange"][0].as_u64().and_then(|value| usize::try_from(value).ok()) + && let Some(prefix) = source.get(..start) { + row["line"] = json!(prefix.bytes().filter(|byte| *byte == b'\n').count() + 1); + } + if let Some(suggestion) = finding.get("suggestion").filter(|value| !value.is_null()) { row["suggestion"] = suggestion.clone(); } + groups.insert(key, rows.len()); + rows.push(row); + } + let rank = |row: &Value| match row["severity"].as_str() { Some("error") => 0, Some("warning") => 1, _ => 2 }; + rows.sort_by_key(|row| rank(row)); + let total = rows.len(); + let hidden = rows.iter().skip(100).fold(std::collections::BTreeMap::::new(), |mut counts, row| { + *counts.entry(row["severity"].as_str().unwrap_or("info").to_owned()).or_default() += 1; + counts + }); + rows.truncate(100); + let risk_count = result["layoutRisks"].as_array().map_or(0, Vec::len); + if let Some(object) = result.as_object_mut() { + object.retain(|key, value| matches!(key.as_str(), "ok" | "okReason" | "strict" | "violationCounts" | "themeAvailable" | "tokens" | "nextAction" | "recoveryState" | "recoveryReason" | "nextActionReason" | "skillGap" | "themeNotes" | "pathsRead") || (key == "themeGuardrail" && !value.is_null())); + } + result["violations"] = json!(rows); + result["layoutRiskCount"] = json!(risk_count); + result["detail"] = json!("summary"); + result["full"] = json!({"how":"repeat with detail: \"full\""}); + if total > 100 { result["violationsTruncated"] = json!({"shown":100,"total":total,"hiddenBySeverity":hidden,"order":"error, warning, info"}); } +} diff --git a/crates/devup-mcp/src/server/interactions.rs b/crates/devup-mcp/src/server/interactions.rs index 7b86ef41..9d221bbb 100644 --- a/crates/devup-mcp/src/server/interactions.rs +++ b/crates/devup-mcp/src/server/interactions.rs @@ -7,6 +7,14 @@ use serde_json::{Value, json}; const MAX_CANDIDATES: usize = 128; const MAX_DETAILS: usize = 64; +#[path = "interactions/marquee.rs"] +mod marquee; +fn component_set_error(field: &str, error: &str) -> bool { + matches!(field, "componentProperties" | "variantProperties") + && (error.contains("Component set for node has existing errors") + || error.contains("Component set is invalid")) +} + fn bounded(value: &Value, depth: usize, truncated: &mut bool) -> Value { if depth == 0 && (value.is_object() || value.is_array()) { *truncated = true; @@ -52,6 +60,7 @@ pub(super) enum Role { Nav, Checkbox, Radio, + Marquee, } #[derive(Debug, Clone, Serialize, schemars::JsonSchema)] @@ -235,6 +244,10 @@ pub(super) fn candidates(snapshot: &Snapshot, roots: &[String]) -> Vec Value { ] .contains(&field.as_str()) { - read_errors.push(json!({"nodeId":id,"field":field,"reason":error})); + read_errors.push(json!({"nodeId":id,"field":field,"reason":error, + "classification":if component_set_error(field,error){"design-component-set-error"}else{"read-error"}, + "componentSetId":view.string("mainComponentSetId"), + "componentSetName":view.string("mainComponentSetName")})); } } if let Some(master) = view @@ -513,7 +529,7 @@ pub(super) fn evidence(snapshot: &Snapshot, roots: &[String]) -> Value { "shapeAndStateVerified":false})); } } - let mut shared = masters.into_iter().filter(|(_,members)| members.len()>1).map(|(master,mut members)| { + let mut shared = masters.into_iter().map(|(master,mut members)| { let signatures: BTreeSet<_> = members.iter().map(|m| json!([m["text"],m["variantProperties"],m["componentProperties"]]).to_string()).collect(); let total=members.len(); members.truncate(24); json!({"componentId":master,"instances":members,"instanceCount":total,"instancesOmitted":total-members.len(),"variantDifferences":signatures.len()>1, @@ -593,11 +609,20 @@ pub(super) fn attach(result: &mut Value, snapshot: &Snapshot) { ); } } - if data["interactionReadErrors"] - .as_array() - .is_some_and(|a| !a.is_empty()) - { - result.as_object_mut().unwrap().entry("projectionIssues").or_insert(json!([])).as_array_mut().unwrap().push(json!({"code":"DEVUP_CODEGEN_INTERACTION_UNVERIFIED","severity":"warning","fidelityImpact":"none","message":"Interaction/state fields could not be read; candidate coverage is incomplete.","details":{"classification":"interaction-unverified","nextAction":"Review interactionReadErrors and recollect failed fields before claiming interaction coverage."}})); + let read_errors = data["interactionReadErrors"].as_array().unwrap(); + for error in read_errors.iter().filter(|error| error["classification"] == "design-component-set-error") { + result.as_object_mut().unwrap().entry("projectionIssues").or_insert(json!([])).as_array_mut().unwrap().push(json!({ + "code":"DEVUP_DESIGN_COMPONENT_SET_ERROR","nodeId":error["nodeId"],"property":error["field"], + "severity":"warning","fidelityImpact":"none", + "message":"the Figma component set is broken; open it in Figma and fix its error (Figma shows a warning on the set)", + "details":error})); + } + let unreadable = read_errors.iter().filter(|error| error["classification"] != "design-component-set-error" + && matches!(error["field"].as_str(),Some("reactions" | "mainComponentId" | "mainComponentSetId"))) + .collect::>(); + if !unreadable.is_empty() { + let fields = unreadable.iter().map(|error| format!("{} on node {}",error["field"].as_str().unwrap(),error["nodeId"].as_str().unwrap())).collect::>().join(", "); + result.as_object_mut().unwrap().entry("projectionIssues").or_insert(json!([])).as_array_mut().unwrap().push(json!({"code":"DEVUP_CODEGEN_INTERACTION_UNVERIFIED","severity":"warning","fidelityImpact":"none","message":format!("Interaction fields could not be read: {fields}; candidate coverage is incomplete."),"details":{"classification":"interaction-unverified","readErrors":unreadable,"nextAction":"Review interactionReadErrors and recollect failed fields before claiming interaction coverage."}})); if result["deliverable"].is_object() { result["deliverable"]["isFinal"] = json!(false); result["deliverable"]["note"] = @@ -636,6 +661,7 @@ pub(super) fn attach(result: &mut Value, snapshot: &Snapshot) { let unknown_targets = result_candidates(&data) .iter() .filter(|c| { + if c["role"] == "marquee" { return false; } c["prototypeActions"] .as_array() .is_none_or(|a| a.is_empty() || a.iter().any(|a| !a["url"].is_string())) diff --git a/crates/devup-mcp/src/server/interactions/marquee.rs b/crates/devup-mcp/src/server/interactions/marquee.rs new file mode 100644 index 00000000..a4ea664a --- /dev/null +++ b/crates/devup-mcp/src/server/interactions/marquee.rs @@ -0,0 +1,109 @@ +use super::*; +use devup_mcp_devup_ui::ui_validate::visual_behavior::{MARQUEE_RECIPE, repeated_separator}; + +pub(super) fn candidate(snapshot: &Snapshot, node: &RawNode) -> Option { + let view = node.typed_view(); + let children = view.child_ids().filter_map(|id| snapshot.nodes.get(id)) + .filter(|child| child.typed_view().bool("visible") != Some(false)).collect::>(); + let row = view.string("layoutMode") == Some("HORIZONTAL") + && view.string("layoutWrap") != Some("WRAP") + && children.len() >= 3 + && children.iter().all(|child| child.node_type == "TEXT") + && children.iter().filter_map(|child| child.typed_view().string("characters")) + .collect::>().len() < children.len(); + if node.node_type != "TEXT" && !row { return None } + let text = if row { + children.iter().filter_map(|child| child.typed_view().string("characters")).collect::>().join(" ") + } else { str_field(node, "characters").to_owned() }; + if text.trim().is_empty() || text.contains(['\n', '\r']) { return None } + let single_line = row || view.string("textAutoResize") == Some("WIDTH_AND_HEIGHT") + || view.number("maxLines") == Some(1.0) + || matches!((view.number("height"), view.number("fontSize")), (Some(height), Some(size)) if height <= size * 1.6); + let width = view.number("width"); + let mut current = node; + let mut visited = BTreeSet::new(); + let mut clip = None; + while visited.insert(current.id.as_str()) { + let parent = current.typed_view().string("parentId") + .and_then(|id| snapshot.nodes.get(id)) + .or_else(|| snapshot.nodes.values().find(|parent| parent.typed_view().child_ids().any(|id| id == current.id))); + let Some(parent) = parent else { break }; + let parent_view = parent.typed_view(); + if parent_view.bool("clipsContent") == Some(true) + && matches!((width, parent_view.number("width")), (Some(width), Some(clip_width)) if width > clip_width + 1.0) + { + clip = Some(parent.id.clone()); + break; + } + current = parent; + } + let separators = repeated_separator(&text); + if !(separators || single_line && clip.is_some()) { return None } + Some(InteractiveCandidate { + node_id: node.id.clone(), name: str_field(node, "name").into(), + text: text.chars().take(500).collect(), role: Role::Marquee, + reason: [separators.then_some("repeated-separator-pattern"), + (single_line && clip.is_some()).then_some("single-line-text-exceeds-clipping-ancestor")] + .into_iter().flatten().map(str::to_owned).collect(), + has_prototype_target: false, target_resolved: false, + unresolved_reasons: vec!["animation-intent-is-heuristic-not-figma-animation-data".into()], + prototype_actions: Vec::new(), + states: json!({"confidence":"heuristic", "clippingAncestorId":clip, + "runtimeStateVerified":false,"recipe":MARQUEE_RECIPE, + "animation":{"from":"translateX(0)","to":"translateX(-50%)","iterationCount":"infinite","timingFunction":"linear","reducedMotion":"none","duplicateAriaHidden":true}}), + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn snapshot(nodes: Value) -> Snapshot { + Snapshot { file_key: "fixture".into(), version: None, roots: vec!["clip".into()], + nodes: nodes.as_array().unwrap().iter().map(|node| { + let node: RawNode = serde_json::from_value(node.clone()).unwrap(); + (node.id.clone(), node) + }).collect(), diagnostics: Vec::new() } + } + + #[test] + fn marquee_candidates_report_overflow_through_intermediate_ancestors() { + let snapshot = snapshot(json!([ + {"id":"clip","type":"FRAME","fields":{"width":400,"clipsContent":true,"childrenIds":["track"]}}, + {"id":"track","type":"FRAME","fields":{"childrenIds":["text"]}}, + {"id":"text","type":"TEXT","fields":{"width":1200,"textAutoResize":"WIDTH_AND_HEIGHT","characters":"An overflowing research strip without separator glyphs"}} + ])); + let output = evidence(&snapshot, &snapshot.roots); + let candidate = &output["interactiveCandidates"][0]; + assert_eq!(candidate["role"], "marquee"); + assert_eq!(candidate["states"]["confidence"], "heuristic"); + assert_eq!(candidate["states"]["clippingAncestorId"], "clip"); + assert_eq!(candidate["states"]["animation"]["duplicateAriaHidden"], true); + } + + #[test] + fn marquee_candidates_detect_separators_and_repeated_rows() { + let snapshot = snapshot(json!([ + {"id":"clip","type":"FRAME","fields":{"width":400,"clipsContent":true,"childrenIds":["text","row"]}}, + {"id":"text","type":"TEXT","fields":{"characters":"Research ✦ Repair ✦ Renew ✦ Research"}}, + {"id":"row","type":"FRAME","fields":{"width":600,"layoutMode":"HORIZONTAL","childrenIds":["a","b","c"]}}, + {"id":"a","type":"TEXT","fields":{"characters":"Research","width":200}}, + {"id":"b","type":"TEXT","fields":{"characters":"Research","width":200}}, + {"id":"c","type":"TEXT","fields":{"characters":"Research","width":200}} + ])); + assert!(candidate(&snapshot, &snapshot.nodes["text"]).is_some()); + assert!(candidate(&snapshot, &snapshot.nodes["row"]).is_some()); + let mut wrapped = snapshot; + wrapped.nodes.get_mut("row").unwrap().fields.insert("layoutWrap".into(), json!("WRAP")); + assert!(candidate(&wrapped, &wrapped.nodes["row"]).is_none()); + } + + #[test] + fn marquee_candidates_ignore_wrapped_unclipped_and_short_text() { + let snapshot = snapshot(json!([ + {"id":"clip","type":"FRAME","fields":{"width":400,"childrenIds":["text"]}}, + {"id":"text","type":"TEXT","fields":{"width":1200,"textAutoResize":"HEIGHT","characters":"Research ✦ Repair\n✦ Renew ✦ Research"}} + ])); + assert!(candidate(&snapshot, &snapshot.nodes["text"]).is_none()); + } +} diff --git a/crates/devup-mcp/src/server/mod.rs b/crates/devup-mcp/src/server/mod.rs index db0144e5..ca3f4fd0 100644 --- a/crates/devup-mcp/src/server/mod.rs +++ b/crates/devup-mcp/src/server/mod.rs @@ -1,5 +1,6 @@ // Source sections share this module to preserve privacy and test names. include!("mod_parts/state_and_services.rs"); include!("mod_parts/acquisition.rs"); +include!("mod_parts/component_width_acquisition.rs"); include!("mod_parts/exploration_and_responses.rs"); include!("mod_parts/p2_recovery_tests.rs"); diff --git a/crates/devup-mcp/src/server/mod_parts/acquisition.rs b/crates/devup-mcp/src/server/mod_parts/acquisition.rs index 5601c2c8..7c86630a 100644 --- a/crates/devup-mcp/src/server/mod_parts/acquisition.rs +++ b/crates/devup-mcp/src/server/mod_parts/acquisition.rs @@ -485,6 +485,18 @@ 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 { + explored["figmaTheme"] = screen_plan::planning_theme(&variables)?; + } + } let candidates = explored["candidates"] .as_array() .cloned() @@ -517,6 +529,8 @@ impl DevupServer { "nodeId": explored["anchor"]["nodeId"], "anchorName": explored["anchor"]["name"], "anchorBreadcrumb": explored["anchor"]["breadcrumb"], + "nodePage": explored["nodePage"], + "figmaTheme": explored["figmaTheme"], "targetKind": explored["targetKind"], "fileKey": explored["source"]["fileKey"], "truncation": explored["truncation"], @@ -897,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; @@ -908,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.", }); @@ -931,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.", }); @@ -954,4 +974,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/mod_parts/component_width_acquisition.rs b/crates/devup-mcp/src/server/mod_parts/component_width_acquisition.rs new file mode 100644 index 00000000..282184cb --- /dev/null +++ b/crates/devup-mcp/src/server/mod_parts/component_width_acquisition.rs @@ -0,0 +1,85 @@ +impl DevupServer { + async fn plan_component_widths( + &self, + input: &screen_plan::ScreenPlanInput, + screens: &[screen_plan::PlanScreenInput], + source: &mut Value, + ) -> Result<(), DevupError> { + if !input.screens.is_empty() + && self.services.upstream.bridge_path_snapshot().await.is_none_or(|bridge| bridge.attached_files.len() != 1) { + source["componentWidthEvidence"] = json!({"state":"unavailable","reason":"Caller screen summaries have no component identity; no unique Bridge file is attached."}); + return Ok(()); + } + let target = self.bind_to_bridge(parse_link(input.url.as_deref())?, Unnamed::Selection, + &json!({"tool":"devup_screen_plan","arguments":{"action":"draft"}})).await?; + let mut snapshot = None; + let mut payloads = Vec::new(); + for screen in screens.iter().filter(|screen| screen.visible != Some(false)) { + let mut target = target.clone(); + target.node_id = Some(screen.node_id.clone()); + let request = CollectionRequest::new(target, CollectionScope::Node); + let key = ArtifactRequestKey::from_collection(&request); + let payload = CollectedPayload::try_from(self.run_direct(request, &PendingOperation::Collect, None).await?)?; + let artifact = self.artifacts.insert(key, payload).await?; + match &mut snapshot { + None => snapshot = Some(artifact.payload.snapshot.clone()), + Some(combined) => combined.nodes.extend(artifact.payload.snapshot.nodes.clone()), + } + payloads.push(artifact.artifact_id); + } + let Some(snapshot) = snapshot else { return Ok(()) }; + let roots = screens.iter().map(|screen| screen.node_id.clone()).collect::>(); + let mut mismatches = screen_plan::component_width_mismatches(&snapshot, &roots); + for mismatch in &mut mismatches { + let mut references = Vec::new(); + let mut render_bytes = Vec::new(); + for field in ["nodeId", "correctNodeId"] { + let Some(id) = mismatch[field].as_str() else { continue }; + let mut target = target.clone(); + target.node_id = Some(id.into()); + let mut request = CollectionRequest::new(target, CollectionScope::Node); + request.asset_selections.push(devup_mcp_figma::AssetSelection { + asset_id: format!("{id}:node"), format: devup_mcp_figma::AssetFormat::Png, + scale: 1.0, render_mode: devup_mcp_figma::AssetRenderMode::Node, + usage_aspect_ratio: None, max_width: None, srcset: false, + visible_scope_node_id: None, quality: None, lossless: None, + }); + let key = ArtifactRequestKey::from_collection(&request); + match self.run_direct(request, &PendingOperation::Collect, None).await.and_then(CollectedPayload::try_from) { + Ok(payload) => { + let artifact = self.artifacts.insert(key, payload).await?; + for asset in &artifact.payload.assets { + if asset.node_id == id + && asset.status == devup_mcp_figma::AssetStatus::Exported + && asset.format == Some(devup_mcp_figma::AssetFormat::Png) + && asset.scale == Some(1.0) + && asset.render_info.as_ref().is_some_and(|info|info["representation"] == "native-node") { + references.push(json!({"nodeId":id,"artifactId":artifact.artifact_id,"assetId":asset.asset_id,"renderMode":"node","format":"png","scale":1})); + if let Some(encoded) = &asset.data_base64 { + use base64::Engine as _; + if let Ok(bytes) = base64::engine::general_purpose::STANDARD.decode(encoded) { + render_bytes.push(bytes); + } + } + } + } + payloads.push(artifact.artifact_id); + } + Err(error) => mismatch["renderError"] = json!(error.to_string()), + } + } + mismatch["render"] = json!(if render_bytes.len() == 2 { "captured" } else { "unavailable" }); + mismatch["renderReferences"] = json!(references); + mismatch["comparison"] = json!({"method":"drawn-width-majority","pixelComparison":"unverified","decision":"use expected master as one responsive component"}); + if let [suspect, correct] = render_bytes.as_slice() { + match devup_mcp_visual::compare_png_bytes(correct, suspect, &devup_mcp_visual::CompareOptions::default(), false) { + Ok((report, _)) => mismatch["comparison"]["pixelComparison"] = serde_json::to_value(report).map_err(|error|DevupError::new(ErrorCode::DevupInvalidInput,error.to_string(),false))?, + Err(error) => mismatch["comparison"]["renderError"] = json!(error.to_string()), + } + } + } + source["componentWidthArtifacts"] = json!(payloads); + source["componentWidthMismatches"] = json!(mismatches); + Ok(()) + } +} diff --git a/crates/devup-mcp/src/server/mod_parts/exploration_and_responses.rs b/crates/devup-mcp/src/server/mod_parts/exploration_and_responses.rs index 277798be..5d3e0954 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 @@ -214,6 +214,7 @@ fn attach_layout_and_source_links(result: &mut Value, input: &UiValidateInput) { }; let matches = entries .iter() + .copied() .filter(|entry| entry.element_index == usize::try_from(element).ok()) .filter(|entry| { entry @@ -224,15 +225,25 @@ fn attach_layout_and_source_links(result: &mut Value, input: &UiValidateInput) { }) }) .take(3) - .map(|entry| { - json!({"nodeId": entry.node_id, "property": entry.property, - "generatedProperty": entry.generated_property, "resolution": entry.resolution}) - }) .collect::>(); - if !matches.is_empty() { - linked += 1; - violation["source"] = json!(matches); + if matches.is_empty() { + continue; + } + linked += 1; + if violation["rule"] + .as_str() + .is_some_and(|rule| rule.starts_with("hardcoded-")) + { + violation["designBinding"] = design_binding(&matches); } + violation["source"] = json!( + matches + .iter() + .map(|entry| json!({"nodeId": entry.node_id, "property": entry.property, + "generatedProperty": entry.generated_property, "resolution": entry.resolution, + "variableId": entry.variable_id})) + .collect::>() + ); } let unlinked = result["violations"].as_array().map_or(0, Vec::len) - linked; result["sourceMapLink"] = json!({"state": "linked", "linkedViolations": linked, @@ -244,6 +255,73 @@ fn attach_layout_and_source_links(result: &mut Value, input: &UiValidateInput) { } } +/// A hardcoded value whose Figma node binds no variable is what the design +/// says, so a token that only shares the value is a coincidence: the finding +/// drops to info and offers no replacement. Runs before severities are +/// counted, so `ok`, `okReason`, `strict` and `correctedTsx` all see it. +fn relax_unbound_token_matches( + violations: &mut [devup_mcp_devup_ui::ui_validate::Violation], + input: &UiValidateInput, +) { + use devup_mcp_devup_ui::ui_validate::Severity; + let Some(source_map) = &input.source_map else { + return; + }; + if input.files.is_some() { + return; + } + let entries = source_map + .entries + .iter() + .chain(&source_map.tsx) + .collect::>(); + for violation in violations.iter_mut().filter(|v| { + v.severity == Severity::Warning && matches!(v.rule, "hardcoded-color" | "hardcoded-length") + }) { + let (Some(element), Some(attribute)) = ( + violation.context.get("elementIndex").and_then(Value::as_u64), + violation.context.get("attribute").and_then(Value::as_str), + ) else { + continue; + }; + let generated = entries + .iter() + .copied() + .filter(|entry| entry.element_index == usize::try_from(element).ok()) + .filter(|entry| { + entry.generated_property.as_deref().is_some_and(|generated| { + generated == attribute || generated.starts_with(&format!("{attribute}=")) + }) + }) + .collect::>(); + if generated.is_empty() || generated.iter().any(|entry| entry.variable_id.is_some()) { + continue; + } + violation.severity = Severity::Info; + violation.suggestion = Some( + "The Figma design hardcodes this value too (no variable bound), so keeping it matches the design. Bind a variable in Figma and export again if it should follow the theme.".into(), + ); + } +} + +/// Whether the Figma node behind a hardcoded value bound a variable. A value +/// match against the theme is a coincidence of numbers; the binding is what +/// the designer chose, so it outranks `tokenMatches`. Unbound means the +/// design itself hardcodes the value, and no token replacement is implied. +fn design_binding(entries: &[&crate::server::tools::SourceMapEntryInput]) -> Value { + let variables = entries + .iter() + .filter_map(|entry| entry.variable_id.as_deref()) + .collect::>(); + if variables.is_empty() { + json!({"state": "unbound", + "note": "The Figma node has no variable bound to this property: the design also hardcodes it. tokenMatches are value coincidences, not the designer's choice."}) + } else { + json!({"state": "bound", "variableIds": variables, + "note": "Use the token generated for this Figma variable (devupJson/variable mapping), not a token that merely shares the value."}) + } +} + /// The target a tool's url names. No url is the file the attached Devup Bridge /// plugin has open, which [`DevupServer::bind_to_bridge`] then resolves. fn parse_link(url: Option<&str>) -> Result { @@ -418,11 +496,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 @@ -437,16 +510,21 @@ fn with_project_checks( } } } - // Before the theme half, and before its `project_root` gate: a response - // that handed back code has to name the conventions that code is written - // in whether or not the caller told us where the project is. + generated.extend(projection::default_code_paths::written_sources(&result)); + // Code delivery must name its conventions, independently of project_root. let produced_code = !generated.is_empty() || result.get("deliverable").is_some(); if produced_code { result = with_skill_gap(result, lookup); } + validation_guidance::attach_export_formatting(&mut result, &generated); 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 @@ -516,8 +594,31 @@ 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 { 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/navigation.rs b/crates/devup-mcp/src/server/navigation.rs index 4e56bd76..d38916c8 100644 --- a/crates/devup-mcp/src/server/navigation.rs +++ b/crates/devup-mcp/src/server/navigation.rs @@ -1,5 +1,6 @@ //! Static navigation checks share the UI inventory; they never guess destinations. use std::collections::{BTreeMap, BTreeSet}; +use devup_mcp_devup_ui::effective_attributes::AttributeContext; use oxc_allocator::Allocator; use oxc_ast::ast::*; @@ -33,6 +34,7 @@ struct Collector<'s> { link_names: BTreeSet, elements: Vec, diagnostics: Vec, + attributes: AttributeContext, } // Only literal visual markers on a navigation candidate warrant this hint. @@ -89,22 +91,6 @@ fn fixed_visual_marker(attrs: &BTreeMap<&str, &JSXAttribute<'_>>) -> bool { .is_some_and(|v| !v.is_empty() && v != "transparent")) } -fn boolean(a: &JSXAttribute<'_>) -> Option { - match &a.value { - None => Some(true), - Some(JSXAttributeValue::ExpressionContainer(c)) => match &c.expression { - JSXExpression::BooleanLiteral(b) => Some(b.value), - _ => None, - }, - Some(JSXAttributeValue::StringLiteral(s)) => match s.value.as_str() { - "true" => Some(true), - "false" => Some(false), - _ => None, - }, - _ => None, - } -} - impl<'a> Visit<'a> for Collector<'_> { fn visit_import_declaration(&mut self, declaration: &ImportDeclaration<'a>) { if declaration.source.value == "next/link" { @@ -131,35 +117,31 @@ impl<'a> Visit<'a> for Collector<'_> { } }) .collect(); - let href = attrs.get("href").and_then(|a| literal(a)); + let effective = self.attributes.read(&element.opening_element); + let rendered_as = effective.rendered_as(); + let name = rendered_as.as_deref().unwrap_or(name); + let has_href = effective.contains("href"); + let href = effective.literal("href"); let is_link = name == "a" || self.link_names.contains(name); let native = matches!(name, "button" | "input" | "select" | "textarea") - && !attrs - .get("type") - .and_then(|a| literal(a)) + && !effective.literal("type") .is_some_and(|s| s == "hidden"); - let handler = attrs - .keys() - .any(|k| matches!(*k, "onClick" | "onChange" | "onSubmit")); + let handler = ["onClick", "onChange", "onSubmit"].iter().any(|name| effective.contains(name)); let static_target = href .as_deref() .is_some_and(|h| !h.trim().is_empty() && h != "#" && !h.starts_with("javascript:")); - let disabled = attrs.get("disabled").and_then(|a| boolean(a)) == Some(true); + let disabled = effective.boolean("disabled") == Some(true); let native_toggle = name == "input" - && attrs - .get("type") - .and_then(|a| literal(a)) + && effective.literal("type") .is_some_and(|s| ["checkbox", "radio"].contains(&s.as_str())); let form_control = native && self.form_ancestors > 0 && (name != "button" - || attrs - .get("type") - .and_then(|a| literal(a)) + || effective.literal("type") .is_none_or(|t| t == "submit")); let wired = (is_link && static_target) || (native && handler) || form_control; let form_action = - name == "form" && (attrs.contains_key("action") || attrs.contains_key("onSubmit")); + name == "form" && (effective.contains("action") || effective.contains("onSubmit")); let own_text = element .children .iter() @@ -172,11 +154,9 @@ impl<'a> Visit<'a> for Collector<'_> { }) .collect::>() .join(" "); - let accessible_name = attrs - .get("aria-label") - .and_then(|a| literal(a)) + let accessible_name = effective.literal("aria-label") .unwrap_or(own_text); - let role = attrs.get("role").and_then(|a| literal(a)); + let role = effective.literal("role"); let nav = name == "nav" || role.as_deref() == Some("navigation") || matches!( @@ -202,15 +182,11 @@ impl<'a> Visit<'a> for Collector<'_> { .filter(|b| *b == b'\n') .count() + 1; - let active = attrs - .get("aria-current") - .is_some_and(|a| literal(a).is_some_and(|s| s != "false")) + let active = effective.literal("aria-current").is_some_and(|s| s != "false") || ["data-active", "aria-selected"] .iter() - .any(|k| attrs.get(k).is_some_and(|a| boolean(a) == Some(true))) - || attrs.get("data-state").is_some_and(|a| { - literal(a).is_some_and(|s| ["active", "selected"].contains(&s.as_str())) - }); + .any(|k| effective.boolean(k) == Some(true)) + || effective.literal("data-state").is_some_and(|s| ["active", "selected"].contains(&s.as_str())); let visual_marker = !active && ((candidate && (self.nav_ancestors > 0 || nav || is_link || role.as_deref() == Some("tab"))) @@ -218,7 +194,7 @@ impl<'a> Visit<'a> for Collector<'_> { && fixed_visual_marker(&attrs); if candidate || active || visual_marker { self.elements.push(json!({"element":name,"name":accessible_name.trim(),"line":line,"offset":element.span.start, - "candidate":candidate,"wired":wired || self.linked_ancestors>0,"href":href,"dynamicHref":attrs.contains_key("href") && href.is_none(), + "candidate":candidate,"wired":wired || self.linked_ancestors>0,"href":href,"dynamicHref":has_href && href.is_none(), "nodeId":attrs.get("data-figma-node-id").and_then(|a|literal(a)), "semanticControl":is_link || native,"nativeToggleOnly":native_toggle && !wired,"disabled":disabled, "staticActive":active,"nonSemanticClick":handler && matches!(name,"div"|"span"|"Box"|"Flex"|"Text"|"Center"|"Grid"), @@ -250,6 +226,7 @@ pub(super) fn source_evidence(source: &str) -> Value { } let mut collector = Collector { source, + attributes: AttributeContext::from_program(&parsed.program), ..Default::default() }; // Imports may follow declarations; collect aliases before traversing JSX. @@ -555,17 +532,46 @@ pub(super) fn suggestions(candidates: &Value, inventory: &Value) -> Value { ("reservation", vec!["reservation", "견적", "문의하기"]), ("about", vec!["about", "소개"]), ("services", vec!["services", "서비스"]), + ("location", vec!["location", "오시는 길", "찾아오시는 길", "map"]), + ("contact", vec!["contact", "문의", "연락"]), + ("company", vec!["company", "회사", "기업"]), + ("news", vec!["news", "뉴스", "소식"]), + ("notice", vec!["notice", "공지"]), + ("products", vec!["products", "제품"]), + ("technology", vec!["technology", "기술"]), + ("research", vec!["research", "연구"]), + ("patents", vec!["patents", "특허"]), + ("vision", vec!["vision", "비전"]), + ("history", vec!["history", "연혁"]), + ("ceo", vec!["ceo", "인사말"]), + ("careers", vec!["careers", "채용"]), + ("partners", vec!["partners", "파트너"]), ]; - let options=array(&inventory["routes"]).iter().filter(|r| { + let compact = |value: &str| value.split_whitespace().collect::().to_lowercase(); + let matches_label = |value: &Value| value.as_str().is_some_and(|value| !value.is_empty() && ["text", "name"].iter().any(|key| candidate[*key].as_str().is_some_and(|text| compact(text) == compact(value)))); + let mut options = array(&inventory["codeLinks"]).iter().filter(|link| matches_label(&link["label"])) + .map(|link| json!({"route":link["href"],"evidence":link["evidence"],"kind":"existing-code-link","requiresRecordId":link["href"].as_str().is_some_and(|s|s.contains('['))})).collect::>(); + let resolved = !options.is_empty(); + if options.is_empty() { + options = array(&inventory["confirmedRoutes"]).iter().filter(|route| matches_label(&route["name"])) + .map(|route| json!({"route":route["route"],"evidence":"confirmed-screen-plan","requiresRecordId":route["route"].as_str().is_some_and(|s|s.contains('['))})).collect(); + } + if options.is_empty() { + options=array(&inventory["routes"]).iter().filter(|r| { let route=r["route"].as_str().unwrap_or("").to_lowercase(); - concepts.iter().any(|(segment,labels)|route.split('/').any(|s|s==*segment) && labels.iter().any(|s|label.contains(s))) + concepts.iter().any(|(segment,labels)|route.split('/').any(|s|s==*segment) && labels.iter().any(|s|compact(&label).contains(&compact(s)))) }).map(|r|json!({"route":r["route"],"page":r["page"],"appliesTo":r["appliesTo"],"requiresRecordId":r["route"].as_str().is_some_and(|s|s.contains('['))})).collect::>(); - result.push(json!({"nodeId":candidate["nodeId"],"suggestions":options,"targetResolved":false, - "reason":if options.is_empty(){"No matching route evidence; ask for destination/action."}else{"Label/path heuristic only; confirm authority and record IDs before wiring. Suggestions never resolve a target automatically."}})); + } + result.push(json!({"nodeId":candidate["nodeId"],"suggestions":options,"targetResolved":resolved, + "reason":if resolved {"Matching existing code link provides a literal destination and file:line evidence; runtime wiring remains unverified."} else if options.is_empty(){"No matching route evidence; ask for destination/action."}else{"Plan/label/path suggestion only; confirm authority and record IDs before wiring."}})); } json!(result) } +#[cfg(test)] +#[path = "navigation_suggestion_tests.rs"] +mod suggestion_tests; + /// Compare only explicitly bound URLs. Figma destination node IDs remain unverified. pub(super) fn graph( design: &Value, diff --git a/crates/devup-mcp/src/server/navigation_suggestion_tests.rs b/crates/devup-mcp/src/server/navigation_suggestion_tests.rs new file mode 100644 index 00000000..c9ac0048 --- /dev/null +++ b/crates/devup-mcp/src/server/navigation_suggestion_tests.rs @@ -0,0 +1,27 @@ +use super::*; + +#[test] +fn nested_korean_location_uses_any_route_segment() { + let result = suggestions(&json!([{"nodeId":"location","text":"오시는 길"}]), &json!({"routes":[{"route":"/company/location"}]})); + assert_eq!(result[0]["suggestions"][0]["route"], "/company/location"); + assert_eq!(result[0]["targetResolved"], false); +} + +#[test] +fn existing_links_precede_confirmed_names_and_concepts() { + let result = suggestions(&json!([{"nodeId":"location","text":"오시는 길"}]), &json!({ + "routes":[{"route":"/company/location"}], + "codeLinks":[{"label":"오시는길","href":"/visit","evidence":"src/components/Header.tsx:7"}], + "confirmedRoutes":[{"name":"오시는 길","route":"/directions"}] + })); + assert_eq!(result[0]["suggestions"][0]["route"], "/visit"); + assert_eq!(result[0]["suggestions"][0]["evidence"], "src/components/Header.tsx:7"); + assert_eq!(result[0]["targetResolved"], true); +} + +#[test] +fn confirmed_names_are_suggestions_not_resolved_targets() { + let result = suggestions(&json!([{"nodeId":"location","text":"오시는 길"}]), &json!({"confirmedRoutes":[{"name":"오시는길","route":"/directions"}]})); + assert_eq!(result[0]["suggestions"][0]["route"], "/directions"); + assert_eq!(result[0]["targetResolved"], false); +} diff --git a/crates/devup-mcp/src/server/page_scaffold.rs b/crates/devup-mcp/src/server/page_scaffold.rs index 41e9570b..c399ca13 100644 --- a/crates/devup-mcp/src/server/page_scaffold.rs +++ b/crates/devup-mcp/src/server/page_scaffold.rs @@ -70,6 +70,9 @@ pub(crate) fn validate( } fn key(result: &Map) -> Result { + if result.get("responsiveTsx").is_some() { + return Ok("pageScaffold".into()); + } if let Some(frames) = result.get("frames").and_then(Value::as_array) { if frames.len() != 1 { return Err(invalid( @@ -161,7 +164,7 @@ pub(crate) fn attach( .as_object_mut() .unwrap() }; - let code = carrier.get("tsx").and_then(Value::as_str).ok_or_else(|| { + let code = carrier.get("responsiveTsx").or_else(|| carrier.get("tsx")).and_then(Value::as_str).ok_or_else(|| { invalid("pageScaffold has no generated screen; inspect projection failures.") })?; // The existing generator emits one named function. Do not split its JSX or infer hooks. @@ -252,7 +255,7 @@ pub(crate) fn attach( .get_mut("outputResults") .and_then(Value::as_object_mut) { - if let Some(tsx) = output.get("tsx").cloned() { + if let Some(tsx) = output.get("responsiveTsx").or_else(|| output.get("tsx")).cloned() { output.insert("pageScaffold".into(), tsx); } if !keep_tsx { diff --git a/crates/devup-mcp/src/server/project_context/context_and_inventory.rs b/crates/devup-mcp/src/server/project_context/context_and_inventory.rs index 59493f45..1d0fa33a 100644 --- a/crates/devup-mcp/src/server/project_context/context_and_inventory.rs +++ b/crates/devup-mcp/src/server/project_context/context_and_inventory.rs @@ -17,6 +17,7 @@ mod ui; mod export; pub(super) use export::attach as attach_export_context; pub(super) use export::export_context; +pub(super) use export::checked_export; pub(super) fn routes_for_validation(project_root: Option<&str>) -> Value { project_root.map(export_context).map(|v|v["ui"].clone()) @@ -721,4 +722,4 @@ pub(super) fn relative_display(root: &Path, file: &Path) -> String { file.strip_prefix(root) .map(|relative| relative.to_string_lossy().replace('\\', "/")) .unwrap_or_else(|_| display_path(file)) -} \ No newline at end of file +} diff --git a/crates/devup-mcp/src/server/project_context_export.rs b/crates/devup-mcp/src/server/project_context_export.rs index 66d5b516..3fc8cf37 100644 --- a/crates/devup-mcp/src/server/project_context_export.rs +++ b/crates/devup-mcp/src/server/project_context_export.rs @@ -115,13 +115,18 @@ pub(crate) fn export_context(project_root: &str) -> Value { /// Added context is bounded independently and respects remaining inline space. pub(crate) fn attach(result: &mut Value, root: &str) { - let context = export_context(root); + let mut context = export_context(root); + if let Some(evidence) = result.as_object_mut().unwrap().remove("_navigationEvidence") { + context["ui"]["codeLinks"] = evidence["codeLinks"].clone(); + context["ui"]["confirmedRoutes"] = evidence["confirmedRoutes"].clone(); + } let suggestions = super::super::navigation::suggestions(&result["interactiveCandidates"], &context["ui"]); + summarize(&mut context, result, &suggestions, root); let mut added = json!({"projectContext":context,"routeSuggestions":suggestions}); let wire_bytes = serde_json::to_vec(&super::super::tool_result(result.clone())) .map_or(usize::MAX, |v| v.len()); - let budget = 65_536usize.min( + let budget = 4_096usize.min( super::super::delivery::MAX_INLINE_TOTAL_BYTES // Review, skills, theme-validation and job metadata are attached // around this boundary and are also duplicated in MCP content. @@ -139,6 +144,8 @@ pub(crate) fn attach(result: &mut Value, root: &str) { "/projectContext/ui/routes", "/projectContext/ui/diagnostics", "/projectContext/ui/excludedPaths", + "/projectContext/ui/nonPageDestinations", + "/projectContext/referencedAssets", "/routeSuggestions", ]; let path = pointers @@ -173,3 +180,69 @@ pub(crate) fn attach(result: &mut Value, root: &str) { .unwrap() .extend(added.as_object().unwrap().clone()); } + +fn summarize(context: &mut Value, result: &Value, suggestions: &Value, root: &str) { + let ui = &context["ui"]; + let routes = ui["routes"].as_array().into_iter().flatten().filter(|route| suggestions.as_array().into_iter().flatten().any(|suggestion| suggestion["suggestions"].as_array().into_iter().flatten().any(|target| target["route"] == route["route"]))) + .map(|route| json!({"route":route["route"],"page":route["page"],"appliesTo":route["appliesTo"],"components":route["components"]})).collect::>(); + let components = ui["components"].as_array().into_iter().flatten().filter(|component| routes.iter().any(|route| route["components"].as_array().is_some_and(|paths| paths.contains(&component["path"])))) + .map(|component| json!({"path":component["path"],"exports":component["exports"],"appliesTo":component["appliesTo"]})).collect::>(); + let mut urls = BTreeSet::new(); + let mut sources = super::super::projection::default_code_paths::written_sources(result); + for scope in std::iter::once(result).chain(result["frames"].as_array().into_iter().flatten()) { + for output in ["tsx", "componentTsx", "responsiveTsx"] { + if let Some(source) = scope[output].as_str() { sources.push((output.into(), source.into())); } + } + for asset in scope["assetManifest"]["assets"].as_array().into_iter().flatten() { + if let Some(path) = asset["path"].as_str() { urls.insert(path.to_owned()); } + } + } + for (_, source) in sources { urls.extend(super::super::asset_guard::asset_urls(&source).into_iter().map(|(url, _)| url)); } + let destinations = ui["nonPageDestinations"].as_array().into_iter().flatten().filter(|destination| destination.as_str().is_some_and(|url| urls.contains(url) || suggestions.as_array().into_iter().flatten().any(|suggestion| suggestion["suggestions"].as_array().into_iter().flatten().any(|target| target["route"] == url)))).cloned().collect::>(); + let (public_files, _) = find_matching_files(Path::new(root), |path| path.components().any(|component| component.as_os_str() == "public"), usize::MAX); + let public_urls = public_files.iter().filter_map(|path| super::relative_display(Path::new(root), path).split_once("public/").map(|(_, asset)|format!("/{asset}"))).collect::>(); + let assets = urls.into_iter().map(|url| json!({"exists":public_urls.contains(url.split(['?', '#']).next().unwrap_or(&url)),"url":url})).collect::>(); + let counts = json!({"routes":ui["totalRoutes"],"components":ui["totalComponents"],"publicFiles":public_files.len()}); + let mut destinations = destinations; + destinations.extend(assets.iter().filter(|asset| asset["exists"] == true && !destinations.contains(&asset["url"])).map(|asset|asset["url"].clone()).collect::>()); + let summary = json!({"found":ui["found"],"routes":routes,"components":components,"nonPageDestinations":destinations,"customRoutingUnverified":ui["customRoutingUnverified"],"truncation":ui["truncation"]}); + *context = json!({"state":"summary","counts":counts,"ui":summary,"referencedAssets":assets, + "nextAction":{"tool":"devup_project_context","arguments":{"projectRoot":root,"scope":"ui"},"alternativeScope":"routes","how":"Read ui for the full component inventory, or routes for page destinations."}}); +} + +pub(crate) async fn navigation_evidence(result: &mut Value, root: &str) { + let code_links = match super::super::screen_plan::project_evidence(root).await { + Ok(evidence) => evidence.nav_links.into_iter().map(|(label, href, evidence)|json!({"label":label,"href":href,"evidence":evidence})).collect::>(), + Err(_) => vec![], + }; + let plan = super::super::screen_plan::read_plan(root).ok().flatten().unwrap_or(Value::Null); + let mut confirmed = vec![]; + if plan["status"] == "confirmed" { + for route in plan["routes"].as_array().into_iter().flatten() { + confirmed.push(json!({"name":route["name"],"route":route["url"]})); + } + for frame in plan["frameInventory"]["frames"].as_array().into_iter().flatten() { + if frame["route"].as_str().is_some_and(|route| route.starts_with('/')) { + confirmed.push(json!({"name":frame["name"],"route":frame["route"]})); + } + } + for label in plan["linkLabelCheck"]["labels"].as_array().into_iter().flatten().filter(|label|label["state"] == "route-bound") { + if let Some(url) = label["resolved"]["url"].as_str().filter(|url|url.starts_with('/')) { + confirmed.push(json!({"name":label["label"],"route":url})); + } + } + } + result["_navigationEvidence"] = json!({"codeLinks":code_links,"confirmedRoutes":confirmed}); +} + +pub(crate) async fn checked_export(mut result: Value, root: Option<&str>, lookup: &super::super::skills::Lookup) -> Value { + if result.get("interactionCoverage").is_some() && let Some(root) = root { + navigation_evidence(&mut result, root).await; + } + let mut result = super::super::with_project_checks(result, root, lookup); + if result["metadataDelivery"]["name"] == "written-export-details.json" && result["skillGap"].is_object() { + let gap = &result["skillGap"]; + result["skillGap"] = json!({"missing":gap["missing"].as_array().into_iter().flatten().map(|item|json!({"skill":item["skill"],"whyYouNeedIt":item["whyYouNeedIt"]})).collect::>(),"outdated":gap["outdated"],"runtime":gap["runtime"],"workspace":gap["workspace"],"nextAction":{"tool":"devup_skills","arguments":{"action":"status","projectRoot":root},"how":"Read skill installation details, install missing skills, then load them before editing generated code."}}); + } + result +} diff --git a/crates/devup-mcp/src/server/project_plan/erd.rs b/crates/devup-mcp/src/server/project_plan/erd.rs index 4dbe7f5d..7273e51a 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() @@ -149,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 { @@ -234,9 +266,32 @@ pub(super) fn draft(input: &ProjectPlanInput, plan: &mut Value) -> Result<(), De "vespertide revision -m \"confirmed project plan\"", "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." + ]); 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"]); @@ -254,6 +309,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")); } @@ -298,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, @@ -312,6 +386,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/mod.rs b/crates/devup-mcp/src/server/project_plan/mod.rs index 3300f3e4..57b41408 100644 --- a/crates/devup-mcp/src/server/project_plan/mod.rs +++ b/crates/devup-mcp/src/server/project_plan/mod.rs @@ -2,9 +2,11 @@ //! local evidence is collected before generating any unresolved questions. mod erd; mod rounds; +mod recovery; #[cfg(test)] mod tests; mod units; +mod wiring; use super::{ output::{OutputPolicy, OutputTransaction}, @@ -28,6 +30,10 @@ pub struct ProjectPlanInput { pub action: String, pub project_root: String, pub plan_id: Option, + #[serde(default)] + pub restart: bool, + #[serde(default)] + pub scope_exclusions: Vec, /// Facts read from Jira issues/epic children, Figma exploration, spec documents, /// and devup_project_context all scopes. Every fact includes its source. #[serde(default)] @@ -57,6 +63,14 @@ 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"]))] + pub detail: Option, } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] @@ -76,6 +90,9 @@ pub struct Feature { pub screens: Vec, #[serde(default)] pub acceptance: Vec, + /// Tables this feature reads or writes; shared tables group domain workers. + #[serde(default)] + pub entities: Vec, } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] @@ -113,15 +130,36 @@ pub struct Decision { pub recommended: String, #[serde(default)] pub destructive: bool, + pub state: Option, + pub kind: Option, } -#[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 { @@ -172,19 +210,276 @@ 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); + 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(()) +} +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 !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."}), + 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_plan(root: &str) -> Option { + match screen_plan::read_plan(root) { + 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() +} +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(deny_unknown_fields)] +#[schemars(extend("additionalProperties" = json!({"not": {}})))] +pub struct ScopeExclusion { + pub r#ref: String, + pub reason: String, +} +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", +]; +/// 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!({ + "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), + "backendChecks": wiring::backend_checks(plan, root), + "dataMappings": plan["dataMappings"], + "endpointContracts": plan["endpointContracts"], + }); + 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."}); + } + extra +} +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 sourced_defaults = array(&plan["questions"]).iter().filter(|question| !question["sourcedDefaults"].is_null()) + .map(|question| json!({"questionId":question["id"],"evidence":question["sourcedDefaults"]})).collect::>(); + let conflicts = array(&plan["questions"]).iter().filter(|question| { + text(&question["id"]).ends_with(".notifications") + && text(&question["sourcedDefaults"]["recommended"]) != "No notifications or integrations" + && plan["answers"][text(&question["id"])] == "No notifications or integrations" + }).map(|question| json!({"questionId":question["id"],"warning":"recommendationConflictsWithEvidence","evidence":question["sourcedDefaults"]})).collect::>(); + let mut out = base_response(plan, root, full); + out["sourcedDefaults"] = json!(sourced_defaults); + out["recommendationConflictsWithEvidence"] = json!(conflicts); + 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()); + } + 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()) .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":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":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,14 +493,32 @@ 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(); if input.action == "intake" { - if Path::new(&input.project_root).join(FILE).exists() { - return Err(invalid( + let existing = Path::new(&input.project_root).join(FILE).exists() + .then(|| read(&input.project_root)).transpose()?; + if let Some(existing) = existing.as_ref().filter(|_| !input.restart) { + let mut error = invalid( "Project plan exists; use draft/status instead of replacing decisions", - )); + ); + error.details = json!({"existingPlan":{"planId":existing["planId"],"state":existing["state"], + "confirmedAt":existing["confirmedAt"],"writtenBy":existing["writtenBy"],"files":[FILE]}, + "nextAction":{"tool":"devup_project_plan","arguments":{"action":"status", + "projectRoot":input.project_root,"planId":existing["planId"]}}, + "choices":["Continue existing plan","Archive existing plan before restarting"], + "restartAction":{"tool":"devup_project_plan","arguments":{"action":"intake","projectRoot":input.project_root,"restart":true}}, + "warnings": if existing["writtenBy"]["buildId"].as_str().is_some_and(|id| id != env!("DEVUP_MCP_BUILD_ID")) {vec!["Existing plan was written by a different build"]} else {vec![]}}); + return Err(error); } let facts = rounds::facts(input)?; let id = sha256(format!("{}{}", input.project_root, now()).as_bytes()); @@ -213,25 +526,84 @@ pub(super) async fn run( "facts":facts,"evidence":input.evidence,"answers":{},"decisions":[],"units":[], "questions":rounds::intake(&facts),"entities":[],"relations":[],"features":[],"issues":[], "unitQuestions":"supervisor"}); + plan["writtenBy"] = json!({"commit":option_env!("DEVUP_MCP_GIT_COMMIT"),"buildId":env!("DEVUP_MCP_BUILD_ID")}); + plan["scopeExclusions"] = json!(input.scope_exclusions); rounds::answers(&mut plan, &input.answers)?; - write(&mut tx, policy, &input.project_root, &plan)?; - tx.commit()?; - return Ok(response(plan)); + let archive = if input.restart && existing.is_some() { + Some(recovery::archive(&input.project_root, policy)?) + } else { None }; + if let Some(existing) = existing + && input.scope_exclusions.is_empty() { + plan["scopeExclusions"] = existing["scopeExclusions"].clone(); + } + let persisted = write(&mut tx, policy, &input.project_root, &plan).and_then(|()| tx.commit().map(|_| ())); + if let Err(error) = persisted { + if let Some(archive) = archive.as_ref() { + recovery::restore(&input.project_root, archive)?; + } + return Err(error); + } + if let Some(archive) = archive { plan["archivedTo"] = json!(archive); } + 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")); + 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() { + 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); + } + if !input.scope_exclusions.is_empty() { + for exclusion in &input.scope_exclusions { + if exclusion.r#ref.trim().is_empty() || exclusion.reason.trim().is_empty() { + return Err(invalid("Scope exclusions require ref and reason")); + } + } + plan["scopeExclusions"] = json!(input.scope_exclusions); } + let (answers, forward) = if matches!(input.action.as_str(), "draft" | "confirm") { + 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) + }; match input.action.as_str() { "draft" => { if plan["state"] == "confirmed" { return Err(invalid("Confirmed plans cannot be redrafted")); } - rounds::answers(&mut plan, &input.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)?; } - 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")); @@ -261,18 +633,37 @@ 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)) + .collect(); + rounds::answers(&mut plan, &new_answers)?; + if input.accept_recommended { + rounds::accept_recommended(&mut plan)?; + } plan["state"] = json!("draft"); } "confirm" => { 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)?; } - 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() .unwrap() @@ -287,15 +678,25 @@ 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) + ))); } + 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() + .map(|entity| format!("{}/{}.json", text(&plan["modelsDir"]), text(&entity["name"]))) + .collect::>() + } else { vec![] }); plan["state"] = json!("confirmed"); plan["confirmedAt"] = json!(now()); } @@ -345,7 +746,22 @@ 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)) + 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() + { + summary.remove("questionRounds"); + summary.remove("proposalMarkdown"); + } + if !forward.is_null() { + out["screenPlanForward"] = forward; + } + Ok(out) } diff --git a/crates/devup-mcp/src/server/project_plan/recovery.rs b/crates/devup-mcp/src/server/project_plan/recovery.rs new file mode 100644 index 00000000..55761ff0 --- /dev/null +++ b/crates/devup-mcp/src/server/project_plan/recovery.rs @@ -0,0 +1,33 @@ +use super::*; + +const FILES: [&str; 4] = ["project-plan.json", "screen-plan.json", "screen-plan.md", "units"]; + +pub(super) fn archive(root: &str, policy: &OutputPolicy) -> Result { + let relative = format!(".devup/archive/{}-{}", now().replace([':', '.'], "-"), rand::random::()); + let base = Path::new(root).join(".devup"); + let destination = Path::new(root).join(&relative); + for name in FILES { + policy.resolve(&base.join(name).to_string_lossy())?; + policy.resolve(&destination.join(name).to_string_lossy())?; + } + std::fs::create_dir_all(&destination).map_err(|error| invalid(error.to_string()))?; + for name in FILES { + let source = base.join(name); + if source.exists() && let Err(error) = std::fs::rename(&source, destination.join(name)) { + restore(root, &relative)?; + return Err(invalid(format!("Archive project plan: {error}"))); + } + } + Ok(relative) +} + +pub(super) fn restore(root: &str, archive: &str) -> Result<(), DevupError> { + let destination = Path::new(root).join(".devup"); + for name in FILES.into_iter().rev() { + let source = Path::new(root).join(archive).join(name); + if source.exists() { + std::fs::rename(source, destination.join(name)).map_err(|error| invalid(format!("Restore archived plan from {archive}: {error}")))?; + } + } + Ok(()) +} diff --git a/crates/devup-mcp/src/server/project_plan/rounds.rs b/crates/devup-mcp/src/server/project_plan/rounds.rs index d417380e..dabacee8 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,6 +25,29 @@ 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(), @@ -43,7 +70,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"]), ] @@ -106,7 +133,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", @@ -120,23 +150,64 @@ 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)", - ), - ] { + if array(&plan["scopeExclusions"]).iter().any(|exclusion| exclusion["ref"] == id) { + continue; + } + let sources = array(&plan["evidence"]).iter().filter(|evidence| { + !array(&plan["scopeExclusions"]).iter().any(|exclusion| exclusion["ref"] == evidence["source"]) + }).filter_map(|evidence| evidence["facts"].as_object().map(|facts| { + facts.iter().filter(|(key, _)| key.starts_with(&format!("{id}."))) + .map(|(_, value)| value.to_string()).collect::>().join(" ") + })).collect::>().join(" "); + let haystack = format!("{} {} {sources}", text(&feature["scope"]), feature["acceptance"]).to_lowercase(); + 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"]), + ("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 mut options = options; + if topic == "notifications" { + if ["mail", "smtp"].iter().any(|keyword| haystack.contains(keyword)) { + options.swap(0, 1); + } else if ["payment", "map", "oauth"].iter().any(|keyword| haystack.contains(keyword)) { + options.swap(0, 2); + } + } + let relevant = match topic { + "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 array(&plan["scopeExclusions"]).iter().any(|exclusion| exclusion["ref"] == key) { + continue; + } + if relevant && plan["facts"][&key].is_null() { + let mut question = 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); + question["sourcedDefaults"] = json!({"source":format!("features[{id}].scope/acceptance and feature-keyed evidence"),"recommended":options[0]}); + questions.push(question); + } + } + } + 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); } - if erd::api(plan) { + let mut questions = grouped; + if erd::api(plan) && !array(&plan["entities"]).is_empty() { for (key, topic) in [ ( "erd", @@ -158,7 +229,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.rs b/crates/devup-mcp/src/server/project_plan/tests.rs index cfbf45c6..026eeeb6 100644 --- a/crates/devup-mcp/src/server/project_plan/tests.rs +++ b/crates/devup-mcp/src/server/project_plan/tests.rs @@ -19,12 +19,17 @@ impl Drop for TestDir { } } 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() } 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 +52,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", @@ -191,7 +197,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 [ @@ -216,16 +222,17 @@ 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(), 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() }); assert!(run(&i, &policy).await.is_err()); i.unit_updates.clear(); @@ -235,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(); @@ -247,3 +256,131 @@ 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, + state: None, + kind: None, + } +} +#[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?")); +} + +#[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/tests/contracts.rs b/crates/devup-mcp/src/server/project_plan/tests/contracts.rs new file mode 100644 index 00000000..d97d312f --- /dev/null +++ b/crates/devup-mcp/src/server/project_plan/tests/contracts.rs @@ -0,0 +1,196 @@ +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","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","createdAt","views","categoryId"])); + let mut plain = plan.clone(); + plain["features"][0]["scope"] = json!("List"); + wiring::draft(&mut plain); + 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 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}, + {"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_requires_camel_case_fields_with_structural_paths_and_common_prefix() { + let report = report_contracts(json!([ + {"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":{"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":"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 [ + (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 new file mode 100644 index 00000000..77fe8072 --- /dev/null +++ b/crates/devup-mcp/src/server/project_plan/tests/feedback.rs @@ -0,0 +1,510 @@ +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","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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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":[], + "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":[ + {"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(); + 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(); + 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(); + 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(); + 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"])))); + 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] +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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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() { + 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(); + 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(); + 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(); + 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"); +} 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/tests/recovery.rs b/crates/devup-mcp/src/server/project_plan/tests/recovery.rs new file mode 100644 index 00000000..1df59d45 --- /dev/null +++ b/crates/devup-mcp/src/server/project_plan/tests/recovery.rs @@ -0,0 +1,182 @@ +use super::*; + +#[tokio::test] +async fn scope_exclusions_survive_status_and_restart() { + let dir = TestDir::new(); + let (mut request, policy, _) = drafted(dir.path(), false).await; + request.action = "draft".into(); + request.answers.clear(); + request.scope_exclusions = vec![ScopeExclusion { r#ref: "posts".into(), reason: "Outside this release".into() }]; + run(&request, &policy).await.unwrap(); + request.scope_exclusions.clear(); + request.action = "status".into(); + let status = run(&request, &policy).await.unwrap(); + assert_eq!(status["plan"]["scopeExclusions"][0]["ref"], "posts"); + request.action = "intake".into(); + request.restart = true; + run(&request, &policy).await.unwrap(); + assert_eq!(read(&request.project_root).unwrap()["scopeExclusions"][0]["ref"], "posts"); +} + +#[test] +fn feature_keyed_source_facts_supply_integration_recommendations() { + let plan = json!({"facts":{},"answers":{},"entities":[],"issues":[], + "evidence":[{"source":"spec","facts":{"contact.delivery":"SMTP email"}}], + "features":[{"id":"contact","scope":"Contact","screens":[],"acceptance":[]}]}); + let questions = rounds::detail(&plan, &[]).unwrap(); + let question = questions.iter().find(|question| question["id"] == "contact.notifications").unwrap(); + assert!(text(&question["options"][0]["label"]).starts_with("SMTP email")); + assert!(!question["sourcedDefaults"]["source"].is_null()); +} + +#[test] +fn excluded_features_do_not_supply_recommendations() { + let plan = json!({"facts":{},"answers":{},"entities":[],"issues":[], + "scopeExclusions":[{"ref":"contact","reason":"Not in release"}], + "features":[{"id":"contact","scope":"Send SMTP email","screens":[],"acceptance":[]}]}); + assert!(!rounds::detail(&plan, &[]).unwrap().iter().any(|question| text(&question["id"]).starts_with("contact."))); +} + +#[test] +fn smtp_evidence_recommends_email_instead_of_no_integration() { + let plan = json!({"facts":{},"answers":{},"entities":[],"issues":[], + "features":[{"id":"contact","scope":"Send SMTP email","screens":[],"acceptance":[]} ]}); + let questions = rounds::detail(&plan, &[]).unwrap(); + let question = questions.iter().find(|question| question["id"] == "contact.notifications").unwrap(); + assert!(text(&question["options"][0]["label"]).starts_with("SMTP email")); +} + +#[tokio::test] +async fn restart_archives_plan_screen_and_units_before_creating_new_intake() { + let dir = TestDir::new(); + let (_, policy, _) = drafted(dir.path(), false).await; + std::fs::write(dir.path().join(".devup/screen-plan.json"), "original screen").unwrap(); + std::fs::create_dir_all(dir.path().join(".devup/units")).unwrap(); + std::fs::write(dir.path().join(".devup/units/worker.md"), "original unit").unwrap(); + let mut request = input(dir.path(), "intake"); + request.restart = true; + request.detail = Some("full".into()); + let result = run(&request, &policy).await.unwrap(); + let archive = dir.path().join(text(&result["plan"]["archivedTo"])); + assert!(archive.join("project-plan.json").is_file()); + assert!(archive.join("screen-plan.json").is_file()); + assert!(archive.join("units/worker.md").is_file()); + assert!(!dir.path().join(".devup/screen-plan.json").exists()); + assert_eq!(result["state"], "intake"); +} + +#[tokio::test] +async fn existing_intake_returns_current_plan_and_status_recovery() { + let dir = TestDir::new(); + let (_, policy, draft) = drafted(dir.path(), false).await; + let request = input(dir.path(), "intake"); + let error = run(&request, &policy).await.unwrap_err(); + assert_eq!(error.details["existingPlan"]["planId"], draft["planId"]); + assert_eq!(error.details["existingPlan"]["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"]); +} + +#[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"]}}]}], + "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(); + 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"; + 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(); + 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"]); +} diff --git a/crates/devup-mcp/src/server/project_plan/units.rs b/crates/devup-mcp/src/server/project_plan/units.rs index 3f966e89..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, @@ -9,8 +10,132 @@ 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"]}) } +pub(super) 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"], + json!(feature_screens(plan, feature)) + ) + .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, &[]), + "scopePrecedence": u["scopePrecedence"], + "scopeOverrides": u["scopeOverrides"], + "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, 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}) +} +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| { + !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 @@ -76,27 +201,55 @@ pub(super) fn split( json!(["Shared foundations verified"]), "shared", )); - 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 { - vec!["front"] - }; - for layer in layers { + units[0]["sharedParts"] = json!(shared_parts(plan)); + let mut model_units = std::collections::BTreeSet::new(); + 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 { + 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 layer = if used.is_empty() { "front" } else { "domain" }; + { let uid = format!("{id}-{layer}"); - units.push(unit( + let mut u = unit( uid.clone(), - format!("{}: {layer}", text(&feature["scope"])), - vec![previous], - feature["acceptance"].clone(), + features.iter().map(|feature| text(&feature["scope"])).collect::>().join("; "), + previous, + json!(features.iter().flat_map(|feature| array(&feature["acceptance"]).to_vec()).collect::>()), layer, - )); - previous = uid; + ); + 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); } } } + 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() { @@ -104,17 +257,34 @@ pub(super) fn split( context.remove(key); } } - for u in &units { + 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"]); + let screens = array(&u["screens"]); + let screen_slice = slim_screens(&screen_context, screens); + let endpoints = array(&plan["endpointContracts"]).iter().filter(|contract| { + u["stage"] != 1 || array(&u["features"]).contains(&contract["feature"]) + }).cloned().collect::>(); + 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"] { + u["contract"][field] = drafts[field].clone(); + } let brief = format!( include_str!("../text/units-split-bf50d8a5.md"), text(&u["id"]), 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, - json!({"acceptance":u["acceptance"],"checklist":u["checklist"]}) + slim_context(plan, u), + erd::mermaid(&unit_entities(plan, u)), + screen_slice, + json!({"acceptance":u["acceptance"],"checklist":u["checklist"],"contract":u["contract"]}) ); stage( tx, @@ -127,9 +297,51 @@ 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() + .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/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 +} 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..4bdec444 --- /dev/null +++ b/crates/devup-mcp/src/server/project_plan/wiring.rs @@ -0,0 +1,171 @@ +use super::*; +mod draft; +mod contract; +mod schema; +mod names; +pub(super) use draft::draft; + + + +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(|(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() +} + +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 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"})); + } + } + 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})); + } + } + 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 { + 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"]) + ) + }); + 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 { + 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() { + return vec![]; + } + let base = Path::new(root).join(dir); + let mut found = vec![]; + 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"))) + { + 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/project_plan/wiring/contract.rs b/crates/devup-mcp/src/server/project_plan/wiring/contract.rs new file mode 100644 index 00000000..5f691bda --- /dev/null +++ b/crates/devup-mcp/src/server/project_plan/wiring/contract.rs @@ -0,0 +1,75 @@ +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); + reasons.extend(names::schema_violations(&doc, schema::body_schema(&doc, &operation["requestBody"]), 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; + 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); + 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(reason) = names::violation(name) { reasons.push(reason); } + if let Some((spelling, actual)) = fields.get_key_value(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 &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(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}); + } 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..978b0a83 --- /dev/null +++ b/crates/devup-mcp/src/server/project_plan/wiring/draft.rs @@ -0,0 +1,151 @@ +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"]) + || 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", "정렬"]) { + 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| 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", "필터", "카테고리", "탭"]) + || 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", "카테고리"]) { + queries.push(json!({"name":names::camel_case(name),"column":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 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", "삭제"]), + ] { + 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 { + 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,"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 { + 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":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":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"})); + } + } + } + plan["dataMappings"] = json!(mappings); + plan["endpointContracts"] = json!(contracts); +} 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/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 +} diff --git a/crates/devup-mcp/src/server/projection.rs b/crates/devup-mcp/src/server/projection.rs index f8344b03..4b5d265d 100644 --- a/crates/devup-mcp/src/server/projection.rs +++ b/crates/devup-mcp/src/server/projection.rs @@ -13,6 +13,7 @@ include!("projection/artifact_metadata.rs"); include!("projection/verification_scope.rs"); include!("projection/diagnostics.rs"); include!("projection/component_projection.rs"); +include!("projection/instance_overrides.rs"); include!("projection/completion.rs"); include!("projection/diagnostic_screens.rs"); include!("projection/regression_tests.rs"); diff --git a/crates/devup-mcp/src/server/projection/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..d6122a79 100644 --- a/crates/devup-mcp/src/server/projection/completion.rs +++ b/crates/devup-mcp/src/server/projection/completion.rs @@ -19,24 +19,33 @@ pub(super) async fn complete_operation( artifact_store, ) .await?; + response["artifactId"] = json!(artifact.artifact_id); if let Some(source) = response .get_mut("source") .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. - if let Some(root) = project_root.as_deref() - && response.get("figmaNodeIds").is_some() - { + if let Some(root) = project_root.as_deref() { let root_node = payload.target.node_id.clone().unwrap_or_default(); + let mut record_source = response.clone(); + for (key, code) in default_code_paths::written_sources(&response) { + if let Some(frame_key) = key.strip_prefix("frame:") { + if let Some((id, output)) = frame_key.rsplit_once(':') + && let Some(frames) = record_source["frames"].as_array_mut() { + for frame in frames.iter_mut().filter(|frame| frame["nodeId"] == id) { frame[output] = json!(code); } + } + } else { record_source[&key] = json!(code); } + } match super::export_record::write( output_policy, root, &payload.target.file_key, &root_node, - &response, + &record_source, ) { Ok(path) => response["exportRecord"] = json!(path), Err(error) => response["exportRecordError"] = json!(error.message), @@ -97,10 +106,68 @@ 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; + }; + 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; + 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 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." + )); + } 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 +177,90 @@ 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", "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"}); + 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/component_projection.rs b/crates/devup-mcp/src/server/projection/component_projection.rs index e21aa9c5..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, @@ -58,43 +56,75 @@ 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( + 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/default_code_paths.rs b/crates/devup-mcp/src/server/projection/default_code_paths.rs new file mode 100644 index 00000000..3cca1761 --- /dev/null +++ b/crates/devup-mcp/src/server/projection/default_code_paths.rs @@ -0,0 +1,121 @@ +use super::*; + +pub(crate) fn written_sources(result: &Value) -> Vec<(String, String)> { + result["outputPaths"].as_object().into_iter().flat_map(|paths| paths.iter()) + .filter(|(key, _)| matches!(key.rsplit(':').next(), Some("tsx" | "componentTsx" | "responsiveTsx"))) + .filter_map(|(key, path)| Some((key.clone(), std::fs::read_to_string(path.as_str()?).ok()?))) + .collect() +} + +pub(super) fn apply(result: &mut Map, paths: &mut BTreeMap, root: Option<&str>, policy: &OutputPolicy) { + let Some(root) = root else { return }; + let mut defaults = Map::new(); + for (output, extension) in [("tsx", "tsx"), ("componentTsx", "component.tsx"), ("responsiveTsx", "responsive.tsx"), ("sourceMap", "sourcemap.json")] { + if paths.contains_key(output) { continue; } + let targets: Vec<(String, String)> = if let Some(frames) = result.get("frames").and_then(Value::as_array) { + frames.iter().filter(|frame| frame.get(output).is_some()).filter_map(|frame| { + let id = frame["nodeId"].as_str()?; + Some((format!("frame:{id}:{output}"), id.into())) + }).collect() + } else if result.contains_key(output) { + let id = result.get("_defaultNodeId").and_then(Value::as_str).unwrap_or("export"); + vec![(output.into(), id.into())] + } else { vec![] }; + for (key, id) in targets { + if paths.contains_key(&key) { continue; } + let path = std::path::Path::new(root).join(".devup/exports").join(format!("{}.{extension}", id.replace([':', ';'], "-"))); + let path = path.to_string_lossy().into_owned(); + if policy.resolve(&path).is_ok() { + paths.insert(key.clone(), path.clone()); + defaults.insert(key, json!(path)); + } else if let Some(warnings) = result.entry("warnings").or_insert_with(|| json!([])).as_array_mut() { + warnings.push(json!({"code":"DEVUP_DEFAULT_OUTPUT_PATH_SKIPPED","path":path,"message":"Default output is not allowed; keeping inline delivery."})); + } + } + } + if !defaults.is_empty() { result.insert("defaultOutputPaths".into(), json!(defaults)); } +} + +pub(super) fn omit_written(result: &mut Map) { + let paths = result.get("outputPaths").and_then(Value::as_object).cloned().unwrap_or_default(); + for (key, path) in paths { + if let Some(frame_key) = key.strip_prefix("frame:") { + if let Some((id, output)) = frame_key.rsplit_once(':') + && matches!(output, "tsx" | "componentTsx" | "responsiveTsx") + && let Some(frames) = result.get_mut("frames").and_then(Value::as_array_mut) { + for frame in frames.iter_mut().filter(|frame| frame["nodeId"] == id) { + if let Some(object) = frame.as_object_mut() { object.remove(output); } + frame["outputResults"][output]["bodyDelivery"] = json!("written-to-file"); + frame["outputResults"][output]["path"] = path.clone(); + } + } + } else if matches!(key.as_str(), "tsx" | "componentTsx" | "responsiveTsx") { + result.remove(&key); + let outputs = result.entry("outputResults").or_insert_with(|| json!({})); + outputs[&key]["bodyDelivery"] = json!("written-to-file"); + outputs[&key]["path"] = path; + } + } +} + +pub(super) fn compact_written(result: &mut Map, outputs: &mut Vec, artifact_id: &str) -> Result<(), DevupError> { + if !result.get("outputPaths").and_then(Value::as_object).is_some_and(|paths| paths.keys().any(|key| matches!(key.rsplit(':').next(), Some("tsx" | "componentTsx" | "responsiveTsx")))) { + return Ok(()); + } + let details = ProjectedOutput::text("written-export-details.json", "application/json", serde_json::to_vec(result).map_err(|error| DevupError::new(ErrorCode::DevupInvalidInput, error.to_string(), false))?); + let uri = details.manifest_uri(artifact_id); + let issues = result.get("projectionIssues").cloned().unwrap_or_else(|| json!([])); + let preview = issues.as_array().into_iter().flatten().take(10).map(|issue|json!({"code":issue["code"],"nodeId":issue["nodeId"],"property":issue["property"],"message":issue["message"],"fidelityImpact":issue["fidelityImpact"],"how":issue["details"]["nextAction"].as_str().or_else(|| issue["details"]["nextAction"]["how"].as_str())})).collect::>(); + result.retain(|key, _| !matches!(key.as_str(), "sectionChecklist" | "imageFillAudit" | "placementContracts" | "projectionReview" | "verdictScope" | "widthBehavior" | "fidelity" | "completionEvidence")); + result.insert("projectionIssues".into(), json!(preview)); + result.insert("metadataDelivery".into(), json!({"mode":"resource-details","uri":uri,"name":"written-export-details.json","requiresFullDetailRead":true,"projectionIssueCount":issues.as_array().map_or(0, Vec::len)})); + outputs.push(details); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn defaults_fill_each_frame_without_replacing_an_explicit_path() -> anyhow::Result<()> { + let root = std::env::temp_dir(); + let policy = OutputPolicy::from_roots(vec![root.clone()])?; + let mut result = json!({"frames":[ + {"nodeId":"1:2","tsx":"first","responsiveTsx":"responsive"}, + {"nodeId":"1:3;4","tsx":"second"} + ]}).as_object().unwrap().clone(); + let mut paths = BTreeMap::from([("frame:1:2:tsx".into(), "explicit.tsx".into())]); + apply(&mut result, &mut paths, root.to_str(), &policy); + assert_eq!(paths["frame:1:2:tsx"], "explicit.tsx"); + assert!(paths["frame:1:3;4:tsx"].ends_with("1-3-4.tsx")); + assert!(paths["frame:1:2:responsiveTsx"].ends_with("1-2.responsive.tsx")); + assert_eq!(result["defaultOutputPaths"].as_object().unwrap().len(), 2); + Ok(()) + } + + #[test] + fn default_extensions_match_the_output_format() -> anyhow::Result<()> { + let root = std::env::temp_dir(); + let policy = OutputPolicy::from_roots(vec![root.clone()])?; + let mut result = json!({"_defaultNodeId":"51:6723","componentTsx":"component","sourceMap":{}}).as_object().unwrap().clone(); + let mut paths = BTreeMap::new(); + apply(&mut result, &mut paths, root.to_str(), &policy); + assert!(paths["componentTsx"].ends_with("51-6723.component.tsx")); + assert!(paths["sourceMap"].ends_with("51-6723.sourcemap.json")); + Ok(()) + } + + #[test] + fn disallowed_defaults_keep_inline_code() -> anyhow::Result<()> { + let root = std::env::temp_dir(); + let policy = OutputPolicy::from_roots(vec![root.join(".")])?; + let mut result = json!({"tsx":"code","_defaultNodeId":"1:2"}).as_object().unwrap().clone(); + let mut paths = BTreeMap::new(); + apply(&mut result, &mut paths, root.parent().and_then(std::path::Path::to_str), &policy); + assert!(paths.is_empty()); + assert_eq!(result["tsx"], "code"); + assert_eq!(result["warnings"][0]["code"], "DEVUP_DEFAULT_OUTPUT_PATH_SKIPPED"); + Ok(()) + } +} diff --git a/crates/devup-mcp/src/server/projection/feedback_codegen_tests.rs b/crates/devup-mcp/src/server/projection/feedback_codegen_tests.rs new file mode 100644 index 00000000..615381e0 --- /dev/null +++ b/crates/devup-mcp/src/server/projection/feedback_codegen_tests.rs @@ -0,0 +1,339 @@ +#[test] +fn responsive_merge_diagnostics_survive_export_summary_without_opt_in() { + let data = responsive_payload(); + let outputs = vec!["responsiveTsx".into()]; + let codes = [ + ("DEVUP_CODEGEN_RESPONSIVE_UNMATCHED", devup_mcp_figma::FidelityImpact::Approximated), + ("DEVUP_CODEGEN_RESPONSIVE_DRAWN_WIDTH_EMPTY", devup_mcp_figma::FidelityImpact::Lossy), + ]; + let diagnostics = codes.iter().map(|(code, impact)| Diagnostic { + code: (*code).into(), + message: "Responsive merge requires review".into(), + node_id: Some("1:1".into()), + property: Some("w".into()), + details: Some(json!({"output":"responsiveTsx","slot":0,"width":390, + "originalValue":"390px","nextAction":"Inspect responsive merge provenance."})), + severity: Some(DiagnosticSeverity::Warning), + fidelity_impact: Some(*impact), + ..Diagnostic::default() + }).collect(); + let (result,) = summarize_projection_diagnostics::summarize_projection_diagnostics( + summarize_projection_diagnostics::ProjectionDiagnostics { + payload: &data, + outputs: &outputs, + include_diagnostics: false, + root_layout: devup_mcp_devup_ui::codegen::RootLayout::Standalone, + asset_names_per_node: true, + scope: &"node".into(), + strict: false, + frame_ids: &Vec::new(), + all_screens: false, + asset_captures: &Vec::new(), + delivery: DeliveryMode::Inline, + result: json!({"responsiveTsx":"export default function Page() { return null }"}) + .as_object().unwrap().clone(), + failures: Vec::new(), + warnings: Vec::new(), + completeness_report: &data.completeness_report(), + section_tsx_projected: false, + projection_diagnostics: diagnostics, + fidelity_reports: Vec::new(), + theme_conflict_count: 0, + theme_unresolved_count: 0, + component_name_for_components: &None, + target_kind: TargetKind::Screen, + }, + ).unwrap(); + let mut result = Value::Object(result); + super::super::review::attach(&mut result); + for (code, impact) in codes { + let issue = result["projectionIssues"].as_array().unwrap().iter() + .find(|issue| issue["code"] == code).unwrap(); + assert_eq!(issue["details"]["output"], "responsiveTsx"); + assert_eq!(issue["fidelityImpact"], json!(impact)); + let review = result["reviewChecklist"].as_array().unwrap().iter() + .find(|item| item["code"] == code).unwrap(); + assert_eq!(review["nodeIds"], json!(["1:1"])); + assert_eq!(review["severity"], if impact == devup_mcp_figma::FidelityImpact::Lossy { + "required" + } else { + "recommended" + }); + } +} + +#[tokio::test] +async fn instance_overrides_reach_export_and_required_review_without_opt_in() { + for output in ["tsx", "componentTsx"] { + let mut data = payload(); + let root = data.snapshot.roots[0].clone(); + let mut master = data.snapshot.nodes[&root].clone(); + master.id = "override-master".into(); + master.node_type = "COMPONENT".into(); + data.snapshot.nodes.insert(master.id.clone(), master); + let instance = data.snapshot.nodes.get_mut(&root).unwrap(); + instance.node_type = "INSTANCE".into(); + instance.fields.insert("mainComponentId".into(), json!("override-master")); + instance.fields.insert("overrides".into(), json!([{"id":root,"overriddenFields":["fills"]}])); + instance.fields.insert("fills".into(), json!([{"type":"SOLID","color":{"r":0,"g":0.5,"b":0.5}}])); + let mut result = project(data, operation(&[output])).await.unwrap(); + super::super::review::attach(&mut result); + assert_eq!(result["instanceOverrides"][0]["instanceNodeId"], root, "{result}"); + assert_eq!(result["instanceOverrides"][0]["resolution"], "resolved"); + assert!(result[output].as_str().unwrap().contains("// @devup-override ")); + assert!(result["projectionIssues"].as_array().unwrap().iter().any(|item| + item["code"] == "DEVUP_CODEGEN_INSTANCE_OVERRIDE")); + assert!(result["reviewChecklist"].as_array().unwrap().iter().any(|item| + item["code"] == "DEVUP_CODEGEN_INSTANCE_OVERRIDE" && item["severity"] == "required")); + } +} + +#[tokio::test] +async fn responsive_unmatched_diagnostic_reaches_export_review_without_opt_in() { + let mut data = responsive_payload(); + for (root, children) in [ + ("1:1", vec!["small"]), + ("1:2", vec!["wide-a", "wide-b"]), + ] { + data.snapshot.nodes.get_mut(root).unwrap().fields.insert("childrenIds".into(), json!(children)); + for child in children { + data.snapshot.nodes.insert(child.into(), serde_json::from_value(json!({ + "id":child,"type":"FRAME","fields":{ + "name":"Logo","parentId":root,"width":100,"height":24 + } + })).unwrap()); + } + } + let mut op = operation(&["responsiveTsx"]); + if let PendingOperation::Export { frame_ids, include_diagnostics, .. } = &mut op { + *frame_ids = vec!["1:1".into(), "1:2".into()]; + *include_diagnostics = false; + } + let mut result = project(data, op).await.unwrap(); + super::super::review::attach(&mut result); + assert!(result["responsiveTsx"].is_string(), "{result}"); + let code = "DEVUP_CODEGEN_RESPONSIVE_UNMATCHED"; + for items in [ + &result["projectionIssues"], + &result["responsiveReviewChecklist"]["items"], + &result["reviewChecklist"], + ] { + assert!(items.as_array().unwrap().iter().any(|item| item["code"] == code), "{items}"); + } +} + +#[tokio::test] +async fn suspicious_design_values_are_published_for_recommended_review_without_loss() { + let mut data = payload(); + let root = data.snapshot.roots[0].clone(); + let fields = &mut data.snapshot.nodes.get_mut(&root).unwrap().fields; + fields.insert("width".into(), json!(171)); + fields.insert("height".into(), json!(80)); + fields.insert("layoutSizingHorizontal".into(), json!("FIXED")); + fields.insert("layoutSizingVertical".into(), json!("FIXED")); + fields.remove("parentType"); + fields.remove("childrenIds"); + let mut result = project(data, operation(&["tsx"])).await.unwrap(); + super::super::review::attach(&mut result); + let issue = result["projectionIssues"].as_array().unwrap().iter() + .find(|issue| issue["code"] == "DEVUP_CODEGEN_SUSPICIOUS_VALUE").expect("published advisory"); + assert_eq!(issue["fidelityImpact"], "none"); + assert_eq!(issue["details"]["appliedValue"], "171px"); + assert!(result["tsx"].as_str().unwrap().contains("w=\"171px\"")); + assert_ne!(result["quality"]["projection"], "lossy"); + assert!(result["reviewChecklist"].as_array().unwrap().iter().any(|item| + item["code"] == "DEVUP_CODEGEN_SUSPICIOUS_VALUE" && item["severity"] == "recommended")); +} + +#[tokio::test] +async fn stale_plugin_warning_is_visible_without_optional_diagnostics() { + let mut data = payload(); + data.snapshot.diagnostics.push(serde_json::from_value(json!({ + "code":"DEVUP_BRIDGE_PLUGIN_STALE", "message":"Plugin scripts are stale.", + "severity":"warning", "fidelityImpact":"none", + "details":{"pluginStale":true,"expected":"current","actual":"old"} + })).unwrap()); + let result = project(data, operation(&["tsx"])).await.unwrap(); + let issues = result["projectionIssues"].as_array().unwrap(); + let warning = issues.iter().find(|issue| issue["code"] == "DEVUP_BRIDGE_PLUGIN_STALE").unwrap(); + assert_eq!(warning["fidelityImpact"], "none"); + assert_eq!(warning["details"]["pluginStale"], true); +} + +#[tokio::test] +async fn feedback36_confirmed_pair_merges_despite_different_names() { + let directory = std::env::temp_dir().join(format!("feedback36-{}", rand::random::())); + 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!(!std::fs::read_to_string(result["outputPaths"]["responsiveTsx"].as_str().unwrap()).unwrap().is_empty()); + assert!(result.get("responsiveTsx").is_none()); + assert_eq!(result["responsiveSlots"], json!([0,4])); + std::fs::remove_dir_all(directory).unwrap(); +} + +#[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(); +} + +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 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 = std::fs::read_to_string(&path).unwrap(); + assert!(code.contains("export function Button"), "{root}: {result}"); + assert!(result.get("componentTsx").is_none()); + 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(); + let code = result["tsx"].as_str().unwrap(); + assert_eq!(code.matches("export function Button").count(), 1); + assert!(!code.contains("900px"), "{code}"); + assert!(!code.contains("600px"), "{code}"); + assert!(code.contains("_hover="), "{code}"); + assert!(!code.contains("effect:"), "{code}"); + assert!(!code.contains("dashed"), "{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 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(); + 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"]); +} + +#[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("