This commit is contained in:
Gaspard Jankowiak 2025-12-14 20:23:50 +01:00
commit b4a76500b6
20 changed files with 18 additions and 1059 deletions

View file

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) by Marsupilami -->
<svg
version="1.1"
width="892"
height="768"
viewBox="-1.66203747 -1.66203747 68.18782194 58.72532394"
id="svg9193"
sodipodi:docname="unigrazlogo-white-border.svg"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="1.0520833"
inkscape:cx="446.25743"
inkscape:cy="384"
inkscape:window-width="1920"
inkscape:window-height="1015"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg9193" />
<defs
id="defs9195" />
<path
d="m 0,55.40125 64.86375,0 L 64.86375,0 0,0 0,55.40125 z"
id="path3442"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" />
<path
d="m 2.85501,2.8575 18.9275,0 0,49.67875 -18.9275,0 0,-49.67875 z"
id="path3444"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" />
<path
d="m 21.78251,2.8575 40.21999,0 0,49.67875 -40.21999,0 0,-49.67875 z"
id="path3446"
style="fill:#ffdf00;fill-opacity:1;fill-rule:nonzero;stroke:none" />
<path
d="m 45.67713,45.13812 -1.01375,0 0,1.39875 1.01375,0 c 0.63,0 0.81875,-0.30875 0.81875,-0.705 0,-0.3825 -0.18875,-0.69375 -0.81875,-0.69375 m 5.26,2.27375 1.30625,0 -0.64125,-1.54 -0.665,1.54 z m -29.155,5.1325 40.22625,0 0,-9.61 -40.22625,0 0,9.61 z m 19.82875,-4.2175 c -0.465,0.9 -1.54125,1.4275 -2.6775,1.4275 -1.85375,0 -3.12,-1.36875 -3.12,-3.055 0,-1.6875 1.42625,-3.055 3.12,-3.055 1.01625,0 1.91625,0.49125 2.47375,1.25 l -1.19875,0.84875 c -0.3175,-0.42875 -0.7,-0.6725 -1.275,-0.6725 -0.965,0 -1.61,0.6675 -1.61,1.62875 0,0.95875 0.66625,1.58875 1.63125,1.58875 0.8375,0 1.3,-0.3425 1.41375,-0.7725 l -1.00125,0 0,-1.075 2.50375,0 10e-4,0.4875 c 0,0.51125 -0.0537,1.00375 -0.26125,1.39875 m 11.56125,1.31625 -0.48875,-1.02375 -2.17625,0 -0.48875,1.02375 -3.32625,0 -1.13875,-1.7175 -0.89,0 0,1.7175 -1.64375,0 0,-5.89375 2.8125,0 c 1.31875,0 2.18625,0.91 2.18625,2.0825 0,0.79875 -0.34625,1.37875 -1.0325,1.665 l 1.3975,1.98 3.06,-5.7825 0.385,0 3.05875,5.94875 -1.715,0 z m 6.99125,0 -4.9175,0 0,-0.4575 2.56375,-4.0475 -2.525,0 0,-1.38875 4.87875,0 0,0.3375 -2.67375,4.1675 2.67375,0 0,1.38875 z m -2.93708,-17.83562 2.89126,0 0,10.16375 -2.89126,0 0,-10.16375 z m -10.53987,-0.1905 0.815,0 5.285,4.78125 0,-4.5925 2.88,0 0,10.3 -0.88375,0 -5.2075,-4.7225 0,4.58875 -2.895,-0.001 0.0125,-10.35375 z m -10.97975,0.19175 0,5.85125 c 0,2.5625 2.22,4.51 4.7825,4.50875 2.56,-0.004 4.77375,-1.95 4.77375,-4.51125 l 0,-5.85 -2.91125,0 0,5.41875 c 0,1.28375 -0.765,2.13875 -1.87375,2.14 -1.11125,0 -1.88,-0.80125 -1.88,-2.14 l 0,-5.41875 -2.89125,10e-4 z"
id="path3472"
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none" />
</svg>
<!-- version: 20110311, original size: 64.863747 55.401249, border: 3% -->

After

Width:  |  Height:  |  Size: 3.3 KiB

59
src/helper.typ Normal file
View file

