jsonresume-theme-industrial-engineer 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,20 @@
1
+ # jsonresume-theme-industrial-engineer
2
+
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - b869586: Add new theme `jsonresume-theme-industrial-engineer`: an engineering-datasheet
8
+ aesthetic with a precise grid background, IBM Plex Mono technical labels, a
9
+ spec-table contact block, part-number section headers, dimension-line rules,
10
+ and a drafting-blue / safety-orange palette. Renders all JSON Resume sections.
11
+ Refs #275.
12
+
13
+ ## 0.1.0
14
+
15
+ ### Minor Changes
16
+
17
+ - Initial release. Industrial / technical-spec aesthetic styled like an engineering
18
+ datasheet: precise grid, technical monospace labels, spec-table layouts,
19
+ drafting-blue accent with safety-orange highlights, dimension-line rules, and
20
+ part-number-style section headers. Renders all JSON Resume sections.
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/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=IBM+Plex+Mono:wght@400;500;600;700&family=IBM+Plex+Sans:wght@400;500;600;700&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-industrial-engineer",
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,959 @@
1
+ import React from 'react';
2
+ import styled from 'styled-components';
3
+ import { DateRange, safeUrl } from '@jsonresume/core';
4
+
5
+ /* ============================================================
6
+ INDUSTRIAL ENGINEER — engineering-datasheet aesthetic.
7
+ Drafting-blue + safety-orange. IBM Plex Mono technical labels,
8
+ IBM Plex Sans body. Spec-table layouts, dimension-line rules,
9
+ part-number section headers. ALL styled-components inline
10
+ (separate styles.js breaks registry webpack default export).
11
+ Never name a styled-component `Date` (shadows global → crash).
12
+ ============================================================ */
13
+
14
+ const INK = '#16202b';
15
+ const BLUE = '#1b3a5b';
16
+ const BLUE_MID = '#2f5d8a';
17
+ const ORANGE = '#d6480b';
18
+ const PAPER = '#f4f6f8';
19
+ const PANEL = '#ffffff';
20
+ const GRID = '#d4dde4';
21
+ const MUTE = '#5d6b78';
22
+
23
+ const MONO = "'IBM Plex Mono', 'SFMono-Regular', Menlo, monospace";
24
+ const SANS = "'IBM Plex Sans', -apple-system, BlinkMacSystemFont, sans-serif";
25
+
26
+ const Sheet = styled.div`
27
+ max-width: 940px;
28
+ margin: 0 auto;
29
+ background: ${PAPER};
30
+ color: ${INK};
31
+ font-family: ${SANS};
32
+ font-size: 13.5px;
33
+ line-height: 1.55;
34
+ padding: 40px;
35
+ background-image: linear-gradient(${GRID} 1px, transparent 1px),
36
+ linear-gradient(90deg, ${GRID} 1px, transparent 1px);
37
+ background-size: 28px 28px;
38
+ background-position: -1px -1px;
39
+
40
+ @media print {
41
+ background: #fff;
42
+ padding: 0;
43
+ }
44
+ `;
45
+
46
+ const Frame = styled.div`
47
+ border: 2px solid ${BLUE};
48
+ background: ${PANEL};
49
+ position: relative;
50
+ `;
51
+
52
+ /* drafting corner ticks on the outer frame */
53
+ const CornerTick = styled.span`
54
+ position: absolute;
55
+ width: 12px;
56
+ height: 12px;
57
+ border-color: ${ORANGE};
58
+ border-style: solid;
59
+ border-width: 0;
60
+ &.tl {
61
+ top: -2px;
62
+ left: -2px;
63
+ border-top-width: 3px;
64
+ border-left-width: 3px;
65
+ }
66
+ &.tr {
67
+ top: -2px;
68
+ right: -2px;
69
+ border-top-width: 3px;
70
+ border-right-width: 3px;
71
+ }
72
+ &.bl {
73
+ bottom: -2px;
74
+ left: -2px;
75
+ border-bottom-width: 3px;
76
+ border-left-width: 3px;
77
+ }
78
+ &.br {
79
+ bottom: -2px;
80
+ right: -2px;
81
+ border-bottom-width: 3px;
82
+ border-right-width: 3px;
83
+ }
84
+ `;
85
+
86
+ /* ---- title-block header, like a drawing's nameplate ---- */
87
+ const TitleBlock = styled.header`
88
+ display: grid;
89
+ grid-template-columns: ${(p) => (p.$hasImage ? '96px 1fr' : '1fr')};
90
+ gap: 20px;
91
+ align-items: stretch;
92
+ border-bottom: 2px solid ${BLUE};
93
+ background: ${BLUE};
94
+ color: #eef4f9;
95
+ padding: 22px 24px;
96
+ `;
97
+
98
+ const Portrait = styled.img`
99
+ width: 96px;
100
+ height: 96px;
101
+ object-fit: cover;
102
+ border: 2px solid ${ORANGE};
103
+ filter: grayscale(0.35) contrast(1.05);
104
+ background: #0e1c29;
105
+ `;
106
+
107
+ const TitleMain = styled.div`
108
+ display: flex;
109
+ flex-direction: column;
110
+ justify-content: center;
111
+ min-width: 0;
112
+ `;
113
+
114
+ const DocTag = styled.div`
115
+ font-family: ${MONO};
116
+ font-size: 10.5px;
117
+ letter-spacing: 2px;
118
+ text-transform: uppercase;
119
+ color: #f2954f;
120
+ margin-bottom: 8px;
121
+ display: flex;
122
+ gap: 14px;
123
+ flex-wrap: wrap;
124
+
125
+ span {
126
+ position: relative;
127
+ padding-left: 12px;
128
+ &::before {
129
+ content: '';
130
+ position: absolute;
131
+ left: 0;
132
+ top: 50%;
133
+ width: 5px;
134
+ height: 5px;
135
+ margin-top: -2.5px;
136
+ background: ${ORANGE};
137
+ }
138
+ }
139
+ `;
140
+
141
+ const Name = styled.h1`
142
+ font-family: ${MONO};
143
+ font-size: 30px;
144
+ font-weight: 700;
145
+ letter-spacing: 1px;
146
+ text-transform: uppercase;
147
+ margin: 0;
148
+ line-height: 1.05;
149
+ color: #ffffff;
150
+ `;
151
+
152
+ const Label = styled.div`
153
+ font-size: 14px;
154
+ font-weight: 500;
155
+ color: #b9cee0;
156
+ margin-top: 6px;
157
+ letter-spacing: 0.3px;
158
+ `;
159
+
160
+ /* ---- spec table for contact: key | value rows ---- */
161
+ const SpecBar = styled.div`
162
+ display: grid;
163
+ grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
164
+ border-bottom: 2px solid ${BLUE};
165
+ `;
166
+
167
+ const SpecCell = styled.div`
168
+ padding: 8px 14px;
169
+ border-right: 1px solid ${GRID};
170
+ border-bottom: 1px solid ${GRID};
171
+ &:last-child {
172
+ border-right: none;
173
+ }
174
+ `;
175
+
176
+ const SpecKey = styled.div`
177
+ font-family: ${MONO};
178
+ font-size: 9.5px;
179
+ letter-spacing: 1.5px;
180
+ text-transform: uppercase;
181
+ color: ${MUTE};
182
+ `;
183
+
184
+ const SpecVal = styled.div`
185
+ font-family: ${MONO};
186
+ font-size: 12px;
187
+ font-weight: 500;
188
+ color: ${INK};
189
+ word-break: break-word;
190
+ a {
191
+ color: ${BLUE_MID};
192
+ text-decoration: none;
193
+ &:hover {
194
+ color: ${ORANGE};
195
+ }
196
+ }
197
+ `;
198
+
199
+ const Body = styled.div`
200
+ padding: 26px 28px 30px;
201
+ `;
202
+
203
+ /* ---- part-number section headers ---- */
204
+ const Section = styled.section`
205
+ margin-bottom: 26px;
206
+ &:last-child {
207
+ margin-bottom: 0;
208
+ }
209
+ `;
210
+
211
+ const SectionHead = styled.div`
212
+ display: flex;
213
+ align-items: center;
214
+ gap: 12px;
215
+ margin-bottom: 16px;
216
+ `;
217
+
218
+ const PartNo = styled.span`
219
+ font-family: ${MONO};
220
+ font-size: 11px;
221
+ font-weight: 700;
222
+ color: #fff;
223
+ background: ${ORANGE};
224
+ padding: 3px 8px;
225
+ letter-spacing: 1px;
226
+ flex: none;
227
+ `;
228
+
229
+ const SectionTitle = styled.h2`
230
+ font-family: ${MONO};
231
+ font-size: 16px;
232
+ font-weight: 700;
233
+ letter-spacing: 1.5px;
234
+ text-transform: uppercase;
235
+ color: ${BLUE};
236
+ margin: 0;
237
+ white-space: nowrap;
238
+ `;
239
+
240
+ /* dimension line: rule with end ticks */
241
+ const DimLine = styled.span`
242
+ flex: 1;
243
+ height: 0;
244
+ border-top: 1px solid ${BLUE};
245
+ position: relative;
246
+ &::before,
247
+ &::after {
248
+ content: '';
249
+ position: absolute;
250
+ top: -4px;
251
+ width: 1px;
252
+ height: 9px;
253
+ background: ${BLUE};
254
+ }
255
+ &::before {
256
+ left: 0;
257
+ }
258
+ &::after {
259
+ right: 0;
260
+ }
261
+ `;
262
+
263
+ /* ---- experience / timeline entries as data records ---- */
264
+ const Record = styled.article`
265
+ display: grid;
266
+ grid-template-columns: 150px 1fr;
267
+ border: 1px solid ${GRID};
268
+ border-left: 3px solid ${BLUE_MID};
269
+ margin-bottom: 12px;
270
+ background: ${PANEL};
271
+ &:last-child {
272
+ margin-bottom: 0;
273
+ }
274
+ @media (max-width: 640px) {
275
+ grid-template-columns: 1fr;
276
+ }
277
+ `;
278
+
279
+ const RecordMeta = styled.div`
280
+ padding: 12px 14px;
281
+ border-right: 1px solid ${GRID};
282
+ background: ${PAPER};
283
+ display: flex;
284
+ flex-direction: column;
285
+ gap: 6px;
286
+ @media (max-width: 640px) {
287
+ border-right: none;
288
+ border-bottom: 1px solid ${GRID};
289
+ }
290
+ `;
291
+
292
+ const MetaDate = styled.div`
293
+ font-family: ${MONO};
294
+ font-size: 11px;
295
+ font-weight: 600;
296
+ color: ${BLUE};
297
+ font-variant-numeric: tabular-nums;
298
+ span {
299
+ color: ${BLUE} !important;
300
+ }
301
+ `;
302
+
303
+ const MetaTag = styled.div`
304
+ font-family: ${MONO};
305
+ font-size: 9.5px;
306
+ letter-spacing: 1px;
307
+ text-transform: uppercase;
308
+ color: ${MUTE};
309
+ `;
310
+
311
+ const RecordBody = styled.div`
312
+ padding: 12px 16px;
313
+ min-width: 0;
314
+ `;
315
+
316
+ const RoleTitle = styled.h3`
317
+ font-size: 15px;
318
+ font-weight: 700;
319
+ margin: 0;
320
+ color: ${INK};
321
+ a {
322
+ color: ${INK};
323
+ text-decoration: none;
324
+ border-bottom: 1px dotted ${ORANGE};
325
+ &:hover {
326
+ color: ${ORANGE};
327
+ }
328
+ }
329
+ `;
330
+
331
+ const RoleOrg = styled.div`
332
+ font-family: ${MONO};
333
+ font-size: 12px;
334
+ font-weight: 500;
335
+ color: ${BLUE_MID};
336
+ margin-top: 3px;
337
+ letter-spacing: 0.3px;
338
+ `;
339
+
340
+ const Para = styled.p`
341
+ font-size: 13px;
342
+ color: #2c3845;
343
+ margin: 8px 0 0 0;
344
+ `;
345
+
346
+ const Highlights = styled.ul`
347
+ margin: 8px 0 0 0;
348
+ padding: 0;
349
+ list-style: none;
350
+ li {
351
+ position: relative;
352
+ padding-left: 18px;
353
+ margin-bottom: 5px;
354
+ font-size: 12.5px;
355
+ line-height: 1.5;
356
+ &::before {
357
+ content: '▸';
358
+ position: absolute;
359
+ left: 0;
360
+ color: ${ORANGE};
361
+ font-size: 11px;
362
+ top: 1px;
363
+ }
364
+ }
365
+ `;
366
+
367
+ const SummaryBlock = styled.div`
368
+ position: relative;
369
+ margin: 0 0 26px 0;
370
+ padding: 18px 16px 14px;
371
+ border: 1px dashed ${BLUE_MID};
372
+ background: ${PAPER};
373
+ `;
374
+
375
+ const SummaryLabel = styled.span`
376
+ position: absolute;
377
+ top: -8px;
378
+ left: 12px;
379
+ font-family: ${MONO};
380
+ font-size: 9.5px;
381
+ font-weight: 700;
382
+ letter-spacing: 2px;
383
+ text-transform: uppercase;
384
+ color: ${BLUE};
385
+ background: ${PANEL};
386
+ padding: 0 6px;
387
+ `;
388
+
389
+ const SummaryText = styled.p`
390
+ font-size: 13.5px;
391
+ line-height: 1.6;
392
+ color: #28333f;
393
+ margin: 0;
394
+ `;
395
+
396
+ /* ---- skills as a spec matrix ---- */
397
+ const SkillMatrix = styled.div`
398
+ display: grid;
399
+ grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
400
+ gap: 10px;
401
+ `;
402
+
403
+ const SkillCell = styled.div`
404
+ border: 1px solid ${GRID};
405
+ border-top: 3px solid ${BLUE};
406
+ background: ${PANEL};
407
+ padding: 10px 12px;
408
+ `;
409
+
410
+ const SkillName = styled.div`
411
+ font-family: ${MONO};
412
+ font-size: 12px;
413
+ font-weight: 700;
414
+ text-transform: uppercase;
415
+ letter-spacing: 0.8px;
416
+ color: ${BLUE};
417
+ margin-bottom: 8px;
418
+ display: flex;
419
+ justify-content: space-between;
420
+ align-items: baseline;
421
+ gap: 8px;
422
+ span {
423
+ font-size: 9.5px;
424
+ color: ${MUTE};
425
+ letter-spacing: 1px;
426
+ }
427
+ `;
428
+
429
+ const ChipRow = styled.div`
430
+ display: flex;
431
+ flex-wrap: wrap;
432
+ gap: 6px;
433
+ `;
434
+
435
+ const Chip = styled.span`
436
+ font-family: ${MONO};
437
+ font-size: 11px;
438
+ padding: 2px 7px;
439
+ border: 1px solid ${GRID};
440
+ background: ${PAPER};
441
+ color: #33414e;
442
+ letter-spacing: 0.2px;
443
+ `;
444
+
445
+ /* ---- compact data cards for awards/certs/pubs/refs/interests ---- */
446
+ const CardGrid = styled.div`
447
+ display: grid;
448
+ grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
449
+ gap: 10px;
450
+ `;
451
+
452
+ const Card = styled.div`
453
+ border: 1px solid ${GRID};
454
+ border-left: 3px solid ${ORANGE};
455
+ background: ${PANEL};
456
+ padding: 11px 13px;
457
+ h4 {
458
+ font-size: 13.5px;
459
+ font-weight: 700;
460
+ margin: 0 0 3px 0;
461
+ color: ${INK};
462
+ a {
463
+ color: ${INK};
464
+ text-decoration: none;
465
+ border-bottom: 1px dotted ${ORANGE};
466
+ &:hover {
467
+ color: ${ORANGE};
468
+ }
469
+ }
470
+ }
471
+ .src {
472
+ font-family: ${MONO};
473
+ font-size: 10.5px;
474
+ letter-spacing: 0.5px;
475
+ color: ${BLUE_MID};
476
+ }
477
+ p {
478
+ font-size: 12.5px;
479
+ color: #2c3845;
480
+ margin: 6px 0 0 0;
481
+ }
482
+ `;
483
+
484
+ /* ---- languages / interests as inline spec ---- */
485
+ const InlineList = styled.div`
486
+ display: flex;
487
+ flex-wrap: wrap;
488
+ gap: 8px;
489
+ `;
490
+
491
+ const InlineItem = styled.span`
492
+ font-family: ${MONO};
493
+ font-size: 11.5px;
494
+ border: 1px solid ${GRID};
495
+ background: ${PANEL};
496
+ padding: 5px 10px;
497
+ color: #33414e;
498
+ b {
499
+ color: ${BLUE};
500
+ }
501
+ small {
502
+ color: ${MUTE};
503
+ font-size: 10px;
504
+ margin-left: 4px;
505
+ }
506
+ `;
507
+
508
+ const Footer = styled.div`
509
+ border-top: 2px solid ${BLUE};
510
+ padding: 8px 16px;
511
+ font-family: ${MONO};
512
+ font-size: 9.5px;
513
+ letter-spacing: 1.5px;
514
+ text-transform: uppercase;
515
+ color: ${MUTE};
516
+ display: flex;
517
+ justify-content: space-between;
518
+ flex-wrap: wrap;
519
+ gap: 8px;
520
+ `;
521
+
522
+ function pad(n) {
523
+ return String(n).padStart(2, '0');
524
+ }
525
+
526
+ function locationString(location) {
527
+ if (!location) return '';
528
+ return [location.city, location.region, location.countryCode]
529
+ .filter(Boolean)
530
+ .join(', ');
531
+ }
532
+
533
+ function SectionHeader({ no, title }) {
534
+ return (
535
+ <SectionHead>
536
+ <PartNo>{no}</PartNo>
537
+ <SectionTitle>{title}</SectionTitle>
538
+ <DimLine aria-hidden="true" />
539
+ </SectionHead>
540
+ );
541
+ }
542
+
543
+ function Resume({ resume }) {
544
+ const {
545
+ basics = {},
546
+ work = [],
547
+ volunteer = [],
548
+ education = [],
549
+ awards = [],
550
+ certificates = [],
551
+ publications = [],
552
+ skills = [],
553
+ languages = [],
554
+ interests = [],
555
+ references = [],
556
+ projects = [],
557
+ } = resume;
558
+
559
+ const loc = locationString(basics.location);
560
+
561
+ const contactItems = [];
562
+ if (basics.email)
563
+ contactItems.push({
564
+ k: 'E-MAIL',
565
+ v: <a href={safeUrl(`mailto:${basics.email}`)}>{basics.email}</a>,
566
+ });
567
+ if (basics.phone)
568
+ contactItems.push({
569
+ k: 'PHONE',
570
+ v: <a href={safeUrl(`tel:${basics.phone}`)}>{basics.phone}</a>,
571
+ });
572
+ if (loc) contactItems.push({ k: 'LOCATION', v: loc });
573
+ if (basics.url)
574
+ contactItems.push({
575
+ k: 'WEB',
576
+ v: (
577
+ <a href={safeUrl(basics.url)} target="_blank" rel="noopener noreferrer">
578
+ {basics.url.replace(/^https?:\/\//, '').replace(/\/$/, '')}
579
+ </a>
580
+ ),
581
+ });
582
+ (basics.profiles || []).forEach((p) => {
583
+ if (!p) return;
584
+ contactItems.push({
585
+ k: p.network || 'PROFILE',
586
+ v: p.url ? (
587
+ <a href={safeUrl(p.url)} target="_blank" rel="noopener noreferrer">
588
+ {p.username || p.network}
589
+ </a>
590
+ ) : (
591
+ p.username || ''
592
+ ),
593
+ });
594
+ });
595
+
596
+ let part = 0;
597
+ const next = () => pad(++part);
598
+
599
+ return (
600
+ <Sheet>
601
+ <Frame>
602
+ <CornerTick className="tl" />
603
+ <CornerTick className="tr" />
604
+ <CornerTick className="bl" />
605
+ <CornerTick className="br" />
606
+
607
+ <TitleBlock $hasImage={Boolean(basics.image)}>
608
+ {basics.image && <Portrait src={basics.image} alt={basics.name} />}
609
+ <TitleMain>
610
+ <DocTag>
611
+ <span>
612
+ DOC-NO / {(basics.name || 'RESUME').slice(0, 3).toUpperCase()}
613
+ -001
614
+ </span>
615
+ <span>REV / A</span>
616
+ <span>SHEET 1 OF 1</span>
617
+ </DocTag>
618
+ {basics.name && <Name>{basics.name}</Name>}
619
+ {basics.label && <Label>{basics.label}</Label>}
620
+ </TitleMain>
621
+ </TitleBlock>
622
+
623
+ {contactItems.length > 0 && (
624
+ <SpecBar>
625
+ {contactItems.map((it, i) => (
626
+ <SpecCell key={i}>
627
+ <SpecKey>{it.k}</SpecKey>
628
+ <SpecVal>{it.v}</SpecVal>
629
+ </SpecCell>
630
+ ))}
631
+ </SpecBar>
632
+ )}
633
+
634
+ <Body>
635
+ {basics.summary && (
636
+ <SummaryBlock>
637
+ <SummaryLabel>Notes — Profile</SummaryLabel>
638
+ <SummaryText>{basics.summary}</SummaryText>
639
+ </SummaryBlock>
640
+ )}
641
+
642
+ {work.length > 0 && (
643
+ <Section>
644
+ <SectionHeader no={next()} title="Experience" />
645
+ {work.map((job, i) => (
646
+ <Record key={i}>
647
+ <RecordMeta>
648
+ <MetaDate>
649
+ <DateRange
650
+ startDate={job.startDate}
651
+ endDate={job.endDate || null}
652
+ />
653
+ </MetaDate>
654
+ {job.location && <MetaTag>{job.location}</MetaTag>}
655
+ </RecordMeta>
656
+ <RecordBody>
657
+ <RoleTitle>
658
+ {job.url ? (
659
+ <a
660
+ href={safeUrl(job.url)}
661
+ target="_blank"
662
+ rel="noopener noreferrer"
663
+ >
664
+ {job.position || job.name}
665
+ </a>
666
+ ) : (
667
+ job.position || job.name
668
+ )}
669
+ </RoleTitle>
670
+ {job.name && job.position && <RoleOrg>{job.name}</RoleOrg>}
671
+ {job.summary && <Para>{job.summary}</Para>}
672
+ {job.highlights && job.highlights.length > 0 && (
673
+ <Highlights>
674
+ {job.highlights.map((h, j) => (
675
+ <li key={j}>{h}</li>
676
+ ))}
677
+ </Highlights>
678
+ )}
679
+ </RecordBody>
680
+ </Record>
681
+ ))}
682
+ </Section>
683
+ )}
684
+
685
+ {projects.length > 0 && (
686
+ <Section>
687
+ <SectionHeader no={next()} title="Projects" />
688
+ {projects.map((proj, i) => (
689
+ <Record key={i}>
690
+ <RecordMeta>
691
+ <MetaDate>
692
+ <DateRange
693
+ startDate={proj.startDate}
694
+ endDate={proj.endDate || null}
695
+ />
696
+ </MetaDate>
697
+ {proj.type && <MetaTag>{proj.type}</MetaTag>}
698
+ </RecordMeta>
699
+ <RecordBody>
700
+ <RoleTitle>
701
+ {proj.url ? (
702
+ <a
703
+ href={safeUrl(proj.url)}
704
+ target="_blank"
705
+ rel="noopener noreferrer"
706
+ >
707
+ {proj.name}
708
+ </a>
709
+ ) : (
710
+ proj.name
711
+ )}
712
+ </RoleTitle>
713
+ {proj.entity && <RoleOrg>{proj.entity}</RoleOrg>}
714
+ {proj.description && <Para>{proj.description}</Para>}
715
+ {proj.highlights && proj.highlights.length > 0 && (
716
+ <Highlights>
717
+ {proj.highlights.map((h, j) => (
718
+ <li key={j}>{h}</li>
719
+ ))}
720
+ </Highlights>
721
+ )}
722
+ </RecordBody>
723
+ </Record>
724
+ ))}
725
+ </Section>
726
+ )}
727
+
728
+ {education.length > 0 && (
729
+ <Section>
730
+ <SectionHeader no={next()} title="Education" />
731
+ {education.map((edu, i) => (
732
+ <Record key={i}>
733
+ <RecordMeta>
734
+ <MetaDate>
735
+ <DateRange
736
+ startDate={edu.startDate}
737
+ endDate={edu.endDate || null}
738
+ />
739
+ </MetaDate>
740
+ {edu.score && <MetaTag>GPA {edu.score}</MetaTag>}
741
+ </RecordMeta>
742
+ <RecordBody>
743
+ <RoleTitle>{edu.institution}</RoleTitle>
744
+ {(edu.studyType || edu.area) && (
745
+ <RoleOrg>
746
+ {[edu.studyType, edu.area].filter(Boolean).join(' — ')}
747
+ </RoleOrg>
748
+ )}
749
+ {edu.courses && edu.courses.length > 0 && (
750
+ <Highlights>
751
+ {edu.courses.map((c, j) => (
752
+ <li key={j}>{c}</li>
753
+ ))}
754
+ </Highlights>
755
+ )}
756
+ </RecordBody>
757
+ </Record>
758
+ ))}
759
+ </Section>
760
+ )}
761
+
762
+ {skills.length > 0 && (
763
+ <Section>
764
+ <SectionHeader no={next()} title="Skills" />
765
+ <SkillMatrix>
766
+ {skills.map((skill, i) => (
767
+ <SkillCell key={i}>
768
+ <SkillName>
769
+ {skill.name}
770
+ {skill.level && <span>{skill.level}</span>}
771
+ </SkillName>
772
+ {skill.keywords && skill.keywords.length > 0 && (
773
+ <ChipRow>
774
+ {skill.keywords.map((kw, j) => (
775
+ <Chip key={j}>{kw}</Chip>
776
+ ))}
777
+ </ChipRow>
778
+ )}
779
+ </SkillCell>
780
+ ))}
781
+ </SkillMatrix>
782
+ </Section>
783
+ )}
784
+
785
+ {volunteer.length > 0 && (
786
+ <Section>
787
+ <SectionHeader no={next()} title="Volunteer" />
788
+ {volunteer.map((vol, i) => (
789
+ <Record key={i}>
790
+ <RecordMeta>
791
+ <MetaDate>
792
+ <DateRange
793
+ startDate={vol.startDate}
794
+ endDate={vol.endDate || null}
795
+ />
796
+ </MetaDate>
797
+ </RecordMeta>
798
+ <RecordBody>
799
+ <RoleTitle>
800
+ {vol.url ? (
801
+ <a
802
+ href={safeUrl(vol.url)}
803
+ target="_blank"
804
+ rel="noopener noreferrer"
805
+ >
806
+ {vol.position}
807
+ </a>
808
+ ) : (
809
+ vol.position
810
+ )}
811
+ </RoleTitle>
812
+ {vol.organization && <RoleOrg>{vol.organization}</RoleOrg>}
813
+ {vol.summary && <Para>{vol.summary}</Para>}
814
+ {vol.highlights && vol.highlights.length > 0 && (
815
+ <Highlights>
816
+ {vol.highlights.map((h, j) => (
817
+ <li key={j}>{h}</li>
818
+ ))}
819
+ </Highlights>
820
+ )}
821
+ </RecordBody>
822
+ </Record>
823
+ ))}
824
+ </Section>
825
+ )}
826
+
827
+ {awards.length > 0 && (
828
+ <Section>
829
+ <SectionHeader no={next()} title="Awards" />
830
+ <CardGrid>
831
+ {awards.map((a, i) => (
832
+ <Card key={i}>
833
+ <h4>{a.title}</h4>
834
+ <div className="src">
835
+ {[a.awarder, a.date].filter(Boolean).join(' / ')}
836
+ </div>
837
+ {a.summary && <p>{a.summary}</p>}
838
+ </Card>
839
+ ))}
840
+ </CardGrid>
841
+ </Section>
842
+ )}
843
+
844
+ {certificates.length > 0 && (
845
+ <Section>
846
+ <SectionHeader no={next()} title="Certificates" />
847
+ <CardGrid>
848
+ {certificates.map((c, i) => (
849
+ <Card key={i}>
850
+ <h4>
851
+ {c.url ? (
852
+ <a
853
+ href={safeUrl(c.url)}
854
+ target="_blank"
855
+ rel="noopener noreferrer"
856
+ >
857
+ {c.name}
858
+ </a>
859
+ ) : (
860
+ c.name
861
+ )}
862
+ </h4>
863
+ <div className="src">
864
+ {[c.issuer, c.date].filter(Boolean).join(' / ')}
865
+ </div>
866
+ </Card>
867
+ ))}
868
+ </CardGrid>
869
+ </Section>
870
+ )}
871
+
872
+ {publications.length > 0 && (
873
+ <Section>
874
+ <SectionHeader no={next()} title="Publications" />
875
+ <CardGrid>
876
+ {publications.map((p, i) => (
877
+ <Card key={i}>
878
+ <h4>
879
+ {p.url ? (
880
+ <a
881
+ href={safeUrl(p.url)}
882
+ target="_blank"
883
+ rel="noopener noreferrer"
884
+ >
885
+ {p.name}
886
+ </a>
887
+ ) : (
888
+ p.name
889
+ )}
890
+ </h4>
891
+ <div className="src">
892
+ {[p.publisher, p.releaseDate].filter(Boolean).join(' / ')}
893
+ </div>
894
+ {p.summary && <p>{p.summary}</p>}
895
+ </Card>
896
+ ))}
897
+ </CardGrid>
898
+ </Section>
899
+ )}
900
+
901
+ {languages.length > 0 && (
902
+ <Section>
903
+ <SectionHeader no={next()} title="Languages" />
904
+ <InlineList>
905
+ {languages.map((l, i) => (
906
+ <InlineItem key={i}>
907
+ <b>{l.language}</b>
908
+ {l.fluency && <small>{l.fluency}</small>}
909
+ </InlineItem>
910
+ ))}
911
+ </InlineList>
912
+ </Section>
913
+ )}
914
+
915
+ {interests.length > 0 && (
916
+ <Section>
917
+ <SectionHeader no={next()} title="Interests" />
918
+ <CardGrid>
919
+ {interests.map((it, i) => (
920
+ <Card key={i}>
921
+ <h4>{it.name}</h4>
922
+ {it.keywords && it.keywords.length > 0 && (
923
+ <p>{it.keywords.join(', ')}</p>
924
+ )}
925
+ </Card>
926
+ ))}
927
+ </CardGrid>
928
+ </Section>
929
+ )}
930
+
931
+ {references.length > 0 && (
932
+ <Section>
933
+ <SectionHeader no={next()} title="References" />
934
+ <CardGrid>
935
+ {references.map((r, i) => (
936
+ <Card key={i}>
937
+ <h4>{r.name}</h4>
938
+ {r.reference && <p>{r.reference}</p>}
939
+ </Card>
940
+ ))}
941
+ </CardGrid>
942
+ </Section>
943
+ )}
944
+ </Body>
945
+
946
+ <Footer>
947
+ <span>
948
+ {basics.name
949
+ ? `${basics.name} // CURRICULUM VITAE`
950
+ : 'CURRICULUM VITAE'}
951
+ </span>
952
+ <span>JSON-RESUME / INDUSTRIAL-ENGINEER</span>
953
+ </Footer>
954
+ </Frame>
955
+ </Sheet>
956
+ );
957
+ }
958
+
959
+ export default Resume;
package/vite.config.js ADDED
@@ -0,0 +1,32 @@
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
+ noExternal: [
26
+ 'styled-components',
27
+ '@emotion/is-prop-valid',
28
+ 'stylis',
29
+ 'shallowequal',
30
+ ],
31
+ },
32
+ });