renderreport 0.3.1

Data-driven report generation with Typst as embedded render engine — no CLI dependency
Documentation
// Key-Value List Component
// Inspired by BIRT Parameter Elements

#let key-value-list(data) = {
  box(width: 100%)[
    #if data.title != none [
      #component-title(text(weight: "semibold", size: font-size-base)[#data.title])
    ]
    
    #if data.layout == "horizontal" [
      #for item in data.items [
        #box(
          inset: spacing-2,
          fill: if item.highlight { color-surface } else { none },
        )[
          #text(weight: "semibold", size: font-size-sm)[#item.key:]
          #h(spacing-2)
          #text(size: font-size-sm)[#item.value]
        ]
        #h(spacing-4)
      ]
    ] else [
      // Key column is a fixed fraction, not `auto`: `auto` sizes to the
      // widest *unbroken* run in any key, uncapped. A short label (the
      // component's intended use) is unaffected, but an unbounded-length
      // key (a CSS selector, a URL, a filename) forces the column to claim
      // nearly the full row width, squeezing the value into a
      // one-word-per-line sliver spanning dozens of rows and corrupting the
      // following page break. 32% comfortably fits every key seen in
      // practice (single words, short phrases like "Unminifizierte
      // Dateien") while still bounding the pathological case.
      #grid(
        columns: (32%, 1fr),
        column-gutter: spacing-3,
        row-gutter: spacing-2,
        align: (left, left),

        ..data.items.map(item => (
          box(
            inset: (x: spacing-2, y: 2pt),
            fill: if item.highlight { color-surface } else { none },
          )[
            #text(weight: "semibold", size: font-size-sm, fill: color-text-muted)[#item.key]
          ],
          box(
            inset: (x: spacing-2, y: 2pt),
            fill: if item.highlight { color-surface } else { none },
          )[
            #text(size: font-size-sm)[#item.value]
          ],
        )).flatten()
      )
    ]
  ]
}