@ -0,0 +1,59 @@
// --------------------------------------------------------------------------
// Extra helper functions
// Fix for typst #311 "Behavior of first line indentation in paragraphs ..."
// https://github.com/typst/typst/issues/311#issuecomment-2023038611
// https://forum.typst.app/t/how-to-indent-paragraphs-following-a-list-without-affecting-the-paragraph-after-a-heading/4210?u=fungai2000
#let fix-311 = context {
set par.line(numbering: none)
v(0pt, weak: true) + par(none)
}
#let content-to-string(content) = {
if content.has("text") {
content.text
}else if content.has("child") {
content-to-string(content.child)
} else if content.has("children") {
content.children.map(content-to-string).join("")
} else if content.has("body") {
content-to-string(content.body)
} else if content == [ ] {
" "
}
}
#let content-to-string-omit(content) = {
if content.has("text") {
content.text
}else if content.has("child") {
content-to-string-omit(content.child)
} else if content.has("children") {
content.children.map(content-to-string-omit).join("")
} else if content.has("body") {
content-to-string-omit(content.body)
} else if content == [ ] {
" "
} else {
"..."
}
}
// partitle simulates \paragraph{title} (more or less)
#let partitle(title: [Title], body) = context [
#block(above: 0.8cm)[
*#title.* #h(0.3cm) #body
]
#fix-311
]
// Wrapper for the technique shown here:
// https://typst.app/docs/reference/layout/layout/
#let measure-text-height(body) = {
layout(size => {
let (height,) = measure(width: size.width, body)
return height
})
}
//vim:tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab colorcolumn=81

659
src/lib.typ Normal file
View file

