@bafif/foja 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +674 -0
- package/README.es.md +40 -0
- package/README.md +39 -2
- package/dist/foja.mjs +29255 -0
- package/dist/template/fonts/GUST-FONT-LICENSE.txt +29 -0
- package/dist/template/fonts/NotoSans-Bold.ttf +0 -0
- package/dist/template/fonts/NotoSans-BoldItalic.ttf +0 -0
- package/dist/template/fonts/NotoSans-Italic.ttf +0 -0
- package/dist/template/fonts/NotoSans-LICENSE.txt +94 -0
- package/dist/template/fonts/NotoSans-Medium.ttf +0 -0
- package/dist/template/fonts/NotoSans-Regular.ttf +0 -0
- package/dist/template/fonts/XCharter-Bold.otf +0 -0
- package/dist/template/fonts/XCharter-BoldItalic.otf +0 -0
- package/dist/template/fonts/XCharter-Italic.otf +0 -0
- package/dist/template/fonts/XCharter-LICENSE.txt +14 -0
- package/dist/template/fonts/XCharter-Roman.otf +0 -0
- package/dist/template/fonts/lmmono9-regular.otf +0 -0
- package/dist/template/labels.json +86 -0
- package/dist/template/lib.typ +233 -0
- package/dist/template/styles/arco.typ +177 -0
- package/dist/template/styles/banda.typ +97 -0
- package/dist/template/styles/claustro.typ +91 -0
- package/dist/template/styles/legajo.typ +155 -0
- package/dist/template/styles/llano.typ +80 -0
- package/dist/template/styles/minimal.typ +87 -0
- package/dist/template/styles/portada.typ +132 -0
- package/dist/template/styles/recto.typ +86 -0
- package/dist/template/styles/rotulo.typ +146 -0
- package/dist/template/styles.json +59 -0
- package/package.json +44 -4
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Banda: a band of colour across the top of the first page with the name, the photo beside it;
|
|
2
|
+
// one column below, sans-serif, section titles marked with a block of the accent colour.
|
|
3
|
+
|
|
4
|
+
#import "/lib.typ": *
|
|
5
|
+
|
|
6
|
+
#let ink = rgb("#1d2327")
|
|
7
|
+
#let muted = ink.lighten(42%)
|
|
8
|
+
#let accent-color = accent(rgb("#1f5f5b"))
|
|
9
|
+
#let m = margins((top: 10, bottom: 14, left: 16, right: 16))
|
|
10
|
+
#let band = 36mm
|
|
11
|
+
#let text-width = paper.width - m.left - m.right
|
|
12
|
+
#let small = base-size * 0.9
|
|
13
|
+
|
|
14
|
+
#set document(..document-info)
|
|
15
|
+
#set page(
|
|
16
|
+
width: paper.width,
|
|
17
|
+
height: paper.height,
|
|
18
|
+
margin: m,
|
|
19
|
+
background: context if counter(page).get().first() == 1 { place(top + left, rect(width: 100%, height: band, fill: accent-color)) },
|
|
20
|
+
)
|
|
21
|
+
#set text(font: opt("font", "Noto Sans"), size: base-size * 0.95, fill: ink, lang: lang)
|
|
22
|
+
#set par(leading: 0.6em, spacing: 0.6em * density, justify: false)
|
|
23
|
+
#set list(marker: text(fill: accent-color, sym.bullet), indent: 0.2em, body-indent: 0.5em, spacing: 0.4em * density)
|
|
24
|
+
#show link: set text(hyphenate: false)
|
|
25
|
+
|
|
26
|
+
#geometry(main: (x: m.left, width: text-width))
|
|
27
|
+
|
|
28
|
+
#let section-title(title) = block(above: 1.35em * density, below: 0.6em * density, sticky: true, {
|
|
29
|
+
box(baseline: 0.12em, rect(width: 3.5pt, height: 0.95em, fill: accent-color))
|
|
30
|
+
h(0.5em)
|
|
31
|
+
text(size: base-size * 1.15, weight: "medium", title)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
#let entry(b, compact) = anchored(b.path, block(above: 0.8em * density, below: 0pt, {
|
|
35
|
+
text(weight: "bold", b.title)
|
|
36
|
+
h(1fr)
|
|
37
|
+
text(size: small, fill: muted, b.period)
|
|
38
|
+
if b.place != "" {
|
|
39
|
+
linebreak()
|
|
40
|
+
text(size: small, fill: accent-color, weight: "medium", b.place)
|
|
41
|
+
}
|
|
42
|
+
if b.note != none { par(text(size: small, fill: muted, b.note)) }
|
|
43
|
+
if not compact and b.items.len() > 0 { list(..b.items.map(item-body)) }
|
|
44
|
+
}))
|
|
45
|
+
|
|
46
|
+
#let section(s) = anchored(s.path, {
|
|
47
|
+
section-title(s.title)
|
|
48
|
+
for b in s.blocks {
|
|
49
|
+
if b.type == "entry" { entry(b, s.compact) } else if b.type == "item" {
|
|
50
|
+
anchored(b.path, block(above: 0.45em * density, below: 0pt, list(item-body(b))))
|
|
51
|
+
} else { anchored(b.path, par(inline(b.text))) }
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------- the band
|
|
56
|
+
|
|
57
|
+
#let picture = photo(band - m.top - 8mm, shape: if cv.photo-shape == "square" { "rounded" } else { cv.photo-shape })
|
|
58
|
+
#block(height: band - m.top, width: 100%, {
|
|
59
|
+
set text(fill: white)
|
|
60
|
+
grid(
|
|
61
|
+
columns: (1fr, auto),
|
|
62
|
+
align: (horizon, horizon),
|
|
63
|
+
{
|
|
64
|
+
anchored("name", text(size: base-size * 2.3, weight: "bold", cv.name))
|
|
65
|
+
v(0.1em)
|
|
66
|
+
anchored("subtitle", text(size: base-size * 1.25, cv.subtitle))
|
|
67
|
+
},
|
|
68
|
+
if picture != none { anchored("photo", picture) },
|
|
69
|
+
)
|
|
70
|
+
})
|
|
71
|
+
#v(0.9em)
|
|
72
|
+
|
|
73
|
+
// ---------------------------------------------------------------- body
|
|
74
|
+
|
|
75
|
+
#if cv.contacts.len() > 0 {
|
|
76
|
+
anchored("contacts", text(size: small, fill: muted, cv.contacts.map(contact-text).join(h(0.45em) + text(fill: accent-color, sym.bullet) + h(0.45em))))
|
|
77
|
+
}
|
|
78
|
+
#if cv.skills.len() > 0 {
|
|
79
|
+
v(0.5em)
|
|
80
|
+
for (i, row) in cv.skills.enumerate() {
|
|
81
|
+
anchored("skills." + str(i), grid(
|
|
82
|
+
columns: (1fr,) * row.len(),
|
|
83
|
+
column-gutter: 1.5em,
|
|
84
|
+
..row.map(g => text(size: small)[#text(weight: "bold", g.title): #g.items.join(" · ")]),
|
|
85
|
+
))
|
|
86
|
+
v(0.25em)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
#if cv.highlight != none {
|
|
90
|
+
v(0.5em)
|
|
91
|
+
anchored("highlight", par[#text(fill: accent-color, weight: "bold", cv.highlight.title) #h(0.4em) #cv.highlight.text])
|
|
92
|
+
}
|
|
93
|
+
#if cv.summary != "" {
|
|
94
|
+
v(0.4em)
|
|
95
|
+
anchored("summary", par(justify: true, inline(cv.summary)))
|
|
96
|
+
}
|
|
97
|
+
#for s in cv.sections { section(s) }
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// Claustro: the classic one-page CV of university career offices (in the manner of Harvard's).
|
|
2
|
+
// Serif; the name centred with the contacts on one line below it; section titles in capitals over a
|
|
3
|
+
// rule from margin to margin; each entry with the institution above and the position and dates
|
|
4
|
+
// below, then bullets. One column of plain text, no icons or pictures of text, laid out in the
|
|
5
|
+
// order it is read, so that applicant tracking systems read it as people do.
|
|
6
|
+
|
|
7
|
+
#import "/lib.typ": *
|
|
8
|
+
|
|
9
|
+
#let ink = rgb("#1a1a1a")
|
|
10
|
+
#let muted = ink.lighten(38%)
|
|
11
|
+
#let accent-color = accent(ink)
|
|
12
|
+
#let m = margins((top: 14, bottom: 14, left: 18, right: 18))
|
|
13
|
+
#let text-width = paper.width - m.left - m.right
|
|
14
|
+
#let small = base-size * 0.9
|
|
15
|
+
|
|
16
|
+
#set document(..document-info)
|
|
17
|
+
#set page(width: paper.width, height: paper.height, margin: m)
|
|
18
|
+
#set text(font: opt("font", "XCharter"), size: base-size, fill: ink, lang: lang, hyphenate: false)
|
|
19
|
+
#set par(leading: 0.55em, spacing: 0.55em * density, justify: false)
|
|
20
|
+
#set list(marker: [•], indent: 0.5em, body-indent: 0.5em, spacing: 0.35em * density)
|
|
21
|
+
|
|
22
|
+
#geometry(main: (x: m.left, width: text-width))
|
|
23
|
+
|
|
24
|
+
#let section-title(title) = block(
|
|
25
|
+
above: 1.15em * density,
|
|
26
|
+
below: 0.5em * density,
|
|
27
|
+
sticky: true,
|
|
28
|
+
width: 100%,
|
|
29
|
+
inset: (bottom: 2.5pt),
|
|
30
|
+
stroke: (bottom: 0.7pt + accent-color),
|
|
31
|
+
text(weight: "bold", tracking: 0.05em, upper(title)),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
// The institution in bold above; the position in italics and the dates on the right below it. An
|
|
35
|
+
// entry without an institution has the position in bold and the dates on its line.
|
|
36
|
+
#let entry(b, compact) = anchored(b.path, block(above: 0.8em * density, below: 0pt, {
|
|
37
|
+
if b.place != "" {
|
|
38
|
+
text(weight: "bold", b.place)
|
|
39
|
+
linebreak()
|
|
40
|
+
emph(b.title)
|
|
41
|
+
} else { text(weight: "bold", b.title) }
|
|
42
|
+
if b.period != "" {
|
|
43
|
+
h(1fr)
|
|
44
|
+
b.period
|
|
45
|
+
}
|
|
46
|
+
if b.note != none {
|
|
47
|
+
linebreak()
|
|
48
|
+
text(size: small, fill: muted, b.note)
|
|
49
|
+
}
|
|
50
|
+
if not compact and b.items.len() > 0 { list(..b.items.map(item-body)) }
|
|
51
|
+
}))
|
|
52
|
+
|
|
53
|
+
#let section(s) = anchored(s.path, {
|
|
54
|
+
section-title(s.title)
|
|
55
|
+
for b in s.blocks {
|
|
56
|
+
if b.type == "entry" { entry(b, s.compact) } else if b.type == "item" {
|
|
57
|
+
anchored(b.path, block(above: 0.45em * density, below: 0pt, list(item-body(b))))
|
|
58
|
+
} else { anchored(b.path, par(inline(b.text))) }
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------- header
|
|
63
|
+
|
|
64
|
+
#let head = align(center, {
|
|
65
|
+
anchored("name", par(text(size: base-size * 2.2, weight: "bold", cv.name)))
|
|
66
|
+
if cv.subtitle != "" { anchored("subtitle", par(text(size: base-size * 1.1, style: "italic", cv.subtitle))) }
|
|
67
|
+
if cv.contacts.len() > 0 { anchored("contacts", par(text(size: small, cv.contacts.map(contact-text).join(" · ")))) }
|
|
68
|
+
})
|
|
69
|
+
#let picture = photo(base-size * 7)
|
|
70
|
+
#if picture == none { head } else {
|
|
71
|
+
// The photo on the right, an empty cell as wide on the left, so the header stays centred.
|
|
72
|
+
grid(columns: (base-size * 7, 1fr, base-size * 7), column-gutter: 1em, align: (top, horizon, top), [], head, anchored("photo", picture))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ---------------------------------------------------------------- body
|
|
76
|
+
|
|
77
|
+
#if cv.highlight != none {
|
|
78
|
+
v(0.6em * density)
|
|
79
|
+
anchored("highlight", par[#text(weight: "bold", cv.highlight.title): #cv.highlight.text])
|
|
80
|
+
}
|
|
81
|
+
#if cv.summary != "" {
|
|
82
|
+
section-title(labels.profile)
|
|
83
|
+
anchored("summary", par(inline(cv.summary)))
|
|
84
|
+
}
|
|
85
|
+
#if cv.skills.len() > 0 {
|
|
86
|
+
section-title(labels.skills)
|
|
87
|
+
for (i, row) in cv.skills.enumerate() {
|
|
88
|
+
anchored("skills." + str(i), { for g in row { par[#text(weight: "bold", g.title): #g.items.join(", ")] } })
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
#for s in cv.sections { section(s) }
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// Legajo: Foja's own look, after the "foja de servicios" it is named for. The name in small caps
|
|
2
|
+
// over a rule, a sidebar card on a light tint, small-caps section titles with a hairline, and the
|
|
3
|
+
// dates in a column of their own, like a record. One or two columns, sidebar on either side.
|
|
4
|
+
|
|
5
|
+
#import "/lib.typ": *
|
|
6
|
+
|
|
7
|
+
#let ink = rgb("#1f2a2e")
|
|
8
|
+
#let muted = ink.lighten(38%)
|
|
9
|
+
#let accent-color = accent(rgb("#1f5f5b"))
|
|
10
|
+
#let tint = accent-color.lighten(92%)
|
|
11
|
+
#let m = margins((top: 12, bottom: 12, left: 12, right: 12))
|
|
12
|
+
#let two = opt("columns", 2) == 2
|
|
13
|
+
#let side-left = opt("sidebar", "left") == "left"
|
|
14
|
+
#let text-width = paper.width - m.left - m.right
|
|
15
|
+
#let gutter = 6mm
|
|
16
|
+
#let side-width = if two { text-width * opt("sidebarWidth", 27) / 100 } else { 0pt }
|
|
17
|
+
#let main-width = if two { text-width - side-width - gutter } else { text-width }
|
|
18
|
+
#let main-x = if two and side-left { m.left + side-width + gutter } else { m.left }
|
|
19
|
+
#let side-x = if side-left { m.left } else { m.left + main-width + gutter }
|
|
20
|
+
#let small = base-size * 0.88
|
|
21
|
+
#let date-width = 2cm * (base-size / 10pt)
|
|
22
|
+
|
|
23
|
+
#set document(..document-info)
|
|
24
|
+
#set page(width: paper.width, height: paper.height, margin: m)
|
|
25
|
+
#set text(font: opt("font", "XCharter"), size: base-size, fill: ink, lang: lang)
|
|
26
|
+
#set par(leading: 0.55em, spacing: 0.55em * density, justify: true)
|
|
27
|
+
#set list(marker: text(fill: accent-color, [–]), indent: 0pt, body-indent: 0.5em, spacing: 0.4em * density)
|
|
28
|
+
#show link: set text(hyphenate: false, lang: "en")
|
|
29
|
+
|
|
30
|
+
#geometry(main: (x: main-x, width: main-width), side: if two { (x: side-x, width: side-width) } else { none })
|
|
31
|
+
|
|
32
|
+
#let caps(body, tracking: 0.05em) = text(tracking: tracking, smallcaps(body))
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------- pieces
|
|
35
|
+
|
|
36
|
+
#let section-title(title) = block(above: 1.15em * density, below: 0.6em * density, sticky: true, {
|
|
37
|
+
text(fill: accent-color, weight: "bold", caps(lower(title), tracking: 0.08em))
|
|
38
|
+
h(0.6em)
|
|
39
|
+
box(width: 1fr, baseline: -0.32em, line(length: 100%, stroke: 0.5pt + accent-color.lighten(50%)))
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
#let entry(b, compact) = anchored(b.path, block(above: 0.75em * density, below: 0pt, grid(
|
|
43
|
+
columns: (date-width, 1fr),
|
|
44
|
+
column-gutter: 0.8em,
|
|
45
|
+
par(justify: false, text(size: small, fill: muted, b.period)),
|
|
46
|
+
{
|
|
47
|
+
text(weight: "bold", b.title)
|
|
48
|
+
if b.place != "" {
|
|
49
|
+
linebreak()
|
|
50
|
+
text(style: "italic", fill: muted, b.place)
|
|
51
|
+
}
|
|
52
|
+
if b.note != none { par(text(size: small, fill: muted, b.note)) }
|
|
53
|
+
if not compact and b.items.len() > 0 { list(..b.items.map(item-body)) }
|
|
54
|
+
},
|
|
55
|
+
)))
|
|
56
|
+
|
|
57
|
+
#let main-section(s) = anchored(s.path, {
|
|
58
|
+
section-title(s.title)
|
|
59
|
+
for b in s.blocks {
|
|
60
|
+
if b.type == "entry" { entry(b, s.compact) } else if b.type == "item" {
|
|
61
|
+
anchored(b.path, block(above: 0.5em * density, below: 0pt, list(item-body(b))))
|
|
62
|
+
} else { anchored(b.path, par(inline(b.text))) }
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
#let side-heading(title) = text(size: small * 0.95, fill: accent-color, weight: "bold", caps(lower(title), tracking: 0.08em))
|
|
67
|
+
#let side-block(title, body) = block(below: 1.1em * density, {
|
|
68
|
+
side-heading(title)
|
|
69
|
+
v(0.25em)
|
|
70
|
+
body
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
#let side-section(s) = side-block(s.title, for b in s.blocks {
|
|
74
|
+
anchored(b.path, column: "side", block(below: 0.6em, if b.type == "entry" {
|
|
75
|
+
strong(b.title)
|
|
76
|
+
if b.place != "" { linebreak() + emph(b.place) }
|
|
77
|
+
if b.period != "" { linebreak() + text(fill: muted, b.period) }
|
|
78
|
+
} else {
|
|
79
|
+
par(justify: true, item-body(b))
|
|
80
|
+
}))
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
#let contacts-lines = cv.contacts.map(contact-text).join(linebreak())
|
|
84
|
+
|
|
85
|
+
#let sidebar = block(fill: tint, inset: (x: 10pt, y: 12pt), radius: 3pt, width: 100%, {
|
|
86
|
+
set text(size: small)
|
|
87
|
+
set par(justify: false, leading: 0.5em)
|
|
88
|
+
let picture = photo(side-width - 20pt)
|
|
89
|
+
if picture != none {
|
|
90
|
+
anchored("photo", column: "side", align(center, picture))
|
|
91
|
+
v(6pt)
|
|
92
|
+
}
|
|
93
|
+
if cv.highlight != none { anchored("highlight", column: "side", side-block(cv.highlight.title, cv.highlight.text)) }
|
|
94
|
+
if cv.contacts.len() > 0 { anchored("contacts", column: "side", side-block(labels.contact, contacts-lines)) }
|
|
95
|
+
for (i, row) in cv.skills.enumerate() {
|
|
96
|
+
anchored("skills." + str(i), column: "side", if row.len() == 1 {
|
|
97
|
+
side-block(row.first().title, row.first().items.join(linebreak()))
|
|
98
|
+
} else {
|
|
99
|
+
grid(columns: row.len(), column-gutter: 1em, ..row.map(g => side-block(g.title, g.items.join(linebreak()))))
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
for s in cv.sections.filter(s => s.side) { anchored(s.path, column: "side", side-section(s)) }
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
#let heading-lines = {
|
|
106
|
+
anchored("name", text(size: base-size * 2.1, caps(cv.name, tracking: 0.04em)))
|
|
107
|
+
v(-0.35em)
|
|
108
|
+
line(length: 100%, stroke: 1.6pt + accent-color)
|
|
109
|
+
v(0.1em)
|
|
110
|
+
anchored("subtitle", text(size: base-size * 1.25, style: "italic", fill: muted, cv.subtitle))
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
#let summary = if cv.summary != "" { anchored("summary", par(inline(cv.summary))) }
|
|
114
|
+
|
|
115
|
+
// ---------------------------------------------------------------- page
|
|
116
|
+
|
|
117
|
+
#if two {
|
|
118
|
+
heading-lines
|
|
119
|
+
v(1em * density)
|
|
120
|
+
let main = {
|
|
121
|
+
summary
|
|
122
|
+
for s in cv.sections.filter(s => not s.side) { main-section(s) }
|
|
123
|
+
}
|
|
124
|
+
if side-left { grid(columns: (side-width, main-width), column-gutter: gutter, sidebar, main) } else {
|
|
125
|
+
grid(columns: (main-width, side-width), column-gutter: gutter, main, sidebar)
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
let picture = photo(base-size * 8.5)
|
|
129
|
+
let head = {
|
|
130
|
+
heading-lines
|
|
131
|
+
if cv.contacts.len() > 0 {
|
|
132
|
+
v(0.4em)
|
|
133
|
+
anchored("contacts", text(size: small, fill: muted, cv.contacts.map(contact-text).join(h(0.5em) + sym.dot.c + h(0.5em))))
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if picture == none { head } else {
|
|
137
|
+
grid(columns: (1fr, auto), column-gutter: 1.2em, align: (bottom, top), head, anchored("photo", picture))
|
|
138
|
+
}
|
|
139
|
+
v(1em * density)
|
|
140
|
+
if cv.highlight != none { anchored("highlight", par[#text(fill: accent-color, weight: "bold", cv.highlight.title) #h(0.5em) #cv.highlight.text]) }
|
|
141
|
+
summary
|
|
142
|
+
if cv.skills.len() > 0 {
|
|
143
|
+
section-title(labels.skills)
|
|
144
|
+
for (i, row) in cv.skills.enumerate() {
|
|
145
|
+
anchored("skills." + str(i), grid(
|
|
146
|
+
columns: (date-width, 1fr),
|
|
147
|
+
column-gutter: 0.8em,
|
|
148
|
+
row-gutter: 0.45em,
|
|
149
|
+
..row.map(g => (text(size: small, fill: muted, caps(lower(g.title))), g.items.join(", "))).flatten(),
|
|
150
|
+
))
|
|
151
|
+
v(0.45em)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
for s in cv.sections { main-section(s) }
|
|
155
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Llano: the plainest and the densest, flush left in a sans-serif. The name in the accent colour,
|
|
2
|
+
// the subtitle and the contacts below it; section titles in bold, in the accent colour, with no
|
|
3
|
+
// rule; each entry on one line, the position and the institution with the dates on the right. One
|
|
4
|
+
// column of plain text, no icons or pictures of text, laid out in the order it is read, so that
|
|
5
|
+
// applicant tracking systems read it as people do.
|
|
6
|
+
|
|
7
|
+
#import "/lib.typ": *
|
|
8
|
+
|
|
9
|
+
#let ink = rgb("#202020")
|
|
10
|
+
#let muted = ink.lighten(42%)
|
|
11
|
+
#let accent-color = accent(rgb("#1f5f5b"))
|
|
12
|
+
#let m = margins((top: 13, bottom: 13, left: 15, right: 15))
|
|
13
|
+
#let text-width = paper.width - m.left - m.right
|
|
14
|
+
#let small = base-size * 0.9
|
|
15
|
+
|
|
16
|
+
#set document(..document-info)
|
|
17
|
+
#set page(width: paper.width, height: paper.height, margin: m)
|
|
18
|
+
#set text(font: opt("font", "Noto Sans"), size: base-size * 0.95, fill: ink, lang: lang, hyphenate: false)
|
|
19
|
+
#set par(leading: 0.55em, spacing: 0.55em * density, justify: false)
|
|
20
|
+
#set list(marker: text(fill: muted, [–]), indent: 0pt, body-indent: 0.55em, spacing: 0.35em * density)
|
|
21
|
+
|
|
22
|
+
#geometry(main: (x: m.left, width: text-width))
|
|
23
|
+
|
|
24
|
+
#let section-title(title) = block(
|
|
25
|
+
above: 1.2em * density,
|
|
26
|
+
below: 0.45em * density,
|
|
27
|
+
sticky: true,
|
|
28
|
+
text(size: base-size * 1.1, weight: "bold", fill: accent-color, title),
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
#let entry(b, compact) = anchored(b.path, block(above: 0.7em * density, below: 0pt, {
|
|
32
|
+
text(weight: "bold", b.title)
|
|
33
|
+
if b.place != "" { [, #b.place] }
|
|
34
|
+
if b.period != "" {
|
|
35
|
+
h(1fr)
|
|
36
|
+
text(size: small, fill: muted, b.period)
|
|
37
|
+
}
|
|
38
|
+
if b.note != none {
|
|
39
|
+
linebreak()
|
|
40
|
+
text(size: small, fill: muted, b.note)
|
|
41
|
+
}
|
|
42
|
+
if not compact and b.items.len() > 0 { list(..b.items.map(item-body)) }
|
|
43
|
+
}))
|
|
44
|
+
|
|
45
|
+
#let section(s) = anchored(s.path, {
|
|
46
|
+
section-title(s.title)
|
|
47
|
+
for b in s.blocks {
|
|
48
|
+
if b.type == "entry" { entry(b, s.compact) } else if b.type == "item" {
|
|
49
|
+
anchored(b.path, block(above: 0.4em * density, below: 0pt, list(item-body(b))))
|
|
50
|
+
} else { anchored(b.path, par(inline(b.text))) }
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------- header
|
|
55
|
+
|
|
56
|
+
#let head = {
|
|
57
|
+
anchored("name", par(text(size: base-size * 2, weight: "medium", fill: accent-color, cv.name)))
|
|
58
|
+
if cv.subtitle != "" { anchored("subtitle", par(text(size: base-size * 1.1, cv.subtitle))) }
|
|
59
|
+
if cv.contacts.len() > 0 { anchored("contacts", par(text(size: small, fill: muted, cv.contacts.map(contact-text).join(" · ")))) }
|
|
60
|
+
}
|
|
61
|
+
#let picture = photo(base-size * 6.5)
|
|
62
|
+
#if picture == none { head } else { grid(columns: (1fr, auto), column-gutter: 1.2em, align: (horizon, top), head, anchored("photo", picture)) }
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------- body
|
|
65
|
+
|
|
66
|
+
#if cv.highlight != none {
|
|
67
|
+
v(0.5em * density)
|
|
68
|
+
anchored("highlight", par[#text(weight: "bold", cv.highlight.title): #cv.highlight.text])
|
|
69
|
+
}
|
|
70
|
+
#if cv.summary != "" {
|
|
71
|
+
section-title(labels.profile)
|
|
72
|
+
anchored("summary", par(inline(cv.summary)))
|
|
73
|
+
}
|
|
74
|
+
#if cv.skills.len() > 0 {
|
|
75
|
+
section-title(labels.skills)
|
|
76
|
+
for (i, row) in cv.skills.enumerate() {
|
|
77
|
+
anchored("skills." + str(i), { for g in row { par[#text(weight: "bold", g.title): #g.items.join(", ")] } })
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
#for s in cv.sections { section(s) }
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Minimal: black on white with a lot of air, hierarchy only from size and weight, the accent
|
|
2
|
+
// colour on the name and nothing else. One column; contacts on the right of the header.
|
|
3
|
+
|
|
4
|
+
#import "/lib.typ": *
|
|
5
|
+
|
|
6
|
+
#let ink = rgb("#141414")
|
|
7
|
+
#let muted = rgb("#6b6b6b")
|
|
8
|
+
#let accent-color = accent(ink)
|
|
9
|
+
#let m = margins((top: 15, bottom: 15, left: 17, right: 17))
|
|
10
|
+
#let text-width = paper.width - m.left - m.right
|
|
11
|
+
#let small = base-size * 0.88
|
|
12
|
+
|
|
13
|
+
#set document(..document-info)
|
|
14
|
+
#set page(width: paper.width, height: paper.height, margin: m)
|
|
15
|
+
#set text(font: opt("font", "Noto Sans"), size: base-size * 0.95, fill: ink, lang: lang)
|
|
16
|
+
#set par(leading: 0.65em, spacing: 0.65em * density, justify: false)
|
|
17
|
+
#set list(marker: text(fill: muted, [–]), indent: 0pt, body-indent: 0.6em, spacing: 0.45em * density)
|
|
18
|
+
#show link: set text(hyphenate: false)
|
|
19
|
+
|
|
20
|
+
#geometry(main: (x: m.left, width: text-width))
|
|
21
|
+
|
|
22
|
+
#let section-title(title) = block(above: 1.5em * density, below: 0.6em * density, sticky: true, text(
|
|
23
|
+
size: small * 0.95,
|
|
24
|
+
weight: "bold",
|
|
25
|
+
tracking: 0.14em,
|
|
26
|
+
upper(title),
|
|
27
|
+
))
|
|
28
|
+
|
|
29
|
+
#let entry(b, compact) = anchored(b.path, block(above: 1em * density, below: 0pt, {
|
|
30
|
+
grid(
|
|
31
|
+
columns: (1fr, auto),
|
|
32
|
+
column-gutter: 1em,
|
|
33
|
+
text(weight: "medium", b.title), text(size: small, fill: muted, b.period),
|
|
34
|
+
)
|
|
35
|
+
if b.place != "" { text(size: small, fill: muted, b.place) }
|
|
36
|
+
if b.note != none { par(text(size: small, fill: muted, b.note)) }
|
|
37
|
+
if not compact and b.items.len() > 0 {
|
|
38
|
+
v(0.2em)
|
|
39
|
+
list(..b.items.map(item-body))
|
|
40
|
+
}
|
|
41
|
+
}))
|
|
42
|
+
|
|
43
|
+
#let section(s) = anchored(s.path, {
|
|
44
|
+
section-title(s.title)
|
|
45
|
+
for b in s.blocks {
|
|
46
|
+
if b.type == "entry" { entry(b, s.compact) } else if b.type == "item" {
|
|
47
|
+
anchored(b.path, block(above: 0.5em * density, below: 0pt, list(item-body(b))))
|
|
48
|
+
} else { anchored(b.path, par(inline(b.text))) }
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------- page
|
|
53
|
+
|
|
54
|
+
#let picture = photo(base-size * 7.5)
|
|
55
|
+
#grid(
|
|
56
|
+
columns: (1fr, auto, auto),
|
|
57
|
+
column-gutter: 1.4em,
|
|
58
|
+
align: (bottom, bottom + right, top),
|
|
59
|
+
{
|
|
60
|
+
anchored("name", text(size: base-size * 2.6, fill: accent-color, cv.name))
|
|
61
|
+
v(0.15em)
|
|
62
|
+
anchored("subtitle", text(size: base-size * 1.2, fill: muted, cv.subtitle))
|
|
63
|
+
},
|
|
64
|
+
if cv.contacts.len() > 0 { anchored("contacts", align(right, text(size: small, fill: muted, cv.contacts.map(contact-text).join(linebreak())))) },
|
|
65
|
+
if picture != none { anchored("photo", picture) },
|
|
66
|
+
)
|
|
67
|
+
#if cv.highlight != none {
|
|
68
|
+
v(1.4em * density)
|
|
69
|
+
anchored("highlight", par[#text(weight: "bold", cv.highlight.title) #h(0.4em) #cv.highlight.text])
|
|
70
|
+
}
|
|
71
|
+
#if cv.summary != "" {
|
|
72
|
+
v(1.4em * density)
|
|
73
|
+
anchored("summary", par(justify: true, text(size: base-size * 1.02, inline(cv.summary))))
|
|
74
|
+
}
|
|
75
|
+
#if cv.skills.len() > 0 {
|
|
76
|
+
section-title(labels.skills)
|
|
77
|
+
for (i, row) in cv.skills.enumerate() {
|
|
78
|
+
anchored("skills." + str(i), grid(
|
|
79
|
+
columns: (auto, 1fr),
|
|
80
|
+
column-gutter: 1.5em,
|
|
81
|
+
row-gutter: 0.5em,
|
|
82
|
+
..row.map(g => (text(size: small, fill: muted, g.title), g.items.join(" · "))).flatten(),
|
|
83
|
+
))
|
|
84
|
+
v(0.5em)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
#for s in cv.sections { section(s) }
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// Portada: an editorial page, as in a magazine. The contacts in spaced capitals across the top,
|
|
2
|
+
// the photo beside the name set very large in capitals, the subtitle in italics; then every part
|
|
3
|
+
// on a row of its own, its title on the left in a serif and the content on the right, with a rule
|
|
4
|
+
// between rows. The skills close the page in columns. It looks as good with little as with a lot.
|
|
5
|
+
|
|
6
|
+
#import "/lib.typ": *
|
|
7
|
+
|
|
8
|
+
#let ink = rgb("#1b1b1b")
|
|
9
|
+
#let muted = ink.lighten(40%)
|
|
10
|
+
#let accent-color = accent(rgb("#1b1b1b"))
|
|
11
|
+
#let m = margins((top: 12, bottom: 14, left: 17, right: 17))
|
|
12
|
+
#let text-width = paper.width - m.left - m.right
|
|
13
|
+
#let label-width = text-width * 0.22
|
|
14
|
+
#let gutter = 7mm
|
|
15
|
+
#let small = base-size * 0.9
|
|
16
|
+
#let serif = "XCharter"
|
|
17
|
+
#let shows-picture = opt("photo", "square") != "none"
|
|
18
|
+
|
|
19
|
+
#set document(..document-info)
|
|
20
|
+
#set page(width: paper.width, height: paper.height, margin: m)
|
|
21
|
+
#set text(font: opt("font", "Noto Sans"), size: base-size, fill: ink, lang: lang)
|
|
22
|
+
#set par(leading: 0.62em, spacing: 0.62em * density, justify: false)
|
|
23
|
+
#set list(marker: text(fill: accent-color, sym.bullet), indent: 0pt, body-indent: 0.5em, spacing: 0.42em * density)
|
|
24
|
+
#show link: set text(hyphenate: false)
|
|
25
|
+
|
|
26
|
+
// Every part spans the page: its title on the left belongs to it as much as its content.
|
|
27
|
+
#geometry(main: (x: m.left, width: text-width))
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------- pieces
|
|
30
|
+
|
|
31
|
+
#let row-title(title) = par(leading: 0.3em, text(font: serif, size: base-size * 1.3, fill: ink, title))
|
|
32
|
+
#let rule = block(above: 1.1em * density, below: 1.1em * density, sticky: true, line(length: 100%, stroke: 0.6pt + accent-color))
|
|
33
|
+
#let row(title, body) = grid(columns: (label-width, 1fr), column-gutter: gutter, row-title(title), body)
|
|
34
|
+
|
|
35
|
+
#let entry(b, compact) = anchored(b.path, block(above: 1em * density, below: 0pt, {
|
|
36
|
+
text(font: serif, size: base-size * 1.2, b.title)
|
|
37
|
+
if b.place != "" { linebreak() + text(size: small, weight: "bold", b.place) }
|
|
38
|
+
if b.period != "" { linebreak() + text(size: small, fill: muted, b.period) }
|
|
39
|
+
if b.note != none { par(text(size: small, fill: muted, b.note)) }
|
|
40
|
+
if not compact and b.items.len() > 0 { block(above: 0.5em * density, list(..b.items.map(item-body))) }
|
|
41
|
+
}))
|
|
42
|
+
|
|
43
|
+
#let section(s) = anchored(s.path, row(s.title, for b in s.blocks {
|
|
44
|
+
if b.type == "entry" { entry(b, s.compact) } else if b.type == "item" {
|
|
45
|
+
anchored(b.path, block(above: 0.5em * density, below: 0pt, list(item-body(b))))
|
|
46
|
+
} else { anchored(b.path, block(above: 0.5em * density, below: 0pt, par(inline(b.text)))) }
|
|
47
|
+
}))
|
|
48
|
+
|
|
49
|
+
#let first-letter(s) = if s.len() > 0 { upper(s.clusters().first()) } else { "" }
|
|
50
|
+
#let pic-height = label-width * 1.25
|
|
51
|
+
#let picture = if not shows-picture { none } else if cv.photo != none {
|
|
52
|
+
if cv.photo-shape == "circle" {
|
|
53
|
+
box(clip: true, radius: 50%, width: label-width, height: label-width, image(cv.photo, width: label-width, height: label-width, fit: "cover"))
|
|
54
|
+
} else {
|
|
55
|
+
let r = if cv.photo-shape == "rounded" { 4pt } else { 0pt }
|
|
56
|
+
box(clip: true, radius: r, width: label-width, height: pic-height, image(cv.photo, width: label-width, height: pic-height, fit: "cover"))
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
box(width: label-width, height: pic-height, fill: accent-color.lighten(90%), align(center + horizon, text(
|
|
60
|
+
font: serif,
|
|
61
|
+
size: label-width * 0.36,
|
|
62
|
+
fill: accent-color,
|
|
63
|
+
first-letter(cv.given) + first-letter(cv.family),
|
|
64
|
+
)))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------- page
|
|
68
|
+
|
|
69
|
+
// The place goes in the heading, the rest across the top, spread out.
|
|
70
|
+
#let place-contact = cv.contacts.find(c => c.kind == "location")
|
|
71
|
+
#let top-contacts = cv.contacts.filter(c => c.kind != "location")
|
|
72
|
+
#if top-contacts.len() > 0 {
|
|
73
|
+
anchored("contacts", block(below: 1.3em * density, par(justify: false, text(size: small * 0.88, tracking: 0.07em, top-contacts
|
|
74
|
+
.map(c => box(upper(contact-text(c))))
|
|
75
|
+
.join(h(1.5em) + h(1fr))))))
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
#let head = {
|
|
79
|
+
anchored("name", par(leading: 0.3em, text(font: serif, size: base-size * 3.4, tracking: -0.01em, upper({
|
|
80
|
+
cv.given
|
|
81
|
+
linebreak()
|
|
82
|
+
cv.family
|
|
83
|
+
}))))
|
|
84
|
+
v(1em * density)
|
|
85
|
+
grid(
|
|
86
|
+
columns: (1fr, 1fr),
|
|
87
|
+
column-gutter: 1.5em,
|
|
88
|
+
align: bottom,
|
|
89
|
+
anchored("subtitle", text(font: serif, size: base-size * 1.3, style: "italic", cv.subtitle)),
|
|
90
|
+
{
|
|
91
|
+
if place-contact != none {
|
|
92
|
+
text(size: small, weight: "bold", place-contact.value)
|
|
93
|
+
if cv.highlight != none { v(0.5em) }
|
|
94
|
+
}
|
|
95
|
+
if cv.highlight != none {
|
|
96
|
+
anchored("highlight", {
|
|
97
|
+
text(size: small * 0.9, weight: "bold", tracking: 0.1em, upper(cv.highlight.title))
|
|
98
|
+
linebreak()
|
|
99
|
+
text(font: serif, size: base-size * 1.3, cv.highlight.text)
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
#if picture == none { head } else {
|
|
106
|
+
grid(columns: (label-width, 1fr), column-gutter: gutter, align: (top, bottom), anchored("photo", picture), head)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
#let first = true
|
|
110
|
+
#if cv.summary != "" {
|
|
111
|
+
v(1.6em * density)
|
|
112
|
+
anchored("summary", row(labels.profile, par(inline(cv.summary))))
|
|
113
|
+
first = false
|
|
114
|
+
}
|
|
115
|
+
#for s in cv.sections {
|
|
116
|
+
if first { v(1.6em * density) } else { rule }
|
|
117
|
+
section(s)
|
|
118
|
+
first = false
|
|
119
|
+
}
|
|
120
|
+
#for (i, groups) in cv.skills.enumerate() {
|
|
121
|
+
if first { v(1.6em * density) } else { rule }
|
|
122
|
+
first = false
|
|
123
|
+
anchored("skills." + str(i), if groups.len() == 1 {
|
|
124
|
+
row(groups.first().title, par(groups.first().items.join(h(0.4em) + text(fill: muted, sym.dot.c) + h(0.4em))))
|
|
125
|
+
} else {
|
|
126
|
+
grid(columns: (label-width, ..((1fr,) * (groups.len() - 1))), column-gutter: gutter, ..groups.map(g => {
|
|
127
|
+
row-title(g.title)
|
|
128
|
+
v(0.35em)
|
|
129
|
+
if g.items.len() > 0 { text(size: small, g.items.map(x => "· " + x).join(linebreak())) }
|
|
130
|
+
}))
|
|
131
|
+
})
|
|
132
|
+
}
|