jsonresume-theme-data-driven 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/README.md ADDED
@@ -0,0 +1,48 @@
1
+ # Data-Driven JSON Resume Theme
2
+
3
+ An analytical and metrics-focused theme for JSON Resume, designed to highlight quantifiable achievements and data-driven results.
4
+
5
+ ## Design Principles
6
+
7
+ **Vibe**: Analytical and factual
8
+ **Layout**: Single column with strong headings and bullet hierarchy
9
+ **Typography**: Geometric sans-serif; compact
10
+ **Accent Color**: Sky blue (#0ea5e9)
11
+ **Focus**: Metrics and measurable outcomes
12
+
13
+ ## Key Features
14
+
15
+ - **Metrics Emphasis**: Automatically bolds numbers, percentages, and dollar amounts in highlights and descriptions
16
+ - **Strong Visual Hierarchy**: Uppercase section titles, clear heading structure
17
+ - **Compact Layout**: Space-efficient design maximizing content density
18
+ - **Geometric Typography**: Clean, modern sans-serif fonts (DM Sans, Inter)
19
+ - **Bullet Hierarchy**: Clear visual structure drawing attention to key results
20
+ - **Data-First Approach**: Every section feels quantified and analytical
21
+
22
+ ## Perfect For
23
+
24
+ - Data analysts and scientists
25
+ - Product managers tracking metrics
26
+ - Engineers highlighting performance improvements
27
+ - Anyone with quantifiable achievements
28
+ - Roles requiring analytical thinking
29
+
30
+ ## Usage
31
+
32
+ ```javascript
33
+ import { render } from 'jsonresume-theme-data-driven';
34
+
35
+ const html = render(resumeData);
36
+ ```
37
+
38
+ ## Styling
39
+
40
+ The theme uses sky blue (#0ea5e9) as the primary accent color, creating a professional, analytical appearance. Numbers and metrics are automatically emphasized with bold formatting to draw the reader's eye to measurable outcomes.
41
+
42
+ ## Built With
43
+
44
+ This theme is built using [@resume/core](../../core) primitives, ensuring consistency with the JSON Resume ecosystem.
45
+
46
+ ## License
47
+
48
+ MIT
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "jsonresume-theme-data-driven",
3
+ "version": "0.1.0",
4
+ "description": "Data-driven analytical theme with sky blue accent - built with @resume/core",
5
+ "type": "module",
6
+ "main": "./src/index.jsx",
7
+ "exports": {
8
+ ".": "./src/index.jsx"
9
+ },
10
+ "keywords": [
11
+ "jsonresume",
12
+ "theme",
13
+ "resume",
14
+ "cv",
15
+ "data-driven",
16
+ "analytical",
17
+ "metrics",
18
+ "compact"
19
+ ],
20
+ "dependencies": {
21
+ "react": "^19.2.0",
22
+ "react-dom": "^19.2.0",
23
+ "styled-components": "^6.1.19",
24
+ "@resume/core": "0.1.0"
25
+ },
26
+ "devDependencies": {
27
+ "vitest": "^1.6.0"
28
+ },
29
+ "peerDependencies": {
30
+ "styled-components": "^6"
31
+ },
32
+ "scripts": {
33
+ "test": "vitest run",
34
+ "test:watch": "vitest"
35
+ }
36
+ }
package/src/index.jsx ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * JSON Resume Data-Driven Theme
3
+ * Analytical design with metrics-focused layout and sky blue accent
4
+ * Built with @resume/core primitives and React SSR
5
+ */
6
+
7
+ import { renderToString } from 'react-dom/server';
8
+ import { ServerStyleSheet } from 'styled-components';
9
+ import Resume from './ui/Resume.jsx';
10
+
11
+ export const render = (resume) => {
12
+ const sheet = new ServerStyleSheet();
13
+ try {
14
+ const html = renderToString(
15
+ sheet.collectStyles(<Resume resume={resume} />)
16
+ );
17
+ const styles = sheet.getStyleTags();
18
+ return `<!DOCTYPE html>
19
+ <html lang="en">
20
+ <head>
21
+ <meta charset="UTF-8">
22
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
23
+ <title>${resume.basics?.name || 'Resume'}</title>
24
+ ${styles}
25
+ </head>
26
+ <body>
27
+ ${html}
28
+ </body>
29
+ </html>`;
30
+ } finally {
31
+ sheet.seal();
32
+ }
33
+ };
34
+
35
+ export { Resume };
36
+ export default { render };
@@ -0,0 +1,562 @@
1
+ import styled, { createGlobalStyle } from 'styled-components';
2
+ import { formatDateRange } from '@resume/core';
3
+
4
+ const GlobalStyle = createGlobalStyle`
5
+ * {
6
+ margin: 0;
7
+ padding: 0;
8
+ box-sizing: border-box;
9
+ }
10
+
11
+ :root {
12
+ --resume-color-primary: #0f172a;
13
+ --resume-color-secondary: #475569;
14
+ --resume-color-accent: #0ea5e9;
15
+ --resume-color-accent-dark: #0284c7;
16
+ --resume-color-border: #e2e8f0;
17
+ --resume-color-bg: #ffffff;
18
+ --resume-font-geometric: 'DM Sans', 'Inter', 'Helvetica Neue', Arial, sans-serif;
19
+ }
20
+
21
+ body {
22
+ font-family: var(--resume-font-geometric);
23
+ font-size: 11px;
24
+ line-height: 1.5;
25
+ color: var(--resume-color-primary);
26
+ background: var(--resume-color-bg);
27
+ padding: 0;
28
+ }
29
+
30
+ @media print {
31
+ body {
32
+ padding: 0;
33
+ }
34
+ }
35
+ `;
36
+
37
+ const Container = styled.div`
38
+ max-width: 750px;
39
+ margin: 0 auto;
40
+ padding: 32px 40px;
41
+
42
+ @media print {
43
+ padding: 16px;
44
+ }
45
+
46
+ @media (max-width: 600px) {
47
+ padding: 20px;
48
+ }
49
+ `;
50
+
51
+ const Header = styled.header`
52
+ margin-bottom: 28px;
53
+ padding-bottom: 16px;
54
+ border-bottom: 2px solid var(--resume-color-accent);
55
+ `;
56
+
57
+ const Name = styled.h1`
58
+ font-size: 28px;
59
+ font-weight: 700;
60
+ color: var(--resume-color-primary);
61
+ margin-bottom: 4px;
62
+ letter-spacing: -0.03em;
63
+ text-transform: uppercase;
64
+ `;
65
+
66
+ const Label = styled.div`
67
+ font-size: 13px;
68
+ color: var(--resume-color-accent);
69
+ font-weight: 600;
70
+ margin-bottom: 10px;
71
+ letter-spacing: 0.02em;
72
+ `;
73
+
74
+ const Contact = styled.div`
75
+ display: flex;
76
+ flex-wrap: wrap;
77
+ gap: 12px;
78
+ font-size: 10px;
79
+ color: var(--resume-color-secondary);
80
+ font-weight: 500;
81
+
82
+ a {
83
+ color: var(--resume-color-accent);
84
+ text-decoration: none;
85
+ }
86
+ `;
87
+
88
+ const Summary = styled.p`
89
+ margin-top: 12px;
90
+ font-size: 11px;
91
+ line-height: 1.6;
92
+ color: var(--resume-color-secondary);
93
+ `;
94
+
95
+ const Section = styled.section`
96
+ margin-bottom: 24px;
97
+
98
+ @media print {
99
+ page-break-inside: avoid;
100
+ }
101
+ `;
102
+
103
+ const SectionTitle = styled.h2`
104
+ font-size: 14px;
105
+ font-weight: 700;
106
+ color: var(--resume-color-primary);
107
+ margin-bottom: 12px;
108
+ text-transform: uppercase;
109
+ letter-spacing: 0.05em;
110
+ border-bottom: 1px solid var(--resume-color-border);
111
+ padding-bottom: 4px;
112
+ `;
113
+
114
+ const Item = styled.div`
115
+ margin-bottom: 16px;
116
+ padding-left: 0;
117
+
118
+ @media print {
119
+ page-break-inside: avoid;
120
+ }
121
+ `;
122
+
123
+ const ItemHeader = styled.div`
124
+ display: flex;
125
+ justify-content: space-between;
126
+ align-items: baseline;
127
+ margin-bottom: 3px;
128
+
129
+ @media (max-width: 600px) {
130
+ flex-direction: column;
131
+ align-items: flex-start;
132
+ }
133
+ `;
134
+
135
+ const ItemTitle = styled.div`
136
+ font-size: 12px;
137
+ font-weight: 700;
138
+ color: var(--resume-color-primary);
139
+ `;
140
+
141
+ const ItemSubtitle = styled.div`
142
+ font-size: 11px;
143
+ color: var(--resume-color-secondary);
144
+ margin-bottom: 2px;
145
+ font-weight: 600;
146
+ `;
147
+
148
+ const ItemMeta = styled.div`
149
+ display: flex;
150
+ gap: 12px;
151
+ font-size: 10px;
152
+ color: var(--resume-color-secondary);
153
+ margin-bottom: 6px;
154
+ font-weight: 500;
155
+ `;
156
+
157
+ const Description = styled.p`
158
+ margin-bottom: 6px;
159
+ color: var(--resume-color-secondary);
160
+ line-height: 1.5;
161
+ font-size: 10px;
162
+ `;
163
+
164
+ const Highlights = styled.ul`
165
+ list-style: none;
166
+ margin-top: 4px;
167
+
168
+ li {
169
+ position: relative;
170
+ padding-left: 12px;
171
+ margin-bottom: 3px;
172
+ color: var(--resume-color-secondary);
173
+ font-size: 10px;
174
+ line-height: 1.5;
175
+
176
+ &::before {
177
+ content: '▪';
178
+ position: absolute;
179
+ left: 0;
180
+ color: var(--resume-color-accent);
181
+ font-weight: 700;
182
+ }
183
+
184
+ strong,
185
+ b {
186
+ color: var(--resume-color-primary);
187
+ font-weight: 700;
188
+ }
189
+ }
190
+ `;
191
+
192
+ const SkillGroup = styled.div`
193
+ margin-bottom: 10px;
194
+
195
+ strong {
196
+ display: block;
197
+ margin-bottom: 6px;
198
+ color: var(--resume-color-primary);
199
+ font-size: 11px;
200
+ font-weight: 700;
201
+ text-transform: uppercase;
202
+ letter-spacing: 0.02em;
203
+ }
204
+ `;
205
+
206
+ const BadgeList = styled.div`
207
+ display: flex;
208
+ flex-wrap: wrap;
209
+ gap: 6px;
210
+ margin-top: 8px;
211
+ `;
212
+
213
+ const Badge = styled.span`
214
+ display: inline-block;
215
+ padding: 3px 10px;
216
+ border-radius: 3px;
217
+ font-size: 9px;
218
+ font-weight: 600;
219
+ background: transparent;
220
+ color: var(--resume-color-accent);
221
+ border: 1px solid var(--resume-color-accent);
222
+ text-transform: uppercase;
223
+ letter-spacing: 0.03em;
224
+ `;
225
+
226
+ // Bold numbers and percentages for data-driven emphasis
227
+ function boldNumbers(text) {
228
+ if (!text) return text;
229
+ // Match: $1.5M, 42%, 1,200+, 99.9%, etc.
230
+ // Unified regex to avoid nested <strong> tags
231
+ return text.replace(
232
+ /(\$)?(\d+(?:,\d{3})*(?:\.\d+)?)([KkMmBb])?([+\-])?(%)?/g,
233
+ (match, dollar, number, suffix, plusminus, percent) => {
234
+ // Only bold if it has special markers (dollar, suffix, plusminus, percent, comma, or decimal)
235
+ const hasMarker =
236
+ dollar ||
237
+ suffix ||
238
+ plusminus ||
239
+ percent ||
240
+ number.includes(',') ||
241
+ number.includes('.');
242
+ if (!hasMarker) return match;
243
+
244
+ const parts = [dollar, number, suffix, plusminus, percent]
245
+ .filter(Boolean)
246
+ .join('');
247
+ return `<strong>${parts}</strong>`;
248
+ }
249
+ );
250
+ }
251
+
252
+ function Resume({ resume }) {
253
+ const {
254
+ basics = {},
255
+ work = [],
256
+ education = [],
257
+ skills = [],
258
+ projects = [],
259
+ volunteer = [],
260
+ awards = [],
261
+ } = resume;
262
+
263
+ return (
264
+ <>
265
+ <GlobalStyle />
266
+ <Container>
267
+ {/* Hero */}
268
+ <Header>
269
+ {basics.name && <Name>{basics.name}</Name>}
270
+ {basics.label && <Label>{basics.label}</Label>}
271
+ {(basics.email ||
272
+ basics.phone ||
273
+ basics.url ||
274
+ basics.location ||
275
+ basics.profiles?.length > 0) && (
276
+ <Contact>
277
+ {basics.email && (
278
+ <a href={`mailto:${basics.email}`}>{basics.email}</a>
279
+ )}
280
+ {basics.phone && <span>{basics.phone}</span>}
281
+ {basics.url && (
282
+ <a href={basics.url} target="_blank" rel="noopener noreferrer">
283
+ {basics.url.replace(/^https?:\/\//, '')}
284
+ </a>
285
+ )}
286
+ {basics.location && (
287
+ <span>
288
+ {[basics.location.city, basics.location.region]
289
+ .filter(Boolean)
290
+ .join(', ')}
291
+ </span>
292
+ )}
293
+ {basics.profiles?.map((profile, i) => (
294
+ <a
295
+ key={i}
296
+ href={profile.url}
297
+ target="_blank"
298
+ rel="noopener noreferrer"
299
+ >
300
+ {profile.network}
301
+ </a>
302
+ ))}
303
+ </Contact>
304
+ )}
305
+ {basics.summary && (
306
+ <Summary
307
+ dangerouslySetInnerHTML={{ __html: boldNumbers(basics.summary) }}
308
+ />
309
+ )}
310
+ </Header>
311
+
312
+ {/* Work Experience */}
313
+ {work.length > 0 && (
314
+ <Section>
315
+ <SectionTitle>Professional Experience</SectionTitle>
316
+ {work.map((job, i) => (
317
+ <Item key={i}>
318
+ <ItemHeader>
319
+ <ItemTitle>{job.position}</ItemTitle>
320
+ </ItemHeader>
321
+ {job.name && <ItemSubtitle>{job.name}</ItemSubtitle>}
322
+ {(job.startDate || job.location) && (
323
+ <ItemMeta>
324
+ {job.startDate && (
325
+ <span>
326
+ {formatDateRange({
327
+ startDate: job.startDate,
328
+ endDate: job.endDate,
329
+ })}
330
+ </span>
331
+ )}
332
+ {job.location && <span>{job.location}</span>}
333
+ </ItemMeta>
334
+ )}
335
+ {job.summary && (
336
+ <Description
337
+ dangerouslySetInnerHTML={{
338
+ __html: boldNumbers(job.summary),
339
+ }}
340
+ />
341
+ )}
342
+ {job.highlights?.length > 0 && (
343
+ <Highlights>
344
+ {job.highlights.map((highlight, j) => (
345
+ <li
346
+ key={j}
347
+ dangerouslySetInnerHTML={{
348
+ __html: boldNumbers(highlight),
349
+ }}
350
+ />
351
+ ))}
352
+ </Highlights>
353
+ )}
354
+ </Item>
355
+ ))}
356
+ </Section>
357
+ )}
358
+
359
+ {/* Education */}
360
+ {education.length > 0 && (
361
+ <Section>
362
+ <SectionTitle>Education</SectionTitle>
363
+ {education.map((edu, i) => {
364
+ const title = [edu.studyType, edu.area]
365
+ .filter(Boolean)
366
+ .join(' in ');
367
+ const highlights = [];
368
+ if (edu.score)
369
+ highlights.push(`GPA: <strong>${edu.score}</strong>`);
370
+ if (edu.courses?.length > 0)
371
+ highlights.push(`Courses: ${edu.courses.join(', ')}`);
372
+
373
+ return (
374
+ <Item key={i}>
375
+ <ItemHeader>
376
+ <ItemTitle>{title || edu.institution}</ItemTitle>
377
+ </ItemHeader>
378
+ {title && <ItemSubtitle>{edu.institution}</ItemSubtitle>}
379
+ {edu.startDate && (
380
+ <ItemMeta>
381
+ <span>
382
+ {formatDateRange({
383
+ startDate: edu.startDate,
384
+ endDate: edu.endDate,
385
+ })}
386
+ </span>
387
+ </ItemMeta>
388
+ )}
389
+ {highlights.length > 0 && (
390
+ <Highlights>
391
+ {highlights.map((highlight, j) => (
392
+ <li
393
+ key={j}
394
+ dangerouslySetInnerHTML={{ __html: highlight }}
395
+ />
396
+ ))}
397
+ </Highlights>
398
+ )}
399
+ </Item>
400
+ );
401
+ })}
402
+ </Section>
403
+ )}
404
+
405
+ {/* Skills */}
406
+ {skills.length > 0 && (
407
+ <Section>
408
+ <SectionTitle>Technical Skills</SectionTitle>
409
+ {skills.map((skillGroup, i) => (
410
+ <SkillGroup key={i}>
411
+ {skillGroup.name && <strong>{skillGroup.name}</strong>}
412
+ {skillGroup.keywords?.length > 0 && (
413
+ <BadgeList>
414
+ {skillGroup.keywords.map((keyword, j) => (
415
+ <Badge key={j}>{keyword}</Badge>
416
+ ))}
417
+ </BadgeList>
418
+ )}
419
+ </SkillGroup>
420
+ ))}
421
+ </Section>
422
+ )}
423
+
424
+ {/* Projects */}
425
+ {projects.length > 0 && (
426
+ <Section>
427
+ <SectionTitle>Key Projects</SectionTitle>
428
+ {projects.map((project, i) => (
429
+ <Item key={i}>
430
+ <ItemHeader>
431
+ <ItemTitle>{project.name}</ItemTitle>
432
+ </ItemHeader>
433
+ {project.startDate && (
434
+ <ItemMeta>
435
+ <span>
436
+ {formatDateRange({
437
+ startDate: project.startDate,
438
+ endDate: project.endDate,
439
+ })}
440
+ </span>
441
+ </ItemMeta>
442
+ )}
443
+ {project.description && (
444
+ <Description
445
+ dangerouslySetInnerHTML={{
446
+ __html: boldNumbers(project.description),
447
+ }}
448
+ />
449
+ )}
450
+ {project.url && (
451
+ <Description>
452
+ <a
453
+ href={project.url}
454
+ target="_blank"
455
+ rel="noopener noreferrer"
456
+ >
457
+ {project.url}
458
+ </a>
459
+ </Description>
460
+ )}
461
+ {project.keywords?.length > 0 && (
462
+ <BadgeList>
463
+ {project.keywords.map((keyword, j) => (
464
+ <Badge key={j}>{keyword}</Badge>
465
+ ))}
466
+ </BadgeList>
467
+ )}
468
+ {project.highlights?.length > 0 && (
469
+ <Highlights>
470
+ {project.highlights.map((highlight, j) => (
471
+ <li
472
+ key={j}
473
+ dangerouslySetInnerHTML={{
474
+ __html: boldNumbers(highlight),
475
+ }}
476
+ />
477
+ ))}
478
+ </Highlights>
479
+ )}
480
+ </Item>
481
+ ))}
482
+ </Section>
483
+ )}
484
+
485
+ {/* Volunteer */}
486
+ {volunteer.length > 0 && (
487
+ <Section>
488
+ <SectionTitle>Volunteer Work</SectionTitle>
489
+ {volunteer.map((vol, i) => (
490
+ <Item key={i}>
491
+ <ItemHeader>
492
+ <ItemTitle>{vol.position}</ItemTitle>
493
+ </ItemHeader>
494
+ {vol.organization && (
495
+ <ItemSubtitle>{vol.organization}</ItemSubtitle>
496
+ )}
497
+ {vol.startDate && (
498
+ <ItemMeta>
499
+ <span>
500
+ {formatDateRange({
501
+ startDate: vol.startDate,
502
+ endDate: vol.endDate,
503
+ })}
504
+ </span>
505
+ </ItemMeta>
506
+ )}
507
+ {vol.summary && (
508
+ <Description
509
+ dangerouslySetInnerHTML={{
510
+ __html: boldNumbers(vol.summary),
511
+ }}
512
+ />
513
+ )}
514
+ {vol.highlights?.length > 0 && (
515
+ <Highlights>
516
+ {vol.highlights.map((highlight, j) => (
517
+ <li
518
+ key={j}
519
+ dangerouslySetInnerHTML={{
520
+ __html: boldNumbers(highlight),
521
+ }}
522
+ />
523
+ ))}
524
+ </Highlights>
525
+ )}
526
+ </Item>
527
+ ))}
528
+ </Section>
529
+ )}
530
+
531
+ {/* Awards */}
532
+ {awards.length > 0 && (
533
+ <Section>
534
+ <SectionTitle>Awards & Recognition</SectionTitle>
535
+ {awards.map((award, i) => (
536
+ <Item key={i}>
537
+ <ItemHeader>
538
+ <ItemTitle>{award.title}</ItemTitle>
539
+ </ItemHeader>
540
+ {award.awarder && <ItemSubtitle>{award.awarder}</ItemSubtitle>}
541
+ {award.date && (
542
+ <ItemMeta>
543
+ <span>{award.date}</span>
544
+ </ItemMeta>
545
+ )}
546
+ {award.summary && (
547
+ <Description
548
+ dangerouslySetInnerHTML={{
549
+ __html: boldNumbers(award.summary),
550
+ }}
551
+ />
552
+ )}
553
+ </Item>
554
+ ))}
555
+ </Section>
556
+ )}
557
+ </Container>
558
+ </>
559
+ );
560
+ }
561
+
562
+ export default Resume;
@@ -0,0 +1,178 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { render } from '../src/index.jsx';
3
+
4
+ const completeResume = {
5
+ basics: {
6
+ name: 'Sarah Martinez',
7
+ label: 'Data Analytics Manager',
8
+ email: 'sarah.martinez@example.com',
9
+ phone: '+1 (555) 987-6543',
10
+ url: 'https://sarahmartinez.io',
11
+ summary:
12
+ 'Analytics leader with 10+ years driving data-driven decision making. Delivered $5M in cost savings through optimization initiatives.',
13
+ location: {
14
+ city: 'Austin',
15
+ region: 'TX',
16
+ },
17
+ profiles: [
18
+ { network: 'LinkedIn', url: 'https://linkedin.com/in/sarahmartinez' },
19
+ { network: 'GitHub', url: 'https://github.com/smartinez' },
20
+ ],
21
+ },
22
+ work: [
23
+ {
24
+ name: 'DataCorp Analytics',
25
+ position: 'Senior Analytics Manager',
26
+ location: 'Austin, TX',
27
+ startDate: '2020-06-01',
28
+ endDate: null,
29
+ summary: 'Leading team of 8 analysts across 3 business units',
30
+ highlights: [
31
+ 'Increased revenue forecasting accuracy by 42% through ML model implementation',
32
+ 'Reduced operational costs by $2.3M annually via process optimization',
33
+ 'Built real-time dashboard serving 500+ daily users with 99.9% uptime',
34
+ ],
35
+ },
36
+ ],
37
+ education: [
38
+ {
39
+ institution: 'University of Texas at Austin',
40
+ area: 'Statistics',
41
+ studyType: 'Master of Science',
42
+ startDate: '2014-09-01',
43
+ endDate: '2016-05-31',
44
+ score: '3.9',
45
+ courses: ['Machine Learning', 'Statistical Modeling', 'Data Mining'],
46
+ },
47
+ ],
48
+ skills: [
49
+ {
50
+ name: 'Analytics',
51
+ keywords: ['Python', 'R', 'SQL', 'Tableau', 'Power BI'],
52
+ },
53
+ ],
54
+ projects: [
55
+ {
56
+ name: 'Predictive Analytics Platform',
57
+ description:
58
+ 'Built ML-powered forecasting system processing 50GB daily data',
59
+ highlights: [
60
+ 'Achieved 87% prediction accuracy (15% improvement over baseline)',
61
+ 'Deployed to 1,200+ users across 5 business units',
62
+ ],
63
+ keywords: ['Python', 'TensorFlow', 'PostgreSQL', 'Docker'],
64
+ startDate: '2021-01-01',
65
+ endDate: '2022-12-31',
66
+ url: 'https://github.com/smartinez/predictive-analytics',
67
+ },
68
+ ],
69
+ };
70
+
71
+ describe('Data-Driven Theme', () => {
72
+ it('renders complete HTML document', () => {
73
+ const html = render(completeResume);
74
+
75
+ expect(html).toContain('<!DOCTYPE html>');
76
+ expect(html).toContain('<html lang="en">');
77
+ expect(html).toContain('</html>');
78
+ });
79
+
80
+ it('applies data-driven sky blue accent', () => {
81
+ const html = render(completeResume);
82
+
83
+ // Sky blue accent color in CSS
84
+ expect(html).toContain('#0ea5e9');
85
+ });
86
+
87
+ it('renders basics section', () => {
88
+ const html = render(completeResume);
89
+
90
+ expect(html).toContain('Sarah Martinez');
91
+ expect(html).toContain('Data Analytics Manager');
92
+ expect(html).toContain('sarah.martinez@example.com');
93
+ });
94
+
95
+ it('renders work experience', () => {
96
+ const html = render(completeResume);
97
+
98
+ expect(html).toContain('Professional Experience');
99
+ expect(html).toContain('DataCorp Analytics');
100
+ expect(html).toContain('Senior Analytics Manager');
101
+ });
102
+
103
+ it('bolds numbers in summary', () => {
104
+ const html = render(completeResume);
105
+
106
+ // Numbers in summary should be wrapped in <strong> tags
107
+ expect(html).toContain('<strong>10+</strong>');
108
+ expect(html).toContain('<strong>$5M</strong>');
109
+ });
110
+
111
+ it('bolds numbers in work highlights', () => {
112
+ const html = render(completeResume);
113
+
114
+ // Metrics should be emphasized
115
+ expect(html).toContain('<strong>42%</strong>');
116
+ expect(html).toContain('<strong>$2.3M</strong>');
117
+ expect(html).toContain('<strong>99.9%</strong>');
118
+ });
119
+
120
+ it('renders education section', () => {
121
+ const html = render(completeResume);
122
+
123
+ expect(html).toContain('Education');
124
+ expect(html).toContain('University of Texas at Austin');
125
+ expect(html).toContain('Statistics');
126
+ });
127
+
128
+ it('renders skills section', () => {
129
+ const html = render(completeResume);
130
+
131
+ expect(html).toContain('Technical Skills');
132
+ expect(html).toContain('Python');
133
+ expect(html).toContain('SQL');
134
+ });
135
+
136
+ it('renders projects section', () => {
137
+ const html = render(completeResume);
138
+
139
+ expect(html).toContain('Key Projects');
140
+ expect(html).toContain('Predictive Analytics Platform');
141
+ });
142
+
143
+ it('bolds numbers in project highlights', () => {
144
+ const html = render(completeResume);
145
+
146
+ expect(html).toContain('<strong>87%</strong>');
147
+ expect(html).toContain('<strong>1,200+</strong>');
148
+ });
149
+
150
+ it('includes styled-components CSS', () => {
151
+ const html = render(completeResume);
152
+
153
+ // Styled-components generates style tags
154
+ expect(html).toContain('<style');
155
+ expect(html).toContain('data-styled');
156
+ });
157
+
158
+ it('uses geometric sans-serif font', () => {
159
+ const html = render(completeResume);
160
+
161
+ expect(html).toContain('DM Sans');
162
+ });
163
+
164
+ it('handles minimal data gracefully', () => {
165
+ const minimalResume = {
166
+ basics: {
167
+ name: 'John Doe',
168
+ email: 'john@example.com',
169
+ },
170
+ };
171
+
172
+ const html = render(minimalResume);
173
+
174
+ expect(html).toContain('John Doe');
175
+ expect(html).toContain('john@example.com');
176
+ expect(html).toContain('</html>');
177
+ });
178
+ });
@@ -0,0 +1,11 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ esbuild: {
5
+ jsx: 'automatic',
6
+ },
7
+ test: {
8
+ environment: 'node',
9
+ globals: true,
10
+ },
11
+ });