jsonresume-theme-sidebar 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,37 @@
1
+ {
2
+ "name": "jsonresume-theme-sidebar",
3
+ "version": "0.1.0",
4
+ "description": "Two-column sidebar resume theme with dark sidebar and light main content - ATS-friendly",
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
+ "sidebar",
16
+ "two-column",
17
+ "professional",
18
+ "ats-friendly"
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
+ "react": "^18.0.0 || ^19.0.0",
31
+ "react-dom": "^18.0.0 || ^19.0.0"
32
+ },
33
+ "scripts": {
34
+ "test": "vitest run",
35
+ "test:watch": "vitest"
36
+ }
37
+ }
package/src/Resume.jsx ADDED
@@ -0,0 +1,590 @@
1
+ import styled from 'styled-components';
2
+ import {
3
+ Section,
4
+ SectionTitle,
5
+ ListItem,
6
+ DateRange,
7
+ BadgeList,
8
+ safeUrl,
9
+ getLinkRel,
10
+ } from '@resume/core';
11
+
12
+ /**
13
+ * Sidebar Resume Theme
14
+ * Two-column layout with dark sidebar and light main content
15
+ */
16
+
17
+ const Layout = styled.div`
18
+ display: grid;
19
+ grid-template-columns: 315px 1fr;
20
+ min-height: 100vh;
21
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
22
+ font-size: 11pt;
23
+ line-height: 1.6;
24
+ color: #333;
25
+
26
+ @media print {
27
+ min-height: auto;
28
+ }
29
+ `;
30
+
31
+ const Sidebar = styled.aside`
32
+ background: #1e3a52;
33
+ color: #ffffff;
34
+ padding: 40px 30px;
35
+ `;
36
+
37
+ const MainContent = styled.main`
38
+ background: #f5f2ed;
39
+ padding: 60px 50px;
40
+ `;
41
+
42
+ const ProfilePhoto = styled.div`
43
+ width: 180px;
44
+ height: 180px;
45
+ border-radius: 50%;
46
+ overflow: hidden;
47
+ border: 8px solid #ffffff;
48
+ margin: 0 auto 40px;
49
+ background: #ffffff;
50
+
51
+ img {
52
+ width: 100%;
53
+ height: 100%;
54
+ object-fit: cover;
55
+ }
56
+ `;
57
+
58
+ const Name = styled.h1`
59
+ font-size: 42pt;
60
+ font-weight: 700;
61
+ margin: 0 0 10px 0;
62
+ color: #2c2c2c;
63
+ letter-spacing: 0.5px;
64
+
65
+ span {
66
+ font-weight: 400;
67
+ }
68
+ `;
69
+
70
+ const JobTitle = styled.div`
71
+ font-size: 18pt;
72
+ color: #666;
73
+ margin-bottom: 40px;
74
+ text-transform: uppercase;
75
+ letter-spacing: 2px;
76
+ padding-bottom: 15px;
77
+ border-bottom: 3px solid #1e3a52;
78
+ display: inline-block;
79
+ `;
80
+
81
+ const SidebarSection = styled(Section)`
82
+ margin-bottom: 40px;
83
+ `;
84
+
85
+ const SidebarTitle = styled.h2`
86
+ font-size: 14pt;
87
+ font-weight: 700;
88
+ text-transform: uppercase;
89
+ letter-spacing: 1.5px;
90
+ margin: 0 0 20px 0;
91
+ padding-bottom: 10px;
92
+ border-bottom: 2px solid rgba(255, 255, 255, 0.3);
93
+ `;
94
+
95
+ const ContactItem = styled.div`
96
+ display: flex;
97
+ align-items: flex-start;
98
+ margin-bottom: 15px;
99
+ font-size: 10pt;
100
+
101
+ svg {
102
+ margin-right: 12px;
103
+ flex-shrink: 0;
104
+ margin-top: 2px;
105
+ }
106
+
107
+ a {
108
+ color: #ffffff;
109
+ text-decoration: none;
110
+ word-break: break-word;
111
+
112
+ &:hover {
113
+ text-decoration: underline;
114
+ }
115
+ }
116
+ `;
117
+
118
+ const Icon = styled.span`
119
+ display: inline-block;
120
+ width: 16px;
121
+ margin-right: 12px;
122
+ flex-shrink: 0;
123
+ `;
124
+
125
+ const EducationItem = styled.div`
126
+ margin-bottom: 25px;
127
+
128
+ h3 {
129
+ font-size: 11pt;
130
+ font-weight: 600;
131
+ margin: 0 0 5px 0;
132
+ }
133
+
134
+ .dates {
135
+ font-size: 10pt;
136
+ margin-bottom: 8px;
137
+ opacity: 0.9;
138
+ }
139
+
140
+ ul {
141
+ margin: 5px 0 0 0;
142
+ padding-left: 18px;
143
+ font-size: 10pt;
144
+
145
+ li {
146
+ margin: 3px 0;
147
+ }
148
+ }
149
+ `;
150
+
151
+ const SkillsList = styled.ul`
152
+ list-style: none;
153
+ padding: 0;
154
+ margin: 0;
155
+
156
+ li {
157
+ margin: 8px 0;
158
+ padding-left: 15px;
159
+ position: relative;
160
+ font-size: 10pt;
161
+
162
+ &:before {
163
+ content: '▪';
164
+ position: absolute;
165
+ left: 0;
166
+ color: rgba(255, 255, 255, 0.7);
167
+ }
168
+ }
169
+ `;
170
+
171
+ const MainSection = styled(Section)`
172
+ margin-bottom: 40px;
173
+ `;
174
+
175
+ const MainSectionTitle = styled.h2`
176
+ font-size: 16pt;
177
+ font-weight: 700;
178
+ text-transform: uppercase;
179
+ letter-spacing: 1.5px;
180
+ margin: 0 0 25px 0;
181
+ padding-bottom: 10px;
182
+ border-bottom: 2px solid #1e3a52;
183
+ color: #1e3a52;
184
+ `;
185
+
186
+ const ProfileText = styled.p`
187
+ text-align: justify;
188
+ line-height: 1.8;
189
+ color: #4a4a4a;
190
+ `;
191
+
192
+ const WorkItem = styled.div`
193
+ margin-bottom: 30px;
194
+ position: relative;
195
+ padding-left: 25px;
196
+
197
+ &:before {
198
+ content: '';
199
+ position: absolute;
200
+ left: 4px;
201
+ top: 8px;
202
+ width: 10px;
203
+ height: 10px;
204
+ background: #1e3a52;
205
+ border-radius: 50%;
206
+ }
207
+
208
+ &:after {
209
+ content: '';
210
+ position: absolute;
211
+ left: 8px;
212
+ top: 20px;
213
+ width: 2px;
214
+ height: calc(100% - 10px);
215
+ background: #d0d0d0;
216
+ }
217
+
218
+ &:last-child:after {
219
+ display: none;
220
+ }
221
+ `;
222
+
223
+ const WorkHeader = styled.div`
224
+ display: flex;
225
+ justify-content: space-between;
226
+ align-items: flex-start;
227
+ margin-bottom: 8px;
228
+ `;
229
+
230
+ const WorkTitle = styled.h3`
231
+ font-size: 12pt;
232
+ font-weight: 700;
233
+ margin: 0;
234
+ color: #2c2c2c;
235
+ `;
236
+
237
+ const WorkCompany = styled.div`
238
+ font-size: 11pt;
239
+ color: #666;
240
+ margin-bottom: 10px;
241
+ `;
242
+
243
+ const WorkDate = styled.div`
244
+ font-size: 10pt;
245
+ color: #666;
246
+ white-space: nowrap;
247
+ text-align: right;
248
+ `;
249
+
250
+ const WorkDescription = styled.ul`
251
+ margin: 10px 0 0 0;
252
+ padding-left: 20px;
253
+ color: #4a4a4a;
254
+
255
+ li {
256
+ margin: 6px 0;
257
+ text-align: justify;
258
+ }
259
+ `;
260
+
261
+ const ReferenceGrid = styled.div`
262
+ display: grid;
263
+ grid-template-columns: 1fr 1fr;
264
+ gap: 30px;
265
+ `;
266
+
267
+ const ReferenceCard = styled.div`
268
+ h3 {
269
+ font-size: 12pt;
270
+ font-weight: 700;
271
+ margin: 0 0 5px 0;
272
+ color: #2c2c2c;
273
+ }
274
+
275
+ .title {
276
+ font-size: 10pt;
277
+ color: #666;
278
+ margin-bottom: 8px;
279
+ }
280
+
281
+ .contact {
282
+ font-size: 9pt;
283
+ color: #666;
284
+ margin: 3px 0;
285
+ }
286
+ `;
287
+
288
+ const LanguagesList = styled.ul`
289
+ list-style: none;
290
+ padding: 0;
291
+ margin: 0;
292
+
293
+ li {
294
+ margin: 8px 0;
295
+ padding-left: 15px;
296
+ position: relative;
297
+ font-size: 10pt;
298
+
299
+ &:before {
300
+ content: '▪';
301
+ position: absolute;
302
+ left: 0;
303
+ color: rgba(255, 255, 255, 0.7);
304
+ }
305
+ }
306
+ `;
307
+
308
+ function Resume({ resume }) {
309
+ const {
310
+ basics = {},
311
+ work = [],
312
+ education = [],
313
+ skills = [],
314
+ languages = [],
315
+ references = [],
316
+ projects = [],
317
+ awards = [],
318
+ interests = [],
319
+ } = resume;
320
+
321
+ // Split name into first and last for styling
322
+ const nameParts = basics.name ? basics.name.split(' ') : [];
323
+ const firstName = nameParts.slice(0, -1).join(' ');
324
+ const lastName = nameParts[nameParts.length - 1];
325
+
326
+ return (
327
+ <Layout>
328
+ {/* Left Sidebar */}
329
+ <Sidebar>
330
+ {/* Profile Photo */}
331
+ {basics.image && (
332
+ <ProfilePhoto>
333
+ <img src={basics.image} alt={basics.name} />
334
+ </ProfilePhoto>
335
+ )}
336
+
337
+ {/* Contact Section */}
338
+ {(basics.phone || basics.email || basics.location || basics.url) && (
339
+ <SidebarSection>
340
+ <SidebarTitle>CONTACT</SidebarTitle>
341
+ {basics.phone && (
342
+ <ContactItem>
343
+ <Icon>📞</Icon>
344
+ <span>{basics.phone}</span>
345
+ </ContactItem>
346
+ )}
347
+ {basics.email && (
348
+ <ContactItem>
349
+ <Icon>✉️</Icon>
350
+ <a href={safeUrl(`mailto:${basics.email}`)}>{basics.email}</a>
351
+ </ContactItem>
352
+ )}
353
+ {basics.location && (
354
+ <ContactItem>
355
+ <Icon>📍</Icon>
356
+ <span>
357
+ {[
358
+ basics.location.address,
359
+ basics.location.city,
360
+ basics.location.region,
361
+ ]
362
+ .filter(Boolean)
363
+ .join(', ')}
364
+ </span>
365
+ </ContactItem>
366
+ )}
367
+ {basics.url && (
368
+ <ContactItem>
369
+ <Icon>🌐</Icon>
370
+ <a
371
+ href={safeUrl(basics.url)}
372
+ target="_blank"
373
+ rel={getLinkRel(basics.url, true)}
374
+ >
375
+ {basics.url.replace(/^https?:\/\//, '')}
376
+ </a>
377
+ </ContactItem>
378
+ )}
379
+ </SidebarSection>
380
+ )}
381
+
382
+ {/* Education Section */}
383
+ {education.length > 0 && (
384
+ <SidebarSection>
385
+ <SidebarTitle>EDUCATION</SidebarTitle>
386
+ {education.map((edu, index) => (
387
+ <EducationItem key={index}>
388
+ <div className="dates">
389
+ {edu.startDate && edu.endDate
390
+ ? `${new Date(edu.startDate).getFullYear()} - ${new Date(
391
+ edu.endDate
392
+ ).getFullYear()}`
393
+ : ''}
394
+ </div>
395
+ <h3>{edu.institution}</h3>
396
+ {edu.studyType && edu.area && (
397
+ <ul>
398
+ <li>
399
+ {edu.studyType} of {edu.area}
400
+ </li>
401
+ {edu.score && <li>GPA: {edu.score}</li>}
402
+ </ul>
403
+ )}
404
+ </EducationItem>
405
+ ))}
406
+ </SidebarSection>
407
+ )}
408
+
409
+ {/* Skills Section */}
410
+ {skills.length > 0 && (
411
+ <SidebarSection>
412
+ <SidebarTitle>SKILLS</SidebarTitle>
413
+ <SkillsList>
414
+ {skills.flatMap((skillGroup) =>
415
+ (skillGroup.keywords || []).map((skill, idx) => (
416
+ <li key={`${skillGroup.name}-${idx}`}>{skill}</li>
417
+ ))
418
+ )}
419
+ </SkillsList>
420
+ </SidebarSection>
421
+ )}
422
+
423
+ {/* Languages Section */}
424
+ {languages.length > 0 && (
425
+ <SidebarSection>
426
+ <SidebarTitle>LANGUAGES</SidebarTitle>
427
+ <LanguagesList>
428
+ {languages.map((lang, index) => (
429
+ <li key={index}>
430
+ {lang.language}
431
+ {lang.fluency && ` (${lang.fluency})`}
432
+ </li>
433
+ ))}
434
+ </LanguagesList>
435
+ </SidebarSection>
436
+ )}
437
+ </Sidebar>
438
+
439
+ {/* Right Main Content */}
440
+ <MainContent>
441
+ {/* Header */}
442
+ <div>
443
+ <Name>
444
+ {firstName && <span>{firstName} </span>}
445
+ {lastName}
446
+ </Name>
447
+ {basics.label && <JobTitle>{basics.label}</JobTitle>}
448
+ </div>
449
+
450
+ {/* Profile/Summary Section */}
451
+ {basics.summary && (
452
+ <MainSection>
453
+ <MainSectionTitle>PROFILE</MainSectionTitle>
454
+ <ProfileText>{basics.summary}</ProfileText>
455
+ </MainSection>
456
+ )}
457
+
458
+ {/* Work Experience Section */}
459
+ {work.length > 0 && (
460
+ <MainSection>
461
+ <MainSectionTitle>WORK EXPERIENCE</MainSectionTitle>
462
+ {work.map((job, index) => (
463
+ <WorkItem key={index}>
464
+ <WorkHeader>
465
+ <div>
466
+ <WorkTitle>{job.name}</WorkTitle>
467
+ <WorkCompany>{job.position}</WorkCompany>
468
+ </div>
469
+ <WorkDate>
470
+ {job.startDate && (
471
+ <>
472
+ {new Date(job.startDate).getFullYear()} -{' '}
473
+ {job.endDate
474
+ ? new Date(job.endDate).getFullYear()
475
+ : 'PRESENT'}
476
+ </>
477
+ )}
478
+ </WorkDate>
479
+ </WorkHeader>
480
+ {job.summary && (
481
+ <p style={{ marginBottom: '10px', color: '#4a4a4a' }}>
482
+ {job.summary}
483
+ </p>
484
+ )}
485
+ {job.highlights && job.highlights.length > 0 && (
486
+ <WorkDescription>
487
+ {job.highlights.map((highlight, idx) => (
488
+ <li key={idx}>{highlight}</li>
489
+ ))}
490
+ </WorkDescription>
491
+ )}
492
+ </WorkItem>
493
+ ))}
494
+ </MainSection>
495
+ )}
496
+
497
+ {/* Projects Section */}
498
+ {projects.length > 0 && (
499
+ <MainSection>
500
+ <MainSectionTitle>PROJECTS</MainSectionTitle>
501
+ {projects.map((project, index) => (
502
+ <WorkItem key={index}>
503
+ <WorkHeader>
504
+ <div>
505
+ <WorkTitle>{project.name}</WorkTitle>
506
+ {project.url && (
507
+ <WorkCompany>
508
+ <a
509
+ href={safeUrl(project.url)}
510
+ target="_blank"
511
+ rel={getLinkRel(project.url, true)}
512
+ style={{ color: '#1e3a52' }}
513
+ >
514
+ {project.url}
515
+ </a>
516
+ </WorkCompany>
517
+ )}
518
+ </div>
519
+ <WorkDate>
520
+ {project.startDate && (
521
+ <>
522
+ {new Date(project.startDate).getFullYear()}
523
+ {project.endDate &&
524
+ ` - ${new Date(project.endDate).getFullYear()}`}
525
+ </>
526
+ )}
527
+ </WorkDate>
528
+ </WorkHeader>
529
+ {project.description && (
530
+ <p style={{ marginBottom: '10px', color: '#4a4a4a' }}>
531
+ {project.description}
532
+ </p>
533
+ )}
534
+ {project.highlights && project.highlights.length > 0 && (
535
+ <WorkDescription>
536
+ {project.highlights.map((highlight, idx) => (
537
+ <li key={idx}>{highlight}</li>
538
+ ))}
539
+ </WorkDescription>
540
+ )}
541
+ </WorkItem>
542
+ ))}
543
+ </MainSection>
544
+ )}
545
+
546
+ {/* Awards Section */}
547
+ {awards.length > 0 && (
548
+ <MainSection>
549
+ <MainSectionTitle>AWARDS & HONORS</MainSectionTitle>
550
+ {awards.map((award, index) => (
551
+ <div
552
+ key={index}
553
+ style={{ marginBottom: '20px', paddingLeft: '25px' }}
554
+ >
555
+ <WorkTitle>{award.title}</WorkTitle>
556
+ <WorkCompany>
557
+ {award.awarder}
558
+ {award.date &&
559
+ ` - ${new Date(award.date).toLocaleDateString()}`}
560
+ </WorkCompany>
561
+ {award.summary && (
562
+ <p style={{ marginTop: '8px', color: '#4a4a4a' }}>
563
+ {award.summary}
564
+ </p>
565
+ )}
566
+ </div>
567
+ ))}
568
+ </MainSection>
569
+ )}
570
+
571
+ {/* References Section */}
572
+ {references.length > 0 && (
573
+ <MainSection>
574
+ <MainSectionTitle>REFERENCE</MainSectionTitle>
575
+ <ReferenceGrid>
576
+ {references.map((ref, index) => (
577
+ <ReferenceCard key={index}>
578
+ <h3>{ref.name}</h3>
579
+ <div className="title">{ref.reference}</div>
580
+ </ReferenceCard>
581
+ ))}
582
+ </ReferenceGrid>
583
+ </MainSection>
584
+ )}
585
+ </MainContent>
586
+ </Layout>
587
+ );
588
+ }
589
+
590
+ export default Resume;
package/src/index.js ADDED
@@ -0,0 +1,91 @@
1
+ import { renderToString } from 'react-dom/server';
2
+ import { ServerStyleSheet } from 'styled-components';
3
+ import Resume from './Resume.jsx';
4
+
5
+ /**
6
+ * JSON Resume Sidebar Theme
7
+ * Two-column layout with dark sidebar and cream main content
8
+ *
9
+ * @param {Object} resume - JSON Resume object
10
+ * @param {Object} [options] - Rendering options
11
+ * @returns {string} Complete HTML document
12
+ */
13
+ export function render(resume, options = {}) {
14
+ const {
15
+ locale = 'en',
16
+ dir = 'ltr',
17
+ title = resume.basics?.name || 'Resume',
18
+ } = options;
19
+
20
+ const sheet = new ServerStyleSheet();
21
+
22
+ try {
23
+ const html = renderToString(
24
+ sheet.collectStyles(<Resume resume={resume} />)
25
+ );
26
+
27
+ const styles = sheet.getStyleTags();
28
+
29
+ const designTokens = `
30
+ :root {
31
+ --resume-font-sans: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
32
+ --resume-color-sidebar: #1e3a52;
33
+ --resume-color-main: #f5f2ed;
34
+ --resume-color-text: #333;
35
+ --resume-color-accent: #1e3a52;
36
+ }
37
+ `;
38
+
39
+ const globalStyles = `
40
+ * {
41
+ margin: 0;
42
+ padding: 0;
43
+ box-sizing: border-box;
44
+ }
45
+
46
+ body {
47
+ margin: 0;
48
+ -webkit-font-smoothing: antialiased;
49
+ -moz-osx-font-smoothing: grayscale;
50
+ }
51
+
52
+ @media print {
53
+ body {
54
+ background: #fff;
55
+ }
56
+
57
+ @page {
58
+ size: A4;
59
+ margin: 0;
60
+ }
61
+ }
62
+ `;
63
+
64
+ return `<!DOCTYPE html>
65
+ <html lang="${locale}" dir="${dir}">
66
+ <head>
67
+ <meta charset="UTF-8">
68
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
69
+ <title>${title}</title>
70
+
71
+ <style>
72
+ ${designTokens}
73
+ </style>
74
+
75
+ ${styles}
76
+
77
+ <style>
78
+ ${globalStyles}
79
+ </style>
80
+ </head>
81
+ <body>
82
+ ${html}
83
+ </body>
84
+ </html>`;
85
+ } finally {
86
+ sheet.seal();
87
+ }
88
+ }
89
+
90
+ export { Resume };
91
+ export default { render };