@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,86 @@
|
|
|
1
|
+
// Recto: modern and plain, everything flush left in a sans-serif. The name in bold, the subtitle in
|
|
2
|
+
// the accent colour, the contacts on one line; section titles in small capitals of the accent colour
|
|
3
|
+
// over a hairline; each entry with the position and the dates on one line and the institution
|
|
4
|
+
// below. One column of plain text, no icons or pictures of text, laid out in the order it is read,
|
|
5
|
+
// so that applicant tracking systems read it as people do.
|
|
6
|
+
|
|
7
|
+
#import "/lib.typ": *
|
|
8
|
+
|
|
9
|
+
#let ink = rgb("#1b1f23")
|
|
10
|
+
#let muted = ink.lighten(40%)
|
|
11
|
+
#let accent-color = accent(rgb("#1f4e79"))
|
|
12
|
+
#let m = margins((top: 14, bottom: 14, left: 16, right: 16))
|
|
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.6em, spacing: 0.6em * density, justify: false)
|
|
20
|
+
#set list(marker: text(fill: accent-color, sym.bullet), indent: 0.2em, body-indent: 0.5em, spacing: 0.4em * density)
|
|
21
|
+
|
|
22
|
+
#geometry(main: (x: m.left, width: text-width))
|
|
23
|
+
|
|
24
|
+
#let section-title(title) = block(
|
|
25
|
+
above: 1.3em * density,
|
|
26
|
+
below: 0.55em * density,
|
|
27
|
+
sticky: true,
|
|
28
|
+
width: 100%,
|
|
29
|
+
inset: (bottom: 3pt),
|
|
30
|
+
stroke: (bottom: 0.5pt + ink.lighten(78%)),
|
|
31
|
+
text(size: small, weight: "bold", tracking: 0.1em, fill: accent-color, upper(title)),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
#let entry(b, compact) = anchored(b.path, block(above: 0.85em * density, below: 0pt, {
|
|
35
|
+
text(weight: "bold", b.title)
|
|
36
|
+
if b.period != "" {
|
|
37
|
+
h(1fr)
|
|
38
|
+
text(size: small, fill: muted, b.period)
|
|
39
|
+
}
|
|
40
|
+
if b.place != "" {
|
|
41
|
+
linebreak()
|
|
42
|
+
text(fill: muted, b.place)
|
|
43
|
+
}
|
|
44
|
+
if b.note != none {
|
|
45
|
+
linebreak()
|
|
46
|
+
text(size: small, fill: muted, b.note)
|
|
47
|
+
}
|
|
48
|
+
if not compact and b.items.len() > 0 { list(..b.items.map(item-body)) }
|
|
49
|
+
}))
|
|
50
|
+
|
|
51
|
+
#let section(s) = anchored(s.path, {
|
|
52
|
+
section-title(s.title)
|
|
53
|
+
for b in s.blocks {
|
|
54
|
+
if b.type == "entry" { entry(b, s.compact) } else if b.type == "item" {
|
|
55
|
+
anchored(b.path, block(above: 0.45em * density, below: 0pt, list(item-body(b))))
|
|
56
|
+
} else { anchored(b.path, par(inline(b.text))) }
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------- header
|
|
61
|
+
|
|
62
|
+
#let head = {
|
|
63
|
+
anchored("name", par(text(size: base-size * 2.2, weight: "bold", cv.name)))
|
|
64
|
+
if cv.subtitle != "" { anchored("subtitle", par(text(size: base-size * 1.15, weight: "medium", fill: accent-color, cv.subtitle))) }
|
|
65
|
+
if cv.contacts.len() > 0 { anchored("contacts", par(text(size: small, fill: muted, cv.contacts.map(contact-text).join(" | ")))) }
|
|
66
|
+
}
|
|
67
|
+
#let picture = photo(base-size * 7)
|
|
68
|
+
#if picture == none { head } else { grid(columns: (1fr, auto), column-gutter: 1.2em, align: (horizon, top), head, anchored("photo", picture)) }
|
|
69
|
+
|
|
70
|
+
// ---------------------------------------------------------------- body
|
|
71
|
+
|
|
72
|
+
#if cv.highlight != none {
|
|
73
|
+
v(0.6em * density)
|
|
74
|
+
anchored("highlight", par[#text(weight: "bold", fill: accent-color, cv.highlight.title): #cv.highlight.text])
|
|
75
|
+
}
|
|
76
|
+
#if cv.summary != "" {
|
|
77
|
+
section-title(labels.profile)
|
|
78
|
+
anchored("summary", par(inline(cv.summary)))
|
|
79
|
+
}
|
|
80
|
+
#if cv.skills.len() > 0 {
|
|
81
|
+
section-title(labels.skills)
|
|
82
|
+
for (i, row) in cv.skills.enumerate() {
|
|
83
|
+
anchored("skills." + str(i), { for g in row { par[#text(weight: "bold", g.title): #g.items.join(", ")] } })
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
#for s in cv.sections { section(s) }
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// Rótulo: light and airy, for a short CV. The photo in a circle with a ring of the accent colour
|
|
2
|
+
// (the initials, when there is no photo), the name large and light on two lines, the subtitle in
|
|
3
|
+
// spaced capitals, and every title on a label of the accent colour, softened. A fine rule
|
|
4
|
+
// separates the two columns: the contacts, the skills and the side sections go on the left.
|
|
5
|
+
|
|
6
|
+
#import "/lib.typ": *
|
|
7
|
+
|
|
8
|
+
#let ink = rgb("#25272a")
|
|
9
|
+
#let muted = ink.lighten(40%)
|
|
10
|
+
#let accent-color = accent(rgb("#a35a83"))
|
|
11
|
+
#let deep = accent-color.darken(32%)
|
|
12
|
+
#let label-fill = accent-color.lighten(84%)
|
|
13
|
+
#let m = margins((top: 16, bottom: 14, left: 15, right: 15))
|
|
14
|
+
#let text-width = paper.width - m.left - m.right
|
|
15
|
+
#let side-width = text-width * 0.34
|
|
16
|
+
#let gutter = 11mm
|
|
17
|
+
#let main-width = text-width - side-width - gutter
|
|
18
|
+
#let main-x = m.left + side-width + gutter
|
|
19
|
+
#let small = base-size * 0.9
|
|
20
|
+
#let shows-picture = opt("photo", "square") != "none"
|
|
21
|
+
#let pic = side-width * 0.72
|
|
22
|
+
|
|
23
|
+
#set document(..document-info)
|
|
24
|
+
#set page(width: paper.width, height: paper.height, margin: m)
|
|
25
|
+
#set text(font: opt("font", "Noto Sans"), size: base-size, fill: ink, lang: lang)
|
|
26
|
+
#set par(leading: 0.7em, spacing: 0.7em * density, justify: false)
|
|
27
|
+
#set list(marker: text(fill: accent-color, sym.bullet), indent: 0pt, body-indent: 0.5em, spacing: 0.5em * density)
|
|
28
|
+
#show link: set text(hyphenate: false)
|
|
29
|
+
|
|
30
|
+
#geometry(main: (x: main-x, width: main-width), side: (x: m.left, width: side-width))
|
|
31
|
+
|
|
32
|
+
/// A title on its label.
|
|
33
|
+
#let tag(title) = block(above: 1.6em * density, below: 0.8em * density, sticky: true, box(
|
|
34
|
+
fill: label-fill,
|
|
35
|
+
inset: (x: 6pt, y: 4pt),
|
|
36
|
+
radius: 1.5pt,
|
|
37
|
+
text(size: small * 0.95, weight: "bold", tracking: 0.14em, fill: deep, upper(title)),
|
|
38
|
+
))
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------- the left column
|
|
41
|
+
|
|
42
|
+
#let first-letter(s) = if s.len() > 0 { upper(s.clusters().first()) } else { "" }
|
|
43
|
+
#let picture = if not shows-picture { none } else {
|
|
44
|
+
let inner = if cv.photo != none {
|
|
45
|
+
box(clip: true, radius: 50%, width: pic, height: pic, image(cv.photo, width: pic, height: pic, fit: "cover"))
|
|
46
|
+
} else {
|
|
47
|
+
circle(radius: pic / 2, fill: label-fill, align(center + horizon, text(
|
|
48
|
+
size: pic * 0.3,
|
|
49
|
+
tracking: 0.04em,
|
|
50
|
+
fill: deep,
|
|
51
|
+
first-letter(cv.given) + first-letter(cv.family),
|
|
52
|
+
)))
|
|
53
|
+
}
|
|
54
|
+
box(width: pic + 14pt, height: pic + 14pt, {
|
|
55
|
+
place(center + horizon, circle(radius: pic / 2 + 5.5pt, stroke: 2.4pt + accent-color))
|
|
56
|
+
place(center + horizon, inner)
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
#let line-block(body) = block(above: 0.55em * density, below: 0pt, body)
|
|
61
|
+
|
|
62
|
+
#let sidebar = {
|
|
63
|
+
set text(size: small)
|
|
64
|
+
if picture != none {
|
|
65
|
+
anchored("photo", column: "side", align(center, picture))
|
|
66
|
+
v(0.4em)
|
|
67
|
+
}
|
|
68
|
+
if cv.contacts.len() > 0 {
|
|
69
|
+
anchored("contacts", column: "side", {
|
|
70
|
+
tag(labels.contact)
|
|
71
|
+
for c in cv.contacts { line-block(contact-text(c)) }
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
for (i, row) in cv.skills.enumerate() {
|
|
75
|
+
anchored("skills." + str(i), column: "side", for g in row {
|
|
76
|
+
tag(g.title)
|
|
77
|
+
for it in g.items { line-block(it) }
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
for s in cv.sections.filter(s => s.side) {
|
|
81
|
+
anchored(s.path, column: "side", {
|
|
82
|
+
tag(s.title)
|
|
83
|
+
for b in s.blocks {
|
|
84
|
+
anchored(b.path, column: "side", block(above: 0.75em * density, below: 0pt, if b.type == "entry" {
|
|
85
|
+
text(weight: "bold", b.title)
|
|
86
|
+
if b.place != "" { linebreak() + b.place }
|
|
87
|
+
if b.period != "" { linebreak() + text(fill: muted, b.period) }
|
|
88
|
+
} else { item-body(b) }))
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------- the right column
|
|
95
|
+
|
|
96
|
+
#let entry(b, compact) = anchored(b.path, block(above: 1.05em * density, below: 0pt, {
|
|
97
|
+
grid(columns: (auto, 1fr), column-gutter: 0.5em, text(fill: accent-color, sym.bullet), {
|
|
98
|
+
text(weight: "bold", b.title)
|
|
99
|
+
let meta = (b.place, b.period).filter(x => x != "")
|
|
100
|
+
if meta.len() > 0 {
|
|
101
|
+
linebreak()
|
|
102
|
+
text(size: small, fill: muted, meta.join(", "))
|
|
103
|
+
}
|
|
104
|
+
if b.note != none { par(text(size: small, fill: muted, b.note)) }
|
|
105
|
+
})
|
|
106
|
+
if not compact and b.items.len() > 0 {
|
|
107
|
+
block(above: 0.6em, below: 0.6em, line(length: 100%, stroke: 0.5pt + ink.lighten(80%)))
|
|
108
|
+
list(..b.items.map(item-body))
|
|
109
|
+
}
|
|
110
|
+
}))
|
|
111
|
+
|
|
112
|
+
#let main-section(s) = anchored(s.path, {
|
|
113
|
+
tag(s.title)
|
|
114
|
+
for b in s.blocks {
|
|
115
|
+
if b.type == "entry" { entry(b, s.compact) } else if b.type == "item" {
|
|
116
|
+
anchored(b.path, block(above: 0.55em * density, below: 0pt, list(item-body(b))))
|
|
117
|
+
} else { anchored(b.path, block(above: 0.55em * density, below: 0pt, par(inline(b.text)))) }
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
#let main = {
|
|
122
|
+
v(0.6em)
|
|
123
|
+
anchored("name", par(leading: 0.34em, text(size: base-size * 3.2, tracking: 0.01em, {
|
|
124
|
+
cv.given
|
|
125
|
+
linebreak()
|
|
126
|
+
cv.family
|
|
127
|
+
})))
|
|
128
|
+
v(0.9em)
|
|
129
|
+
anchored("subtitle", text(weight: "medium", tracking: 0.24em, upper(cv.subtitle)))
|
|
130
|
+
if cv.highlight != none {
|
|
131
|
+
v(0.35em)
|
|
132
|
+
anchored("highlight", text(size: small)[#text(weight: "bold", fill: deep, cv.highlight.title) #h(0.4em) #cv.highlight.text])
|
|
133
|
+
}
|
|
134
|
+
if cv.summary != "" {
|
|
135
|
+
v(0.9em)
|
|
136
|
+
anchored("summary", par(inline(cv.summary)))
|
|
137
|
+
}
|
|
138
|
+
for s in cv.sections.filter(s => not s.side) { main-section(s) }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
#grid(
|
|
142
|
+
columns: (side-width + gutter / 2, main-width + gutter / 2),
|
|
143
|
+
stroke: (x, y) => if x == 0 { (right: 0.6pt + ink.lighten(78%)) },
|
|
144
|
+
inset: (x, y) => if x == 0 { (right: gutter / 2) } else { (left: gutter / 2) },
|
|
145
|
+
sidebar, main,
|
|
146
|
+
)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "legajo",
|
|
4
|
+
"name": "Legajo",
|
|
5
|
+
"description": "Serif, el nombre en versalitas sobre una regla, barra lateral en una tarjeta y las fechas en su propia columna, como un registro.",
|
|
6
|
+
"options": ["columns", "sidebar", "sidebarWidth", "photo", "font", "fontSize", "accent", "margins", "density", "paper", "dateFormat", "labels"]
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"id": "banda",
|
|
10
|
+
"name": "Banda",
|
|
11
|
+
"description": "Una franja de color con el nombre y la foto; una columna en sans, títulos marcados con el color de acento.",
|
|
12
|
+
"options": ["photo", "font", "fontSize", "accent", "margins", "density", "paper", "dateFormat", "labels"]
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"id": "minimal",
|
|
16
|
+
"name": "Minimal",
|
|
17
|
+
"description": "Blanco y negro con mucho aire; la jerarquía sale del tamaño y el peso, el color solo en el nombre.",
|
|
18
|
+
"options": ["photo", "font", "fontSize", "accent", "margins", "density", "paper", "dateFormat", "labels"]
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"id": "claustro",
|
|
22
|
+
"name": "Claustro",
|
|
23
|
+
"description": "Al estilo de los CV de Harvard: serif, el nombre centrado con el contacto en una línea, títulos en mayúsculas sobre una raya y la institución arriba del cargo. Una columna, legible para ATS.",
|
|
24
|
+
"options": ["photo", "font", "fontSize", "accent", "margins", "density", "paper", "dateFormat", "labels"],
|
|
25
|
+
"preset": { "columns": 1, "photo": "none", "length": "short" }
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"id": "recto",
|
|
29
|
+
"name": "Recto",
|
|
30
|
+
"description": "Moderno y simple, en sans y alineado a la izquierda: títulos en mayúsculas del color de acento sobre una línea fina, el cargo y las fechas en un renglón. Una columna, legible para ATS.",
|
|
31
|
+
"options": ["photo", "font", "fontSize", "accent", "margins", "density", "paper", "dateFormat", "labels"],
|
|
32
|
+
"preset": { "columns": 1, "photo": "none", "length": "short" }
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"id": "llano",
|
|
36
|
+
"name": "Llano",
|
|
37
|
+
"description": "El más sobrio y compacto, en sans: títulos en negrita del color de acento y cada entrada en un renglón, con el cargo y la institución. Una columna, legible para ATS.",
|
|
38
|
+
"options": ["photo", "font", "fontSize", "accent", "margins", "density", "paper", "dateFormat", "labels"],
|
|
39
|
+
"preset": { "columns": 1, "photo": "none", "length": "short" }
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"id": "arco",
|
|
43
|
+
"name": "Arco",
|
|
44
|
+
"description": "Para un CV con pocas cosas: un panel de color a la izquierda con la foto en arco (o las iniciales), el nombre grande en serif, el subtítulo y el resumen en una tarjeta suave y títulos con una línea fina. Dos columnas.",
|
|
45
|
+
"options": ["photo", "font", "fontSize", "accent", "margins", "density", "paper", "dateFormat", "labels"]
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"id": "rotulo",
|
|
49
|
+
"name": "Rótulo",
|
|
50
|
+
"description": "Liviano y con aire: la foto en un círculo con aro de color (o las iniciales), el nombre grande en dos renglones, el subtítulo en mayúsculas espaciadas y cada título sobre un rótulo de color. Dos columnas separadas por una línea fina.",
|
|
51
|
+
"options": ["photo", "font", "fontSize", "accent", "margins", "density", "paper", "dateFormat", "labels"]
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"id": "portada",
|
|
55
|
+
"name": "Portada",
|
|
56
|
+
"description": "Editorial, como una revista: el contacto en mayúsculas arriba, la foto junto al nombre muy grande, y cada parte en su fila, con el título a la izquierda en serif y una raya entre filas. Las habilidades cierran la hoja en columnas.",
|
|
57
|
+
"options": ["photo", "font", "fontSize", "accent", "margins", "density", "paper", "dateFormat", "labels"]
|
|
58
|
+
}
|
|
59
|
+
]
|
package/package.json
CHANGED
|
@@ -1,6 +1,46 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bafif/foja",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Build CVs with Typst from a Foja project: PDF, SVG, Typst, LaTeX, Markdown, JSON and Word",
|
|
5
|
+
"license": "GPL-3.0-only",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"foja": "dist/foja.mjs"
|
|
9
|
+
},
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=24"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@myriaddreamin/typst-ts-node-compiler": "0.7.0"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/bafif/foja.git",
|
|
19
|
+
"directory": "packages/cli"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://github.com/bafif/foja",
|
|
22
|
+
"keywords": [
|
|
23
|
+
"cv",
|
|
24
|
+
"resume",
|
|
25
|
+
"typst",
|
|
26
|
+
"pdf",
|
|
27
|
+
"latex",
|
|
28
|
+
"docx"
|
|
29
|
+
],
|
|
30
|
+
"files": [
|
|
31
|
+
"dist",
|
|
32
|
+
"README.md",
|
|
33
|
+
"LICENSE"
|
|
34
|
+
],
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "node build.mjs",
|
|
40
|
+
"prepack": "node build.mjs"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@foja/core": "*",
|
|
44
|
+
"@foja/template": "*"
|
|
45
|
+
}
|
|
46
|
+
}
|