Files
university-portfolio-typst/template.typ
T
2026-09-06 21:49:21 +09:00

801 lines
16 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 = ("Source Code Pro", "Pretendard")
#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
#let page-paper = "a4"
#let page-height = 297mm
#let page-margin = (x: 2.8cm, y: 4.2cm)
// ==================================================
// 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,
),
)
#let evidence-data = state("evidence-data", none)
// ==================================================
// 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 fill_break() = context {
v(1fr)
}
#let heading_text(it) = {
if type(it) == str {
it
} else if it == linebreak() {
" "
} else if it.has("text") {
it.text
} else if it.has("children") {
it.children.map(heading_text).join("")
} else {
""
}
}
// Must be called from inside a context block.
#let cur_activity() = {
let m = meta.get()
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) + ". "
(
prefix: prefix,
name: heading_text(cur.body),
location: cur.location(),
)
}
// ==================================================
// Body components
// ==================================================
#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 callout(title: "", 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 }
block(
width: 100%,
inset: (x: 12pt, y: 18pt),
spacing: 1.6em,
// radius: 4pt,
fill: bg_c,
stroke: (paint: border_c),
)[
#if title != "" {
kicker(color: cp.accent)[#title]
}
#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,
height: size.height,
)
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 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()
let body = box(
inset: (x: if content == [] { 0pt } else { 10pt }),
)[#content]
set text(
font: font-body,
weight: 700,
fill: cp.accent,
tracking: 1pt,
size: 10pt,
)
grid(
columns: (1fr, auto, 1fr),
align: horizon,
line(length: 100%, stroke: (paint: cp.rule)), body, line(length: 100%, stroke: (paint: cp.rule)),
)
}
#let block_equation(title: "", description: "", content) = context {
let cp = color-palette.get()
callout()[
#set par(
spacing: 1em,
)
#if title != "" {
kicker(color: cp.accent)[#title]
}
#content
#if description != "" {
set text(
size: 9pt,
fill: color-ink-helper,
)
align(right)[#description]
}
]
}
#let u_link(url, content) = link(url)[
#underline(
content,
)
]
#let steps(..layers) = context {
let cp = color-palette.get()
let radius = 12pt
let gutter = 12pt
let gap = 24pt
let indent = radius + gutter
for (idx, (title, body)) in layers.pos().enumerate() {
let is_last = idx == layers.len() - 1
block(
above: if idx == 0 { auto } else { 0pt },
below: if is_last { auto } else { 0pt },
inset: (left: indent),
stroke: (left: (paint: cp.accent, thickness: 1pt)),
width: 100%,
)[
#place(
left + top,
dx: -indent - radius,
)[
#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 }
]
]
#v(radius - 12pt / 2)
#text(weight: 700)[
#title
#linebreak()
]
#body
#v(gap)
]
}
}
#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 ref_evidence(kinds: none) = {
let ca = cur_activity()
set ref(
supplement: "증빙 자료.",
)
align(bottom)[
#hl[]
*활동 증빙 자료:*
#underline[#ref(label("evidence) " + ca.name))]
#if kinds != none {
text(fill: color-ink-helper)[(#kinds)]
}
]
}
#let add_evidence(activity_ref: none, ..content) = context {
let name = str(activity_ref.target)
let data = (
(
name,
content.pos().flatten(),
),
).to-dict()
evidence-data.update(old => {
if old == none {
data
} else {
old + data
}
})
}
#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: 2, x: 0pt, y: 0pt))[#m.cur_domain. #m.domain_name]
h(1fr)
if show_activity {
let ca = cur_activity()
link(
ca.location,
ca.prefix + ca.name,
)
}
}
]
]
]
#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 template function
#let table_of_contents() = context {
let cp = color-palette.get()
let m = meta.get()
page(
footer: footer_template(),
)[
#show outline.entry.where(level: 1): it => link(it.element.location())[
#block(above: 25pt)[
#grid(
columns: (1fr, auto),
align: horizon,
text(font: font-subhead, size: 15pt, weight: 700)[
#it.indented(it.prefix(), heading_text(it.body()))
],
text(size: 10pt, fill: color-ink-helper)[
#it.page()
],
)
#let desc = it.element.supplement
#if desc != [domain] and desc != [evidence] {
text(fill: color-ink-helper, size: 10pt)[
#it.element.supplement
]
}
#v(10pt)
#line(length: 100%, stroke: (paint: cp.accent-faint, thickness: 0.5pt))
]
]
#heading(level: 1, outlined: false, numbering: none)[#m.cur_domain. #m.domain_name]
#label("table_of_contents")
#outline(title: none, depth: 1)
]
}
// Domain frame template function
#let domain_frame(
title: "",
bg_img_path: none,
cover_color: white.transparentize(50%),
font_color: color-ink,
chrome_color: color-ink-helper,
content,
) = page(
background: if bg_img_path != none {
stack(
image(bg_img_path, width: 100.5%, height: 100.5%, fit: "cover"),
move(
dy: -50%,
rect(width: 100.5%, height: 100.5%, fill: cover_color),
),
)
} else { none },
header: header_template(font_color: chrome_color),
footer: footer_template(font_color: chrome_color),
)[
#set text(fill: font_color)
#show strong: set text(fill: font_color)
#heading(level: 1, supplement: [domain])[
#title
]
#content
]
// Activity cover template function
#let activity_cover(name: "", hook: "", content) = context {
let cp = color-palette.get()
page(
header: header_template(),
footer: footer_template(),
fill: cp.cover-surface,
)[
#show heading.where(level: 1): set block(above: 0em, below: 25pt)
#heading(level: 1, supplement: hook)[
#name
]
#label(heading_text(name))
#block_quote()[
#hook
]
#v(25pt)
#content
]
}
#let activity_table() = context {
let cp = color-palette.get()
let ca = cur_activity()
let level1s = query(heading.where(level: 1, outlined: true))
let n = counter(heading).get().at(0)
let sel = heading.where(level: 2, outlined: true).after(ca.location)
if n < level1s.len() {
sel = sel.before(level1s.at(n).location(), inclusive: false)
}
page(
header: header_template(show_activity: true),
footer: footer_template(),
)[
#show outline: it => [
#line(length: 100%, stroke: (paint: cp.accent-faint, thickness: 0.5pt))
#it
]
#show outline.entry.where(level: 2): it => link(it.element.location())[
#set par(
spacing: 1em,
)
#block(above: 15pt)[
#grid(
columns: (1fr, auto),
align: horizon,
text(size: 12pt, weight: 700)[
#it.body()
],
text(size: 8pt, fill: color-ink-helper)[
#it.page()
],
)
#let desc = it.element.supplement
#par(
spacing: 0pt,
leading: 0.8em,
)[
#text(fill: color-ink-helper, size: 10pt)[
#it.element.supplement
]
]
#line(length: 100%, stroke: (paint: cp.accent-faint, thickness: 0.5pt))
]
]
#heading(level: 2, outlined: false)[
시행착오 목차
]
#outline(title: none, target: sel)
]
}
// Activity process
#let activity_process(title: "", hook: [], content) = page(
header: header_template(show_activity: true),
footer: footer_template(),
)[
#kicker("시행착오")
#heading(level: 2, supplement: hook)[
#title
]
#content
]
#let evidence() = context {
let cp = color-palette.get()
let ed = evidence-data.get()
page(
header: header_template(),
footer: footer_template(),
)[
#set heading(
numbering: (..nums) => {
numbering("A", nums.at(1))
},
supplement: [evidence],
)
#show heading.where(level: 2): it => {
let num = counter(heading).display(
(..nums) => numbering("A", nums.at(1)),
)
block[
#num. #it.body
]
}
#show table.cell.where(x: 0): set text(
weight: 700,
fill: cp.accent,
)
#heading(level: 1, numbering: none)[
증빙 자료
]
#let cnt = ed.len()
#for (name, data) in ed [
#heading(level: 2, supplement: "evidence")[#name]
#label("evidence) " + name)
#table(
columns: (10em, 1fr),
..data,
)
#{ cnt -= 1 }
#if cnt > 0 { pagebreak() }
]
]
}
// ==================================================
// 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: page-paper,
margin: page-margin,
fill: color-surface,
)
// text config
set text(
font: font-body,
weight: 400,
fill: color-ink,
size: 12pt,
)
set par(
leading: 1.2em,
spacing: 1.8em,
)
show raw: set text(font: font-code)
show strong: set text(fill: cp.accent)
// underline config
set underline(
offset: 2pt,
stroke: (paint: cp.accent, thickness: 1pt),
)
// list config
show list: set par(leading: 0.8em)
set list(
marker: ([--], [•]),
indent: 0.6em,
body-indent: 0.8em,
spacing: 0.8em,
)
show enum: set par(leading: 0.8em)
set enum(
indent: 0.6em,
body-indent: 0.8em,
spacing: 0.8em,
)
// image config
show image: set align(center)
show image: set par(
spacing: 1.6em,
)
// figure config
show figure: set text(
fill: color-ink-helper,
size: 10pt,
)
show figure.caption: it => {
"[ " + it.body + " ]"
}
// table config
show table: set text(size: 10pt)
set table(
inset: 0.8em,
align: left + top,
stroke: (paint: cp.rule),
)
// 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,
)
// render content
doc
}