119 lines
3.6 KiB
Typst
119 lines
3.6 KiB
Typst
// Typst template for member export (PDF)
|
|
// Expected sys.inputs.elixir_data:
|
|
// {
|
|
// "columns": [{"key": "...", "kind": "...", "label": "..."}, ...],
|
|
// "rows": [["cell1", "cell2", ...], ...],
|
|
// "meta": {"generated_at": "...", "member_count": 123}
|
|
// }
|
|
|
|
#set page(
|
|
paper: "a4",
|
|
flipped: true,
|
|
margin: (top: 1.2cm, bottom: 1.2cm, left: 1.0cm, right: 1.0cm),
|
|
numbering: "1",
|
|
footer: context [
|
|
#set text(size: 8pt)
|
|
#set align(center)
|
|
#counter(page).display("1 / 1", both: true)
|
|
]
|
|
)
|
|
|
|
#set text(size: 9pt, hyphenate: true)
|
|
#set heading(numbering: none)
|
|
|
|
// Enable text wrapping in table cells
|
|
#show table.cell: it => box(width: 100%)[#it]
|
|
|
|
#let data = sys.inputs.elixir_data
|
|
#let columns = data.at("columns", default: ())
|
|
#let rows = data.at("rows", default: ())
|
|
#let meta = data.at("meta", default: (generated_at: "", member_count: rows.len()))
|
|
#let title = data.at("title", default: "Member Export")
|
|
#let created_at_label = data.at("created_at_label", default: "Created at:")
|
|
#let member_count_label = data.at("member_count_label", default: "Member count:")
|
|
|
|
// Title
|
|
#align(center)[
|
|
#text(size: 14pt, weight: "bold")[#title]
|
|
]
|
|
|
|
#v(0.4cm)
|
|
|
|
// Export metadata
|
|
#set text(size: 8pt, fill: black)
|
|
#grid(
|
|
columns: (1fr, 1fr),
|
|
gutter: 1cm,
|
|
[*#created_at_label* #meta.at("generated_at", default: "")],
|
|
[*#member_count_label* #meta.at("member_count", default: rows.len())],
|
|
)
|
|
|
|
#v(0.6cm)
|
|
|
|
// ---- Horizontal paging config ----
|
|
#let fixed_count = calc.min(2, columns.len())
|
|
#let max_dynamic_cols = 5
|
|
#let fixed_col_widths = (32mm, 32mm)
|
|
|
|
#let fixed_cols = columns.slice(0, fixed_count)
|
|
#let dynamic_cols = columns.slice(fixed_count, columns.len())
|
|
#let dynamic_chunks = dynamic_cols.chunks(max_dynamic_cols)
|
|
|
|
#let render_chunk(chunk_index, dyn_cols_chunk) = [
|
|
#let dyn_count = dyn_cols_chunk.len()
|
|
#let start = fixed_count + chunk_index * max_dynamic_cols
|
|
|
|
#let page_cols = fixed_cols + dyn_cols_chunk
|
|
|
|
// widths: first two columns fixed (32mm, 42mm), rest distributed as 1fr
|
|
#let widths = (
|
|
if fixed_count >= 1 { fixed_col_widths.at(0) } else { 1fr },
|
|
if fixed_count >= 2 { fixed_col_widths.at(1) } else { 1fr },
|
|
..((1fr,) * dyn_count)
|
|
)
|
|
|
|
#let header_cells = page_cols.map(c => text(weight: "bold", size: 9pt)[#c.at("label", default: "")])
|
|
|
|
// Body cells (row-major), only columns of this chunk
|
|
#let body_cells = (
|
|
rows
|
|
.map(row => row.slice(0, fixed_count) + row.slice(start, start + dyn_count))
|
|
.map(cells => cells.map(cell => text(size: 8.5pt)[#cell]))
|
|
.flatten()
|
|
)
|
|
|
|
// Thinner grid for body; thicker vertical line between column 2 and 3; header and outer borders thick
|
|
#let thin_stroke = 0.3pt + black
|
|
#let thick_sep = 1.5pt + black
|
|
#let thick_stroke = 1pt + black
|
|
#let last_x = fixed_count + dyn_count - 1
|
|
#let last_y = rows.len()
|
|
#let stroke_fn = (x, y) => {
|
|
let top = if y == 0 or y == 1 { thick_stroke } else { thin_stroke }
|
|
let bottom = if y == 0 or y == last_y { thick_stroke } else { thin_stroke }
|
|
let left = if x == 0 { thick_stroke } else if x == 2 { thick_sep } else { thin_stroke }
|
|
let right = if x == last_x { thick_stroke } else { thin_stroke }
|
|
(top: top, bottom: bottom, left: left, right: right)
|
|
}
|
|
|
|
// Light gray background for first two columns (first_name, last_name)
|
|
#let fill_fn = (x, y) => if x < 2 { rgb("f2f2f2") } else { none }
|
|
|
|
#table(
|
|
columns: widths,
|
|
stroke: stroke_fn,
|
|
fill: fill_fn,
|
|
table.header(..header_cells),
|
|
..body_cells,
|
|
)
|
|
]
|
|
|
|
// ---- Output ----
|
|
#if dynamic_cols.len() == 0 {
|
|
render_chunk(0, ())
|
|
} else {
|
|
for (i, chunk) in dynamic_chunks.enumerate() {
|
|
render_chunk(i, chunk)
|
|
if i < dynamic_chunks.len() - 1 { pagebreak() }
|
|
}
|
|
}
|