cardbuddy 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/dist/preview-editor/assets/index-BkbBxDTT.css +1 -0
  2. package/dist/preview-editor/assets/{index-CIIfMlwI.js → index-Bro07DTa.js} +91 -88
  3. package/dist/preview-editor/index.html +2 -2
  4. package/dist/vendored/card-editor/runtime/adapter.js +2 -2
  5. package/dist/vendored/mcp/client.mjs +1 -1
  6. package/dist/vendored/server/core/ai-chat.js +251 -80
  7. package/dist/vendored/server/core/cardValidate.js +16 -5
  8. package/dist/vendored/skills/card-skill-trainer/SKILL.md +61 -6
  9. package/dist/vendored/skills/card-skill-trainer/scripts/diagnose_case.mjs +14 -82
  10. package/dist/vendored/skills/card-skill-trainer/scripts/lib/diagnosis.mjs +207 -0
  11. package/dist/vendored/skills/card-skill-trainer/scripts/lib/layout-correction-sim.mjs +62 -0
  12. package/dist/vendored/skills/card-skill-trainer/scripts/lib/paradigm-router.mjs +76 -0
  13. package/dist/vendored/skills/card-skill-trainer/scripts/lib/pixel-worker-runner.mjs +31 -0
  14. package/dist/vendored/skills/card-skill-trainer/scripts/lib/pixel-worker.mjs +138 -0
  15. package/dist/vendored/skills/card-skill-trainer/scripts/lib/scoring.mjs +24 -20
  16. package/dist/vendored/skills/card-skill-trainer/scripts/lib/telemetry.mjs +46 -0
  17. package/dist/vendored/skills/card-skill-trainer/scripts/plan_next_round.mjs +4 -1
  18. package/dist/vendored/skills/card-skill-trainer/scripts/run_round.mjs +279 -90
  19. package/dist/vendored/skills/card-skill-trainer/scripts/score_round.mjs +74 -2
  20. package/dist/vendored/skills/card-skill-trainer/scripts/sink_rules.mjs +127 -0
  21. package/dist/vendored/skills/dsl-card-generator/SKILL.md +93 -27
  22. package/dist/vendored/skills/dsl-card-generator/references/dsl_structure.md +1 -15
  23. package/dist/vendored/skills/dsl-card-generator/references/expression_engine.md +11 -20
  24. package/dist/vendored/skills/dsl-card-generator/references/generation_pitfalls.md +169 -44
  25. package/dist/vendored/skills/dsl-card-generator/references/ir-format.md +87 -3
  26. package/dist/vendored/skills/dsl-card-generator/references/iteration_protocol.md +35 -6
  27. package/dist/vendored/skills/dsl-card-generator/references/quickref.md +28 -27
  28. package/dist/vendored/skills/dsl-card-generator/references/recipes.md +30 -8
  29. package/dist/vendored/skills/dsl-card-generator/references/rules.md +3 -2
  30. package/dist/vendored/skills/dsl-card-generator/references/scripts_reference.md +5 -2
  31. package/dist/vendored/skills/dsl-card-generator/references/slices_convention.md +2 -3
  32. package/dist/vendored/skills/dsl-card-generator/references/spec_to_schema_patterns.md +1 -4
  33. package/dist/vendored/skills/dsl-card-generator/scripts/add_component.mjs +5 -14
  34. package/dist/vendored/skills/dsl-card-generator/scripts/autofix_schema.py +88 -0
  35. package/dist/vendored/skills/dsl-card-generator/scripts/check_fidelity.mjs +143 -42
  36. package/dist/vendored/skills/dsl-card-generator/scripts/check_topology.py +0 -35
  37. package/dist/vendored/skills/dsl-card-generator/scripts/expand_ir.mjs +99 -53
  38. package/dist/vendored/skills/dsl-card-generator/scripts/gate.mjs +5 -2
  39. package/dist/vendored/skills/dsl-card-generator/scripts/iterate.mjs +388 -29
  40. package/dist/vendored/skills/dsl-card-generator/scripts/lib/component_contract.mjs +315 -0
  41. package/dist/vendored/skills/dsl-card-generator/scripts/lib/layout-tokens.mjs +1 -1
  42. package/dist/vendored/skills/dsl-card-generator/scripts/lib/paths.mjs +23 -0
  43. package/dist/vendored/skills/dsl-card-generator/scripts/lib/paths.py +0 -37
  44. package/dist/vendored/skills/dsl-card-generator/scripts/lib/python.mjs +99 -0
  45. package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/adaptive-width.mjs +105 -0
  46. package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/bindings.mjs +260 -0
  47. package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/expr.mjs +52 -0
  48. package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/image.mjs +96 -0
  49. package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/registry.mjs +166 -0
  50. package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/structure.mjs +127 -0
  51. package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/walk.mjs +61 -0
  52. package/dist/vendored/skills/dsl-card-generator/scripts/lib/scope_alias.mjs +125 -0
  53. package/dist/vendored/skills/dsl-card-generator/scripts/lib/v3_assemble.mjs +325 -41
  54. package/dist/vendored/skills/dsl-card-generator/scripts/lib/v3_extract.mjs +112 -6
  55. package/dist/vendored/skills/dsl-card-generator/scripts/lib/v3_shared.mjs +0 -6
  56. package/dist/vendored/skills/dsl-card-generator/scripts/lint_card.mjs +119 -78
  57. package/dist/vendored/skills/dsl-card-generator/scripts/preflight.mjs +48 -50
  58. package/dist/vendored/skills/dsl-card-generator/scripts/readiness.mjs +634 -0
  59. package/dist/vendored/skills/dsl-card-generator/scripts/register_card_template.mjs +5 -8
  60. package/dist/vendored/skills/dsl-card-generator/scripts/route_input.mjs +5 -10
  61. package/dist/vendored/skills/dsl-card-generator/scripts/spec_audit.mjs +122 -8
  62. package/dist/vendored/skills/dsl-card-generator/scripts/v3_regression.mjs +3 -1
  63. package/dist/vendored/skills/dsl-card-generator/scripts/validate_schema.py +23 -15
  64. package/dist/vendored/skills/shared/exit-codes.md +79 -0
  65. package/dist/vendored/skills/shared/spec-contract.md +118 -0
  66. package/dist/vendored/skills/sketch-spec-to-card/SKILL.md +26 -9
  67. package/dist/vendored/skills/sketch-spec-to-card/references/spec-schema.md +3 -3
  68. package/dist/vendored/skills/sketch-spec-to-card/scripts/build_prompt.py +0 -8
  69. package/dist/vendored/skills/sketch-spec-to-card/scripts/cluster_states.py +14 -25
  70. package/dist/vendored/skills/sketch-spec-to-card/scripts/parse_meaxure.py +91 -47
  71. package/dist/vendored/skills/sketch-spec-to-card/scripts/png_utils.py +0 -5
  72. package/dist/vendored/skills/sketch-spec-to-card/scripts/test_text_only_artboard.py +75 -15
  73. package/dist/vendored/skills/sketch-spec-to-card/scripts/vision.py +40 -34
  74. package/package.json +1 -1
  75. package/dist/preview-editor/assets/index-DQn0zQBP.css +0 -1
  76. package/dist/vendored/skills/card-skill-trainer/scripts/test_missing_slice.py +0 -122
  77. package/dist/vendored/skills/dsl-card-generator/scripts/analyze_component_usage.mjs +0 -153
