|
201 | 201 | } |
202 | 202 |
|
203 | 203 | // Skills component with categories |
| 204 | +// mode: |
| 205 | +// - "text" : only text (default, backwards compatible) |
| 206 | +// - "bar" : quantified progress bars |
| 207 | +// - "rating" : quantified stars or dots (controlled by rating-style) |
204 | 208 | #let cv-skills( |
205 | 209 | skills: (:), |
206 | 210 | theme: (:), |
207 | 211 | compact: false, |
| 212 | + mode: "text", |
| 213 | + rating-style: "stars", // "stars" or "dots" |
| 214 | + max-level: 5, |
208 | 215 | ) = { |
| 216 | + // Helper: normalize each item to a (name, level) pair. |
| 217 | + // Supports: |
| 218 | + // - "React" -> (name: "React", level: max-level) |
| 219 | + // - (name: "React", level: 4) -> (name: "React", level: 4) |
| 220 | + // - ("React", 4) -> (name: "React", level: 4) |
| 221 | + let normalize-items = items => { |
| 222 | + items.map(item => { |
| 223 | + if type(item) == str { |
| 224 | + (name: item, level: max-level) |
| 225 | + } else if "name" in item and "level" in item { |
| 226 | + item |
| 227 | + } else if type(item) == array and item.len() == 2 { |
| 228 | + (name: item.at(0), level: item.at(1)) |
| 229 | + } else { |
| 230 | + // Fallback: try to stringify |
| 231 | + (name: str(item), level: max-level) |
| 232 | + } |
| 233 | + }) |
| 234 | + } |
| 235 | + |
| 236 | + let accent = if "colors" in theme and "accent" in theme.colors { |
| 237 | + theme.colors.accent |
| 238 | + } else if "primary" in theme.colors { |
| 239 | + theme.colors.primary |
| 240 | + } else { |
| 241 | + rgb("#1e88e5") |
| 242 | + } |
| 243 | + |
| 244 | + let rating-filled-col = accent |
| 245 | + let rating-empty-col = accent.lighten(70%) |
| 246 | + |
209 | 247 | block[ |
210 | 248 | #set par(justify: false) |
211 | 249 | #for (category, items) in skills { |
212 | | - if compact { |
213 | | - text(weight: "semibold", size: 9pt, category + ":") |
214 | | - linebreak() |
215 | | - text(size: 9pt, items.join(", ")) |
| 250 | + if mode == "text" { |
| 251 | + if compact { |
| 252 | + text(weight: "semibold", size: 9pt, category + ":") |
| 253 | + linebreak() |
| 254 | + text(size: 9pt, items.join(", ")) |
| 255 | + } else { |
| 256 | + text(weight: "semibold", category + ": ") |
| 257 | + items.join(", ") |
| 258 | + } |
216 | 259 | } else { |
217 | | - text(weight: "semibold", category + ": ") |
218 | | - items.join(", ") |
| 260 | + let normalized = normalize-items(items) |
| 261 | + |
| 262 | + if compact { |
| 263 | + text(weight: "semibold", size: 9pt, category) |
| 264 | + } else { |
| 265 | + text(weight: "semibold", category) |
| 266 | + } |
| 267 | + linebreak() |
| 268 | + |
| 269 | + for skill in normalized { |
| 270 | + grid( |
| 271 | + columns: (auto, 1fr), |
| 272 | + column-gutter: 0.4em, |
| 273 | + [ |
| 274 | + - #text(size: 9pt, skill.name) |
| 275 | + ], |
| 276 | + align(right)[ |
| 277 | + #if mode == "bar" { |
| 278 | + skill-bar(level: skill.level, max: max-level, width: 100%, color: accent) |
| 279 | + } else if mode == "rating" { |
| 280 | + rating( |
| 281 | + level: skill.level, |
| 282 | + max: max-level, |
| 283 | + style: rating-style, |
| 284 | + filled-color: rating-filled-col, |
| 285 | + empty-color: rating-empty-col, |
| 286 | + ) |
| 287 | + } |
| 288 | + ], |
| 289 | + ) |
| 290 | + v(0.1em) |
| 291 | + } |
219 | 292 | } |
| 293 | + |
220 | 294 | v(0.3em) |
221 | 295 | } |
222 | 296 | ] |
|
0 commit comments