From a9b96f24df759eeca9de20538c2b87bc7780cae6 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Sat, 12 Sep 2026 23:37:36 +0900 Subject: [PATCH 1/2] fix: preserve integer text advances and inside stroke geometry --- .../src/codegen/component.rs | 1 + .../devup-mcp-devup-ui/src/codegen/layout.rs | 1 + .../devup-mcp-devup-ui/src/codegen/style.rs | 18 +- crates/devup-mcp-devup-ui/src/codegen/text.rs | 96 +++++- crates/devup-mcp-devup-ui/src/provenance.rs | 39 ++- .../src/theme/devup_json.rs | 26 +- crates/devup-mcp-devup-ui/tests/codegen.rs | 6 +- .../tests/integer_line_advance.rs | 137 ++++++++ crates/devup-mcp-devup-ui/tests/provenance.rs | 5 +- ...wquw_151__wquw_151_proofread_devup_ui.snap | 199 +++++++++-- ..._wquw_151_proofread_devup_ui_embedded.snap | 199 +++++++++-- ...w_151__wquw_151_proofread_diagnostics.snap | 7 +- ...uw_151__wquw_151_proofread_source_map.snap | 325 +++++++++++++++++- ...151_frames__wquw_151_frame_3879_35503.snap | 12 +- ...151_frames__wquw_151_frame_3879_35518.snap | 152 +++++++- ...151_frames__wquw_151_frame_3879_35569.snap | 283 +++++++++++++-- ...151_frames__wquw_151_frame_3879_35652.snap | 235 +++++++++++-- ...151_frames__wquw_151_frame_3879_35729.snap | 219 ++++++++++-- ...151_frames__wquw_151_frame_3879_35887.snap | 284 +++++++++++++-- ...151_frames__wquw_151_frame_3879_35973.snap | 284 +++++++++++++-- ...151_frames__wquw_151_frame_3879_36059.snap | 100 +++++- ...151_frames__wquw_151_frame_3879_36108.snap | 53 ++- ...151_frames__wquw_151_frame_3879_36144.snap | 136 +++++++- crates/devup-mcp-devup-ui/tests/theme.rs | 4 +- .../tests/unresolved_token_binding.rs | 4 +- crates/devup-mcp/src/server/projection.rs | 4 +- docs/line-box-displacement.md | 191 ++++++++++ fixtures/devup-figma-plugin/manifest.json | 48 +-- .../upstream-codegen-109-e2824ad5be.snap | 8 +- .../upstream-codegen-161-5b230821f2.snap | 2 +- .../upstream-codegen-162-51e1254b4c.snap | 2 +- .../upstream-codegen-163-2a9f9a056a.snap | 2 +- .../upstream-codegen-164-33e4c6591c.snap | 2 +- .../upstream-codegen-165-501ee1df12.snap | 4 +- .../upstream-codegen-166-cda14f0b08.snap | 6 +- .../upstream-codegen-184-0c09788bb7.snap | 2 +- .../upstream-codegen-185-e79c2ae189.snap | 2 +- .../upstream-codegen-186-0988e78887.snap | 2 +- .../upstream-codegen-191-2021d398f8.snap | 8 +- .../upstream-codegen-213-aa53408d2f.snap | 8 +- .../upstream-codegen-222-f2d780c847.snap | 2 +- .../upstream-codegen-223-4dc8092992.snap | 16 +- .../upstream-codegen-243-14366a47ff.snap | 2 +- .../upstream-codegen-245-537356bcf4.snap | 4 +- .../upstream-codegen-246-295f39e09b.snap | 2 +- .../upstream-codegen-247-39d6959685.snap | 2 +- .../upstream-codegen-248-d1bc18e068.snap | 2 +- .../upstream-codegen-249-6b8a42981d.snap | 2 +- .../upstream-codegen-251-50acddfb96.snap | 8 +- .../upstream-codegen-252-49e979239e.snap | 12 +- .../upstream-codegen-253-347a2e5fb2.snap | 8 +- .../upstream-codegen-254-9f340b6a10.snap | 2 +- harness/render/thresholds.json | 6 +- 53 files changed, 2796 insertions(+), 388 deletions(-) create mode 100644 crates/devup-mcp-devup-ui/tests/integer_line_advance.rs create mode 100644 docs/line-box-displacement.md diff --git a/crates/devup-mcp-devup-ui/src/codegen/component.rs b/crates/devup-mcp-devup-ui/src/codegen/component.rs index dccc60ee..4a104a85 100644 --- a/crates/devup-mcp-devup-ui/src/codegen/component.rs +++ b/crates/devup-mcp-devup-ui/src/codegen/component.rs @@ -1622,6 +1622,7 @@ fn render_node( asset_names_per_node: context.asset_names_per_node, }, ); + text::validate_line_metrics(&view, &context.text_style_tokens)?; text::push_text_props( &view, &context.text_style_tokens, diff --git a/crates/devup-mcp-devup-ui/src/codegen/layout.rs b/crates/devup-mcp-devup-ui/src/codegen/layout.rs index 27e8e110..344aeb33 100644 --- a/crates/devup-mcp-devup-ui/src/codegen/layout.rs +++ b/crates/devup-mcp-devup-ui/src/codegen/layout.rs @@ -1304,6 +1304,7 @@ fn push_padding(snapshot: &Snapshot, node: &RawNode, props: &mut Vec) { // the padding puts the content where Figma has it on every axis: it // starts `p` in and, hugging, the box is content plus `2p`. if view.string("strokeAlign").unwrap_or("INSIDE") == "INSIDE" + && !super::style::inside_stroke_uses_outline(&view) && view.node_type() != "LINE" && let Some(weight) = view.number("strokeWeight").filter(|weight| *weight > 0.0) && view diff --git a/crates/devup-mcp-devup-ui/src/codegen/style.rs b/crates/devup-mcp-devup-ui/src/codegen/style.rs index 105f28f9..29c4429d 100644 --- a/crates/devup-mcp-devup-ui/src/codegen/style.rs +++ b/crates/devup-mcp-devup-ui/src/codegen/style.rs @@ -1454,6 +1454,19 @@ fn push_radius(view: &TypedNode<'_>, props: &mut Vec) { } } +/// An inside stroke cannot consume layout space when the padding is too small +/// to absorb a CSS border. Paint it inward with an outline instead. +pub(super) fn inside_stroke_uses_outline(view: &TypedNode<'_>) -> bool { + view.string("strokeAlign").unwrap_or("INSIDE") == "INSIDE" + && matches!(view.string("layoutMode"), Some("HORIZONTAL" | "VERTICAL")) + && view.number("strokeWeight").is_some_and(|weight| { + weight > 0.0 + && ["paddingTop", "paddingRight", "paddingBottom", "paddingLeft"] + .iter() + .any(|field| view.number(field).is_some_and(|padding| padding < weight)) + }) +} + fn push_strokes( view: &TypedNode<'_>, props: &mut Vec, @@ -1535,7 +1548,10 @@ fn push_strokes( } return; } - if align == "INSIDE" { + if inside_stroke_uses_outline(view) { + string_prop(props, "outline", format!("{style} {} {color}", px(weight))); + string_prop(props, "outlineOffset", px(-weight)); + } else if align == "INSIDE" { string_prop(props, "border", format!("{style} {} {color}", px(weight))); } else { string_prop(props, "outline", format!("{style} {} {color}", px(weight))); diff --git a/crates/devup-mcp-devup-ui/src/codegen/text.rs b/crates/devup-mcp-devup-ui/src/codegen/text.rs index f4de7a9f..ae9830a6 100644 --- a/crates/devup-mcp-devup-ui/src/codegen/text.rs +++ b/crates/devup-mcp-devup-ui/src/codegen/text.rs @@ -53,9 +53,7 @@ pub(super) fn push_text_props( { string_prop(props, "fontStyle", "italic"); } - if typography.is_none() - && let Some(font_size) = value("fontSize").and_then(Value::as_f64) - { + if let Some(font_size) = value("fontSize").and_then(Value::as_f64) { string_prop(props, "fontSize", px(font_size)); } if typography.is_none() @@ -68,9 +66,10 @@ pub(super) fn push_text_props( { string_prop(props, "letterSpacing", letter_spacing); } - if typography.is_none() - && let Some(line_height) = line_height(value("lineHeight")) - { + if let Some(line_height) = line_height( + value("lineHeight"), + value("fontSize").and_then(Value::as_f64), + ) { string_prop(props, "lineHeight", line_height); } if typography.is_none() { @@ -203,7 +202,7 @@ fn letter_spacing(value: Option<&Value>) -> Option { } } -fn line_height(value: Option<&Value>) -> Option { +fn line_height(value: Option<&Value>, font_size: Option) -> Option { let value = value?; if let Some(number) = value.as_f64() { return Some(px(number)); @@ -214,7 +213,8 @@ fn line_height(value: Option<&Value>) -> Option { Some("PERCENT") => object .get("value") .and_then(Value::as_f64) - .map(|number| format_number((number / 10.0).round() / 10.0)), + .zip(font_size) + .map(|(number, size)| px((size * number / 100.0).round())), _ => object.get("value").and_then(Value::as_f64).map(px), } } @@ -381,9 +381,7 @@ fn typography_props( { string_prop(&mut props, "fontStyle", "italic"); } - if typography.is_none() - && let Some(value) = segment.get("fontSize").and_then(Value::as_f64) - { + if let Some(value) = segment.get("fontSize").and_then(Value::as_f64) { string_prop(&mut props, "fontSize", px(value)); } if typography.is_none() @@ -396,9 +394,10 @@ fn typography_props( { string_prop(&mut props, "letterSpacing", value); } - if typography.is_none() - && let Some(value) = line_height(segment.get("lineHeight")) - { + if let Some(value) = line_height( + segment.get("lineHeight"), + segment.get("fontSize").and_then(Value::as_f64), + ) { string_prop(&mut props, "lineHeight", value); } if typography.is_none() { @@ -422,6 +421,75 @@ fn record_used_color(color: &str, used_tokens: &mut BTreeSet) { } } +/// Token names carry no metrics. Resolved overrides must travel as a pair; +/// otherwise a pixel advance from the token may belong to another font size. +pub(super) fn validate_line_metrics( + view: &TypedNode<'_>, + tokens: &BTreeMap, +) -> Result<(), devup_mcp_figma::DevupError> { + if view.node_type() != "TEXT" { + return Ok(()); + } + let validate = |size: Option<&Value>, + height: Option<&Value>, + bound: Option<&Value>, + styled: bool| { + let percent = height.and_then(|h| h.get("unit")).and_then(Value::as_str) == Some("PERCENT"); + let invalid = (percent + && (size.and_then(Value::as_f64).is_none() + || bound.and_then(|v| v.get("fontSize")).is_some())) + || (styled + && size.is_some() + && line_height(height, size.and_then(Value::as_f64)).is_none()); + if invalid { + Err(devup_mcp_figma::DevupError::new( + devup_mcp_figma::ErrorCode::DevupCodegenFailed, + format!( + "Text node '{}' cannot represent a size-dependent line advance without resolved fontSize and lineHeight; variable sizes require mode-aware metrics.", + view.id() + ), + false, + )) + } else { + Ok(()) + } + }; + let segment = default_segment(view); + let value = |field: &str| { + view.value(field) + .filter(|v| is_resolved_value(v)) + .or_else(|| segment.and_then(|s| s.get(field))) + }; + let styled = segment + .and_then(|s| s.get("textStyleId")) + .and_then(Value::as_str) + .is_some_and(|id| tokens.contains_key(id)); + validate( + value("fontSize"), + value("lineHeight"), + value("boundVariables"), + styled, + )?; + for segment in view + .value("styledTextSegments") + .and_then(Value::as_array) + .into_iter() + .flatten() + { + let styled = segment + .get("textStyleId") + .and_then(Value::as_str) + .is_some_and(|id| tokens.contains_key(id)); + validate( + segment.get("fontSize"), + segment.get("lineHeight"), + segment.get("boundVariables"), + styled, + )?; + } + Ok(()) +} + fn bound_segment_color( fills: Option<&Value>, variable_tokens: &BTreeMap, diff --git a/crates/devup-mcp-devup-ui/src/provenance.rs b/crates/devup-mcp-devup-ui/src/provenance.rs index 494449a2..3d50b79a 100644 --- a/crates/devup-mcp-devup-ui/src/provenance.rs +++ b/crates/devup-mcp-devup-ui/src/provenance.rs @@ -1952,6 +1952,8 @@ const PROP_SOURCES: &[(&str, &str)] = &[ ("minH", "minHeight"), ("minW", "minWidth"), ("opacity", "opacity"), + ("outline", "strokes"), + ("outlineOffset", "strokeWeight"), ("overflow", "clipsContent"), ("overflow", "overflowDirection"), ("overflowX", "overflowDirection"), @@ -1998,19 +2000,50 @@ fn add_text_entries( segment .get("characters") .and_then(serde_json::Value::as_str) + .map(|characters| (characters, Some(segment))) }) - .filter(|characters| !characters.is_empty()) + .filter(|(characters, _)| !characters.is_empty()) .collect::>(); if text_segments.is_empty() && let Some(characters) = view .string("characters") .filter(|characters| !characters.is_empty()) { - text_segments.push(characters); + text_segments.push((characters, None)); } let mut cursor = 0; - for characters in text_segments { + for (characters, segment) in text_segments { if let Some((start, end)) = find_text_span(source, characters, cursor) { + // A rich-text wrapper can now carry its own integer advance. + // Attribute ownership belongs to the segment whose text directly + // follows this opening, rather than the node's default metrics. + if let Some(segment) = segment + && let Some(open_start) = source[..start].rfind("') + && source[open_start + close + 1..start].trim().is_empty() + { + let opening = &source[open_start..open_start + close]; + for field in ["fontSize", "lineHeight"] { + if segment.get(field).is_none() { + continue; + } + let needle = format!("{field}=\""); + if let Some(attr_start) = find_prop(opening, &needle) + && let Some(attr_end) = opening[attr_start + needle.len()..].find('"') + { + entries.push(generated_entry( + range.start + open_start + attr_start, + range.start + open_start + attr_start + needle.len() + attr_end + 1, + node_id, + "styledTextSegments", + None, + None, + "raw-fallback", + )); + } + } + } entries.push(generated_entry( range.start + start, range.start + end, 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 1b448d42..6ddf9e9b 100644 --- a/crates/devup-mcp-devup-ui/src/theme/devup_json.rs +++ b/crates/devup-mcp-devup-ui/src/theme/devup_json.rs @@ -423,6 +423,27 @@ pub fn generate_devup_json( let level = level.min(5); match style.style_type.as_str() { "TEXT" => { + if style + .value + .pointer("/lineHeight/unit") + .and_then(Value::as_str) + == Some("PERCENT") + && (style + .value + .get("fontSize") + .and_then(Value::as_f64) + .is_none() + || style.value.pointer("/boundVariables/fontSize").is_some()) + { + return Err(DevupError::new( + ErrorCode::DevupThemeConflict, + format!( + "Text style '{}' needs a fixed numeric font size for an integer percentage line advance; variable or missing sizes are not representable.", + style.id + ), + false, + )); + } let slots = typography_slots.entry(token.clone()).or_default(); // The first style seen for a slot keeps it, as in the plugin. if slots[level].is_none() { @@ -671,7 +692,10 @@ fn typography_value(style: &Value, variable_names: &BTreeMap<&str, String>) -> V 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::from((percent / 10.0).round() / 10.0)), + (Some("PERCENT"), Some(percent)) => style + .get("fontSize") + .and_then(Value::as_f64) + .map(|size| Value::String(format_px((size * percent / 100.0).round()))), (Some(_), Some(pixels)) => Some(Value::String(format_px(pixels))), _ => None, }; diff --git a/crates/devup-mcp-devup-ui/tests/codegen.rs b/crates/devup-mcp-devup-ui/tests/codegen.rs index 3b2a51df..dc7406a3 100644 --- a/crates/devup-mcp-devup-ui/tests/codegen.rs +++ b/crates/devup-mcp-devup-ui/tests/codegen.rs @@ -195,6 +195,7 @@ fn nested_text_style_uses_typography() { "start": 0, "end": 3, "textStyleId": "S:body", + "lineHeight": {"unit": "AUTO"}, "fontName": {"family": "Pretendard", "style": "Regular"}, "fontSize": 16, "fontWeight": 400, @@ -209,6 +210,7 @@ fn nested_text_style_uses_typography() { "start": 3, "end": 10, "textStyleId": "S:bodySemibold", + "lineHeight": {"unit": "AUTO"}, "fontName": {"family": "Pretendard", "style": "SemiBold"}, "fontSize": 16, "fontWeight": 600, @@ -223,6 +225,7 @@ fn nested_text_style_uses_typography() { "start": 10, "end": 20, "textStyleId": "S:body", + "lineHeight": {"unit": "AUTO"}, "fontName": {"family": "Pretendard", "style": "Regular"}, "fontSize": 16, "fontWeight": 400, @@ -269,7 +272,8 @@ fn nested_text_style_uses_typography() { assert!(output.used_tokens.contains("text")); assert!(output.used_tokens.contains("primaryLight")); assert!(output.tsx.contains("{\" \"}왔어?
다음 줄")); - assert!(!output.tsx.contains("fontSize=\"16px\"")); + assert!(output.tsx.contains("fontSize=\"16px\"")); + assert!(output.tsx.contains("lineHeight=\"normal\"")); assert!(!output.tsx.contains("fontWeight=\"600\"")); } diff --git a/crates/devup-mcp-devup-ui/tests/integer_line_advance.rs b/crates/devup-mcp-devup-ui/tests/integer_line_advance.rs new file mode 100644 index 00000000..4349257b --- /dev/null +++ b/crates/devup-mcp-devup-ui/tests/integer_line_advance.rs @@ -0,0 +1,137 @@ +use devup_mcp_devup_ui::{ + codegen::{CodegenOptions, generate_component}, + theme::{ThemeScope, VariableSnapshot, generate_devup_json}, +}; +use devup_mcp_figma::Snapshot; +use serde_json::{Value, json}; + +fn styled_snapshot(segment: Value) -> Snapshot { + serde_json::from_value(json!({ + "fileKey":"test", "version":"1", "roots":["t"], "diagnostics":[], + "nodes":{"t":{"id":"t","type":"TEXT","fields":{ + "characters":"Heading", "styledTextSegments":[segment] + },"extra":{},"fieldErrors":{}}} + })) + .unwrap() +} + +#[test] +fn reused_token_gets_resolved_size_and_advance_together() { + let options = CodegenOptions { + text_style_tokens: [("s".into(), "heading".into())].into(), + ..CodegenOptions::default() + }; + let snapshot = styled_snapshot(json!({"characters":"Heading", "textStyleId":"s", + "fontSize":52,"lineHeight":{"unit":"PERCENT","value":130}})); + let output = generate_component(&snapshot, "t", &options).unwrap(); + assert!(output.tsx.contains("fontSize=\"52px\""), "{}", output.tsx); + assert!(output.tsx.contains("lineHeight=\"68px\""), "{}", output.tsx); +} + +#[test] +fn rich_text_advance_has_segment_provenance() { + let mut snapshot = styled_snapshot(json!({"characters":"Long default heading", "fontSize":38, + "lineHeight":{"unit":"PERCENT","value":130}})); + snapshot.nodes.get_mut("t").unwrap().fields.insert("styledTextSegments".into(), json!([ + {"characters":"Long default heading", "fontSize":38,"lineHeight":{"unit":"PERCENT","value":130}}, + {"characters":"small", "fontSize":18,"lineHeight":{"unit":"PERCENT","value":130}} + ])); + let output = generate_component(&snapshot, "t", &CodegenOptions::default()).unwrap(); + assert!(output.tsx.contains("lineHeight=\"23px\""), "{}", output.tsx); + assert!( + output + .source_map + .entries + .iter() + .any( + |entry| entry.generated_property.as_deref() == Some("lineHeight=\"23px\"") + && entry.property.as_deref() == Some("styledTextSegments") + ) + ); +} + +#[test] +fn token_size_override_without_line_metrics_is_refused() { + let options = CodegenOptions { + text_style_tokens: [("s".into(), "heading".into())].into(), + ..CodegenOptions::default() + }; + let snapshot = + styled_snapshot(json!({"characters":"Heading", "textStyleId":"s", "fontSize":52})); + assert!(generate_component(&snapshot, "t", &options).is_err()); +} + +#[test] +fn variable_font_size_cannot_publish_a_fixed_percentage_advance() { + let snapshot: VariableSnapshot = serde_json::from_value(json!({ + "styles":[{"id":"s","name":"heading","styleType":"TEXT","value":{ + "fontSize":52,"lineHeight":{"unit":"PERCENT","value":130}, + "boundVariables":{"fontSize":{"type":"VARIABLE_ALIAS","id":"size"}} + }}] + })) + .unwrap(); + assert!(generate_devup_json(&snapshot, ThemeScope::File).is_err()); +} + +#[test] +fn percentage_style_uses_its_own_integer_advance() { + for (size, advance) in [(38, "49px"), (18, "23px"), (52, "68px"), (36, "47px")] { + let snapshot: VariableSnapshot = serde_json::from_value(json!({ + "styles": [{"id":"s", "name":"heading", "styleType":"TEXT", "value": { + "fontSize":size, "lineHeight":{"unit":"PERCENT","value":129.99999523162842} + }}] + })) + .unwrap(); + let output = generate_devup_json(&snapshot, ThemeScope::File).unwrap(); + let theme: Value = serde_json::from_str(&output.json).unwrap(); + assert_eq!( + theme["theme"]["typography"]["heading"]["lineHeight"], advance, + "size {size}" + ); + } +} + +#[test] +fn inline_text_uses_integer_advance() { + let snapshot: Snapshot = serde_json::from_value(json!({ + "fileKey":"test", "version":"1", "roots":["t"], "diagnostics":[], + "nodes":{"t":{"id":"t","type":"TEXT","fields":{ + "characters":"Heading", "fontSize":52, + "lineHeight":{"unit":"PERCENT","value":129.99999523162842} + },"extra":{},"fieldErrors":{}}} + })) + .unwrap(); + let output = generate_component(&snapshot, "t", &CodegenOptions::default()).unwrap(); + assert!(output.tsx.contains("lineHeight=\"68px\""), "{}", output.tsx); +} + +#[test] +fn inside_stroke_without_padding_does_not_expand_hugged_content() { + let snapshot: Snapshot = serde_json::from_value(json!({ + "fileKey":"test", "version":"1", "roots":["f"], "diagnostics":[], + "nodes":{"f":{"id":"f","type":"FRAME","fields":{ + "childrenIds":["c"], "layoutMode":"HORIZONTAL", + "layoutSizingHorizontal":"HUG", "layoutSizingVertical":"HUG", + "paddingTop":0,"paddingBottom":0,"paddingLeft":0,"paddingRight":0, + "strokeAlign":"INSIDE","strokeWeight":1, + "strokes":[{"type":"SOLID","color":{"r":0,"g":0,"b":0}}] + },"extra":{},"fieldErrors":{}}, + "c":{"id":"c","type":"FRAME","fields":{ + "parentId":"f","width":100,"height":44, + "layoutSizingHorizontal":"FIXED","layoutSizingVertical":"FIXED" + },"extra":{},"fieldErrors":{}}} + })) + .unwrap(); + let output = generate_component(&snapshot, "f", &CodegenOptions::default()).unwrap(); + assert!( + output.tsx.contains("outline=\"solid 1px #000\""), + "{}", + output.tsx + ); + assert!( + output.tsx.contains("outlineOffset=\"-1px\""), + "{}", + output.tsx + ); + assert!(!output.tsx.contains("border="), "{}", output.tsx); +} diff --git a/crates/devup-mcp-devup-ui/tests/provenance.rs b/crates/devup-mcp-devup-ui/tests/provenance.rs index e15e3620..4383150c 100644 --- a/crates/devup-mcp-devup-ui/tests/provenance.rs +++ b/crates/devup-mcp-devup-ui/tests/provenance.rs @@ -53,6 +53,7 @@ fn fidelity_snapshot(characters: &str) -> Snapshot { "characters": characters, "start": 0, "end": characters.len(), "textStyleId": "s", "fontName": {"family": "Inter", "style": "Regular"}, "fontSize": 16, "fontWeight": 400, + "lineHeight": {"unit": "AUTO"}, "fills": [{"type": "SOLID", "color": {"r": 0, "g": 0, "b": 0}}] }], "fills": [{"type": "SOLID", "color": {"r": 0, "g": 0, "b": 0}}] @@ -277,13 +278,13 @@ fn strict_fidelity_consumes_repeated_text_segment_mappings_one_to_one() { { "characters": "A", "start": 0, "end": 1, "textStyleId": "s", "fontName": {"family": "Inter", "style": "Regular"}, - "fontSize": 16, "fontWeight": 400, + "fontSize": 16, "fontWeight": 400, "lineHeight": {"unit":"AUTO"}, "fills": [{"type": "SOLID", "color": {"r": 0, "g": 0, "b": 0}}] }, { "characters": "A", "start": 1, "end": 2, "textStyleId": "s", "fontName": {"family": "Inter", "style": "Regular"}, - "fontSize": 16, "fontWeight": 400, + "fontSize": 16, "fontWeight": 400, "lineHeight": {"unit":"AUTO"}, "fills": [{"type": "SOLID", "color": {"r": 0, "g": 0, "b": 0}}] } ]), diff --git a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151__wquw_151_proofread_devup_ui.snap b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151__wquw_151_proofread_devup_ui.snap index a4da6b85..5532a08c 100644 --- a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151__wquw_151_proofread_devup_ui.snap +++ b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151__wquw_151_proofread_devup_ui.snap @@ -23,6 +23,8 @@ export function AStoryFProofread() { >
- + 현재 적용된 문체 : - + 담담체
- + 공개 설정 : - + 나만 보기 어울리는 사진을 추가해보세요.
사진은 선택사항이며,
나중에 천천히 추가하셔도 괜찮습니다.
- + 사진은 책에 인쇄하기 위해 고화질 사진(긴 쪽 기준 최소 1000px)만 업로드할 수 있어요.
@@ -107,10 +141,16 @@ export function AStoryFProofread() { pos="relative" w="100%" > - + TIP - + 각 단락을 선택해서 고유명사를 수정하거나,
의도와 다르게 작성된 이야기를 원하는 내용으로 직접 바꿀 수 있습니다.{" "}
- + 제목 - + 수정
- + 작은 시장, 큰 사랑
@@ -169,18 +228,37 @@ export function AStoryFProofread() { py="10px" w="360px" > - + 1단락 - + 수정
- + 학교를 마치고 집으로 돌아오던 늦은 오후였다. 가방을 메고 좁은 골목길을 따라 시장 쪽으로 걸어가는데, 어디선가 군밤 굽는 냄새가 솔솔 풍겨왔다. 구수하고 달큰한 향이 코끝을 간질이자, 학교 끝나고 집에 가는 길이 조금은 설레였다.

겨울이 다가오고 있었고, 해는 이미 많이 기울어 있었다. 시장 입구에 들어서자, 늘 그렇듯 분주한 풍경이 눈에 들어왔다. 가게 앞에 나란히 놓인 귤 상자, 손님을 부르는 상인들의 목소리, 생선 비린내와 튀김 기름 냄새가 뒤섞인 공기. 그때 나는 사람들 사이로 장을 보고 계신 어머니의 모습을 발견했다.
@@ -194,24 +272,43 @@ export function AStoryFProofread() { py="10px" w="360px" > - + 2단락 - + 수정
- + “엄마!”

나는 반가운 마음에 큰 소리로 어머니를 불렀다. 어머니는 미역이 담긴 비닐봉지를 고르고 계셨는데, 내 목소리에 깜짝 놀라시더니 고개를 돌려 나를 보셨다. 순간 어머니의 얼굴에 번져가는 웃음이 얼마나 따뜻하고 아름다웠는지 모른다. 놀라움이 가득한 표정에서 환한 미소로 변해가는 그 표정을, 나는 아직도 잊을 수 없다.

“우리{" "} - + [1. 이름] {" "}왔어?”

어머니는 장바구니를 들어 올리며 내 앞까지 걸어오셨다. 얼굴에는 하루의 피로가 묻어 있었지만, 나를 보는 눈빛은 그 어떤 보석보다 반짝였다. 나는 어머니가 드신 장바구니를 대신 들어드리려고 손을 뻗었지만, 어머니는 살짝 웃으며 고개를 저으셨다.

“괜찮아. 엄마가 들 수 있어.{" "} - + [1. 이름] {" "}학교 끝났어? 배고프지?” @@ -227,18 +324,37 @@ export function AStoryFProofread() { py="10px" w="360px" > - + 3단락 - + 수정
- + 나는 고개를 끄덕였다. 사실 점심을 먹고도 오후 수업 내내 배가 고팠다. 그날 따라 학교 급식으로 나온 미역국은 너무 싱거웠고, 반찬도 입에 잘 맞지 않았다. 그래서인지 방과 후의 시장은 더욱 풍요롭게 느껴졌다. 시장 어귀에 있는 떡볶이 가게에서는 매콤달콤한 양념 냄새가 풍겨왔고, 튀김 옆에서는 순대와 오뎅 국물이 모락모락 김을 피우고 있었다.

어머니는 내 손을 잡고 떡볶이 집 앞에 멈추셨다.

“오늘은 떡볶이 먹고 갈까?”

나는 두 눈을 반짝이며 고개를 끄덕였다. 어머니는 웃으며 천 원짜리 두 장을 떡볶이 아줌마에게 내셨고, 우리는 떡볶이와 오뎅 두 개를 받았다. 김이 모락모락 나는 떡볶이를 한 입 베어무는 순간, 달고 맵고 뜨거운 맛이 입안 가득 번졌다. 혀끝이 얼얼했지만, 너무 맛있어서 눈물이 날 뻔했다.

어머니는 내 옆에서 오뎅 국물을 후후 불어 드셨다. 그 모습이 참 편안하고 따뜻해 보였다. 시장의 소음 속에서, 우리는 그렇게 둘만의 시간을 보냈다. 어머니는 가끔씩 내 머리를 쓰다듬어 주셨다. 손끝에서 전해지는 온기가 내 마음을 간질이며, 이상하게도 울고 싶어졌다.
@@ -252,18 +368,37 @@ export function AStoryFProofread() { py="10px" w="360px" > - + 4단락 - + 수정
- + 그때 나는 몰랐다. 어머니가 그날 장바구니에 담았던 미역과 두부, 콩나물과 달걀을 가지고 집에 가셔서, 얼마나 정성스럽게 저녁상을 차리셨는지. 나중에야 알았다. 하루 종일 서서 일하고, 시장까지 들렀다가도 내 얼굴을 보며 웃어주던 그 미소 뒤에, 얼마나 큰 사랑과 노력이 숨어 있었는지를.

집으로 돌아오는 길, 어머니와 나는 나란히 걸었다. 어머니의 코트 깃에는 생선 비린내와 미역 냄새가 살짝 묻어 있었지만, 나는 그 냄새가 좋았다. 왠지 모르게 든든했고, 집에 간다는 느낌이 들어서 마음이 편안해졌다.

그날 저녁, 어머니는 미역국을 끓여주셨다. 따뜻한 국물 한 숟가락을 떠먹는 순간, 학교 급식으로 먹었던 싱거운 미역국은 잊혀지고, 어머니의 국물이야말로 진짜 미역국이라는 생각이 들었다. 부드럽고 고소하면서도, 뱃속까지 따뜻해지는 그 맛. 나는 밥그릇을 비우고 나서야, 배가 부른 게 아니라 마음이 든든해진다는 것을 알았다.

그리고 지금도, 살아가는 일이 힘들고 지칠 때면 그날의 기억이 떠오른다. 시장에서 나를 보고 환하게 웃으시던 어머니의 얼굴, 따뜻한 떡볶이와 오뎅 국물의 맛, 그리고 겨울 저녁집으로 돌아오던 길의 포근한 공기. 그 모든 것들이 내 안에서 작은 등불처럼 빛나며, 다시 일어설 힘을 준다.
@@ -289,12 +424,24 @@ export function AStoryFProofread() { px="19px" py="9px" > - + 편집 설정
- + 검토 완료
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 b620c1f9..665564c3 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 @@ -17,6 +17,8 @@ export function AStoryFProofread() { >
- + 현재 적용된 문체 : - + 담담체
- + 공개 설정 : - + 나만 보기 어울리는 사진을 추가해보세요.
사진은 선택사항이며,
나중에 천천히 추가하셔도 괜찮습니다.
- + 사진은 책에 인쇄하기 위해 고화질 사진(긴 쪽 기준 최소 1000px)만 업로드할 수 있어요.
@@ -101,10 +135,16 @@ export function AStoryFProofread() { pos="relative" w="100%" > - + TIP - + 각 단락을 선택해서 고유명사를 수정하거나,
의도와 다르게 작성된 이야기를 원하는 내용으로 직접 바꿀 수 있습니다.{" "}
- + 제목 - + 수정
- + 작은 시장, 큰 사랑
@@ -163,18 +222,37 @@ export function AStoryFProofread() { py="10px" w="360px" > - + 1단락 - + 수정
- + 학교를 마치고 집으로 돌아오던 늦은 오후였다. 가방을 메고 좁은 골목길을 따라 시장 쪽으로 걸어가는데, 어디선가 군밤 굽는 냄새가 솔솔 풍겨왔다. 구수하고 달큰한 향이 코끝을 간질이자, 학교 끝나고 집에 가는 길이 조금은 설레였다.

겨울이 다가오고 있었고, 해는 이미 많이 기울어 있었다. 시장 입구에 들어서자, 늘 그렇듯 분주한 풍경이 눈에 들어왔다. 가게 앞에 나란히 놓인 귤 상자, 손님을 부르는 상인들의 목소리, 생선 비린내와 튀김 기름 냄새가 뒤섞인 공기. 그때 나는 사람들 사이로 장을 보고 계신 어머니의 모습을 발견했다.
@@ -188,24 +266,43 @@ export function AStoryFProofread() { py="10px" w="360px" > - + 2단락 - + 수정
- + “엄마!”

나는 반가운 마음에 큰 소리로 어머니를 불렀다. 어머니는 미역이 담긴 비닐봉지를 고르고 계셨는데, 내 목소리에 깜짝 놀라시더니 고개를 돌려 나를 보셨다. 순간 어머니의 얼굴에 번져가는 웃음이 얼마나 따뜻하고 아름다웠는지 모른다. 놀라움이 가득한 표정에서 환한 미소로 변해가는 그 표정을, 나는 아직도 잊을 수 없다.

“우리{" "} - + [1. 이름] {" "}왔어?”

어머니는 장바구니를 들어 올리며 내 앞까지 걸어오셨다. 얼굴에는 하루의 피로가 묻어 있었지만, 나를 보는 눈빛은 그 어떤 보석보다 반짝였다. 나는 어머니가 드신 장바구니를 대신 들어드리려고 손을 뻗었지만, 어머니는 살짝 웃으며 고개를 저으셨다.

“괜찮아. 엄마가 들 수 있어.{" "} - + [1. 이름] {" "}학교 끝났어? 배고프지?” @@ -221,18 +318,37 @@ export function AStoryFProofread() { py="10px" w="360px" > - + 3단락 - + 수정
- + 나는 고개를 끄덕였다. 사실 점심을 먹고도 오후 수업 내내 배가 고팠다. 그날 따라 학교 급식으로 나온 미역국은 너무 싱거웠고, 반찬도 입에 잘 맞지 않았다. 그래서인지 방과 후의 시장은 더욱 풍요롭게 느껴졌다. 시장 어귀에 있는 떡볶이 가게에서는 매콤달콤한 양념 냄새가 풍겨왔고, 튀김 옆에서는 순대와 오뎅 국물이 모락모락 김을 피우고 있었다.

어머니는 내 손을 잡고 떡볶이 집 앞에 멈추셨다.

“오늘은 떡볶이 먹고 갈까?”

나는 두 눈을 반짝이며 고개를 끄덕였다. 어머니는 웃으며 천 원짜리 두 장을 떡볶이 아줌마에게 내셨고, 우리는 떡볶이와 오뎅 두 개를 받았다. 김이 모락모락 나는 떡볶이를 한 입 베어무는 순간, 달고 맵고 뜨거운 맛이 입안 가득 번졌다. 혀끝이 얼얼했지만, 너무 맛있어서 눈물이 날 뻔했다.

어머니는 내 옆에서 오뎅 국물을 후후 불어 드셨다. 그 모습이 참 편안하고 따뜻해 보였다. 시장의 소음 속에서, 우리는 그렇게 둘만의 시간을 보냈다. 어머니는 가끔씩 내 머리를 쓰다듬어 주셨다. 손끝에서 전해지는 온기가 내 마음을 간질이며, 이상하게도 울고 싶어졌다.
@@ -246,18 +362,37 @@ export function AStoryFProofread() { py="10px" w="360px" > - + 4단락 - + 수정
- + 그때 나는 몰랐다. 어머니가 그날 장바구니에 담았던 미역과 두부, 콩나물과 달걀을 가지고 집에 가셔서, 얼마나 정성스럽게 저녁상을 차리셨는지. 나중에야 알았다. 하루 종일 서서 일하고, 시장까지 들렀다가도 내 얼굴을 보며 웃어주던 그 미소 뒤에, 얼마나 큰 사랑과 노력이 숨어 있었는지를.

집으로 돌아오는 길, 어머니와 나는 나란히 걸었다. 어머니의 코트 깃에는 생선 비린내와 미역 냄새가 살짝 묻어 있었지만, 나는 그 냄새가 좋았다. 왠지 모르게 든든했고, 집에 간다는 느낌이 들어서 마음이 편안해졌다.

그날 저녁, 어머니는 미역국을 끓여주셨다. 따뜻한 국물 한 숟가락을 떠먹는 순간, 학교 급식으로 먹었던 싱거운 미역국은 잊혀지고, 어머니의 국물이야말로 진짜 미역국이라는 생각이 들었다. 부드럽고 고소하면서도, 뱃속까지 따뜻해지는 그 맛. 나는 밥그릇을 비우고 나서야, 배가 부른 게 아니라 마음이 든든해진다는 것을 알았다.

그리고 지금도, 살아가는 일이 힘들고 지칠 때면 그날의 기억이 떠오른다. 시장에서 나를 보고 환하게 웃으시던 어머니의 얼굴, 따뜻한 떡볶이와 오뎅 국물의 맛, 그리고 겨울 저녁집으로 돌아오던 길의 포근한 공기. 그 모든 것들이 내 안에서 작은 등불처럼 빛나며, 다시 일어설 힘을 준다.
@@ -283,12 +418,24 @@ export function AStoryFProofread() { px="19px" py="9px" > - + 편집 설정
- + 검토 완료
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 9917f7f6..9b8b42d1 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: 213 expression: json!(output.diagnostics) --- [ @@ -92,7 +91,7 @@ expression: json!(output.diagnostics) "appliedValue": { "state": "emitted", "generatedNodeId": "3879:35547", - "generatedSource": " \n 작은 시장, 큰 사랑\n ", + "generatedSource": " \n 작은 시장, 큰 사랑\n
", "sourceTruncated": false, "propertyMappingVerified": false }, @@ -107,7 +106,7 @@ expression: json!(output.diagnostics) "parentLayoutMode": "HORIZONTAL", "childSizing": "FILL", "generatedParent": "
\n \n \n 편집 설정\n \n
\n
\n \n 검토 완료\n \n
\n " + " \n \n \n 편집 설정\n
\n \n
\n \n 검토 완료\n \n
\n " ], "fallback": null, "heightPreservation": { diff --git a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151__wquw_151_proofread_source_map.snap b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151__wquw_151_proofread_source_map.snap index 8f7e652c..c621b331 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 @@ -1,6 +1,5 @@ --- source: crates/devup-mcp-devup-ui/tests/wquw_151.rs -assertion_line: 178 expression: output.source_map --- { @@ -124,6 +123,18 @@ expression: output.source_map "variableId": "VariableID:1:998", "resolution": "variable-token" }, + { + "generatedProperty": "fontSize=\"28px\"", + "nodeId": "3879:35520", + "property": "fontSize", + "resolution": "raw-fallback" + }, + { + "generatedProperty": "lineHeight=\"39px\"", + "nodeId": "3879:35520", + "property": "lineHeight", + "resolution": "raw-fallback" + }, { "generatedProperty": "textAlign=\"center\"", "nodeId": "3879:35520", @@ -254,6 +265,18 @@ expression: output.source_map "variableId": "VariableID:1:998", "resolution": "variable-token" }, + { + "generatedProperty": "fontSize=\"16px\"", + "nodeId": "3879:35523", + "property": "fontSize", + "resolution": "raw-fallback" + }, + { + "generatedProperty": "lineHeight=\"26px\"", + "nodeId": "3879:35523", + "property": "lineHeight", + "resolution": "raw-fallback" + }, { "generatedProperty": "typography=\"bodyXs\"", "nodeId": "3879:35523", @@ -300,6 +323,18 @@ expression: output.source_map "variableId": "VariableID:1:995", "resolution": "variable-token" }, + { + "generatedProperty": "fontSize=\"17px\"", + "nodeId": "3879:35524", + "property": "fontSize", + "resolution": "raw-fallback" + }, + { + "generatedProperty": "lineHeight=\"29px\"", + "nodeId": "3879:35524", + "property": "lineHeight", + "resolution": "raw-fallback" + }, { "generatedProperty": "typography=\"bodySmSemibold\"", "nodeId": "3879:35524", @@ -456,6 +491,18 @@ expression: output.source_map "variableId": "VariableID:1:998", "resolution": "variable-token" }, + { + "generatedProperty": "fontSize=\"16px\"", + "nodeId": "3879:35528", + "property": "fontSize", + "resolution": "raw-fallback" + }, + { + "generatedProperty": "lineHeight=\"26px\"", + "nodeId": "3879:35528", + "property": "lineHeight", + "resolution": "raw-fallback" + }, { "generatedProperty": "typography=\"bodyXs\"", "nodeId": "3879:35528", @@ -502,6 +549,18 @@ expression: output.source_map "variableId": "VariableID:1:995", "resolution": "variable-token" }, + { + "generatedProperty": "fontSize=\"17px\"", + "nodeId": "3879:35529", + "property": "fontSize", + "resolution": "raw-fallback" + }, + { + "generatedProperty": "lineHeight=\"29px\"", + "nodeId": "3879:35529", + "property": "lineHeight", + "resolution": "raw-fallback" + }, { "generatedProperty": "typography=\"bodySmSemibold\"", "nodeId": "3879:35529", @@ -736,6 +795,18 @@ expression: output.source_map "variableId": "VariableID:17:1554", "resolution": "variable-token" }, + { + "generatedProperty": "fontSize=\"15px\"", + "nodeId": "3879:35535", + "property": "fontSize", + "resolution": "raw-fallback" + }, + { + "generatedProperty": "lineHeight=\"24px\"", + "nodeId": "3879:35535", + "property": "lineHeight", + "resolution": "raw-fallback" + }, { "generatedProperty": "textAlign=\"center\"", "nodeId": "3879:35535", @@ -814,6 +885,18 @@ expression: output.source_map "variableId": "VariableID:17:1554", "resolution": "variable-token" }, + { + "generatedProperty": "fontSize=\"14px\"", + "nodeId": "3879:35536", + "property": "fontSize", + "resolution": "raw-fallback" + }, + { + "generatedProperty": "lineHeight=\"22px\"", + "nodeId": "3879:35536", + "property": "lineHeight", + "resolution": "raw-fallback" + }, { "generatedProperty": "typography=\"captionSm\"", "nodeId": "3879:35536", @@ -927,6 +1010,18 @@ expression: output.source_map "variableId": "VariableID:1:998", "resolution": "variable-token" }, + { + "generatedProperty": "fontSize=\"16px\"", + "nodeId": "3879:35538", + "property": "fontSize", + "resolution": "raw-fallback" + }, + { + "generatedProperty": "lineHeight=\"26px\"", + "nodeId": "3879:35538", + "property": "lineHeight", + "resolution": "raw-fallback" + }, { "generatedProperty": "typography=\"bodyXsSemibold\"", "nodeId": "3879:35538", @@ -967,6 +1062,18 @@ expression: output.source_map "variableId": "VariableID:1:998", "resolution": "variable-token" }, + { + "generatedProperty": "fontSize=\"16px\"", + "nodeId": "3879:35539", + "property": "fontSize", + "resolution": "raw-fallback" + }, + { + "generatedProperty": "lineHeight=\"26px\"", + "nodeId": "3879:35539", + "property": "lineHeight", + "resolution": "raw-fallback" + }, { "generatedProperty": "typography=\"bodyXs\"", "nodeId": "3879:35539", @@ -1209,6 +1316,18 @@ expression: output.source_map "variableId": "VariableID:1:998", "resolution": "variable-token" }, + { + "generatedProperty": "fontSize=\"16px\"", + "nodeId": "I3879:35545;1690:32933", + "property": "fontSize", + "resolution": "raw-fallback" + }, + { + "generatedProperty": "lineHeight=\"26px\"", + "nodeId": "I3879:35545;1690:32933", + "property": "lineHeight", + "resolution": "raw-fallback" + }, { "generatedProperty": "typography=\"bodyXsSemibold\"", "nodeId": "I3879:35545;1690:32933", @@ -1310,6 +1429,18 @@ expression: output.source_map "variableId": "VariableID:17:1554", "resolution": "variable-token" }, + { + "generatedProperty": "fontSize=\"14px\"", + "nodeId": "I3879:35545;1690:32947", + "property": "fontSize", + "resolution": "raw-fallback" + }, + { + "generatedProperty": "lineHeight=\"22px\"", + "nodeId": "I3879:35545;1690:32947", + "property": "lineHeight", + "resolution": "raw-fallback" + }, { "generatedProperty": "typography=\"captionSmSemibold\"", "nodeId": "I3879:35545;1690:32947", @@ -1405,6 +1536,18 @@ expression: output.source_map "property": "layoutGrow", "resolution": "raw-fallback" }, + { + "generatedProperty": "fontSize=\"22px\"", + "nodeId": "3879:35547", + "property": "fontSize", + "resolution": "raw-fallback" + }, + { + "generatedProperty": "lineHeight=\"31px\"", + "nodeId": "3879:35547", + "property": "lineHeight", + "resolution": "raw-fallback" + }, { "generatedProperty": "typography=\"h5\"", "nodeId": "3879:35547", @@ -1524,6 +1667,18 @@ expression: output.source_map "variableId": "VariableID:1:998", "resolution": "variable-token" }, + { + "generatedProperty": "fontSize=\"16px\"", + "nodeId": "I3879:35549;1690:32933", + "property": "fontSize", + "resolution": "raw-fallback" + }, + { + "generatedProperty": "lineHeight=\"26px\"", + "nodeId": "I3879:35549;1690:32933", + "property": "lineHeight", + "resolution": "raw-fallback" + }, { "generatedProperty": "typography=\"bodyXsSemibold\"", "nodeId": "I3879:35549;1690:32933", @@ -1625,6 +1780,18 @@ expression: output.source_map "variableId": "VariableID:17:1554", "resolution": "variable-token" }, + { + "generatedProperty": "fontSize=\"14px\"", + "nodeId": "I3879:35549;1690:32947", + "property": "fontSize", + "resolution": "raw-fallback" + }, + { + "generatedProperty": "lineHeight=\"22px\"", + "nodeId": "I3879:35549;1690:32947", + "property": "lineHeight", + "resolution": "raw-fallback" + }, { "generatedProperty": "typography=\"captionSmSemibold\"", "nodeId": "I3879:35549;1690:32947", @@ -1720,6 +1887,18 @@ expression: output.source_map "variableId": "VariableID:1:998", "resolution": "variable-token" }, + { + "generatedProperty": "fontSize=\"18px\"", + "nodeId": "3879:35551", + "property": "fontSize", + "resolution": "raw-fallback" + }, + { + "generatedProperty": "lineHeight=\"32px\"", + "nodeId": "3879:35551", + "property": "lineHeight", + "resolution": "raw-fallback" + }, { "generatedProperty": "typography=\"body\"", "nodeId": "3879:35551", @@ -1845,6 +2024,18 @@ expression: output.source_map "variableId": "VariableID:1:998", "resolution": "variable-token" }, + { + "generatedProperty": "fontSize=\"16px\"", + "nodeId": "I3879:35553;1690:32933", + "property": "fontSize", + "resolution": "raw-fallback" + }, + { + "generatedProperty": "lineHeight=\"26px\"", + "nodeId": "I3879:35553;1690:32933", + "property": "lineHeight", + "resolution": "raw-fallback" + }, { "generatedProperty": "typography=\"bodyXsSemibold\"", "nodeId": "I3879:35553;1690:32933", @@ -1946,6 +2137,18 @@ expression: output.source_map "variableId": "VariableID:17:1554", "resolution": "variable-token" }, + { + "generatedProperty": "fontSize=\"14px\"", + "nodeId": "I3879:35553;1690:32947", + "property": "fontSize", + "resolution": "raw-fallback" + }, + { + "generatedProperty": "lineHeight=\"22px\"", + "nodeId": "I3879:35553;1690:32947", + "property": "lineHeight", + "resolution": "raw-fallback" + }, { "generatedProperty": "typography=\"captionSmSemibold\"", "nodeId": "I3879:35553;1690:32947", @@ -2041,6 +2244,18 @@ expression: output.source_map "variableId": "VariableID:1:998", "resolution": "variable-token" }, + { + "generatedProperty": "fontSize=\"18px\"", + "nodeId": "3879:35555", + "property": "fontSize", + "resolution": "raw-fallback" + }, + { + "generatedProperty": "lineHeight=\"32px\"", + "nodeId": "3879:35555", + "property": "lineHeight", + "resolution": "raw-fallback" + }, { "generatedProperty": "typography=\"body\"", "nodeId": "3879:35555", @@ -2080,6 +2295,12 @@ expression: output.source_map "variableId": "VariableID:98:3276", "resolution": "variable-token" }, + { + "generatedProperty": "lineHeight=\"31px\"", + "nodeId": "3879:35555", + "property": "styledTextSegments", + "resolution": "raw-fallback" + }, { "generatedProperty": "typography=\"bodySemibold\"", "nodeId": "3879:35555", @@ -2106,6 +2327,12 @@ expression: output.source_map "variableId": "VariableID:98:3276", "resolution": "variable-token" }, + { + "generatedProperty": "lineHeight=\"31px\"", + "nodeId": "3879:35555", + "property": "styledTextSegments", + "resolution": "raw-fallback" + }, { "generatedProperty": "typography=\"bodySemibold\"", "nodeId": "3879:35555", @@ -2218,6 +2445,18 @@ expression: output.source_map "variableId": "VariableID:1:998", "resolution": "variable-token" }, + { + "generatedProperty": "fontSize=\"16px\"", + "nodeId": "I3879:35557;1690:32933", + "property": "fontSize", + "resolution": "raw-fallback" + }, + { + "generatedProperty": "lineHeight=\"26px\"", + "nodeId": "I3879:35557;1690:32933", + "property": "lineHeight", + "resolution": "raw-fallback" + }, { "generatedProperty": "typography=\"bodyXsSemibold\"", "nodeId": "I3879:35557;1690:32933", @@ -2319,6 +2558,18 @@ expression: output.source_map "variableId": "VariableID:17:1554", "resolution": "variable-token" }, + { + "generatedProperty": "fontSize=\"14px\"", + "nodeId": "I3879:35557;1690:32947", + "property": "fontSize", + "resolution": "raw-fallback" + }, + { + "generatedProperty": "lineHeight=\"22px\"", + "nodeId": "I3879:35557;1690:32947", + "property": "lineHeight", + "resolution": "raw-fallback" + }, { "generatedProperty": "typography=\"captionSmSemibold\"", "nodeId": "I3879:35557;1690:32947", @@ -2414,6 +2665,18 @@ expression: output.source_map "variableId": "VariableID:1:998", "resolution": "variable-token" }, + { + "generatedProperty": "fontSize=\"18px\"", + "nodeId": "3879:35559", + "property": "fontSize", + "resolution": "raw-fallback" + }, + { + "generatedProperty": "lineHeight=\"32px\"", + "nodeId": "3879:35559", + "property": "lineHeight", + "resolution": "raw-fallback" + }, { "generatedProperty": "typography=\"body\"", "nodeId": "3879:35559", @@ -2539,6 +2802,18 @@ expression: output.source_map "variableId": "VariableID:1:998", "resolution": "variable-token" }, + { + "generatedProperty": "fontSize=\"16px\"", + "nodeId": "I3879:35561;1690:32933", + "property": "fontSize", + "resolution": "raw-fallback" + }, + { + "generatedProperty": "lineHeight=\"26px\"", + "nodeId": "I3879:35561;1690:32933", + "property": "lineHeight", + "resolution": "raw-fallback" + }, { "generatedProperty": "typography=\"bodyXsSemibold\"", "nodeId": "I3879:35561;1690:32933", @@ -2640,6 +2915,18 @@ expression: output.source_map "variableId": "VariableID:17:1554", "resolution": "variable-token" }, + { + "generatedProperty": "fontSize=\"14px\"", + "nodeId": "I3879:35561;1690:32947", + "property": "fontSize", + "resolution": "raw-fallback" + }, + { + "generatedProperty": "lineHeight=\"22px\"", + "nodeId": "I3879:35561;1690:32947", + "property": "lineHeight", + "resolution": "raw-fallback" + }, { "generatedProperty": "typography=\"captionSmSemibold\"", "nodeId": "I3879:35561;1690:32947", @@ -2735,6 +3022,18 @@ expression: output.source_map "variableId": "VariableID:1:998", "resolution": "variable-token" }, + { + "generatedProperty": "fontSize=\"18px\"", + "nodeId": "3879:35563", + "property": "fontSize", + "resolution": "raw-fallback" + }, + { + "generatedProperty": "lineHeight=\"32px\"", + "nodeId": "3879:35563", + "property": "lineHeight", + "resolution": "raw-fallback" + }, { "generatedProperty": "typography=\"body\"", "nodeId": "3879:35563", @@ -2935,6 +3234,18 @@ expression: output.source_map "variableId": "VariableID:1:998", "resolution": "variable-token" }, + { + "generatedProperty": "fontSize=\"18px\"", + "nodeId": "3879:35566", + "property": "fontSize", + "resolution": "raw-fallback" + }, + { + "generatedProperty": "lineHeight=\"31px\"", + "nodeId": "3879:35566", + "property": "lineHeight", + "resolution": "raw-fallback" + }, { "generatedProperty": "typography=\"bodySemibold\"", "nodeId": "3879:35566", @@ -3022,6 +3333,18 @@ expression: output.source_map "property": "fills", "resolution": "raw-fallback" }, + { + "generatedProperty": "fontSize=\"18px\"", + "nodeId": "3879:35568", + "property": "fontSize", + "resolution": "raw-fallback" + }, + { + "generatedProperty": "lineHeight=\"31px\"", + "nodeId": "3879:35568", + "property": "lineHeight", + "resolution": "raw-fallback" + }, { "generatedProperty": "typography=\"bodySemibold\"", "nodeId": "3879:35568", 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 7f8fb4a7..ee1a56be 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 @@ -1,6 +1,5 @@ --- source: crates/devup-mcp-devup-ui/tests/wquw_151_frames.rs -assertion_line: 218 expression: output.tsx --- import { Box, Center, Image, Text, VStack } from "@devup-ui/react"; @@ -32,7 +31,14 @@ export function Wquw151Frame387935503() { maskSize="contain" w="64px" /> - + 기록해주신 이야기를
정리하고 있어요
잠시만 기다려주세요
@@ -48,6 +54,8 @@ export function Wquw151Frame387935503() {
- + 현재 적용된 문체 : - + 담담체
- + 공개 설정 : - + 나만 보기 - + TIP - + 제목 - + 수정 @@ -174,6 +218,8 @@ export function Wquw151Frame387935518() { - + 1단락 - + 수정 @@ -205,6 +263,8 @@ export function Wquw151Frame387935518() {
- + 2단락 - + 수정 @@ -237,6 +309,8 @@ export function Wquw151Frame387935518() {
“엄마!”

나는 반가운 마음에 큰 소리로 어머니를 불렀다. 어머니는 미역이 담긴 비닐봉지를 고르고 계셨는데, 내 목소리에 깜짝 놀라시더니 고개를 돌려 나를 보셨다. 순간 어머니의 얼굴에 번져가는 웃음이 얼마나 따뜻하고 아름다웠는지 모른다. 놀라움이 가득한 표정에서 환한 미소로 변해가는 그 표정을, 나는 아직도 잊을 수 없다.

“우리{" "} - + [1. 이름] {" "}왔어?”

어머니는 장바구니를 들어 올리며 내 앞까지 걸어오셨다. 얼굴에는 하루의 피로가 묻어 있었지만, 나를 보는 눈빛은 그 어떤 보석보다 반짝였다. 나는 어머니가 드신 장바구니를 대신 들어드리려고 손을 뻗었지만, 어머니는 살짝 웃으며 고개를 저으셨다.

“괜찮아. 엄마가 들 수 있어.{" "} - + [1. 이름] {" "}학교 끝났어? 배고프지?” @@ -264,12 +338,24 @@ export function Wquw151Frame387935518() { py="10px" w="360px" > - + 3단락 - + 수정 @@ -277,6 +363,8 @@ export function Wquw151Frame387935518() {
- + 4단락 - + 수정 @@ -309,6 +409,8 @@ export function Wquw151Frame387935518() {
- + 편집 설정
- + 검토 완료
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 f8842118..93a47ab3 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 @@ -1,6 +1,5 @@ --- source: crates/devup-mcp-devup-ui/tests/wquw_151_frames.rs -assertion_line: 218 expression: output.tsx --- import { Box, Center, Flex, Image, Text, VStack } from "@devup-ui/react"; @@ -24,6 +23,8 @@ export function Wquw151Frame387935569() { >
- + 현재 적용된 문체 : - + 담담체 @@ -52,6 +65,8 @@ export function Wquw151Frame387935569() { > - + AI 편집자에게 다듬기 맡기기 @@ -82,7 +103,13 @@ export function Wquw151Frame387935569() { boxSize="20px" overflow="hidden" /> - + 내 문장 최대한 유지하기 @@ -90,10 +117,22 @@ export function Wquw151Frame387935569() {
- + 공개 설정 : - + 나만 보기 - + TIP - + 제목 - + 수정 @@ -220,6 +277,8 @@ export function Wquw151Frame387935569() { - + 1단락 - + 수정 @@ -251,6 +322,8 @@ export function Wquw151Frame387935569() {
- + 2단락 - + 수정 @@ -283,6 +368,8 @@ export function Wquw151Frame387935569() {
“엄마!”

나는 반가운 마음에 큰 소리로 어머니를 불렀다. 어머니는 미역이 담긴 비닐봉지를 고르고 계셨는데, 내 목소리에 깜짝 놀라시더니 고개를 돌려 나를 보셨다. 순간 어머니의 얼굴에 번져가는 웃음이 얼마나 따뜻하고 아름다웠는지 모른다. 놀라움이 가득한 표정에서 환한 미소로 변해가는 그 표정을, 나는 아직도 잊을 수 없다.

“우리{" "} - + [1. 이름] {" "}왔어?”

어머니는 장바구니를 들어 올리며 내 앞까지 걸어오셨다. 얼굴에는 하루의 피로가 묻어 있었지만, 나를 보는 눈빛은 그 어떤 보석보다 반짝였다. 나는 어머니가 드신 장바구니를 대신 들어드리려고 손을 뻗었지만, 어머니는 살짝 웃으며 고개를 저으셨다.

“괜찮아. 엄마가 들 수 있어.{" "} - + [1. 이름] {" "}학교 끝났어? 배고프지?” @@ -310,12 +397,24 @@ export function Wquw151Frame387935569() { py="10px" w="360px" > - + 3단락 - + 수정 @@ -323,6 +422,8 @@ export function Wquw151Frame387935569() {
- + 4단락 - + 수정 @@ -355,6 +468,8 @@ export function Wquw151Frame387935569() {
- + 문체 변경
- + 검토 완료
@@ -414,10 +541,22 @@ export function Wquw151Frame387935569() { - + 문체 변경 - + 책 전체 @@ -456,10 +595,22 @@ export function Wquw151Frame387935569() { maskSize="contain" /> - + 담담체 - + 차분하고 담백한 문체 @@ -476,10 +627,22 @@ export function Wquw151Frame387935569() { maskSize="contain" /> - + 서정체 - + 감성적이고 부드러운 문체 @@ -496,10 +659,22 @@ export function Wquw151Frame387935569() { maskSize="contain" /> - + 강건체 - + 단호하고 힘찬 문체 @@ -507,10 +682,22 @@ export function Wquw151Frame387935569() { - + 교정 설정 - + 이번 이야기 @@ -530,10 +717,22 @@ export function Wquw151Frame387935569() { maskSize="contain" /> - + AI 담당자와 함께 문체 다듬기 - + AI 담당자가 문맥을 자연스럽게 수정해요 @@ -550,10 +749,22 @@ export function Wquw151Frame387935569() { maskSize="contain" /> - + 내 문장 그대로 맞춤법만 다듬기 - + 입력하신 문장 그대로의 느낌을 담아내요 diff --git a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35652.snap b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35652.snap index f3a610be..0d3cbaf2 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 @@ -1,6 +1,5 @@ --- source: crates/devup-mcp-devup-ui/tests/wquw_151_frames.rs -assertion_line: 218 expression: output.tsx --- import { Box, Center, Flex, Image, Text, VStack } from "@devup-ui/react"; @@ -24,6 +23,8 @@ export function Wquw151Frame387935652() { >
- + 현재 적용된 문체 : - + 담담체 @@ -52,6 +65,8 @@ export function Wquw151Frame387935652() { > - + AI 편집자에게 다듬기 맡기기 @@ -82,7 +103,13 @@ export function Wquw151Frame387935652() { boxSize="20px" overflow="hidden" /> - + 내 문장 최대한 유지하기 @@ -90,10 +117,22 @@ export function Wquw151Frame387935652() {
- + 공개 설정 : - + 나만 보기 - + TIP - + 제목 - + 수정 @@ -220,6 +277,8 @@ export function Wquw151Frame387935652() { - + 1단락 - + 수정 @@ -251,6 +322,8 @@ export function Wquw151Frame387935652() {
- + 2단락 - + 수정 @@ -283,6 +368,8 @@ export function Wquw151Frame387935652() {
“엄마!”

나는 반가운 마음에 큰 소리로 어머니를 불렀다. 어머니는 미역이 담긴 비닐봉지를 고르고 계셨는데, 내 목소리에 깜짝 놀라시더니 고개를 돌려 나를 보셨다. 순간 어머니의 얼굴에 번져가는 웃음이 얼마나 따뜻하고 아름다웠는지 모른다. 놀라움이 가득한 표정에서 환한 미소로 변해가는 그 표정을, 나는 아직도 잊을 수 없다.

“우리{" "} - + [1. 이름] {" "}왔어?”

어머니는 장바구니를 들어 올리며 내 앞까지 걸어오셨다. 얼굴에는 하루의 피로가 묻어 있었지만, 나를 보는 눈빛은 그 어떤 보석보다 반짝였다. 나는 어머니가 드신 장바구니를 대신 들어드리려고 손을 뻗었지만, 어머니는 살짝 웃으며 고개를 저으셨다.

“괜찮아. 엄마가 들 수 있어.{" "} - + [1. 이름] {" "}학교 끝났어? 배고프지?” @@ -310,12 +397,24 @@ export function Wquw151Frame387935652() { py="10px" w="360px" > - + 3단락 - + 수정 @@ -323,6 +422,8 @@ export function Wquw151Frame387935652() {
- + 4단락 - + 수정 @@ -355,6 +468,8 @@ export function Wquw151Frame387935652() {
- + 문체 변경
- + 검토 완료
@@ -414,10 +541,22 @@ export function Wquw151Frame387935652() { - + 문체 변경 - + 책 전체 @@ -456,10 +595,22 @@ export function Wquw151Frame387935652() { maskSize="contain" /> - + 담담체 - + 차분하고 담백한 문체 @@ -476,10 +627,22 @@ export function Wquw151Frame387935652() { maskSize="contain" /> - + 서정체 - + 감성적이고 부드러운 문체 @@ -496,10 +659,22 @@ export function Wquw151Frame387935652() { maskSize="contain" /> - + 강건체 - + 단호하고 힘찬 문체 diff --git a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35729.snap b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35729.snap index b4d78b29..d04585fa 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 @@ -1,6 +1,5 @@ --- source: crates/devup-mcp-devup-ui/tests/wquw_151_frames.rs -assertion_line: 218 expression: output.tsx --- import { Box, Center, Flex, Image, Text, VStack } from "@devup-ui/react"; @@ -24,6 +23,8 @@ export function Wquw151Frame387935729() { >
- + 현재 적용된 문체 : - + 담담체 @@ -52,6 +65,8 @@ export function Wquw151Frame387935729() { > - + AI 편집자에게 다듬기 맡기기 @@ -82,7 +103,13 @@ export function Wquw151Frame387935729() { boxSize="20px" overflow="hidden" /> - + 내 문장 최대한 유지하기 @@ -90,10 +117,22 @@ export function Wquw151Frame387935729() {
- + 공개 설정 : - + 나만 보기 - + TIP - + 제목 - + 수정 @@ -220,6 +277,8 @@ export function Wquw151Frame387935729() { - + 1단락 - + 수정 @@ -251,6 +322,8 @@ export function Wquw151Frame387935729() {
- + 2단락 - + 수정 @@ -283,6 +368,8 @@ export function Wquw151Frame387935729() {
“엄마!”

나는 반가운 마음에 큰 소리로 어머니를 불렀다. 어머니는 미역이 담긴 비닐봉지를 고르고 계셨는데, 내 목소리에 깜짝 놀라시더니 고개를 돌려 나를 보셨다. 순간 어머니의 얼굴에 번져가는 웃음이 얼마나 따뜻하고 아름다웠는지 모른다. 놀라움이 가득한 표정에서 환한 미소로 변해가는 그 표정을, 나는 아직도 잊을 수 없다.

“우리{" "} - + [1. 이름] {" "}왔어?”

어머니는 장바구니를 들어 올리며 내 앞까지 걸어오셨다. 얼굴에는 하루의 피로가 묻어 있었지만, 나를 보는 눈빛은 그 어떤 보석보다 반짝였다. 나는 어머니가 드신 장바구니를 대신 들어드리려고 손을 뻗었지만, 어머니는 살짝 웃으며 고개를 저으셨다.

“괜찮아. 엄마가 들 수 있어.{" "} - + [1. 이름] {" "}학교 끝났어? 배고프지?” @@ -310,12 +397,24 @@ export function Wquw151Frame387935729() { py="10px" w="360px" > - + 3단락 - + 수정 @@ -323,6 +422,8 @@ export function Wquw151Frame387935729() {
- + 4단락 - + 수정 @@ -355,6 +468,8 @@ export function Wquw151Frame387935729() {
- + 문체 변경
- + 검토 완료
@@ -414,10 +541,22 @@ export function Wquw151Frame387935729() { - + 교정 설정 - + 이번 이야기 @@ -456,10 +595,22 @@ export function Wquw151Frame387935729() { maskSize="contain" /> - + AI 담당자와 함께 문체 다듬기 - + AI 담당자가 문맥을 자연스럽게 수정해요 @@ -476,10 +627,22 @@ export function Wquw151Frame387935729() { maskSize="contain" /> - + 내 문장 그대로 맞춤법만 다듬기 - + 입력하신 문장 그대로의 느낌을 담아내요 diff --git a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35887.snap b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_35887.snap index c110ac5e..2117c0e2 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 @@ -1,6 +1,5 @@ --- source: crates/devup-mcp-devup-ui/tests/wquw_151_frames.rs -assertion_line: 218 expression: output.tsx --- import { Box, Center, Flex, Image, Text, VStack } from "@devup-ui/react"; @@ -24,6 +23,8 @@ export function Wquw151Frame387935887() { >
- + 현재 적용된 문체 : - + 담담체 @@ -52,6 +65,8 @@ export function Wquw151Frame387935887() { > - + AI 편집자에게 다듬기 맡기기 @@ -82,7 +103,13 @@ export function Wquw151Frame387935887() { boxSize="20px" overflow="hidden" /> - + 내 문장 최대한 유지하기 @@ -90,10 +117,22 @@ export function Wquw151Frame387935887() {
- + 공개 설정 : - + 나만 보기 - + TIP - + 제목 - + 수정 @@ -220,6 +277,8 @@ export function Wquw151Frame387935887() { - + 1단락 - + 수정 @@ -251,6 +322,8 @@ export function Wquw151Frame387935887() {
- + 2단락 - + 수정 @@ -283,6 +368,8 @@ export function Wquw151Frame387935887() {
“엄마!”

나는 반가운 마음에 큰 소리로 어머니를 불렀다. 어머니는 미역이 담긴 비닐봉지를 고르고 계셨는데, 내 목소리에 깜짝 놀라시더니 고개를 돌려 나를 보셨다. 순간 어머니의 얼굴에 번져가는 웃음이 얼마나 따뜻하고 아름다웠는지 모른다. 놀라움이 가득한 표정에서 환한 미소로 변해가는 그 표정을, 나는 아직도 잊을 수 없다.

“우리{" "} - + [1. 이름] {" "}왔어?”

어머니는 장바구니를 들어 올리며 내 앞까지 걸어오셨다. 얼굴에는 하루의 피로가 묻어 있었지만, 나를 보는 눈빛은 그 어떤 보석보다 반짝였다. 나는 어머니가 드신 장바구니를 대신 들어드리려고 손을 뻗었지만, 어머니는 살짝 웃으며 고개를 저으셨다.

“괜찮아. 엄마가 들 수 있어.{" "} - + [1. 이름] {" "}학교 끝났어? 배고프지?” @@ -310,12 +397,24 @@ export function Wquw151Frame387935887() { py="10px" w="360px" > - + 3단락 - + 수정 @@ -323,6 +422,8 @@ export function Wquw151Frame387935887() {
- + 4단락 - + 수정 @@ -355,6 +468,8 @@ export function Wquw151Frame387935887() {
- + 문체 변경
- + 검토 완료
@@ -414,10 +541,22 @@ export function Wquw151Frame387935887() { - + 공개 설정 - + 작성하신 이야기를 누구와 함께 나눌지 선택해 주세요. @@ -453,11 +592,19 @@ export function Wquw151Frame387935887() { maskSize="contain" /> - + 동행인 - + 나만 보기 - + 교정 설정 - + 이번 이야기 @@ -519,10 +686,22 @@ export function Wquw151Frame387935887() { maskSize="contain" /> - + AI 담당자와 함께 문체 다듬기 - + AI 담당자가 문맥을 자연스럽게 수정해요 @@ -539,10 +718,22 @@ export function Wquw151Frame387935887() { maskSize="contain" /> - + 내 문장 그대로 맞춤법만 다듬기 - + 입력하신 문장 그대로의 느낌을 담아내요 @@ -564,18 +755,27 @@ export function Wquw151Frame387935887() { >
- + 글 다듬기 방식을 변경할까요?
- + 취소
@@ -600,7 +806,13 @@ export function Wquw151Frame387935887() { px="10px" py="16px" > - + 다시 교정하기
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 c46191ac..d22c66a0 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 @@ -1,6 +1,5 @@ --- source: crates/devup-mcp-devup-ui/tests/wquw_151_frames.rs -assertion_line: 218 expression: output.tsx --- import { Box, Center, Flex, Image, Text, VStack } from "@devup-ui/react"; @@ -24,6 +23,8 @@ export function Wquw151Frame387935973() { >
- + 현재 적용된 문체 : - + 담담체 @@ -52,6 +65,8 @@ export function Wquw151Frame387935973() { > - + AI 편집자에게 다듬기 맡기기 @@ -82,7 +103,13 @@ export function Wquw151Frame387935973() { boxSize="20px" overflow="hidden" /> - + 내 문장 최대한 유지하기 @@ -90,10 +117,22 @@ export function Wquw151Frame387935973() {
- + 공개 설정 : - + 나만 보기 - + TIP - + 제목 - + 수정 @@ -220,6 +277,8 @@ export function Wquw151Frame387935973() { - + 1단락 - + 수정 @@ -251,6 +322,8 @@ export function Wquw151Frame387935973() {
- + 2단락 - + 수정 @@ -283,6 +368,8 @@ export function Wquw151Frame387935973() {
“엄마!”

나는 반가운 마음에 큰 소리로 어머니를 불렀다. 어머니는 미역이 담긴 비닐봉지를 고르고 계셨는데, 내 목소리에 깜짝 놀라시더니 고개를 돌려 나를 보셨다. 순간 어머니의 얼굴에 번져가는 웃음이 얼마나 따뜻하고 아름다웠는지 모른다. 놀라움이 가득한 표정에서 환한 미소로 변해가는 그 표정을, 나는 아직도 잊을 수 없다.

“우리{" "} - + [1. 이름] {" "}왔어?”

어머니는 장바구니를 들어 올리며 내 앞까지 걸어오셨다. 얼굴에는 하루의 피로가 묻어 있었지만, 나를 보는 눈빛은 그 어떤 보석보다 반짝였다. 나는 어머니가 드신 장바구니를 대신 들어드리려고 손을 뻗었지만, 어머니는 살짝 웃으며 고개를 저으셨다.

“괜찮아. 엄마가 들 수 있어.{" "} - + [1. 이름] {" "}학교 끝났어? 배고프지?” @@ -310,12 +397,24 @@ export function Wquw151Frame387935973() { py="10px" w="360px" > - + 3단락 - + 수정 @@ -323,6 +422,8 @@ export function Wquw151Frame387935973() {
- + 4단락 - + 수정 @@ -355,6 +468,8 @@ export function Wquw151Frame387935973() {
- + 문체 변경
- + 검토 완료
@@ -414,10 +541,22 @@ export function Wquw151Frame387935973() { - + 공개 설정 - + 작성하신 이야기를 누구와 함께 나눌지 선택해 주세요. @@ -453,11 +592,19 @@ export function Wquw151Frame387935973() { maskSize="contain" /> - + 동행인 - + 나만 보기 - + 교정 설정 - + 이번 이야기 @@ -519,10 +686,22 @@ export function Wquw151Frame387935973() { maskSize="contain" /> - + AI 담당자와 함께 문체 다듬기 - + AI 담당자가 문맥을 자연스럽게 수정해요 @@ -539,10 +718,22 @@ export function Wquw151Frame387935973() { maskSize="contain" /> - + 내 문장 그대로 맞춤법만 다듬기 - + 입력하신 문장 그대로의 느낌을 담아내요 @@ -565,18 +756,27 @@ export function Wquw151Frame387935973() { >
- + 문체를 적용합니다.
- + 취소
@@ -605,7 +811,13 @@ export function Wquw151Frame387935973() { px="10px" py="16px" > - + 적용하기
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 b50df4eb..94a5a431 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 @@ -1,6 +1,5 @@ --- source: crates/devup-mcp-devup-ui/tests/wquw_151_frames.rs -assertion_line: 218 expression: output.tsx --- import { Box, Center, Flex, Image, Text, VStack } from "@devup-ui/react"; @@ -43,7 +42,13 @@ export function Wquw151Frame387936059() { maskRepeat="no-repeat" maskSize="contain" /> - + 초기화 @@ -58,6 +63,8 @@ export function Wquw151Frame387936059() { > - + 이야기가 막힐 때 눌러보세요
- + 직접 입력
- + 녹음하기
- + 작성 완료
- + 임시 저장하고 다음에 이어서 하기
@@ -137,18 +176,27 @@ export function Wquw151Frame387936059() { >
- + 이야기 다듬기 시작
- + AI 담당자와 함께 문체 다듬기 @@ -198,7 +254,13 @@ export function Wquw151Frame387936059() { boxSize="20px" overflow="hidden" /> - + 내 문장 그대로 맞춤법만 다듬기 @@ -208,7 +270,13 @@ export function Wquw151Frame387936059() {
- + 더 이야기 할게요
@@ -219,7 +287,13 @@ export function Wquw151Frame387936059() { px="10px" py="16px" > - + 마무리 할게요
diff --git a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_36108.snap b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_36108.snap index 218c8e40..c6fcc6a9 100644 --- a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_36108.snap +++ b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_36108.snap @@ -1,6 +1,5 @@ --- source: crates/devup-mcp-devup-ui/tests/wquw_151_frames.rs -assertion_line: 218 expression: output.tsx --- import { Box, Center, Flex, Image, Text, VStack } from "@devup-ui/react"; @@ -37,7 +36,13 @@ export function Wquw151Frame387936108() { maskRepeat="no-repeat" maskSize="contain" /> - + 초기화 @@ -52,6 +57,8 @@ export function Wquw151Frame387936108() { > - + 이야기가 막힐 때 눌러보세요
- + 직접 입력
- + 녹음하기
- + 작성 완료
- + 임시 저장하고 다음에 이어서 하기 diff --git a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_36144.snap b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_36144.snap index 192b6ab3..67efd292 100644 --- a/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_36144.snap +++ b/crates/devup-mcp-devup-ui/tests/snapshots/wquw_151_frames__wquw_151_frame_3879_36144.snap @@ -23,6 +23,8 @@ export function Wquw151Frame387936144() { >
- + 공개 설정 : - + 나만 보기 - + TIP - + 제목 - + 수정 @@ -165,6 +197,8 @@ export function Wquw151Frame387936144() { - + 1단락 - + 수정 @@ -196,6 +242,8 @@ export function Wquw151Frame387936144() {
- + 2단락 - + 수정 @@ -228,6 +288,8 @@ export function Wquw151Frame387936144() {
“엄마!”

나는 반가운 마음에 큰 소리로 어머니를 불렀다. 어머니는 미역이 담긴 비닐봉지를 고르고 계셨는데, 내 목소리에 깜짝 놀라시더니 고개를 돌려 나를 보셨다. 순간 어머니의 얼굴에 번져가는 웃음이 얼마나 따뜻하고 아름다웠는지 모른다. 놀라움이 가득한 표정에서 환한 미소로 변해가는 그 표정을, 나는 아직도 잊을 수 없다.

“우리{" "} - + [1. 이름] {" "}왔어?”

어머니는 장바구니를 들어 올리며 내 앞까지 걸어오셨다. 얼굴에는 하루의 피로가 묻어 있었지만, 나를 보는 눈빛은 그 어떤 보석보다 반짝였다. 나는 어머니가 드신 장바구니를 대신 들어드리려고 손을 뻗었지만, 어머니는 살짝 웃으며 고개를 저으셨다.

“괜찮아. 엄마가 들 수 있어.{" "} - + [1. 이름] {" "}학교 끝났어? 배고프지?” @@ -255,12 +317,24 @@ export function Wquw151Frame387936144() { py="10px" w="360px" > - + 3단락 - + 수정 @@ -268,6 +342,8 @@ export function Wquw151Frame387936144() {
- + 4단락 - + 수정 @@ -300,6 +388,8 @@ export function Wquw151Frame387936144() {
- + 교정 설정
- + 검토 완료
diff --git a/crates/devup-mcp-devup-ui/tests/theme.rs b/crates/devup-mcp-devup-ui/tests/theme.rs index 955a1b0e..cc1b1866 100644 --- a/crates/devup-mcp-devup-ui/tests/theme.rs +++ b/crates/devup-mcp-devup-ui/tests/theme.rs @@ -83,7 +83,7 @@ fn maps_variables_modes_aliases_and_styles_to_devup_json() { " \"fontFamily\": \"Pretendard\",\n", " \"fontWeight\": 700,\n", " \"fontSize\": \"32px\",\n", - " \"lineHeight\": 1.3,\n", + " \"lineHeight\": \"42px\",\n", " \"letterSpacing\": \"-0.02em\"\n", " }\n", " },\n", @@ -179,7 +179,7 @@ fn styles_named_for_a_breakpoint_become_one_responsive_entry() { {"fontFamily": "Pretendard", "fontWeight": 800, "fontSize": "$sizeLg", "lineHeight": "48px", "letterSpacing": "0em"} ], // one slot only is the value alone, wherever the slot was - "label": {"fontFamily": "Noto Sans KR", "fontWeight": 500, "fontSize": "13px", "lineHeight": 1.5, "letterSpacing": "-0.04em"} + "label": {"fontFamily": "Noto Sans KR", "fontWeight": 500, "fontSize": "13px", "lineHeight": "20px", "letterSpacing": "-0.04em"} }) ); // A blur is not a shadow; an effect style with only blurs is no entry. diff --git a/crates/devup-mcp-devup-ui/tests/unresolved_token_binding.rs b/crates/devup-mcp-devup-ui/tests/unresolved_token_binding.rs index 5dd669b2..2bffab45 100644 --- a/crates/devup-mcp-devup-ui/tests/unresolved_token_binding.rs +++ b/crates/devup-mcp-devup-ui/tests/unresolved_token_binding.rs @@ -162,8 +162,8 @@ fn a_binding_the_catalog_carried_is_written_as_its_token() { output.tsx ); assert!( - !output.tsx.contains("fontSize=\"14px\""), - "a typography token replaces the font properties it stands for:\n{}", + output.tsx.contains("fontSize=\"14px\"") && output.tsx.contains("lineHeight=\"22px\""), + "resolved size and advance keep the typography token valid for this node:\n{}", output.tsx ); assert!( diff --git a/crates/devup-mcp/src/server/projection.rs b/crates/devup-mcp/src/server/projection.rs index 2985b8a6..019a9641 100644 --- a/crates/devup-mcp/src/server/projection.rs +++ b/crates/devup-mcp/src/server/projection.rs @@ -3316,7 +3316,7 @@ mod w1_regressions { node.node_type = "TEXT".into(); node.fields.extend(serde_json::from_value::>(json!({ "characters":"ab", "textTruncation":"DISABLED", - "styledTextSegments":[{"characters":"a","fontSize":16},{"characters":"b","fontSize":20}] + "styledTextSegments":[{"characters":"a","fontSize":16,"fontWeight":400},{"characters":"b","fontSize":16,"fontWeight":700}] })).unwrap()); let output = generate_component(&data.snapshot, "1:1", &CodegenOptions::default()).unwrap(); assert!( @@ -3336,7 +3336,7 @@ mod w1_regressions { .is_some_and(|issues| issues.iter().any(|d| d["code"] == "DEVUP_CODEGEN_PROPERTY_UNMAPPED" && d["nodeId"] == "1:1" - && d["property"] == "fontSize" + && d["property"] == "fontWeight" && d["fidelityImpact"] == "none")) ); } diff --git a/docs/line-box-displacement.md b/docs/line-box-displacement.md new file mode 100644 index 00000000..9dece4bb --- /dev/null +++ b/docs/line-box-displacement.md @@ -0,0 +1,191 @@ +# Integer advances and the remaining two-pixel displacement + +Measured from base `8ab69cbe42b68c6b8eac23e70a6da087c1153083` on 2026-09-12. +The integer-advance mechanism in `integer-line-advance-verification.md` was +accepted as the starting point. This change also removes the separate, +width-independent displacement before the benchmark section. + +## Root cause and browser evidence + +The Star control contains a 44px child (24px icon plus 10px padding on each +side). Its outer auto-layout frame has zero padding and a 1px inside stroke. +Figma gives both frames a height of 44px. The generated outer flex container +had an automatic height of **46px**, even with `box-sizing: border-box`. +Border-box includes borders in a specified height; it does not subtract +borders from an automatic height around intrinsically sized children. + +Existing `push_padding()` already subtracts inside stroke width from padding, +but clamps the result at zero. That cannot absorb a border when padding is +less than stroke width. The sibling Sponsor control stretches to the taller +Star control, making the whole row two pixels too tall. + +The reset was checked once with the requested script: five stylesheets, +body margin 0px, root font size 16px, root line height 24px. Browser measurements +also confirmed border-box on the measured elements. All containers between +the subtitle and benchmarks use flex layout; no unintended inherited line +box was found. Each screen was measured using its own acquired theme; the +reset script alone otherwise uses the last theme built by the harness. + +| Width | Baseline benchmark Y | Browser-only outer-border removal | Change | Candidate Y | Design Y | +| --- | ---: | ---: | ---: | ---: | ---: | +| 360 | 893.125 | 891.125 | -2 | 888 | 888 | +| 992 | 940.75 | 938.75 | -2 | 940 | 940 | +| 1920 | 900.75 | 898.75 | -2 | 900 | 900 | + +The border probe changed just the Star outer container's border width in +the browser, without changing typography or the reset. On the candidate, +the same probe changes benchmark Y by zero: the stroke is already painted +without taking layout space. The prior report's pixel-only values were +890 / 942 / 902; this fixes that precise residual. + +## Emission and token validity + +Both percentage emitters now write `round(fontSize * percent / 100)` as a +pixel length. AUTO and pixel-valued conversion branches retain their existing +semantics. Each responsive theme slot computes its own advance from its own +font size. Node and rich-text callers supply resolved numeric font sizes. + +Codegen still receives token names without style metrics. It therefore emits +resolved `fontSize` and `lineHeight` together even when it emits a typography +token. A reused token at a different source size gets that size's own advance; +it cannot silently retain the token's advance. This also preserves explicit +AUTO or pixel line-height overrides. A token-only source with no explicit +metrics continues to use the token's paired font size and advance. + +If a styled source supplies a size but no usable line-height metrics, codegen +refuses with `DevupCodegenFailed`. A percentage advance with a missing numeric +size or a bound font-size variable is likewise refused. Theme generation +refuses such percentage styles with `DevupThemeConflict`. These cases require +mode-aware metrics that the name-only map cannot represent; no guessed pixel +advance or partially generated theme is returned. Consumers must also update +line height if they subsequently override a generated token's font size by hand. + +For an inside uniform stroke on a horizontal or vertical auto-layout node, +if any collected padding is smaller than the stroke width, codegen paints +the stroke with `outline` and `outlineOffset = -strokeWeight`. Padding is then +left intact. Otherwise the existing border/padding treatment stays in use. +This decision reads stroke, padding and layout fields, never viewport width, +frame identity, current font scale, or a landing-specific correction. + +Outline properties map to their stroke source fields. New rich-text metric +attributes map to the owning `styledTextSegments`; the source-map test catches +the previously unmapped nested advance. Fidelity assertions and corpus +consistency assertions were not weakened. + +## Tests first + +Captured logs are in the main harness's ignored `out/` directory. + +| Log | RED result before implementation | +| --- | --- | +| `w13-red.log` | 0 passed, 2 failed: theme returned 1.3 instead of 49px; inline text returned 1.3 instead of 68px | +| `w13-geometry-red.log` | 0 passed, 3 failed: zero-padding wrapper emitted a border | +| `w13-token-red.log` | 3 passed, 3 failed: reused-token metrics absent and unsafe cases accepted | +| `w13-provenance-red.log` | 6 passed, 1 failed: nested 23px advance lacked segment provenance | + +All seven regression tests subsequently passed. Handbuilt typography and +provenance test inputs that supplied a size but omitted line height now state +AUTO explicitly, keeping those tests about their original concerns while +meeting the new representation contract. + +The server's strict mapping-gap regression previously used nested font size +as its unmapped input. Since that metric is now mapped, the input now varies +nested font weight instead, retaining the same strict rejection and diagnostic +assertions for an actual remaining mapping gap. + +## Reviewed plugin goldens + +Exactly 24 of 268 plugin goldens change. The following table enumerates every +one under `fixtures/devup-figma-plugin/snapshots/codegen/`; each was compared +with its corresponding collected fixture. Only these 24 snapshot checksums +were updated in the manifest. No fixture inputs or consistency test changed. +Percentages below are readable abbreviations of the captured floating-point +values, and the emitter uses the original value before rounding the advance. + +| Golden stem | Reviewed reason | +| --- | --- | +| upstream-codegen-109-e2824ad5be | Token safety: explicitly emit the fixture's 16px size and PIXELS 1.5 advance as a pair; 1.5px is already the source's pixel value, not a ratio conversion | +| upstream-codegen-161-5b230821f2 | 16px at 160% becomes 26px | +| upstream-codegen-162-51e1254b4c | 16px at 160% becomes 26px | +| upstream-codegen-163-2a9f9a056a | 16px at 160% becomes 26px | +| upstream-codegen-164-33e4c6591c | 18px at 160% becomes 29px | +| upstream-codegen-165-501ee1df12 | 20px at 140% becomes 28px; 16px at 160% becomes 26px | +| upstream-codegen-166-cda14f0b08 | Three 36px card labels at 160% become 58px | +| upstream-codegen-184-0c09788bb7 | 16px at 160% becomes 26px | +| upstream-codegen-185-e79c2ae189 | 16px at 160% becomes 26px | +| upstream-codegen-186-0988e78887 | 16px at 160% becomes 26px | +| upstream-codegen-191-2021d398f8 | 12px/160%, 20px/120%, 14px/160% become 19px, 24px, 22px | +| upstream-codegen-213-aa53408d2f | 16px/150% and 15px/150% become 24px and 23px | +| upstream-codegen-222-f2d780c847 | 64px at 140% becomes 90px | +| upstream-codegen-223-4dc8092992 | 20px/150%, 16px/150%, 15px/150% become 30px, 24px, 23px | +| upstream-codegen-243-14366a47ff | 36px at 130% becomes 47px | +| upstream-codegen-245-537356bcf4 | 40px/140% becomes 56px; the 42px rich segment explicitly gets 59px rather than inheriting 56px | +| upstream-codegen-246-295f39e09b | 64px at 140% becomes 90px | +| upstream-codegen-247-39d6959685 | 64px at 140% becomes 90px; text stroke and shadows unchanged | +| upstream-codegen-248-d1bc18e068 | 15px at 200% becomes 30px; list semantics unchanged | +| upstream-codegen-249-6b8a42981d | 16px at 140% becomes 22px | +| upstream-codegen-251-50acddfb96 | 20px/200%, 16px/180%, 17px/170% become 40px, 29px, 29px | +| upstream-codegen-252-49e979239e | 20px/150%, 35px/150%, 17px/200%, 15px/170%, 16px/170% become 30px, 53px, 34px, 26px, 27px | +| upstream-codegen-253-347a2e5fb2 | 28px/150%, 16px/180%, 24px/170% become 42px, 29px, 41px | +| upstream-codegen-254-9f340b6a10 | Frame 638 has horizontal auto-layout, zero padding and a 1px inside stroke: border becomes inward outline, the same geometry defect as Star | + +The WQUW proofread standalone/embedded TSX and ten frame snapshots also gain +resolved metric pairs and the applicable inward strokes. The proofread source +map gains mappings for those actual attributes; diagnostic embedded source +excerpts change accordingly. No new unmapped-advance diagnostic is accepted. +Snapshot tooling removes stale `assertion_line` headers where it rewrites files. + +## Visual identity and final gate + +All acquisition builds use this worktree's own executable: + +`C:/Users/owjs3/orca/workspaces/devup-mcp/W13-line-box-displacement/target/debug/devup-mcp.exe` + +| Build | SHA-256 | +| --- | --- | +| Baseline, production at 8ab69cb | EC8407214626EB5D635548BB104ECAF8E30F55322AFA35C1A4A6F84B3D1915DA | +| First measured candidate | 460E421A73B90A3C2F6D7A7BB12147BC8F7F2AD73E109FEB4FD2ED9FCBFAA1C6 | +| Final candidate with token refusal and segment provenance | 3E35923B557CD7AC045F4991CB732B3D8F939596B19BC877931ED33795240427 | + +Baseline and final candidate were each acquired in the main checkout's +`harness/render` with `DEVUP_MCP_BIN` set to that path, then rendered with the +unchanged `node scripts/render.mjs`. No harness Python script was edited. + +| Screen | Width | Baseline binary | First candidate | Final candidate | +| --- | ---: | ---: | ---: | ---: | +| landing-833-3640 | 360 | 11.41% | 7.75% | 4.99% | +| landing-833-3322 | 992 | 3.10% | 2.47% | 2.47% | +| landing-832-2975 | 1920 | 1.84% | 1.50% | 1.50% | + +The final browser measurement confirms benchmark Y remains exactly +888 / 940 / 900 after adding token-safety emission. Final rendered page sizes +are 360x2955, 992x2964 and 1920x3084. Only these three measured thresholds +were tightened, to 4.99 / 2.47 / 1.50. Other screens remain unmeasured by this +task. Font rasterization differences remain; no glyph-position correction was +introduced. + +| Width | Heading Y / height / advance | Subtitle Y / height / advance | Benchmark height / advance | +| --- | --- | --- | --- | +| 360 | 90 / 245 / 49px | 359 / 69 / 23px | 36 / 36px | +| 992 | 250 / 272 / 68px | 546 / 62 / 31px | 47 / 47px | +| 1920 | 200 / 272 / 68px | 496 / 62 / 31px | 47 / 47px | + +Every Cargo command uses `CARGO_PROFILE_DEV_DEBUG=0`, +`CARGO_PROFILE_TEST_DEBUG=0`, `CARGO_INCREMENTAL=0`, and two jobs; no +`CARGO_TARGET_DIR` was set. + +| Final gate | Result | +| --- | --- | +| `cargo fmt --all -- --check` | Pass | +| `cargo clippy --locked --workspace --all-targets --all-features -j 2 -- -D warnings` | Pass, zero warnings | +| `cargo test --workspace -j 2 --no-fail-fast` | 1056 passed, 0 failed, 2 ignored | +| `cargo insta test --workspace --all-features --check` | Pass; 1056 passed, 0 failed, 2 ignored; no snapshots to review | +| `cargo test --locked -p devup-mcp --test stdio_smoke` | 2 passed, 0 failed | + +Final logs: `w13-workspace-final.log`, `w13-insta.log`, +`w13-clippy-final.log`, and `w13-smoke.log` in the main harness `out/`. +MSVC test linking reports its existing localized library-creation messages; +the clippy gate itself has zero warnings. No forbidden source file or harness +Python script was modified. All temporary measurement scripts are removed; +browser and acquisition child processes were closed. The change is committed +locally without pushing, followed by `cargo clean` as required. diff --git a/fixtures/devup-figma-plugin/manifest.json b/fixtures/devup-figma-plugin/manifest.json index 4abbd1d9..83ac4461 100644 --- a/fixtures/devup-figma-plugin/manifest.json +++ b/fixtures/devup-figma-plugin/manifest.json @@ -1579,7 +1579,7 @@ }, { "path": "snapshots/codegen/upstream-codegen-109-e2824ad5be.snap", - "sha256": "f3df1e13ac6361639dc9538a210f16f81ccafbcd3ab45199c7700b4892d0174f" + "sha256": "ef653c1dff43907bd0735cb13407486bba3596df87ac0bc328c27d67ccb7ddc1" }, { "path": "snapshots/codegen/upstream-codegen-110-f2505cce4b.snap", @@ -1787,27 +1787,27 @@ }, { "path": "snapshots/codegen/upstream-codegen-161-5b230821f2.snap", - "sha256": "bd8d6869ee543fe5765acb31927c5f090473003e6f0d22563951bb201180d5e8" + "sha256": "9d7c6ca792642d02ad7d4fdbdb2f2c48886234af5ef68dc661ce214f4e782555" }, { "path": "snapshots/codegen/upstream-codegen-162-51e1254b4c.snap", - "sha256": "2f3ff0f4f6817e7791581b5c15ba1ff21b8dff64c1e82b657f3c9fa9997a10d1" + "sha256": "2a2f3d03bdcca8b2017274486bd2a41bc965f6fb03137efb36e31503e7140293" }, { "path": "snapshots/codegen/upstream-codegen-163-2a9f9a056a.snap", - "sha256": "7e246fbf50e071ab82a7bfeb4f1f8b637088db23b81e928596bbf700115d324d" + "sha256": "1d7e81bfada5252d932f77c46f398f54ad63caf2d3c343083c4ed8c98641953b" }, { "path": "snapshots/codegen/upstream-codegen-164-33e4c6591c.snap", - "sha256": "4c49a83e62c25bc80c586b49b8c4ee7db5965a7e1f0507ca869b75cbee4e4db8" + "sha256": "eebaa0436df2c1bd77b33ae594c7e933874a87bd5167f438eb049ec22dfb35e6" }, { "path": "snapshots/codegen/upstream-codegen-165-501ee1df12.snap", - "sha256": "bbd514b21e0c8fd1c30baed210570aef96d21e3a1d21e4780134e653a477acf0" + "sha256": "454f7e3d3e01d6a8d9c3df304534450b58ecedd9b099c98a1f1565ef7b48617c" }, { "path": "snapshots/codegen/upstream-codegen-166-cda14f0b08.snap", - "sha256": "bdc8eedbba50a15d8bb417baf0a60765de1e304e187c4c23c173b9729cdadda6" + "sha256": "b9d7c0159106e942c20b8b1163f10176efe9588eccc21e1f5dc008a5167743d2" }, { "path": "snapshots/codegen/upstream-codegen-167-114a88771f.snap", @@ -1879,15 +1879,15 @@ }, { "path": "snapshots/codegen/upstream-codegen-184-0c09788bb7.snap", - "sha256": "791a2ff594f70f1cfd9ccf8a3a20fb9e35f12124f29fb79f1218e74caff5e5c3" + "sha256": "81e5c047cf5d1260093010177caca6ce53f591d609c78210f2640b499c9a0559" }, { "path": "snapshots/codegen/upstream-codegen-185-e79c2ae189.snap", - "sha256": "955eaadf183c9a6bfe6dcd3baca9690e4a4fd551900744488cd15a456c53cc71" + "sha256": "068446027b2b618ed3c4ffe25ef9b7a002969c2395d374faac12253deb90ef17" }, { "path": "snapshots/codegen/upstream-codegen-186-0988e78887.snap", - "sha256": "b03b52f1c04b5ce92f14923ae8a5eb9ca756aafd9de7e5821d8514ee22bae358" + "sha256": "692fa4519a1358c0bc548a9ee37134532dc528636b221fd0a2d7d0f251f96fec" }, { "path": "snapshots/codegen/upstream-codegen-187-8b30b935a4.snap", @@ -1907,7 +1907,7 @@ }, { "path": "snapshots/codegen/upstream-codegen-191-2021d398f8.snap", - "sha256": "114ccc54bcd28c6ae6bdc0188db43757e1fbfec5c6284a73b5d97c7eb9aa5d2b" + "sha256": "583cc5bfb826a9e7f653ca21f950e484522d9220635d5797ac233dba44f3d227" }, { "path": "snapshots/codegen/upstream-codegen-192-dd2c3f0c1c.snap", @@ -1995,7 +1995,7 @@ }, { "path": "snapshots/codegen/upstream-codegen-213-aa53408d2f.snap", - "sha256": "65b933ca7a214ff585857195d4885a6b0632849e7c6227045a0a4949add4e9eb" + "sha256": "a9d4208072b46676cd2ad4d5320f526fe7b4ae2bb7b9360eed36c13179614070" }, { "path": "snapshots/codegen/upstream-codegen-214-6dc49544c0.snap", @@ -2031,11 +2031,11 @@ }, { "path": "snapshots/codegen/upstream-codegen-222-f2d780c847.snap", - "sha256": "127e0e2def7894eb615c3d0747dc7e50d678412d821d13cec2b9283e1b04cf8b" + "sha256": "aceebdd85489cf7d781d3919d77342b655377971de700d2a3cbbb08f6cb4eb8a" }, { "path": "snapshots/codegen/upstream-codegen-223-4dc8092992.snap", - "sha256": "0d7053d2ebef79732c704f9f82bc6085d26be51bbdd12d9b51b0f8666ab720de" + "sha256": "1930db8d9deea8afe86275e0c99c6426143e2c56536448b1a36f61e1b64dd83c" }, { "path": "snapshots/codegen/upstream-codegen-224-075a41e65c.snap", @@ -2115,7 +2115,7 @@ }, { "path": "snapshots/codegen/upstream-codegen-243-14366a47ff.snap", - "sha256": "5fbba0ac55fed83c64aeeb4ee3984b06683325b8663f1ab33ed1201e2fdbc933" + "sha256": "d9f3b59a8585884d957b690651be81ca7e5bf03ce3d553ac6c95465f9ebe8370" }, { "path": "snapshots/codegen/upstream-codegen-244-4ccea91f13.snap", @@ -2123,23 +2123,23 @@ }, { "path": "snapshots/codegen/upstream-codegen-245-537356bcf4.snap", - "sha256": "1b6c59a5f079a273885a7a5111b72d4de873f8fc09b0b0a4141ccf21f2218475" + "sha256": "29e92ac662b46d796a0c86f8d8b69dbcf0b0eb5e8835a4d05ea57a359cae47dc" }, { "path": "snapshots/codegen/upstream-codegen-246-295f39e09b.snap", - "sha256": "132da3aba8a8a5fef1e243b257dbd49fe4f12dfa402b6764123194f284c5c535" + "sha256": "6ee6e667f7d635bfaf3542b6e8d405178d30fe2fbee618ca8303155c8f01809a" }, { "path": "snapshots/codegen/upstream-codegen-247-39d6959685.snap", - "sha256": "e2a6dad76fd959a0cf0c79ba7fa4d14968bb8a134b349c039b5cb1cdfe4beb3e" + "sha256": "3ff541729986f11bdaa79caa77c109b64844fb3e3c075fa2cfd87136afa54fa4" }, { "path": "snapshots/codegen/upstream-codegen-248-d1bc18e068.snap", - "sha256": "c6668d372d8bd8bcd23b559f1d33a426fae6f5a1e119a06f402d351d40f6b708" + "sha256": "8ff54872fbb3d924fc8ac618bd891e25b9d4a576d38bca9165db186500e66c3b" }, { "path": "snapshots/codegen/upstream-codegen-249-6b8a42981d.snap", - "sha256": "6384095759151e9d7ade571db4134e5f0e6bcbc56fbc564fa9d87f274c0a54ef" + "sha256": "61fd552e6a512a8c457cc9b2b164447ff60d710c4de4d54d23e6f3deb050e592" }, { "path": "snapshots/codegen/upstream-codegen-250-935ccdb80f.snap", @@ -2147,19 +2147,19 @@ }, { "path": "snapshots/codegen/upstream-codegen-251-50acddfb96.snap", - "sha256": "f1666f0f50971bef11ffb5d07367271a3102130d9dd34675ce2a9d48752fdfbe" + "sha256": "c610358b3d7fdc43beccfb47cadd798f38449ce5f9076503e9f7b9a3be48220d" }, { "path": "snapshots/codegen/upstream-codegen-252-49e979239e.snap", - "sha256": "393603262e6db645be8775a0ec2555f6af891606d5b161b3d5a37d0350569269" + "sha256": "a7984418738788b64ccb4cb39cd7bd323f76aac2144feffe86add5d5d49ad401" }, { "path": "snapshots/codegen/upstream-codegen-253-347a2e5fb2.snap", - "sha256": "fc13c44bd00fa699a9212a0c8500f1a696fad6d4930239c5f795931f79a0c35a" + "sha256": "a6444895d6bf34addb63a9a3179e85d449ad5c7c3c044e8a59710974ee7cb0e1" }, { "path": "snapshots/codegen/upstream-codegen-254-9f340b6a10.snap", - "sha256": "d8b418d12b3d4bd9e4fec80cd2b6aae5d55d062309bf39ea08c58c94a76dc951" + "sha256": "fd0f329de2950b50d2896ba50e463a877c8dfc2ba353ae84770082a2586757d5" }, { "path": "snapshots/codegen/upstream-codegen-255-d01d8c7de6.snap", diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-109-e2824ad5be.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-109-e2824ad5be.snap index 9d8cc0c2..e9909802 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-109-e2824ad5be.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-109-e2824ad5be.snap @@ -3,6 +3,12 @@ source: crates/devup-mcp-devup-ui/tests/compat_fixtures.rs expression: actual --- -" +" Heading " diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-161-5b230821f2.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-161-5b230821f2.snap index c1408cb4..d6f06903 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-161-5b230821f2.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-161-5b230821f2.snap @@ -12,7 +12,7 @@ expression: actual fontSize="16px" fontWeight="400" letterSpacing="0em" - lineHeight="1.6" + lineHeight="26px" > Hello World diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-162-51e1254b4c.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-162-51e1254b4c.snap index 14b6226b..aedb9f45 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-162-51e1254b4c.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-162-51e1254b4c.snap @@ -12,7 +12,7 @@ expression: actual fontSize="16px" fontWeight="400" letterSpacing="0em" - lineHeight="1.6" + lineHeight="26px" > Hello World
diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-163-2a9f9a056a.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-163-2a9f9a056a.snap index 4ca2335a..c0859309 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-163-2a9f9a056a.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-163-2a9f9a056a.snap @@ -12,7 +12,7 @@ expression: actual fontSize="16px" fontWeight="400" letterSpacing="0em" - lineHeight="1.6" + lineHeight="26px" > Hello World
diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-164-33e4c6591c.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-164-33e4c6591c.snap index b5247b51..84d7bebd 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-164-33e4c6591c.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-164-33e4c6591c.snap @@ -19,7 +19,7 @@ expression: actual fontSize="18px" fontWeight="700" letterSpacing="-0.04em" - lineHeight="1.6" + lineHeight="29px" wordBreak="keep-all" > 더 자세히 알아보기 diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-165-501ee1df12.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-165-501ee1df12.snap index 90bd3c40..b163f380 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-165-501ee1df12.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-165-501ee1df12.snap @@ -13,7 +13,7 @@ expression: actual fontSize="20px" fontWeight="800" letterSpacing="-0.04em" - lineHeight="1.4" + lineHeight="28px" wordBreak="keep-all" > @@ -27,7 +27,7 @@ expression: actual fontSize="16px" fontWeight="500" letterSpacing="-0.06em" - lineHeight="1.6" + lineHeight="26px" wordBreak="keep-all" > 웹앱팩토리와 Presskit 등 자체 운영 중인
솔루션과의 연계를 통해 프로젝트의 서비스 범위 확장,
운영 효율화, 성장 기회까지 제안드립니다. diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-166-cda14f0b08.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-166-cda14f0b08.snap index 6604e48c..c3304bac 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-166-cda14f0b08.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-166-cda14f0b08.snap @@ -20,7 +20,7 @@ expression: actual fontSize="36px" fontWeight="500" letterSpacing="-0.06em" - lineHeight="1.6" + lineHeight="58px" > CARD 1
@@ -39,7 +39,7 @@ expression: actual fontSize="36px" fontWeight="500" letterSpacing="-0.06em" - lineHeight="1.6" + lineHeight="58px" > CARD 2 @@ -58,7 +58,7 @@ expression: actual fontSize="36px" fontWeight="500" letterSpacing="-0.06em" - lineHeight="1.6" + lineHeight="58px" > CARD 3 diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-184-0c09788bb7.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-184-0c09788bb7.snap index f716c9d4..3b339b01 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-184-0c09788bb7.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-184-0c09788bb7.snap @@ -13,7 +13,7 @@ expression: actual fontSize="16px" fontWeight="400" letterSpacing="0em" - lineHeight="1.6" + lineHeight="26px" overflow="hidden" textOverflow="ellipsis" whiteSpace="nowrap" diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-185-e79c2ae189.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-185-e79c2ae189.snap index bb1b930c..4699a84e 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-185-e79c2ae189.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-185-e79c2ae189.snap @@ -12,7 +12,7 @@ expression: actual fontSize="16px" fontWeight="400" letterSpacing="0em" - lineHeight="1.6" + lineHeight="26px" whiteSpace="nowrap" > Getting into life {"'"}Cause I found that it{"'"}s not so boring Not anymore. diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-186-0988e78887.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-186-0988e78887.snap index 821fb04a..2c25cd35 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-186-0988e78887.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-186-0988e78887.snap @@ -16,7 +16,7 @@ expression: actual fontSize="16px" fontWeight="400" letterSpacing="0em" - lineHeight="1.6" + lineHeight="26px" overflow="hidden" textOverflow="ellipsis" > diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-191-2021d398f8.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-191-2021d398f8.snap index 964e3311..d352f053 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-191-2021d398f8.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-191-2021d398f8.snap @@ -43,7 +43,7 @@ expression: actual fontSize="12px" fontWeight="600" letterSpacing="-0.04em" - lineHeight="1.6" + lineHeight="19px" wordBreak="keep-all" > 지금 시작할 수 있어요 @@ -54,7 +54,7 @@ expression: actual fontSize="20px" fontWeight="700" letterSpacing="-0.04em" - lineHeight="1.2" + lineHeight="24px" wordBreak="keep-all" > 내 삶을 더 잘 이해하는 첫 걸음 @@ -66,7 +66,7 @@ expression: actual fontSize="14px" fontWeight="400" letterSpacing="-0.04em" - lineHeight="1.6" + lineHeight="22px" wordBreak="keep-all" > 자꾸 미뤄졌던 나에 대한 고민,
퍼즐핏에서 구체적인 답을 찾아보세요. @@ -79,7 +79,7 @@ expression: actual fontSize="12px" fontWeight="400" letterSpacing="-0.04em" - lineHeight="1.6" + lineHeight="19px" opacity="0.5" wordBreak="keep-all" > diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-213-aa53408d2f.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-213-aa53408d2f.snap index 4da053d0..b13ad94d 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-213-aa53408d2f.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-213-aa53408d2f.snap @@ -19,7 +19,7 @@ expression: actual fontSize="16px" fontWeight="600" letterSpacing="-1px" - lineHeight="1.5" + lineHeight="24px" wordBreak="keep-all" > 플레이 제목 @@ -37,7 +37,7 @@ expression: actual fontSize="15px" fontWeight="400" letterSpacing="-0.5px" - lineHeight="1.5" + lineHeight="23px" > 2025.03.03 18:00 @@ -64,7 +64,7 @@ expression: actual fontSize="15px" fontWeight="600" letterSpacing="-0.5px" - lineHeight="1.5" + lineHeight="23px" > 2 @@ -76,7 +76,7 @@ expression: actual fontSize="16px" fontWeight="400" letterSpacing="-1px" - lineHeight="1.5" + lineHeight="24px" wordBreak="keep-all" > 댓글 내용 출력 댓글 내용 출력 댓글 내용 출력 댓글 내용 출력 댓글 내용 출력 diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-222-f2d780c847.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-222-f2d780c847.snap index 3b351e28..38ee9f1d 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-222-f2d780c847.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-222-f2d780c847.snap @@ -11,7 +11,7 @@ expression: actual fontSize="64px" fontWeight="400" letterSpacing="-0.05em" - lineHeight="1.4" + lineHeight="90px" mixBlendMode="darken" wordBreak="keep-all" > diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-223-4dc8092992.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-223-4dc8092992.snap index 3963cf4f..798dc1a0 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-223-4dc8092992.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-223-4dc8092992.snap @@ -19,7 +19,7 @@ expression: actual fontSize="20px" fontWeight="700" letterSpacing="-1px" - lineHeight="1.5" + lineHeight="30px" w="100%" wordBreak="keep-all" > @@ -40,7 +40,7 @@ expression: actual fontSize="16px" fontWeight="600" letterSpacing="-1px" - lineHeight="1.5" + lineHeight="24px" wordBreak="keep-all" > 카카오 @@ -51,7 +51,7 @@ expression: actual fontSize="15px" fontWeight="400" letterSpacing="-0.5px" - lineHeight="1.5" + lineHeight="23px" wordBreak="keep-all" > 연결 전 @@ -90,7 +90,7 @@ expression: actual fontSize="16px" fontWeight="600" letterSpacing="-1px" - lineHeight="1.5" + lineHeight="24px" wordBreak="keep-all" > 구글 @@ -101,7 +101,7 @@ expression: actual fontSize="15px" fontWeight="400" letterSpacing="-0.5px" - lineHeight="1.5" + lineHeight="23px" > cooolvita@gmail.com @@ -142,7 +142,7 @@ expression: actual fontSize="16px" fontWeight="600" letterSpacing="-1px" - lineHeight="1.5" + lineHeight="24px" wordBreak="keep-all" > 서비스 이용약관 @@ -171,7 +171,7 @@ expression: actual fontSize="16px" fontWeight="600" letterSpacing="-1px" - lineHeight="1.5" + lineHeight="24px" wordBreak="keep-all" > 개인정보 처리방침 @@ -200,7 +200,7 @@ expression: actual fontSize="16px" fontWeight="600" letterSpacing="-1px" - lineHeight="1.5" + lineHeight="24px" wordBreak="keep-all" > 회원 탈퇴 diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-243-14366a47ff.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-243-14366a47ff.snap index d8e80133..ed13c7b3 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-243-14366a47ff.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-243-14366a47ff.snap @@ -13,7 +13,7 @@ expression: actual fontSize="36px" fontWeight="700" letterSpacing="-0.03em" - lineHeight="1.3" + lineHeight="47px" > 16.8s diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-245-537356bcf4.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-245-537356bcf4.snap index 4a94767c..635cb6ef 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-245-537356bcf4.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-245-537356bcf4.snap @@ -11,10 +11,10 @@ expression: actual fontSize="40px" fontWeight="500" letterSpacing="-0.05em" - lineHeight="1.4" + lineHeight="56px" wordBreak="keep-all" > - + 온글 과 함께라면 diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-246-295f39e09b.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-246-295f39e09b.snap index 2aa558df..2b5e1962 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-246-295f39e09b.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-246-295f39e09b.snap @@ -13,7 +13,7 @@ expression: actual fontSize="64px" fontWeight="400" letterSpacing="-0.05em" - lineHeight="1.4" + lineHeight="90px" wordBreak="keep-all" > 데브파이브 diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-247-39d6959685.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-247-39d6959685.snap index dfa286dc..b4a61b73 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-247-39d6959685.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-247-39d6959685.snap @@ -12,7 +12,7 @@ expression: actual fontSize="64px" fontWeight="400" letterSpacing="-0.05em" - lineHeight="1.4" + lineHeight="90px" paintOrder="stroke fill" textShadow="0 0 12px #7C70FF, 0 10px 24px #FFC9FE66" wordBreak="keep-all" diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-248-d1bc18e068.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-248-d1bc18e068.snap index c5a07557..16781bf2 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-248-d1bc18e068.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-248-d1bc18e068.snap @@ -12,7 +12,7 @@ expression: actual fontSize="15px" fontWeight="400" letterSpacing="-0.04em" - lineHeight="2" + lineHeight="30px" my="0px" pl="1.5em" wordBreak="keep-all" diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-249-6b8a42981d.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-249-6b8a42981d.snap index 0ef3e585..0747f284 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-249-6b8a42981d.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-249-6b8a42981d.snap @@ -32,7 +32,7 @@ expression: actual fontSize="16px" fontWeight="700" letterSpacing="-0.04em" - lineHeight="1.4" + lineHeight="22px" wordBreak="keep-all" > 자세히 알아보기 → diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-251-50acddfb96.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-251-50acddfb96.snap index ea7abbad..3049ce6d 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-251-50acddfb96.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-251-50acddfb96.snap @@ -71,7 +71,7 @@ expression: actual fontSize="20px" fontWeight="400" letterSpacing="-0.03em" - lineHeight="2" + lineHeight="40px" textAlign="center" wordBreak="keep-all" > @@ -83,7 +83,7 @@ expression: actual fontSize="16px" fontWeight="400" letterSpacing="-0.02em" - lineHeight="1.8" + lineHeight="29px" textAlign="center" > We are currently updating our website to serve you better.
We apologize for the inconvenience. @@ -97,7 +97,7 @@ expression: actual fontSize="17px" fontWeight="700" letterSpacing="-0.02em" - lineHeight="1.7" + lineHeight="29px" wordBreak="keep-all" > 제주국제관악제 최신 소식 확인하기 @@ -121,7 +121,7 @@ expression: actual fontSize="17px" fontWeight="500" letterSpacing="-0.02em" - lineHeight="1.7" + lineHeight="29px" wordBreak="keep-all" > 문의 : 064-722-8704 / bandfestival@hanmail.net diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-252-49e979239e.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-252-49e979239e.snap index 8e575b87..21d2c733 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-252-49e979239e.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-252-49e979239e.snap @@ -50,7 +50,7 @@ expression: actual fontSize="20px" fontWeight="700" letterSpacing="0.04em" - lineHeight="1.5" + lineHeight="30px" > Under Construction
@@ -60,7 +60,7 @@ expression: actual fontSize="35px" fontWeight="700" letterSpacing="-0.02em" - lineHeight="1.5" + lineHeight="53px" textAlign="center" w="100%" wordBreak="keep-all" @@ -75,7 +75,7 @@ expression: actual fontSize="17px" fontWeight="400" letterSpacing="-0.03em" - lineHeight="2" + lineHeight="34px" textAlign="center" w="100%" wordBreak="keep-all" @@ -88,7 +88,7 @@ expression: actual fontSize="15px" fontWeight="400" letterSpacing="-0.02em" - lineHeight="1.7" + lineHeight="26px" textAlign="center" w="100%" > @@ -103,7 +103,7 @@ expression: actual fontSize="16px" fontWeight="700" letterSpacing="-0.02em" - lineHeight="1.7" + lineHeight="27px" wordBreak="keep-all" > 제주국제관악제 최신 소식 확인하기 @@ -128,7 +128,7 @@ expression: actual fontSize="16px" fontWeight="500" letterSpacing="-0.02em" - lineHeight="1.7" + lineHeight="27px" textAlign="center" wordBreak="keep-all" > diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-253-347a2e5fb2.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-253-347a2e5fb2.snap index e09778db..17c16b9f 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-253-347a2e5fb2.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-253-347a2e5fb2.snap @@ -59,7 +59,7 @@ expression: actual fontSize="28px" fontWeight="700" letterSpacing="-0.03em" - lineHeight="1.5" + lineHeight="42px" wordBreak="keep-all" > “ 섬, 그 바람의 울림! ”
희망찬 봄을 여는 관악의 울림이 더
널리,
더 멀리 퍼져나가기를 기원합니다.
@@ -70,7 +70,7 @@ expression: actual fontSize="16px" fontWeight="400" letterSpacing="-0.02em" - lineHeight="1.8" + lineHeight="29px" wordBreak="keep-all" > 음악과 자연이 함께 어우러지는 제주국제관악제가 30주년을 맞게 되었습니다.

제주국제관악제를 아껴주시는 모든 분들과 함께 세계 속의 제주관악의 역사를 써 왔음에 깊이 감사드립니다.
그동안 힘들고 어려운 순간들 속에서도 관악의 힘찬 울림은 우리에게 큰 위로와 보람을 안겨주었습니다.
새로운 세대를 여는 앞으로의 시간과 함께 켜켜이 쌓여가는 역사 속에서, 세계를 향한 우리의 각오와 노력이 더해질 것입니다.

희망찬 봄을 여는 관악의 울림이 더 널리, 더 멀리 퍼져나가기를 기원합니다.
감사합니다. @@ -82,7 +82,7 @@ expression: actual fontSize="16px" fontWeight="400" letterSpacing="-0.02em" - lineHeight="1.8" + lineHeight="29px" wordBreak="keep-all" > 제주국제관악제조직위원장
@@ -93,7 +93,7 @@ expression: actual fontSize="24px" fontWeight="500" letterSpacing="0.1em" - lineHeight="1.7" + lineHeight="41px" wordBreak="keep-all" > 양승보 diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-254-9f340b6a10.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-254-9f340b6a10.snap index 3adbf921..b883e4fd 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-254-9f340b6a10.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-254-9f340b6a10.snap @@ -23,7 +23,7 @@ expression: actual
- + diff --git a/harness/render/thresholds.json b/harness/render/thresholds.json index 646ccf05..8867733c 100644 --- a/harness/render/thresholds.json +++ b/harness/render/thresholds.json @@ -17,8 +17,8 @@ "about-422-3376": 11.25, "about-422-3180": 6.93, "about-422-2987": 4.42, - "landing-833-3640": 11.41, - "landing-833-3322": 3.1, - "landing-832-2975": 1.84 + "landing-833-3640": 4.99, + "landing-833-3322": 2.47, + "landing-832-2975": 1.5 } } From 24bdd9c35efa94da1f2a238cc03ded4eef0c3fde Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Sat, 12 Sep 2026 23:52:36 +0900 Subject: [PATCH 2/2] chore: record the line box displacement fix as a minor changepack --- .changepacks/changepack_log_line_box_displacement.json | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .changepacks/changepack_log_line_box_displacement.json diff --git a/.changepacks/changepack_log_line_box_displacement.json b/.changepacks/changepack_log_line_box_displacement.json new file mode 100644 index 00000000..15892c06 --- /dev/null +++ b/.changepacks/changepack_log_line_box_displacement.json @@ -0,0 +1,8 @@ +{ + "changes": { + "crates/devup-mcp-devup-ui/Cargo.toml": "Minor", + "crates/devup-mcp/Cargo.toml": "Minor" + }, + "note": "Make the generated screen stand where Figma draws it. Figma advances each line by round(fontSize * ratio) and the browser accumulates the fraction instead, so a percentage line height drifted a little on every line and compounded through hugging parents; the emitted advance is now the whole-pixel length rather than a ratio, and all seven measured text blocks match their design height exactly. Because a pixel advance is only valid for the size it was computed from, the resolved font size now travels with it, and a percentage line height whose font size is missing or bound to a mode-dependent variable is refused with a typed error naming the node or text style rather than emitted as a number that would be wrong at another size. The second half of the divergence was an inside stroke: Figma's inside stroke consumes no layout space, and the existing padding compensation cannot absorb a CSS border when the design's own padding is narrower than the stroke, so every bordered auto-layout container grew by twice its stroke weight and pushed the sections below it down. That case now paints inward with an outline at a negative offset, taking no layout space at all, and the padding compensation stands down for it; the choice keys on stroke alignment, layout mode, and stroke weight against padding, never on a viewport width, so it survives a design redrawn at another size. Source map and provenance follow the new emission: outline and outlineOffset carry their stroke provenance, and a rich-text wrapper's own advance is attributed to the segment whose text follows it. Twenty-four plugin goldens change, each enumerated with its reason in docs/line-box-displacement.md; the corpus consistency test and the manifest checksums are unchanged in force. Measured on the landing screen against Figma's own PNG, the three widths move from 11.41 / 3.10 / 1.84 percent to 4.99 / 2.47 / 1.50 percent, and the rendered height now equals the design height at all three.", + "date": "2026-09-12T23:55:00+09:00" +}