renderreport 0.2.4

Data-driven report generation with Typst as embedded render engine — no CLI dependency
Documentation
// Hero Summary Component

#let hero-summary(data) = {
  let status-color = if data.computed_status == "good" {
    color-ok
  } else if data.computed_status == "warning" {
    color-warn
  } else {
    color-bad
  }

  // ── Score + Domain header ──────────────────────────────────
  grid(
    columns: (1.1fr, 1.4fr),
    column-gutter: spacing-5,

    // Left: Score card
    theme-card[
      #align(center)[
        #small-text(data.domain)
        #v(spacing-3)
        #text(size: 52pt, weight: "bold", fill: status-color)[#data.score]
        #v(spacing-2)
        #text(size: font-size-lg, weight: "bold", fill: status-color)[Grade #data.grade]
        #v(spacing-3)
        #theme-progress-bar(data.score, bar-color: status-color)
      ]
    ],

    // Right: Verdict
    theme-card[
      #text(size: font-size-2xl, weight: "bold")[Kurzfazit]
      #v(spacing-3)
      #text(size: font-size-base)[#data.verdict]
    ],
  )

  v(spacing-5)

  // ── KPI Metric cards ───────────────────────────────────────
  if data.metrics.len() > 0 {
    let boxes = data.metrics.map(metric => {
      let accent = if metric.accent_color != none { rgb(metric.accent_color) } else { color-primary }
      theme-card[
        #label-text(metric.title)
        #v(spacing-2)
        #text(size: font-size-2xl, weight: "bold", fill: accent)[#metric.value]
      ]
    })

    grid(
      columns: (1fr,) * data.metrics.len(),
      column-gutter: spacing-3,
      ..boxes,
    )

    v(spacing-5)
  }

  // ── Top 3 Actions ──────────────────────────────────────────
  if data.top_actions.len() > 0 {
    text(size: font-size-xl, weight: "bold")[Top Actions]
    v(spacing-3)

    for (i, action) in data.top_actions.enumerate() {
      theme-card(fill: color-surface-soft)[
        #grid(
          columns: (auto, 1fr),
          gutter: spacing-3,
          text(size: font-size-lg, weight: "bold", fill: color-primary)[#{i + 1}],
          text(size: font-size-base)[#action],
        )
      ]
      v(spacing-2)
    }

    v(spacing-3)
  }

  // ── Positive aspects ───────────────────────────────────────
  if data.positive_aspects.len() > 0 {
    text(size: font-size-xl, weight: "bold", fill: color-ok)[Strengths]
    v(spacing-3)
    for aspect in data.positive_aspects {
      text(size: font-size-base)[#text(fill: color-ok)[✓ ]#aspect]
      v(spacing-2)
    }
  }
}