grelin_templates/resume/data/layout.typ

277 lines
7.7 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 icon-tech-frontend = image("../assets/icons/tech-frontend.svg")
#let icon-tech-backend = image("../assets/icons/tech-backend.svg")
#let icon-tech-ai = image("../assets/icons/tech-ai.svg")
#let icon-tech-database = image("../assets/icons/tech-database.svg")
#let icon-tech-devops = image("../assets/icons/tech-devops.svg")
#let tech-category-icon(name) = {
if name == "frontend" { icon-tech-frontend }
else if name == "backend" { icon-tech-backend }
else if name == "ai" { icon-tech-ai }
else if name == "database" { icon-tech-database }
else { icon-tech-devops }
}
#let profile-photo = if author-photo != none {
box(width: 100%, height: 100%, clip: true, author-photo)
} else {
rect(
width: 100%,
height: 100%,
fill: color-bar-bg,
stroke: color-divider + 0.5pt,
)[
#align(center + horizon)[
#text(fill: color-muted, size: size-caption)[照片]
]
]
}
/// 左上 / 右下装饰条
#let page-corners() = context {
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-accent-bright),
)
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-accent-bright),
)
}
/// 页眉:姓名 + 职位 + 简介 + 2 寸照片
#let hero-section() = block(width: 100%, below: space-md)[
#grid(
columns: (1fr, photo-width),
column-gutter: space-hero-gutter,
align: (top, top),
[
#text(
font: heading-font,
size: size-name,
weight: "bold",
fill: color-ink,
)[#author-name]
#v(space-xs)
#text(
font: heading-font,
size: size-subtitle,
weight: "bold",
fill: color-accent-bright,
)[#author-title]
#v(space-md)
#set text(weight: "regular")
#set par(leading: leading-summary, spacing: par-spacing-tight, justify: true)
#text(fill: color-text-light, size: size-body)[#author-summary]
],
[
#align(right)[
#box(
width: photo-width,
height: photo-height,
clip: true,
stroke: color-divider + 0.6pt,
profile-photo,
)
]
],
)
]
/// 页眉与正文之间的横线
#let header-divider() = block(width: 100%, above: space-xs, below: space-section)[
#line(length: 100%, stroke: color-divider + 0.55pt)
]
/// 分栏标题
#let section-title(title, sidebar: false, divider: false) = {
let above = if sidebar { space-section } else { space-between-sections }
let below = if sidebar { space-sidebar-title-gap } else { space-sm }
block(width: 100%, above: above, below: below)[
#text(
font: heading-font,
size: size-section,
weight: "bold",
fill: color-ink,
)[#title]
#if divider [
#v(space-sm)
#line(length: 100%, stroke: color-divider + 0.55pt)
]
]
}
/// 侧栏区块分隔线
#let sidebar-section-divider() = block(
width: 100%,
above: space-sidebar-divider,
below: space-xs,
)[
#line(length: 100%, stroke: color-divider + 0.55pt)
]
#let accent-icon(icon) = box(
width: 1em,
height: 1em,
baseline: 0.08em,
icon,
)
#let contact-line(icon, body) = block(below: space-xs)[
#grid(
columns: (1em, 1fr),
column-gutter: space-xs,
align: (center + horizon, left),
box(width: 0.95em, height: 0.95em, baseline: 0.08em, icon),
text(fill: color-text-light, size: size-contact)[#body],
)
]
#let contact-block() = block(above: 0pt)[
#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, note: none) = block(above: space-sm, below: space-xs)[
#text(fill: color-body, size: size-body)[#degree]
#v(space-xxs)
#text(fill: color-muted, size: size-meta)[
#school#if period != "" [ · #period]#if location != "" [ · #location]
]
#if note != none [
#v(space-xxs)
#text(fill: color-muted, size: size-small)[#note]
]
]
#let education-block() = {
for item in education {
edu-entry(item.degree, item.school, item.period, item.location, note: item.note)
}
}
/// 侧栏技能子标题
#let sidebar-subtitle(title) = block(above: space-md, below: space-xs)[
#text(
font: heading-font,
size: size-small,
fill: color-body,
)[#title]
]
/// 技能进度条level: 01
#let skill-bar(name, level, subtitle: none) = block(above: space-sm, below: space-xs)[
#if subtitle != none [
#grid(
columns: (1fr, auto),
align: horizon,
text(fill: color-body, size: size-body)[#name],
text(fill: color-muted, size: size-meta)[#subtitle],
)
] else [
#text(fill: color-body, size: size-body)[#name]
]
#v(space-xs)
#box(width: 100%, height: 4pt, radius: 1pt, clip: true)[
#place(left + top, block(width: 100%, height: 4pt, fill: color-bar-bg))
#place(left + top, block(width: level * 100%, height: 4pt, fill: color-skill-bar))
]
]
#let tech-pill(label) = box(
fill: color-page,
stroke: color-divider + 0.65pt,
radius: 3pt,
inset: (x: 5pt, y: 2pt),
baseline: 0.15em,
text(fill: color-body, size: size-tech-pill)[#label],
)
#let tech-category-block(category) = block(above: space-sm, below: space-xxs)[
#grid(
columns: (0.8em, 1fr),
column-gutter: space-xs,
align: horizon,
box(width: 0.8em, height: 0.8em, baseline: 0.1em, tech-category-icon(category.icon)),
text(font: heading-font, weight: "bold", size: size-tech-category, fill: color-ink)[#category.title]
)
#v(space-xs)
#box(width: 100%)[
#for (i, tag) in category.tags.enumerate() [
#if i > 0 [#h(3pt)]
#tech-pill(tag)
]
]
]
#let tech-stack-block() = block(width: 100%, above: space-xxs)[
#for category in tech-categories {
tech-category-block(category)
}
]
#let skills-section-block() = {
sidebar-subtitle("能力")
for item in abilities {
skill-bar(item.name, item.level)
}
sidebar-subtitle("技术栈")
tech-stack-block()
}
/// 主栏经历 / 项目条目
#let timeline-entry(entry, title-key: "company", subtitle-key: "role") = block[
#text(fill: color-accent, size: size-subtitle, weight: "bold")[
#if subtitle-key != none [
#entry.at(title-key) / #entry.at(subtitle-key)
] else [
#entry.at(title-key)
]
]
#v(space-xs)
#text(fill: color-muted, size: size-meta)[
#entry.location#if entry.location != "" []#entry.start - #entry.end
]
#v(space-sm)
#set text(weight: "regular", fill: color-text-light, size: size-body)
#set par(leading: leading-entry, spacing: par-spacing-tight, justify: true)
#set list(spacing: par-spacing-tight, indent: 0.8em, body-indent: 0.4em)
#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)
}
}