This commit is contained in:
Morten Olsen
2023-03-26 22:15:07 +02:00
commit 9b1a067d56
80 changed files with 7889 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
\documentclass{article}
\usepackage{graphicx}
\usepackage{hyperref}
\title{<%-article.title%>}
\begin{document}
\maketitle
\includegraphics[width=0.5\textwidth]{<%-article.cover%>}
<%-article.body%>
\end{document}

View File

@@ -0,0 +1,149 @@
\documentclass[10pt, a4paper]{article}
\usepackage[top=2cm, bottom=2cm, left=2cm, right=2cm]{geometry}
\usepackage{graphicx}
\usepackage{hyperref}
\usepackage{calc}
\usepackage{multicol}
\usepackage{fancyhdr}
\setlength{\columnseprule}{0.1pt}
%\setlength{\columnsep}{1.5cm}
\def \columncount {2}
\def \skillcolumncount {2}
\pagestyle{fancy}
\fancyhf{}
\rhead{<%-profile.name%> \today}
\lhead{Curriculum Vitae}
\rfoot{Page \thepage}
\newenvironment{columns}{
\ifnum\columncount>1
\begin{multicols}{\columncount}
\fi
}{
\ifnum\columncount>1
\end{multicols}
\fi
\vspace{0.5cm}
\hrule
}
\newcommand{\cvinfo}[2]{
\noindent \textbf{#1}\dotfill#2
}
\newenvironment{cvtitle}[3]{
\noindent\begin{minipage}{\textwidth}
\noindent\begin{minipage}{\textwidth - 3.2cm}
\Huge #1\newline\large #3
\end{minipage}
\noindent\begin{minipage}{3cm}
\begin{flushright}
\includegraphics[height=3cm]{#2}
\end{flushright}
\end{minipage}
\vspace{0.5cm}
\hrule
\vspace{0.5cm}
\ifnum\skillcolumncount>1
\begin{multicols}{\skillcolumncount}
\fi
}{
\ifnum\skillcolumncount>1
\end{multicols}
\fi
\end{minipage}
\hfill
\begin{minipage}{\textwidth/3-2cm}
\end{minipage}
\vspace{1cm}
\hrule
}
\newenvironment{cvskills}{
\noindent\begin{minipage}{\textwidth}
\ifnum\skillcolumncount>1
\begin{multicols}{\skillcolumncount}
\fi
}{
\ifnum\skillcolumncount>1
\end{multicols}
\fi
\vspace{0.5cm}
\hrule
\end{minipage}
}
\newenvironment{cvbox}[3]
{
\noindent\begin{columns}
\noindent{\Large \textbf{#1}} \hfill {\small #2} \\
\textit{#3}
\ifnum\columncount>2
\vfill\null\columnbreak
\else
\\\\
\fi
}
{
\end{columns}
%\end{minipage}
\vspace{0.5cm}
}
\newcommand{\cvskill}[2]{
\textbf{#1}\dotfill
\textit{#2}
}
\newenvironment{cvexp}[4]
{ \begin{cvbox}{#1}{#2 - #3}{#4} }
{\end{cvbox}}
\newenvironment{cvproj}[3]
{
\noindent
\begin{columns}
\noindent{\Large \textbf{#1}} \\ {\small #3} \\
{\tiny\textit{#2}}
\ifnum\columncount>2
\vfill\null\columnbreak
\else
\\\\
\fi
}
{
\end{columns}
\vspace{0.5cm}
}
\begin{document}
\begin{cvtitle}{<%-profile.name%>}{<%-profile.imagePath%>}{<%-profile.tagline%>}
<% for (let info of profile.info) { %>
\cvinfo{<%-info.name%>}{<%-info.value%>}
<% } %>
\end{cvtitle}
\begin{columns}
\section*{Who am I?}
<%-profile.about%>
\end{columns}
\section*{Platform and languages}
Platforms and languages which I have worked with. The list is a shortened down version\\\\
\begin{cvskills}
<% for (let skill of profile.skills.sort((a, b) => b.level - a.level)) { %>
\cvskill{<%-skill.name%>}{<%-skill.level%>}
<% } %>
\end{cvskills}
\section*{Experience}
<% for (let exp of positions.sort((a, b) => new Date(b.from) - new Date(a.from))) { %>
\begin{cvexp}{<%-exp.company%>}{<%-exp.from%>}{<%-exp.to%>}{<%-exp.title%>}
<%-exp.content%>
\end{cvexp}
<% } %>
\end{document}

View File

@@ -0,0 +1,292 @@
import styled, { createGlobalStyle } from "styled-components";
import ReactMarkdown from "react-markdown";
import { Jumbo } from "./typography";
import { createTheme, ThemeProvider } from "./theme";
import { Helmet } from "react-helmet-async";
import { Page } from "types";
const GlobalStyle = createGlobalStyle`
* { box-sizing: border-box; }
body, html { height: 100%; margin: 0; }
body {
font-size: 17px;
background-color: ${({ theme }) => theme.colors.background};
color: ${({ theme }) => theme.colors.foreground};
}
`;
const Title = styled(Jumbo)`
display: block;
padding: 5rem 2rem;
display: flex;
align-items: flex-end;
flex-wrap: wrap;
min-height: 100%;
margin-bottom: 10rem;
margin-top: 10rem;
@media only screen and (max-width: 900px) {
padding-bottom: 2rem;
}
`;
const ArticleTitleWord = styled(Jumbo)`
font-size: 4rem;
line-height: 4.1rem;
display: inline-block;
padding: 0 15px;
text-transform: uppercase;
margin: 10px;
font-family: "Black Ops One", sans-serif;
background: ${({ theme }) => theme.colors.primary};
color: ${({ theme }) => theme.colors.foreground};
@media only screen and (max-width: 900px) {
font-size: 2.5rem;
line-height: 3.1rem;
}
@media only screen and (max-width: 700px) {
padding: 5px;
font-size: 2rem;
line-height: 2.1rem;
}
`;
const Wrapper = styled.div`
display: flex;
height: 100%;
`;
const ArticleWrapper = styled.article`
font-size: 1.1rem;
font-family: "Merriweather", serif;
> p,
ul,
ol {
letter-spacing: 0.08rem;
line-height: 2.1rem;
text-align: justify;
max-width: 700px;
margin: 2rem 4rem;
list-style-position: inside;
background: ${({ theme }) => theme.colors.background};
@media only screen and (max-width: 700px) {
margin: 2rem 2rem;
}
}
> p:first-of-type {
display: block;
margin-bottom: 2rem;
padding-bottom: 2rem;
}
> p:first-of-type::first-letter {
font-family: "Black Ops One", sans-serif;
border: solid 5px ${({ theme }) => theme.colors.foreground};
margin: 0 1rem 0 0;
font-size: 6rem;
float: left;
clear: left;
padding: 1rem;
}
li {
padding: 0.5rem 0;
margin: 0;
}
img {
max-width: 100%;
margin: auto;
}
> p + p::first-letter {
margin-left: 1.8rem;
}
h1,
h2,
h3,
h4,
h5,
h6 {
max-width: 350px;
margin-left: -100px;
float: left;
padding: 20px;
padding-right: 40px;
shape-outside: padding-box;
position: relative;
font-family: "Black Ops One", sans-serif;
text-transform: uppercase;
display: flex;
align-items: flex-start;
@media only screen and (max-width: 900px) {
width: 100%;
margin-left: 0;
shape-outside: inherit;
float: none;
}
&:after {
position: absolute;
content: "";
right: 20px;
top: 0;
bottom: 0;
border: solid 5px ${({ theme }) => theme.colors.foreground};
left: 0;
@media only screen and (max-width: 900px) {
border: none;
}
}
}
blockquote {
width: 350px;
font-size: 3rem;
margin-left: -100px;
float: left;
padding: 50px;
shape-outside: padding-box;
position: relative;
text-transform: uppercase;
@media only screen and (max-width: 900px) {
width: 100%;
margin-left: 0;
}
&:before {
color: ${({ theme }) => theme.colors.primary};
content: "\\00BB";
float: left;
font-size: 6rem;
}
&:after {
position: absolute;
content: "";
right: 20px;
top: 0;
bottom: 0;
border-right: 5px solid;
border-color: ${({ theme }) => theme.colors.primary};
}
}
`;
const Content = styled.div`
margin-right: 40%;
max-width: 700px;
width: 100%;
position: absolute;
right: 0;
@media only screen and (max-width: 900px) {
margin-right: 0;
}
`;
const Side = styled.aside`
position: fixed;
right: 0;
top: 0;
width: 40%;
height: 100%;
clip-path: polygon(40% 0%, 100% 0%, 100% 100%, 0% 100%, 0% 50%);
@media only screen and (max-width: 900px) {
position: static;
width: 100%;
height: 350px;
clip-path: none;
}
`;
const Cover = styled.div<{ src: string }>`
background: url(${({ src }) => src});
background-size: cover;
background-position: center;
width: 100%;
height: 100%;
`;
const Download = styled.a`
display: inline-block;
background: ${({ theme }) => theme.colors.primary};
color: ${({ theme }) => theme.colors.foreground};
text-align: center;
padding: 1rem;
font-size: 1rem;
font-family: "Black Ops One", sans-serif;
text-transform: uppercase;
text-decoration: none;
`;
const Author = styled.a`
text-transform: uppercase;
font-family: "Black Ops One", sans-serif;
font-size: 2rem;
margin: 1rem;
display: inline-block;
position: relative;
text-decoration: none;
color: ${({ theme }) => theme.colors.foreground};
&:after {
content: "";
border-bottom: solid 15px ${({ theme }) => theme.colors.primary};
display: block;
width: 100%;
height: 5px;
bottom: 0px;
z-index: -1;
position: absolute;
}
`;
const ArticlePage: Page<"article"> = ({ article, profile, pdfUrl }) => {
return (
<ThemeProvider theme={createTheme({ baseColor: article.color })}>
<Helmet>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link
rel="preconnect"
href="https://fonts.gstatic.com"
crossOrigin="anonymous"
/>
<link
href="https://fonts.googleapis.com/css2?family=Archivo+Black&family=Black+Ops+One&family=Merriweather:wght@400;700&display=swap"
rel="stylesheet"
/>
</Helmet>
<GlobalStyle />
<Wrapper>
<Content>
<Title>
{article.title.split(" ").map((word, index) => (
<ArticleTitleWord key={index}>{word}</ArticleTitleWord>
))}
<Author href="/">by {profile.name}</Author>
</Title>
<Download href={pdfUrl} target="_blank" rel="noreferrer">
Download PDF
</Download>
<ArticleWrapper>
<ReactMarkdown>{article.content}</ReactMarkdown>
</ArticleWrapper>
</Content>
<Side>
<Cover src={article.coverUrl} />
</Side>
</Wrapper>
</ThemeProvider>
);
};
export default ArticlePage;

View File

@@ -0,0 +1,77 @@
import React, { useMemo } from "react";
import styled from "styled-components";
import ArticlePreview from "../preview";
import { JumboArticlePreview } from "../preview/jumbo";
import { MiniArticlePreview } from "../preview/mini";
import { Article } from "types";
type Props = {
articles: Article[];
};
const Wrapper = styled.div`
width: 100%;
`;
const FeaturedArticle = styled.div`
display: flex;
flex-wrap: wrap;
margin: 0 auto;
align-items: center;
justify-content: center;
width: 100%;
`;
const FeaturedArticles = styled.div`
display: flex;
flex-direction: row;
margin: 0 auto;
width: 100%;
flex-wrap: wrap;
`;
const RemainingArticles = styled.div`
display: flex;
flex-direction: row;
flex-wrap: wrap;
margin: 0 auto;
width: 100%;
`;
const ArticleGrid: React.FC<Props> = ({ articles }) => {
const sorted = useMemo(
() => articles,
// TODO:
// articles.sort(
// (a, b) =>
// new Date(b.published).getTime() -
// new Date(a.published).getTime()
// ),
[articles]
);
const featured1 = useMemo(() => sorted.slice(0, 1)[0], [sorted]);
const featured2 = useMemo(() => sorted.slice(1, 4), [sorted]);
const remaining = useMemo(() => sorted.slice(4, 12), [sorted]);
return (
<Wrapper>
<FeaturedArticle>
<JumboArticlePreview article={featured1} />
</FeaturedArticle>
<FeaturedArticles>
{featured2.map((article) => (
<ArticlePreview key={article.title} article={article} />
))}
</FeaturedArticles>
<RemainingArticles>
{remaining.map((article) => (
<MiniArticlePreview key={article.title} article={article} />
))}
</RemainingArticles>
</Wrapper>
);
};
export { ArticleGrid };

View File

@@ -0,0 +1,82 @@
import React, { useMemo } from "react";
import styled from "styled-components";
import { Title1 } from "@/typography";
import { createTheme } from "@/theme/create";
import { ThemeProvider } from "@/theme/provider";
import { Article } from "types";
type Props = {
article: Article;
};
const Wrapper = styled.a`
height: 500px;
border-right: 2px solid rgba(0, 0, 0, 0.1);
flex: 1;
min-width: 200px;
position: relative;
margin: 15px;
cursor: pointer;
display: flex;
flex-direction: column;
@media only screen and (max-width: 700px) {
max-height: 300px;
}
`;
const Title = styled(Title1)`
background: ${({ theme }) => theme.colors.primary};
line-height: 40px;
font-family: "Black Ops One", sans-serif;
font-size: 25px;
padding: 0 5px;
margin: 5px 0;
`;
const MetaWrapper = styled.div`
top: 10px;
left: 10px;
right: 10px;
display: flex;
flex-wrap: wrap;
`;
const AsideWrapper = styled.aside<{
image?: string;
}>`
background: ${({ theme }) => theme.colors.primary};
background-size: cover;
background-position: center;
${({ image }) => (image ? `background-image: url(${image});` : "")}
flex: 1;
top: 0;
bottom: 0;
right: 0;
left: 0;
}
`;
const ArticlePreview: React.FC<Props> = ({ article }) => {
const theme = useMemo(
() =>
createTheme({
baseColor: article.color,
}),
[article.color]
);
return (
<ThemeProvider theme={theme}>
<Wrapper href={`/articles/${article.slug}`}>
<AsideWrapper image={article.thumbUrl} />
<MetaWrapper>
{article.title.split(" ").map((word, index) => (
<Title key={index}>{word}</Title>
))}
</MetaWrapper>
</Wrapper>
</ThemeProvider>
);
};
export default ArticlePreview;

View File

@@ -0,0 +1,79 @@
import React from "react";
import styled from "styled-components";
import { Title1, Body1 } from "@/typography";
import { Article } from "types";
type Props = {
article: Article;
};
const Wrapper = styled.a`
height: 300px;
flex: 1;
position: relative;
margin: 15px;
cursor: pointer;
display: flex;
background: ${({ theme }) => theme.colors.background};
@media only screen and (max-width: 700px) {
flex-direction: column;
height: 500px;
}
`;
const Title = styled(Title1)`
line-height: 40px;
font-family: "Black Ops One", sans-serif;
font-size: 25px;
padding: 0 5px;
margin: 5px 0;
`;
const Summery = styled(Body1)`
max-width: 300px;
padding: 0 5px;
margin: 5px 0;
overflow: hidden;
letter-spacing: 0.5px;
line-height: 2.1rem;
@media only screen and (max-width: 700px) {
max-height: 100px;
}
`;
const MetaWrapper = styled.div`
display: flex;
flex-direction: column;
padding: 40px;
`;
const AsideWrapper = styled.aside<{
image?: string;
}>`
background: ${({ theme }) => theme.colors.primary};
background-size: cover;
background-position: center;
${({ image }) => (image ? `background-image: url(${image});` : "")}
flex: 1;
top: 0;
bottom: 0;
right: 0;
left: 0;
}
`;
const JumboArticlePreview: React.FC<Props> = ({ article }) => {
return (
<Wrapper href={`/articles/${article.slug}`}>
<AsideWrapper image={article.coverUrl} />
<MetaWrapper>
<Title>{article.title}</Title>
<Summery>{article.content}</Summery>
</MetaWrapper>
</Wrapper>
);
};
export { JumboArticlePreview };

View File

@@ -0,0 +1,80 @@
import React, { useMemo } from "react";
import styled from "styled-components";
import { Title1 } from "@/typography";
import { createTheme } from "@/theme/create";
import { ThemeProvider } from "@/theme/provider";
import { Article } from "types";
type Props = {
article: Article;
};
const Wrapper = styled.a`
position: relative;
margin: 15px;
cursor: pointer;
display: flex;
width: 220px;
height: 200px;
@media only screen and (max-width: 700px) {
width: 100%;
}
`;
const Title = styled(Title1)`
line-height: 20px;
font-size: 20px;
padding: 5px 5px;
font-family: "Black Ops One", sans-serif;
margin: 5px 0;
background: ${({ theme }) => theme.colors.background};
`;
const MetaWrapper = styled.div`
display: flex;
flex-direction: row;
flex-wrap: wrap;
padding: 10px;
max-width: 220px;
position: absolute;
`;
const AsideWrapper = styled.aside<{
image?: string;
}>`
background: ${({ theme }) => theme.colors.primary};
background-size: cover;
background-position: center;
${({ image }) => (image ? `background-image: url(${image});` : "")}
position: absolute;
top: 0;
bottom: 0;
right: 0;
left: 0;
}
`;
const MiniArticlePreview: React.FC<Props> = ({ article }) => {
const theme = useMemo(
() =>
createTheme({
baseColor: article.color,
}),
[article.color]
);
return (
<ThemeProvider theme={theme}>
<Wrapper href={`/articles/${article.slug}`}>
<AsideWrapper image={article.thumbUrl} />
<MetaWrapper>
{article.title.split(" ").map((word, index) => (
<Title key={index}>{word}</Title>
))}
</MetaWrapper>
</Wrapper>
</ThemeProvider>
);
};
export { MiniArticlePreview };

View File

@@ -0,0 +1,30 @@
import { FC, ReactNode } from "react"
type HtmlProps = {
body: ReactNode;
head: ReactNode;
scripts: string[];
};
const Html: FC<HtmlProps> = ({ body, head, scripts }) => {
return (
<html>
<head>
<title>My App</title>
{head}
{scripts.map((script, index) => (
<script key={index} src={script} />
))}
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous"/>
<link href="https://fonts.googleapis.com/css2?family=Archivo+Black&family=Black+Ops+One&family=Merriweather:wght@400;700&display=swap" rel="stylesheet" />
</head>
<body>
<div id="root">{body}</div>
</body>
</html>
);
};
export { Html };

View File

@@ -0,0 +1,65 @@
import React, { ReactNode, useMemo } from "react";
import styled from "styled-components";
import { createTheme } from "@/theme/create";
import { ThemeProvider } from "@/theme/provider";
const Wrapper = styled.div`
background: ${({ theme }) => theme.colors.background};
color: ${({ theme }) => theme.colors.foreground};
min-height: 100%;
position: relative;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
`;
const BackgroundWrapper = styled.div<{
image?: string;
}>`
position: absolute;
left: 0;
top: 0;
bottom: 0;
right: 0;
background-size: cover;
background-position: center;
opacity: 0.2;
${({ image }) => (image ? `background-image: url(${image});` : "")}
`;
const Content = styled.div`
z-index: 1;
display: flex;
max-width: 1000px;
width: 100%;
align-items: center;
justify-content: center;
flex-direction: column;
`;
type Props = {
children: ReactNode;
background?: string;
color?: string;
};
const Sheet: React.FC<Props> = ({ color, background, children }) => {
const theme = useMemo(
() =>
createTheme({
baseColor: color,
}),
[color]
);
return (
<ThemeProvider theme={theme}>
<Wrapper>
<BackgroundWrapper image={background} />
<Content>{children}</Content>
</Wrapper>
</ThemeProvider>
);
};
export { Sheet };

View File

@@ -0,0 +1,160 @@
import styled, { createGlobalStyle } from "styled-components";
import { ArticleGrid } from "@/components/article/grid";
import { Jumbo } from "@/typography";
import { useMemo } from "react";
import { Sheet } from "./components/sheet";
import { ThemeProvider, createTheme } from "./theme";
import chroma from "chroma-js";
import { Helmet } from "react-helmet-async";
import { Page } from "../../../types";
const GlobalStyle = createGlobalStyle`
* { box-sizing: border-box; }
body, html { height: 100%; margin: 0; }
body {
font-size: 17px;
background-color: ${({ theme }) => theme.colors.background};
color: ${({ theme }) => theme.colors.foreground};
}
`;
const Hero = styled.div`
display: flex;
flex-wrap: wrap;
`;
const Download = styled.a`
font-size: 30px;
line-height: 40px;
display: inline-block;
background: ${({ theme }) => theme.colors.foreground};
color: ${({ theme }) => theme.colors.primary};
padding: 0 15px;
text-transform: uppercase;
margin: 10px;
font-family: "Black Ops One", sans-serif;
@media only screen and (max-width: 700px) {
margin: 5px;
font-size: 3rem;
line-height: 3.1rem;
}
`;
const Title = styled(Jumbo)`
font-size: 60px;
line-height: 80px;
display: inline-block;
background: ${({ theme }) => theme.colors.primary};
color: ${({ theme }) => theme.colors.foreground};
padding: 0 15px;
text-transform: uppercase;
margin: 10px;
font-family: "Black Ops One", sans-serif;
@media only screen and (max-width: 700px) {
margin: 5px;
font-size: 3rem;
line-height: 3.1rem;
}
`;
const Arrow = styled.div`
position: absolute;
bottom: 20px;
display: flex;
align-items: center;
justify-content: center;
:after {
display: flex;
align-items: center;
justify-content: center;
background: ${({ theme }) => theme.colors.primary};
border-radius: 50%;
width: 80px;
height: 80px;
content: "↓";
font-size: 50px;
@media only screen and (max-width: 700px) {
width: 40px;
height: 40px;
}
}
`;
const ImageBg = styled.picture`
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
height: 100%;
object-fit: cover;
object-position: center;
z-index: -1;
opacity: 0.5;
img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
}
`;
const FrontPage: Page<"frontpage"> = ({ articles, profile }) => {
const theme = useMemo(
() =>
createTheme({
baseColor: chroma.random().brighten(1).hex(),
}),
[]
);
return (
<ThemeProvider theme={theme}>
<Helmet>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link
rel="preconnect"
href="https://fonts.gstatic.com"
crossOrigin="anonymous"
/>
<link
href="https://fonts.googleapis.com/css2?family=Archivo+Black&family=Black+Ops+One&family=Merriweather:wght@400;700&display=swap"
rel="stylesheet"
/>
</Helmet>
<GlobalStyle />
<Sheet color="#c85279">
<ImageBg>
<img src={profile.imageUrl} loading="lazy" />
</ImageBg>
<Arrow />
<Hero>
{"Hi, I'm Morten".split(" ").map((char, index) => (
<Title key={index}>{char}</Title>
))}
</Hero>
<Hero>
{"And I make software".split(" ").map((char, index) => (
<Title key={index}>{char}</Title>
))}
</Hero>
<Hero>
<Download href="/resume.pdf" download>
Download resumé
</Download>
</Hero>
</Sheet>
<Sheet color="#ef23e2">
<Hero>
{"Table of Content".split(" ").map((char, index) => (
<Title key={index}>{char}</Title>
))}
</Hero>
<ArticleGrid articles={articles} />
</Sheet>
</ThemeProvider>
);
};
export default FrontPage;

View File

@@ -0,0 +1,60 @@
import chroma from 'chroma-js';
import { Theme } from './theme';
const WHITE = chroma('white');
const BLACK = chroma('black');
type CreateOptions = {
baseColor?: string;
};
const isBright = (color: chroma.Color) => color.luminance() > 0.4;
const createTheme = (options: CreateOptions = {}) => {
const baseColor = options.baseColor
? chroma(options.baseColor)
: chroma.random();
const text = isBright(baseColor) ? BLACK : WHITE;
const bg = isBright(baseColor)
? baseColor.luminance(0.9)
: baseColor.luminance(0.01);
const theme: Theme = {
typography: {
Jumbo: {
weight: 'bold',
size: 2.8,
},
Title1: {
weight: 'bold',
},
Title2: {
weight: 'bold',
size: 1.3,
},
Body1: {},
Overline: {
size: 0.8,
upperCase: true,
},
Caption: {
size: 0.8,
},
Link: {
upperCase: true,
weight: 'bold',
},
},
colors: {
primary: baseColor.hex(),
foreground: text.hex(),
background: bg.hex(),
},
font: {
baseSize: 16,
},
};
return theme;
};
export { createTheme };

View File

@@ -0,0 +1,6 @@
import {} from 'styled-components';
import { Theme } from './theme';
declare module 'styled-components' {
export interface DefaultTheme extends Theme {}
}

View File

@@ -0,0 +1,5 @@
import { createTheme } from './create';
import { ThemeProvider } from './provider';
export * from './theme';
export { createTheme, ThemeProvider };

View File

@@ -0,0 +1,14 @@
import React, { ReactNode } from 'react';
import { ThemeProvider as StyledThemeProvider } from 'styled-components';
import { Theme } from './theme';
type Props = {
theme: Theme;
children: ReactNode;
};
const ThemeProvider: React.FC<Props> = ({ theme, children }) => (
<StyledThemeProvider theme={theme}>{children}</StyledThemeProvider>
);
export { ThemeProvider };

View File

@@ -0,0 +1,30 @@
type Typography = {
family?: string;
size?: number;
spacing?: number;
weight?: string;
upperCase?: boolean;
};
type Theme = {
typography: {
Jumbo: Typography;
Title2: Typography;
Title1: Typography;
Body1: Typography;
Caption: Typography;
Overline: Typography;
Link: Typography;
};
colors: {
primary: string;
foreground: string;
background: string;
};
font: {
baseSize: number;
family?: string;
};
};
export type { Theme, Typography };

View File

@@ -0,0 +1,28 @@
{
"compilerOptions": {
"target": "es2018",
"module": "commonjs",
"strict": true,
"jsx": "react-jsx",
"esModuleInterop": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"sourceMap": true,
"skipLibCheck": true,
"declaration": true,
"declarationMap": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"forceConsistentCasingInFileNames": true,
"baseUrl": ".",
"paths": {
"@/*": ["./*"]
"types/*": ["../../../types/*"],
"types": ["../../../types"]
}
},
"ts-node": {
"files": true
},
"include": ["./**/*", "../../../types/**/*"],
}

View File

@@ -0,0 +1,57 @@
import styled from "styled-components";
import { Theme, Typography } from "../theme";
interface TextProps {
color?: keyof Theme["colors"];
bold?: boolean;
theme: Theme;
}
const BaseText = styled.span<TextProps>`
${({ theme }) =>
theme.font.family ? `font-family: ${theme.font.family};` : ""}
color: ${({ color, theme }) =>
color ? theme.colors[color] : theme.colors.foreground};
font-weight: ${({ bold }) => (bold ? "bold" : "normal")};
font-size: ${({ theme }) => theme.font.baseSize}px;
`;
const get = (name: keyof Theme["typography"], theme: Theme): Typography => {
const typography = theme.typography[name];
return typography;
};
const createTypography = (name: keyof Theme["typography"]) => {
const Component = styled(BaseText) <TextProps>`
font-size: ${({ theme }) =>
theme.font.baseSize * (get(name, theme).size || 1)}px;
font-weight: ${({ bold, theme }) =>
typeof bold !== "undefined"
? "bold"
: get(name, theme).weight || "normal"};
${({ theme }) =>
get(name, theme).upperCase ? "text-transform: uppercase;" : ""}
`;
return Component;
};
const Jumbo = createTypography("Jumbo");
const Title2 = createTypography("Title2");
const Title1 = createTypography("Title1");
const Body1 = createTypography("Body1");
const Overline = createTypography("Overline");
const Caption = createTypography("Caption");
const Link = createTypography("Link");
const types: { [key in keyof Theme["typography"]]: typeof BaseText } = {
Jumbo,
Title2,
Title1,
Body1,
Overline,
Caption,
Link,
};
export type { TextProps };
export { types, Jumbo, Title2, Title1, Body1, Overline, Caption, Link };