jsonresume-theme-writers-portfolio 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/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "jsonresume-theme-writers-portfolio",
3
+ "version": "0.1.0",
4
+ "description": "Literary and elegant resume theme for content writers, editors, and communications professionals. Features transitional serif typography, generous spacing, and prose-like readability.",
5
+ "type": "module",
6
+ "main": "./src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js"
9
+ },
10
+ "keywords": [
11
+ "jsonresume",
12
+ "theme",
13
+ "resume",
14
+ "cv",
15
+ "writer",
16
+ "portfolio",
17
+ "literary",
18
+ "elegant",
19
+ "serif",
20
+ "editorial",
21
+ "content",
22
+ "communications",
23
+ "ats-friendly"
24
+ ],
25
+ "dependencies": {
26
+ "react": "^19.2.0",
27
+ "react-dom": "^19.2.0",
28
+ "styled-components": "^6.1.19",
29
+ "@resume/core": "0.1.0"
30
+ },
31
+ "devDependencies": {
32
+ "vitest": "^1.6.0"
33
+ },
34
+ "peerDependencies": {
35
+ "react": "^18.0.0 || ^19.0.0",
36
+ "react-dom": "^18.0.0 || ^19.0.0"
37
+ },
38
+ "scripts": {
39
+ "test": "vitest run",
40
+ "test:watch": "vitest"
41
+ }
42
+ }
package/src/Resume.jsx ADDED
@@ -0,0 +1,509 @@
1
+ import React from 'react';
2
+ import styled from 'styled-components';
3
+ import { Section, SectionTitle, DateRange, ContactInfo } from '@resume/core';
4
+
5
+ /**
6
+ * Writer's Portfolio Resume Theme
7
+ * Literary, elegant design with transitional serif typography
8
+ * Perfect for content writers, editors, and communications professionals
9
+ */
10
+
11
+ const Layout = styled.div`
12
+ max-width: 680px;
13
+ margin: 0 auto;
14
+ padding: 60px 50px;
15
+ background: #fefdfb;
16
+ font-family: 'Merriweather', 'Charter', Georgia, serif;
17
+ color: #2f2f2f;
18
+ font-size: 17px;
19
+ line-height: 1.75;
20
+
21
+ @media print {
22
+ padding: 40px 30px;
23
+ background: white;
24
+ }
25
+
26
+ @media (max-width: 768px) {
27
+ padding: 40px 30px;
28
+ }
29
+ `;
30
+
31
+ const Header = styled.header`
32
+ margin-bottom: 50px;
33
+ padding-bottom: 30px;
34
+ border-bottom: 1px solid #d4d0c8;
35
+ text-align: center;
36
+ `;
37
+
38
+ const Name = styled.h1`
39
+ font-size: 38px;
40
+ font-weight: 400;
41
+ color: #2f2f2f;
42
+ margin: 0 0 12px 0;
43
+ letter-spacing: 0.5px;
44
+ font-family: 'Merriweather', 'Charter', Georgia, serif;
45
+ `;
46
+
47
+ const Label = styled.div`
48
+ font-size: 16px;
49
+ color: #5a5a5a;
50
+ margin-bottom: 20px;
51
+ font-weight: 300;
52
+ font-style: italic;
53
+ letter-spacing: 0.3px;
54
+ `;
55
+
56
+ const StyledContactInfo = styled(ContactInfo)`
57
+ font-size: 15px;
58
+ display: flex;
59
+ flex-wrap: wrap;
60
+ gap: 16px;
61
+ justify-content: center;
62
+ margin-top: 20px;
63
+
64
+ a {
65
+ font-size: 15px;
66
+ color: #2f2f2f;
67
+ text-decoration: none;
68
+ border-bottom: 1px solid #d4d0c8;
69
+ transition: border-color 0.2s;
70
+
71
+ &:hover {
72
+ border-bottom-color: #2f2f2f;
73
+ }
74
+ }
75
+ `;
76
+
77
+ const Summary = styled.div`
78
+ font-size: 18px;
79
+ line-height: 1.8;
80
+ color: #3a3a3a;
81
+ margin: 40px 0 0 0;
82
+ text-align: left;
83
+ text-indent: 2em;
84
+
85
+ &::first-letter {
86
+ font-size: 2.5em;
87
+ float: left;
88
+ line-height: 0.85;
89
+ margin: 0.05em 0.1em 0 0;
90
+ color: #2f2f2f;
91
+ }
92
+ `;
93
+
94
+ const StyledSection = styled(Section)`
95
+ margin: 45px 0;
96
+ `;
97
+
98
+ const StyledSectionTitle = styled(SectionTitle)`
99
+ font-size: 22px;
100
+ font-weight: 400;
101
+ color: #2f2f2f;
102
+ margin: 0 0 25px 0;
103
+ padding-bottom: 12px;
104
+ border-bottom: 1px solid #d4d0c8;
105
+ text-align: center;
106
+ letter-spacing: 1px;
107
+ font-family: 'Merriweather', 'Charter', Georgia, serif;
108
+ `;
109
+
110
+ const WorkItem = styled.div`
111
+ margin-bottom: 35px;
112
+ page-break-inside: avoid;
113
+
114
+ &:last-child {
115
+ margin-bottom: 0;
116
+ }
117
+ `;
118
+
119
+ const WorkHeader = styled.div`
120
+ margin-bottom: 10px;
121
+ `;
122
+
123
+ const Position = styled.h3`
124
+ font-size: 19px;
125
+ font-weight: 700;
126
+ color: #2f2f2f;
127
+ margin: 0 0 6px 0;
128
+ font-family: 'Merriweather', 'Charter', Georgia, serif;
129
+ `;
130
+
131
+ const CompanyLine = styled.div`
132
+ font-size: 16px;
133
+ color: #5a5a5a;
134
+ display: flex;
135
+ gap: 12px;
136
+ align-items: center;
137
+ font-style: italic;
138
+ flex-wrap: wrap;
139
+ `;
140
+
141
+ const Company = styled.span`
142
+ font-weight: 400;
143
+ `;
144
+
145
+ const DateText = styled.span`
146
+ font-size: 15px;
147
+ color: #7a7a7a;
148
+ font-style: normal;
149
+ font-variant-numeric: oldstyle-nums;
150
+
151
+ &::before {
152
+ content: '•';
153
+ margin-right: 12px;
154
+ color: #d4d0c8;
155
+ }
156
+ `;
157
+
158
+ const WorkSummary = styled.p`
159
+ margin: 15px 0;
160
+ color: #3a3a3a;
161
+ line-height: 1.8;
162
+ font-size: 17px;
163
+ text-align: justify;
164
+ `;
165
+
166
+ const Highlights = styled.ul`
167
+ margin: 15px 0 0 25px;
168
+ padding: 0;
169
+ list-style-type: '—';
170
+
171
+ li {
172
+ margin: 10px 0;
173
+ padding-left: 12px;
174
+ color: #3a3a3a;
175
+ line-height: 1.8;
176
+ font-size: 16px;
177
+ text-align: justify;
178
+ }
179
+ `;
180
+
181
+ const EducationItem = styled.div`
182
+ margin-bottom: 30px;
183
+ page-break-inside: avoid;
184
+
185
+ &:last-child {
186
+ margin-bottom: 0;
187
+ }
188
+ `;
189
+
190
+ const Institution = styled.h3`
191
+ font-size: 18px;
192
+ font-weight: 700;
193
+ color: #2f2f2f;
194
+ margin: 0 0 6px 0;
195
+ font-family: 'Merriweather', 'Charter', Georgia, serif;
196
+ `;
197
+
198
+ const Degree = styled.div`
199
+ font-size: 16px;
200
+ color: #5a5a5a;
201
+ margin-bottom: 4px;
202
+ font-style: italic;
203
+ `;
204
+
205
+ const EducationDate = styled.div`
206
+ font-size: 15px;
207
+ color: #7a7a7a;
208
+ font-variant-numeric: oldstyle-nums;
209
+ `;
210
+
211
+ const SkillsContainer = styled.div`
212
+ display: flex;
213
+ flex-direction: column;
214
+ gap: 20px;
215
+ `;
216
+
217
+ const SkillItem = styled.div`
218
+ border-left: 2px solid #d4d0c8;
219
+ padding-left: 20px;
220
+ `;
221
+
222
+ const SkillName = styled.h4`
223
+ font-size: 16px;
224
+ font-weight: 700;
225
+ color: #2f2f2f;
226
+ margin: 0 0 8px 0;
227
+ text-transform: uppercase;
228
+ letter-spacing: 1px;
229
+ font-family: 'Merriweather', 'Charter', Georgia, serif;
230
+ `;
231
+
232
+ const SkillKeywords = styled.div`
233
+ font-size: 16px;
234
+ color: #5a5a5a;
235
+ line-height: 1.7;
236
+ `;
237
+
238
+ const ProjectItem = styled.div`
239
+ margin-bottom: 30px;
240
+ page-break-inside: avoid;
241
+
242
+ &:last-child {
243
+ margin-bottom: 0;
244
+ }
245
+ `;
246
+
247
+ const ProjectHeader = styled.div`
248
+ margin-bottom: 10px;
249
+ `;
250
+
251
+ const ProjectName = styled.h3`
252
+ font-size: 18px;
253
+ font-weight: 700;
254
+ color: #2f2f2f;
255
+ margin: 0 0 6px 0;
256
+ font-family: 'Merriweather', 'Charter', Georgia, serif;
257
+ `;
258
+
259
+ const TwoColumnGrid = styled.div`
260
+ display: grid;
261
+ grid-template-columns: repeat(2, 1fr);
262
+ gap: 20px;
263
+
264
+ @media (max-width: 640px) {
265
+ grid-template-columns: 1fr;
266
+ }
267
+ `;
268
+
269
+ const CompactItem = styled.div`
270
+ font-size: 16px;
271
+ line-height: 1.7;
272
+ color: #3a3a3a;
273
+ `;
274
+
275
+ const CompactTitle = styled.span`
276
+ font-weight: 700;
277
+ color: #2f2f2f;
278
+ `;
279
+
280
+ function Resume({ resume }) {
281
+ const {
282
+ basics = {},
283
+ work = [],
284
+ education = [],
285
+ skills = [],
286
+ projects = [],
287
+ volunteer = [],
288
+ awards = [],
289
+ publications = [],
290
+ languages = [],
291
+ interests = [],
292
+ references = [],
293
+ } = resume;
294
+
295
+ return (
296
+ <Layout>
297
+ <Header>
298
+ <Name>{basics.name}</Name>
299
+ {basics.label && <Label>{basics.label}</Label>}
300
+ <StyledContactInfo basics={basics} />
301
+ </Header>
302
+
303
+ {basics.summary && <Summary>{basics.summary}</Summary>}
304
+
305
+ {work?.length > 0 && (
306
+ <StyledSection>
307
+ <StyledSectionTitle>Experience</StyledSectionTitle>
308
+ {work.map((job, index) => (
309
+ <WorkItem key={index}>
310
+ <WorkHeader>
311
+ <Position>{job.position}</Position>
312
+ <CompanyLine>
313
+ {job.name && <Company>{job.name}</Company>}
314
+ <DateText>
315
+ <DateRange
316
+ startDate={job.startDate}
317
+ endDate={job.endDate}
318
+ />
319
+ </DateText>
320
+ </CompanyLine>
321
+ </WorkHeader>
322
+ {job.summary && <WorkSummary>{job.summary}</WorkSummary>}
323
+ {job.highlights?.length > 0 && (
324
+ <Highlights>
325
+ {job.highlights.map((highlight, i) => (
326
+ <li key={i}>{highlight}</li>
327
+ ))}
328
+ </Highlights>
329
+ )}
330
+ </WorkItem>
331
+ ))}
332
+ </StyledSection>
333
+ )}
334
+
335
+ {publications?.length > 0 && (
336
+ <StyledSection>
337
+ <StyledSectionTitle>Publications</StyledSectionTitle>
338
+ {publications.map((pub, index) => (
339
+ <EducationItem key={index}>
340
+ <Institution>{pub.name}</Institution>
341
+ {pub.publisher && <Degree>Published by {pub.publisher}</Degree>}
342
+ {pub.releaseDate && (
343
+ <EducationDate>{pub.releaseDate}</EducationDate>
344
+ )}
345
+ {pub.summary && <WorkSummary>{pub.summary}</WorkSummary>}
346
+ </EducationItem>
347
+ ))}
348
+ </StyledSection>
349
+ )}
350
+
351
+ {skills?.length > 0 && (
352
+ <StyledSection>
353
+ <StyledSectionTitle>Skills & Expertise</StyledSectionTitle>
354
+ <SkillsContainer>
355
+ {skills.map((skill, index) => (
356
+ <SkillItem key={index}>
357
+ <SkillName>{skill.name}</SkillName>
358
+ <SkillKeywords>
359
+ {skill.keywords?.length > 0
360
+ ? skill.keywords.join(' · ')
361
+ : '—'}
362
+ </SkillKeywords>
363
+ </SkillItem>
364
+ ))}
365
+ </SkillsContainer>
366
+ </StyledSection>
367
+ )}
368
+
369
+ {education?.length > 0 && (
370
+ <StyledSection>
371
+ <StyledSectionTitle>Education</StyledSectionTitle>
372
+ {education.map((edu, index) => (
373
+ <EducationItem key={index}>
374
+ <Institution>{edu.institution}</Institution>
375
+ <Degree>
376
+ {edu.studyType} in {edu.area}
377
+ {edu.score && ` • ${edu.score}`}
378
+ </Degree>
379
+ <EducationDate>
380
+ <DateRange startDate={edu.startDate} endDate={edu.endDate} />
381
+ </EducationDate>
382
+ </EducationItem>
383
+ ))}
384
+ </StyledSection>
385
+ )}
386
+
387
+ {projects?.length > 0 && (
388
+ <StyledSection>
389
+ <StyledSectionTitle>Projects</StyledSectionTitle>
390
+ {projects.map((project, index) => (
391
+ <ProjectItem key={index}>
392
+ <ProjectHeader>
393
+ <ProjectName>{project.name}</ProjectName>
394
+ {(project.startDate || project.endDate) && (
395
+ <DateText>
396
+ <DateRange
397
+ startDate={project.startDate}
398
+ endDate={project.endDate}
399
+ />
400
+ </DateText>
401
+ )}
402
+ </ProjectHeader>
403
+ {project.description && (
404
+ <WorkSummary>{project.description}</WorkSummary>
405
+ )}
406
+ {project.highlights?.length > 0 && (
407
+ <Highlights>
408
+ {project.highlights.map((highlight, i) => (
409
+ <li key={i}>{highlight}</li>
410
+ ))}
411
+ </Highlights>
412
+ )}
413
+ </ProjectItem>
414
+ ))}
415
+ </StyledSection>
416
+ )}
417
+
418
+ {volunteer?.length > 0 && (
419
+ <StyledSection>
420
+ <StyledSectionTitle>Volunteer</StyledSectionTitle>
421
+ {volunteer.map((vol, index) => (
422
+ <WorkItem key={index}>
423
+ <WorkHeader>
424
+ <Position>{vol.position}</Position>
425
+ <CompanyLine>
426
+ {vol.organization && <Company>{vol.organization}</Company>}
427
+ {(vol.startDate || vol.endDate) && (
428
+ <DateText>
429
+ <DateRange
430
+ startDate={vol.startDate}
431
+ endDate={vol.endDate}
432
+ />
433
+ </DateText>
434
+ )}
435
+ </CompanyLine>
436
+ </WorkHeader>
437
+ {vol.summary && <WorkSummary>{vol.summary}</WorkSummary>}
438
+ {vol.highlights?.length > 0 && (
439
+ <Highlights>
440
+ {vol.highlights.map((highlight, i) => (
441
+ <li key={i}>{highlight}</li>
442
+ ))}
443
+ </Highlights>
444
+ )}
445
+ </WorkItem>
446
+ ))}
447
+ </StyledSection>
448
+ )}
449
+
450
+ {awards?.length > 0 && (
451
+ <StyledSection>
452
+ <StyledSectionTitle>Awards & Recognition</StyledSectionTitle>
453
+ {awards.map((award, index) => (
454
+ <EducationItem key={index}>
455
+ <Institution>{award.title}</Institution>
456
+ {award.awarder && <Degree>Awarded by {award.awarder}</Degree>}
457
+ {award.date && <EducationDate>{award.date}</EducationDate>}
458
+ {award.summary && <WorkSummary>{award.summary}</WorkSummary>}
459
+ </EducationItem>
460
+ ))}
461
+ </StyledSection>
462
+ )}
463
+
464
+ {languages?.length > 0 && (
465
+ <StyledSection>
466
+ <StyledSectionTitle>Languages</StyledSectionTitle>
467
+ <TwoColumnGrid>
468
+ {languages.map((lang, index) => (
469
+ <CompactItem key={index}>
470
+ <CompactTitle>{lang.language}:</CompactTitle>{' '}
471
+ {lang.fluency || '—'}
472
+ </CompactItem>
473
+ ))}
474
+ </TwoColumnGrid>
475
+ </StyledSection>
476
+ )}
477
+
478
+ {interests?.length > 0 && (
479
+ <StyledSection>
480
+ <StyledSectionTitle>Interests</StyledSectionTitle>
481
+ <TwoColumnGrid>
482
+ {interests.map((interest, index) => (
483
+ <CompactItem key={index}>
484
+ <CompactTitle>{interest.name}</CompactTitle>
485
+ {interest.keywords?.length > 0 && (
486
+ <>: {interest.keywords.join(', ')}</>
487
+ )}
488
+ </CompactItem>
489
+ ))}
490
+ </TwoColumnGrid>
491
+ </StyledSection>
492
+ )}
493
+
494
+ {references?.length > 0 && (
495
+ <StyledSection>
496
+ <StyledSectionTitle>References</StyledSectionTitle>
497
+ {references.map((ref, index) => (
498
+ <EducationItem key={index}>
499
+ <Institution>{ref.name}</Institution>
500
+ {ref.reference && <WorkSummary>"{ref.reference}"</WorkSummary>}
501
+ </EducationItem>
502
+ ))}
503
+ </StyledSection>
504
+ )}
505
+ </Layout>
506
+ );
507
+ }
508
+
509
+ export default Resume;
package/src/index.js ADDED
@@ -0,0 +1,98 @@
1
+ import { renderToString } from 'react-dom/server';
2
+ import { ServerStyleSheet } from 'styled-components';
3
+ import Resume from './Resume.jsx';
4
+
5
+ /**
6
+ * Writer's Portfolio Resume Theme
7
+ * Literary and elegant design with transitional serif typography
8
+ * Perfect for content writers, editors, and communications professionals
9
+ *
10
+ * @param {Object} resume - JSON Resume object
11
+ * @param {Object} [options] - Rendering options
12
+ * @returns {string} Complete HTML document
13
+ */
14
+ export function render(resume, options = {}) {
15
+ const {
16
+ locale = 'en',
17
+ dir = 'ltr',
18
+ title = resume.basics?.name || 'Resume',
19
+ } = options;
20
+
21
+ const sheet = new ServerStyleSheet();
22
+
23
+ try {
24
+ const html = renderToString(
25
+ sheet.collectStyles(<Resume resume={resume} />)
26
+ );
27
+
28
+ const styles = sheet.getStyleTags();
29
+
30
+ const designTokens = `
31
+ :root {
32
+ --resume-color-primary: #2f2f2f;
33
+ --resume-color-secondary: #5a5a5a;
34
+ --resume-color-accent: #3a3a3a;
35
+ --resume-color-bg: #fefdfb;
36
+ --resume-color-card: #ffffff;
37
+ --resume-color-border: #d4d0c8;
38
+ }
39
+ `;
40
+
41
+ const globalStyles = `
42
+ * {
43
+ margin: 0;
44
+ padding: 0;
45
+ box-sizing: border-box;
46
+ }
47
+
48
+ body {
49
+ margin: 0;
50
+ background: #fefdfb;
51
+ -webkit-font-smoothing: antialiased;
52
+ -moz-osx-font-smoothing: grayscale;
53
+ }
54
+
55
+ @media print {
56
+ body {
57
+ background: white;
58
+ }
59
+
60
+ @page {
61
+ size: A4;
62
+ margin: 0.75in;
63
+ }
64
+ }
65
+ `;
66
+
67
+ return `<!DOCTYPE html>
68
+ <html lang="${locale}" dir="${dir}">
69
+ <head>
70
+ <meta charset="UTF-8">
71
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
72
+ <title>${title}</title>
73
+
74
+ <link rel="preconnect" href="https://fonts.googleapis.com">
75
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
76
+ <link href="https://fonts.googleapis.com/css2?family=Merriweather:ital,wght@0,300;0,400;0,700;1,300;1,400&display=swap" rel="stylesheet">
77
+
78
+ <style>
79
+ ${designTokens}
80
+ </style>
81
+
82
+ ${styles}
83
+
84
+ <style>
85
+ ${globalStyles}
86
+ </style>
87
+ </head>
88
+ <body>
89
+ ${html}
90
+ </body>
91
+ </html>`;
92
+ } finally {
93
+ sheet.seal();
94
+ }
95
+ }
96
+
97
+ export { Resume };
98
+ export default { render };