jsonresume-theme-reference 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/src/Resume.jsx ADDED
@@ -0,0 +1,381 @@
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
+ * Resume Component
14
+ * THE PERFECT SHOWCASE of @resume/core and resume design best practices
15
+ *
16
+ * This demonstrates:
17
+ * - All 5 @resume/core primitives with JSX
18
+ * - All 11 JSON Resume schema sections
19
+ * - ATS-friendly patterns
20
+ * - Design token usage
21
+ * - Beautiful component composition
22
+ */
23
+
24
+ const Layout = styled.div`
25
+ max-width: var(--resume-max-width, 660px);
26
+ margin: 0 auto;
27
+ padding: 40px 20px;
28
+ font-family: var(
29
+ --resume-font-sans,
30
+ 'Helvetica Neue',
31
+ Helvetica,
32
+ Arial,
33
+ sans-serif
34
+ );
35
+ font-size: var(--resume-size-body, 11pt);
36
+ line-height: 1.6;
37
+ color: var(--resume-color-primary, #000);
38
+ `;
39
+
40
+ const Header = styled.header`
41
+ text-align: center;
42
+ margin-bottom: var(--resume-space-section, 2rem);
43
+ `;
44
+
45
+ const Name = styled.h1`
46
+ font-size: var(--resume-size-name, 28pt);
47
+ font-weight: 700;
48
+ margin: 0 0 8px 0;
49
+ color: var(--resume-color-primary, #000);
50
+ `;
51
+
52
+ const Label = styled.p`
53
+ font-size: var(--resume-size-heading, 16pt);
54
+ color: var(--resume-color-secondary, #333);
55
+ margin: 0 0 16px 0;
56
+ `;
57
+
58
+ const Contact = styled.div`
59
+ display: flex;
60
+ flex-wrap: wrap;
61
+ justify-content: center;
62
+ gap: 16px;
63
+ font-size: var(--resume-size-body, 11pt);
64
+
65
+ a {
66
+ color: var(--resume-color-accent, #0066cc);
67
+ text-decoration: none;
68
+
69
+ &:hover {
70
+ text-decoration: underline;
71
+ }
72
+ }
73
+ `;
74
+
75
+ const Summary = styled.p`
76
+ text-align: center;
77
+ margin: 16px 0;
78
+ color: var(--resume-color-secondary, #333);
79
+ `;
80
+
81
+ const SkillGroup = styled.div`
82
+ margin-bottom: 12px;
83
+
84
+ strong {
85
+ margin-right: 8px;
86
+ }
87
+ `;
88
+
89
+ function Resume({ resume }) {
90
+ const {
91
+ basics = {},
92
+ work = [],
93
+ education = [],
94
+ skills = [],
95
+ volunteer = [],
96
+ awards = [],
97
+ publications = [],
98
+ languages = [],
99
+ interests = [],
100
+ references = [],
101
+ projects = [],
102
+ } = resume;
103
+
104
+ return (
105
+ <Layout>
106
+ {/* Hero Section - Name, Title, Contact */}
107
+ {basics && (
108
+ <Header>
109
+ <Name>{basics.name}</Name>
110
+ {basics.label && <Label>{basics.label}</Label>}
111
+
112
+ <Contact>
113
+ {basics.email && (
114
+ <a href={safeUrl(`mailto:${basics.email}`)}>{basics.email}</a>
115
+ )}
116
+ {basics.phone && <span>{basics.phone}</span>}
117
+ {basics.url && (
118
+ <a
119
+ href={safeUrl(basics.url)}
120
+ target="_blank"
121
+ rel={getLinkRel(basics.url, true)}
122
+ >
123
+ {basics.url}
124
+ </a>
125
+ )}
126
+ {basics.location && (
127
+ <span>
128
+ {[
129
+ basics.location.city,
130
+ basics.location.region,
131
+ basics.location.countryCode,
132
+ ]
133
+ .filter(Boolean)
134
+ .join(', ')}
135
+ </span>
136
+ )}
137
+ {basics.profiles?.map((profile) => {
138
+ const profileUrl = safeUrl(profile.url);
139
+ return (
140
+ profileUrl && (
141
+ <a
142
+ key={profile.network}
143
+ href={profileUrl}
144
+ target="_blank"
145
+ rel={getLinkRel(profileUrl, true)}
146
+ >
147
+ {profile.network}
148
+ </a>
149
+ )
150
+ );
151
+ })}
152
+ </Contact>
153
+
154
+ {basics.summary && <Summary>{basics.summary}</Summary>}
155
+ </Header>
156
+ )}
157
+
158
+ {/* Work Experience Section */}
159
+ {work.length > 0 && (
160
+ <Section id="work">
161
+ <SectionTitle>Work Experience</SectionTitle>
162
+ {work.map((job, index) => (
163
+ <ListItem
164
+ key={index}
165
+ title={job.position}
166
+ subtitle={job.name}
167
+ dateRange={
168
+ job.startDate ? (
169
+ <DateRange startDate={job.startDate} endDate={job.endDate} />
170
+ ) : null
171
+ }
172
+ location={job.location}
173
+ description={job.summary}
174
+ highlights={job.highlights}
175
+ />
176
+ ))}
177
+ </Section>
178
+ )}
179
+
180
+ {/* Education Section */}
181
+ {education.length > 0 && (
182
+ <Section id="education">
183
+ <SectionTitle>Education</SectionTitle>
184
+ {education.map((edu, index) => {
185
+ const title = [edu.studyType, edu.area]
186
+ .filter(Boolean)
187
+ .join(' in ');
188
+ const highlights = [
189
+ edu.score ? `GPA: ${edu.score}` : '',
190
+ ...(edu.courses || []),
191
+ ].filter(Boolean);
192
+
193
+ return (
194
+ <ListItem
195
+ key={index}
196
+ title={title || edu.institution}
197
+ subtitle={title ? edu.institution : ''}
198
+ dateRange={
199
+ edu.startDate ? (
200
+ <DateRange
201
+ startDate={edu.startDate}
202
+ endDate={edu.endDate}
203
+ />
204
+ ) : null
205
+ }
206
+ highlights={highlights.length > 0 ? highlights : undefined}
207
+ />
208
+ );
209
+ })}
210
+ </Section>
211
+ )}
212
+
213
+ {/* Skills Section */}
214
+ {skills.length > 0 && (
215
+ <Section id="skills">
216
+ <SectionTitle>Skills</SectionTitle>
217
+ {skills.map((skillGroup, index) => (
218
+ <SkillGroup key={index}>
219
+ {skillGroup.name && <strong>{skillGroup.name}:</strong>}
220
+ <BadgeList items={skillGroup.keywords} variant="default" />
221
+ </SkillGroup>
222
+ ))}
223
+ </Section>
224
+ )}
225
+
226
+ {/* Projects Section */}
227
+ {projects.length > 0 && (
228
+ <Section id="projects">
229
+ <SectionTitle>Projects</SectionTitle>
230
+ {projects.map((project, index) => (
231
+ <ListItem
232
+ key={index}
233
+ title={project.name}
234
+ dateRange={
235
+ project.startDate ? (
236
+ <DateRange
237
+ startDate={project.startDate}
238
+ endDate={project.endDate}
239
+ />
240
+ ) : null
241
+ }
242
+ description={
243
+ <>
244
+ {project.description}
245
+ {project.url && safeUrl(project.url) && (
246
+ <>
247
+ <br />
248
+ <a
249
+ href={safeUrl(project.url)}
250
+ target="_blank"
251
+ rel={getLinkRel(project.url, true)}
252
+ >
253
+ {project.url}
254
+ </a>
255
+ </>
256
+ )}
257
+ {project.keywords && project.keywords.length > 0 && (
258
+ <>
259
+ <br />
260
+ <BadgeList items={project.keywords} variant="accent" />
261
+ </>
262
+ )}
263
+ </>
264
+ }
265
+ highlights={project.highlights}
266
+ />
267
+ ))}
268
+ </Section>
269
+ )}
270
+
271
+ {/* Volunteer Section */}
272
+ {volunteer.length > 0 && (
273
+ <Section id="volunteer">
274
+ <SectionTitle>Volunteer Experience</SectionTitle>
275
+ {volunteer.map((vol, index) => (
276
+ <ListItem
277
+ key={index}
278
+ title={vol.position}
279
+ subtitle={vol.organization}
280
+ dateRange={
281
+ vol.startDate ? (
282
+ <DateRange startDate={vol.startDate} endDate={vol.endDate} />
283
+ ) : null
284
+ }
285
+ description={vol.summary}
286
+ highlights={vol.highlights}
287
+ />
288
+ ))}
289
+ </Section>
290
+ )}
291
+
292
+ {/* Awards Section */}
293
+ {awards.length > 0 && (
294
+ <Section id="awards">
295
+ <SectionTitle>Awards & Honors</SectionTitle>
296
+ {awards.map((award, index) => (
297
+ <ListItem
298
+ key={index}
299
+ title={award.title}
300
+ subtitle={award.awarder}
301
+ dateRange={award.date}
302
+ description={award.summary}
303
+ />
304
+ ))}
305
+ </Section>
306
+ )}
307
+
308
+ {/* Publications Section */}
309
+ {publications.length > 0 && (
310
+ <Section id="publications">
311
+ <SectionTitle>Publications</SectionTitle>
312
+ {publications.map((pub, index) => (
313
+ <ListItem
314
+ key={index}
315
+ title={pub.name}
316
+ subtitle={pub.publisher}
317
+ dateRange={pub.releaseDate}
318
+ description={
319
+ <>
320
+ {pub.summary}
321
+ {pub.url && safeUrl(pub.url) && (
322
+ <>
323
+ <br />
324
+ <a
325
+ href={safeUrl(pub.url)}
326
+ target="_blank"
327
+ rel={getLinkRel(pub.url, true)}
328
+ >
329
+ {pub.url}
330
+ </a>
331
+ </>
332
+ )}
333
+ </>
334
+ }
335
+ />
336
+ ))}
337
+ </Section>
338
+ )}
339
+
340
+ {/* Languages Section */}
341
+ {languages.length > 0 && (
342
+ <Section id="languages">
343
+ <SectionTitle>Languages</SectionTitle>
344
+ {languages.map((lang, index) => (
345
+ <div key={index}>
346
+ {lang.language}
347
+ {lang.fluency && ` (${lang.fluency})`}
348
+ </div>
349
+ ))}
350
+ </Section>
351
+ )}
352
+
353
+ {/* Interests Section */}
354
+ {interests.length > 0 && (
355
+ <Section id="interests">
356
+ <SectionTitle>Interests</SectionTitle>
357
+ <BadgeList
358
+ items={interests.flatMap((i) => i.keywords || [])}
359
+ variant="default"
360
+ />
361
+ </Section>
362
+ )}
363
+
364
+ {/* References Section */}
365
+ {references.length > 0 && (
366
+ <Section id="references">
367
+ <SectionTitle>References</SectionTitle>
368
+ {references.map((ref, index) => (
369
+ <ListItem
370
+ key={index}
371
+ title={ref.name}
372
+ description={ref.reference}
373
+ />
374
+ ))}
375
+ </Section>
376
+ )}
377
+ </Layout>
378
+ );
379
+ }
380
+
381
+ export default Resume;
package/src/index.js ADDED
@@ -0,0 +1,262 @@
1
+ import { renderToString } from 'react-dom/server';
2
+ import { ServerStyleSheet } from 'styled-components';
3
+ import Resume from './Resume.jsx';
4
+
5
+ /**
6
+ * JSON Resume Reference Theme (JSX Edition)
7
+ *
8
+ * THE PERFECT SHOWCASE of @resume/core with beautiful React components.
9
+ *
10
+ * This is the NEW architecture that demonstrates:
11
+ * - Clean JSX syntax (no template strings)
12
+ * - React component composition
13
+ * - styled-components with design tokens
14
+ * - All @resume/core primitives as React components
15
+ * - Beautiful developer experience
16
+ *
17
+ * @param {Object} resume - JSON Resume object
18
+ * @param {Object} [options] - Rendering options
19
+ * @param {string} [options.locale='en'] - Locale for date formatting (e.g., 'en-US', 'fr-FR')
20
+ * @param {string} [options.dir='ltr'] - Text direction ('ltr' or 'rtl')
21
+ * @param {string} [options.title] - Custom document title (defaults to resume name)
22
+ * @param {string} [options.theme='default'] - Theme variant (default, modern, classic, minimal)
23
+ * @param {boolean} [options.structured=false] - Return structured object instead of HTML string
24
+ * @returns {string|Object} Complete HTML document or structured object with parts
25
+ *
26
+ * @example
27
+ * // Basic usage (backwards compatible)
28
+ * const html = render(resume);
29
+ *
30
+ * @example
31
+ * // With options
32
+ * const html = render(resume, {
33
+ * locale: 'fr-FR',
34
+ * dir: 'ltr',
35
+ * title: 'My Professional Resume',
36
+ * theme: 'modern'
37
+ * });
38
+ *
39
+ * @example
40
+ * // Get structured output
41
+ * const { html, head, body, css } = render(resume, { structured: true });
42
+ */
43
+ export function render(resume, options = {}) {
44
+ const {
45
+ locale = 'en',
46
+ dir = 'ltr',
47
+ title = resume.basics?.name || 'Resume',
48
+ theme = 'default',
49
+ structured = false,
50
+ } = options;
51
+
52
+ const sheet = new ServerStyleSheet();
53
+
54
+ try {
55
+ // Render React component to HTML string with styled-components
56
+ const bodyHtml = renderToString(
57
+ sheet.collectStyles(<Resume resume={resume} />)
58
+ );
59
+
60
+ // Extract CSS from styled-components
61
+ const styledComponentsCss = sheet.getStyleTags();
62
+
63
+ // Build structured parts
64
+ const globalStyles = `
65
+ /*
66
+ * Global Styles
67
+ * Base styles that complement @resume/core design tokens
68
+ */
69
+ * {
70
+ margin: 0;
71
+ padding: 0;
72
+ box-sizing: border-box;
73
+ }
74
+
75
+ body {
76
+ background: #fff;
77
+ -webkit-font-smoothing: antialiased;
78
+ -moz-osx-font-smoothing: grayscale;
79
+ }
80
+
81
+ @media print {
82
+ body {
83
+ background: #fff;
84
+ }
85
+
86
+ a {
87
+ color: inherit;
88
+ text-decoration: none;
89
+ }
90
+ }
91
+ `;
92
+
93
+ // Design tokens CSS (inlined from @resume/core)
94
+ const designTokens = `
95
+ :root {
96
+ --resume-font-sans: "Helvetica Neue", Helvetica, Arial, sans-serif;
97
+ --resume-font-serif: Cambria, Georgia, "Times New Roman", serif;
98
+ --resume-font-mono: "Courier New", Courier, monospace;
99
+
100
+ --resume-size-name: 36px;
101
+ --resume-size-heading: 16px;
102
+ --resume-size-subheading: 14px;
103
+ --resume-size-body: 11px;
104
+ --resume-size-small: 10px;
105
+
106
+ --resume-weight-normal: 400;
107
+ --resume-weight-medium: 500;
108
+ --resume-weight-semibold: 600;
109
+ --resume-weight-bold: 700;
110
+
111
+ --resume-line-height-tight: 1.2;
112
+ --resume-line-height-normal: 1.5;
113
+ --resume-line-height-relaxed: 1.75;
114
+
115
+ --resume-color-primary: #1a1a1a;
116
+ --resume-color-secondary: #4a4a4a;
117
+ --resume-color-accent: #2563eb;
118
+ --resume-color-background: #ffffff;
119
+ --resume-color-border: #e5e7eb;
120
+
121
+ --resume-space-section: 24px;
122
+ --resume-space-item: 16px;
123
+ --resume-space-tight: 8px;
124
+ --resume-space-margin: 48px;
125
+
126
+ --resume-max-width: 660px;
127
+ --resume-column-gap: 24px;
128
+
129
+ --resume-radius-sm: 4px;
130
+ --resume-radius-md: 8px;
131
+ --resume-radius-lg: 12px;
132
+
133
+ --resume-shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
134
+ --resume-shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
135
+ }
136
+
137
+ [data-theme="modern"] {
138
+ --resume-color-primary: #0f172a;
139
+ --resume-color-secondary: #475569;
140
+ --resume-color-accent: #8b5cf6;
141
+ --resume-font-sans: "Inter", -apple-system, BlinkMacSystemFont, sans-serif;
142
+ }
143
+
144
+ [data-theme="classic"] {
145
+ --resume-color-primary: #000000;
146
+ --resume-color-secondary: #333333;
147
+ --resume-color-accent: #0066cc;
148
+ --resume-font-sans: Georgia, "Times New Roman", serif;
149
+ }
150
+
151
+ [data-theme="minimal"] {
152
+ --resume-color-primary: #18181b;
153
+ --resume-color-secondary: #71717a;
154
+ --resume-color-accent: #000000;
155
+ --resume-space-section: 32px;
156
+ --resume-space-item: 20px;
157
+ }
158
+
159
+ [data-theme="high-contrast"] {
160
+ --resume-color-primary: #000000;
161
+ --resume-color-secondary: #000000;
162
+ --resume-color-accent: #0000ff;
163
+ --resume-color-background: #ffffff;
164
+ --resume-color-border: #000000;
165
+ }
166
+
167
+ @media print {
168
+ :root {
169
+ --resume-space-section: 18px;
170
+ --resume-space-item: 12px;
171
+ }
172
+
173
+ .resume-section {
174
+ page-break-inside: avoid;
175
+ }
176
+
177
+ .resume-item {
178
+ break-inside: avoid;
179
+ }
180
+
181
+ p, li {
182
+ widows: 3;
183
+ orphans: 3;
184
+ }
185
+
186
+ .resume-description {
187
+ hyphens: auto;
188
+ }
189
+
190
+ .no-print {
191
+ display: none !important;
192
+ }
193
+ }
194
+
195
+ [dir="rtl"] {
196
+ text-align: right;
197
+ }
198
+
199
+ [dir="rtl"] .resume-item {
200
+ padding-left: 0;
201
+ padding-right: var(--resume-space-tight);
202
+ }
203
+ `;
204
+
205
+ const head = `
206
+ <meta charset="UTF-8">
207
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
208
+ <title>${title}</title>
209
+
210
+ <!-- Design Tokens (inlined) -->
211
+ <style>
212
+ ${designTokens}
213
+ </style>
214
+
215
+ <!-- Styled Components CSS -->
216
+ ${styledComponentsCss}
217
+
218
+ <style>
219
+ ${globalStyles}
220
+ </style>
221
+ `;
222
+
223
+ // Return structured object if requested
224
+ if (structured) {
225
+ return {
226
+ html: `<!DOCTYPE html>
227
+ <html lang="${locale}" dir="${dir}"${
228
+ theme !== 'default' ? ` data-theme="${theme}"` : ''
229
+ }>
230
+ <head>${head}</head>
231
+ <body>
232
+ ${bodyHtml}
233
+ </body>
234
+ </html>`,
235
+ head,
236
+ body: bodyHtml,
237
+ css: styledComponentsCss,
238
+ globalStyles,
239
+ locale,
240
+ dir,
241
+ theme,
242
+ };
243
+ }
244
+
245
+ // Return complete HTML document (backwards compatible)
246
+ return `<!DOCTYPE html>
247
+ <html lang="${locale}" dir="${dir}"${
248
+ theme !== 'default' ? ` data-theme="${theme}"` : ''
249
+ }>
250
+ <head>${head}</head>
251
+ <body>
252
+ ${bodyHtml}
253
+ </body>
254
+ </html>`;
255
+ } finally {
256
+ // Clean up styled-components sheet
257
+ sheet.seal();
258
+ }
259
+ }
260
+
261
+ export { Resume };
262
+ export default { render };