jsonresume-theme-desert-modern 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 ADDED
@@ -0,0 +1,46 @@
1
+ # Desert Modern - JSON Resume Theme
2
+
3
+ A warm minimalist JSON Resume theme inspired by desert landscapes and contemporary architecture. Designed for creative technologists, wellness professionals, and anyone who wants a calm, grounded presentation.
4
+
5
+ ## Design Specification
6
+
7
+ - **Vibe:** Warm minimalism with sand-toned calm
8
+ - **Layout:** Single column with gentle vertical rhythm and thin separators
9
+ - **Typography:** Light sans-serif stack (Nunito, Inter) with generous letter spacing
10
+ - **Colors:** Accent `#d97706` (desert amber) on a soft cream background
11
+ - **Notes:** Ideal for portfolios with both creative and professional touchpoints
12
+
13
+ ## Highlights
14
+
15
+ - Soft cream paper background with subtle shadowing for print-friendly warmth
16
+ - Thin amber dividers and section markers to reinforce the desert tone
17
+ - Balanced typography with lighter weights for headings and relaxed body copy
18
+ - Clean metadata layout prioritizing readability and ATS compatibility
19
+
20
+ ## Supported JSON Resume Sections
21
+
22
+ All 12 standard sections render with consistent styling:
23
+
24
+ - Basics (name, contact, summary)
25
+ - Work
26
+ - Projects
27
+ - Education
28
+ - Skills
29
+ - Volunteer
30
+ - Awards
31
+ - Certifications
32
+ - Publications
33
+ - Languages
34
+ - Interests
35
+ - References
36
+
37
+ ## Usage
38
+
39
+ ```bash
40
+ npm install jsonresume-theme-desert-modern
41
+ jsonresume serve --theme desert-modern
42
+ ```
43
+
44
+ ## License
45
+
46
+ MIT
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "jsonresume-theme-desert-modern",
3
+ "version": "0.1.0",
4
+ "description": "Warm minimalist resume theme inspired by desert modern aesthetics",
5
+ "main": "src/index.js",
6
+ "type": "module",
7
+ "license": "MIT",
8
+ "keywords": [
9
+ "jsonresume",
10
+ "theme",
11
+ "desert",
12
+ "minimalist",
13
+ "modern"
14
+ ],
15
+ "peerDependencies": {
16
+ "react": "^18.0.0 || ^19.0.0",
17
+ "react-dom": "^18.0.0 || ^19.0.0"
18
+ },
19
+ "dependencies": {
20
+ "styled-components": "^6.1.19",
21
+ "@resume/core": "0.1.0"
22
+ }
23
+ }
package/src/Resume.jsx ADDED
@@ -0,0 +1,333 @@
1
+ import React from 'react';
2
+ import { DateRange } from '@resume/core';
3
+ import {
4
+ Layout,
5
+ Header,
6
+ Name,
7
+ Label,
8
+ StyledContactInfo,
9
+ Summary,
10
+ SectionBlock,
11
+ SectionHeading,
12
+ Entry,
13
+ EntryHeader,
14
+ EntryTitle,
15
+ EntrySubtitle,
16
+ EntryMeta,
17
+ EntrySummary,
18
+ BulletList,
19
+ SkillGrid,
20
+ SkillCard,
21
+ SkillTitle,
22
+ KeywordList,
23
+ Pill,
24
+ Anchor,
25
+ } from './styles';
26
+
27
+ const joinParts = (...parts) => parts.filter(Boolean).join(' • ');
28
+
29
+ const renderHighlights = (items) =>
30
+ items?.length ? (
31
+ <BulletList>
32
+ {items.map((text, index) => (
33
+ <li key={index}>{text}</li>
34
+ ))}
35
+ </BulletList>
36
+ ) : null;
37
+
38
+ function Resume({ resume }) {
39
+ const {
40
+ basics = {},
41
+ work = [],
42
+ education = [],
43
+ projects = [],
44
+ volunteer = [],
45
+ skills = [],
46
+ awards = [],
47
+ certificates = [],
48
+ publications = [],
49
+ languages = [],
50
+ interests = [],
51
+ references = [],
52
+ } = resume;
53
+
54
+ return (
55
+ <Layout>
56
+ <Header>
57
+ {basics.name && <Name>{basics.name}</Name>}
58
+ {basics.label && <Label>{basics.label}</Label>}
59
+ <StyledContactInfo basics={basics} />
60
+ {basics.summary && <Summary>{basics.summary}</Summary>}
61
+ </Header>
62
+
63
+ {work?.length > 0 && (
64
+ <SectionBlock>
65
+ <SectionHeading>Experience</SectionHeading>
66
+ {work.map((job, index) => (
67
+ <Entry key={index}>
68
+ <EntryHeader>
69
+ <div>
70
+ {job.position && <EntryTitle>{job.position}</EntryTitle>}
71
+ {job.name || job.location ? (
72
+ <EntrySubtitle>
73
+ {joinParts(job.name, job.location)}
74
+ </EntrySubtitle>
75
+ ) : null}
76
+ </div>
77
+ {(job.startDate || job.endDate) && (
78
+ <EntryMeta>
79
+ <DateRange
80
+ startDate={job.startDate}
81
+ endDate={job.endDate}
82
+ />
83
+ </EntryMeta>
84
+ )}
85
+ </EntryHeader>
86
+ {job.summary && <EntrySummary>{job.summary}</EntrySummary>}
87
+ {renderHighlights(job.highlights)}
88
+ </Entry>
89
+ ))}
90
+ </SectionBlock>
91
+ )}
92
+
93
+ {projects?.length > 0 && (
94
+ <SectionBlock>
95
+ <SectionHeading>Projects</SectionHeading>
96
+ {projects.map((project, index) => {
97
+ const summary = project.description || project.summary;
98
+ const descriptor = joinParts(project.entity, project.type);
99
+ return (
100
+ <Entry key={index}>
101
+ <EntryHeader>
102
+ <div>
103
+ {project.name && <EntryTitle>{project.name}</EntryTitle>}
104
+ {(descriptor || project.url) && (
105
+ <EntrySubtitle>
106
+ {descriptor}
107
+ {descriptor && project.url ? ' • ' : ''}
108
+ {project.url && (
109
+ <Anchor href={project.url}>{project.url}</Anchor>
110
+ )}
111
+ </EntrySubtitle>
112
+ )}
113
+ </div>
114
+ {(project.startDate || project.endDate) && (
115
+ <EntryMeta>
116
+ <DateRange
117
+ startDate={project.startDate}
118
+ endDate={project.endDate}
119
+ />
120
+ </EntryMeta>
121
+ )}
122
+ </EntryHeader>
123
+ {summary && <EntrySummary>{summary}</EntrySummary>}
124
+ {renderHighlights(project.highlights)}
125
+ {project.keywords?.length > 0 && (
126
+ <KeywordList>{project.keywords.join(' • ')}</KeywordList>
127
+ )}
128
+ </Entry>
129
+ );
130
+ })}
131
+ </SectionBlock>
132
+ )}
133
+
134
+ {education?.length > 0 && (
135
+ <SectionBlock>
136
+ <SectionHeading>Education</SectionHeading>
137
+ {education.map((edu, index) => (
138
+ <Entry key={index}>
139
+ <EntryHeader>
140
+ <div>
141
+ {edu.institution && (
142
+ <EntryTitle>{edu.institution}</EntryTitle>
143
+ )}
144
+ {(edu.studyType || edu.area || edu.score) && (
145
+ <EntrySubtitle>
146
+ {joinParts(
147
+ [edu.studyType, edu.area].filter(Boolean).join(' in '),
148
+ edu.score
149
+ )}
150
+ </EntrySubtitle>
151
+ )}
152
+ </div>
153
+ {(edu.startDate || edu.endDate) && (
154
+ <EntryMeta>
155
+ <DateRange
156
+ startDate={edu.startDate}
157
+ endDate={edu.endDate}
158
+ />
159
+ </EntryMeta>
160
+ )}
161
+ </EntryHeader>
162
+ {edu.courses?.length > 0 && (
163
+ <KeywordList>Key courses: {edu.courses.join(', ')}</KeywordList>
164
+ )}
165
+ </Entry>
166
+ ))}
167
+ </SectionBlock>
168
+ )}
169
+
170
+ {skills?.length > 0 && (
171
+ <SectionBlock>
172
+ <SectionHeading>Skills</SectionHeading>
173
+ <SkillGrid>
174
+ {skills.map((skill, index) => (
175
+ <SkillCard key={index}>
176
+ <SkillTitle>
177
+ {skill.name}
178
+ {skill.level && <Pill>{skill.level}</Pill>}
179
+ </SkillTitle>
180
+ {skill.keywords?.length > 0 && (
181
+ <KeywordList>{skill.keywords.join(' • ')}</KeywordList>
182
+ )}
183
+ </SkillCard>
184
+ ))}
185
+ </SkillGrid>
186
+ </SectionBlock>
187
+ )}
188
+
189
+ {volunteer?.length > 0 && (
190
+ <SectionBlock>
191
+ <SectionHeading>Volunteer</SectionHeading>
192
+ {volunteer.map((item, index) => (
193
+ <Entry key={index}>
194
+ <EntryHeader>
195
+ <div>
196
+ {item.position && <EntryTitle>{item.position}</EntryTitle>}
197
+ {(item.organization || item.location) && (
198
+ <EntrySubtitle>
199
+ {joinParts(item.organization, item.location)}
200
+ </EntrySubtitle>
201
+ )}
202
+ </div>
203
+ {(item.startDate || item.endDate) && (
204
+ <EntryMeta>
205
+ <DateRange
206
+ startDate={item.startDate}
207
+ endDate={item.endDate}
208
+ />
209
+ </EntryMeta>
210
+ )}
211
+ </EntryHeader>
212
+ {item.summary && <EntrySummary>{item.summary}</EntrySummary>}
213
+ {renderHighlights(item.highlights)}
214
+ </Entry>
215
+ ))}
216
+ </SectionBlock>
217
+ )}
218
+
219
+ {awards?.length > 0 && (
220
+ <SectionBlock>
221
+ <SectionHeading>Awards</SectionHeading>
222
+ {awards.map((award, index) => (
223
+ <Entry key={index}>
224
+ <EntryHeader>
225
+ <div>
226
+ {award.title && <EntryTitle>{award.title}</EntryTitle>}
227
+ {award.awarder && (
228
+ <EntrySubtitle>Issued by {award.awarder}</EntrySubtitle>
229
+ )}
230
+ </div>
231
+ {award.date && <EntryMeta>{award.date}</EntryMeta>}
232
+ </EntryHeader>
233
+ {award.summary && <EntrySummary>{award.summary}</EntrySummary>}
234
+ </Entry>
235
+ ))}
236
+ </SectionBlock>
237
+ )}
238
+
239
+ {certificates?.length > 0 && (
240
+ <SectionBlock>
241
+ <SectionHeading>Certifications</SectionHeading>
242
+ {certificates.map((cert, index) => (
243
+ <Entry key={index}>
244
+ <EntryHeader>
245
+ <div>
246
+ {cert.name && <EntryTitle>{cert.name}</EntryTitle>}
247
+ {cert.issuer && (
248
+ <EntrySubtitle>Issued by {cert.issuer}</EntrySubtitle>
249
+ )}
250
+ </div>
251
+ {cert.date && <EntryMeta>{cert.date}</EntryMeta>}
252
+ </EntryHeader>
253
+ {cert.summary && <EntrySummary>{cert.summary}</EntrySummary>}
254
+ {cert.url && (
255
+ <KeywordList>
256
+ <Anchor href={cert.url}>{cert.url}</Anchor>
257
+ </KeywordList>
258
+ )}
259
+ </Entry>
260
+ ))}
261
+ </SectionBlock>
262
+ )}
263
+
264
+ {publications?.length > 0 && (
265
+ <SectionBlock>
266
+ <SectionHeading>Publications</SectionHeading>
267
+ {publications.map((pub, index) => (
268
+ <Entry key={index}>
269
+ <EntryHeader>
270
+ <div>
271
+ {pub.name && <EntryTitle>{pub.name}</EntryTitle>}
272
+ {(pub.publisher || pub.url) && (
273
+ <EntrySubtitle>
274
+ {pub.publisher}
275
+ {pub.publisher && pub.url ? ' • ' : ''}
276
+ {pub.url && <Anchor href={pub.url}>{pub.url}</Anchor>}
277
+ </EntrySubtitle>
278
+ )}
279
+ </div>
280
+ {pub.releaseDate && <EntryMeta>{pub.releaseDate}</EntryMeta>}
281
+ </EntryHeader>
282
+ {pub.summary && <EntrySummary>{pub.summary}</EntrySummary>}
283
+ </Entry>
284
+ ))}
285
+ </SectionBlock>
286
+ )}
287
+
288
+ {languages?.length > 0 && (
289
+ <SectionBlock>
290
+ <SectionHeading>Languages</SectionHeading>
291
+ <SkillGrid>
292
+ {languages.map((lang, index) => (
293
+ <SkillCard key={index}>
294
+ <SkillTitle>{lang.language}</SkillTitle>
295
+ {lang.fluency && <Pill>{lang.fluency}</Pill>}
296
+ </SkillCard>
297
+ ))}
298
+ </SkillGrid>
299
+ </SectionBlock>
300
+ )}
301
+
302
+ {interests?.length > 0 && (
303
+ <SectionBlock>
304
+ <SectionHeading>Interests</SectionHeading>
305
+ <SkillGrid>
306
+ {interests.map((interest, index) => (
307
+ <SkillCard key={index}>
308
+ <SkillTitle>{interest.name}</SkillTitle>
309
+ {interest.keywords?.length > 0 && (
310
+ <KeywordList>{interest.keywords.join(' • ')}</KeywordList>
311
+ )}
312
+ </SkillCard>
313
+ ))}
314
+ </SkillGrid>
315
+ </SectionBlock>
316
+ )}
317
+
318
+ {references?.length > 0 && (
319
+ <SectionBlock>
320
+ <SectionHeading>References</SectionHeading>
321
+ {references.map((ref, index) => (
322
+ <Entry key={index}>
323
+ {ref.name && <EntryTitle>{ref.name}</EntryTitle>}
324
+ {ref.reference && <EntrySummary>{ref.reference}</EntrySummary>}
325
+ </Entry>
326
+ ))}
327
+ </SectionBlock>
328
+ )}
329
+ </Layout>
330
+ );
331
+ }
332
+
333
+ export default Resume;
package/src/index.js ADDED
@@ -0,0 +1,54 @@
1
+ import React from 'react';
2
+ import { renderToString } from 'react-dom/server';
3
+ import { ServerStyleSheet } from 'styled-components';
4
+ import Resume from './Resume.jsx';
5
+
6
+ export function render(resume) {
7
+ const sheet = new ServerStyleSheet();
8
+
9
+ try {
10
+ const html = renderToString(
11
+ sheet.collectStyles(<Resume resume={resume} />)
12
+ );
13
+ const styles = sheet.getStyleTags();
14
+
15
+ return `<!DOCTYPE html>
16
+ <html lang="en">
17
+ <head>
18
+ <meta charset="UTF-8">
19
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
20
+ <title>${resume.basics?.name || 'Resume'} - Resume</title>
21
+ <link rel="preconnect" href="https://fonts.googleapis.com">
22
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
23
+ <link href="https://fonts.googleapis.com/css2?family=Nunito:wght@300;400;600;700&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
24
+ ${styles}
25
+ <style>
26
+ * {
27
+ box-sizing: border-box;
28
+ }
29
+ body {
30
+ margin: 0;
31
+ padding: 0;
32
+ background: #fdf7f2;
33
+ color: #1f2933;
34
+ }
35
+ @media print {
36
+ body {
37
+ background: white;
38
+ }
39
+ @page {
40
+ margin: 0.5in;
41
+ }
42
+ }
43
+ </style>
44
+ </head>
45
+ <body>
46
+ ${html}
47
+ </body>
48
+ </html>`;
49
+ } finally {
50
+ sheet.seal();
51
+ }
52
+ }
53
+
54
+ export { Resume };
package/src/styles.js ADDED
@@ -0,0 +1,244 @@
1
+ import styled from 'styled-components';
2
+ import { Section, SectionTitle, ContactInfo } from '@resume/core';
3
+
4
+ const ACCENT = '#d97706';
5
+
6
+ export const Layout = styled.div`
7
+ max-width: 880px;
8
+ margin: 0 auto;
9
+ padding: 64px 72px;
10
+ background: #fefbf5;
11
+ color: #3f2f25;
12
+ font-family: 'Nunito', 'Inter', 'Segoe UI', sans-serif;
13
+ line-height: 1.7;
14
+ border-radius: 20px;
15
+ box-shadow: 0 32px 60px rgba(149, 108, 57, 0.12);
16
+ border: 1px solid rgba(217, 119, 6, 0.16);
17
+
18
+ @media (max-width: 720px) {
19
+ padding: 48px 28px;
20
+ }
21
+
22
+ @media print {
23
+ padding: 40px 48px;
24
+ border-radius: 0;
25
+ box-shadow: none;
26
+ border: none;
27
+ background: white;
28
+ }
29
+ `;
30
+
31
+ export const Header = styled.header`
32
+ display: flex;
33
+ flex-direction: column;
34
+ gap: 16px;
35
+ margin-bottom: 40px;
36
+ padding-bottom: 28px;
37
+ border-bottom: 1px solid rgba(217, 119, 6, 0.25);
38
+ `;
39
+
40
+ export const Name = styled.h1`
41
+ font-size: 38px;
42
+ font-weight: 700;
43
+ letter-spacing: 0.02em;
44
+ color: #92400e;
45
+ margin: 0;
46
+ `;
47
+
48
+ export const Label = styled.div`
49
+ font-size: 14px;
50
+ font-weight: 600;
51
+ letter-spacing: 0.28em;
52
+ text-transform: uppercase;
53
+ color: #b45309;
54
+ `;
55
+
56
+ export const StyledContactInfo = styled(ContactInfo)`
57
+ display: flex;
58
+ flex-wrap: wrap;
59
+ gap: 12px 24px;
60
+ font-size: 13px;
61
+ color: #6f5d4e;
62
+
63
+ a {
64
+ color: #b45309;
65
+ text-decoration: none;
66
+ border-bottom: 1px solid transparent;
67
+ transition: border-color 0.2s ease;
68
+
69
+ &:hover {
70
+ border-color: rgba(217, 119, 6, 0.5);
71
+ }
72
+ }
73
+ `;
74
+
75
+ export const Summary = styled.p`
76
+ margin: 0;
77
+ padding: 18px 22px;
78
+ border-radius: 16px;
79
+ background: rgba(255, 255, 255, 0.75);
80
+ border: 1px solid rgba(217, 119, 6, 0.12);
81
+ color: #4f3d31;
82
+ font-size: 15px;
83
+ line-height: 1.8;
84
+ `;
85
+
86
+ export const SectionBlock = styled(Section)`
87
+ margin-bottom: 36px;
88
+ padding-top: 24px;
89
+ border-top: 1px solid rgba(217, 119, 6, 0.18);
90
+
91
+ &:last-of-type {
92
+ margin-bottom: 0;
93
+ }
94
+ `;
95
+
96
+ export const SectionHeading = styled(SectionTitle)`
97
+ display: inline-flex;
98
+ align-items: center;
99
+ gap: 12px;
100
+ font-size: 15px;
101
+ font-weight: 700;
102
+ letter-spacing: 0.28em;
103
+ text-transform: uppercase;
104
+ color: #b45309;
105
+ margin: 0 0 20px 0;
106
+
107
+ &::before {
108
+ content: '';
109
+ display: block;
110
+ width: 36px;
111
+ height: 2px;
112
+ background: currentColor;
113
+ opacity: 0.4;
114
+ }
115
+ `;
116
+
117
+ export const Entry = styled.div`
118
+ display: flex;
119
+ flex-direction: column;
120
+ gap: 10px;
121
+ margin-bottom: 24px;
122
+
123
+ &:last-child {
124
+ margin-bottom: 0;
125
+ }
126
+ `;
127
+
128
+ export const EntryHeader = styled.div`
129
+ display: flex;
130
+ justify-content: space-between;
131
+ align-items: flex-start;
132
+ gap: 16px;
133
+
134
+ @media (max-width: 640px) {
135
+ flex-direction: column;
136
+ align-items: flex-start;
137
+ gap: 8px;
138
+ }
139
+ `;
140
+
141
+ export const EntryTitle = styled.h3`
142
+ font-size: 18px;
143
+ font-weight: 600;
144
+ margin: 0;
145
+ color: #4a3326;
146
+ letter-spacing: 0.01em;
147
+ `;
148
+
149
+ export const EntrySubtitle = styled.div`
150
+ font-size: 14px;
151
+ color: #7c6a58;
152
+ `;
153
+
154
+ export const EntryMeta = styled.div`
155
+ font-size: 12px;
156
+ font-weight: 600;
157
+ letter-spacing: 0.12em;
158
+ text-transform: uppercase;
159
+ color: #b45309;
160
+ white-space: nowrap;
161
+ `;
162
+
163
+ export const EntrySummary = styled.p`
164
+ margin: 0;
165
+ color: #4f3d31;
166
+ line-height: 1.75;
167
+ font-size: 14px;
168
+ `;
169
+
170
+ export const BulletList = styled.ul`
171
+ margin: 0;
172
+ padding: 0;
173
+ list-style: none;
174
+ display: grid;
175
+ gap: 8px;
176
+
177
+ li {
178
+ position: relative;
179
+ padding-left: 18px;
180
+ color: #4f3d31;
181
+ font-size: 14px;
182
+ line-height: 1.7;
183
+ }
184
+
185
+ li::before {
186
+ content: '';
187
+ position: absolute;
188
+ left: 0;
189
+ top: 10px;
190
+ width: 6px;
191
+ height: 6px;
192
+ border-radius: 50%;
193
+ background: ${ACCENT};
194
+ opacity: 0.6;
195
+ }
196
+ `;
197
+
198
+ export const SkillGrid = styled.div`
199
+ display: grid;
200
+ gap: 18px;
201
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
202
+ `;
203
+
204
+ export const SkillCard = styled.div`
205
+ display: flex;
206
+ flex-direction: column;
207
+ gap: 8px;
208
+ `;
209
+
210
+ export const SkillTitle = styled.div`
211
+ font-size: 15px;
212
+ font-weight: 600;
213
+ color: #4a3326;
214
+ `;
215
+
216
+ export const KeywordList = styled.div`
217
+ font-size: 13px;
218
+ color: #6f5d4e;
219
+ `;
220
+
221
+ export const Pill = styled.span`
222
+ display: inline-flex;
223
+ align-items: center;
224
+ margin-left: 8px;
225
+ padding: 4px 10px;
226
+ border-radius: 999px;
227
+ background: rgba(217, 119, 6, 0.14);
228
+ color: #8a4d0f;
229
+ font-size: 11px;
230
+ font-weight: 600;
231
+ letter-spacing: 0.08em;
232
+ text-transform: uppercase;
233
+ `;
234
+
235
+ export const Anchor = styled.a`
236
+ color: #b45309;
237
+ text-decoration: none;
238
+ border-bottom: 1px solid rgba(217, 119, 6, 0.3);
239
+ transition: border-color 0.2s ease;
240
+
241
+ &:hover {
242
+ border-color: rgba(217, 119, 6, 0.6);
243
+ }
244
+ `;