@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.
@@ -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
+ }