I made some light changes to the solution from hpcfzl. Mostly:
- aligned the numbers of level 2+ to have the same alignment as level 1’s text/body. though in a hacky way by just adding an offset variable
- added font size variable for level 1 heading
- made it into a function
Code
Put this into `functions.typ` for example:#let docTableOfContents = {
// Outline rules from @hpcfzl on Typst forum:
// forum.typst.app/t/how-to-align-the-outline-entries-and-fill-plus-more/8753
// Excludes headings level `excl`+
let excl = 4
set outline(depth: excl)
/*
This is unreliable for more than single-digit numbers, but
would be in case `auto` was allowed in place of `1em`
*/
// Indents only heading numbers level 2+, all the same
// set outline(indent: n => if n == 0 { 0em } else { 1em })
// Indentation is done in the `outline.entry` rule
// set outline(indent: 0em)
// Indentation is actually NOT needed for heading numbers level 2+
// This would be equivalent to `set outline(indent: n => if n == 0 { auto } else { 0em })`
show selector.or(..range(2, excl + 1).map(it => outline.entry.where(level: it))): set outline(indent: 0em)
// Repeating separator
let fill-gap = 0.5em
let fill-gap-pad = fill-gap / 5
set outline.entry(fill: repeat(gap: fill-gap, justify: false)[.])
show repeat: set align(right)
// Rule for outline entries
show outline.entry: it => {
let indented-gap = 1em
let indented-gap-offset = 0.3em // to align numbers of level 2+ with body of level 1
let text-weight = "bold"
let text-size = 14pt
set text(weight: text-weight, size: text-size) if it.level == 1
set block(above: 1.5em) if it.level == 1 // Could be made proportional to the `indented-gap`
// Seizure warning
let widest-heading = {
let heading-locs = query(heading).map(h => h.location())
(
n: calc.max(
// could be simplified? : forum.typst.app/t/how-to-align-the-outline-entries-and-fill-plus-more/8753/11
..heading-locs.map(loc => counter(heading).at(loc)).map(it => it.map(str).join(".")).map(it => measure(it).width)
),
p: measure(
text(weight: text-weight, str(counter(page).at(heading-locs.last()).first()))
).width
)
}
link(
it.element.location(),
it.indented(
if it.level == 1 { it.prefix() },
{
if it.level > 1 {
hide(
text(
weight: text-weight,
// Wrong to use `counter(heading).display()`
str(counter(heading).final().first()) + h(indented-gap + indented-gap-offset),
)
)
box(width: widest-heading.n + 1em, it.prefix()) // Pointless to add `h(indented-gap)`
}
it.body()
h(fill-gap-pad)
box(width: 1fr, if it.level > 1 { it.fill })
h(fill-gap-pad)
box(width: widest-heading.p + 2em, align(right, it.page()))
},
gap: indented-gap,
)
)
}
outline()
}
and call in main.typ like this:
#import "functions.typ": *
or: #import "functions.typ": docTableOfContents
...
#docTableOfContents
You can grab my rewritten LaTeX template here or look at the typst code here.
Same link as in the first reply, just adding it here again to have all in one place.
Is it ok to mark this as the solution? Technically it is correcter but @hpcfzl did 99% of the work. I do not want to overshadow that, even if their reply is marked in the code

