jsonresume-theme-art-deco 0.2.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/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ # jsonresume-theme-art-deco
2
+
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 536adbf: Add the Art Deco theme: 1920s Gatsby elegance with a symmetric centered masthead on a midnight-and-emerald jewel panel, metallic-gold accents, geometric chevron and sunburst dividers, and a high-contrast Cinzel didone-style display serif over a Cormorant Garamond body. Renders all JSON Resume sections. (#275)
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2014-2026 JSON Resume contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,16 @@
1
+ # jsonresume-theme-art-deco
2
+
3
+ 1920s Gatsby elegance for JSON Resume. A symmetric centered masthead on a deep
4
+ midnight-to-emerald jewel panel with metallic-gold accents, geometric chevron
5
+ and sunburst dividers, and a high-contrast Cinzel didone-style display serif
6
+ over a graceful Cormorant Garamond body. Ornamental but legible.
7
+
8
+ ## Usage
9
+
10
+ ```bash
11
+ resume export resume.html --theme art-deco
12
+ ```
13
+
14
+ Renders every JSON Resume section: basics/summary, work, education, skills,
15
+ projects, volunteer, awards, certificates, publications, languages, interests,
16
+ and references.
package/index.jsx ADDED
@@ -0,0 +1,24 @@
1
+ import { renderToString } from 'react-dom/server';
2
+ import { ServerStyleSheet } from 'styled-components';
3
+ import Resume from './src/Resume.jsx';
4
+
5
+ export function render(resume) {
6
+ const sheet = new ServerStyleSheet();
7
+ const html = renderToString(sheet.collectStyles(<Resume resume={resume} />));
8
+ const styles = sheet.getStyleTags();
9
+ const title = (resume.basics && resume.basics.name) || 'Resume';
10
+
11
+ return `<!DOCTYPE html>
12
+ <html lang="en">
13
+ <head>
14
+ <meta charset="utf-8">
15
+ <title>${title}</title>
16
+ <meta name="viewport" content="width=device-width, initial-scale=1">
17
+ <link rel="preconnect" href="https://fonts.googleapis.com">
18
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
19
+ <link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@500;700&family=Poiret+One&family=Cormorant+Garamond:ital,wght@0,400;0,500;0,600;1,400&display=swap" rel="stylesheet">
20
+ ${styles}
21
+ </head>
22
+ <body>${html}</body>
23
+ </html>`;
24
+ }
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "jsonresume-theme-art-deco",
3
+ "version": "0.2.0",
4
+ "type": "module",
5
+ "main": "./index.jsx",
6
+ "peerDependencies": {
7
+ "react": "^18.0.0 || ^19.0.0",
8
+ "react-dom": "^18.0.0 || ^19.0.0"
9
+ },
10
+ "dependencies": {
11
+ "styled-components": "^6.1.19",
12
+ "@jsonresume/core": "0.1.0"
13
+ },
14
+ "exports": {
15
+ ".": {
16
+ "import": "./index.jsx",
17
+ "default": "./index.jsx"
18
+ },
19
+ "./dist": {
20
+ "import": "./dist/index.js",
21
+ "default": "./dist/index.js"
22
+ }
23
+ },
24
+ "devDependencies": {
25
+ "@vitejs/plugin-react": "^4.3.4",
26
+ "vite": "^5.4.21"
27
+ },
28
+ "scripts": {
29
+ "build": "vite build"
30
+ }
31
+ }
package/src/Resume.jsx ADDED
@@ -0,0 +1,722 @@
1
+ import React from 'react';
2
+ import styled from 'styled-components';
3
+ import {
4
+ Section,
5
+ SectionTitle,
6
+ DateRange,
7
+ Badge,
8
+ BadgeList,
9
+ ContactInfo,
10
+ Link,
11
+ safeUrl,
12
+ } from '@jsonresume/core';
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // ART DECO — 1920s Gatsby elegance. Symmetric centered masthead, deep jewel
16
+ // (midnight + emerald) panels, metallic-gold accents, geometric chevron /
17
+ // sunburst dividers, and a high-contrast Cinzel didone-style display serif.
18
+ // Ornamental but legible. styled-components are defined INLINE (a separate
19
+ // styles.js crashes the registry webpack with "withConfig undefined").
20
+ // ---------------------------------------------------------------------------
21
+
22
+ const NIGHT = '#0f1418'; // near-black ink panel
23
+ const EMERALD = '#103b34'; // deep jewel green panel
24
+ const GOLD = '#c9a24b'; // metallic gold
25
+ const GOLD_LT = '#e3c98c';
26
+ const CREAM = '#f6f1e6'; // warm paper
27
+ const INK = '#1c1a16'; // body text on cream
28
+
29
+ const Page = styled.div`
30
+ max-width: 880px;
31
+ margin: 0 auto;
32
+ background: ${CREAM};
33
+ color: ${INK};
34
+ font-family: 'Cormorant Garamond', Georgia, 'Times New Roman', serif;
35
+ font-size: 16px;
36
+ line-height: 1.55;
37
+ border: 1px solid ${GOLD};
38
+ box-shadow: inset 0 0 0 6px ${CREAM}, inset 0 0 0 7px ${GOLD};
39
+ `;
40
+
41
+ // --- Geometric chevron / sunburst divider built from CSS only --------------
42
+ const Sunburst = styled.div`
43
+ height: 18px;
44
+ background-image: repeating-linear-gradient(
45
+ 90deg,
46
+ ${GOLD} 0 2px,
47
+ transparent 2px 14px
48
+ );
49
+ opacity: ${(props) => (props.$muted ? 0.5 : 0.85)};
50
+ margin: ${(props) => (props.$tight ? '0' : '0 0 4px 0')};
51
+ `;
52
+
53
+ const Chevron = styled.div`
54
+ text-align: center;
55
+ color: ${GOLD};
56
+ letter-spacing: 8px;
57
+ font-size: 14px;
58
+ margin: 6px 0 0 0;
59
+
60
+ &::before {
61
+ content: '◆ ◇ ◆';
62
+ }
63
+ `;
64
+
65
+ // --- Masthead --------------------------------------------------------------
66
+ const Masthead = styled.header`
67
+ background: linear-gradient(160deg, ${NIGHT} 0%, ${EMERALD} 140%);
68
+ color: ${CREAM};
69
+ text-align: center;
70
+ padding: 48px 40px 38px 40px;
71
+ position: relative;
72
+ border-bottom: 3px double ${GOLD};
73
+ `;
74
+
75
+ const TopRule = styled.div`
76
+ display: flex;
77
+ align-items: center;
78
+ justify-content: center;
79
+ gap: 16px;
80
+ color: ${GOLD};
81
+ letter-spacing: 6px;
82
+ font-family: 'Poiret One', 'Cinzel', serif;
83
+ text-transform: uppercase;
84
+ font-size: 12px;
85
+ margin-bottom: 24px;
86
+
87
+ &::before,
88
+ &::after {
89
+ content: '';
90
+ width: 70px;
91
+ height: 1px;
92
+ background: ${GOLD};
93
+ }
94
+ `;
95
+
96
+ const Name = styled.h1`
97
+ font-family: 'Cinzel', 'Cormorant Garamond', serif;
98
+ font-weight: 700;
99
+ font-size: clamp(34px, 7vw, 58px);
100
+ line-height: 1.04;
101
+ margin: 0;
102
+ letter-spacing: 4px;
103
+ text-transform: uppercase;
104
+ color: ${CREAM};
105
+ text-shadow: 0 1px 0 rgba(201, 162, 75, 0.35);
106
+ `;
107
+
108
+ const Label = styled.div`
109
+ font-family: 'Poiret One', serif;
110
+ font-size: 17px;
111
+ letter-spacing: 5px;
112
+ text-transform: uppercase;
113
+ color: ${GOLD_LT};
114
+ margin-top: 16px;
115
+ `;
116
+
117
+ const MastChevron = styled.div`
118
+ color: ${GOLD};
119
+ letter-spacing: 10px;
120
+ font-size: 15px;
121
+ margin-top: 22px;
122
+
123
+ &::before {
124
+ content: '◆ ◇ ◆ ◇ ◆';
125
+ }
126
+ `;
127
+
128
+ const StyledContactInfo = styled(ContactInfo)`
129
+ justify-content: center;
130
+ gap: 8px 18px;
131
+ margin-top: 22px;
132
+ font-family: 'Poiret One', serif;
133
+ font-size: 13px;
134
+ letter-spacing: 1.5px;
135
+ color: ${GOLD_LT};
136
+
137
+ a,
138
+ span {
139
+ color: ${GOLD_LT};
140
+ text-decoration: none;
141
+ }
142
+
143
+ a:hover {
144
+ color: ${CREAM};
145
+ text-decoration: underline;
146
+ }
147
+ `;
148
+
149
+ // --- Body ------------------------------------------------------------------
150
+ const Body = styled.main`
151
+ padding: 40px 46px 48px 46px;
152
+ `;
153
+
154
+ const Summary = styled.p`
155
+ text-align: center;
156
+ font-size: 19px;
157
+ font-style: italic;
158
+ line-height: 1.6;
159
+ color: ${INK};
160
+ max-width: 60ch;
161
+ margin: 0 auto 16px auto;
162
+ `;
163
+
164
+ const Block = styled(Section)`
165
+ margin-top: 38px;
166
+
167
+ &:first-of-type {
168
+ margin-top: 8px;
169
+ }
170
+ `;
171
+
172
+ const Heading = styled(SectionTitle)`
173
+ font-family: 'Cinzel', serif;
174
+ font-weight: 700;
175
+ font-size: 22px;
176
+ letter-spacing: 4px;
177
+ text-transform: uppercase;
178
+ text-align: center;
179
+ color: ${EMERALD};
180
+ margin: 0 0 6px 0;
181
+ `;
182
+
183
+ const HeadingRule = styled.div`
184
+ width: 100%;
185
+ text-align: center;
186
+ margin: 0 0 24px 0;
187
+ `;
188
+
189
+ const Entry = styled.div`
190
+ margin-bottom: 26px;
191
+ padding-bottom: 22px;
192
+ border-bottom: 1px solid rgba(201, 162, 75, 0.4);
193
+
194
+ &:last-child {
195
+ margin-bottom: 0;
196
+ padding-bottom: 0;
197
+ border-bottom: none;
198
+ }
199
+ `;
200
+
201
+ const EntryHead = styled.div`
202
+ text-align: center;
203
+ margin-bottom: 6px;
204
+ `;
205
+
206
+ const EntryTitle = styled.h3`
207
+ font-family: 'Cinzel', serif;
208
+ font-weight: 500;
209
+ font-size: 20px;
210
+ letter-spacing: 1px;
211
+ margin: 0;
212
+ color: ${NIGHT};
213
+
214
+ a {
215
+ color: ${EMERALD};
216
+ text-decoration: none;
217
+ border-bottom: 1px solid ${GOLD};
218
+ }
219
+ `;
220
+
221
+ const EntrySub = styled.div`
222
+ font-family: 'Poiret One', serif;
223
+ font-size: 14px;
224
+ letter-spacing: 2px;
225
+ text-transform: uppercase;
226
+ color: ${GOLD};
227
+ margin-top: 6px;
228
+ `;
229
+
230
+ const EntryMeta = styled.div`
231
+ font-family: 'Poiret One', serif;
232
+ font-size: 13px;
233
+ letter-spacing: 2px;
234
+ text-transform: uppercase;
235
+ color: #6b6552;
236
+ margin-top: 4px;
237
+ `;
238
+
239
+ const Para = styled.p`
240
+ margin: 12px 0 0 0;
241
+ font-size: 16px;
242
+ line-height: 1.6;
243
+ text-align: center;
244
+ max-width: 62ch;
245
+ margin-left: auto;
246
+ margin-right: auto;
247
+ `;
248
+
249
+ const Bullets = styled.ul`
250
+ list-style: none;
251
+ margin: 14px auto 0 auto;
252
+ padding: 0;
253
+ max-width: 62ch;
254
+
255
+ li {
256
+ position: relative;
257
+ padding-left: 22px;
258
+ margin-bottom: 8px;
259
+ line-height: 1.55;
260
+
261
+ &::before {
262
+ content: '◆';
263
+ position: absolute;
264
+ left: 0;
265
+ top: 1px;
266
+ color: ${GOLD};
267
+ font-size: 11px;
268
+ }
269
+ }
270
+ `;
271
+
272
+ const SkillsWrap = styled.div`
273
+ display: grid;
274
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
275
+ gap: 22px;
276
+ text-align: center;
277
+ `;
278
+
279
+ const SkillGroup = styled.div`
280
+ h4 {
281
+ font-family: 'Cinzel', serif;
282
+ font-weight: 500;
283
+ font-size: 16px;
284
+ letter-spacing: 2px;
285
+ text-transform: uppercase;
286
+ color: ${EMERALD};
287
+ margin: 0 0 10px 0;
288
+ }
289
+ `;
290
+
291
+ const StyledBadgeList = styled(BadgeList)`
292
+ display: flex;
293
+ flex-wrap: wrap;
294
+ justify-content: center;
295
+ gap: 8px;
296
+ `;
297
+
298
+ const StyledBadge = styled(Badge)`
299
+ font-family: 'Poiret One', serif;
300
+ font-size: 13px;
301
+ letter-spacing: 1px;
302
+ background: transparent;
303
+ color: ${INK};
304
+ border: 1px solid ${GOLD};
305
+ border-radius: 0;
306
+ padding: 4px 12px;
307
+ `;
308
+
309
+ const Cards = styled.div`
310
+ display: grid;
311
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
312
+ gap: 22px;
313
+ `;
314
+
315
+ const Card = styled.div`
316
+ text-align: center;
317
+ padding: 18px 18px;
318
+ border: 1px solid rgba(201, 162, 75, 0.55);
319
+ background: rgba(201, 162, 75, 0.06);
320
+
321
+ h4 {
322
+ font-family: 'Cinzel', serif;
323
+ font-weight: 500;
324
+ font-size: 17px;
325
+ letter-spacing: 1px;
326
+ margin: 0 0 8px 0;
327
+ color: ${NIGHT};
328
+
329
+ a {
330
+ color: ${EMERALD};
331
+ text-decoration: none;
332
+ border-bottom: 1px solid ${GOLD};
333
+ }
334
+ }
335
+
336
+ p {
337
+ margin: 6px 0 0 0;
338
+ font-size: 15px;
339
+
340
+ &:first-of-type {
341
+ margin-top: 0;
342
+ }
343
+ }
344
+ `;
345
+
346
+ const CardMeta = styled.div`
347
+ font-family: 'Poiret One', serif;
348
+ font-size: 12px;
349
+ letter-spacing: 2px;
350
+ text-transform: uppercase;
351
+ color: ${GOLD};
352
+ margin-bottom: 8px;
353
+ `;
354
+
355
+ function SectionRule({ children, withTop = false }) {
356
+ return (
357
+ <>
358
+ {withTop && <MastChevron aria-hidden="true" />}
359
+ <HeadingRule>
360
+ <Sunburst aria-hidden="true" />
361
+ {children}
362
+ <Chevron aria-hidden="true" />
363
+ </HeadingRule>
364
+ </>
365
+ );
366
+ }
367
+
368
+ function Resume({ resume }) {
369
+ const {
370
+ basics = {},
371
+ work = [],
372
+ education = [],
373
+ skills = [],
374
+ projects = [],
375
+ volunteer = [],
376
+ awards = [],
377
+ certificates = [],
378
+ publications = [],
379
+ languages = [],
380
+ interests = [],
381
+ references = [],
382
+ } = resume || {};
383
+
384
+ return (
385
+ <Page>
386
+ <Masthead>
387
+ <TopRule>Curriculum Vitae</TopRule>
388
+ {basics.name && <Name>{basics.name}</Name>}
389
+ {basics.label && <Label>{basics.label}</Label>}
390
+ <MastChevron aria-hidden="true" />
391
+ {(basics.email ||
392
+ basics.phone ||
393
+ basics.url ||
394
+ (basics.profiles && basics.profiles.length > 0)) && (
395
+ <StyledContactInfo basics={basics} separator="◆" />
396
+ )}
397
+ </Masthead>
398
+
399
+ <Body>
400
+ {basics.summary && <Summary>{basics.summary}</Summary>}
401
+
402
+ {work.length > 0 && (
403
+ <Block>
404
+ <Heading>Experience</Heading>
405
+ <SectionRule />
406
+ {work.map((job, i) => (
407
+ <Entry key={i}>
408
+ <EntryHead>
409
+ <EntryTitle>{job.position || job.name || 'Role'}</EntryTitle>
410
+ {job.name && job.position && (
411
+ <EntrySub>
412
+ {job.url ? (
413
+ <Link href={safeUrl(job.url)}>{job.name}</Link>
414
+ ) : (
415
+ job.name
416
+ )}
417
+ </EntrySub>
418
+ )}
419
+ {(job.startDate || job.endDate || job.location) && (
420
+ <EntryMeta>
421
+ {(job.startDate || job.endDate) && (
422
+ <DateRange
423
+ startDate={job.startDate}
424
+ endDate={job.endDate}
425
+ />
426
+ )}
427
+ {job.location ? ` · ${job.location}` : ''}
428
+ </EntryMeta>
429
+ )}
430
+ </EntryHead>
431
+ {job.summary && <Para>{job.summary}</Para>}
432
+ {job.highlights && job.highlights.length > 0 && (
433
+ <Bullets>
434
+ {job.highlights.map((h, hi) => (
435
+ <li key={hi}>{h}</li>
436
+ ))}
437
+ </Bullets>
438
+ )}
439
+ </Entry>
440
+ ))}
441
+ </Block>
442
+ )}
443
+
444
+ {education.length > 0 && (
445
+ <Block>
446
+ <Heading>Education</Heading>
447
+ <SectionRule />
448
+ {education.map((edu, i) => (
449
+ <Entry key={i}>
450
+ <EntryHead>
451
+ <EntryTitle>{edu.institution || 'Institution'}</EntryTitle>
452
+ {(edu.studyType || edu.area) && (
453
+ <EntrySub>
454
+ {[edu.studyType, edu.area].filter(Boolean).join(' · ')}
455
+ </EntrySub>
456
+ )}
457
+ {(edu.startDate || edu.endDate) && (
458
+ <EntryMeta>
459
+ <DateRange
460
+ startDate={edu.startDate}
461
+ endDate={edu.endDate}
462
+ />
463
+ </EntryMeta>
464
+ )}
465
+ </EntryHead>
466
+ {edu.score && <Para>GPA: {edu.score}</Para>}
467
+ {edu.courses && edu.courses.length > 0 && (
468
+ <Bullets>
469
+ {edu.courses.map((c, ci) => (
470
+ <li key={ci}>{c}</li>
471
+ ))}
472
+ </Bullets>
473
+ )}
474
+ </Entry>
475
+ ))}
476
+ </Block>
477
+ )}
478
+
479
+ {skills.length > 0 && (
480
+ <Block>
481
+ <Heading>Skills</Heading>
482
+ <SectionRule />
483
+ <SkillsWrap>
484
+ {skills.map((skill, i) => (
485
+ <SkillGroup key={i}>
486
+ <h4>
487
+ {skill.name || 'Skills'}
488
+ {skill.level ? ` · ${skill.level}` : ''}
489
+ </h4>
490
+ {skill.keywords && skill.keywords.length > 0 && (
491
+ <StyledBadgeList>
492
+ {skill.keywords.map((kw, ki) => (
493
+ <StyledBadge key={ki}>{kw}</StyledBadge>
494
+ ))}
495
+ </StyledBadgeList>
496
+ )}
497
+ </SkillGroup>
498
+ ))}
499
+ </SkillsWrap>
500
+ </Block>
501
+ )}
502
+
503
+ {projects.length > 0 && (
504
+ <Block>
505
+ <Heading>Projects</Heading>
506
+ <SectionRule />
507
+ {projects.map((project, i) => (
508
+ <Entry key={i}>
509
+ <EntryHead>
510
+ <EntryTitle>
511
+ {project.url ? (
512
+ <Link href={safeUrl(project.url)}>
513
+ {project.name || 'Project'}
514
+ </Link>
515
+ ) : (
516
+ project.name || 'Project'
517
+ )}
518
+ </EntryTitle>
519
+ {(project.entity || project.type) && (
520
+ <EntrySub>
521
+ {[project.entity, project.type]
522
+ .filter(Boolean)
523
+ .join(' · ')}
524
+ </EntrySub>
525
+ )}
526
+ {(project.startDate || project.endDate) && (
527
+ <EntryMeta>
528
+ <DateRange
529
+ startDate={project.startDate}
530
+ endDate={project.endDate}
531
+ />
532
+ </EntryMeta>
533
+ )}
534
+ </EntryHead>
535
+ {project.description && <Para>{project.description}</Para>}
536
+ {project.highlights && project.highlights.length > 0 && (
537
+ <Bullets>
538
+ {project.highlights.map((h, hi) => (
539
+ <li key={hi}>{h}</li>
540
+ ))}
541
+ </Bullets>
542
+ )}
543
+ {project.keywords && project.keywords.length > 0 && (
544
+ <StyledBadgeList style={{ marginTop: '14px' }}>
545
+ {project.keywords.map((kw, ki) => (
546
+ <StyledBadge key={ki}>{kw}</StyledBadge>
547
+ ))}
548
+ </StyledBadgeList>
549
+ )}
550
+ </Entry>
551
+ ))}
552
+ </Block>
553
+ )}
554
+
555
+ {volunteer.length > 0 && (
556
+ <Block>
557
+ <Heading>Volunteer</Heading>
558
+ <SectionRule />
559
+ {volunteer.map((vol, i) => (
560
+ <Entry key={i}>
561
+ <EntryHead>
562
+ <EntryTitle>{vol.position || 'Volunteer'}</EntryTitle>
563
+ {vol.organization && (
564
+ <EntrySub>
565
+ {vol.url ? (
566
+ <Link href={safeUrl(vol.url)}>{vol.organization}</Link>
567
+ ) : (
568
+ vol.organization
569
+ )}
570
+ </EntrySub>
571
+ )}
572
+ {(vol.startDate || vol.endDate) && (
573
+ <EntryMeta>
574
+ <DateRange
575
+ startDate={vol.startDate}
576
+ endDate={vol.endDate}
577
+ />
578
+ </EntryMeta>
579
+ )}
580
+ </EntryHead>
581
+ {vol.summary && <Para>{vol.summary}</Para>}
582
+ {vol.highlights && vol.highlights.length > 0 && (
583
+ <Bullets>
584
+ {vol.highlights.map((h, hi) => (
585
+ <li key={hi}>{h}</li>
586
+ ))}
587
+ </Bullets>
588
+ )}
589
+ </Entry>
590
+ ))}
591
+ </Block>
592
+ )}
593
+
594
+ {awards.length > 0 && (
595
+ <Block>
596
+ <Heading>Awards</Heading>
597
+ <SectionRule />
598
+ <Cards>
599
+ {awards.map((award, i) => (
600
+ <Card key={i}>
601
+ <h4>{award.title || 'Award'}</h4>
602
+ {(award.awarder || award.date) && (
603
+ <CardMeta>
604
+ {[award.awarder, award.date].filter(Boolean).join(' · ')}
605
+ </CardMeta>
606
+ )}
607
+ {award.summary && <p>{award.summary}</p>}
608
+ </Card>
609
+ ))}
610
+ </Cards>
611
+ </Block>
612
+ )}
613
+
614
+ {certificates.length > 0 && (
615
+ <Block>
616
+ <Heading>Certificates</Heading>
617
+ <SectionRule />
618
+ <Cards>
619
+ {certificates.map((cert, i) => (
620
+ <Card key={i}>
621
+ <h4>
622
+ {cert.url ? (
623
+ <Link href={safeUrl(cert.url)}>
624
+ {cert.name || 'Certificate'}
625
+ </Link>
626
+ ) : (
627
+ cert.name || 'Certificate'
628
+ )}
629
+ </h4>
630
+ {(cert.issuer || cert.date) && (
631
+ <CardMeta>
632
+ {[cert.issuer, cert.date].filter(Boolean).join(' · ')}
633
+ </CardMeta>
634
+ )}
635
+ </Card>
636
+ ))}
637
+ </Cards>
638
+ </Block>
639
+ )}
640
+
641
+ {publications.length > 0 && (
642
+ <Block>
643
+ <Heading>Publications</Heading>
644
+ <SectionRule />
645
+ <Cards>
646
+ {publications.map((pub, i) => (
647
+ <Card key={i}>
648
+ <h4>
649
+ {pub.url ? (
650
+ <Link href={safeUrl(pub.url)}>
651
+ {pub.name || 'Publication'}
652
+ </Link>
653
+ ) : (
654
+ pub.name || 'Publication'
655
+ )}
656
+ </h4>
657
+ {(pub.publisher || pub.releaseDate) && (
658
+ <CardMeta>
659
+ {[pub.publisher, pub.releaseDate]
660
+ .filter(Boolean)
661
+ .join(' · ')}
662
+ </CardMeta>
663
+ )}
664
+ {pub.summary && <p>{pub.summary}</p>}
665
+ </Card>
666
+ ))}
667
+ </Cards>
668
+ </Block>
669
+ )}
670
+
671
+ {languages.length > 0 && (
672
+ <Block>
673
+ <Heading>Languages</Heading>
674
+ <SectionRule />
675
+ <StyledBadgeList>
676
+ {languages.map((lang, i) => (
677
+ <StyledBadge key={i}>
678
+ {lang.language || 'Language'}
679
+ {lang.fluency ? ` · ${lang.fluency}` : ''}
680
+ </StyledBadge>
681
+ ))}
682
+ </StyledBadgeList>
683
+ </Block>
684
+ )}
685
+
686
+ {interests.length > 0 && (
687
+ <Block>
688
+ <Heading>Interests</Heading>
689
+ <SectionRule />
690
+ <Cards>
691
+ {interests.map((interest, i) => (
692
+ <Card key={i}>
693
+ <h4>{interest.name || 'Interest'}</h4>
694
+ {interest.keywords && interest.keywords.length > 0 && (
695
+ <p>{interest.keywords.join(', ')}</p>
696
+ )}
697
+ </Card>
698
+ ))}
699
+ </Cards>
700
+ </Block>
701
+ )}
702
+
703
+ {references.length > 0 && (
704
+ <Block>
705
+ <Heading>References</Heading>
706
+ <SectionRule />
707
+ <Cards>
708
+ {references.map((ref, i) => (
709
+ <Card key={i}>
710
+ <h4>{ref.name || 'Reference'}</h4>
711
+ {ref.reference && <p>{ref.reference}</p>}
712
+ </Card>
713
+ ))}
714
+ </Cards>
715
+ </Block>
716
+ )}
717
+ </Body>
718
+ </Page>
719
+ );
720
+ }
721
+
722
+ export default Resume;
package/vite.config.js ADDED
@@ -0,0 +1,33 @@
1
+ import { defineConfig } from 'vite';
2
+ import react from '@vitejs/plugin-react';
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ build: {
7
+ ssr: true,
8
+ target: 'node18',
9
+ outDir: './dist',
10
+ emptyOutDir: true,
11
+ minify: false,
12
+ lib: {
13
+ entry: './index.jsx',
14
+ formats: ['es'],
15
+ fileName: 'index',
16
+ },
17
+ rollupOptions: {
18
+ external: ['react', 'react-dom', 'react-dom/server', 'react/jsx-runtime'],
19
+ output: {
20
+ exports: 'named',
21
+ },
22
+ },
23
+ },
24
+ ssr: {
25
+ // Force bundle these packages instead of externalizing
26
+ noExternal: [
27
+ 'styled-components',
28
+ '@emotion/is-prop-valid',
29
+ 'stylis',
30
+ 'shallowequal',
31
+ ],
32
+ },
33
+ });