grelin_templates/resume/data/layout.typ
MORRO 350d101e0f Finalize resume template with modular config and layout polish.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-23 16:30:26 +08:00

309 lines
9.2 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 profile.photo-path != "" {
image(profile.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)[照片]
]
]
}
}
/// 左上 / 右下装饰条(通过 page foreground 绘制,每页自动出现)
#let page-corners-layer() = box(width: page-paper-width, height: page-paper-height)[
#place(
top + left,
dx: corner-bar-inset-x,
dy: corner-bar-inset-y,
block(width: corner-bar-width, height: corner-bar-height, fill: color-skill-bar),
)
#place(
bottom + right,
dx: -corner-bar-inset-x,
dy: -corner-bar-inset-y,
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)[#profile.name]
#v(space-hero-name-gap)
#text(font: heading-font, size: size-heading, weight: "bold", fill: color-accent-bright)[#profile.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-summary)[#profile.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, contact.address)
#contact-line(icon-phone, link("tel:" + contact.phone, contact.phone))
#contact-line(icon-mail, link("mailto:" + contact.email, contact.email))
#if contact.website != "" {
contact-line(icon-globe, link(contact.website, contact.website-label))
}
]
/// 教育背景条目notes 可选,一段普通正文)
#let edu-entry(degree, school, period, location, notes: "") = 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]
]
#if notes != "" [
#v(space-edu-notes-before)
#set par(leading: leading-edu-notes, spacing: space-none, justify: true)
#text(fill: color-text-light, size: size-secondary)[#notes]
]
]
#let education-block(show-notes: false) = {
for (i, item) in education.enumerate() {
if i > 0 {
v(space-between-entries)
}
edu-entry(
item.degree,
item.school,
item.period,
item.location,
notes: if show-notes { item.at("notes", default: "") } else { "" },
)
}
}
/// 侧栏技能补充列表skills.notes 为空则不渲染)
#let skill-notes-block() = {
if skills.notes.len() > 0 {
block(above: space-none)[
#set text(fill: color-text-light, size: size-secondary)
#set par(leading: list-item-leading, spacing: space-none, justify: true)
#set list(
tight: true,
indent: list-indent,
body-indent: list-body-indent,
)
#for line in skills.notes [
- #line
]
]
}
}
/// 侧栏技能子标题
#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 skills.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 skills.abilities.enumerate() {
skill-bar(
item.name,
item.level,
above: if i == 0 { space-none } else { space-skill-gap },
)
}
#skill-bar(
skills.language.name,
skills.language.level,
subtitle: skills.language.label,
above: space-skill-gap,
)
]
sidebar-subtitle("技术栈", above: space-subsection-gap)
tech-stack-block()
if skills.notes.len() > 0 {
sidebar-subtitle("补充", above: space-subsection-gap)
skill-notes-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,
)
#for line in entry.bullets [
- #line
]
]
#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)
}
}
/// 获奖经历条目bullets 可留空)
#let award-entry(entry) = block[
#text(fill: color-body, size: size-body, weight: "bold")[#entry.title]
#v(space-body)
#text(fill: color-text-light, size: size-body)[#entry.meta]
#if entry.bullets.len() > 0 [
#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,
)
#for line in entry.bullets [
- #line
]
]
]
#let awards-block() = {
for (i, entry) in awards.enumerate() {
if i > 0 { v(space-between-entries) }
award-entry(entry)
}
}