jsonresume-theme-asymmetric-timeline 1.0.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/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "jsonresume-theme-asymmetric-timeline",
3
+ "version": "1.0.0",
4
+ "description": "Cinematic storytelling with staggered timeline layout and serif-sans typography mix",
5
+ "main": "src/index.js",
6
+ "type": "module",
7
+ "license": "MIT",
8
+ "keywords": [
9
+ "jsonresume",
10
+ "theme",
11
+ "timeline",
12
+ "asymmetric",
13
+ "storytelling",
14
+ "narrative"
15
+ ],
16
+ "peerDependencies": {
17
+ "react": "^18.0.0 || ^19.0.0",
18
+ "react-dom": "^18.0.0 || ^19.0.0"
19
+ },
20
+ "dependencies": {
21
+ "styled-components": "^6.1.19",
22
+ "@resume/core": "0.1.0"
23
+ }
24
+ }
package/src/Resume.jsx ADDED
@@ -0,0 +1,130 @@
1
+ import React from 'react';
2
+ import { Header } from './components/Header.jsx';
3
+ import { TimelineItem } from './components/TimelineItem.jsx';
4
+ import { SimpleItem } from './components/SimpleSection.jsx';
5
+ import { SkillsGrid } from './components/SkillsGrid.jsx';
6
+ import {
7
+ Layout,
8
+ TimelineSection,
9
+ StyledSectionTitle,
10
+ GridSection,
11
+ SimpleSection,
12
+ } from './styles.jsx';
13
+
14
+ function Resume({ resume }) {
15
+ const {
16
+ basics = {},
17
+ work = [],
18
+ education = [],
19
+ skills = [],
20
+ projects = [],
21
+ volunteer = [],
22
+ awards = [],
23
+ publications = [],
24
+ languages = [],
25
+ interests = [],
26
+ references = [],
27
+ } = resume;
28
+
29
+ return (
30
+ <Layout>
31
+ <Header basics={basics} />
32
+
33
+ {work?.length > 0 && (
34
+ <TimelineSection>
35
+ <StyledSectionTitle>Experience</StyledSectionTitle>
36
+ {work.map((job, index) => (
37
+ <TimelineItem
38
+ key={index}
39
+ item={job}
40
+ isLeft={index % 2 === 0}
41
+ type="work"
42
+ />
43
+ ))}
44
+ </TimelineSection>
45
+ )}
46
+
47
+ {education?.length > 0 && (
48
+ <TimelineSection>
49
+ <StyledSectionTitle>Education</StyledSectionTitle>
50
+ {education.map((edu, index) => (
51
+ <TimelineItem
52
+ key={index}
53
+ item={edu}
54
+ isLeft={index % 2 === 0}
55
+ type="education"
56
+ />
57
+ ))}
58
+ </TimelineSection>
59
+ )}
60
+
61
+ {projects?.length > 0 && (
62
+ <SimpleSection>
63
+ <StyledSectionTitle>Projects</StyledSectionTitle>
64
+ {projects.map((project, index) => (
65
+ <SimpleItem key={index} item={project} type="project" />
66
+ ))}
67
+ </SimpleSection>
68
+ )}
69
+
70
+ {skills?.length > 0 && (
71
+ <GridSection>
72
+ <StyledSectionTitle>Skills</StyledSectionTitle>
73
+ <SkillsGrid items={skills} type="skills" />
74
+ </GridSection>
75
+ )}
76
+
77
+ {volunteer?.length > 0 && (
78
+ <SimpleSection>
79
+ <StyledSectionTitle>Volunteer</StyledSectionTitle>
80
+ {volunteer.map((vol, index) => (
81
+ <SimpleItem key={index} item={vol} type="volunteer" />
82
+ ))}
83
+ </SimpleSection>
84
+ )}
85
+
86
+ {awards?.length > 0 && (
87
+ <SimpleSection>
88
+ <StyledSectionTitle>Awards</StyledSectionTitle>
89
+ {awards.map((award, index) => (
90
+ <SimpleItem key={index} item={award} type="award" />
91
+ ))}
92
+ </SimpleSection>
93
+ )}
94
+
95
+ {publications?.length > 0 && (
96
+ <SimpleSection>
97
+ <StyledSectionTitle>Publications</StyledSectionTitle>
98
+ {publications.map((pub, index) => (
99
+ <SimpleItem key={index} item={pub} type="publication" />
100
+ ))}
101
+ </SimpleSection>
102
+ )}
103
+
104
+ {languages?.length > 0 && (
105
+ <GridSection>
106
+ <StyledSectionTitle>Languages</StyledSectionTitle>
107
+ <SkillsGrid items={languages} type="languages" />
108
+ </GridSection>
109
+ )}
110
+
111
+ {interests?.length > 0 && (
112
+ <GridSection>
113
+ <StyledSectionTitle>Interests</StyledSectionTitle>
114
+ <SkillsGrid items={interests} type="interests" />
115
+ </GridSection>
116
+ )}
117
+
118
+ {references?.length > 0 && (
119
+ <SimpleSection>
120
+ <StyledSectionTitle>References</StyledSectionTitle>
121
+ {references.map((ref, index) => (
122
+ <SimpleItem key={index} item={ref} type="reference" />
123
+ ))}
124
+ </SimpleSection>
125
+ )}
126
+ </Layout>
127
+ );
128
+ }
129
+
130
+ export default Resume;
@@ -0,0 +1,57 @@
1
+ import React from 'react';
2
+ import styled from 'styled-components';
3
+ import { ContactInfo } from '@resume/core';
4
+
5
+ const HeaderContainer = styled.header`
6
+ text-align: center;
7
+ margin-bottom: 60px;
8
+ padding-bottom: 40px;
9
+ border-bottom: 1px solid #e2e8f0;
10
+ `;
11
+
12
+ const Name = styled.h1`
13
+ font-family: 'Lora', Georgia, serif;
14
+ font-size: 48px;
15
+ font-weight: 600;
16
+ color: #0f172a;
17
+ margin: 0 0 12px 0;
18
+ letter-spacing: -0.5px;
19
+ `;
20
+
21
+ const Label = styled.div`
22
+ font-size: 18px;
23
+ font-weight: 300;
24
+ color: #64748b;
25
+ margin-bottom: 24px;
26
+ letter-spacing: 0.5px;
27
+ `;
28
+
29
+ const StyledContactInfo = styled(ContactInfo)`
30
+ font-size: 14px;
31
+ font-weight: 300;
32
+ justify-content: center;
33
+
34
+ a {
35
+ font-size: 14px;
36
+ }
37
+ `;
38
+
39
+ const Summary = styled.p`
40
+ font-size: 16px;
41
+ line-height: 1.8;
42
+ color: #475569;
43
+ margin: 24px auto 0;
44
+ max-width: 700px;
45
+ font-weight: 300;
46
+ `;
47
+
48
+ export function Header({ basics }) {
49
+ return (
50
+ <HeaderContainer>
51
+ <Name>{basics.name}</Name>
52
+ {basics.label && <Label>{basics.label}</Label>}
53
+ <StyledContactInfo basics={basics} />
54
+ {basics.summary && <Summary>{basics.summary}</Summary>}
55
+ </HeaderContainer>
56
+ );
57
+ }
@@ -0,0 +1,98 @@
1
+ import React from 'react';
2
+ import styled from 'styled-components';
3
+ import { DateRange } from '@resume/core';
4
+
5
+ const Item = styled.div`
6
+ margin-bottom: 28px;
7
+ padding: 20px;
8
+ background: #f8fafc;
9
+ border-radius: 6px;
10
+
11
+ &:last-child {
12
+ margin-bottom: 0;
13
+ }
14
+ `;
15
+
16
+ const Title = styled.h3`
17
+ font-family: 'Lora', Georgia, serif;
18
+ font-size: 18px;
19
+ font-weight: 600;
20
+ color: #0f172a;
21
+ margin: 0 0 8px 0;
22
+ `;
23
+
24
+ const Subtitle = styled.div`
25
+ font-size: 15px;
26
+ color: #475569;
27
+ margin-bottom: 6px;
28
+ `;
29
+
30
+ const DateText = styled.div`
31
+ font-size: 13px;
32
+ font-weight: 300;
33
+ color: #64748b;
34
+ `;
35
+
36
+ const Summary = styled.p`
37
+ margin: 12px 0;
38
+ color: #475569;
39
+ line-height: 1.7;
40
+ font-size: 15px;
41
+ font-weight: 300;
42
+ `;
43
+
44
+ const Highlights = styled.ul`
45
+ margin: 12px 0 0 0;
46
+ padding-left: 20px;
47
+ list-style-type: disc;
48
+
49
+ li {
50
+ margin: 8px 0;
51
+ color: #475569;
52
+ line-height: 1.7;
53
+ padding-left: 4px;
54
+ font-weight: 300;
55
+ }
56
+ `;
57
+
58
+ export function SimpleItem({ item, type }) {
59
+ return (
60
+ <Item>
61
+ <Title>
62
+ {type === 'project' && item.name}
63
+ {type === 'volunteer' && item.position}
64
+ {type === 'award' && item.title}
65
+ {type === 'publication' && item.name}
66
+ {type === 'reference' && item.name}
67
+ </Title>
68
+ {type === 'volunteer' && item.organization && (
69
+ <Subtitle>{item.organization}</Subtitle>
70
+ )}
71
+ {type === 'award' && item.awarder && (
72
+ <Subtitle>Awarded by {item.awarder}</Subtitle>
73
+ )}
74
+ {type === 'publication' && item.publisher && (
75
+ <Subtitle>Published by {item.publisher}</Subtitle>
76
+ )}
77
+ {type === 'volunteer' && (item.startDate || item.endDate) && (
78
+ <DateText>
79
+ <DateRange startDate={item.startDate} endDate={item.endDate} />
80
+ </DateText>
81
+ )}
82
+ {type === 'award' && item.date && <DateText>{item.date}</DateText>}
83
+ {type === 'publication' && item.releaseDate && (
84
+ <DateText>{item.releaseDate}</DateText>
85
+ )}
86
+ {item.summary && <Summary>{item.summary}</Summary>}
87
+ {item.description && <Summary>{item.description}</Summary>}
88
+ {item.reference && <Summary>{item.reference}</Summary>}
89
+ {item.highlights?.length > 0 && (
90
+ <Highlights>
91
+ {item.highlights.map((highlight, i) => (
92
+ <li key={i}>{highlight}</li>
93
+ ))}
94
+ </Highlights>
95
+ )}
96
+ </Item>
97
+ );
98
+ }
@@ -0,0 +1,52 @@
1
+ import React from 'react';
2
+ import styled from 'styled-components';
3
+
4
+ const Grid = styled.div`
5
+ display: grid;
6
+ grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
7
+ gap: 20px;
8
+ `;
9
+
10
+ const Category = styled.div`
11
+ padding: 20px;
12
+ background: #f8fafc;
13
+ border-radius: 6px;
14
+ border-left: 3px solid #334155;
15
+ `;
16
+
17
+ const Name = styled.h4`
18
+ font-size: 16px;
19
+ font-weight: 500;
20
+ color: #0f172a;
21
+ margin: 0 0 10px 0;
22
+ `;
23
+
24
+ const Tags = styled.div`
25
+ font-size: 14px;
26
+ font-weight: 300;
27
+ color: #64748b;
28
+ line-height: 1.6;
29
+ `;
30
+
31
+ export function SkillsGrid({ items, type = 'skills' }) {
32
+ return (
33
+ <Grid>
34
+ {items.map((item, index) => (
35
+ <Category key={index}>
36
+ <Name>
37
+ {type === 'skills' && item.name}
38
+ {type === 'languages' && item.language}
39
+ {type === 'interests' && item.name}
40
+ </Name>
41
+ {type === 'skills' && item.keywords?.length > 0 && (
42
+ <Tags>{item.keywords.join(', ')}</Tags>
43
+ )}
44
+ {type === 'languages' && item.fluency && <Tags>{item.fluency}</Tags>}
45
+ {type === 'interests' && item.keywords?.length > 0 && (
46
+ <Tags>{item.keywords.join(', ')}</Tags>
47
+ )}
48
+ </Category>
49
+ ))}
50
+ </Grid>
51
+ );
52
+ }
@@ -0,0 +1,148 @@
1
+ import React from 'react';
2
+ import styled from 'styled-components';
3
+ import { DateRange } from '@resume/core';
4
+
5
+ const Item = styled.div`
6
+ position: relative;
7
+ margin-bottom: 60px;
8
+ display: flex;
9
+ justify-content: ${(props) => (props.$isLeft ? 'flex-start' : 'flex-end')};
10
+
11
+ /* Timeline dot */
12
+ &::before {
13
+ content: '';
14
+ position: absolute;
15
+ left: 50%;
16
+ top: 0;
17
+ width: 16px;
18
+ height: 16px;
19
+ background: #334155;
20
+ border: 4px solid white;
21
+ border-radius: 50%;
22
+ transform: translateX(-50%);
23
+ z-index: 2;
24
+ box-shadow: 0 0 0 2px #334155;
25
+ }
26
+
27
+ /* Connector line from dot to content */
28
+ &::after {
29
+ content: '';
30
+ position: absolute;
31
+ top: 8px;
32
+ width: calc(50% - 40px);
33
+ height: 2px;
34
+ background: #64748b;
35
+ ${(props) =>
36
+ props.$isLeft
37
+ ? `
38
+ left: 32px;
39
+ `
40
+ : `
41
+ right: 32px;
42
+ `}
43
+ }
44
+
45
+ @media (max-width: 768px) {
46
+ justify-content: flex-start;
47
+ padding-left: 40px;
48
+
49
+ &::before {
50
+ left: 0;
51
+ transform: none;
52
+ }
53
+
54
+ &::after {
55
+ left: 16px;
56
+ width: 24px;
57
+ }
58
+ }
59
+ `;
60
+
61
+ const Content = styled.div`
62
+ width: calc(50% - 60px);
63
+ padding: 24px 32px;
64
+ background: #f8fafc;
65
+ border-radius: 8px;
66
+ border-left: 4px solid #64748b;
67
+
68
+ @media (max-width: 768px) {
69
+ width: 100%;
70
+ padding: 20px 24px;
71
+ }
72
+ `;
73
+
74
+ const Position = styled.h3`
75
+ font-family: 'Lora', Georgia, serif;
76
+ font-size: 22px;
77
+ font-weight: 600;
78
+ color: #0f172a;
79
+ margin: 0 0 8px 0;
80
+ line-height: 1.3;
81
+ `;
82
+
83
+ const Company = styled.div`
84
+ font-size: 16px;
85
+ color: #334155;
86
+ font-weight: 500;
87
+ margin-bottom: 8px;
88
+ `;
89
+
90
+ const DateText = styled.div`
91
+ font-size: 13px;
92
+ font-weight: 300;
93
+ color: #64748b;
94
+ margin-bottom: 16px;
95
+ letter-spacing: 0.3px;
96
+ `;
97
+
98
+ const Summary = styled.p`
99
+ margin: 12px 0;
100
+ color: #475569;
101
+ line-height: 1.7;
102
+ font-size: 15px;
103
+ font-weight: 300;
104
+ `;
105
+
106
+ const Highlights = styled.ul`
107
+ margin: 12px 0 0 0;
108
+ padding-left: 20px;
109
+ list-style-type: disc;
110
+
111
+ li {
112
+ margin: 8px 0;
113
+ color: #475569;
114
+ line-height: 1.7;
115
+ padding-left: 4px;
116
+ font-weight: 300;
117
+ }
118
+ `;
119
+
120
+ export function TimelineItem({ item, isLeft, type = 'work' }) {
121
+ return (
122
+ <Item $isLeft={isLeft}>
123
+ <Content>
124
+ <Position>
125
+ {type === 'work' ? item.position : item.institution}
126
+ </Position>
127
+ {type === 'work' && item.name && <Company>{item.name}</Company>}
128
+ {type === 'education' && (
129
+ <Company>
130
+ {item.studyType} in {item.area}
131
+ {item.score && ` • ${item.score}`}
132
+ </Company>
133
+ )}
134
+ <DateText>
135
+ <DateRange startDate={item.startDate} endDate={item.endDate} />
136
+ </DateText>
137
+ {item.summary && <Summary>{item.summary}</Summary>}
138
+ {item.highlights?.length > 0 && (
139
+ <Highlights>
140
+ {item.highlights.map((highlight, i) => (
141
+ <li key={i}>{highlight}</li>
142
+ ))}
143
+ </Highlights>
144
+ )}
145
+ </Content>
146
+ </Item>
147
+ );
148
+ }
package/src/index.js ADDED
@@ -0,0 +1,53 @@
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=Inter:wght@300;400;500;600&family=Lora:wght@400;500;600&display=swap" rel="stylesheet">
24
+ ${styles}
25
+ <style>
26
+ * {
27
+ box-sizing: border-box;
28
+ margin: 0;
29
+ padding: 0;
30
+ }
31
+ body {
32
+ margin: 0;
33
+ padding: 0;
34
+ background: #f1f5f9;
35
+ }
36
+ @media print {
37
+ body {
38
+ background: white;
39
+ }
40
+ @page {
41
+ margin: 0.5in;
42
+ }
43
+ }
44
+ </style>
45
+ </head>
46
+ <body>
47
+ ${html}
48
+ </body>
49
+ </html>`;
50
+ } finally {
51
+ sheet.seal();
52
+ }
53
+ }
package/src/styles.jsx ADDED
@@ -0,0 +1,50 @@
1
+ import styled from 'styled-components';
2
+ import { Section, SectionTitle } from '@resume/core';
3
+
4
+ export const Layout = styled.div`
5
+ max-width: 900px;
6
+ margin: 0 auto;
7
+ padding: 60px 40px;
8
+ background: white;
9
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI',
10
+ sans-serif;
11
+ color: #1e293b;
12
+
13
+ @media print {
14
+ padding: 40px;
15
+ }
16
+ `;
17
+
18
+ export const TimelineSection = styled(Section)`
19
+ position: relative;
20
+ padding-left: 0;
21
+
22
+ /* Central timeline line */
23
+ &::before {
24
+ content: '';
25
+ position: absolute;
26
+ left: 50%;
27
+ top: 0;
28
+ bottom: 0;
29
+ width: 3px;
30
+ background: #334155;
31
+ transform: translateX(-50%);
32
+ }
33
+ `;
34
+
35
+ export const StyledSectionTitle = styled(SectionTitle)`
36
+ font-family: 'Lora', Georgia, serif;
37
+ font-size: 28px;
38
+ font-weight: 600;
39
+ color: #0f172a;
40
+ margin: 50px 0 40px 0;
41
+ text-align: center;
42
+ `;
43
+
44
+ export const GridSection = styled(Section)`
45
+ margin-top: 50px;
46
+ `;
47
+
48
+ export const SimpleSection = styled(Section)`
49
+ margin-top: 40px;
50
+ `;