// ============================================================================= // arxivai.typ — Oxford Blue / Academic Gold arXiv preprint theme for Typst // // Builds on lettreai.typ (shared palette, helpers, callouts) and adds the // components an AI-platform arXiv article needs: // // * operation-box — the flagship element: narrates one orchestrated // AI operation (question -> subagent steps -> answer), // typeset like a code listing but speaking the language // of agents. Subagents are the new code of operations. // * agent-pill — inline monospace pill naming a subagent/specialist // * app-figure — screenshot figure with a slim browser-chrome frame // * concept-box — gold panel for philosophical framing // * method-box — blue panel for method-transparency notes // * arxiv title block, abstract panel, keywords line, running header // // Requires Typst >= 0.14; polished on 0.15 (via lettreai's feature detection). // ============================================================================= #import "lettreai.typ" as lettreai #let arxivai-version = "0.1.0" // Re-export the shared visual language. #let colors = lettreai.colors #let tint = lettreai.tint #let shade = lettreai.shade #let body-font = lettreai.body-font #let mono-font = lettreai.mono-font #let badge = lettreai.badge #let divider-line = lettreai.divider-line #let index = lettreai.index #let make-index = lettreai.make-index #let to-string = lettreai.to-string // =========================================================================== // AGENT PILL — inline name of a subagent / specialist / pipeline stage. // Usage: #arxivai.agent-pill("volume-letter-count") // =========================================================================== #let agent-pill(name) = box( fill: tint(colors.gold, 0.72), stroke: 0.5pt + tint(colors.gold, 0.15), inset: (x: 5pt, y: 1.5pt), outset: (y: 1pt), radius: 3pt, text( font: mono-font, size: 0.8em, weight: 600, fill: shade(colors.oxford-blue, 0.05), name, ), ) // =========================================================================== // OPERATION BOX — one orchestrated operation, narrated step by step. // // #arxivai.operation-box( // title: [Volume inventory], // question: [How many letters does KGW 13 contain?], // steps: ( // (agent: "decompose", does: [classifies the question as an // inventory query over one volume]), // (agent: "volume-letter-count", does: [reads the edition's Nr. headers // from the OCR layer]), // (agent: "assemble", does: [returns the count with the // method trace attached]), // ), // latency: "0.4 s", // answer: [122 letters (Nr. 1-127, with gaps the edition itself leaves)], // ) // // Every field is optional except steps; the box degrades gracefully. // =========================================================================== #let operation-box( title: [Operation], question: none, steps: (), latency: none, answer: none, ) = block( width: 100%, radius: 4pt, breakable: true, stroke: 1pt + colors.oxford-blue, fill: colors.white, above: 1.3em, below: 1.3em, inset: 0pt, clip: true, { // Header strip: OPERATION label + title, latency badge right. block( width: 100%, fill: colors.oxford-blue, inset: (x: 10pt, y: 7pt), below: 0pt, grid( columns: (1fr, auto), align: (left + horizon, right + horizon), text(fill: colors.white, weight: 700, size: 0.95em, { text(fill: colors.gold, weight: 800, size: 0.85em, smallcaps[operation]) h(0.7em) title }), if latency != none { box( fill: colors.gold, inset: (x: 6pt, y: 2pt), radius: 3pt, text(fill: colors.oxford-blue, weight: 700, size: 0.78em, font: mono-font, latency), ) } else { [] }, ), ) // Question line. if question != none { block( width: 100%, fill: tint(colors.oxford-blue, 0.955), inset: (x: 10pt, y: 7pt), below: 0pt, stroke: (bottom: 0.5pt + tint(colors.oxford-blue, 0.75)), text(size: 0.93em, { text(fill: colors.oxford-blue, weight: 800, [Q]) h(0.6em) text(style: "italic", fill: shade(colors.ink, 0.0), question) }), ) } // Numbered subagent steps. if steps.len() > 0 { block( width: 100%, inset: (x: 10pt, y: 9pt), below: 0pt, { set text(size: 0.92em) set par(justify: false) grid( columns: (auto, auto, 1fr), column-gutter: 8pt, row-gutter: 7pt, align: (center + top, left + top, left + top), ..steps.enumerate().map(((i, s)) => ( box( width: 14pt, height: 14pt, radius: 7pt, fill: tint(colors.gold, 0.3), align(center + horizon, text( size: 0.72em, weight: 800, fill: colors.oxford-blue, str(i + 1), )), ), agent-pill(s.agent), s.does, )).flatten(), ) }, ) } // Answer line. if answer != none { block( width: 100%, inset: (x: 10pt, y: 7pt), below: 0pt, stroke: (top: 1pt + colors.gold), fill: tint(colors.gold, 0.88), text(size: 0.93em, { text(fill: colors.oxford-blue, weight: 800, sym.arrow.r) h(0.6em) text(fill: shade(colors.oxford-blue, 0.0), weight: 600, answer) }), ) } }, ) // =========================================================================== // APP FIGURE — screenshot with a slim browser-chrome frame. // Returns a figure, so it numbers and cross-references like any figure. // // #arxivai.app-figure( // "figures/viewer-split.png", // url: "ai-viewer - kepler-prim/kgw-13, page 42", // caption: [Split-view workplace.], // ) // =========================================================================== #let app-figure(path, caption: none, url: none, width: 100%) = figure( align(center, block( width: width, radius: 4pt, stroke: 1pt + tint(colors.oxford-blue, 0.35), clip: true, inset: 0pt, { block( width: 100%, fill: colors.oxford-blue, inset: (x: 9pt, y: 5pt), below: 0pt, grid( columns: (auto, 1fr), column-gutter: 9pt, align: (left + horizon, left + horizon), { box(width: 7pt, height: 7pt, radius: 3.5pt, fill: tint(colors.danger, 0.25)) h(3.5pt) box(width: 7pt, height: 7pt, radius: 3.5pt, fill: tint(colors.gold, 0.15)) h(3.5pt) box(width: 7pt, height: 7pt, radius: 3.5pt, fill: tint(colors.success, 0.2)) }, if url != none { text(font: mono-font, size: 0.7em, fill: tint(colors.oxford-blue, 0.72), url) } else { [] }, ), ) image(path, width: 100%) }, )), caption: caption, ) // =========================================================================== // CONCEPT BOX — philosophical framing (gold-dominant panel). // =========================================================================== #let concept-box(body, title: none) = block( width: 100%, inset: 12pt, radius: 4pt, above: 1.2em, below: 1.2em, breakable: true, fill: tint(colors.gold, 0.9), stroke: (left: 3pt + colors.gold, rest: 0.5pt + tint(colors.gold, 0.4)), { if title != none { text(fill: shade(colors.gold, 0.45), weight: 800, size: 0.85em, smallcaps[concept]) h(0.7em) text(fill: colors.oxford-blue, weight: 700, title) v(0.5em, weak: true) } set text(size: 0.95em) body }, ) // =========================================================================== // METHOD BOX — method transparency / integrity notes (blue panel). // =========================================================================== #let method-box(body, title: none) = block( width: 100%, inset: 12pt, radius: 4pt, above: 1.2em, below: 1.2em, breakable: true, fill: tint(colors.oxford-blue, 0.94), stroke: (left: 3pt + colors.oxford-blue, rest: 0.5pt + tint(colors.oxford-blue, 0.6)), { if title != none { text(fill: colors.oxford-blue, weight: 800, size: 0.85em, smallcaps[method]) h(0.7em) text(fill: colors.oxford-blue, weight: 700, title) v(0.5em, weak: true) } set text(size: 0.95em) body }, ) // =========================================================================== // TITLE BLOCK — centered arXiv-preprint layout with gold rule. // =========================================================================== #let arxiv-title-block( title: none, subtitle: none, authors: none, date: none, ) = { set align(center) block(width: 100%, below: 1.4em, { if title != none { block(below: 0.55em, text(size: 1.7em, weight: 800, fill: colors.oxford-blue, title)) } if subtitle != none { block(below: 0.8em, text(size: 1.15em, style: "italic", fill: shade(colors.oxford-blue, 0.0), subtitle)) } align(center, line(length: 42%, stroke: 1.5pt + colors.gold)) if authors != none and authors != () { v(0.7em, weak: true) for a in authors { let name = if type(a) == dictionary { a.at("name", default: none) } else { a } let affiliation = if type(a) == dictionary { a.at("affiliation", default: none) } else { none } let email = if type(a) == dictionary { a.at("email", default: none) } else { none } let orcid = if type(a) == dictionary { a.at("orcid", default: none) } else { none } text(size: 1.05em, weight: 700, fill: colors.ink, name) if orcid != none and to-string(orcid) != "" { h(0.35em) link("https://orcid.org/" + to-string(orcid), text( size: 0.7em, fill: colors.success, weight: 700, [iD], )) } if affiliation != none and to-string(affiliation) != "" { v(0.25em, weak: true) text(size: 0.92em, fill: shade(colors.ink, 0.0), affiliation) } if email != none and to-string(email) != "" { v(0.25em, weak: true) text(size: 0.85em, font: mono-font, fill: colors.oxford-blue, email) } v(0.6em, weak: true) } } if date != none { v(0.3em, weak: true) text(size: 0.9em, fill: tint(colors.ink, 0.3), date) } }) } // =========================================================================== // ABSTRACT PANEL + KEYWORDS LINE // =========================================================================== #let arxiv-abstract(abstract, abstract-title: [Abstract]) = align(center, block( width: 90%, inset: 14pt, radius: 4pt, fill: tint(colors.oxford-blue, 0.96), stroke: (left: 3pt + colors.gold), below: 1.2em, { align(center, text(weight: 800, size: 0.95em, fill: colors.oxford-blue, smallcaps(abstract-title))) v(0.55em, weak: true) set align(left) set text(size: 0.93em) set par(justify: true) abstract }, )) #let arxiv-keywords(keywords) = { if keywords == none or keywords == () { return } align(center, block(width: 90%, below: 1.6em, { set align(left) text(fill: shade(colors.gold, 0.4), weight: 800, size: 0.85em, smallcaps[keywords]) h(0.7em) text(size: 0.88em, fill: shade(colors.ink, 0.0), keywords.map(k => to-string(k)).join(" · ")) })) } // =========================================================================== // PAGE FURNITURE — running header with gold hairline, footer page number. // =========================================================================== #let arxiv-header(running-title) = context { if counter(page).get().first() > 1 { grid( columns: (1fr, auto), align: (left + horizon, right + horizon), text(size: 0.8em, weight: 600, fill: colors.oxford-blue, smallcaps(running-title)), text(size: 0.8em, fill: tint(colors.ink, 0.2), counter(page).display()), ) v(-0.5em) line(length: 100%, stroke: 0.5pt + colors.gold) } } #let arxiv-footer() = context { if counter(page).get().first() == 1 { align(center, text(size: 0.85em, fill: tint(colors.ink, 0.4), counter(page).display())) } } // =========================================================================== // BOOK MODE — O'Reilly-guide layout: chapter openers, outer margin column, // margin notes, sidebars, chapter-aware running heads. // Activated by `book: true` in the document front matter. // =========================================================================== // Book page geometry (A4): text block ~12.2cm + 4.6cm margin-note column. #let book-margin = (left: 2.2cm, right: 6.6cm, top: 2.6cm, bottom: 2.8cm) // Margin note — the O'Reilly signature aside, placed in the outer column. // #arxivai.margin-note[Short, load-bearing aside.] #let margin-note(body, dy: -0.8em) = place( right, dx: 5.1cm, dy: dy, block( width: 4.6cm, inset: (left: 7pt, y: 3pt), stroke: (left: 2pt + colors.gold), { set align(left) set par(justify: false, leading: 0.5em) text(size: 0.76em, style: "italic", fill: shade(colors.ink, 0.0), body) }, ), ) // Sidebar — boxed digression with double gold rules top and bottom. // #arxivai.sidebar(title: [A short history])[ ... ] #let sidebar(body, title: none) = block( width: 100%, breakable: true, above: 1.5em, below: 1.5em, inset: (y: 12pt, x: 4pt), stroke: (top: 2.5pt + colors.gold, bottom: 2.5pt + colors.gold), { place(top, dy: -8.5pt, line(length: 100%, stroke: 0.75pt + colors.gold)) place(bottom, dy: 8.5pt, line(length: 100%, stroke: 0.75pt + colors.gold)) if title != none { align(center, text(fill: colors.oxford-blue, weight: 800, size: 0.95em, smallcaps(title))) v(0.7em, weak: true) } set text(size: 0.94em) body }, ) // Chapter openers: level-1 headings become chapter pages. #let book-heading-rules(body) = { // outlined: true excludes synthetic headings (the outline's own title), // which may live inside containers where pagebreaks are illegal. show heading.where(level: 1, outlined: true): it => { pagebreak(weak: true) block(above: 0em, below: 2em, width: 100%, { v(2.2cm) if it.numbering != none { text( size: 0.95em, weight: 800, fill: shade(colors.gold, 0.35), smallcaps[chapter #counter(heading).display("1")], ) v(0.6em, weak: true) } text(size: 2em, weight: 800, fill: colors.oxford-blue, it.body) v(0.7em, weak: true) line(length: 42%, stroke: 2.5pt + colors.gold) }) } body } // Chapter-aware running head; suppressed on page 1 and chapter-opener pages. #let book-header() = context { if counter(page).get().first() <= 1 { return } let here-page = here().page() let openers = query(heading.where(level: 1)).filter(h => h.location().page() == here-page) if openers.len() > 0 { return } let before = query(heading.where(level: 1).before(here())) if before.len() == 0 { return } grid( columns: (1fr, auto), align: (left + horizon, right + horizon), text(size: 0.8em, weight: 600, fill: colors.oxford-blue, smallcaps(before.last().body)), text(size: 0.8em, fill: tint(colors.ink, 0.2), counter(page).display()), ) v(-0.5em) line(length: 100%, stroke: 0.5pt + colors.gold) } // Page number centered on title and chapter-opener pages (where the head is off). #let book-footer() = context { let here-page = here().page() let openers = query(heading.where(level: 1)).filter(h => h.location().page() == here-page) if counter(page).get().first() == 1 or openers.len() > 0 { align(center, text(size: 0.85em, fill: tint(colors.ink, 0.4), counter(page).display())) } } // =========================================================================== // ANNOTATED LISTINGS & SCORE BOXES — the O'Reilly callout-marker convention. // // Lines are rendered with controlled mono typography (indentation preserved, // light YAML colouring), so gold circled markers can attach to exact lines // and a keyed legend follows the fragment. // =========================================================================== // Gold circled marker; also usable inline in prose: "at #arxivai.mark(2) ..." #let mark(n) = box( width: 13pt, height: 13pt, radius: 6.5pt, fill: colors.gold, align(center + horizon, text(size: 0.68em, weight: 800, fill: colors.oxford-blue, str(n))), ) // One listing line: preserved indentation + light YAML colouring. #let _listing-line(ln) = { let rest = ln.trim(" ", at: start) let lead = ln.len() - rest.len() h(lead * 0.55em) if rest.starts-with("#") { text(fill: tint(colors.ink, 0.42), style: "italic", rest) } else { let idx = rest.position(":") if idx != none { text(fill: colors.oxford-blue, weight: 600, rest.slice(0, idx)) text(fill: shade(colors.ink, 0.0), rest.slice(idx)) } else { text(fill: shade(colors.ink, 0.0), rest) } } } // Shared body renderer for annotated-listing and score-box. #let _listing-body(source, marks) = { let lines = source.split("\n") let lines = if lines.len() > 0 and lines.last().trim() == "" { lines.slice(0, lines.len() - 1) } else { lines } let marked-lines = marks.map(m => m.line) block( width: 100%, fill: tint(colors.oxford-blue, 0.965), stroke: (left: 3pt + colors.gold), inset: (x: 10pt, y: 9pt), below: 0pt, { set par(justify: false, leading: 0.52em) set text(font: mono-font, size: 0.8em) for (i, ln) in lines.enumerate() { let n = i + 1 _listing-line(ln) if marked-lines.contains(n) { h(0.6em) mark(marks.position(m => m.line == n) + 1) } if i < lines.len() - 1 { linebreak() } } }, ) } // Legend rows: marker → annotation. #let _listing-legend(marks) = { if marks.len() == 0 { return } block( width: 100%, inset: (x: 10pt, y: 9pt), below: 0pt, stroke: (top: 0.5pt + tint(colors.oxford-blue, 0.7)), { set text(size: 0.88em) set par(justify: false) grid( columns: (auto, 1fr), column-gutter: 9pt, row-gutter: 7pt, align: (center + top, left + top), ..marks.enumerate().map(((j, m)) => (mark(j + 1), m.note)).flatten(), ) }, ) } // Annotated listing for any code fragment. // #arxivai.annotated-listing(source, title: [...], marks: ((line: 3, note: [...]),)) #let annotated-listing(source, title: none, marks: ()) = block( width: 100%, radius: 4pt, clip: true, breakable: true, stroke: 1pt + tint(colors.oxford-blue, 0.4), above: 1.3em, below: 1.3em, inset: 0pt, { if title != none { block( width: 100%, fill: colors.oxford-blue, inset: (x: 10pt, y: 6pt), below: 0pt, text(fill: colors.white, weight: 700, size: 0.9em, title), ) } _listing-body(source, marks) _listing-legend(marks) }, ) // Score box — a partitur fragment quoted like bars of a musical score. // #arxivai.score-box( // source, // the YAML fragment (string) // id: "ai-query-phase1", // score id, shown in the header strip // path: "stanza-or-skip", // stage path within the score // marks: ((line: 4, note: [OR-fallback: skip when nothing remains]),), // plays-on: "Conductor / pdai:duckdb", // who plays this score // ) #let score-box( source, id: none, path: none, marks: (), plays-on: none, ) = block( width: 100%, radius: 4pt, clip: true, breakable: true, stroke: 1pt + colors.oxford-blue, above: 1.3em, below: 1.3em, inset: 0pt, { // Header strip: PARTITUR + score id + stage path. block( width: 100%, fill: colors.oxford-blue, inset: (x: 10pt, y: 7pt), below: 0pt, grid( columns: (1fr, auto), align: (left + horizon, right + horizon), text(size: 0.92em, { text(fill: colors.gold, weight: 800, size: 0.88em, smallcaps[partitur]) if id != none { h(0.7em) text(fill: colors.white, weight: 700, font: mono-font, id) } if path != none { text(fill: tint(colors.oxford-blue, 0.6), font: mono-font, [ \/ #path]) } }), [], ), ) _listing-body(source, marks) _listing-legend(marks) // Footer: who plays this score. if plays-on != none { block( width: 100%, inset: (x: 10pt, y: 6pt), below: 0pt, stroke: (top: 1pt + colors.gold), fill: tint(colors.gold, 0.88), text(size: 0.85em, { text(fill: shade(colors.gold, 0.45), weight: 800, smallcaps[played by]) h(0.6em) text(fill: colors.oxford-blue, weight: 600, font: mono-font, plays-on) }), ) } }, ) // =========================================================================== // FIGURE STYLING — thin Oxford-Blue frame on every image, themed captions. // Applied from the template via arxiv-figure-rules. // =========================================================================== #let arxiv-figure-rules(body) = { show image: it => block( radius: 3pt, clip: true, stroke: 0.75pt + tint(colors.oxford-blue, 0.45), inset: 0pt, it, ) show figure.caption: it => block(width: 92%, { set align(left) set par(justify: false) set text(size: 0.87em) text(fill: colors.oxford-blue, weight: 700, { it.supplement [ ] context it.counter.display(it.numbering) }) h(0.55em) it.body }) body }