@ -0,0 +1,659 @@
#import "@preview/touying:0.6.1": *
// Project
#import "helper.typ": *
#import "logos.typ": *
// Core Imports
#import "@preview/codly:1.3.0": * // For bindings
#import "@preview/cetz:0.4.1" // For bindings
#import "@preview/fletcher:0.5.8" as fletcher: edge, node // For bindings
#import "@preview/tiaoma:0.3.0" // For auto QR generation
// Styling Macro Imports
#import "@preview/showybox:2.0.4": showybox
// -----------------------------------------------------------------------------
// General Config
// -----------------------------------------------------------------------------
// Touying bindings for cetz
#let cetz-canvas = touying-reducer.with(
reduce: cetz.canvas,
cover: cetz.draw.hide.with(bounds: true),
)
// Touying bindings for fletcher
#let fletcher-diagram = touying-reducer.with(
reduce: fletcher.diagram,
cover: fletcher.hide,
)
// -----------------------------------------------------------------------------
// Slide Types
// -----------------------------------------------------------------------------
/// Normal slide for the presentation, with title (header) and footer
///
/// Example:
///
/// ```typst
/// #slide(title: [Slide Title])[
/// #lorem(20)
/// ]
/// ```
///
/// - title (content): Title for the slide
/// - alignment (alignment): Alignment of the contents of the slide
/// - outlined (boolean): If the slide shows on the PDF ToC
///
/// -> content
#let slide(
title: auto,
alignment: none,
outlined: true,
..args,
) = touying-slide-wrapper(self => {
let info = self.info + args.named()
// Header:
// ---------------------------------------------------------------------------
// [ ] Slide Title [ ] Logo [ ]
// ---------------------------------------------------------------------------
let header(self) = {
// Slide Title: if the user overrides the title of a certain slide, use it
// Progress bar
if self.store.progress-bar {
place(top + left, float: false,
move(dy: 0.00cm, // Bad solution, I know
components.progress-bar(
height: 3pt,
self.colors.primary,
white,
)
)
)
}
let hdr = if title != auto { title } else { self.store.header }
show heading: set text(size: 24pt, weight: "semibold")
grid(
columns: (self.page.margin.left, 1fr, 1cm, auto, 1.2cm),
block(),
heading(level: 1, outlined: outlined, hdr),
block(),
move(dy: -0.31cm, institute-logo(self)),
block(),
)
}
// Footer:
// ---------------------------------------------------------------------------
// Slide Number [ ] First Author
// ---------------------------------------------------------------------------
let footer(self) = context {
set block(height: 100%, width: 100%)
set text(size: 15pt, fill: self.colors.footer)
grid(
columns: (self.page.margin.bottom, 1.3%, auto, 1cm),
move(dx:0.3em, dy: 0.3em, circle(radius: 0.75em, stroke: self.colors.primary + 2pt)[
#set align(center + horizon)
#set text(size: 12pt)
#utils.slide-counter.display()
]),
block(),
block[
#set align(left + horizon)
#set text(size: 13pt)
#info.at("footer", default: "")
],
block(),
)
}
let self = utils.merge-dicts(self, config-page(
header: header,
footer: footer,
))
set align(
if alignment == none {
self.store.default-alignment
} else {
alignment
}
)
touying-slide(self: self, ..args)
})
/// Title slide for the presentation
///
/// Example:
///
/// ```typst
/// #title-slide()
/// ```
///
/// -> content
#let title-slide(..args) = touying-slide-wrapper(self => {
let info = self.info + args.named()
let body = {
let footer-kfu = [
#set text(size: 13.3pt, weight: "medium")
#let circle-icon = [
#move(dy: 0.03cm, circle(
fill: none,
radius: 0.18cm,
stroke: self.colors.primary + 1.35pt,
)
)
]
#v(-0.5cm)
#box(circle-icon) #h(0.1cm) #self.store.institute
]
set page(footer: footer-kfu, header: none)
set block(below: 0pt, above: 0pt)
place(center + horizon, dx: -15em, [
#circle(stroke: self.colors.primary + 13em, radius: 32em)
])
// Top-right icon + text
place(top + right, dy: -1.9cm, dx: 0.78cm, [
#self.store.logo
])
v(0.8cm)
block(width: 83%)[
#let title = text(size: 40.5pt, weight: "bold")[#info.at(
"title",
default: "",
)]
#move(dx: 0.04em)[
#grid(
columns: (0.195cm, auto),
column-gutter: 0.7cm,
context [
#let s = measure(title)
#move(dy: -0.4cm, rect(
fill: self.colors.primary,
height: s.height + 0.65cm,
))
],
title,
)
]
]
v(0.6cm)
block(width: 70%)[
#text(
size: 28.3pt,
fill: self.colors.fore,
weight: "bold",
)[#info.subtitle]
]
v(1.48cm)
block(width: 70%)[
#set text(size: 19pt)
#if type(info.authors) == array [
#for author in info.authors [
#author #h(1.1em)
]
] else [
#info.authors
]
]
v(0.95cm)
block(width: 70%)[
#info.extra
]
if (
self.info.at("download-qr", default: none) != none
and self.info.at("download-qr", default: none) != ""
) {
place(bottom + right)[
#align(center + horizon)[
#let s = 4.9cm
#tiaoma.qrcode(self.info.download-qr, width: s, height: s)
]
]
}
}
touying-slide(self: self, body)
})
/// Standout slide for the presentation
///
/// Example:
///
/// ```typst
/// #standout-slide(title: [Text])
/// ```
///
/// - title (content): Title for the standout slide
///
/// -> content
#let standout-slide(
title: none,
..args,
) = touying-slide-wrapper(self => {
let body = {
set align(center + horizon)
set text(size: 28pt)
if title != none {
move(dy: -2.08cm)[
#text(weight: "semibold")[#title]
]
}
}
let self = utils.merge-dicts(self, config-page(
header: none,
footer: none,
))
//counter("touying-slide-counter").update(n => if n > 0 { n - 1 } else { 0 })
touying-slide(self: self, body, ..args)
})
/// Section slide for the presentation
///
/// Example:
///
/// ```typst
/// #section-slide(title: [Section A], subtitle: [Subtitle])
/// ```
///
/// - title (content): Title for the section
/// - subtitle (content): Subtitle for the section
///
/// -> content
#let section-slide(
title: none,
subtitle: none,
..args,
) = touying-slide-wrapper(self => {
let body = {
align(center + horizon)[
#move(dy: -0.4cm)[
#if title != none [
#text(size: 36pt, weight: "semibold")[#title]
]
#if subtitle != none [
#text(size: 20pt)[#subtitle]
]
]
]
}
let self = utils.merge-dicts(self, config-page(
header: none,
footer: none,
))
touying-slide(self: self, body, ..args)
})
/// Blank slide for free content in the presentation
///
/// Example:
///
/// ```typst
/// #blank-slide[
/// #align(center + horizon)[#lorem(5)]
/// ]
/// ```
///
/// -> content
#let blank-slide(
..args,
body,
) = touying-slide-wrapper(self => {
let body = {
align(center + horizon)[
#body
]
}
let self = utils.merge-dicts(self, config-page(
header: none,
footer: none,
margin: 0pt,
))
touying-slide(self: self, body, ..args)
})
// -----------------------------------------------------------------------------
// Main Function
// -----------------------------------------------------------------------------
/// Theme configuration
///
/// Example:
///
/// ```typst
/// #show: definitely-not-kfu-theme.with(
/// aspect-ratio: "16-9",
/// slide-alignment: top,
/// config-info(
/// title: [Long Paper Title \ with One to Three Lines],
/// subtitle: [An optional short subtitle],
/// authors: ([*First Author*], [Second Author], [Third Author]),
/// extra: [SomeConf 2025],
/// footer: [First Author, Second Author, Third Author],
/// download-qr: "",
/// ),
/// config-common(
/// handout: false,
/// ),
/// config-colors(
/// ),
/// )
/// ```
///
/// - aspect-ratio (str): Aspect ratio for the page. See typst documentatin.
/// - slide-alignemnt (alignment): Default alignment for `#slide()`
/// - config-info (dict):
/// - title (content): Title for the presentation
/// - subtitle (content): Subtitle for the presentation
/// - authors (array): Arrray of authors (content)
/// - extra (content): Extra information for the presentation
/// - footer (content): Footer for each `#slide()`
/// - download-qr (str): URL to show on `#title-slide()` with a QR
/// - config-common (dict):
/// - handout (bool): Boolean for handout mode
/// - config-colors (dict): Colors for the presentation
/// - ... see definition of `#definitely-not-kfu-theme`
#let definitely-not-kfu-theme(
aspect-ratio: "16-9",
header: utils.display-current-heading(level: 1),
font: "Open Sans",
institute: [uni-graz.at],
logo: unigrazgraz-logo,
slide-alignment: top,
progress-bar: true,
..args,
body,
) = {
// Touying configuration
show: touying-slides.with(
config-page(
paper: "presentation-" + aspect-ratio,
margin: (
left: 1.49cm,
right: 1.48cm,
top: 2.6cm,
bottom: 1.6cm,
)
),
config-store(
header: header,
font: font,
institute: institute,
logo: logo,
default-alignment: slide-alignment,
progress-bar: progress-bar,
),
config-common(
slide-fn: slide,
new-section-slide-fn: none,
preamble: {
codly(
display-name: false,
display-icon: false,
radius: 0pt,
stroke: 1pt + black,
smart-indent: true,
fill: luma(260),
zebra-fill: luma(250),
number-format: number => [#text(size: 12pt, fill: gray)[#number]],
number-align: right + horizon,
breakable: true,
)
}
),
config-colors( // Exported from official template
kfu: rgb("FED501"),
primary: rgb("FED501"),
secondary: rgb("9933cc"),
footer: rgb("808080"),
foot: rgb("e1e1e1"),
web: rgb("0c5a77"),
csbme: rgb("19b4e3"),
arch: rgb("0a98a2"),
bauw: rgb("d68e23"),
etec: rgb("68242c"),
mach: rgb("3066ba"),
chem: rgb("5e60a8"),
math: rgb("1e6934"),
crypto: rgb("a6c947"),
system: rgb("1171a8"),
formal: rgb("f7931e"),
applied: rgb("7d219e"),
page: rgb("e4154b"),
fore: rgb("0f0f0f"),
back: rgb("ffffff"),
dark: rgb("3b5a70"),
lite: rgb("fefcf1"),
head: rgb("245b78"),
body: rgb("e2e9ed"),
urlA: rgb("0066d8"),
urlB: rgb("6c2f91"),
colA: rgb("e4154b"),
colB: rgb("5191c1"),
colC: rgb("a5a5a5"),
colD: rgb("285f82"),
colE: rgb("78b473"),
colF: rgb("e59352"),
main: rgb("e4154b"),
emph: rgb("285f82"),
standout: rgb("245b78"),
),
config-methods(
cover: (self: none, body) => hide(body),
init: (
self: none,
body,
) => {
set text(size: 20pt, lang: "en", region: "US", font: font)
show emph: it => { text(self.colors.secondary, it.body) }
show cite: it => { text(self.colors.secondary, it) }
show strong: it => { text(weight: "bold", it.body) }
// Bibliography
set bibliography(title: none, style: "ieee")
set cite(style: "alphanumeric")
show bibliography: set par(spacing: 0.4cm)
show bibliography: set grid(align: top + left)
show bibliography: set text(17pt)
show bibliography: t => {
show grid.cell.where(x: 0): set text(fill: self.colors.secondary)
show grid.cell.where(x: 0): set align(top + left)
show link: set text(fill: gray)
t
}
// Lists & Enums
set list(
marker: (
(move(dx: 0.15cm, dy: 0.1cm, circle(radius: 0.2em, stroke: self.colors.primary + 2pt, fill: self.colors.primary))),
(move(dy: 0.2cm, circle(width: 0.2em, fill: black))),
(move(dy: 0.2cm, circle(width: 0.2em, fill: gray))),
),
body-indent: 1.2em,
)
set enum(
numbering: n => {
circle(stroke: self.colors.primary + 2pt, radius: 0.3cm)[
#align(center + horizon)[ #text(size: 12pt, fill: black)[#n] ]
]
},
body-indent: 0.6cm
)
// Code blocks
show: codly-init.with()
show raw.where(block: true): set text(size: 13pt)
// Hotfixes, the messy part
// https://github.com/touying-typ/touying/issues/136
set par(spacing: 0.65em)
body
}),
..args,
)
body
}
// -----------------------------------------------------------------------------
// Macros
// -----------------------------------------------------------------------------
/// Macro for a pdfpc note
///
/// Example:
///
/// ```typst
/// #note("This will show on pdfpc speaker notes")
/// ```
///
/// - text (str): Note for pdfpc
///
/// -> content
#let note(text) = [
#pdfpc.speaker-note(text)
]
/// Quote block for phrases. Has a color.primary rectangle in the left
///
/// Example:
///
/// ```typst
/// #quote-block[
/// #lorem(10)
/// ]
/// ```
///
/// - top-pad (length): Extra height of the quote colored block
/// - color (color): Color of the quote block
/// - spacing (length): Spacing after the `#quote-block`
///
/// -> content
#let quote-block(
top-pad: 0.55cm,
color: none,
spacing: 0.3cm,
body,
) = [
#touying-fn-wrapper((self: none) => [
// Grid with the design
#let g(s: 0cm, body) = [
#grid(
columns: (0.195cm, auto),
column-gutter: 0.7cm,
row-gutter: 0cm,
[
#rect(
fill: if color == none { self.colors.primary } else { color },
height: s + top-pad,
)
],
align(horizon, body),
)
]
// We compute its "auto" heigth and then print it with the correct height
#layout(size => {
let (height,) = measure(width: size.width, g(body))
g(s: height, body)
})
#v(spacing)
])
]
/// Block with title and content
///
/// Example:
///
/// ```typst
/// #color-block(title: [Advantages])[
/// - A
/// - B
/// - C
/// ]
/// ```
///
/// - title (content): Title for the color block
/// - icon (str): Icon to show at the left of the title (Tableau Icons) https://tabler.io/icons
/// - spacing (length): Spacing before and after the color block
/// - color (color): Color for the title block
/// - color-body (color): Color for the background of the body
///
/// -> content
#let color-block(
title: [],
icon: none,
spacing: 0.78em,
color: none,
color-body: none,
body
) = [
#import "@preview/tableau-icons:0.331.0": *
#touying-fn-wrapper((self: none) => [
#show emph: it => {
text(weight: "medium", fill: self.colors.primary, it.body)
}
#showybox(
title-style: (
color: black,
sep-thickness: 0pt,
),
frame: (
//inset: 0.4em,
radius: 0pt,
thickness: 0pt,
border-color: if color == none { self.colors.primary } else { color },
title-color: if color == none { self.colors.primary } else { color },
body-color: if color-body == none { self.colors.lite } else { color-body },
inset: (x: 0.55em, y: 0.65em),
),
above: spacing,
below: spacing,
title: if icon == none {
align(horizon)[#strong(title)]
} else {
align(horizon)[
#draw-icon(icon, height: 1.2em, baseline: 20%, fill: white) #h(0.2cm) #strong[#title]
]
},
body,
)
])
]
//vim:tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab colorcolumn=81

21
src/logos.typ Normal file
View file

@ -0,0 +1,21 @@
#let institute-logo(self) = [
#block[
#text(size: 13.5pt, weight: "medium")[#self.store.institute]
#h(0.1cm)
#box(inset: 0pt, outset: 0pt, baseline: 15%)[#circle(
radius: 0.25cm,
outset: 0pt,
inset: 0pt,
stroke: self.colors.primary + 2pt,
fill: none,
)]
]
]
#let unigrazgraz-logo = [
#set align(right)
#set text(size: 12pt, tracking: 3.6pt)
#image("assets/unigrazlogo-white-border.svg", width: 4.1cm)
]