grelin_templates/resume/data/layout.typ
2026-06-23 14:39:21 +08:00

247 lines
7.4 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#import "theme.typ": *
#import "config.typ": *
#let icon-map-pin = image("../assets/icons/map-pin.svg")
#let icon-phone = image("../assets/icons/phone.svg")
#let icon-mail = image("../assets/icons/mail.svg")
#let icon-globe = image("../assets/icons/globe.svg")
#let profile-photo = if author-photo-path != "" {
image(author-photo-path, width: 100%, height: 100%, fit: "cover")
} else {
none
}
#let profile-photo-frame(content) = {
if content != none {
box(width: 100%, height: 100%, clip: true, content)
} else {
rect(
width: 100%,
height: 100%,
fill: color-bar-bg,
stroke: stroke-placeholder,
)[
#align(center + horizon)[
#text(fill: color-muted, size: size-body)[照片]
]
]
}
}
/// 左上 / 右下装饰条
#let page-corners() = {
place(
top + left,
dx: -page-margin.left,
dy: -page-margin.top + corner-bar-inset,
block(width: corner-bar-width, height: corner-bar-height, fill: color-skill-bar),
)
place(
bottom + right,
dx: page-margin.right,
dy: page-margin.bottom - corner-bar-inset,
block(width: corner-bar-width, height: corner-bar-height, fill: color-skill-bar),
)
}
/// 页眉:姓名 + 职位 + 简介 + 2 寸照片
#let hero-section() = block(width: 100%, below: space-none)[
#grid(
columns: (1fr, photo-width),
column-gutter: space-hero-to-photo,
align: (top, top),
[
#text(font: heading-font, size: size-name, weight: "bold", fill: color-ink)[#author-name]
#v(space-hero-name-gap)
#text(font: heading-font, size: size-heading, weight: "bold", fill: color-accent-bright)[#author-title]
#v(space-hero-title-gap)
#set text(weight: "regular")
#set par(leading: leading-summary, spacing: space-none, justify: true)
#text(fill: color-text-light, size: size-body)[#author-summary]
],
[
#align(right)[
#box(width: photo-width, height: photo-height, clip: true, stroke: stroke-photo)[
#profile-photo-frame(profile-photo)
]
]
],
)
]
/// 页眉与正文之间的横线
#let header-divider() = block(width: 100%, above: space-block, below: space-block)[
#line(length: 100%, stroke: stroke-divider)
]
/// 分栏标题
#let section-title(title, sidebar: false, divider: false, above: auto, below: auto) = {
let title-above = if above != auto {
above
} else if sidebar {
space-none
} else {
space-block
}
let title-below = if below != auto { below } else { space-title-gap }
block(width: 100%, above: title-above, below: title-below)[
#text(font: heading-font, size: size-heading, weight: "bold", fill: color-ink)[#title]
#if divider [
#v(space-inline)
#line(length: 100%, stroke: stroke-divider)
]
]
}
/// 侧栏区块分隔线
#let sidebar-section-divider() = block(width: 100%, above: space-block, below: space-block)[
#line(length: 100%, stroke: stroke-divider)
]
#let contact-line(icon, body) = block(below: space-inline)[
#grid(
columns: (icon-column-width, 1fr),
column-gutter: space-micro,
align: (center + horizon, left),
box(width: icon-box-size, height: icon-box-size, baseline: 0.08em, icon),
text(fill: color-body, size: size-secondary)[#body],
)
]
#let contact-block() = block(above: space-none)[
#contact-line(icon-map-pin, author-address)
#contact-line(icon-phone, link("tel:" + author-phone, author-phone))
#contact-line(icon-mail, link("mailto:" + author-email, author-email))
#if author-website != "" {
contact-line(icon-globe, link(author-website, author-website-label))
}
]
/// 教育背景条目
#let edu-entry(degree, school, period, location) = block(above: space-none, below: space-none)[
#text(fill: color-body, size: size-body)[#degree]
#v(space-inline)
#text(fill: color-text-light, size: size-secondary)[
#school#if period != "" [ · #period]#if location != "" [ · #location]
]
]
#let education-block() = {
for (i, item) in education.enumerate() {
if i > 0 {
v(space-between-entries)
}
edu-entry(item.degree, item.school, item.period, item.location)
}
}
/// 侧栏技能子标题
#let sidebar-subtitle(title, above: auto, below: auto) = {
let gap-above = if above != auto { above } else { space-inline }
let gap-below = if below != auto { below } else { space-subtitle-gap }
block(above: gap-above, below: gap-below)[
#text(font: heading-font, size: size-body, fill: color-body)[#title]
]
}
/// 技能进度条level: 01条与条之间仅用 above 控制间距
#let skill-bar(name, level, subtitle: none, above: space-none) = block(above: above, below: space-none)[
#if subtitle != none [
#grid(
columns: (1fr, auto),
align: horizon,
text(fill: color-text-light, size: size-secondary)[#name],
text(fill: color-text-light, size: size-secondary)[#subtitle],
)
] else [
#text(fill: color-text-light, size: size-secondary)[#name]
]
#v(space-inline)
#box(width: 100%, height: skill-bar-height, radius: skill-bar-radius, clip: true)[
#place(left + top, block(width: 100%, height: skill-bar-height, fill: color-bar-bg))
#place(left + top, block(width: level * 100%, height: skill-bar-height, fill: color-skill-bar))
]
]
#let tech-pill(label) = box(
fill: color-page,
stroke: stroke-pill,
radius: pill-radius,
inset: (x: pill-inset-x, y: pill-inset-y),
baseline: 0.15em,
text(fill: color-text-light, size: size-secondary)[#label],
)
#let tech-stack-block() = block(width: 100%, above: space-none, below: space-inline)[
#box(width: 100%)[
#set par(spacing: pill-gap, leading: leading-pill-wrap)
#for (i, tag) in tech-tags.enumerate() [
#if i > 0 [#h(pill-gap)]
#tech-pill(tag)
]
]
]
#let skills-section-block() = {
sidebar-subtitle("能力", above: space-none)
block(below: space-none)[
#for (i, item) in abilities.enumerate() {
skill-bar(
item.name,
item.level,
above: if i == 0 { space-none } else { space-skill-gap },
)
}
#skill-bar(
language_profile.name,
language_profile.level,
subtitle: language_profile.label,
above: space-skill-gap,
)
]
sidebar-subtitle("技术栈", above: space-subsection-gap)
tech-stack-block()
}
/// 主栏经历 / 项目条目
#let timeline-entry(entry, title-key: "company", subtitle-key: "role") = block[
#text(fill: color-body, size: size-body, weight: "bold")[
#if subtitle-key != none [
#entry.at(title-key) / #entry.at(subtitle-key)
] else [
#entry.at(title-key)
]
]
#v(space-body)
#text(fill: color-text-light, size: size-body)[
#entry.location#if entry.location != "" []#entry.start - #entry.end
]
#v(space-body)
#set text(weight: "regular", fill: color-text-light, size: size-body)
#set par(leading: list-item-leading, spacing: space-none, justify: true)
#set list(
tight: true,
indent: list-indent,
body-indent: list-body-indent,
)
#entry.description
]
#let project-entry(entry) = timeline-entry(entry, title-key: "name", subtitle-key: "org")
#let experience-entry(entry) = timeline-entry(entry, title-key: "company", subtitle-key: "role")
#let experiences-block() = {
for (i, entry) in experiences.enumerate() {
if i > 0 { v(space-between-entries) }
experience-entry(entry)
}
}
#let projects-block() = {
for (i, entry) in projects.enumerate() {
if i > 0 { v(space-between-entries) }
project-entry(entry)
}
}