grelin_templates/resume-bp/data/layout.typ
MORRO 769decd445 Add resume-career template and migrate resume to resume-bp.
Introduce one-page career resume alongside the full BP resume, unify brand color across templates, and update monorepo docs and gitignore.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 23:19:37 +08:00

391 lines
12 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-main-section
}
let title-below = if below != auto {
below
} else if sidebar {
space-title-gap
} else {
space-main-title-below
}
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-edu-meta-gap)
#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: 01config 提供 abilities / language 时由 abilities-block 渲染
#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 has-abilities-section() = {
let ability-list = skills.at("abilities", default: ())
let has-lang = skills.at("language", default: none) != none
ability-list.len() > 0 or has-lang
}
/// 「能力」进度条区块skills.abilities / skills.language 有数据时才显示)
#let abilities-block() = {
if has-abilities-section() {
sidebar-subtitle("能力", above: space-none)
block(below: space-none)[
#for (i, item) in skills.at("abilities", default: ()).enumerate() {
skill-bar(
item.name,
item.level,
above: if i == 0 { space-none } else { space-skill-gap },
)
}
#let lang = skills.at("language", default: none)
#if lang != none {
skill-bar(
lang.name,
lang.level,
subtitle: lang.at("label", default: none),
above: if skills.at("abilities", default: ()).len() == 0 {
space-none
} else {
space-skill-gap
},
)
}
]
}
}
#let has-skill-groups() = skills.at("groups", default: ()).len() > 0
#let skill-groups-block() = {
for (i, group) in skills.at("groups", default: ()).enumerate() {
sidebar-subtitle(
group.title,
above: if i == 0 and not has-abilities-section() { space-none } else { space-subsection-gap },
)
block(below: space-none)[
#set par(leading: list-item-leading, spacing: space-none, justify: true)
#text(fill: color-text-light, size: size-secondary)[#group.items]
]
}
}
#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() = {
abilities-block()
if has-skill-groups() {
skill-groups-block()
} else {
sidebar-subtitle(
"技术栈",
above: if has-abilities-section() { space-subsection-gap } else { space-none },
)
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[
#block(above: space-entry-title-above, below: space-entry-title-below)[
#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)
]
]
]
#text(fill: color-text-light, size: size-body)[
#entry.location#if entry.location != "" []#entry.start - #entry.end
]
#let intro = entry.at("intro", default: "")
#if intro != "" [
#v(space-body)
#set par(leading: list-item-leading, spacing: space-none, justify: true)
#text(weight: "regular", fill: color-text-light, size: size-body)[#intro]
]
#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[
#block(above: space-entry-title-above, below: space-entry-title-below)[
#text(fill: color-body, size: size-body, weight: "bold")[#entry.title]
]
#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)
}
}
#let research-block() = {
if research != "" {
block(above: space-none)[
#set par(leading: leading-body, spacing: space-none, justify: true)
#text(fill: color-text-light, size: size-body)[#research]
]
}
}
#let publication-entry(entry, index) = block(above: space-none, below: space-none)[
#if index > 1 { v(space-between-entries) }
#text(fill: color-body, size: size-secondary, weight: "bold")[
\[#index\] #entry.title
]
#v(space-inline)
#text(fill: color-text-light, size: size-secondary)[#entry.authors]
#v(space-micro)
#text(fill: color-text-light, size: size-secondary)[#entry.venue]
]
#let publications-block() = {
for (i, entry) in publications.enumerate() {
publication-entry(entry, i + 1)
}
}