diff --git a/packages/preview/athena-tu-darmstadt-exercise/0.3.0/LICENSE b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/LICENSE
new file mode 100644
index 0000000000..ba6d86ec0a
--- /dev/null
+++ b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 Joshua J.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/packages/preview/athena-tu-darmstadt-exercise/0.3.0/README.md b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/README.md
new file mode 100644
index 0000000000..5084aa0509
--- /dev/null
+++ b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/README.md
@@ -0,0 +1,115 @@
+# Typst Templates for the Corporate Design of TU Darmstadt :book:
+These **unofficial** templates enable you to write documents in [Typst](https://github.com/typst/typst) with the corporate design of [TU Darmstadt](https://www.tu-darmstadt.de/).
+
+#### Disclaimer
+Please ask your supervisor if you are allowed to use Typst and one of these templates for your thesis or other documents.
+Note that this template is not checked by TU Darmstadt for correctness.
+Thus, this template does not guarantee completeness or correctness.
+For notes for publishing on TUbama see [Publishing](#publishing-on-tubama).
+
+
+## Implemented Templates
+The templates imitate the style of the corresponding latex templates in [tuda_latex_templates](https://github.com/tudace/tuda_latex_templates).
+Note that there can be visual differences between the original latex template and the Typst template (you may open an issue when you find one).
+
+For missing features, ideas or other problems you can just open an issue :wink:. Contributions are also welcome.
+
+| Template | Preview | Example | Scope |
+|----------|---------|---------|-------|
+| [tudapub](https://github.com/JeyRunner/tuda-typst-templates/blob/main/tudapub/template/tudapub.typ) | | [example_tudapub.pdf](https://github.com/JeyRunner/tuda-typst-templates/blob/main/example_tudapub.pdf) [example_tudapub.typ](https://github.com/JeyRunner/tuda-typst-templates/blob/main/example_tudapub.typ) | Master and Bachelor thesis |
+| [tudaexercise](https://github.com/JeyRunner/tuda-typst-templates/blob/main/tudaexercise/template/tudaexercise.typ) | | [Example File](https://github.com/JeyRunner/tuda-typst-templates/blob/main/tudaexercise/example/main.typ) | Exercises |
+| [not-tudabeamer-2023](https://github.com/JeyRunner/tuda-typst-templates/blob/main/tudabeamer/template/lib.typ) | | [Example File](https://github.com/JeyRunner/tuda-typst-templates/blob/main/tudabeamer/example/main.typ) | Presentations |
+
+## Usage
+Create a new typst project based on this template locally.
+```bash
+# for tudapub
+typst init @preview/athena-tu-darmstadt-thesis
+cd athena-tu-darmstadt-thesis
+
+# for tudaexercise
+typst init @preview/athena-tu-darmstadt-exercise
+cd athena-tu-darmstadt-exercise
+
+# for not-tudabeamer-2023
+typst init @preview/not-tudabeamer-2023
+cd not-tudabeamer-2023
+```
+Or create a project on the typst web app based on this template.
+
+
+Or do a manual installation of this template.
+For a manual setup create a folder for your writing project and download this template into the `templates` folder:
+
+```bash
+mkdir my_thesis && cd my_thesis
+mkdir templates && cd templates
+git clone https://github.com/JeyRunner/tuda-typst-templates
+```
+
+
+### Logo and Font Setup
+Download the tud logo from [download.hrz.tu-darmstadt.de/protected/ULB/tuda_logo.pdf](https://download.hrz.tu-darmstadt.de/protected/ULB/tuda_logo.pdf) and put it into the `assets/logos` folder.
+Now execute the following script in the `assets/logos` folder to convert it into an svg:
+
+```bash
+cd assets/logos
+./convert_logo.sh
+```
+
+Note: The here used `pdf2svg` command might not be available. In this case we recommend a online converter like [PDF24 Tools](https://tools.pdf24.org/en/pdf-to-svg). There is also a [tool](https://github.com/FussballAndy/typst-img-to-local) to install images as local typst packages.
+
+Also download the required fonts `Roboto` and `XCharter`:
+```bash
+cd assets/fonts
+./download_fonts.sh
+```
+Optionally, you can install all fonts in the folders in `fonts` on your system. But you can also use Typst's `--font-path` option. Or install them in a folder and add the folder to `TYPST_FONT_PATHS` for a single font folder.
+
+Note: wget might not be available. In this case, either download it or replace the command with something like `curl -o -L`
+
+
+Create a main.typ file for the manual template installation.
+Create a simple `main.typ` in the root folder (`my_thesis`) of your new project:
+
+```js
+#import "templates/tuda-typst-templates/templates/tudapub/template/lib.typ": *
+
+#show: tudapub.with(
+ title: [
+ My Thesis
+ ],
+ author: "My Name",
+ accentcolor: "3d"
+)
+
+= My First Chapter
+Some Text
+```
+
+
+
+### Compile you typst file
+
+```bash
+typst watch main.typ --font-path assets/fonts/
+```
+
+This will watch your file and recompile it to a pdf when the file is saved. For writing, you can use [Vscode](https://code.visualstudio.com/) with these extensions: [Tinymist Typst](https://marketplace.visualstudio.com/items?itemName=myriad-dreamin.tinymist). Or use the [typst web app](https://typst.app/) (here you need to upload the logo and the fonts).
+
+Note that we add `--font-path` to ensure that the correct fonts are used.
+Due to a bug (typst/typst#2917 typst/typst#2098) typst sometimes uses the font `Roboto condensed` instead of `Roboto`.
+To be on the safe side, double-check the embedded fonts in the pdf (there should be no `Roboto condensed`).
+What also works is to uninstall/deactivate all `Roboto condensed` fonts from your system.
+
+### Publishing on TUbama
+For publishing your compiled document (e.g. thesis) on TUbama, the document has to comply with the pdf/A standard.
+Therefore, set the PDF standard for compiling for the final submission:
+```bash
+typst compile main.typ --font-path assets/fonts/ --pdf-standard a-2b
+```
+In case this should not yield a PDF which is accepted by TUbama, you can use a converter to convert from the Typst output to PDF/A, but check that there are no losses during the conversion.
+
+## Contributing
+
+See [CONTRIBUTING.md](https://github.com/JeyRunner/tuda-typst-templates/blob/main/./CONTRIBUTING.md)
diff --git a/packages/preview/athena-tu-darmstadt-exercise/0.3.0/example/fonts/download_fonts.sh b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/example/fonts/download_fonts.sh
new file mode 100755
index 0000000000..e2f0c711cc
--- /dev/null
+++ b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/example/fonts/download_fonts.sh
@@ -0,0 +1,26 @@
+#!/usr/bin/env bash
+SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
+cd "${SCRIPT_DIR}"
+
+echo "> will download fonts ..."
+
+
+# roboto font
+#wget https://dl.dafont.com/dl/?f=roboto
+wget https://mirrors.ctan.org/fonts/roboto.zip
+unzip -o roboto.zip
+mv roboto/opentype roboto_
+rm -r roboto
+mv roboto_ roboto
+rm roboto/RobotoCondensed*
+rm roboto/RobotoSerif_Condensed*
+rm roboto/RobotoSerif*
+rm roboto.zip
+
+# xcharta font
+wget http://mirrors.ctan.org/fonts/xcharter.zip
+unzip -o xcharter.zip
+mv xcharter/opentype xcharter_
+rm -r xcharter
+mv xcharter_ xcharter
+rm xcharter.zip
diff --git a/packages/preview/athena-tu-darmstadt-exercise/0.3.0/example/logos/convert_logo.sh b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/example/logos/convert_logo.sh
new file mode 100755
index 0000000000..bdfd81564c
--- /dev/null
+++ b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/example/logos/convert_logo.sh
@@ -0,0 +1,9 @@
+#!/bin/sh
+
+# first download the logo from:
+# https://download.hrz.tu-darmstadt.de/protected/ULB/tuda_logo.pdf
+
+cd "$(dirname "$0")"
+
+# install with: apt get install pdf2svg
+pdf2svg tuda_logo.pdf tuda_logo.svg
diff --git a/packages/preview/athena-tu-darmstadt-exercise/0.3.0/example/logos/tuda_logo_replace.svg b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/example/logos/tuda_logo_replace.svg
new file mode 100644
index 0000000000..de11a4ed4d
--- /dev/null
+++ b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/example/logos/tuda_logo_replace.svg
@@ -0,0 +1,21 @@
+
+
+
+
diff --git a/packages/preview/athena-tu-darmstadt-exercise/0.3.0/example/main.typ b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/example/main.typ
new file mode 100644
index 0000000000..1ae4126231
--- /dev/null
+++ b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/example/main.typ
@@ -0,0 +1,197 @@
+#import "@preview/athena-tu-darmstadt-exercise:0.3.0": (
+ difficulty-format, info-layout, point-format, subtask, task, task-points-header, text-roboto, tuda-difficulty-stars,
+ tuda-gray-info, tuda-section, tuda-subsection, tudaexercise,
+)
+
+#show: tudaexercise.with(
+ language: "en",
+ info: (
+ title: "Usage of TUDaExercise",
+ header_title: "TUDaExercise",
+ subtitle: "A small guide.",
+ author: (("Andreas", "129219"), "Dennis"),
+ term: auto,
+ date: datetime.today(),
+ sheet: 5,
+ group: 1,
+ tutor: "Dr. John Smith",
+ lecturer: "Prof. Dr. Jane Doe",
+ ),
+ info-layout: info-layout.exercise(),
+ headline: ("title", "name", "id"),
+ logo: image("logos/tuda_logo_replace.svg"),
+ design: (
+ accentcolor: "0b",
+ colorback: true,
+ darkmode: "darkmode" in sys.inputs,
+ ),
+ task-prefix: auto,
+ task-prefix-subtasks: false,
+)
+
+#set enum(spacing: 1em, numbering: "1.", indent: 5pt)
+#set list(marker: [--], indent: 5pt, spacing: 1em)
+
+= Most basic usage
+
+The easiest way is by using `typst init` like on this templates universe page. But here is everything broken down:
+
+== Add to typst
++ Import the package: `#import "@preview/athena-tu-darmstadt-exercise:0.3.0": *`
+
++ Apply the template using `#show: tudaexercise.with()`
+
+== Fonts
+The template requires the following fonts: Roboto and XCharter. Typst right now does not allow fonts to be installed as packages. So you will either need to install them locally or configure Typst and co. to use the fonts.
+
+#tuda-gray-info(title: "For more info:")[
+ https://github.com/JeyRunner/tuda-typst-templates?tab=readme-ov-file#logo-and-font-setup
+]
+
+== Logo
+Similarly as the logo is protected and Typst does not have a folder for global resources you will need to setup the logo manually. You will need to download the logo and convert it into a svg. Then pass the `logo: image()` option to this package. The height of the logo will automatically be set to 22mm.
+
+Additionally, a partner or institution logo can be passed using the `sublogo` parameter.
+
+= Configuring the title
+All options of the title can be controlled using the `info` dictionary:
+
+```
+info: (
+ title: "The big title",
+ header_title: "The title in the page header",
+ subtitle: "The smaller title below",
+ author: "The author",
+ // author: ("Author 1", "Author 2"), // can also be an array of authors
+ // author: (("Author 1", "123456"), "Author 2"), // or the matriculation number can be provided
+
+ term: "The current term aka. semester",
+ // term: auto, // can also be inferred automatically
+ date: "The current date",
+ // date: datetime.today(), // can also be a datetime object
+ // _date: datetime.today(), // can start with an underscore to control the date for automatic term generation but not show date in the info
+ sheet: 0, // The current sheetnumber
+
+ // submission extras:
+ group: "05", // the lecture group you are in
+ tutor: "John", // the tutor of your group
+ lecturer: "Karpfen", // the lecturer of the module that this assignment is for
+)
+```
+The options can also be left empty. Then their corresponding item will not appear.
+
+Additionally there is the `info-layout` field which controls the subline of the title's look. By default this is set to the exercise version. There also is a submission version which displays the submission's additional information fields. Or, if both don't fit your needs, you can also pass raw content to the field and control the subline to your will. \
+For more info see the exported `info-layout` module of this template.
+
+If you do not want to have a title card you can also set `show-title` to `false`.
+
+= Design
+
+You can control the design using the following options of the `design` dictionary:
+
+```
+design: (
+ accentcolor: "0b", // either be color code of the TUDa coloring scheme or a typst color object
+ colorback: true, // whether the title should have the accent color as background,
+ darkmode: false, // If you like a dark background
+)
+```
+
+Furthermore using the `tud_design` state you get a dictionary with the following colors used by the template: ` text_color, background_color, accent_color, text_on_accent_color`.
+
+Note that changing any of the state's values will have no effect on the template. See the state as read-only.
+
+If you do not like lines around subtasks you can pass `subtask: "plain"` to not show the lines.
+
+= More options
+
+The leftover options are:
+- `language` to control the language of certain keywords (can either be `"de"` or `"en"`)
+- `margins` which is a dictionary controlling the page margins
+- `paper` which currently only supports `"a4"`
+- `headline` control the headline. The following values are supported:
+ - An array (or single string) with keys `"title"`, `"name"` and `"id"` for the default headline style. Further, `"fl"` can be provided to control the order of first and last name in the header.
+ - Raw `content` that will be displayed
+ - `none` or `()` for no headline
+
+
+= Creating tasks
+
+Creating tasks is fairly easy. You simply write
+```
+= Title of your task
+```
+Similarly subtasks are created using
+```
+== Title of your subtask
+```
+
+If you dislike the default task format, you can slightly customize it using the `task-prefix`, `task-separator` and `task-prefix-subtasks` fields of the template.
+
+= Tasks with points and difficulty #task-points-header(points: 5, difficulty: 2.65)
+== Task point header #task-points-header(points: 2)
+If you want to add points and difficulty to your tasks, you can use the `task-points-header` function. This will add a header to the task with the points and difficulty.
+You can pass the following parameters:
+- `points` (int or float): The amount of points of the task
+- `difficulty` (int or float): The difficulty rating the task, must be a number between 0 and `max-difficulty`
+- `max-difficulty` (int): The maximum difficulty, default is 5
+- `hspace` (length): The horizontal space between the task title and the points, default is 1em
+- `details-seperator` (string): The string that separates the task title from the points
+ header, default is `", "`
+- `star-fill` (color): The fill color of the stars, default is the currentaccent color
+- `points-function` (function): The function to format the points, default is `point-format`
+- `difficulty-function` (function): The function to format the difficulty, default is `tuda-difficulty-stars`, but you can also pass `difficulty-format` to use a more simple text representation of the difficulty (or even a custom function). See @task-and-subtask-commands to see `difficulty-format` in action.
+
+For example you can writethe following command to recreate the header of this task:
+```typst
+= Tasks with points and difficulty #task-points-header(points: 5, difficulty: 2.65)
+== Task point header #task-points-header(points: 2)
+```
+
+== Task and subtask commands #task-points-header(points: 1, difficulty: 1, difficulty-function: difficulty-format)
+Instead of the normal section and subsection commands you can also use the `task` and `subtask` functions to create tasks and subtasks with points and difficulty:
+```typst
+#task(points: 5, difficulty: 3.69)[Tasks with *points* and _difficulty_]
+// you can also just pass the points and omit the title if desired
+#subtask(points: 2)
+```
+They take the same parameters as the `task-points-header` function, but additionally you can pass a `title` parameter to set the title of the task or subtask.
+== Advanced task header styling (#task-points-header(
+ points: 2,
+ difficulty: 1.5,
+ max-difficulty: 3,
+ details-seperator: " | ",
+ hspace: none,
+ star-fill: blue,
+ points-function: point-format.with(points-name-single: "Bonus point", points-name-plural: "Bonus points"),
+ difficulty-function: tuda-difficulty-stars.with(difficulty-name: "Effort", edges: 6, rotation: 45deg, baseline: 2pt),
+))
+As mentioned above, you can overwrite the point- and difficulty functions of the `task-points-header` function. This allows you to customize the header even further. For example, you can change the number of edges of the stars, the rotation of the stars, or the fill color of the stars:
+```typst
+== Advanced task header styling (#task-points-header(points: 2, difficulty: 1.5, max-difficulty: 3, details-seperator: " | ", hspace: none, star-fill: blue, points-function: point-format.with(points-name-single: "Bonus point", points-name-plural: "Bonus points", baseline: 2pt), difficulty-function: tuda-difficulty-stars.with(difficulty-name: "Effort", edges: 6, rotation: 45deg)))
+```
+#tuda-gray-info(title: "Note:")[
+ Passing all these parameters everytime is a bit cumbersome, but since typst #link("https://github.com/typst/typst/issues/147")[does not yet support user-defined elements], this is the only way to archieve this without relying on states. You can create your own function to simplify this if you want to: ```typst
+ #let custom-tph = task-points-header.with(points-function: point-format.with(points-name-single: "Bonus point", points-name-plural: "Bonus points"), difficulty-function: difficulty-format)
+ ```
+]
+
+#pagebreak()
+
+#tuda-subsection("Sections")
+
+If you want to create an unnumbered section you can use the `tuda-section` or `tuda-subsection` functions accordingly. Simply pass the section title as a string.
+```
+#tuda-subsection("Sections")
+```
+
+= Currently not supported features from the LaTeX template and the why
+
++ Points -- This would require a state and make declaring tasks far more complex than just using headings. Though technically the points can also be written manually into the task title.
+
++ Solutions -- Enabling whether solutions should be shown or not from within the template would again require a state and is thus rather costly. However you can implement them rather easily as from outside the template a boolean will already do.
+
+= Migrations from v0.2.0 to v0.3.0
+
+- The `title-sub` parameter was renamed to `info-layout`. Further, it now generates no subline, if set to `none`, or no relevant info keys are passed.
+- A `task-prefix` of `none` now removes the task prefix. Instead, `auto` should be passed, to have the default task prefix.
diff --git a/packages/preview/athena-tu-darmstadt-exercise/0.3.0/preview/tudaexercise-dark.png b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/preview/tudaexercise-dark.png
new file mode 100644
index 0000000000..ffe741dfd6
Binary files /dev/null and b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/preview/tudaexercise-dark.png differ
diff --git a/packages/preview/athena-tu-darmstadt-exercise/0.3.0/preview/tudaexercise-light.png b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/preview/tudaexercise-light.png
new file mode 100644
index 0000000000..0d69063fb8
Binary files /dev/null and b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/preview/tudaexercise-light.png differ
diff --git a/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/common/addons/difficulty-points.typ b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/common/addons/difficulty-points.typ
new file mode 100644
index 0000000000..9c71eecb56
--- /dev/null
+++ b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/common/addons/difficulty-points.typ
@@ -0,0 +1,95 @@
+#import "../tudacolors.typ": tuda_colors
+/// Draws a star with the given number of edges, size, stroke width, fill color and rotation. Usage:
+/// ```example
+/// #draw-star(fill: red)
+/// ```
+///
+/// - edges (int): The number of edges of the star. Default is 5.
+/// - size (length): The size of the star. Default is 1cm.
+/// - stroke (length): The stroke width of the star. Default is 1.5pt.
+/// - fill (color): The fill color of the star. Default is red.
+/// - rotation (angle): The rotation of the star in degrees. Default is 90deg.
+/// - baseline (length): The baseline of the star. Default is 0.5pt.
+/// -> Returns: A star shape.
+#let draw-star(edges: 5, size: 1em, stroke: .8pt, fill: red, rotation: 270deg, baseline: 0.5pt) = {
+ let inner_size = size / 2 - stroke
+ let outer_r = inner_size
+ let inner_r = inner_size * 0.4
+ let center_p = (inner_size, inner_size)
+ let points = ()
+ for idx in range(edges * 2) {
+ let angle = idx * (360deg / (edges * 2)) + rotation
+ let radius = if calc.rem(idx, 2) == 0 { outer_r } else { inner_r }
+ points.push((
+ center_p.at(0) + radius * calc.cos(angle),
+ center_p.at(1) + radius * calc.sin(angle),
+ ))
+ }
+ box(width: size, height: size, baseline: baseline, inset: 0pt, outset: 0pt, align(center + horizon, curve(
+ stroke: stroke,
+ fill: fill,
+ curve.move(points.remove(0)),
+ ..points.map(p => curve.line(p)),
+ curve.close(mode: "straight"),
+ )))
+}
+
+/// Draws a number of stars to represent the difficulty of a task.
+///
+/// - difficulty (float): The difficulty of the task, must be between 0 and `max-difficulty`.
+/// - max_difficulty (int): The maximum difficulty, default is 5.
+/// - fill (color): The fill color of the stars, default is `rgb(tuda_colors.at("3b"))`.
+/// - spacing (length): The spacing between the stars, default is 2pt.
+/// - difficulty-name (str): The name of the difficulty, prefix for the stars, default is `none`.
+/// - otherargs: Additional arguments to pass to the `draw-star` function.
+/// -> Returns: A canvas with the stars drawn on it.
+#let difficulty-stars(
+ difficulty,
+ max-difficulty: 5,
+ fill: rgb(tuda_colors.at("3b")),
+ spacing: 2pt,
+ difficulty-name: none,
+ difficulty-sep: ": ",
+ ..otherargs,
+) = {
+ assert(type(difficulty) in (float, int), message: "difficulty must be a number")
+ assert.eq(type(max-difficulty), int, message: "max-difficulty must be an integer")
+ assert(
+ difficulty >= 0 and difficulty <= max-difficulty,
+ message: "difficulty must be between 0 and " + str(max-difficulty),
+ )
+ assert.eq(type(fill), color, message: "fill must be a color, got " + str(type(fill)))
+ let remaining_difficulty = difficulty
+ let first = true
+ if difficulty-name != none {
+ difficulty-name
+ difficulty-sep
+ }
+ for d in range(max-difficulty) {
+ let fill_percentage = if remaining_difficulty > 0 {
+ 100% * calc.min(1, remaining_difficulty)
+ } else {
+ 0%
+ }
+ if first {
+ first = false
+ } else {
+ h(spacing)
+ }
+ draw-star(
+ fill: if fill_percentage > 0% {
+ gradient.linear(
+ (fill, 0%),
+ (fill, fill_percentage),
+ (rgb("#00000000"), fill_percentage),
+ (rgb("#00000000"), 100%),
+ angle: 0deg,
+ )
+ } else {
+ rgb("#00000000")
+ },
+ ..otherargs,
+ )
+ remaining_difficulty -= 1
+ }
+}
diff --git a/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/common/colorutil.typ b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/common/colorutil.typ
new file mode 100644
index 0000000000..77b6f4a4e0
--- /dev/null
+++ b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/common/colorutil.typ
@@ -0,0 +1,27 @@
+#let calc-color-component(srgb) = {
+ if srgb <= 0.03928 {
+ return srgb / 12.92
+ } else {
+ return calc.pow((srgb + 0.055) / 1.055, 2.4)
+ }
+}
+
+#let calc-relative-luminance(color) = {
+ let rgb_color = rgb(color.to-hex())
+ let components = rgb_color.components(alpha: false)
+
+ let r_srgb = float(components.at(0))
+ let b_srgb = float(components.at(1))
+ let g_srgb = float(components.at(2))
+ return (
+ calc-color-component(r_srgb) * 0.2126
+ + calc-color-component(b_srgb) * 0.0722
+ + calc-color-component(g_srgb) * 0.7152
+ )
+}
+
+#let calc-contrast(rl1, rl2) = {
+ let l1 = calc.max(rl1, rl2)
+ let l2 = calc.min(rl1, rl2)
+ return (l1 + 0.05) / (l2 + 0.05)
+}
diff --git a/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/common/footnotes.typ b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/common/footnotes.typ
new file mode 100644
index 0000000000..d190ca2992
--- /dev/null
+++ b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/common/footnotes.typ
@@ -0,0 +1,24 @@
+
+#let tud-footnote(
+ footnote_rewritten_fix_alignment_hanging_indent: true,
+ it,
+) = {
+ if footnote_rewritten_fix_alignment {
+ let loc = it.note.location()
+ let it_counter_arr = counter(footnote).at(loc)
+ let idx_str = numbering(it.note.numbering, ..it_counter_arr)
+ //[#it.fields()]
+
+ stack(dir: ltr, h(5pt), super(idx_str), {
+ // optional add indent to multi-line footnote
+ if footnote_rewritten_fix_alignment_hanging_indent {
+ par(hanging-indent: 5pt)[#it.note.body]
+ } else {
+ it.note.body
+ }
+ })
+ } else {
+ // if not footnote_rewritten_fix_alignment keep as is
+ it
+ }
+}
diff --git a/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/common/format.typ b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/common/format.typ
new file mode 100644
index 0000000000..560dc96f5d
--- /dev/null
+++ b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/common/format.typ
@@ -0,0 +1,17 @@
+#let format-date(date, language) = if type(date) != datetime {
+ date
+} else if language == "de" {
+ date.display("[day].[month repr:numerical].[year]")
+} else {
+ date.display("[month repr:long] [day], [year]")
+}
+
+#let text-roboto(body) = {
+ set text(font: "Roboto")
+ body
+}
+
+#let text-xcharter(body) = {
+ set text(font: "XCharter")
+ body
+}
diff --git a/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/common/headings.typ b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/common/headings.typ
new file mode 100644
index 0000000000..3bdef26104
--- /dev/null
+++ b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/common/headings.typ
@@ -0,0 +1,100 @@
+#import "props.typ": tud_header_line_height, tud_heading_line_thin_stroke
+
+#let tud_body_line_height = tud_header_line_height / 2
+
+#let tud-heading-with-lines(
+ heading_margin_before: 0mm,
+ heading_line_spacing: 0mm,
+ text-size: 14.3pt,
+ text-weight: "bold",
+ text-prefix: "",
+ text-suffix: "",
+ counter-suffix: "",
+ it,
+) = {
+ set text(
+ font: "Roboto",
+ fallback: false,
+ weight: text-weight,
+ size: text-size,
+ )
+ //set block(below: 0.5em, above: 2em)
+ block(
+ breakable: false,
+ inset: 0pt,
+ outset: 0pt,
+ fill: none,
+ //above: heading_margin_before,
+ //below: 0.6em //+ 10pt
+ )[
+ #stack(
+ v(heading_margin_before),
+ line(length: 100%, stroke: tud_heading_line_thin_stroke),
+ v(heading_line_spacing),
+ block({
+ text-prefix
+ if it.outlined and it.numbering != none {
+ counter(heading).display(it.numbering)
+ counter-suffix
+ h(2pt)
+ }
+ it.body
+ text-suffix
+ }),
+ v(heading_line_spacing),
+ line(length: 100%, stroke: tud_heading_line_thin_stroke),
+ v(10pt),
+ )
+ ]
+}
+
+#let tuda-section-lines(above: 1.8em, below: 1.2em, ruled: true, body) = {
+ block(
+ width: 100%,
+ inset: (y: 0.2em),
+ outset: 0mm,
+ above: above,
+ below: below,
+ stroke: if ruled {
+ (y: tud_body_line_height)
+ },
+ body,
+ )
+}
+
+
+/// Creates a section similar to headers
+/// But does not add other text or a counter.
+/// ```
+/// #tuda-section("Lorem ipsum")
+/// ```
+/// - title (str): The title of this section
+#let tuda-section(title) = {
+ tuda-section-lines(text(title, font: "Roboto", weight: "bold", size: 11pt))
+}
+
+/// Creates a subsection similar to level 2 headers.
+/// But does not add other text or a counter.
+/// ```
+/// #tuda-subsection("Lorem ipsum")
+/// ```
+/// - title (str): The title of this subsection
+/// - ruled (bool): Whether to add lines around the section
+#let tuda-subsection(title, ruled: true) = {
+ tuda-section-lines(above: 1.4em, below: 1em, ruled: ruled, text(title, font: "Roboto", weight: "regular", size: 11pt))
+}
+
+#let tuda-subsection-unruled = tuda-subsection.with(ruled: false)
+
+/// Creates a subsection similar to level 3 headers and beyond.
+/// But does not add other text or a counter.
+/// ```
+/// #tuda-nthsection("Lorem ipsum")
+/// ```
+/// - title (str): The title of this (sub)+section
+/// - ruled (bool): Whether to add lines around the section
+#let tuda-nthsection(title, ruled: true) = tuda-section-lines(above: 1em, below: 0.7em, ruled: ruled, text(
+ title,
+ font: "Roboto",
+ weight: "regular",
+))
diff --git a/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/common/page_components.typ b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/common/page_components.typ
new file mode 100644
index 0000000000..445dc31d83
--- /dev/null
+++ b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/common/page_components.typ
@@ -0,0 +1,21 @@
+#let tud-header(
+ accentcolor_rgb: "55555555",
+ content: [],
+) = {
+ box(
+ //fill: white,
+ grid(
+ rows: auto,
+ rect(
+ fill: color.rgb(accentcolor_rgb),
+ width: 100%,
+ height: 4mm, //- 0.05mm
+ ),
+ v(1.4mm + 0.25mm),
+ // should be 1.4mm according to guidelines
+ line(length: 100%, stroke: 1.2pt),
+ //+ 0.1pt) // should be 1.2pt according to guidelines
+ content,
+ ),
+ )
+}
diff --git a/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/common/props.typ b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/common/props.typ
new file mode 100644
index 0000000000..2f5426e3ed
--- /dev/null
+++ b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/common/props.typ
@@ -0,0 +1,43 @@
+
+
+#let tud_heading_line_thin_stroke = 0.75pt
+
+#let tud_header_line_height = 1.2pt
+#let tud_inner_page_margin_top = 22pt
+#let tud_title_logo_height = 22mm
+
+
+#let tud_page_margin_title_page = (
+ top: 15mm,
+ left: 15mm,
+ right: 15mm,
+ bottom: 15mm - 1mm, // should be 20mm according to guidelines
+)
+
+#let tud_page_margin_small = tud_page_margin_title_page
+
+
+// Same margins as the default ones in the tudpub latex template
+#let tud_page_margin_big = (
+ top: 30mm, //35.25mm + 0.05mm,//+ 0.02mm,
+ left: 31.5mm,
+ right: 31.5mm,
+ bottom: 56mm,
+)
+
+
+
+
+#let tud_page_margin_medium = (
+ top: 15mm,
+ left: 25mm,
+ right: 25mm,
+ bottom: 15mm - 1mm, // should be 20mm according to guidelines
+)
+
+#let tud_exercise_page_margin = (
+ top: 15mm,
+ left: 15mm,
+ right: 15mm,
+ bottom: 20mm,
+)
diff --git a/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/common/thesis_statement_pursuant.typ b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/common/thesis_statement_pursuant.typ
new file mode 100644
index 0000000000..3f315898f3
--- /dev/null
+++ b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/common/thesis_statement_pursuant.typ
@@ -0,0 +1,63 @@
+#import "format.typ": *
+
+#let tudapub-get-thesis-statement-pursuant(
+ date: none,
+ author: none,
+ location: none,
+ include-english-translation: false,
+ signature: none,
+) = [
+ #set heading(outlined: false)
+
+ #block(breakable: false)[
+ === Erklärung zur Abschlussarbeit gemäß § 22 Abs. 7 APB TU Darmstadt
+
+ Hiermit erkläre ich, #author, dass ich die vorliegende Arbeit gemäß § 22 Abs. 7 APB TU Darmstadt selbstständig, ohne Hilfe Dritter und nur mit den angegebenen Quellen und Hilfsmitteln angefertigt habe. Ich habe mit Ausnahme der zitierten Literatur und anderer in der Arbeit genannter Quellen keine fremden Hilfsmittel benutzt. Die von mir bei der Anfertigung dieser wissenschaftlichen Arbeit wörtlich oder inhaltlich benutzte Literatur und alle anderen Quellen habe ich im Text deutlich gekennzeichnet und gesondert aufgeführt. Dies gilt auch für Quellen oder Hilfsmittel aus dem Internet.
+
+ Diese Arbeit hat in gleicher oder ähnlicher Form noch keiner Prüfungsbehörde vorgelegen.
+
+ Mir ist bekannt, dass im Falle eines Plagiats (§38 Abs.2 APB) ein Täuschungsversuch vorliegt, der dazu führt, dass die Arbeit mit 5,0 bewertet und damit ein Prüfungsversuch verbraucht wird. Abschlussarbeiten dürfen nur einmal wiederholt werden.
+
+ Bei einer Thesis des Fachbereichs Architektur entspricht die eingereichte elektronische Fassung dem vorgestellten Modell und den vorgelegten Plänen.
+ ]
+
+ #block(breakable: false)[
+ #if include-english-translation [
+ === English translation for information purposes only: \ Thesis Statement pursuant to § 22 paragraph 7 of APB TU Darmstadt
+
+ I herewith formally declare that I, #author, have written the submitted thesis independently pursuant to § 22 paragraph 7 of APB TU Darmstadt without any outside support and using only the quoted literature and other sources. I did not use any outside support except for the quoted literature and other sources mentioned in the paper. I have clearly marked and separately listed in the text the literature used literally or in terms of content and all other sources I used for the preparation of this academic work. This also applies to sources or aids from the Internet.
+
+ This thesis has not been handed in or published before in the same or similar form.
+
+ I am aware, that in case of an attempt at deception based on plagiarism (§38 Abs. 2 APB), the thesis would be graded with 5,0 and counted as one failed examination attempt. The thesis may only be repeated once.
+
+ For a thesis of the Department of Architecture, the submitted electronic version corresponds to the presented model and the submitted architectural plans.
+ ]
+
+ #v(1.4cm)
+
+ #table(
+ columns: (1fr, auto),
+ inset: 0pt,
+ stroke: none,
+ align: horizon,
+ [
+ #location,
+ #format-date(date, "de")
+ ],
+ align(right)[
+ #stack(
+ [
+ #set image(width: 4.5cm, height: 1cm, fit: "contain")
+ #place(center + bottom)[#signature]
+ ],
+ line(length: 5cm, stroke: 0.3pt),
+ v(2mm),
+ align(center)[
+ #author
+ ],
+ )
+ ],
+ )
+ ]
+]
diff --git a/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/common/tudacolors.typ b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/common/tudacolors.typ
new file mode 100644
index 0000000000..79f7f5679f
--- /dev/null
+++ b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/common/tudacolors.typ
@@ -0,0 +1,106 @@
+
+
+
+// tuda colors as key-value array
+// from https://github.com/tudace/tuda_latex_templates/blob/main/tex/tudacolors.def
+#let tuda_colors = (
+ "0d": "535353",
+ "0c": "898989",
+ "0b": "B5B5B5",
+ "0a": "DCDCDC",
+ "1a": "5D85C3",
+ "2a": "009CDA",
+ "3a": "50B695",
+ "4a": "AFCC50",
+ "5a": "DDDF48",
+ "6a": "FFE05C",
+ "7a": "F8BA3C",
+ "8a": "EE7A34",
+ "9a": "E9503E",
+ "10a": "C9308E",
+ "11a": "804597",
+ "1b": "005AA9",
+ "2b": "0083CC",
+ "3b": "009D81",
+ "4b": "99C000",
+ "5b": "C9D400",
+ "6b": "FDCA00",
+ "7b": "F5A300",
+ "8b": "EC6500",
+ "9b": "E6001A",
+ "10b": "A60084",
+ "11b": "721085",
+ "1c": "004E8A",
+ "2c": "00689D",
+ "3c": "008877",
+ "4c": "7FAB16",
+ "5c": "B1BD00",
+ "6c": "D7AC00",
+ "7c": "D28700",
+ "8c": "CC4C03",
+ "9c": "B90F22",
+ "10c": "951169",
+ "11c": "611C73",
+ "1d": "243572",
+ "2d": "004E73",
+ "3d": "00715E",
+ "4d": "6A8B22",
+ "5d": "99A604",
+ "6d": "AE8E00",
+ "7d": "BE6F00",
+ "8d": "A94913",
+ "9d": "961C26",
+ "10d": "732054",
+ "11d": "4C226A",
+)
+
+#let text_colors = (
+ "0a": black,
+ "0b": white,
+ "0c": white,
+ "0d": white,
+ "1a": white,
+ "1b": white,
+ "1c": white,
+ "1d": white,
+ "2a": white,
+ "2b": white,
+ "2c": white,
+ "2d": white,
+ "3a": white,
+ "3b": white,
+ "3c": white,
+ "3d": white,
+ "4a": black,
+ "4b": white,
+ "4c": white,
+ "4d": white,
+ "5a": black,
+ "5b": black,
+ "5c": black,
+ "5d": white,
+ "6a": black,
+ "6b": black,
+ "6c": white,
+ "6d": white,
+ "7a": black,
+ "7b": black,
+ "7c": white,
+ "7d": white,
+ "8a": white,
+ "8b": white,
+ "8c": white,
+ "8d": white,
+ "9a": white,
+ "9b": white,
+ "9c": white,
+ "9d": white,
+ "10a": white,
+ "10b": white,
+ "10c": white,
+ "10d": white,
+ "11a": white,
+ "11b": white,
+ "11c": white,
+ "11d": white,
+)
diff --git a/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/common/tudapub_outline.typ b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/common/tudapub_outline.typ
new file mode 100644
index 0000000000..2fb1bcd291
--- /dev/null
+++ b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/common/tudapub_outline.typ
@@ -0,0 +1,193 @@
+
+
+#let tudapub-make-outline-table-of-contents(
+ // How the table of contents outline is displayed.
+ // Either "adapted": use the default typst outline and adapt the style
+ // or "rewritten": use own custom outline implementation which better reproduces the look of the original latex template.
+ // Note that this may be less stable than "adapted", thus when you notice visual problems with the outline switch to "adapted".
+ outline_table_of_contents_style: "rewritten",
+
+ // style/layout parameters
+ fill_dot_space: 3pt,
+ heading_numbering_intent: 1em,
+ heading_numbering_width_per_level: 0.65em,
+ heading_first_level_v_space: 15pt,
+ heading_page_number_width: 1.5em,
+
+ // all headings with a level larger than this number will be excluded from the outline
+ heading_numbering_max_level: none,
+) = {
+ // check args
+ if not (outline_table_of_contents_style == "adapted" or outline_table_of_contents_style == "rewritten") {
+ panic("outline_table_of_contents_style has to be either 'adapted' or 'rewritten'")
+ }
+
+ // alternative (simpler than next solution)
+ if outline_table_of_contents_style == "adapted" {
+ show outline.entry.where(
+ level: 1,
+ ): it => {
+ // prevent recursion
+ if it.fill != none {
+ // new outline entry without fill
+ let params = it.fields()
+ params.fill = none
+ //params.page = [#params.page]
+ strong[
+ #set text(
+ font: "Roboto",
+ fallback: false,
+ )
+ #v(14pt, weak: true)
+ #outline.entry(..params.values())
+ ]
+ } else {
+ it
+ }
+ }
+
+ outline(
+ target: heading.where(outlined: true),
+ depth: heading_numbering_max_level,
+ indent: 1em,
+ fill: block(width: 100% - 1em)[
+ #repeat[ #h(fill_dot_space) . #h(fill_dot_space) ]
+ ],
+ )
+ }
+ //*/
+
+ // own outline elements
+ // @deprecated
+ /*
+ let mem-lengths = state("mem-lengths", ())
+ let space = 1em
+ show outline.entry: it => style(styles => {
+ locate(loc => {
+ let el = it.element
+ let c-heading = numbering(el.numbering, ..counter(heading).at(el.location()))
+
+ let indent = if it.level > 1 {
+ mem-lengths.at(loc).at(it.level - 2)
+ } else {0pt}
+
+ let preamb = [#h(indent)#c-heading#h(space)]
+ let size = measure(preamb, styles)
+
+ mem-lengths.update(array => {
+ if array.len() > it.level - 1 {
+ array.at(it.level - 1) = size.width
+ } else {
+ array.push(size.width)
+ }
+ array
+ })
+
+ /*
+ let text_params = ()
+ if it.level == 1 {
+ text_params = (
+ font: "Roboto",
+ fallback: false,
+ weight: "bold"
+ )
+ }
+ set text(
+ ..text_params
+ )*/
+ [
+ #preamb#el.body
+ #box(width: 1fr, repeat[ #h(fill_dot_space) . #h(fill_dot_space)])
+ #box(width: 1.5em)[
+ #align(right)[#it.page]
+ ]
+ //#it.page
+ ]
+ })
+ })
+ */
+
+ // own rewritten outline
+ if outline_table_of_contents_style == "rewritten" {
+ heading(
+ level: 1,
+ outlined: false,
+ )[Contents]
+
+ context {
+ let headings = query(selector(heading.where(outlined: true)).after(here()))
+
+ // outline params
+ let heading_numbering_intent = 1em
+ let heading_numbering_width_per_level = 0.65em
+ let heading_first_level_v_space = 15pt
+ let heading_page_number_width = 1.5em
+
+ // go over all headings
+ for it in headings {
+ if it.level > heading_numbering_max_level {
+ continue
+ }
+
+ // save location and page of current heading
+ let it_loc = it.location()
+ //let it_page = numbering(it_loc.page-numbering(), ..counter(page).at(it_loc))
+ let page = counter(page).at(it.location())
+ let it_counter_arr = counter(heading).at(it_loc)
+
+ let numbering_width = heading_numbering_width_per_level * it.level
+
+ let sum_prev_levels = range(it.level).sum()
+ let padd = (
+ (it.level - 1) * heading_numbering_intent + (sum_prev_levels) * heading_numbering_width_per_level * 1
+ )
+ // box[#pad(left: padd)
+ let preamb = box(fill: none)[#pad(left: padd)[
+ #box(width: numbering_width + heading_numbering_intent, fill: none, {
+ // if heading has numbering
+ if it.numbering != none {
+ numbering(it.numbering, ..it_counter_arr) //.display(it.numbering)
+ } else {
+ //numbering("1.1", ..it_counter_arr) + "?"
+ }
+ })
+ //#h(1em)
+ ]]
+
+ //let t = 0
+ //t = measure(preamb, styles).width
+ //[#t]
+
+ // only count, if the heading is numbered!
+ let text_params = ()
+ let fill_dots = box(width: 1fr, repeat[ #h(fill_dot_space) . #h(fill_dot_space)])
+
+ // heading with level 1 has different styling
+ if it.level == 1 {
+ text_params = (
+ font: "Roboto",
+ fallback: false,
+ weight: "bold",
+ )
+ fill_dots = box(width: 1fr) //, repeat(str.from-unicode(32)))
+ v(heading_first_level_v_space, weak: true)
+ }
+ set text(
+ ..text_params,
+ )
+ link(it_loc)[
+ #preamb#it.body
+ #fill_dots
+ #box(width: heading_page_number_width)[
+ #align(right)[#page.join()]
+ ]
+ //#it.page
+ //\
+ //#it.fields()
+ //#outline.entry(it.level, it, [Test], [], [])
+ \
+ ]
+ }
+ }
+ }
+}
diff --git a/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/common/tudapub_title_page.typ b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/common/tudapub_title_page.typ
new file mode 100644
index 0000000000..87346e0401
--- /dev/null
+++ b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/common/tudapub_title_page.typ
@@ -0,0 +1,246 @@
+#import "props.typ": *
+#import "format.typ": *
+#import "tudacolors.typ": tuda_colors
+
+
+// note the page needs to have the correct margins.
+// Set these up before
+#let tudpub-make-title-page(
+ title: [Title],
+ subtitle: [Subtitle],
+
+ // "master" or "bachelor" thesis
+ thesis_type: "master",
+
+ // the code of the accentcolor.
+ // A list of all available accentcolors is in the list tuda_colors
+ accentcolor: "9c",
+
+ // language for correct hyphenation
+ language: "en",
+
+ // author name as text, e.g "Albert Author"
+ author: "A Author",
+
+ // date of submission as string
+ date_of_submission: datetime(
+ year: 2023,
+ month: 10,
+ day: 4,
+ ),
+
+ location: "Darmstadt",
+
+ // array of the names of the reviewers
+ reviewer_names: ("Super Supervisor 1", "Super Supervisor 2"),
+
+ // tuda logo, has to be a svg. E.g. image("PATH/TO/LOGO")
+ logo_tuda: none,
+
+ // optional sub-logo of an institute.
+ // E.g. image("logos/iasLogo.jpeg")
+ logo_institute: none,
+
+ // How to set the size of the optional sub-logo.
+ // either "width": use tud_logo_width*(2/3)
+ // or "height": use tud_logo_height*(2/3)
+ logo_institute_sizeing_type: "width",
+
+ // Move the optional sub-logo horizontally
+ logo_institute_offset_right: 0mm,
+
+ // an additional white box with content for e.g. the institute, ... below the tud logo.
+ // E.g. logo_sub_content_text: [ Institute A \ filed of study: \ B]
+ logo_sub_content_text: none,
+
+ // Minimum title height is always 3.5em.
+ // If set to auto, title height is inferred from content.
+ title_height: auto,
+) = {
+ // vars
+ let accentcolor_rgb = tuda_colors.at(accentcolor)
+ let title_separator_spacing = 15pt
+ let title = [#title]
+ let title_page_inner_margin_left = 8pt
+ let logo_tud_height = 22mm
+
+ let make-title-box-content(content_width: 100%) = {
+ [
+ #set text(
+ font: "Roboto",
+ weight: "bold",
+ size: 35.86pt,
+ )
+ #set par(
+ justify: false,
+ leading: 20pt,
+ )
+ #box(width: content_width)[#title]
+ ]
+ }
+
+ let submission_date = format-date(date_of_submission, language)
+
+ let thesis_type_text = {
+ if lower(thesis_type) == "master" { "Master" } else if lower(thesis_type) == "bachelor" { "Bachelor" } else {
+ panic("thesis_type has to be either 'master' or 'bachelor'")
+ }
+ }
+
+ ///////////////////////////////////////
+ // Display the title page
+ page(footer: [])[
+ //#set par(leading: 1em)
+ #set text(
+ //font: "Comfortaa",
+ font: "Roboto",
+ //stretch: 50%,
+ //fallback: false,
+ weight: "bold",
+ size: 35.86pt,
+ //height:
+ )
+
+ //#v(80pt)
+ #grid(
+ rows: (auto, 1fr),
+ stack(
+ // title
+ layout(size => {
+ // measure title dimensions for automatic height adjustment
+ let title_content_width = size.width - title_page_inner_margin_left
+ let title_height_min = measure(box(height: 3.5em)[]).height
+ let title_height_calculated = if title_height == auto {
+ calc.max(
+ title_height_min,
+ measure(
+ make-title-box-content(content_width: title_content_width),
+ ).height,
+ )
+ } else {
+ title_height
+ }
+
+ block(
+ inset: (left: title_page_inner_margin_left),
+ height: title_height_calculated,
+ )[
+ #align(bottom)[#make-title-box-content(content_width: title_content_width)]
+ ]
+ }),
+ v(title_separator_spacing),
+ line(length: 100%, stroke: tud_heading_line_thin_stroke),
+ v(3mm), // title_separator_spacing
+ //
+ // sub block with reviewers and other text
+ block(inset: (left: title_page_inner_margin_left))[
+ #set text(size: 12pt)
+ #set par(
+ leading: 5.8pt,
+ )
+ #subtitle
+ \
+ #set text(weight: "regular")
+ #thesis_type_text thesis by #author
+ \
+ Date of submission: #submission_date
+ \
+ \
+ #for (i, reviewer_name) in reviewer_names.enumerate() [
+ #(i + 1). Review: #reviewer_name
+ \
+ ]
+ // looked better with -5pt (but -8pt fits latext template)
+ #v(-8pt) // spacing optional
+ #location
+ ],
+ v(15pt),
+ ),
+ // color rect with logos
+ rect(
+ fill: color.rgb(accentcolor_rgb),
+ stroke: (
+ top: tud_heading_line_thin_stroke,
+ bottom: tud_heading_line_thin_stroke,
+ ),
+ inset: 0mm,
+ width: 100%,
+ height: 100%, //10em
+ )[
+
+ #v(logo_tud_height / 2)
+ #context {
+ //let tud_logo = image(logo_tuda_path, height: logo_tud_height)
+ let tud_logo = [
+ #set image(height: logo_tud_height)
+ #if logo_tuda == none {
+ box(height: logo_tud_height, fill: white)[logo_tuda \ not set!]
+ } else {
+ logo_tuda
+ }
+ ]
+ let tud_logo_width = measure(tud_logo).width
+ let tud_logo_offset_right = -6.3mm
+ tud_logo_width += tud_logo_offset_right
+
+ align(right)[
+ //#natural-image(logo_tuda_path)
+ #grid(
+ // tud logo
+ // move logo(s) to the right
+ box(inset: (right: tud_logo_offset_right), fill: black)[
+ #set image(height: logo_tud_height)
+ #tud_logo
+ ],
+ // sub logo
+ if logo_institute != none { v(5mm) },
+ // height from design guidelines
+ let logo_institute_extend_dx = -tud_logo_offset_right * (2 / 3),
+ if logo_institute != none {
+ move(dx: logo_institute_extend_dx, box(
+ inset: (right: logo_institute_offset_right + logo_institute_extend_dx),
+ fill: white,
+ )[
+ #{
+ if logo_institute_sizeing_type == "width" {
+ //image(logo_institute_path, width: tud_logo_width*(2/3))
+ set image(width: tud_logo_width * (2 / 3), height: auto)
+ logo_institute
+ } else if logo_institute_sizeing_type == "height" {
+ //image(logo_institute_path, height: logo_tud_height*(2/3))
+ set image(height: logo_tud_height * (2 / 3), width: auto)
+ logo_institute
+ } else {
+ panic("logo_institute_sizeing_type has to be width or height")
+ }
+ }
+ ])
+ },
+ if logo_sub_content_text != none { v(5mm) },
+ // sub box with custom text
+ let logo_sub_box_extend_dx = -tud_logo_offset_right,
+ if logo_sub_content_text != none {
+ move(dx: logo_sub_box_extend_dx, box(
+ width: tud_logo_width + logo_sub_box_extend_dx,
+ outset: 0mm,
+ fill: white,
+ inset: (
+ top: 6pt,
+ bottom: 6pt,
+ left: 4.5mm,
+ right: logo_sub_box_extend_dx,
+ ),
+ align(left)[
+ #set text(weight: "regular", size: 9.96pt)
+ #logo_sub_content_text
+ ],
+ ))
+ },
+ )
+ ]
+ }
+
+ ],
+ )
+ ]
+}
diff --git a/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/common/util.typ b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/common/util.typ
new file mode 100644
index 0000000000..4ebf0c201d
--- /dev/null
+++ b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/common/util.typ
@@ -0,0 +1,54 @@
+#let natural-image(..args) = context {
+ let (width, height) = measure(image(..args))
+ image(..args, width: width, height: height)
+}
+
+
+
+#let get-spacing-zero-if-first-on-page(
+ default_spacing,
+ heading_location,
+ content_page_margin_full_top,
+ enable: true,
+) = {
+ // get previous element
+ //let elems = query(
+ // selector(heading).before(loc, inclusive: false),
+ // loc
+ //)
+ //[#elems]
+ if not enable {
+ return (default_spacing, false)
+ }
+
+ // check if heading is first element on page
+ // note: this is a hack
+ let heading_is_first_el_on_page = heading_location.position().y <= content_page_margin_full_top
+
+ // change heading margin depending if its the first on the page
+ if heading_is_first_el_on_page {
+ return (0mm, true)
+ } else {
+ return (default_spacing, false)
+ }
+}
+
+
+
+#let check-font-exists(font-name) = {
+ let measured = measure[
+ #text(font: font-name, fallback: false)[
+ Test Text
+ ]
+ ]
+ if measured.width == 0pt [
+ #rect(stroke: (paint: red), radius: 1mm, inset: 1.5em, width: 100%)[
+ #set text(fallback: true)
+ #set heading(numbering: none)
+ #show link: it => underline(text(blue)[#it])
+ === Error - Can Not Find Font "#font-name"
+ Please install the required font "#font-name". For instructions see the #link("https://github.com/JeyRunner/tuda-typst-templates#logo-and-font-setup")[Readme of this package].
+ ]
+ //#pagebreak()
+ ]
+}
diff --git a/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/headline.typ b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/headline.typ
new file mode 100644
index 0000000000..6cda07b62b
--- /dev/null
+++ b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/headline.typ
@@ -0,0 +1,62 @@
+#let default-headline(headline, info, dict) = {
+ for x in headline {
+ assert(x in ("title", "name", "id", "fl"), message: "Unknown headline key'" + x + "'!")
+ }
+
+ if headline == () or headline == ("fl",) {
+ return none
+ }
+
+ let number_form_box = box(
+ curve(
+ stroke: .5pt,
+ curve.move((0em, -.5em)),
+ curve.line((0em, 0em)),
+ curve.line((1em, 0em)),
+ curve.line((1em, -.5em)),
+ ),
+ )
+
+ let student_id_boxes = range(7).map(_ => number_form_box).join([ ])
+
+ let name-scheme = if "fl" in headline [
+ #dict.firstname, #dict.lastname
+ ] else [
+ #dict.lastname, #dict.firstname
+ ]
+
+ let grid-content = (
+ if "title" in headline {
+ grid.cell(info.at("header_title", default: info.title), colspan: 2)
+ },
+ if "name" in headline [
+ #name-scheme: #box(width: 1fr, line(length: 100%, stroke: 0.5pt))
+ ] else if "id" in headline [
+ // empty cell to have id on the right
+ ],
+ if "id" in headline [
+ #dict.student_id: #student_id_boxes
+ ],
+ ).filter(x => x != none)
+
+ grid(
+ columns: (1fr, auto),
+ column-gutter: 1cm,
+ row-gutter: 2mm,
+ align: (left, right),
+ inset: (right: 1mm), // else student id boxes overflow
+ ..grid-content,
+ )
+}
+
+#let resolve-headline(headline, info, dict) = if headline == none {
+ none
+} else if type(headline) == str {
+ default-headline((headline,), info, dict)
+} else if type(headline) == array {
+ default-headline(headline, info, dict)
+} else if type(headline) == content {
+ headline
+} else {
+ assert(false, message: "Unexpected value for headline: " + repr(headline))
+}
diff --git a/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/info-layout.typ b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/info-layout.typ
new file mode 100644
index 0000000000..c820a32d96
--- /dev/null
+++ b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/info-layout.typ
@@ -0,0 +1,9 @@
+/// Provides functions to style the subline in the title card of the template.
+/// Currently the following two functions exist:
+/// - exercise: Behaves pretty much like in the LaTeX template
+/// - submission: Behaves out of the box more like Rubos LaTeX template but also has
+/// customization options and allows more info-fields.
+
+// this file controls which fields of info-layout should be publicly visible
+#import "info-layout/exercise.typ": exercise
+#import "info-layout/submission.typ": submission
diff --git a/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/info-layout/exercise.typ b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/info-layout/exercise.typ
new file mode 100644
index 0000000000..be41e994da
--- /dev/null
+++ b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/info-layout/exercise.typ
@@ -0,0 +1,27 @@
+#import "../common/format.typ": format-date, text-roboto
+#import "utils.typ": resolve-term
+
+/// The default version of the title subline.
+///
+/// *Possible `info` items:*
+/// - `term`: The current term (types: #str)
+/// - `date`: A date related to the exercise (types: #str, #datetime)
+/// - `sheet`: The number of this sheet/exercise (types: #int)
+///
+/// - additional (content,none): Additional content to be displayed after the previous options
+/// -> function
+#let exercise(additional: none) = (info, dict) => {
+ if "term" in info {
+ resolve-term(info.term, info, dict)
+ linebreak()
+ }
+ if "date" in info {
+ format-date(info.date, dict.locale)
+ linebreak()
+ }
+ if "sheet" in info {
+ [#dict.sheet #info.sheet]
+ linebreak()
+ }
+ additional
+}
diff --git a/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/info-layout/submission.typ b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/info-layout/submission.typ
new file mode 100644
index 0000000000..cd590d53e5
--- /dev/null
+++ b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/info-layout/submission.typ
@@ -0,0 +1,115 @@
+#import "../common/format.typ": format-date, text-roboto
+#import "utils.typ": resolve-term
+
+#let submission-item-style(key, value) = if type(value) == array {
+ [
+ #text-roboto(strong(key)): \
+ #value.join(linebreak())
+ ]
+} else {
+ [#text-roboto(strong(key)): #value]
+}
+
+/// Allows more customization about how to display information about this document.
+///
+/// *Possible `info` items:*
+/// - `term`: The current term (types: #str)
+/// - `date`: The due date of the submission (types: #str, #datetime)
+/// - `sheet`: The number of this sheet/exercise (types: #int)
+/// - `group`: A identifier for the lecture group (types: #int, #str)
+/// - `tutor`: The name of the tutor of the lecture group (types: #str)
+/// - `lecturer`: The name of the lecturer (types: #str)
+///
+/// If you want to add more options you have two options:
+///
+/// 1. Pass the option as key-value pair:
+/// ```
+/// submission(
+/// left: ("sheet", ("Magic", "1234"))
+/// )
+/// ```
+///
+/// 2. Pass option via `info` and add definition to `dict-addon`:
+/// ```
+/// info: (
+/// magic: 1234
+/// )
+/// ...
+/// submission(
+/// left: ("magic"),
+/// dict-addon: (magic: "Magic")
+/// )
+/// ```
+/// Additionally the names for fields can be overwritten by putting a new definition in
+/// `dict-addon`
+///
+/// If you pass something other than a string to left or right the item will simply be
+/// displayed as is. Thus you can also add manual content on either side.
+///
+/// How the information is displayed can further be styled using `item-style`. By default
+/// this has the following format:
+/// *key*: value
+///
+/// - left (array): The options to be displayed on the left side
+/// - right (array): The options to be displayed on the right side
+/// - dict-addon (dictionary): Additional definitions of items to be used
+/// - item-style (function): A function (str,any)=>content that takes in a name, it's value
+/// and yields content correspondingly
+/// -> function
+#let submission(
+ left: ("term", "date"),
+ right: ("sheet", "group", "tutor", "lecturer"),
+ dict-addon: (:),
+ item-style: submission-item-style,
+) = {
+ assert.eq(type(item-style), function, message: "Expected item-style of submission(...) to be a function")
+
+ let resolve-item(i, info, dict) = if type(i) == array {
+ assert.eq(
+ i.len(),
+ 2,
+ message: "Custom items in submission should have form (key, value). Got: '"
+ + repr(i)
+ + "' Or did you mean to just write '"
+ + repr(i.at(0))
+ + "'?",
+ )
+ let (k, v) = i
+ return item-style(k, v)
+ } else if type(i) != str {
+ return i
+ } else if i in info {
+ assert(
+ i in dict,
+ message: "Unknown item '" + i + "' in submission, please use manual syntax: (\"Display Name\", \"Value\")",
+ )
+ let value = info.at(i)
+ if i == "term" {
+ value = resolve-term(value, info, dict)
+ }
+ // Format date ignores formatting if type isn't date thus this works
+ return item-style(dict.at(i), format-date(value, dict.locale))
+ } else {
+ return none
+ }
+
+ let resolve-items(arr, info, dict) = if type(arr) != array {
+ (resolve-item(arr, info, dict),)
+ } else {
+ arr.map(i => resolve-item(i, info, dict))
+ }
+
+ let filter-none(arr) = arr.filter(x => x != none)
+
+ return (info, dict) => {
+ let full-dict = dict + dict-addon
+ let left-items = resolve-items(left, info, full-dict)
+ let right-items = resolve-items(right, info, full-dict)
+ grid(
+ columns: (1fr, 1fr),
+ align: (alignment.left, alignment.right),
+
+ filter-none(left-items).join(linebreak()), filter-none(right-items).join(linebreak()),
+ )
+ }
+}
diff --git a/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/info-layout/utils.typ b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/info-layout/utils.typ
new file mode 100644
index 0000000000..648e3651a5
--- /dev/null
+++ b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/info-layout/utils.typ
@@ -0,0 +1,18 @@
+#let resolve-term(term, info, dict) = if term == auto {
+ let date = info.at("date", default: info.at("_date", default: datetime.today()))
+ assert.eq(type(date), datetime, message: "When using 'auto' for 'term', 'date' must be of type 'datetime' or 'none'.")
+ // if month between 4 and 9 then it's summer term, else it's winter term
+ let month = date.month()
+ let year = date.year()
+ if month >= 4 and month <= 9 {
+ dict.summer_term + " " + str(year)
+ } else {
+ dict.winter_term
+ if month < 4 {
+ year = year - 1
+ }
+ " " + str(year) + "/" + str(year + 1)
+ }
+} else {
+ term
+}
diff --git a/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/lib.typ b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/lib.typ
new file mode 100644
index 0000000000..036c8c0e22
--- /dev/null
+++ b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/lib.typ
@@ -0,0 +1,9 @@
+#import "tudaexercise.typ": (
+ difficulty-format, point-format, subtask, task, task-points-header, tuda-gray-info, tudaexercise,
+)
+#import "info-layout.typ" as info-layout
+#import "common/headings.typ": tuda-section, tuda-subsection
+#import "common/tudacolors.typ": text_colors as tuda_text_colors, tuda_colors
+#import "common/props.typ": *
+#import "common/format.typ": text-roboto
+#import "common/addons/difficulty-points.typ": difficulty-stars as tuda-difficulty-stars
diff --git a/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/locales.typ b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/locales.typ
new file mode 100644
index 0000000000..53298724f2
--- /dev/null
+++ b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/locales.typ
@@ -0,0 +1,56 @@
+#let dict_de = (
+ locale: "de",
+ task: "Aufgabe",
+
+ sheet: "Übungsblatt",
+ group: "Übungsgruppe",
+ tutor: "Tutor",
+ lecturer: "Dozent",
+ point_singular: "Punkt",
+ point_plural: "Punkte",
+ difficulty: "Schwierigkeitsgrad",
+ term: "Semester",
+ date: "Abgabe",
+ firstname: "Vorname",
+ lastname: "Nachname",
+ student_id: "Matrikelnummer",
+ summer_term: "Sommersemester",
+ winter_term: "Wintersemester",
+)
+
+#let dict_en = (
+ locale: "en",
+ task: "Task",
+
+ sheet: "Sheet",
+ group: "Exercise group",
+ tutor: "Tutor",
+ lecturer: "Lecturer",
+ point_singular: "Point",
+ point_plural: "Points",
+ difficulty: "Difficulty",
+ term: "Term",
+ date: "Due",
+ firstname: "First Name",
+ lastname: "Last Name",
+ student_id: "Student ID",
+ summer_term: "Summer semester",
+ winter_term: "Winter semester",
+)
+
+#let dicts = (
+ de: dict_de,
+ en: dict_en,
+)
+
+/// Returns the dictionary for the given locale.
+///
+/// - locale (str): The locale to get the dictionary for, can be "de" or "en".
+/// -> Returns: The dictionary for the given locale.
+#let get-locale-dict(locale) = {
+ let dict = dicts.at(locale, default: none)
+ if dict == none {
+ panic("Unsupported locale: " + locale + ". Supported locales are: " + dicts.keys().join(", ", last: " and "))
+ }
+ dict
+}
diff --git a/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/task-format.typ b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/task-format.typ
new file mode 100644
index 0000000000..0aba4d00fa
--- /dev/null
+++ b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/task-format.typ
@@ -0,0 +1,33 @@
+#let format-task(
+ prefix,
+ nbr,
+ separator,
+ include-subtasks,
+ it,
+ dict,
+) = {
+ let sep = if type(separator) == array {
+ assert.ne(separator.len(), 0, message: "If an array, `task-separator` must have at least one element.")
+ separator.at(it.level - 1, default: separator.at(-1))
+ } else {
+ separator
+ }
+
+ let p = if prefix == auto {
+ dict.task + " "
+ } else if type(prefix) == array {
+ assert.ne(prefix.len(), 0, message: "If an array, `task-prefix` must have at least one element.")
+ context {
+ let c = counter("tuda-task-prefix").get().at(0)
+ prefix.at(calc.rem(c, prefix.len()))
+ }
+ } else {
+ prefix
+ }
+
+ if include-subtasks or it.level == 1 {
+ [#p#nbr#sep]
+ } else {
+ [#nbr#sep]
+ }
+}
diff --git a/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/title.typ b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/title.typ
new file mode 100644
index 0000000000..5a4a48677f
--- /dev/null
+++ b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/title.typ
@@ -0,0 +1,123 @@
+#import "common/format.typ": format-date
+
+#let title-info-keys = ("title", "header_title", "subtitle", "author")
+
+#let resolve-info-layout(info-layout, info, dict) = if type(info-layout) == content {
+ info-layout
+} else if type(info-layout) == function {
+ info-layout(info, dict)
+} else {
+ panic("info-layout has unsupported type. Expected content, function or none. Got " + type(info-layout))
+}
+
+#let tuda-make-title(
+ inner_page_margin_top,
+ title_rule,
+ accent_color,
+ on_accent_color,
+ text_color,
+ colorback,
+ logo_element,
+ sublogo_element,
+ logo_height,
+ info,
+ info-layout,
+ dict,
+) = {
+ let text_on_accent_color = if colorback {
+ on_accent_color
+ } else {
+ text_color
+ }
+
+ let text_inset = if colorback {
+ (x: 3mm)
+ } else {
+ (:)
+ }
+
+ let stroke_color = if colorback {
+ black
+ } else {
+ text_color
+ }
+
+ let stroke = (paint: stroke_color, thickness: title_rule / 2)
+
+ v(-inner_page_margin_top + 0.2mm) // would else draw over header
+
+ set text(fill: text_on_accent_color)
+
+ block(
+ fill: if colorback { accent_color },
+ width: 100%,
+ outset: 0pt,
+ {
+ // line creates a paragraph spacing
+ set par(spacing: 4pt)
+ v(logo_height / 2)
+ grid(
+ columns: (1fr, auto),
+ align: (auto, right),
+ pad(y: 3mm, {
+ set text(font: "Roboto", weight: "bold", size: 12pt)
+ grid(
+ row-gutter: 1em,
+ inset: text_inset,
+ ..(
+ if "title" in info {
+ text(info.title, size: 20pt)
+ },
+ if "subtitle" in info {
+ info.subtitle
+ },
+ if "author" in info {
+ if type(info.author) == array {
+ for author in info.author {
+ if type(author) == array {
+ [#author.at(0)
+ #text(weight: "regular", size: 0.8em)[(Mat.: #author.at(1))]]
+ linebreak()
+ } else {
+ author
+ linebreak()
+ }
+ }
+ } else {
+ info.author
+ }
+ },
+ ).filter(x => x != none)
+ )
+
+ v(.5em)
+ }),
+ {
+ if logo_element != none {
+ move(
+ dx: 6mm,
+ {
+ set image(height: logo_height)
+ logo_element
+ },
+ )
+ }
+ if sublogo_element != none {
+ // 2/3 is from the tudapub example
+ set image(height: logo_height * 2 / 3)
+ sublogo_element
+ }
+ },
+ )
+ v(6pt)
+ line(length: 100%, stroke: stroke)
+ if info-layout != none and info.keys().any(x => not x in title-info-keys) {
+ block(
+ inset: text_inset,
+ resolve-info-layout(info-layout, info, dict),
+ )
+ line(length: 100%, stroke: stroke)
+ }
+ },
+ )
+}
diff --git a/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/tudaexercise.typ b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/tudaexercise.typ
new file mode 100644
index 0000000000..24616f64d8
--- /dev/null
+++ b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/template/tudaexercise.typ
@@ -0,0 +1,549 @@
+#import "common/tudacolors.typ": text_colors, tuda_colors
+#import "common/props.typ": (
+ tud_exercise_page_margin, tud_header_line_height, tud_heading_line_thin_stroke, tud_inner_page_margin_top,
+ tud_title_logo_height,
+)
+#import "common/headings.typ": tuda-nthsection, tuda-section, tuda-subsection
+#import "common/util.typ": check-font-exists
+#import "common/addons/difficulty-points.typ": difficulty-stars
+#import "common/colorutil.typ": calc-contrast, calc-relative-luminance
+#import "common/format.typ": text-roboto
+#import "title.typ": *
+#import "locales.typ": *
+#import "info-layout.typ" as info-layout
+#import "task-format.typ": format-task
+#import "headline.typ": resolve-headline
+
+#let design-defaults = (
+ accentcolor: "0b",
+ colorback: true,
+ darkmode: false,
+)
+
+#let s = state("tud_design")
+
+/// The heart of this template.
+/// Usage:
+/// ```
+/// #show: tudaexercise.with()
+/// ```
+///
+/// - language ("en", "de"): The language for dates and certain keywords
+///
+/// - margins (dictionary): The page margins, possible entries: `top`, `left`,
+/// `bottom`, `right`
+///
+/// - headline (array, str, content, none): Control the headline of pages.
+/// If array or string, the following keys are supported: `"title"`, `"name"`,
+/// `"id"` and `"fl"`. The first three create the correspondig part known from the
+/// LaTeX template. If `"fl"` is present, the ordering for the names is switched to
+/// `First, Last`.
+///
+/// If content is passed, it is displayed directly. If none is passed or at most `"fl"`
+/// is given as key, the headline is omitted.
+///
+/// - paper (str): The type of paper to be used. Currently only a4 is supported.
+///
+/// - logo (content): The tuda logo as an image to be used in the title.
+///
+/// - sublogo (content): A logo of an institution or similar for the title.
+///
+/// - info (dictionary): Info about the document mostly used in the title.
+///
+/// By default accepts the following items:
+/// - `title`
+/// - `subtitle`
+/// - `author`
+///
+/// Additionally the following items are used by the `exercise` info-layout:
+/// - `term`
+/// - `date`
+/// - `sheet`
+///
+/// Other `info-layouts`s may use more options, which can be added here. See the documentation
+/// of the `info-layout` for corresponding items.
+///
+/// Note: Items mapped to `none` are ignored aka. internally the dict is processed without
+/// them.
+///
+/// - info-layout (content, function, none): The content of the subline in the title card.
+/// By default the `info-layout.exercise` style.
+///
+/// See the `info-layout` export for functions to insert here or if you do not find something
+/// fitting to your needs you can also pass raw content and completely customize it yourself.
+///
+/// - design (dictionary): Options for the design of the template. Possible entries:
+/// `accentcolor`, `colorback` and `darkmode`
+///
+/// - task-prefix (auto, str, array, content): How the task numbers are prefixed. If unset or auto,
+/// the tasks use the language default.
+///
+/// If an array is given, it is indexed at the current value of
+/// `counter("tuda-task-prefix")` mod `task-prefix.len()`.
+/// Thus, splits into group-/homework tasks can be implemented as follows:
+///
+/// ```typst
+/// #show: tudaexercise.with(
+/// ...
+/// task-prefix: ("G", "H")
+/// )
+/// = Group tasks
+///
+/// #counter("tuda-task-prefix").step()
+/// #counter(heading).update(0) // to make headings count at 1 again
+///
+/// = Homework tasks
+/// ```
+///
+/// - task-separator (str, array, content): The separator between the task numbering and the task name.
+/// If an array, it is indexed using the current heading level, repeating the last element.
+///
+/// - task-prefix-subtasks (bool): Whether subtasks should also be prefixed or not.
+///
+/// - show-title (bool): Whether to show a title or not
+///
+/// - subtask ("ruled", "plain"): How subtasks are shown
+///
+/// - body (content):
+#let tudaexercise(
+ language: "en",
+
+ margins: tud_exercise_page_margin,
+
+ headline: ("title", "name", "id"),
+
+ paper: "a4",
+
+ logo: none,
+ sublogo: none,
+
+ info: (
+ title: none,
+ header_title: none,
+ subtitle: none,
+ author: none,
+ term: none,
+ date: none,
+ sheet: none,
+ group: none,
+ tutor: none,
+ lecturer: none,
+ ),
+
+ info-layout: info-layout.exercise(),
+
+ design: design-defaults,
+
+ task-prefix: auto,
+ task-separator: (":", ")"),
+ task-prefix-subtasks: false,
+
+ show-title: true,
+
+ subtask: "ruled",
+
+ body,
+) = {
+ assert.eq(paper, "a4", message: "Currently just A4 paper is supported.")
+
+ let margins = tud_exercise_page_margin + margins
+ let design = design-defaults + design
+ let info = info.pairs().filter(x => x.at(1) != none).to-dict()
+
+ let text_color = if design.darkmode {
+ white
+ } else {
+ black
+ }
+
+ let background_color = if design.darkmode {
+ rgb(29, 31, 33)
+ } else {
+ white
+ }
+
+ let accent_color = if type(design.accentcolor) == color {
+ design.accentcolor
+ } else if type(design.accentcolor) == str {
+ rgb(tuda_colors.at(design.accentcolor))
+ } else {
+ panic("Unsupported color format. Either pass a color code as a string or pass an actual color.")
+ }
+
+ let text_on_accent_color = if type(design.accentcolor) == str {
+ text_colors.at(design.accentcolor)
+ } else {
+ let lum = calc-relative-luminance(design.accentcolor)
+ if calc-contrast(lum, 0) > calc-contrast(lum, 1) {
+ black
+ } else {
+ white
+ }
+ }
+
+ s.update((
+ text_color: text_color,
+ background_color: background_color,
+ accent_color: accent_color,
+ text_on_accent_color: text_on_accent_color,
+ darkmode: design.darkmode,
+ ))
+
+ set line(stroke: text_color)
+ set block(stroke: 0pt + text_color)
+ set curve(stroke: 0pt + text_color)
+
+ let ruled_subtask = if subtask == "ruled" {
+ true
+ } else if subtask == "plain" {
+ false
+ } else {
+ panic("Only 'ruled' and 'plain' are supported subtask options")
+ }
+
+ let meta_document_title = if "subtitle" in info and "title" in info {
+ [#info.subtitle #sym.dash.em #info.title]
+ } else if "title" in info {
+ info.title
+ } else if "subtitle" in info {
+ info.subtitle
+ } else {
+ none
+ }
+
+ set document(
+ title: meta_document_title,
+ author: if "author" in info {
+ if type(info.author) == array {
+ let authors = info.author.map(
+ it => if type(it) == array {
+ it.at(0)
+ } else {
+ it
+ },
+ )
+ authors
+ } else {
+ info.author
+ }
+ } else {
+ ()
+ },
+ )
+
+ set par(
+ justify: true,
+ //leading: 4.7pt//0.42em//4.7pt // line spacing
+ leading: 4.8pt, //0.42em//4.7pt // line spacing
+ spacing: 1.1em,
+ )
+
+ set text(
+ font: "XCharter",
+ size: 10.909pt,
+ fallback: false,
+ kerning: true,
+ ligatures: false,
+ spacing: 91%, // to make it look like the latex template,
+ fill: text_color,
+ lang: language,
+ )
+
+ show raw: set text(spacing: 100%)
+
+ let dict = get-locale-dict(language)
+
+ set heading(numbering: (..numbers) => {
+ let len = numbers.pos().len()
+ assert(len < 4, message: "Headings beyond level 3 need to supply their own numbering.")
+
+ let base = "1a i"
+ if "sheet" in info {
+ numbering("1." + base, info.sheet, ..numbers)
+ } else {
+ numbering(base, ..numbers)
+ }
+ })
+
+ show heading: it => {
+ if not it.outlined or it.numbering == none {
+ it
+ return
+ }
+ let c = counter(heading).display(it.numbering)
+ let prefix = format-task(task-prefix, c, task-separator, task-prefix-subtasks, it, dict)
+ if it.level == 1 {
+ tuda-section[#prefix #it.body]
+ } else if it.level == 2 {
+ tuda-subsection(ruled: ruled_subtask)[#prefix #it.body]
+ } else {
+ tuda-nthsection(ruled: ruled_subtask)[#prefix #it.body]
+ }
+ }
+
+ let identbar = rect(
+ fill: accent_color,
+ width: 100%,
+ height: 4mm,
+ )
+
+ let header_frontpage = {
+ set block(above: 1.4mm + 0.25mm, below: 0mm)
+ identbar
+ line(length: 100%, stroke: tud_header_line_height)
+ }
+
+ let headline = resolve-headline(headline, info, dict)
+
+ let show_additional_header = headline != none
+
+ let additional_header = if show_additional_header {
+ set block(above: 2.1mm + 0.25mm, below: 0mm)
+
+ block(headline, width: 100%)
+
+ line(length: 100%, stroke: tud_heading_line_thin_stroke)
+ } else {}
+
+ context {
+ // without width argument, measure sometimes yields imprecise results
+ let height_header = measure(header_frontpage, width: 21cm).height
+ let height_additional_header = if show_additional_header {
+ // measure does not account for block spacing around element
+ measure(additional_header, width: 21cm).height + 2.1mm + 0.25mm
+ } else {
+ 0mm
+ }
+
+ set page(
+ paper: paper,
+ numbering: "1",
+ number-align: right,
+ margin: (
+ top: margins.top + tud_inner_page_margin_top + height_header + height_additional_header,
+ bottom: margins.bottom,
+ left: margins.left,
+ right: margins.right,
+ ),
+ header: context {
+ header_frontpage
+ if here().page() > 1 or not show-title {
+ additional_header
+ } else {
+ hide(additional_header)
+ }
+ },
+ header-ascent: tud_inner_page_margin_top,
+ fill: background_color,
+ )
+
+ if show-title {
+ tuda-make-title(
+ tud_inner_page_margin_top + height_additional_header,
+ tud_header_line_height,
+ accent_color,
+ text_on_accent_color,
+ text_color,
+ design.colorback,
+ logo,
+ sublogo,
+ tud_title_logo_height,
+ info,
+ info-layout,
+ dict,
+ )
+ }
+
+ check-font-exists("Roboto")
+ check-font-exists("XCharter")
+
+ body
+ }
+}
+
+#let tuda-box(title: none, color: none, fill: true, body) = {
+ assert.ne(color, none, message: "Please define a color for the box.")
+ let background = if fill {
+ color.transparentize(80%)
+ }
+ rect(
+ fill: background,
+ // inset: 1em,
+ inset: (
+ left: 8pt,
+ y: 2mm,
+ ),
+ radius: 3pt,
+ width: 100%,
+ stroke: (left: 5pt + color),
+ [
+ #{ if title != none [#text-roboto(strong(title)) \ ] }
+ #body
+ ],
+ )
+}
+
+#let tuda-gray-info = tuda-box.with(color: gray, fill: true)
+
+/// Formats points for display in a task header.
+///
+/// - points (int, float): The number of points, can be an integer or a float.
+/// - points-name-single (str): The singular form of the points name, default is auto and retrieved from the locale dictionary.
+/// - points-name-plural (str): The plural form of the points name, default is auto and retrieved from the locale dictionary.
+/// - pointssep (str): The separator between the points and the name, default is a space.
+/// -> Returns: A string formatted as "points Punkte" or "points Punkt" depending on the value of points.
+#let point-format(
+ points: none,
+ points-name-single: auto,
+ points-name-plural: auto,
+ pointssep: " ",
+) = context {
+ let ctxpoints-name-single = points-name-single
+ let ctxpoints-name-plural = points-name-plural
+ if points-name-single == auto or points-name-plural == auto {
+ let dict = get-locale-dict(text.lang)
+ if points-name-single == auto {
+ ctxpoints-name-single = dict.point_singular
+ }
+ if points-name-plural == auto {
+ ctxpoints-name-plural = dict.point_plural
+ }
+ }
+ assert.ne(points, none, message: "points must be provided")
+ assert(type(points) in (float, int), message: "points must be a number, got " + str(type(points)))
+ str(points)
+ pointssep
+ if points == 1 {
+ ctxpoints-name-single
+ } else {
+ ctxpoints-name-plural
+ }
+}
+
+/// Formats the difficulty of a task for display in a task header.
+///
+/// - difficulty (int, float): The difficulty of the task, must be between 0 and `max-difficulty`.
+/// - max-difficulty (int): The maximum difficulty, default is 5.
+/// - difficulty-name (str, auto): The name of the difficulty, prefix for the stars, default is auto and retrieved from the locale dictionary.
+/// - difficulty-sep (str): The separator between the difficulty name and the stars, default is ": ".
+/// - out-of-sep (str): The separator between the difficulty and the maximum difficulty, default is "/".
+/// - otherargs: Throwaway unneeded args used for different implementations of the difficulty format function.
+/// -> Returns: A string formatted as "difficulty: difficulty/max-difficulty".
+#let difficulty-format(
+ difficulty,
+ max-difficulty: 5,
+ difficulty-name: auto,
+ difficulty-sep: ": ",
+ out-of-sep: "/",
+ ..otherargs,
+) = context {
+ let ctxdifficulty-name = if difficulty-name == auto {
+ get-locale-dict(text.lang).difficulty
+ } else {
+ difficulty-name
+ }
+ if ctxdifficulty-name != none {
+ ctxdifficulty-name + difficulty-sep
+ }
+ str(difficulty) + out-of-sep + str(max-difficulty)
+}
+
+/// A header for tasks that includes points and difficulty information.
+///
+/// - points (int, float, none): The number of points the task is worth, optional.
+/// - difficulty (int, float, none): The difficulty of the task, optional.
+/// - max-difficulty (int): The maximum difficulty of the task, default is 5.
+/// - hspace (length): The horizontal space between the title and the points/difficulty, default is 1fr.
+/// - details-seperator (str): The separator between the points and difficulty information, default is ", ".
+/// - star-fill (color): The fill color of the stars, default is the accent color from the context.
+/// - points-function (function): The function to format the points, default is `point-format`.
+/// - difficulty-function (function): The function to format the difficulty, default is `difficulty-stars`.
+/// -> Returns: A string with the points and difficulty information formatted as "points Punkte, difficulty-stars(difficulty, max_difficulty: max-difficulty)".
+#let task-points-header(
+ points: none,
+ difficulty: none,
+ max-difficulty: 5,
+ hspace: 1fr,
+ details-seperator: ", ",
+ star-fill: auto,
+ points-function: point-format,
+ difficulty-function: difficulty-stars,
+) = context {
+ assert(points != none or difficulty != none, message: "Either points or difficulty must be provided")
+ if hspace != none {
+ h(hspace)
+ }
+ let ctxstar-fill = star-fill
+ if star-fill == auto {
+ ctxstar-fill = s.get().accent_color
+ }
+ let details = ()
+ if points != none {
+ details.push(points-function(points: points))
+ }
+ if difficulty != none {
+ details.push(difficulty-function(difficulty, max-difficulty: max-difficulty, fill: ctxstar-fill))
+ }
+ if details.len() > 0 {
+ details.join(details-seperator)
+ }
+}
+
+/// A wrapper command to create a section (a task in the context of this template) with a title and optional points and difficulty information. See `task-points-header` for the details.
+///
+/// - title (content): The title of the task.
+/// - points (int): The number of points the task is worth, optional.
+/// - difficuty (float): The difficulty of the task, optional.
+/// - otherargs: Additional arguments to pass to the `task-points-header` function.
+/// -> Returns: A heading with the title and optional points and difficulty information.
+#let task(
+ title: none,
+ points: none,
+ difficulty: none,
+ ..otherargs,
+) = {
+ if otherargs.pos().len() > 0 and title == none {
+ title = otherargs.at(0)
+ }
+ heading({
+ title
+ if points != none or difficulty != none {
+ task-points-header(
+ points: points,
+ difficulty: difficulty,
+ ..otherargs.named(),
+ )
+ }
+ })
+}
+
+/// A wrapper command to create a subtask (a subtask in the context of this template) with a title and optional points and difficulty information. See `task-points-header` for the details.
+///
+/// - title (content): The title of the subtask.
+/// - points (int): The number of points the subtask is worth, optional.
+/// - difficulty (float): The difficulty of the subtask, optional.
+/// - otherargs: Additional arguments to pass to the `task-points-header` function.
+/// -> Returns: A heading with the title and optional points and difficulty information.
+#let subtask(
+ title: none,
+ points: none,
+ difficulty: none,
+ ..otherargs,
+) = {
+ if otherargs.pos().len() > 0 and title == none {
+ title = otherargs.at(0)
+ }
+ heading(
+ {
+ title
+ if points != none or difficulty != none {
+ task-points-header(
+ points: points,
+ difficulty: difficulty,
+ ..otherargs.named(),
+ )
+ }
+ },
+ level: 2,
+ )
+}
diff --git a/packages/preview/athena-tu-darmstadt-exercise/0.3.0/typst.toml b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/typst.toml
new file mode 100644
index 0000000000..bc72cd678e
--- /dev/null
+++ b/packages/preview/athena-tu-darmstadt-exercise/0.3.0/typst.toml
@@ -0,0 +1,16 @@
+[package]
+name = "athena-tu-darmstadt-exercise"
+version = "0.3.0"
+entrypoint = "template/lib.typ"
+authors = ["JeyRunner ", "FussballAndy "]
+license = "MIT"
+description = "Exercise template for TU Darmstadt (Technische Universität Darmstadt)."
+repository = "https://github.com/JeyRunner/tuda-typst-templates"
+keywords = ["TU Darmstadt", "Technische Universität Darmstadt", "Exercise"]
+categories = ["layout"]
+compiler = "0.13.0"
+
+[template]
+path = "example"
+entrypoint = "main.typ"
+thumbnail = "preview/tudaexercise-light.png"