Files
university-portfolio-typst/template.typ
T
2026-08-22 11:52:12 +09:00

615 lines
13 KiB
Typst

// ==================================================
// Config Variables
// ==================================================
#let font-body = "Pretendard"
#let font-serif = ("Source Serif 4", "Source Han Serif K")
#let font-display = ("Source Serif 4 Display", "Source Han Serif K")
#let font-subhead = ("Source Serif 4 Subhead", "Source Han Serif K")
#let font-caption = ("Source Serif 4 Caption", "Source Han Serif K")
// #let font-code =
#let color-surface = white
#let color-ink = color.oklch(22%, 0.01, 250deg)
#let color-ink-inverse = color.oklch(97%, 0.01, 250deg)
#let color-ink-secondary = color.oklch(60%, 0.01, 250deg)
#let color-ink-helper = color.oklch(50%, 0.01, 250deg)
#let color-ink-faint = color.oklch(70%, 0.01, 250deg)
#let color-rule-faint = color.oklch(85%, 0.01, 250deg)
#let bleed = 1.5cm
// ==================================================
// Metadata
// ==================================================
#let meta = state(
"meta",
(
cur_domain: 0,
total_domain: 0,
domain_name: "",
domain_detail: "",
date: "",
author: "",
),
)
#let color-palette = state(
"color-palette",
(
cover-surface: white,
callout-surface: white,
callout-border: black,
rule: black,
problem: black,
accent: black,
accent-faint: white,
),
)
// ==================================================
// Util functions
// ==================================================
#let pad-zeros(num, width) = {
let s = str(num)
"0" * calc.max(0, width - s.len()) + s
}
#let make_palette(hue) = (
cover-surface: color.oklch(97%, 0.01, hue),
callout-surface: color.oklch(97%, 0.02, hue),
callout-border: color.oklch(85%, 0.05, hue),
rule: color.oklch(90%, 0.02, hue),
problem: color.oklch(75%, 0.04, hue),
accent: color.oklch(40%, 0.09, hue),
accent-faint: color.oklch(70%, 0.02, hue),
)
#let heading_text(it) = {
if it.has("text") {
it.text
} else if it.has("children") {
it.children.map(heading_text).join("")
} else if it == linebreak() {
" "
} else {
""
}
}
// ==================================================
// Body components
// ==================================================
#let callout(bg_color: none, border_color: none, body) = context {
let cp = color-palette.get()
let bg_c = if bg_color == none { cp.callout-surface } else { bg_color }
let border_c = if border_color == none { cp.callout-border } else { border_color }
let x_pad = 12pt
let y_pad = 18pt
let gap = 24pt
block(
width: 100%,
above: gap + y_pad,
below: gap + y_pad,
outset: (x: x_pad, y: y_pad),
radius: 4pt,
fill: bg_c,
stroke: (paint: border_c),
)[
#body
]
}
#let block_quote(color: none, content) = context {
let gutter = 15pt
let line-width = 2pt
let c = if color != none { color } else { color-palette.get().accent }
layout(size => {
let body-width = size.width - line-width - gutter
let body-size = measure(
content,
width: body-width,
)
grid(
columns: (line-width, 1fr),
gutter: gutter,
align: horizon,
line(
stroke: (
paint: c,
thickness: line-width,
cap: "round",
),
angle: 90deg,
length: body-size.height + 15pt,
),
)[
#content
]
})
}
#let kicker(color: color-ink-helper, content) = {
set text(
font: font-body,
weight: 700,
size: 10pt,
tracking: 1pt,
fill: color,
)
block(below: 1.5em)[
#content
]
}
#let problem(content) = context {
let c = color-palette.get().problem
block_quote(color: c)[
#kicker(color: c)[문제]
#content
]
}
#let solution(content) = context {
let c = color-palette.get().accent
block_quote(color: c)[
#kicker(color: c)[해결책]
#content
]
}
#let hl(content) = context {
let cp = color-palette.get()
// if content == "" {}
set text(
font: font-body,
weight: 700,
fill: cp.accent,
tracking: 1pt,
size: 10pt,
)
grid(
columns: (1fr, auto, 1fr),
align: horizon,
gutter: 10pt,
line(length: 100%, stroke: (paint: cp.rule)), content, line(length: 100%, stroke: (paint: cp.rule)),
)
}
#let block_equation(content) = {
callout()[
#content
]
}
#let stack(layers) = context {
let cp = color-palette.get()
let radius = 12pt
let gutter = 12pt
let gap = 24pt
for (idx, (title, body)) in layers.enumerate() {
let is_last = idx == layers.len() - 1
let content = {
v(radius - 12pt / 2)
text(
weight: 700,
)[
#title
#linebreak()
]
body
v(gap)
}
block(
above: if idx == 0 { auto } else { 0pt },
below: if is_last { auto } else { 0pt },
width: 100%,
)[
#layout(size => {
let content_width = size.width - radius * 2 - gutter
let content_size = measure(content, width: content_width)
grid(
columns: (radius * 2, 1fr),
gutter: gutter,
grid(
rows: (radius * 2, auto),
circle(radius: radius, fill: cp.accent)[
#set align(center + horizon)
#set text(font: font-serif, fill: color-ink-inverse, size: 12pt, weight: 700)
#{ idx + 1 }
],
line(
start: (50%, 0pt),
end: (50%, content_size.height - radius * 2),
stroke: (
paint: cp.accent,
thickness: 1pt,
cap: "round",
),
),
),
content,
)
})
]
}
}
#let selection(choices) = context {
let cp = color-palette.get()
for (name, selected, body) in choices {
let status = if selected [채택] else [기각]
let bg_color = if selected { cp.callout-surface } else { color-surface }
let border_color = if selected { cp.callout-border } else { color-rule-faint }
let name_color = if selected { cp.accent } else { cp.accent-faint }
let body_color = if selected { color-ink } else { color-ink-faint }
callout(bg_color: bg_color, border_color: border_color)[
#text(
weight: 700,
fill: name_color,
)[
#{ name }#h(0.5em)#text(size: 10pt)[#status]
#linebreak()
]
#text(
fill: body_color,
)[
#body
]
]
}
}
#let header_template(dark: false, font_color: none, show_activity: false) = place(
top + left,
dy: 1cm,
dx: -bleed,
)[
#let fc = if font_color != none { font_color } else if dark == true { color-ink-helper } else { color-ink-helper }
#block(width: 100% + 2 * bleed)[
#text(
font: font-caption,
size: 10pt,
fill: fc,
tracking: 0pt,
)[
#context {
let m = meta.get()
link((page: 1, x: 0pt, y: 0pt))[#m.cur_domain. #m.domain_name]
h(1fr)
if show_activity {
let level1s = query(heading.where(level: 1, outlined: true))
let n = counter(heading).get().at(0)
let cur = level1s.at(n - 1)
let prefix = str(m.cur_domain) + "." + str(n) + ". "
link(
cur.location(),
prefix + cur.body,
)
}
}
]
]
]
#let footer_template(dark: false, font_color: none) = context {
let fc = if font_color != none { font_color } else if dark == true { color-ink-helper } else { color-ink-helper }
align(center)[
#set text(
size: 10pt,
fill: fc,
)
#counter(page).display("1")
]
}
// ==================================================
// Page template
// ==================================================
// Cover template function
#let cover() = context {
let cp = color-palette.get()
page(
fill: cp.cover-surface,
)[
#set par(spacing: 0pt)
#v(3cm)
#context {
let m = meta.get()
align(center)[
#set text(
fill: cp.accent,
)
#text(font: font-caption)[
#pad-zeros(m.cur_domain, 2) / #pad-zeros(m.total_domain, 2)
]
#v(12pt)
#text(font: font-display, weight: 700, size: 40pt)[#m.domain_name]
#v(20pt)
#text(font: font-subhead, size: 15pt)[#m.domain_detail]
]
align(center + bottom)[
#text(weight: 700, size: 10pt, tracking: 1pt)[
#m.date
#v(10pt)
#upper(m.author)
]
]
}
#v(1cm)
]
}
// Table of contents templage function
#let table_of_contents() = page(
footer: footer_template(),
)[
#context {
let m = meta.get()
heading(level: 1, outlined: false, numbering: none)[#m.cur_domain. #m.domain_name]
}
#outline(title: none, depth: 2)
]
// Domain frame template function
#let domain_frame(title: "", bg_img_path: none, font_color: color-ink, chrome_color: color-ink-helper, content) = page(
background: place(
center + horizon,
if bg_img_path != none {
image(bg_img_path, width: 100.5%, height: 100.5%, fit: "cover")
},
),
header: header_template(font_color: chrome_color),
footer: footer_template(font_color: chrome_color),
)[
#set text(fill: font_color)
#heading(level: 1)[
#title
]
#content
]
// Activity cover template function
#let activity_cover(name: "", hook: "", content) = page(
header: header_template(),
footer: footer_template(),
)[
#heading(level: 1)[
#name
]
#block_quote()[
#hook
]
#content
]
// Activity intro template function
#let activity_intro(title: "", content) = page(
header: header_template(show_activity: true),
footer: footer_template(),
)[
#kicker("프로젝트 설명")
#heading(level: 2)[
#title
]
#content
]
// Activity process
#let activity_process(title: "", content) = page(
header: header_template(show_activity: true),
)[
#kicker("시행착오")
#heading(level: 2)[
#title
]
#content
]
// Activity result
#let activity_result(title: "", content) = page(
header: header_template(show_activity: true),
)[
#kicker("결론")
#heading(level: 2)[
#title
]
#content
]
// ==================================================
// Style Settings
// ==================================================
#let base(
cur_domain: 0,
total_domain: 0,
domain_name: "",
domain_detail: "",
date: "",
author: "",
main-hue: 0deg,
doc,
) = {
// meta data
meta.update(m => (
m
+ (
cur_domain: cur_domain,
total_domain: total_domain,
domain_name: domain_name,
domain_detail: domain_detail,
date: date,
author: author,
)
))
// color palette
let cp = make_palette(main-hue)
color-palette.update(m => (m + cp))
// document & page config
set document(
title: str(cur_domain) + ". " + domain_name,
description: domain_detail,
author: author,
)
set page(
paper: "a4",
margin: (x: 2.8cm, y: 4.2cm),
fill: color-surface,
)
// text config
set text(
font: font-body,
weight: 400,
fill: color-ink,
size: 12pt,
)
set par(
leading: 1.2em,
spacing: 2.4em,
)
show strong: set text(fill: cp.accent)
// list config
show list: set par(leading: 0.8em)
set list(
marker: ([--], [•]),
indent: 0.6em,
body-indent: 0.8em,
spacing: 1.2em,
)
show enum: set par(leading: 0.8em)
set enum(
indent: 0.6em,
body-indent: 0.8em,
spacing: 1.2em,
)
// heading config
show heading.where(level: 1): set heading(numbering: (..nums) => (
str(cur_domain) + "." + nums.pos().map(str).join(".") + "."
))
show heading.where(level: 1): set block(above: 0em, below: 50pt)
show heading.where(level: 1): set text(
font: font-display,
weight: 700,
size: 27pt,
tracking: 0pt,
)
show heading.where(level: 2): set block(above: 0em, below: 50pt)
show heading.where(level: 2): set par(leading: 1em)
show heading.where(level: 2): set text(
font: font-subhead,
weight: 700,
size: 18pt,
tracking: 0pt,
fill: color-ink,
)
// outline config
set outline(
indent: 1em,
)
show outline: set par(leading: 1em)
show outline.entry: it => link(
it.element.location(),
)[
#grid(
columns: (1fr, auto),
it.indented(it.prefix(), heading_text(it.body())), it.page(),
)
]
show outline.entry.where(level: 1): it => {
set text(
font: font-subhead,
size: 15pt,
weight: 700,
)
block(above: 50pt)[
#it
#line(length: 100%, stroke: (paint: color-ink-secondary, thickness: 1pt))
]
}
show outline.entry.where(level: 2): it => {
set text(
font: font-body,
size: 12pt,
weight: 700,
fill: color-ink-secondary,
)
block(above: 20pt)[
#it
]
}
// render content
doc
}