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/README.md +444 -0
- package/package.json +35 -0
- package/src/Resume.jsx +381 -0
- package/src/index.js +262 -0
- package/tests/fixtures/complete-resume.json +197 -0
- package/tests/theme.test.js +192 -0
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 };
|