jsonresume-theme-nordic-minimal 1.0.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/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "jsonresume-theme-nordic-minimal",
3
+ "version": "1.0.0",
4
+ "description": "Scandinavian restraint, white space worship, quietly confident two-column theme",
5
+ "main": "src/index.js",
6
+ "keywords": [
7
+ "json-resume",
8
+ "theme",
9
+ "nordic",
10
+ "minimal",
11
+ "scandinavian",
12
+ "two-column"
13
+ ],
14
+ "author": "JSON Resume Team",
15
+ "license": "MIT",
16
+ "dependencies": {
17
+ "react": "^19.2.0",
18
+ "react-dom": "^19.2.0",
19
+ "styled-components": "^6.1.19",
20
+ "@resume/core": "0.1.0"
21
+ },
22
+ "peerDependencies": {
23
+ "react": "^18.0.0 || ^19.0.0",
24
+ "react-dom": "^18.0.0 || ^19.0.0"
25
+ },
26
+ "scripts": {
27
+ "test": "echo \"No tests specified\""
28
+ }
29
+ }
package/src/Resume.jsx ADDED
@@ -0,0 +1,524 @@
1
+ import React from 'react';
2
+ import styled from 'styled-components';
3
+ import {
4
+ Section,
5
+ SectionTitle,
6
+ ListItem,
7
+ DateRange,
8
+ ContactInfo,
9
+ Link,
10
+ safeUrl,
11
+ isExternalUrl,
12
+ } from '@resume/core';
13
+
14
+ const ResumeContainer = styled.div`
15
+ max-width: 1200px;
16
+ margin: 0 auto;
17
+ padding: 80px 60px;
18
+ background: #ffffff;
19
+ font-family: 'Lato', 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
20
+ color: #2c3e50;
21
+ line-height: 1.8;
22
+
23
+ @media print {
24
+ padding: 40px 30px;
25
+ max-width: 100%;
26
+ }
27
+ `;
28
+
29
+ const Header = styled.header`
30
+ margin-bottom: 60px;
31
+ border-bottom: 1px solid #ecf0f1;
32
+ padding-bottom: 40px;
33
+ `;
34
+
35
+ const Name = styled.h1`
36
+ font-size: 3rem;
37
+ font-weight: 300;
38
+ color: #2c3e50;
39
+ margin: 0 0 8px 0;
40
+ letter-spacing: -0.5px;
41
+ `;
42
+
43
+ const Title = styled.p`
44
+ font-size: 1.25rem;
45
+ font-weight: 300;
46
+ color: #7f8c8d;
47
+ margin: 0 0 20px 0;
48
+ `;
49
+
50
+ const Summary = styled.p`
51
+ font-size: 1rem;
52
+ line-height: 1.9;
53
+ color: #34495e;
54
+ margin: 20px 0 0 0;
55
+ font-weight: 300;
56
+ `;
57
+
58
+ const TwoColumnLayout = styled.div`
59
+ display: grid;
60
+ grid-template-columns: 320px 1fr;
61
+ gap: 60px;
62
+
63
+ @media print {
64
+ gap: 40px;
65
+ }
66
+ `;
67
+
68
+ const Sidebar = styled.aside`
69
+ padding-right: 20px;
70
+ border-right: 1px solid #ecf0f1;
71
+ `;
72
+
73
+ const MainContent = styled.main`
74
+ min-width: 0;
75
+ `;
76
+
77
+ const StyledSection = styled(Section)`
78
+ margin-bottom: 50px;
79
+
80
+ &:last-child {
81
+ margin-bottom: 0;
82
+ }
83
+ `;
84
+
85
+ const StyledSectionTitle = styled(SectionTitle)`
86
+ font-size: 0.875rem;
87
+ font-weight: 600;
88
+ text-transform: uppercase;
89
+ letter-spacing: 1.5px;
90
+ color: #4682b4;
91
+ margin: 0 0 24px 0;
92
+ padding-bottom: 8px;
93
+ border-bottom: 2px solid #4682b4;
94
+ `;
95
+
96
+ const ExperienceItem = styled.div`
97
+ margin-bottom: 40px;
98
+
99
+ &:last-child {
100
+ margin-bottom: 0;
101
+ }
102
+ `;
103
+
104
+ const ExperienceHeader = styled.div`
105
+ margin-bottom: 12px;
106
+ `;
107
+
108
+ const Position = styled.h3`
109
+ font-size: 1.125rem;
110
+ font-weight: 500;
111
+ color: #2c3e50;
112
+ margin: 0 0 4px 0;
113
+ `;
114
+
115
+ const Company = styled.div`
116
+ font-size: 1rem;
117
+ color: #4682b4;
118
+ font-weight: 400;
119
+ margin-bottom: 4px;
120
+ `;
121
+
122
+ const StyledDateRange = styled(DateRange)`
123
+ font-size: 0.875rem;
124
+ color: #95a5a6;
125
+ font-weight: 300;
126
+ `;
127
+
128
+ const Description = styled.p`
129
+ font-size: 0.9375rem;
130
+ line-height: 1.8;
131
+ color: #34495e;
132
+ margin: 12px 0 16px 0;
133
+ font-weight: 300;
134
+ `;
135
+
136
+ const Highlights = styled.ul`
137
+ list-style: none;
138
+ padding: 0;
139
+ margin: 0;
140
+ `;
141
+
142
+ const Highlight = styled.li`
143
+ font-size: 0.9375rem;
144
+ line-height: 1.7;
145
+ color: #34495e;
146
+ margin-bottom: 8px;
147
+ padding-left: 16px;
148
+ position: relative;
149
+ font-weight: 300;
150
+
151
+ &:before {
152
+ content: '•';
153
+ position: absolute;
154
+ left: 0;
155
+ color: #4682b4;
156
+ font-weight: 700;
157
+ }
158
+ `;
159
+
160
+ const EducationItem = styled.div`
161
+ margin-bottom: 32px;
162
+
163
+ &:last-child {
164
+ margin-bottom: 0;
165
+ }
166
+ `;
167
+
168
+ const Institution = styled.h3`
169
+ font-size: 1rem;
170
+ font-weight: 500;
171
+ color: #2c3e50;
172
+ margin: 0 0 4px 0;
173
+ `;
174
+
175
+ const Degree = styled.div`
176
+ font-size: 0.9375rem;
177
+ color: #7f8c8d;
178
+ font-weight: 300;
179
+ margin-bottom: 4px;
180
+ `;
181
+
182
+ const SkillCategory = styled.div`
183
+ margin-bottom: 24px;
184
+
185
+ &:last-child {
186
+ margin-bottom: 0;
187
+ }
188
+ `;
189
+
190
+ const SkillName = styled.h4`
191
+ font-size: 0.875rem;
192
+ font-weight: 600;
193
+ color: #2c3e50;
194
+ margin: 0 0 8px 0;
195
+ text-transform: uppercase;
196
+ letter-spacing: 0.5px;
197
+ `;
198
+
199
+ const SkillKeywords = styled.div`
200
+ font-size: 0.875rem;
201
+ line-height: 1.6;
202
+ color: #34495e;
203
+ font-weight: 300;
204
+ `;
205
+
206
+ const ContactItem = styled.div`
207
+ margin-bottom: 16px;
208
+ font-size: 0.875rem;
209
+ color: #34495e;
210
+ font-weight: 300;
211
+
212
+ &:last-child {
213
+ margin-bottom: 0;
214
+ }
215
+ `;
216
+
217
+ const ContactLabel = styled.div`
218
+ font-size: 0.75rem;
219
+ text-transform: uppercase;
220
+ letter-spacing: 1px;
221
+ color: #95a5a6;
222
+ margin-bottom: 4px;
223
+ font-weight: 600;
224
+ `;
225
+
226
+ const ContactValue = styled.div`
227
+ color: #2c3e50;
228
+ word-break: break-word;
229
+ `;
230
+
231
+ const InterestsList = styled.div`
232
+ display: flex;
233
+ flex-wrap: wrap;
234
+ gap: 8px;
235
+ `;
236
+
237
+ const InterestTag = styled.span`
238
+ display: inline-block;
239
+ padding: 4px 12px;
240
+ background: #ecf0f1;
241
+ color: #34495e;
242
+ border-radius: 12px;
243
+ font-size: 0.8125rem;
244
+ font-weight: 400;
245
+ `;
246
+
247
+ const LanguageItem = styled.div`
248
+ margin-bottom: 16px;
249
+ font-size: 0.875rem;
250
+
251
+ &:last-child {
252
+ margin-bottom: 0;
253
+ }
254
+ `;
255
+
256
+ const LanguageName = styled.span`
257
+ font-weight: 500;
258
+ color: #2c3e50;
259
+ `;
260
+
261
+ const LanguageFluency = styled.span`
262
+ color: #7f8c8d;
263
+ font-weight: 300;
264
+ margin-left: 8px;
265
+ `;
266
+
267
+ function Resume({ resume }) {
268
+ const {
269
+ basics,
270
+ work,
271
+ education,
272
+ skills,
273
+ volunteer,
274
+ awards,
275
+ publications,
276
+ languages,
277
+ interests,
278
+ projects,
279
+ } = resume;
280
+
281
+ return (
282
+ <ResumeContainer>
283
+ <Header>
284
+ {basics && (
285
+ <>
286
+ {basics.name && <Name>{basics.name}</Name>}
287
+ {basics.label && <Title>{basics.label}</Title>}
288
+ {basics.summary && <Summary>{basics.summary}</Summary>}
289
+ </>
290
+ )}
291
+ </Header>
292
+
293
+ <TwoColumnLayout>
294
+ <Sidebar>
295
+ {/* Contact Information */}
296
+ {basics && (
297
+ <StyledSection>
298
+ <StyledSectionTitle>Contact</StyledSectionTitle>
299
+
300
+ {basics.email && (
301
+ <ContactItem>
302
+ <ContactLabel>Email</ContactLabel>
303
+ <ContactValue>
304
+ <Link href={`mailto:${basics.email}`}>{basics.email}</Link>
305
+ </ContactValue>
306
+ </ContactItem>
307
+ )}
308
+
309
+ {basics.phone && (
310
+ <ContactItem>
311
+ <ContactLabel>Phone</ContactLabel>
312
+ <ContactValue>{basics.phone}</ContactValue>
313
+ </ContactItem>
314
+ )}
315
+
316
+ {basics.location && (
317
+ <ContactItem>
318
+ <ContactLabel>Location</ContactLabel>
319
+ <ContactValue>
320
+ {[
321
+ basics.location.city,
322
+ basics.location.region,
323
+ basics.location.countryCode,
324
+ ]
325
+ .filter(Boolean)
326
+ .join(', ')}
327
+ </ContactValue>
328
+ </ContactItem>
329
+ )}
330
+
331
+ {basics.url && (
332
+ <ContactItem>
333
+ <ContactLabel>Website</ContactLabel>
334
+ <ContactValue>
335
+ <Link
336
+ href={safeUrl(basics.url)}
337
+ target={isExternalUrl(basics.url) ? '_blank' : undefined}
338
+ >
339
+ {basics.url.replace(/^https?:\/\//, '')}
340
+ </Link>
341
+ </ContactValue>
342
+ </ContactItem>
343
+ )}
344
+
345
+ {basics.profiles &&
346
+ basics.profiles.length > 0 &&
347
+ basics.profiles.map((profile, index) => (
348
+ <ContactItem key={index}>
349
+ <ContactLabel>{profile.network}</ContactLabel>
350
+ <ContactValue>
351
+ <Link
352
+ href={safeUrl(profile.url)}
353
+ target={
354
+ isExternalUrl(profile.url) ? '_blank' : undefined
355
+ }
356
+ >
357
+ {profile.username ||
358
+ profile.url.replace(/^https?:\/\//, '')}
359
+ </Link>
360
+ </ContactValue>
361
+ </ContactItem>
362
+ ))}
363
+ </StyledSection>
364
+ )}
365
+
366
+ {/* Skills */}
367
+ {skills && skills.length > 0 && (
368
+ <StyledSection>
369
+ <StyledSectionTitle>Skills</StyledSectionTitle>
370
+ {skills.map((skill, index) => (
371
+ <SkillCategory key={index}>
372
+ {skill.name && <SkillName>{skill.name}</SkillName>}
373
+ {skill.keywords && skill.keywords.length > 0 && (
374
+ <SkillKeywords>{skill.keywords.join(' • ')}</SkillKeywords>
375
+ )}
376
+ </SkillCategory>
377
+ ))}
378
+ </StyledSection>
379
+ )}
380
+
381
+ {/* Languages */}
382
+ {languages && languages.length > 0 && (
383
+ <StyledSection>
384
+ <StyledSectionTitle>Languages</StyledSectionTitle>
385
+ {languages.map((language, index) => (
386
+ <LanguageItem key={index}>
387
+ <LanguageName>{language.language}</LanguageName>
388
+ {language.fluency && (
389
+ <LanguageFluency>({language.fluency})</LanguageFluency>
390
+ )}
391
+ </LanguageItem>
392
+ ))}
393
+ </StyledSection>
394
+ )}
395
+
396
+ {/* Interests */}
397
+ {interests && interests.length > 0 && (
398
+ <StyledSection>
399
+ <StyledSectionTitle>Interests</StyledSectionTitle>
400
+ <InterestsList>
401
+ {interests.map((interest, index) => (
402
+ <InterestTag key={index}>{interest.name}</InterestTag>
403
+ ))}
404
+ </InterestsList>
405
+ </StyledSection>
406
+ )}
407
+ </Sidebar>
408
+
409
+ <MainContent>
410
+ {/* Work Experience */}
411
+ {work && work.length > 0 && (
412
+ <StyledSection>
413
+ <StyledSectionTitle>Experience</StyledSectionTitle>
414
+ {work.map((job, index) => (
415
+ <ExperienceItem key={index}>
416
+ <ExperienceHeader>
417
+ {job.position && <Position>{job.position}</Position>}
418
+ {job.name && <Company>{job.name}</Company>}
419
+ <StyledDateRange
420
+ startDate={job.startDate}
421
+ endDate={job.endDate}
422
+ />
423
+ </ExperienceHeader>
424
+
425
+ {job.summary && <Description>{job.summary}</Description>}
426
+
427
+ {job.highlights && job.highlights.length > 0 && (
428
+ <Highlights>
429
+ {job.highlights.map((highlight, i) => (
430
+ <Highlight key={i}>{highlight}</Highlight>
431
+ ))}
432
+ </Highlights>
433
+ )}
434
+ </ExperienceItem>
435
+ ))}
436
+ </StyledSection>
437
+ )}
438
+
439
+ {/* Education */}
440
+ {education && education.length > 0 && (
441
+ <StyledSection>
442
+ <StyledSectionTitle>Education</StyledSectionTitle>
443
+ {education.map((edu, index) => (
444
+ <EducationItem key={index}>
445
+ {edu.institution && (
446
+ <Institution>{edu.institution}</Institution>
447
+ )}
448
+ {edu.studyType && edu.area && (
449
+ <Degree>
450
+ {edu.studyType} in {edu.area}
451
+ </Degree>
452
+ )}
453
+ <StyledDateRange
454
+ startDate={edu.startDate}
455
+ endDate={edu.endDate}
456
+ />
457
+ </EducationItem>
458
+ ))}
459
+ </StyledSection>
460
+ )}
461
+
462
+ {/* Projects */}
463
+ {projects && projects.length > 0 && (
464
+ <StyledSection>
465
+ <StyledSectionTitle>Projects</StyledSectionTitle>
466
+ {projects.map((project, index) => (
467
+ <ExperienceItem key={index}>
468
+ <ExperienceHeader>
469
+ {project.name && <Position>{project.name}</Position>}
470
+ {project.url && (
471
+ <Company>
472
+ <Link
473
+ href={safeUrl(project.url)}
474
+ target={
475
+ isExternalUrl(project.url) ? '_blank' : undefined
476
+ }
477
+ >
478
+ {project.url.replace(/^https?:\/\//, '')}
479
+ </Link>
480
+ </Company>
481
+ )}
482
+ <StyledDateRange
483
+ startDate={project.startDate}
484
+ endDate={project.endDate}
485
+ />
486
+ </ExperienceHeader>
487
+
488
+ {project.description && (
489
+ <Description>{project.description}</Description>
490
+ )}
491
+
492
+ {project.highlights && project.highlights.length > 0 && (
493
+ <Highlights>
494
+ {project.highlights.map((highlight, i) => (
495
+ <Highlight key={i}>{highlight}</Highlight>
496
+ ))}
497
+ </Highlights>
498
+ )}
499
+ </ExperienceItem>
500
+ ))}
501
+ </StyledSection>
502
+ )}
503
+
504
+ {/* Awards */}
505
+ {awards && awards.length > 0 && (
506
+ <StyledSection>
507
+ <StyledSectionTitle>Awards</StyledSectionTitle>
508
+ {awards.map((award, index) => (
509
+ <EducationItem key={index}>
510
+ {award.title && <Institution>{award.title}</Institution>}
511
+ {award.awarder && <Degree>{award.awarder}</Degree>}
512
+ {award.date && <StyledDateRange startDate={award.date} />}
513
+ {award.summary && <Description>{award.summary}</Description>}
514
+ </EducationItem>
515
+ ))}
516
+ </StyledSection>
517
+ )}
518
+ </MainContent>
519
+ </TwoColumnLayout>
520
+ </ResumeContainer>
521
+ );
522
+ }
523
+
524
+ export default Resume;
package/src/index.js ADDED
@@ -0,0 +1,63 @@
1
+ import { renderToString } from 'react-dom/server';
2
+ import { ServerStyleSheet } from 'styled-components';
3
+ import Resume from './Resume.jsx';
4
+ import React from 'react';
5
+
6
+ export function render(resume) {
7
+ const sheet = new ServerStyleSheet();
8
+
9
+ try {
10
+ const html = renderToString(
11
+ sheet.collectStyles(<Resume resume={resume} />)
12
+ );
13
+ const styles = sheet.getStyleTags();
14
+
15
+ return `<!DOCTYPE html>
16
+ <html lang="en">
17
+ <head>
18
+ <meta charset="UTF-8">
19
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
20
+ <title>${resume.basics?.name || 'Resume'} - Curriculum Vitae</title>
21
+
22
+ <!-- Google Fonts -->
23
+ <link rel="preconnect" href="https://fonts.googleapis.com">
24
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
25
+ <link href="https://fonts.googleapis.com/css2?family=Lato:wght@300;400;700&display=swap" rel="stylesheet">
26
+
27
+ ${styles}
28
+
29
+ <style>
30
+ * {
31
+ box-sizing: border-box;
32
+ }
33
+
34
+ html {
35
+ margin: 0;
36
+ padding: 0;
37
+ }
38
+
39
+ body {
40
+ margin: 0;
41
+ padding: 0;
42
+ background: #f8f9fa;
43
+ }
44
+
45
+ @media print {
46
+ body {
47
+ background: white;
48
+ }
49
+
50
+ @page {
51
+ margin: 0.5cm;
52
+ }
53
+ }
54
+ </style>
55
+ </head>
56
+ <body>
57
+ ${html}
58
+ </body>
59
+ </html>`;
60
+ } finally {
61
+ sheet.seal();
62
+ }
63
+ }