@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,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.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
+ }