@@ -626,6 +626,12 @@ function buildSchemaFromSemantics(sem, id, name, opts = {}) {
626
626
  }
627
627
  }
628
628
  for (const button of sem.buttons || []) addSemanticBound(button.y, button.y + (button.height || 0))
629
+ if (sem.vertexClusters?.length) {
630
+ for (const vc of sem.vertexClusters) {
631
+ const bottom = vc.y + (vc.height || 30) + 35
632
+ addSemanticBound(vc.y, bottom, true)
633
+ }
634
+ }
629
635
  for (const divider of sem.dividers || []) addSemanticBound(divider.y, divider.y + divider.height)
630
636
  for (const ul of sem.titleUnderlines || []) addSemanticBound(ul.y, ul.y + ul.height)
631
637
  if (sem.topDecoration) {
@@ -1234,11 +1240,21 @@ function buildSchemaFromSemantics(sem, id, name, opts = {}) {
1234
1240
  // 内容是左对齐块(如左起的标题簇),强行 center 会把整组推到中间。
1235
1241
  const isGeometricCenter = Math.abs(leftPadding - rightPadding) <= 24
1236
1242
  const effAlign = aux.align === 'center' && !isGeometricCenter ? 'left' : aux.align
1243
+ // 单 aux 与下方 List 之间的间隙:listTitle 路径有 titleBottomGap 桥接,单 aux 行缺底距,
1244
+ // 会把列表整体顶上 ~gap(本例 12px)。仅当该 aux 是列表前最后一个单 aux 时补底距。
1245
+ const auxBottom = (aux.y || 0) + Math.max(aux.height || 0, (aux.fontSize || 14) * 1.4)
1246
+ const listTop = sem.sampleItems[0]?.rowTop ?? sem.sampleItems[0]?.y ?? Infinity
1247
+ const hasLaterSingleAux = sem.auxTexts.some((a) =>
1248
+ a !== aux && !groupedAuxElements.has(a) && !a._consumed
1249
+ && (a.y || 0) > (aux.y || 0) && (a.y || 0) < listTop)
1250
+ const bottomPadding = (sem.sampleItems.length && !hasLaterSingleAux && listTop > auxBottom)
1251
+ ? Math.max(0, Math.round(listTop - auxBottom))
1252
+ : 0
1237
1253
  const geometryPadding = effAlign === 'center'
1238
- ? `${topPadding} 0 0 0`
1254
+ ? `${topPadding} 0 ${bottomPadding} 0`
1239
1255
  : effAlign === 'right'
1240
- ? `${topPadding} ${rightPadding} 0 0`
1241
- : `${topPadding} 0 0 ${leftPadding}`
1256
+ ? `${topPadding} ${rightPadding} ${bottomPadding} 0`
1257
+ : `${topPadding} 0 ${bottomPadding} ${leftPadding}`
1242
1258
  // 多值文本行(含 人名+手机号 等):按 generic multiToken 还原为「字面量 + 绑定」段,不再有任何「联系人块」专门逻辑
1243
1259
  const mt = sem.bindings.multiToken && sem.bindings.multiToken[aux.text.trim()]
1244
1260
  if (mt) {
@@ -1626,9 +1642,11 @@ function buildSchemaFromSemantics(sem, id, name, opts = {}) {
1626
1642
  // 注意:标签拆进顶带后 group.label 已置空,锚点必须回落到选项组顶(group.y),
1627
1643
  // 否则仍按 labelY 归顶带宿主,整组被带高裁掉。
1628
1644
  const host = hostFor(group.label ? (group.labelY || group.y) : group.y)
1629
- const anchorY = group.label ? (group.labelY || group.y) : group.y
1645
+ const anchorY = (group.label && !group.sideLabel) ? (group.labelY || group.y) : group.y
1630
1646
  const topPadding = semanticTopGap(anchorY) ?? Math.max(0, Math.round(anchorY - previousOptionBottom))
1631
- const leftPadding = Math.max(0, Math.round((group.x || 0) - buildCardLeft))
1647
+ const leftPadding = group.sideLabel && group.label
1648
+
1632
1649
  ? Math.max(0, Math.round((group.label.x || 0) - buildCardLeft))
1650
+
1633
1651
  : Math.max(0, Math.round((group.x || 0) - buildCardLeft))
1634
1652
  const rightPadding = Math.max(0, Math.round(buildCardRight - ((group.x || 0) + (group.width || 0))))
1635
1653
  const groupColumn = container('Column', {
1636
1654
  width: '100%',
@@ -1638,7 +1656,7 @@ function buildSchemaFromSemantics(sem, id, name, opts = {}) {
1638
1656
  gap: T.gap('none'),
1639
1657
  padding: `${topPadding} ${rightPadding} 0 ${leftPadding}`,
1640
1658
  })
1641
- if (group.label) {
1659
+ if (group.label && !group.sideLabel) {
1642
1660
  const labelBottom = Math.max(0, Math.round(group.y - (anchorY + (group.label.height || 0))))
1643
1661
  const labelRow = container('Row', {
1644
1662
  width: '100%', justify: 'flex-start', align: 'center',
@@ -1695,6 +1713,14 @@ function buildSchemaFromSemantics(sem, id, name, opts = {}) {
1695
1713
  continue
1696
1714
  }
1697
1715
  const chipsRow = container('Row', { width: '100%', justify: 'flex-start', align: 'center', gap: T.gap('tight') })
1716
+ let _chipTarget = chipsRow
1717
+ if (group.sideLabel && group.label) {
1718
+ const _lblText = wrapTitleWithUnderline(textFromSpec(group.label), group.label)
1719
+ _chipTarget = container('Row', { justify: 'flex-start', align: 'center', gap: T.gap('tight'), flexShrink: '0' })
1720
+ chipsRow.slots[0].components.push(_lblText)
1721
+ chipsRow.slots[0].components.push(_chipTarget)
1722
+ setStyleChild(chipsRow, 'flex', 'justifyContent', 'space-between')
1723
+ }
1698
1724
  group.members.forEach((member, index) => {
1699
1725
  if (index > 0) {
1700
1726
  const previous = group.members[index - 1]
@@ -1749,7 +1775,7 @@ function buildSchemaFromSemantics(sem, id, name, opts = {}) {
1749
1775
  chip.properties = [constProp('value', member.text)]
1750
1776
  }
1751
1777
  chip.eventactions = [clickAction(`// ${member.text} 点击`)]
1752
- chipsRow.slots[0].components.push(chip)
1778
+ _chipTarget.slots[0].components.push(chip)
1753
1779
  })
1754
1780
  groupColumn.slots[0].components.push(chipsRow)
1755
1781
  insertSemantic(host, groupColumn, anchorY)
@@ -2178,15 +2204,16 @@ function buildSchemaFromSemantics(sem, id, name, opts = {}) {
2178
2204
  const mcRightPad = mcAffordR
2179
2205
  ? Math.max(0, Math.round(buildCardRight - ((mcAffordR.x || 0) + (mcAffordR.width || 0))))
2180
2206
  : 0
2207
+ const mcTopGap = semanticTopGap(sem.midCluster.y) ?? mcPad
2181
2208
  const row = mcAffordEl
2182
2209
  ? container('Row', {
2183
2210
  width: '100%', justify: 'space-between', align: 'center',
2184
- padding: `${mcPad} ${mcRightPad} ${mcPad} ${mcLeftPad}`,
2211
+ padding: `${mcTopGap} ${mcRightPad} ${mcPad} ${mcLeftPad}`,
2185
2212
  })
2186
2213
  : container('Row', {
2187
2214
  width: '100%', justify: mcNearCenter ? 'center' : 'flex-start', align: 'center', gap: T.gap('tight'),
2188
2215
  padding: mcNearCenter ? mcPad
2189
- : `${mcPad} 0 ${inFixedGradientShell(sem.midCluster.y) ? 0 : mcPad} ${mcLeftPad}`,
2216
+ : `${mcTopGap} 0 ${inFixedGradientShell(sem.midCluster.y) ? 0 : mcPad} ${mcLeftPad}`,
2190
2217
  })
2191
2218
  const mcClusterHost = mcAffordEl
2192
2219
  ? container('Row', { justify: 'flex-start', align: 'center', gap: T.gap('none') })
@@ -2572,12 +2599,35 @@ function buildSchemaFromSemantics(sem, id, name, opts = {}) {
2572
2599
  const pillBottom = Math.max(...pills.map((b) => (b.y || 0) + (b.height || 0)))
2573
2600
  const rowKeys = new Set(pills.map((b) => Math.round((b.y || 0) / 8)))
2574
2601
  if (rowKeys.size < 2 || pillBottom - pillTop > 260) return null
2575
- const media = (sem.orphanBlocks || []).find((ob) => {
2602
+ let media = (sem.orphanBlocks || []).find((ob) => {
2576
2603
  const w = ob.width || 0
2577
2604
  const h = ob.height || 0
2578
2605
  return w >= 60 && w <= 200 && Math.abs(w - h) <= 14
2579
2606
  && (ob.y || 0) >= pillTop - 80 && (ob.y || 0) + h <= pillBottom + 80
2580
2607
  })
2608
+ // 纯矢量图形组兜底(雷达/环图等无切图导出的中心图形):isolatedBlocks 先于 orphanSlices
2609
+ // 消费了大块矢量组,散点带若只认 orphanBlocks 会漏检 → 胶囊退化成通用按钮行、金额散落
2610
+ // (位置分 0.57 根因)。同窗口同尺寸几何判据从 isolatedBlocks 认领,凭上游
2611
+ // annotationDefect.cropSlice(画板裁切产物)出真图,不认业务词。
2612
+ if (!media) {
2613
+ const vectorCand = (sem.isolatedBlocks || []).find((bl) => {
2614
+ const r = rect(bl.element)
2615
+ const w = r.width || 0
2616
+ const h = r.height || 0
2617
+ return w >= 60 && w <= 200 && Math.abs(w - h) <= 14
2618
+ && (r.y || 0) >= pillTop - 80 && (r.y || 0) + h <= pillBottom + 80
2619
+ })
2620
+ if (vectorCand) {
2621
+ const vr = rect(vectorCand.element)
2622
+ media = {
2623
+ sliceEl: vectorCand.element,
2624
+ x: vr.x || 0,
2625
+ y: vr.y || 0,
2626
+ width: vr.width || 0,
2627
+ height: vr.height || 0,
2628
+ }
2629
+ }
2630
+ }
2581
2631
  if (!media) return null
2582
2632
  const mL = (media.x || 0) - cardX
2583
2633
  const mR = mL + (media.width || 0)
@@ -2619,6 +2669,8 @@ function buildSchemaFromSemantics(sem, id, name, opts = {}) {
2619
2669
  sem.buttons = (sem.buttons || []).filter((b) => !clusters.some((c) => c.pill === b))
2620
2670
  sem.postAmountTexts = aux.filter((t) => !usedAux.has(t))
2621
2671
  sem.orphanBlocks = (sem.orphanBlocks || []).filter((ob) => ob !== media)
2672
+ // 矢量兜底认领的中心媒体同步从 isolatedBlocks 移除,避免孤立媒体兜底再渲染一遍(同箭头先例)
2673
+ sem.isolatedBlocks = (sem.isolatedBlocks || []).filter((bl) => bl.element !== media.sliceEl)
2622
2674
  const arrowEls = new Set(clusters.map((c) => c.arrow).filter(Boolean))
2623
2675
  if (arrowEls.size) {
2624
2676
  sem.slices = (sem.slices || []).filter((sl) => !arrowEls.has(sl))
@@ -2830,13 +2882,18 @@ function buildSchemaFromSemantics(sem, id, name, opts = {}) {
2830
2882
  : null
2831
2883
  const midAuxHasBlocksAfter = (sem.formRows && sem.formRows.length) || sem.buttons.length || footAux.length > 0
2832
2884
  || (midAuxLastBottom != null && nextSemanticTop(midAuxLastBottom - 1) != null)
2833
- for (let i = 0; i < midAux.length; i++) {
2834
- const pat = midAux[i]
2835
- const host = hostFor(pat.y)
2836
- // 行首图标(勾选/圆点等选项符号):文本左侧同行带内的小切片随行渲染并消费,
2837
- // 否则选项行只剩文字、符号丢失(组件计数与像素都扣)。与 appendInlineTrailSlice 同族反方向。
2885
+ const midGrouped = new Set()
2886
+ // 同行判定(与 auxTexts 分组的 centerNear 同口径):垂直中心差 ≤ 较矮元素高度的 0.75
2887
+ // (大小字号混排行,如 标题+描边pill)。centerNear 原版定义在 auxTexts 分组循环块内,此处不可见。
2888
+ const midCenterNear = (ea, eb) => {
2889
+ const ra = rect(ea), rb = rect(eb)
2890
+ const ca = (ra.y || 0) + (ra.height || 0) / 2
2891
+ const cb = (rb.y || 0) + (rb.height || 0) / 2
2892
+ return Math.abs(ca - cb) <= Math.min(ra.height || 0, rb.height || 0) * 0.75
2893
+ }
2894
+ const leadSliceOf = (pat) => {
2838
2895
  const patH = pat.height || (pat.fontSize || 14) * 1.4
2839
- const leadSliceIdx = (sem.slices || []).findIndex((sl) => {
2896
+ const idx = (sem.slices || []).findIndex((sl) => {
2840
2897
  const sr = rect(sl)
2841
2898
  const w = sr.width || 0
2842
2899
  const h = sr.height || 0
@@ -2844,7 +2901,64 @@ function buildSchemaFromSemantics(sem, id, name, opts = {}) {
2844
2901
  if ((sr.x || 0) + w > (pat.x || 0) - 2 || (sr.x || 0) < (pat.x || 0) - 56) return false
2845
2902
  return Math.abs(((sr.y || 0) + h / 2) - ((pat.y || 0) + patH / 2)) <= Math.max(12, h)
2846
2903
  })
2847
- const leadSlice = leadSliceIdx >= 0 ? sem.slices[leadSliceIdx] : null
2904
+ return idx >= 0 ? sem.slices[idx] : null
2905
+ }
2906
+ for (let i = 0; i < midAux.length; i++) {
2907
+ const pat = midAux[i]
2908
+ if (midGrouped.has(pat)) continue
2909
+ // 同行成组(纯几何判据,与 auxTexts 分组同族):同一 y 带内的多条 postAmountText
2910
+ // (如 状态pill「营业中」+ 距离「距您986m」)必须合成一行,否则每条独立成行会把
2911
+ // 头部撑高一行、下方表单/按钮整体下移一行高(1219 网点预约实测 +16~20px 全卡纵移)。
2912
+ // 组内任一带行首图标或命中枚举统计行时不成组(保持单行路径的图标/绑定处理)。
2913
+ const lineGroup = [pat]
2914
+ for (let j = i + 1; j < midAux.length; j++) {
2915
+ const cand = midAux[j]
2916
+ if (midGrouped.has(cand)) continue
2917
+ if (!(sameRow(cand.sourceEl, pat.sourceEl, 8) || midCenterNear(cand.sourceEl, pat.sourceEl))) break
2918
+ lineGroup.push(cand)
2919
+ }
2920
+ const groupable = lineGroup.length > 1
2921
+ && lineGroup.every((p) => !leadSliceOf(p) && !detectEnumStatStrip(p.text))
2922
+ if (groupable) {
2923
+ const host = hostFor(pat.y)
2924
+ const inTopHost = host !== card
2925
+ const groupIsLast = lineGroup[lineGroup.length - 1] === midAux[midAux.length - 1] && !midAuxHasBlocksAfter
2926
+ const beforeBottom = previousSemanticBottom(pat.y) ??
2927
+ (i > 0 ? lineBottom(midAux[i - 1]) : (mainBottom ?? previousAuxBottom))
2928
+ let pad = (inTopHost ? null : auxRowPadding(pat, i > 0 ? midAux[i - 1] : null, groupIsLast, beforeBottom)) || '6'
2929
+ const groupLeft = Math.min(...lineGroup.map((p) => p.x || buildCardLeft))
2930
+ const parts = String(pad).trim().split(/\s+/)
2931
+ while (parts.length < 4) parts.push('0')
2932
+ parts[3] = String(Math.max(0, Math.round(groupLeft - buildCardLeft)))
2933
+ pad = parts.join(' ')
2934
+ const row = container('Row', { width: '100%', justify: 'flex-start', align: 'center', gap: '0', padding: pad })
2935
+ // 组内逐项 spec 间隙用横向 spacer 承担(与 auxTexts 簇内同法;Row gap 表达不了逐项差异)
2936
+ let prevRight = null
2937
+ for (const item of [...lineGroup].sort((a, b) => (a.x || 0) - (b.x || 0))) {
2938
+ const itemRight = (item.x || 0) + (item.width || 0)
2939
+ if (prevRight != null) {
2940
+ const gapW = Math.max(0, Math.round((item.x || 0) - prevRight))
2941
+ if (gapW > 0) {
2942
+ const spacer = container('Column', { direction: 'column', padding: '0' })
2943
+ ensureStyleGroup(spacer, 'rect')
2944
+ ensureStyleGroup(spacer, 'flex')
2945
+ setStyleChild(spacer, 'rect', 'width', String(gapW))
2946
+ setStyleChild(spacer, 'rect', 'height', '1')
2947
+ setStyleChild(spacer, 'flex', 'flexShrink', '0')
2948
+ row.slots[0].components.push(spacer)
2949
+ }
2950
+ }
2951
+ row.slots[0].components.push(auxContent(item))
2952
+ prevRight = itemRight
2953
+ }
2954
+ insertSemantic(host, row, pat.y)
2955
+ for (const g of lineGroup.slice(1)) midGrouped.add(g)
2956
+ continue
2957
+ }
2958
+ const host = hostFor(pat.y)
2959
+ // 行首图标(勾选/圆点等选项符号):文本左侧同行带内的小切片随行渲染并消费,
2960
+ // 否则选项行只剩文字、符号丢失(组件计数与像素都扣)。与 appendInlineTrailSlice 同族反方向。
2961
+ const leadSlice = leadSliceOf(pat)
2848
2962
  // 渐变容器内的行:间距交给容器的 gap/padding(已在 topDecoration 渲染时统一设置),行自身 padding 归零,
2849
2963
  // 否则会叠加出巨大下边距(如标题行被算成卡片末行 → paddingBottom=卡底-行底=169px)。
2850
2964
  const inTopHost = host !== card
@@ -3139,6 +3253,9 @@ function buildSchemaFromSemantics(sem, id, name, opts = {}) {
3139
3253
  const rowTop = previousFormBottom == null
3140
3254
  ? (semanticTopGap(fr.label.y) ?? T.pad('section'))
3141
3255
  : Math.max(0, Math.round((Number.isFinite(rowVisualTop) ? rowVisualTop : fr.label.y) - previousFormBottom))
3256
+ if (process.env.DEBUG_FORMROW === '1') {
3257
+ console.error(`[formrow] ${fr.label.text} labelY=${fr.label.y} visualTop=${rowVisualTop} prevFormBottom=${previousFormBottom} rowTop=${rowTop} semGap=${previousFormBottom == null ? semanticTopGap(fr.label.y) : '-'}`)
3258
+ }
3142
3259
  // 坐标驱动:左 padding 由 label 的 spec x 坐标推导,右 padding 取与左对称
3143
3260
  const leftIcon = fr.leftIconSlice ? imageOrPlaceholder(fr.leftIconSlice, sem.slicesExports) : null
3144
3261
  const rowLeftPad = Math.max(0, Math.round(((leftIcon ? rect(fr.leftIconSlice).x : fr.label.x) || buildCardLeft) - buildCardLeft))
@@ -3410,6 +3527,14 @@ function buildSchemaFromSemantics(sem, id, name, opts = {}) {
3410
3527
  }
3411
3528
  // Skip dividers that were interleaved into formHost
3412
3529
  sem._interleavedDividers = interleavedDividers
3530
+ if (process.env.DEBUG_FORMROW === '1') {
3531
+ const host = hostFor(sorted[0].label.y)
3532
+ const padOf = (n) => { const g = (n.style?.properties || []).find((p) => p.itemKey === 'padding'); const c = g?.itemChildren?.properties || []; const o = {}; for (const p of c) o[p.itemKey] = p.itemValue; return JSON.stringify(o) }
3533
+ console.error(`[formhost] host=${host === card ? 'card' : 'topContentHost'} children:`)
3534
+ for (const ch of host.slots[0].components) {
3535
+ console.error(` - ${ch.className} y=${semanticYByNode.get(ch)} pad=${padOf(ch)} text=${JSON.stringify((ch.properties || []).find((p) => p.itemKey === 'value')?.itemValue || '').slice(0, 24)}`)
3536
+ }
3537
+ }
3413
3538
  insertSemantic(hostFor(sorted[0].label.y), formHost, sorted[0].label.y)
3414
3539
  }
3415
3540
 
@@ -4097,7 +4222,9 @@ if (subsActive.length && !sem._subAmountsConsumed) {
4097
4222
  // desc 的 spec x 坐标可能与标题左沿不一致(百分比文本缩进在标题右沿之后),
4098
4223
  // 用左 padding 补齐差值使 desc 对齐 spec 位置。
4099
4224
  if (firstItem.descX != null && firstItem.y != null) {
4100
- const _titleX = firstItem.rowLeftEdge || (rect(card).x || 0) + 16
4225
+ // desc 缩进基准应是标题左沿(titleX),而非 rowLeftEdge(带图标时会被拉到图标左沿)。
4226
+ const _titleX = (firstItem.titleX != null ? firstItem.titleX : firstItem.rowLeftEdge)
4227
+ || (rect(card).x || 0) + 16
4101
4228
  const _descIndent = Math.max(0, Math.round((firstItem.descX || 0) - _titleX))
4102
4229
  if (_descIndent >= 4) {
4103
4230
  const _descWrap = container('Row', { width: '100%', justify: 'flex-start', align: 'flex-start', padding: `0 0 0 ${_descIndent}` })
@@ -4757,6 +4884,124 @@ for (const fd of sem.floatDecoBlocks || []) {
4757
4884
  insertSemantic(hostFor(fd.y), band, fd.y)
4758
4885
  }
4759
4886
 
4887
+ // Per-element absolute positioning for chart vertex labels (radar pentagon vertices).
4888
+ // Each text/shape positioned at exact spec coordinate via margin offsets (max IoU).
4889
+ if (sem.vertexClusters?.length) {
4890
+ const vcTopY = Math.min(...sem.vertexClusters.map((vc) => vc.y || 0))
4891
+ const vcBottomY = Math.max(...sem.vertexClusters.map((vc) => (vc.y || 0) + (vc.height || 30) + 35))
4892
+ const vcBandH = Math.max(0, Math.round(vcBottomY - vcTopY))
4893
+ const vcBand = container('AbsoluteLayout', { width: '100%' })
4894
+ ensureStyleGroup(vcBand, 'rect')
4895
+ setStyleChild(vcBand, 'rect', 'height', String(vcBandH))
4896
+ ensureStyleGroup(vcBand, 'other')
4897
+ setStyleChild(vcBand, 'other', 'overflow', 'visible')
4898
+ const cardY = buildCard.y || 0
4899
+ const cardLeft = buildCardLeft
4900
+ const absMargin = (node, x, y) => {
4901
+ ensureStyleGroup(node, 'margin')
4902
+ setStyleChild(node, 'margin', 'marginType', '1')
4903
+ setStyleChild(node, 'margin', 'marginTop', String(Math.max(0, Math.round(y - cardY))))
4904
+ setStyleChild(node, 'margin', 'marginLeft', String(Math.max(0, Math.round(x - cardLeft))))
4905
+ setStyleChild(node, 'margin', 'marginRight', '0')
4906
+ setStyleChild(node, 'margin', 'marginBottom', '0')
4907
+ }
4908
+ // Chart center media (radar/polygon chart): same AbsoluteLayout as pills
4909
+ if (sem.vertexClusters.chartMedia) {
4910
+ const cm = sem.vertexClusters.chartMedia
4911
+ const cmEl = cm.element
4912
+ const cropSlice = cmEl?.annotationDefect?.cropSlice
4913
+ const exportRel = cropSlice || resolveSliceExport(sem.slicesExports || [], cmEl?.name) || null
4914
+ if (exportRel) {
4915
+ const cmImg = imageFromSlice(cmEl, sem.slicesExports) || imageFromSlice({ name: cmEl?.name, annotationDefect: cmEl?.annotationDefect }, sem.slicesExports)
4916
+ if (cmImg) {
4917
+ ensureStyleGroup(cmImg, 'rect')
4918
+ setStyleChild(cmImg, 'rect', 'width', String(Math.round(cm.width || 98)))
4919
+ setStyleChild(cmImg, 'rect', 'height', String(Math.round(cm.height || 98)))
4920
+ absMargin(cmImg, cm.x || 0, cm.y || 0)
4921
+ vcBand.slots[0].components.push(cmImg)
4922
+ }
4923
+ } else {
4924
+ const cmBlock = container('Column', { direction: 'column', gap: T.gap('none'), padding: '0' })
4925
+ ensureStyleGroup(cmBlock, 'rect')
4926
+ setStyleChild(cmBlock, 'rect', 'width', String(Math.round(cm.width || 98)))
4927
+ setStyleChild(cmBlock, 'rect', 'height', String(Math.round(cm.height || 98)))
4928
+ const cmColor = cmEl?.backgroundFallback?.color || cmEl?.style?.fill?.color || '#DCDCDC'
4929
+ ensureStyleGroup(cmBlock, 'background')
4930
+ setStyleChild(cmBlock, 'background', 'backgroundType', '01')
4931
+ setStyleChild(cmBlock, 'background', 'backgroundColor', hexToRgba(cmColor, cmEl?.style?.opacity ?? 1))
4932
+ ensureStyleGroup(cmBlock, 'borderRadius')
4933
+ setStyleChild(cmBlock, 'borderRadius', 'borderRadiusType', '0')
4934
+ setStyleChild(cmBlock, 'borderRadius', 'borderRadius', String(Math.round((cm.width || 98) / 2)))
4935
+ absMargin(cmBlock, cm.x || 0, cm.y || 0)
4936
+ vcBand.slots[0].components.push(cmBlock)
4937
+ }
4938
+ }
4939
+ for (const vc of sem.vertexClusters) {
4940
+ if (vc.pill.fillColor) {
4941
+ const pillBg = container('Column', { direction: 'column', gap: T.gap('none'), padding: '0' })
4942
+ ensureStyleGroup(pillBg, 'rect')
4943
+ setStyleChild(pillBg, 'rect', 'width', String(Math.round(vc.width || 88)))
4944
+ setStyleChild(pillBg, 'rect', 'height', String(Math.round(vc.height || 30)))
4945
+ ensureStyleGroup(pillBg, 'background')
4946
+ setStyleChild(pillBg, 'background', 'backgroundType', '01')
4947
+ setStyleChild(pillBg, 'background', 'backgroundColor', hexToRgba(vc.pill.fillColor, vc.pill.opacity ?? 1))
4948
+ ensureStyleGroup(pillBg, 'borderRadius')
4949
+ setStyleChild(pillBg, 'borderRadius', 'borderRadiusType', '0')
4950
+ setStyleChild(pillBg, 'borderRadius', 'borderRadius', String(vc.pill.radius || 14))
4951
+ absMargin(pillBg, vc.x || 0, vc.y || 0)
4952
+ vcBand.slots[0].components.push(pillBg)
4953
+ }
4954
+ if (vc.pill.sourceEl) {
4955
+ const pr = rect(vc.pill.sourceEl)
4956
+ const catText = textFromSpec({ text: vc.pill.text, color: vc.pill.color, fontSize: vc.pill.fontSize, fontWeight: vc.pill.fontWeight })
4957
+ absMargin(catText, pr.x || 0, pr.y || 0)
4958
+ vcBand.slots[0].components.push(catText)
4959
+ }
4960
+ for (const t of (vc.texts || [])) {
4961
+ const tr = rect(t)
4962
+ const txtNode = textFromSpec({ text: textOf(t), color: ts(t).color, fontSize: ts(t).fontSize, fontWeight: ts(t).fontWeight })
4963
+ absMargin(txtNode, tr.x || 0, tr.y || 0)
4964
+ vcBand.slots[0].components.push(txtNode)
4965
+ }
4966
+ for (const sh of (vc.shapes || [])) {
4967
+ const sr = rect(sh)
4968
+ if (sh.type === 'shape' && (sr.width || 0) <= 30 && (sr.height || 0) <= 20 && (sr.width || 0) > 0) {
4969
+ const pbShell = container('Column', { direction: 'column', justify: 'center', align: 'center', gap: T.gap('none') })
4970
+ ensureStyleGroup(pbShell, 'rect')
4971
+ setStyleChild(pbShell, 'rect', 'width', String(Math.round(sr.width || 24)))
4972
+ setStyleChild(pbShell, 'rect', 'height', String(Math.round(sr.height || 14)))
4973
+ const shFill = sh.style?.fill?.color
4974
+ if (shFill) {
4975
+ ensureStyleGroup(pbShell, 'background')
4976
+ setStyleChild(pbShell, 'background', 'backgroundType', '01')
4977
+ setStyleChild(pbShell, 'background', 'backgroundColor', hexToRgba(shFill, sh.style?.opacity ?? 1))
4978
+ }
4979
+ const shRadius = sh.style?.radius
4980
+ const rVal = (shRadius && typeof shRadius === 'object') ? (shRadius.borderRadius || 0) : (Number(shRadius) || 0)
4981
+ ensureStyleGroup(pbShell, 'borderRadius')
4982
+ setStyleChild(pbShell, 'borderRadius', 'borderRadiusType', '0')
4983
+ setStyleChild(pbShell, 'borderRadius', 'borderRadius', String(rVal))
4984
+ absMargin(pbShell, sr.x || 0, sr.y || 0)
4985
+ vcBand.slots[0].components.push(pbShell)
4986
+ }
4987
+ }
4988
+ for (const sl of (vc.shapes || [])) {
4989
+ if (sl.type !== 'slice') continue
4990
+ const slr = rect(sl)
4991
+ const slMedia = imageFromSlice(sl, sem.slicesExports)
4992
+ if (slMedia) {
4993
+ ensureStyleGroup(slMedia, 'rect')
4994
+ setStyleChild(slMedia, 'rect', 'width', String(Math.round(slr.width || 20)))
4995
+ setStyleChild(slMedia, 'rect', 'height', String(Math.round(slr.height || 20)))
4996
+ absMargin(slMedia, slr.x || 0, slr.y || 0)
4997
+ vcBand.slots[0].components.push(slMedia)
4998
+ }
4999
+ }
5000
+ }
5001
+ insertSemantic(hostFor(vcTopY), vcBand, vcTopY)
5002
+ }
5003
+
5004
+
4760
5005
 
4761
5006
  // 薄壳分段横条(时间轴 / 进度分段):按壳几何逐段还原底色/描边/圆角,段间隙用等宽 spacer
4762
5007
  // 承担(Row gap 是常量,表达不了逐段差异)。此前分段壳落在孤立媒体块通道,
@@ -4817,37 +5062,76 @@ for (const fd of sem.floatDecoBlocks || []) {
4817
5062
  insertSemantic(hostFor(strip.y), row, strip.y)
4818
5063
  }
4819
5064
 
4820
- // 孤立媒体块放在语义布局之后兜底,避免箭头/图标抢占主内容顺序。
4821
- for (const block of (sem.isolatedBlocks || []).filter((block) =>
4822
- !(sem.progressBars || []).length && !block.consumedByTabs)) {
4823
- const media = imageOrPlaceholder(block.element, sem.slicesExports)
4824
- if (!media) continue
4825
- const previousBottom = previousSemanticBottom(block.y)
4826
- const nextTop = nextSemanticTop(block.y + block.height)
4827
- const centered = Math.abs(((block.x || buildCardLeft) + (block.width || 0) / 2) -
4828
- (buildCardLeft + (buildCard.width || 0) / 2)) <= Math.max(12, (buildCard.width || 0) * 0.08)
4829
- const noSideNeighbor = ![...sem.auxTexts, ...sem.postAmountTexts]
4830
- .some((text) => Math.abs((text.y + Math.max(text.height || 0, (text.fontSize || 14) * 1.4) / 2) -
4831
- (block.y + (block.height || 0) / 2)) <= Math.max(block.height || 0, 20) / 2)
4832
- const neighborhoodHeight = (block.height || 0)
4833
- const mediaBottom = block.y + neighborhoodHeight
4834
- setStyleChild(media, 'rect', 'width', String(Math.round(block.width || 0)))
4835
- setStyleChild(media, 'rect', 'height', String(Math.round(neighborhoodHeight)))
4836
- const row = container('Row', { width: '100%', direction: 'row', justify: 'flex-start', align: 'center', padding: T.pad('root') })
4837
- const leftPadding = Math.max(0, Math.round((block.x || buildCardLeft) - buildCardLeft))
4838
- const rightPadding = Math.max(0, Math.round(buildCardRight - ((block.x || buildCardLeft) + (block.width || 0))))
5065
+ // isolatedBlocks fallback after semantic layout
5066
+ // Group same-y blocks into one row (e.g. paired avatars)
5067
+ const _isoBlocks = (sem.isolatedBlocks || []).filter((block) =>
5068
+ !(sem.progressBars || []).length && !block.consumedByTabs)
5069
+ const _isoGroups = []
5070
+ for (const block of _isoBlocks) {
5071
+ const grp = _isoGroups.find((g) => Math.abs(g.y - (block.y || 0)) <= 2)
5072
+ if (grp) grp.blocks.push(block)
5073
+ else _isoGroups.push({ y: block.y || 0, blocks: [block] })
5074
+ }
5075
+ for (const group of _isoGroups) {
5076
+ const medias = []
5077
+ for (const block of group.blocks) {
5078
+ const media = imageOrPlaceholder(block.element, sem.slicesExports)
5079
+ if (!media) continue
5080
+ setStyleChild(media, 'rect', 'width', String(Math.round(block.width || 0)))
5081
+ setStyleChild(media, 'rect', 'height', String(Math.round(block.height || 0)))
5082
+ medias.push(media)
5083
+ }
5084
+ if (!medias.length) continue
5085
+ const previousBottom = previousSemanticBottom(group.y)
5086
+ const groupHeight = Math.max(...group.blocks.map((b) => (b.height || 0)))
5087
+ const nextTop = nextSemanticTop(group.y + groupHeight)
5088
+ const mediaBottom = group.y + groupHeight
5089
+ // Overlay: if block vertically overlaps with adjacent semantic elements
5090
+ // (e.g. transfer arrow between paired cards), render as zero-height AbsoluteLayout
5091
+ // to avoid taking vertical space in the flow.
5092
+ const _overlapsText = medias.length === 1 && (sem.auxTexts || []).some((text) => {
5093
+ const tY = text.y || 0
5094
+ const tH = Math.max(text.height || 0, (text.fontSize || 14) * 1.4)
5095
+ const tBottom = tY + tH
5096
+ const _blk = group.blocks[0]
5097
+ const bRight = (_blk.x || 0) + (_blk.width || 0)
5098
+ const tRight = (text.x || 0) + (text.width || 0)
5099
+ return tY < mediaBottom && tBottom > group.y + 2 &&
5100
+ (bRight <= (text.x || 0) + 2 || (_blk.x || 0) >= tRight - 2)
5101
+ })
5102
+ if (_overlapsText) {
5103
+ const _blk = group.blocks[0]
5104
+ const _media = medias[0]
5105
+ const _band = container('AbsoluteLayout', { width: '100%', height: '0' })
5106
+ ensureStyleGroup(_band, 'other')
5107
+ setStyleChild(_band, 'other', 'overflow', 'visible')
5108
+ ensureStyleGroup(_media, 'margin')
5109
+ setStyleChild(_media, 'margin', 'marginType', '1')
5110
+ setStyleChild(_media, 'margin', 'marginTop', String(Math.max(0, Math.round((_blk.y || 0) - (buildCard.y || 0)))))
5111
+ setStyleChild(_media, 'margin', 'marginLeft', String(Math.max(0, Math.round((_blk.x || 0) - buildCardLeft))))
5112
+ setStyleChild(_media, 'margin', 'marginRight', '0')
5113
+ setStyleChild(_media, 'margin', 'marginBottom', '0')
5114
+ _band.slots[0].components.push(_media)
5115
+ insertSemantic(hostFor(group.y), _band, group.y)
5116
+ continue
5117
+ }
5118
+ const leftmost = group.blocks.reduce((a, b) => ((a.x || 0) <= (b.x || 0) ? a : b))
5119
+ const rightmost = group.blocks.reduce((a, b) => (((a.x || 0) + (a.width || 0)) >= ((b.x || 0) + (b.width || 0)) ? a : b))
5120
+ const leftPadding = Math.max(0, Math.round((leftmost.x || buildCardLeft) - buildCardLeft))
5121
+ const rightPadding = Math.max(0, Math.round(buildCardRight - ((rightmost.x || buildCardLeft) + (rightmost.width || 0))))
5122
+ const row = container('Row', { width: '100%', direction: 'row', justify: medias.length > 1 ? 'space-between' : 'flex-start', align: 'center', padding: T.pad('root') })
4839
5123
  setStyleChild(row, 'padding', 'paddingType', '1')
4840
5124
  setStyleChild(row, 'padding', 'paddingTop', String(previousBottom == null
4841
- ? Math.max(0, Math.round(block.y - (buildCard.y || 0)))
4842
- : Math.max(0, Math.round(block.y - previousBottom))))
5125
+ ? Math.max(0, Math.round(group.y - (buildCard.y || 0)))
5126
+ : Math.max(0, Math.round(group.y - previousBottom))))
4843
5127
  const designedCardBottom = (buildCard.y || 0) + (buildCard.height || 0)
4844
5128
  setStyleChild(row, 'padding', 'paddingBottom', String(Math.max(0, nextTop == null
4845
5129
  ? Math.round(designedCardBottom - mediaBottom)
4846
5130
  : 0)))
4847
5131
  setStyleChild(row, 'padding', 'paddingLeft', String(leftPadding))
4848
5132
  setStyleChild(row, 'padding', 'paddingRight', String(rightPadding))
4849
- row.slots[0].components.push(media)
4850
- insertSemantic(hostFor(block.y), row, block.y)
5133
+ medias.forEach((media) => row.slots[0].components.push(media))
5134
+ insertSemantic(hostFor(group.y), row, group.y)
4851
5135
  }
4852
5136
 
4853
5137
  // 采样色带宿主顶距:带内首个内容的 spec 顶距搬到带宿主(带内行自身 padT 已因 containment 归零),