1
mirror of https://github.com/jakejarvis/jarv.is.git synced 2025-09-16 17:35:32 -04:00

CSS modules ➡️ Stitches 🧵 (#799)

This commit is contained in:
2022-03-03 09:18:26 -05:00
committed by GitHub
parent ac7ac71c10
commit c2dde042b7
93 changed files with 2392 additions and 3000 deletions

View File

@@ -1,12 +0,0 @@
{
"extends": ["stylelint-prettier/recommended"],
"plugins": ["stylelint-prettier"],
"rules": {
"alpha-value-notation": "number",
"color-function-notation": "legacy",
"color-hex-length": "long",
"no-descending-specificity": null,
"rule-empty-line-before": null,
"selector-class-pattern": null
}
}

View File

@@ -1,12 +1,9 @@
{ {
"recommendations": [ "recommendations": [
"dbaeumer.vscode-eslint", "dbaeumer.vscode-eslint",
"divlo.vscode-styled-jsx-languageserver",
"divlo.vscode-styled-jsx-syntax",
"editorconfig.editorconfig", "editorconfig.editorconfig",
"esbenp.prettier-vscode", "esbenp.prettier-vscode",
"silvenon.mdx", "silvenon.mdx",
"stylelint.vscode-stylelint",
"wix.vscode-import-cost" "wix.vscode-import-cost"
] ]
} }

View File

@@ -11,9 +11,6 @@
}, },
"prettier.requireConfig": true, "prettier.requireConfig": true,
"prettier.configPath": ".prettierrc.json", "prettier.configPath": ".prettierrc.json",
"stylelint.packageManager": "yarn",
"stylelint.reportNeedlessDisables": true,
"stylelint.reportInvalidScopeDisables": true,
"npm.packageManager": "yarn", "npm.packageManager": "yarn",
"eslint.packageManager": "yarn" "eslint.packageManager": "yarn"
} }

View File

@@ -6,7 +6,7 @@
[![GitHub repo size](https://img.shields.io/github/repo-size/jakejarvis/jarv.is?color=009cdf&label=repo%20size&logo=git&logoColor=white)](https://github.com/jakejarvis/jarv.is) [![GitHub repo size](https://img.shields.io/github/repo-size/jakejarvis/jarv.is?color=009cdf&label=repo%20size&logo=git&logoColor=white)](https://github.com/jakejarvis/jarv.is)
[![Tor mirror uptime](https://img.shields.io/uptimerobot/ratio/m788172098-a4fcb769c8779f9a37a60775?color=7e4798&label=tor%20mirror&logo=tor-project&logoColor=white)](http://jarvis2i2vp4j4tbxjogsnqdemnte5xhzyi7hziiyzxwge3hzmh57zad.onion/) [![Tor mirror uptime](https://img.shields.io/uptimerobot/ratio/m788172098-a4fcb769c8779f9a37a60775?color=7e4798&label=tor%20mirror&logo=tor-project&logoColor=white)](http://jarvis2i2vp4j4tbxjogsnqdemnte5xhzyi7hziiyzxwge3hzmh57zad.onion/)
Personal website of [@jakejarvis](https://github.com/jakejarvis), created and deployed using [Next.js](https://nextjs.org/), [Vercel](https://vercel.com/), [and more](https://jarv.is/humans.txt). Personal website of [@jakejarvis](https://github.com/jakejarvis), created and deployed using [Next.js](https://nextjs.org/), [Stitches](https://stitches.dev/), [Vercel](https://vercel.com/), [and more](https://jarv.is/humans.txt).
I keep an ongoing list of [post ideas](https://github.com/jakejarvis/jarv.is/issues/1) and [coding to-dos](https://github.com/jakejarvis/jarv.is/issues/714) as issues in this repo. Outside contributions, improvements, and/or corrections are welcome too! I keep an ongoing list of [post ideas](https://github.com/jakejarvis/jarv.is/issues/1) and [coding to-dos](https://github.com/jakejarvis/jarv.is/issues/714) as issues in this repo. Outside contributions, improvements, and/or corrections are welcome too!

View File

@@ -1,6 +0,0 @@
.blockquote {
margin-left: 0;
padding-left: 1.25em;
border-left: 0.25em solid var(--link);
color: var(--medium-dark);
}

View File

@@ -1,11 +1,10 @@
import classNames from "classnames"; import { styled } from "../../lib/styles/stitches.config";
import styles from "./Blockquote.module.css"; const Blockquote = styled("blockquote", {
marginLeft: 0,
export type BlockquoteProps = JSX.IntrinsicElements["blockquote"]; paddingLeft: "1.25em",
borderLeft: "0.25em solid $link",
const Blockquote = ({ className, ...rest }: BlockquoteProps) => ( color: "$mediumDark",
<blockquote className={classNames(styles.blockquote, className)} {...rest} /> });
);
export default Blockquote; export default Blockquote;

View File

@@ -7,6 +7,7 @@ export type CaptchaProps = {
size?: "normal" | "compact" | "invisible"; size?: "normal" | "compact" | "invisible";
theme?: "light" | "dark"; theme?: "light" | "dark";
id?: string; id?: string;
className?: string;
// callbacks pulled verbatim from node_modules/@hcaptcha/react-hcaptcha/types/index.d.ts // callbacks pulled verbatim from node_modules/@hcaptcha/react-hcaptcha/types/index.d.ts
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
@@ -20,7 +21,7 @@ export type CaptchaProps = {
/* eslint-enable @typescript-eslint/no-explicit-any */ /* eslint-enable @typescript-eslint/no-explicit-any */
}; };
const Captcha = ({ size = "normal", theme, id, ...rest }: CaptchaProps) => { const Captcha = ({ size = "normal", theme, id, className, ...rest }: CaptchaProps) => {
const { resolvedTheme } = useTheme(); const { resolvedTheme } = useTheme();
return ( return (
@@ -30,15 +31,17 @@ const Captcha = ({ size = "normal", theme, id, ...rest }: CaptchaProps) => {
<link rel="preconnect" href="https://newassets.hcaptcha.com" /> <link rel="preconnect" href="https://newassets.hcaptcha.com" />
</Head> </Head>
<HCaptcha <div className={className}>
sitekey={process.env.NEXT_PUBLIC_HCAPTCHA_SITE_KEY} <HCaptcha
reCaptchaCompat={false} sitekey={process.env.NEXT_PUBLIC_HCAPTCHA_SITE_KEY}
tabIndex={0} reCaptchaCompat={false}
size={size} tabIndex={0}
theme={theme || (resolvedTheme === "dark" ? "dark" : "light")} size={size}
id={id} theme={theme || (resolvedTheme === "dark" ? "dark" : "light")}
{...rest} id={id}
/> {...rest}
/>
</div>
</> </>
); );
}; };

View File

@@ -1,133 +0,0 @@
/* all code */
.code {
font-size: 0.925em;
color: var(--code-text);
page-break-inside: avoid;
background-color: var(--code-background);
border: 1px solid var(--kinda-light);
border-radius: var(--rounded-edge-radius);
/* light-dark theme switch fading */
transition: background 0.25s ease, border 0.25s ease;
}
/* inline code in paragraphs/elsewhere (single backticks) */
.inline {
padding: 0.2em 0.3em;
}
/* full-blown code blocks, with copy/paste button and (usually) line numbers */
.block {
position: relative;
width: 100%;
margin: 1em auto;
}
.block .code {
display: block;
overflow-x: auto;
padding: 1em;
tab-size: 2;
}
.copy_btn {
position: absolute;
top: 0;
right: 0;
padding: 0.65em;
color: var(--medium-dark);
background-color: var(--background-inner);
border: 1px solid var(--kinda-light);
border-top-right-radius: var(--rounded-edge-radius);
border-end-start-radius: var(--rounded-edge-radius);
/* light-dark theme switch fading */
transition: background 0.25s ease, border 0.25s ease;
}
.copy_btn:hover {
color: var(--link);
}
/* the following sub-classes MUST be global -- the prism rehype plugin isn't aware of this file */
/* leave room for clipboard button to the right of the first line */
.highlight > :global(.code-line:first-of-type) {
margin-right: 3em;
}
.highlight > :global(.line-number::before) {
display: inline-block;
width: 1.5em;
margin-right: 1.5em;
text-align: right;
color: var(--code-comment);
content: attr(line); /* added to spans by prism */
}
.highlight :global(.token.comment),
.highlight :global(.token.prolog),
.highlight :global(.token.cdata) {
color: var(--code-comment);
}
.highlight :global(.token.delimiter),
.highlight :global(.token.boolean),
.highlight :global(.token.keyword),
.highlight :global(.token.selector),
.highlight :global(.token.important),
.highlight :global(.token.doctype),
.highlight :global(.token.atrule),
.highlight :global(.token.url) {
color: var(--code-keyword);
}
.highlight :global(.token.tag),
.highlight :global(.token.builtin),
.highlight :global(.token.regex) {
color: var(--code-namespace);
}
.highlight :global(.token.property),
.highlight :global(.token.constant),
.highlight :global(.token.variable),
.highlight :global(.token.attr-value),
.highlight :global(.token.class-name),
.highlight :global(.token.string),
.highlight :global(.token.char) {
color: var(--code-variable);
}
.highlight :global(.token.literal-property),
.highlight :global(.token.attr-name) {
color: var(--code-attribute);
}
.highlight :global(.token.function) {
color: var(--code-literal);
}
.highlight :global(.token.tag .punctuation),
.highlight :global(.token.attr-value .punctuation) {
color: var(--code-punctuation);
}
.highlight :global(.token.inserted) {
background-color: var(--code-addition);
}
.highlight :global(.token.deleted) {
background-color: var(--code-deletion);
}
.highlight :global(.token.url) {
text-decoration: underline;
}
.highlight :global(.token.bold) {
font-weight: bold;
}
.highlight :global(.token.italic) {
font-style: italic;
}

View File

@@ -1,9 +1,109 @@
import classNames from "classnames";
import CopyButton from "../CopyButton/CopyButton"; import CopyButton from "../CopyButton/CopyButton";
import { styled } from "../../lib/styles/stitches.config";
import type { ComponentProps } from "react";
import styles from "./CodeBlock.module.css"; const Code = styled("code", {
fontSize: "0.925em",
color: "$codeText",
pageBreakInside: "avoid",
backgroundColor: "$codeBackground",
border: "1px solid $kindaLight",
borderRadius: "$rounded",
export type CodeBlockProps = JSX.IntrinsicElements["code"] & { // light-dark theme switch fading
transition: "background 0.25s ease, border 0.25s ease",
variants: {
highlight: {
// the following sub-classes MUST be global -- the prism rehype plugin isn't aware of this file
true: {
// leave room for clipboard button to the right of the first line
".code-line:first-of-type": {
marginRight: "3em",
},
".line-number::before": {
display: "inline-block",
width: "1.5em",
marginRight: "1.5em",
textAlign: "right",
color: "$codeComment",
content: "attr(line)", // added to spans by prism
userSelect: "none",
},
".token": {
"&.comment, &.prolog, &.cdata": {
color: "$codeComment",
},
"&.delimiter, &.boolean, &.keyword, &.selector, &.important, &.doctype, &.atrule, &.url": {
color: "$codeKeyword",
},
"&.tag, &.builtin, &.regex": {
color: "$codeNamespace",
},
"&.property, &.constant, &.variable, &.attr-value, &.class-name, &.string, &.char": {
color: "$codeVariable",
},
"&.literal-property, &.attr-name": {
color: "$codeAttribute",
},
"&.function": {
color: "$codeLiteral",
},
"&.tag .punctuation, &.attr-value .punctuation": {
color: "$codePunctuation",
},
"&.inserted": {
color: "$codeAddition",
},
"&.deleted": {
color: "$codeDeletion",
},
"&.url": { textDecoration: "underline" },
"&.bold": { fontWeight: "bold" },
"&.italic": { fontStyle: "italic" },
},
},
},
},
});
const InlineCode = styled(Code, {
padding: "0.2em 0.3em",
});
const Block = styled("div", {
position: "relative",
width: "100%",
margin: "1em auto",
[`& ${Code}`]: {
display: "block",
overflowX: "auto",
padding: "1em",
tabSize: 2,
},
});
const CornerCopyButton = styled(CopyButton, {
position: "absolute",
top: 0,
right: 0,
padding: "0.65em",
color: "$mediumDark",
backgroundColor: "$backgroundInner",
border: "1px solid $kindaLight",
borderTopRightRadius: "$radii$rounded",
borderEndStartRadius: "$radii$rounded",
// light-dark theme switch fading
transition: "background 0.25s ease, border 0.25s ease",
"&:hover": {
color: "$link",
},
});
export type CodeBlockProps = ComponentProps<typeof Code> & {
forceBlock?: boolean; forceBlock?: boolean;
}; };
@@ -15,26 +115,19 @@ const CodeBlock = ({ forceBlock, className, children, ...rest }: CodeBlockProps)
// full multi-line code blocks with copy-to-clipboard button // full multi-line code blocks with copy-to-clipboard button
// automatic if highlighted by prism, otherwise can be forced (rather than inlined) with `forceBlock={true}` // automatic if highlighted by prism, otherwise can be forced (rather than inlined) with `forceBlock={true}`
return ( return (
<div className={styles.block}> <Block>
<CopyButton source={children} className={styles.copy_btn} /> <CornerCopyButton source={children} />
<code <Code highlight={prismEnabled} className={className?.replace("code-highlight", "").trim()} {...rest}>
className={classNames(
styles.code,
prismEnabled && styles.highlight,
className?.replace("code-highlight", "").trim()
)}
{...rest}
>
{children} {children}
</code> </Code>
</div> </Block>
); );
} else { } else {
// inline code in paragraphs, headings, etc. (not highlighted) // inline code in paragraphs, headings, etc. (not highlighted)
return ( return (
<code className={classNames(styles.code, styles.inline, className)} {...rest}> <InlineCode className={className} {...rest}>
{children} {children}
</code> </InlineCode>
); );
} }
}; };

View File

@@ -1,45 +0,0 @@
import { memo } from "react";
import css from "styled-jsx/css";
import classNames from "classnames";
import Link, { CustomLinkProps } from "../Link/Link";
export type ColorfulLinkProps = CustomLinkProps & {
lightColor: string;
darkColor: string;
};
// spits out alpha'd version of given color in rgba() format within a linear-gradient (that's not really a gradient)
const getLinearGradient = (hex: string, alpha = 0.4) => {
// hex -> rgb, pulled from https://github.com/sindresorhus/hex-rgb/blob/main/index.js#L29
const number = Number.parseInt(hex.replace(/^#/, ""), 16);
const red = number >> 16;
const green = (number >> 8) & 255;
const blue = number & 255;
const rgbaString = `rgba(${red},${green},${blue},${alpha})`;
return `linear-gradient(${rgbaString},${rgbaString})`;
};
const ColorfulLink = ({ lightColor, darkColor, className, ...rest }: ColorfulLinkProps) => {
const { className: underlineClassName, styles: underlineStyles } = css.resolve`
a {
color: ${lightColor};
background-image: ${getLinearGradient(lightColor)};
}
:global([data-theme="dark"]) a {
color: ${darkColor};
background-image: ${getLinearGradient(darkColor)};
}
`;
return (
<>
<Link className={classNames(underlineClassName, className)} {...rest} />
{underlineStyles}
</>
);
};
export default memo(ColorfulLink);

View File

@@ -1,6 +0,0 @@
.wrapper :global(.giscus) {
margin-top: 2em;
padding-top: 2em;
border-top: 2px solid var(--light);
min-height: 350px;
}

View File

@@ -1,21 +1,29 @@
import { memo } from "react"; import { memo } from "react";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
import classNames from "classnames";
import { Giscus } from "@giscus/react"; import { Giscus } from "@giscus/react";
import { styled } from "../../lib/styles/stitches.config";
import { giscusConfig } from "../../lib/config"; import { giscusConfig } from "../../lib/config";
import type { ComponentProps } from "react";
import type { GiscusProps } from "@giscus/react"; import type { GiscusProps } from "@giscus/react";
import styles from "./Comments.module.css"; const Wrapper = styled("div", {
".giscus": {
marginTop: "2em",
paddingTop: "2em",
borderTop: "2px solid $light",
minHeight: "350px",
},
});
export type CommentsProps = JSX.IntrinsicElements["div"] & { export type CommentsProps = ComponentProps<typeof Wrapper> & {
title: string; title: string;
}; };
const Comments = ({ title, className, ...rest }: CommentsProps) => { const Comments = ({ title, ...rest }: CommentsProps) => {
const { resolvedTheme } = useTheme(); const { resolvedTheme } = useTheme();
return ( return (
<div className={classNames(styles.wrapper, className)} {...rest}> <Wrapper {...rest}>
<Giscus <Giscus
{...(giscusConfig as GiscusProps)} {...(giscusConfig as GiscusProps)}
term={title} term={title}
@@ -24,7 +32,7 @@ const Comments = ({ title, className, ...rest }: CommentsProps) => {
emitMetadata="0" emitMetadata="0"
theme={resolvedTheme === "dark" ? "dark" : "light"} theme={resolvedTheme === "dark" ? "dark" : "light"}
/> />
</div> </Wrapper>
); );
}; };

View File

@@ -1,94 +0,0 @@
.input {
width: 100%;
padding: 0.8em;
margin: 0.6em 0;
border: 2px solid;
border-radius: var(--rounded-edge-radius);
color: var(--text);
background-color: var(--super-duper-light);
border-color: var(--light);
/* light-dark theme switch fading */
transition: background 0.25s ease;
}
.input:focus {
outline: none;
border-color: var(--link);
}
.input.missing {
border-color: var(--error);
}
.textarea {
margin-bottom: 0;
line-height: 1.5;
min-height: 10em;
resize: vertical;
}
.markdown_tip {
font-size: 0.825em;
line-height: 1.75;
}
.captcha {
margin: 1em 0;
}
.action_row {
display: flex;
align-items: center;
min-height: 3.75em;
}
.btn_submit {
flex-shrink: 0;
height: 3.25em;
padding: 1em 1.25em;
margin-right: 1.5em;
border: 0;
border-radius: var(--rounded-edge-radius);
cursor: pointer;
user-select: none;
font-weight: 500;
color: var(--text);
background-color: var(--kinda-light);
}
.btn_submit:hover {
color: var(--super-duper-light);
background-color: var(--link);
}
.send_icon {
width: 1.2em;
height: 1.2em;
vertical-align: -0.2em;
margin-right: 0.4em;
}
.result_icon {
width: 1.3em;
height: 1.3em;
vertical-align: -0.3em;
}
.result_success,
.result_error {
font-weight: 600;
line-height: 1.5;
}
.result_success {
color: var(--success);
}
.result_error {
color: var(--error);
}
.hidden {
display: none !important;
}

View File

@@ -1,13 +1,126 @@
import { useState } from "react"; import { useState } from "react";
import classNames from "classnames";
import { Formik, Form, Field } from "formik"; import { Formik, Form, Field } from "formik";
import TextareaAutosize from "react-textarea-autosize"; import TextareaAutosize from "react-textarea-autosize";
import Link from "../Link/Link"; import Link from "../Link/Link";
import Captcha from "../Captcha/Captcha"; import Captcha from "../Captcha/Captcha";
import { SendIcon, CheckOcticon, XOcticon } from "../Icons"; import { SendIcon, CheckOcticon, XOcticon } from "../Icons";
import { styled, css } from "../../lib/styles/stitches.config";
import type { FormikHelpers } from "formik"; import type { FormikHelpers } from "formik";
import styles from "./ContactForm.module.css"; // CSS applied to both `<input />` and `<textarea />`
const InputStyles = css({
width: "100%",
padding: "0.8em",
margin: "0.6em 0",
border: "2px solid",
borderRadius: "$rounded",
color: "$text",
backgroundColor: "$superDuperLight",
borderColor: "$light",
// light-dark theme switch fading
transition: "background 0.25s ease",
"&:focus": {
outline: "none",
borderColor: "$link",
},
variants: {
missing: {
true: {
borderColor: "$error",
},
},
},
});
const Input = styled("input", InputStyles);
const TextArea = styled(TextareaAutosize, InputStyles, {
marginBottom: 0,
lineHeight: 1.5,
minHeight: "10em",
resize: "vertical",
});
const MarkdownTip = styled("div", {
fontSize: "0.825em",
lineHeight: 1.75,
});
const HCaptcha = styled(Captcha, {
margin: "1em 0",
});
const ActionRow = styled("div", {
display: "flex",
alignItems: "center",
minHeight: "3.75em",
});
const SubmitButton = styled("button", {
flexShrink: 0,
height: "3.25em",
padding: "1em 1.25em",
marginRight: "1.5em",
border: "0",
borderRadius: "$rounded",
cursor: "pointer",
userSelect: "none",
fontWeight: 500,
color: "$text",
backgroundColor: "$kindaLight",
"&:hover": {
color: "$superDuperLight",
backgroundColor: "$link",
},
variants: {
hidden: {
true: {
display: "none",
},
},
},
});
const SubmitIcon = styled(SendIcon, {
width: "1.2em",
height: "1.2em",
verticalAlign: "-0.2em",
marginRight: "0.4em",
});
const Result = styled("div", {
fontWeight: 600,
lineHeight: 1.5,
variants: {
status: {
success: {
color: "$success",
},
error: {
color: "$error",
},
},
hidden: {
true: {
display: "none",
},
},
},
});
const ResultIcon = styled("svg", {
width: "1.3em",
height: "1.3em",
verticalAlign: "-0.3em",
fill: "currentColor",
});
type Values = { type Values = {
name: string; name: string;
@@ -100,11 +213,11 @@ const ContactForm = ({ className }: ContactFormProps) => {
<Form className={className} name="contact"> <Form className={className} name="contact">
<Field name="name"> <Field name="name">
{({ field, meta }) => ( {({ field, meta }) => (
<input <Input
type="text" type="text"
className={classNames(styles.input, meta.error && meta.touched && styles.missing)}
placeholder="Name" placeholder="Name"
disabled={success} disabled={success}
missing={meta.error && meta.touched}
{...field} {...field}
/> />
)} )}
@@ -112,12 +225,12 @@ const ContactForm = ({ className }: ContactFormProps) => {
<Field name="email"> <Field name="email">
{({ field, meta }) => ( {({ field, meta }) => (
<input <Input
type="email" type="email"
inputMode="email" inputMode="email"
className={classNames(styles.input, meta.error && meta.touched && styles.missing)}
placeholder="Email" placeholder="Email"
disabled={success} disabled={success}
missing={meta.error && meta.touched}
{...field} {...field}
/> />
)} )}
@@ -125,17 +238,17 @@ const ContactForm = ({ className }: ContactFormProps) => {
<Field name="message"> <Field name="message">
{({ field, meta }) => ( {({ field, meta }) => (
<TextareaAutosize <TextArea
className={classNames(styles.input, styles.textarea, meta.error && meta.touched && styles.missing)}
placeholder="Write something..." placeholder="Write something..."
minRows={5} minRows={5}
disabled={success} disabled={success}
missing={meta.error && meta.touched}
{...field} {...field}
/> />
)} )}
</Field> </Field>
<div className={styles.markdown_tip}> <MarkdownTip>
Basic{" "} Basic{" "}
<Link href="https://commonmark.org/help/" title="Markdown reference sheet" style={{ fontWeight: 600 }}> <Link href="https://commonmark.org/help/" title="Markdown reference sheet" style={{ fontWeight: 600 }}>
Markdown syntax Markdown syntax
@@ -145,45 +258,32 @@ const ContactForm = ({ className }: ContactFormProps) => {
links links
</Link> </Link>
](https://jarv.is), and <code>`code`</code>. ](https://jarv.is), and <code>`code`</code>.
</div> </MarkdownTip>
<div className={styles.captcha}> <HCaptcha onVerify={(token) => setFieldValue("h-captcha-response", token)} />
<Captcha onVerify={(token) => setFieldValue("h-captcha-response", token)} />
</div>
<div className={styles.action_row}> <ActionRow>
<button <SubmitButton
className={classNames(styles.btn_submit, success && styles.hidden)}
type="submit" type="submit"
title="Send Message" title="Send Message"
aria-label="Send Message" aria-label="Send Message"
onClick={() => setSubmitted(true)} onClick={() => setSubmitted(true)}
disabled={isSubmitting} disabled={isSubmitting}
hidden={success}
> >
{isSubmitting ? ( {isSubmitting ? (
<span>Sending...</span> <span>Sending...</span>
) : ( ) : (
<> <>
<SendIcon className={classNames(styles.send_icon)} /> <span>Send</span> <SubmitIcon /> <span>Send</span>
</> </>
)} )}
</button> </SubmitButton>
<span <Result status={success ? "success" : "error"} hidden={!submitted || !feedback || isSubmitting}>
className={classNames( <ResultIcon as={success ? CheckOcticon : XOcticon} /> {feedback}
success && styles.result_success, </Result>
!success && styles.result_error, </ActionRow>
(!submitted || !feedback || isSubmitting) && styles.hidden
)}
>
{success ? (
<CheckOcticon className={styles.result_icon} fill="CurrentColor" />
) : (
<XOcticon className={styles.result_icon} fill="CurrentColor" />
)}{" "}
{feedback}
</span>
</div>
</Form> </Form>
)} )}
</Formik> </Formik>

View File

@@ -1,12 +0,0 @@
.content {
font-size: 0.9em;
line-height: 1.7;
color: var(--text);
}
@media screen and (max-width: 768px) {
.content {
font-size: 0.925em;
line-height: 1.85;
}
}

View File

@@ -1,11 +1,14 @@
import classNames from "classnames"; import { styled } from "../../lib/styles/stitches.config";
import styles from "./Content.module.css"; const Content = styled("div", {
fontSize: "0.9em",
lineHeight: 1.7,
color: "$text",
export type ContentProps = JSX.IntrinsicElements["div"]; "@mobile": {
fontSize: "0.925em",
const Content = ({ className, ...rest }: ContentProps) => ( lineHeight: 1.85,
<div className={classNames(styles.content, className)} {...rest} /> },
); });
export default Content; export default Content;

View File

@@ -1,14 +0,0 @@
.button {
line-height: 1;
cursor: pointer;
}
.button.success {
color: var(--success) !important;
}
.icon {
width: 1.25em;
height: 1.25em;
vertical-align: -0.3em;
}

View File

@@ -1,11 +1,29 @@
import { forwardRef, useState, useEffect } from "react"; import { forwardRef, useState, useEffect } from "react";
import classNames from "classnames";
import copy from "copy-to-clipboard"; import copy from "copy-to-clipboard";
import innerText from "react-innertext"; import innerText from "react-innertext";
import { ClipboardOcticon, CheckOcticon } from "../Icons"; import { ClipboardOcticon, CheckOcticon } from "../Icons";
import { styled } from "../../lib/styles/stitches.config";
import type { ReactNode, Ref } from "react"; import type { ReactNode, Ref } from "react";
import styles from "./CopyButton.module.css"; const Button = styled("button", {
lineHeight: 1,
cursor: "pointer",
variants: {
success: {
true: {
color: "$success !important",
},
},
},
});
const Icon = styled("svg", {
width: "1.25em",
height: "1.25em",
verticalAlign: "-0.3em",
fill: "currentColor",
});
export type CopyButtonProps = { export type CopyButtonProps = {
source: ReactNode; source: ReactNode;
@@ -45,20 +63,17 @@ const CopyButton = forwardRef(function CopyButton(
}, [timeout, copied]); }, [timeout, copied]);
return ( return (
<button <Button
className={classNames(styles.button, copied && styles.success, className)} className={className}
title="Copy to clipboard" title="Copy to clipboard"
aria-label="Copy to clipboard" aria-label="Copy to clipboard"
onClick={handleCopy} onClick={handleCopy}
disabled={!!copied} disabled={!!copied}
success={copied}
ref={ref} ref={ref}
> >
{copied ? ( <Icon as={copied ? CheckOcticon : ClipboardOcticon} />
<CheckOcticon className={styles.icon} fill="currentColor" /> </Button>
) : (
<ClipboardOcticon className={styles.icon} fill="currentColor" />
)}
</button>
); );
}); });

View File

@@ -1,16 +0,0 @@
.figure {
margin: 1em auto;
text-align: center;
}
.caption {
font-size: 0.9em;
line-height: 1.5;
color: var(--medium);
margin-top: -0.4em;
}
/* some figcaptions contain paragraphs, some don't, so reset all of them */
.caption p {
margin: 0 !important;
}

View File

@@ -1,10 +1,25 @@
import Image from "../Image/Image"; import Image from "../Image/Image";
import innerText from "react-innertext"; import innerText from "react-innertext";
import classNames from "classnames"; import { styled } from "../../lib/styles/stitches.config";
import type { PropsWithChildren } from "react"; import type { PropsWithChildren } from "react";
import type { ImageProps as NextImageProps } from "next/image"; import type { ImageProps as NextImageProps } from "next/image";
import styles from "./Figure.module.css"; const Wrapper = styled("figure", {
margin: "1em auto",
textAlign: "center",
});
const Caption = styled("figcaption", {
fontSize: "0.9em",
lineHeight: 1.5,
color: "$medium",
marginTop: "-0.4em",
/* some figcaptions contain paragraphs, some don't, so reset all of them */
"& p": {
margin: "0 !important",
},
});
export type FigureProps = Omit<NextImageProps, "alt"> & export type FigureProps = Omit<NextImageProps, "alt"> &
PropsWithChildren<{ PropsWithChildren<{
@@ -13,10 +28,10 @@ export type FigureProps = Omit<NextImageProps, "alt"> &
const Figure = ({ children, alt, className, ...imageProps }: FigureProps) => { const Figure = ({ children, alt, className, ...imageProps }: FigureProps) => {
return ( return (
<figure className={classNames(styles.figure, className)}> <Wrapper className={className}>
<Image alt={alt || innerText(children)} {...imageProps} /> <Image alt={alt || innerText(children)} {...imageProps} />
<figcaption className={styles.caption}>{children}</figcaption> <Caption>{children}</Caption>
</figure> </Wrapper>
); );
}; };

View File

@@ -1,88 +0,0 @@
.footer {
width: 100%;
padding: 1.25em 1.5em;
border-top: 1px solid var(--kinda-light);
background-color: var(--background-outer);
color: var(--medium-dark);
/* light-dark theme switch fading */
transition: color 0.25s ease, background 0.25s ease, border 0.25s ease;
}
.link {
color: inherit;
text-decoration: none;
}
.row {
display: flex;
width: 100%;
max-width: 865px;
margin: 0 auto;
justify-content: space-between;
font-size: 0.85em;
line-height: 2.3;
}
.icon {
width: 1.25em;
height: 1.25em;
vertical-align: -0.25em;
margin: 0 0.075em;
}
.nextjs:hover {
color: var(--medium);
}
.view_source {
padding-bottom: 2px;
border-bottom: 1px solid;
border-color: var(--light);
}
.view_source:hover {
border-color: var(--kinda-light);
}
.heart {
display: inline-block;
animation: beat 10s infinite; /* 6 bpm, call 911 if you see this please. */
animation-delay: 7.5s; /* offset from wave animation */
will-change: transform;
}
@media screen and (max-width: 768px) {
.footer {
padding: 1em 1.25em;
}
/* stack columns on left instead of flexboxing across */
.row {
font-size: 0.8em;
display: block;
}
}
@keyframes beat {
0% {
transform: scale(1);
}
2% {
transform: scale(1.25);
}
4% {
transform: scale(1);
}
6% {
transform: scale(1.2);
}
8% {
transform: scale(1);
}
/* pause for ~9 out of 10 seconds */
100% {
transform: scale(1);
}
}

View File

@@ -1,60 +1,128 @@
import { memo } from "react"; import { memo } from "react";
import Link from "next/link"; import NextLink from "next/link";
import classNames from "classnames";
import { HeartIcon, NextjsLogo } from "../Icons"; import { HeartIcon, NextjsLogo } from "../Icons";
import { keyframes, styled } from "../../lib/styles/stitches.config";
import * as config from "../../lib/config"; import * as config from "../../lib/config";
import type { ComponentProps } from "react";
import styles from "./Footer.module.css"; const Wrapper = styled("footer", {
width: "100%",
padding: "1.25em 1.5em",
borderTop: "1px solid $kindaLight",
backgroundColor: "$backgroundOuter",
color: "$mediumDark",
transition: "color 0.25s ease, background 0.25s ease, border 0.25s ease",
export type FooterProps = JSX.IntrinsicElements["footer"]; "@mobile": {
padding: "1em 1.25em",
},
});
const Footer = ({ className, ...rest }: FooterProps) => ( const Row = styled("div", {
<footer className={classNames(styles.footer, className)} {...rest}> display: "flex",
<div className={styles.row}> width: "100%",
<div className={styles.license}> maxWidth: "865px",
margin: "0 auto",
justifyContent: "space-between",
fontSize: "0.85em",
lineHeight: 2.3,
// stack columns on left instead of flexboxing across
"@mobile": {
fontSize: "0.8em",
display: "block",
},
});
const Link = styled("a", {
color: "$mediumDark",
textDecoration: "none",
});
const NextjsLink = styled(Link, {
"&:hover": {
color: "$medium",
},
});
const ViewSourceLink = styled(Link, {
paddingBottom: "2px",
borderBottom: "1px solid",
borderColor: "$light",
"&:hover": {
borderColor: "$kindaLight",
},
});
const beat = keyframes({
"0%": { transform: "scale(1)" },
"2%": { transform: "scale(1.25)" },
"4%": { transform: "scale(1)" },
"6%": { transform: "scale(1.2)" },
"8%": { transform: "scale(1)" },
// pause for ~9 out of 10 seconds
"100%": { transform: "scale(1)" },
});
const Heart = styled("span", {
display: "inline-block",
animation: `${beat} 10s infinite`,
animationDelay: "7.5s",
willChange: "transform",
});
const Icon = styled("svg", {
width: "1.25em",
height: "1.25em",
verticalAlign: "-0.25em",
margin: "0 0.075em",
});
export type FooterProps = ComponentProps<typeof Wrapper>;
const Footer = ({ ...rest }: FooterProps) => (
<Wrapper {...rest}>
<Row>
<div>
Content{" "} Content{" "}
<Link href="/license/" prefetch={false}> <NextLink href="/license/" prefetch={false} passHref={true}>
<a className={styles.link} title="Creative Commons Attribution 4.0 International"> <Link title="Creative Commons Attribution 4.0 International">licensed under CC-BY-4.0</Link>
licensed under CC-BY-4.0 </NextLink>
</a>
</Link>
,{" "} ,{" "}
<Link href="/previously/" prefetch={false}> <NextLink href="/previously/" prefetch={false} passHref={true}>
<a className={styles.link} title="Previously on..."> <Link title="Previously on...">2001</Link>
2001 </NextLink>{" "}
</a>
</Link>{" "}
{new Date().getFullYear()}. {new Date().getFullYear()}.
</div> </div>
<div className={styles.powered_by}>
<div>
Made with{" "} Made with{" "}
<span className={styles.heart} title="Love"> <Heart title="Love">
<HeartIcon className={styles.icon} /> <Icon as={HeartIcon} />
</span>{" "} </Heart>{" "}
and{" "} and{" "}
<a <NextjsLink
className={classNames(styles.link, styles.nextjs)}
href="https://nextjs.org/" href="https://nextjs.org/"
title="Powered by Next.js" title="Powered by Next.js"
aria-label="Next.js" aria-label="Next.js"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
<NextjsLogo className={styles.icon} fill="currentColor" /> <Icon as={NextjsLogo} fill="currentColor" />
</a> </NextjsLink>
.{" "} .{" "}
<a <ViewSourceLink
className={classNames(styles.link, styles.view_source)}
href={`https://github.com/${config.githubRepo}`} href={`https://github.com/${config.githubRepo}`}
title="View Source on GitHub" title="View Source on GitHub"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
View source. View source.
</a> </ViewSourceLink>
</div> </div>
</div> </Row>
</footer> </Wrapper>
); );
export default memo(Footer); export default memo(Footer);

View File

@@ -1,43 +0,0 @@
.header {
width: 100%;
height: 4.5em;
padding: 0.7em 1.5em;
border-bottom: 1px solid var(--kinda-light);
background-color: var(--background-header);
/* light-dark theme switch fading */
transition: color 0.25s ease, background 0.25s ease, border 0.25s ease;
}
.sticky {
position: sticky;
top: 0;
backdrop-filter: saturate(180%) blur(5px);
z-index: 1000;
}
.nav {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
max-width: 865px;
margin: 0 auto;
}
@media screen and (max-width: 768px) {
.header {
padding: 0.75em 1.25em;
height: 5.9em;
}
.menu {
max-width: 325px;
}
}
@media screen and (max-width: 380px) {
.menu {
max-width: 225px;
}
}

View File

@@ -1,21 +1,68 @@
import { memo } from "react"; import { memo } from "react";
import classNames from "classnames";
import Selfie from "../Selfie/Selfie"; import Selfie from "../Selfie/Selfie";
import Menu from "../Menu/Menu"; import Menu from "../Menu/Menu";
import { styled } from "../../lib/styles/stitches.config";
import type { ComponentProps } from "react";
import styles from "./Header.module.css"; const Wrapper = styled("header", {
width: "100%",
height: "4.5em",
padding: "0.7em 1.5em",
borderBottom: "1px solid $kindaLight",
backgroundColor: "$backgroundHeader",
export type HeaderProps = JSX.IntrinsicElements["header"] & { // light-dark theme switch fading
transition: "color 0.25s ease, background 0.25s ease, border 0.25s ease",
"@mobile": {
padding: "0.75em 1.25em",
height: "5.9em",
},
variants: {
sticky: {
true: {
position: "sticky",
top: 0,
// blurry glass-like background effect (except on firefox)
backdropFilter: "saturate(180%) blur(5px)",
zIndex: 1000,
},
},
},
});
const Nav = styled("nav", {
display: "flex",
alignItems: "center",
justifyContent: "space-between",
width: "100%",
maxWidth: "865px",
margin: "0 auto",
});
const ResponsiveMenu = styled(Menu, {
"@mobile": {
maxWidth: "325px",
},
"@superNarrow": {
maxWidth: "225px",
},
});
export type HeaderProps = ComponentProps<typeof Wrapper> & {
sticky?: boolean; sticky?: boolean;
}; };
const Header = ({ sticky, className, ...rest }: HeaderProps) => ( const Header = ({ sticky, ...rest }: HeaderProps) => (
<header className={classNames(styles.header, sticky && styles.sticky, className)} {...rest}> <Wrapper sticky={sticky} {...rest}>
<nav className={styles.nav}> <Nav>
<Selfie className={styles.selfie} /> <Selfie />
<Menu className={styles.menu} /> <ResponsiveMenu />
</nav> </Nav>
</header> </Wrapper>
); );
export default memo(Header); export default memo(Header);

View File

@@ -1,48 +0,0 @@
.heading {
margin-top: 1em;
margin-bottom: 0.5em;
line-height: 1.5;
/**
* offset (approximately) with sticky header so jumped-to content isn't hiding behind it.
* note: use rem so it isn't based on the heading's font size.
*/
scroll-margin-top: 5.5rem;
}
/* special bottom border for <h2>s */
.h2 {
padding-bottom: 0.25em;
border-bottom: 1px solid var(--kinda-light);
}
/* sub-heading anchor styles */
.anchor {
margin: 0 0.25em;
padding: 0 0.25em;
color: var(--medium-light);
font-weight: 300;
text-decoration: none;
opacity: 0; /* overridden on hover below (except on small screens) */
}
.anchor::before {
content: "\0023"; /* pound sign `#`, done here to keep content DOM cleaner */
}
.anchor:hover {
color: var(--link);
}
/* make anchor link show up on hover over its corresponding heading */
.heading:hover .anchor {
opacity: 1;
}
@media screen and (max-width: 768px) {
.heading {
scroll-margin-top: 6.5rem;
}
/* don't require hover to show anchor link on small (likely touch) screens */
.anchor {
opacity: 1;
}
}

View File

@@ -1,29 +1,71 @@
import classNames from "classnames";
import innerText from "react-innertext"; import innerText from "react-innertext";
import type { HTMLAttributes } from "react"; import { styled } from "../../lib/styles/stitches.config";
import type { ComponentProps } from "react";
import styles from "./Heading.module.css"; const Anchor = styled("a", {
margin: "0 0.25em",
padding: "0 0.25em",
color: "$mediumLight",
fontWeight: 300,
textDecoration: "none",
opacity: 0, // overridden on hover below (except on small screens)
export type HeadingProps = HTMLAttributes<HTMLHeadingElement> & { "&::before": {
// pound sign `#`, done here to keep content DOM cleaner
content: "\\0023",
},
"&:hover": {
color: "$link",
},
// don't require hover to show anchor link on small (likely touch) screens
"@mobile": {
opacity: 1,
},
});
const H = styled("h1", {
marginTop: "1em",
marginBottom: "0.5em",
lineHeight: 1.5,
// offset (approximately) with sticky header so jumped-to content isn't hiding behind it.
// note: use rem so it isn't based on the heading's font size.
scrollMarginTop: "5.5rem",
"@mobile": {
scrollMarginTop: "6.5rem",
},
[`&:hover ${Anchor}`]: {
opacity: 1,
},
variants: {
underline: {
true: {
paddingBottom: "0.25em",
borderBottom: "1px solid $kindaLight",
},
},
},
});
export type HeadingProps = ComponentProps<typeof H> & {
as: "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; as: "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
}; };
const Heading = ({ as: Component, id, className, children, ...rest }: HeadingProps) => { const Heading = ({ as, id, children, ...rest }: HeadingProps) => {
return ( return (
<Component className={classNames(styles.heading, styles[Component], className)} id={id} {...rest}> <H as={as} underline={as === "h2"} id={id} {...rest}>
{children} {children}
{/* add anchor link to H2s and H3s. ID is already generated by rehype-slug. `#` character inserted via CSS. */} {/* add anchor link to H2s and H3s. ID is already generated by rehype-slug. `#` character inserted via CSS. */}
{id && (Component === "h2" || Component === "h3") && ( {id && (as === "h2" || as === "h3") && (
<a <Anchor href={`#${id}`} title={`Jump to "${innerText(children)}"`} tabIndex={-1} aria-hidden={true} />
className={styles.anchor}
href={`#${id}`}
title={`Jump to "${innerText(children)}"`}
tabIndex={-1}
aria-hidden={true}
/>
)} )}
</Component> </H>
); );
}; };

View File

@@ -1,6 +0,0 @@
.hr {
margin: 1.5em auto;
height: 0.175em;
border: 0;
background-color: var(--light);
}

View File

@@ -1,11 +1,10 @@
import classNames from "classnames"; import { styled } from "../../lib/styles/stitches.config";
import styles from "./HorizontalRule.module.css"; const HorizontalRule = styled("hr", {
margin: "1.5em auto",
export type HorizontalRuleProps = JSX.IntrinsicElements["hr"]; height: "0.175em",
border: 0,
const HorizontalRule = ({ className, ...rest }: HorizontalRuleProps) => ( backgroundColor: "$light",
<hr className={classNames(styles.hr, className)} {...rest} /> });
);
export default HorizontalRule; export default HorizontalRule;

View File

@@ -1,7 +0,0 @@
.frame {
width: 100%;
display: block;
margin: 1em auto;
border: 2px solid var(--kinda-light);
border-radius: var(--rounded-edge-radius);
}

View File

@@ -1,8 +1,15 @@
import classNames from "classnames"; import { styled } from "../../lib/styles/stitches.config";
import type { ComponentProps } from "react";
import styles from "./IFrame.module.css"; const RoundedIFrame = styled("iframe", {
width: "100%",
display: "block",
margin: "1em auto",
border: "2px solid $kindaLight",
borderRadius: "$rounded",
});
export type IFrameProps = JSX.IntrinsicElements["iframe"] & { export type IFrameProps = ComponentProps<typeof RoundedIFrame> & {
src: string; src: string;
height: number; height: number;
width?: number; // defaults to 100% width?: number; // defaults to 100%
@@ -10,17 +17,16 @@ export type IFrameProps = JSX.IntrinsicElements["iframe"] & {
noScroll?: boolean; noScroll?: boolean;
}; };
const IFrame = ({ src, title, height, width, allowScripts, noScroll, className, ...rest }: IFrameProps) => ( const IFrame = ({ src, title, height, width, allowScripts, noScroll, ...rest }: IFrameProps) => (
<iframe <RoundedIFrame
className={classNames(styles.frame, className)}
src={src} src={src}
title={title} title={title}
sandbox={allowScripts ? "allow-same-origin allow-scripts allow-popups" : undefined} sandbox={allowScripts ? "allow-same-origin allow-scripts allow-popups" : undefined}
scrolling={noScroll ? "no" : undefined} scrolling={noScroll ? "no" : undefined}
loading="lazy" loading="lazy"
style={{ css={{
height: `${height}px`, height: `${height}px`,
maxWidth: width ? `${width}px` : undefined, maxWidth: width ? `${width}px` : null,
}} }}
{...rest} {...rest}
/> />

View File

@@ -1,11 +0,0 @@
.wrapper {
line-height: 0;
/* default to centering all images */
margin: 1em auto;
text-align: center;
}
.image {
border-radius: var(--rounded-edge-radius);
}

View File

@@ -1,8 +1,19 @@
import NextImage from "next/image"; import NextImage from "next/image";
import classNames from "classnames"; import { styled } from "../../lib/styles/stitches.config";
import type { ComponentProps } from "react";
import type { ImageProps as NextImageProps, StaticImageData } from "next/image"; import type { ImageProps as NextImageProps, StaticImageData } from "next/image";
import styles from "./Image.module.css"; const Wrapper = styled("div", {
lineHeight: 0,
// default to centering all images
margin: "1em auto",
textAlign: "center",
});
const RoundedImage = styled(NextImage, {
borderRadius: "$rounded",
});
const CustomImage = ({ const CustomImage = ({
src, src,
@@ -15,7 +26,7 @@ const CustomImage = ({
priority, priority,
className, className,
...rest ...rest
}: NextImageProps) => { }: NextImageProps & ComponentProps<typeof RoundedImage>) => {
// passed directly into next/image: https://nextjs.org/docs/api-reference/next/image // passed directly into next/image: https://nextjs.org/docs/api-reference/next/image
const imageProps: Partial<NextImageProps> = { const imageProps: Partial<NextImageProps> = {
width: typeof width === "string" ? Number.parseInt(width) : width, width: typeof width === "string" ? Number.parseInt(width) : width,
@@ -40,10 +51,10 @@ const CustomImage = ({
} }
return ( return (
<div className={classNames(styles.wrapper, className)}> <Wrapper className={className}>
{/* @ts-ignore */} {/* @ts-ignore */}
<NextImage className={styles.image} {...imageProps} {...rest} /> <RoundedImage {...imageProps} {...rest} />
</div> </Wrapper>
); );
}; };

View File

@@ -1,27 +0,0 @@
.flex {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.default {
width: 100%;
padding: 1.5em;
}
.container {
max-width: 865px;
margin: 0 auto;
display: block;
}
/* footer needs to fill the remaining vertical screen space. doing it here to keep flex stuff together. */
.footer {
flex: 1;
}
@media screen and (max-width: 768px) {
.main {
padding: 1.25em;
}
}

View File

@@ -1,41 +1,66 @@
import Head from "next/head"; import Head from "next/head";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
import classNames from "classnames";
import Header from "../Header/Header"; import Header from "../Header/Header";
import Footer from "../Footer/Footer"; import Footer from "../Footer/Footer";
import themes from "../../lib/config/themes"; import { styled, theme, darkTheme } from "../../lib/styles/stitches.config";
import type { ComponentProps } from "react";
import styles from "./Layout.module.css"; const Flex = styled("div", {
display: "flex",
flexDirection: "column",
minHeight: "100vh",
});
export type LayoutProps = JSX.IntrinsicElements["div"] & { const Default = styled("main", {
width: "100%",
padding: "1.5em",
});
const Container = styled("div", {
maxWidth: "865px",
margin: "0 auto",
display: "block",
});
// footer needs to fill the remaining vertical screen space. doing it here to keep flex stuff together.
const FlexedFooter = styled(Footer, {
flex: 1,
});
export type LayoutProps = ComponentProps<typeof Flex> & {
container?: boolean; // pass false to disable default `<main>` container styles with padding, etc. container?: boolean; // pass false to disable default `<main>` container styles with padding, etc.
stickyHeader?: boolean; // pass false to override default stickiness of header when scrolling stickyHeader?: boolean; // pass false to override default stickiness of header when scrolling
}; };
const Layout = ({ container = true, stickyHeader = true, className, children, ...rest }: LayoutProps) => { const Layout = ({ container = true, stickyHeader = true, children, ...rest }: LayoutProps) => {
const { resolvedTheme } = useTheme(); const { resolvedTheme } = useTheme();
return ( return (
<> <>
<Head> <Head>
{/* dynamically set browser theme color to match the background color; default to light for SSR */} {/* dynamically set browser theme color to match the background color; default to light for SSR */}
<meta name="theme-color" content={themes[resolvedTheme || "light"].backgroundOuter} /> <meta
name="theme-color"
content={
resolvedTheme === "dark" ? darkTheme.colors.backgroundOuter.value : theme.colors.backgroundOuter.value
}
/>
</Head> </Head>
<div className={classNames(styles.flex, className)} {...rest}> <Flex {...rest}>
<Header sticky={stickyHeader} /> <Header sticky={stickyHeader} />
{/* passing `container={false}` to Layout allows 100% control of the content area on a per-page basis */} {/* passing `container={false}` to Layout allows 100% control of the content area on a per-page basis */}
{container ? ( {container ? (
<main className={styles.default}> <Default>
<div className={styles.container}>{children}</div> <Container>{children}</Container>
</main> </Default>
) : ( ) : (
<>{children}</> <>{children}</>
)} )}
<Footer className={styles.footer} /> <FlexedFooter />
</div> </Flex>
</> </>
); );
}; };

View File

@@ -1,16 +0,0 @@
.link {
color: var(--link);
background-image: linear-gradient(var(--link-underline), var(--link-underline));
background-position: 0% 100%;
background-repeat: no-repeat;
background-size: 0% var(--link-underline-size);
text-decoration: none;
padding-bottom: 0.2rem;
/* background-size is for hover animation, color & border are for theme fading: */
transition: background-size 0.25s ease-in-out, color 0.25s ease, border 0.25s ease;
}
.link:hover {
background-size: 100% var(--link-underline-size);
}

View File

@@ -1,11 +1,28 @@
import NextLink from "next/link"; import NextLink from "next/link";
import classNames from "classnames";
import isAbsoluteUrl from "is-absolute-url"; import isAbsoluteUrl from "is-absolute-url";
import { styled } from "../../lib/styles/stitches.config";
import type { ComponentProps } from "react";
import type { LinkProps as NextLinkProps } from "next/link"; import type { LinkProps as NextLinkProps } from "next/link";
import styles from "./Link.module.css"; const FancyLink = styled("a", {
color: "$link",
textDecoration: "none",
transition: "background-size 0.25s ease-in-out, color 0.25s ease, border 0.25s ease",
export type CustomLinkProps = Omit<JSX.IntrinsicElements["a"], "href"> & backgroundPosition: "0% 100%",
backgroundRepeat: "no-repeat",
backgroundSize: "0% $underline",
paddingBottom: "0.2rem",
// sets psuedo linear-gradient() for cool underline effect
backgroundGradientHack: {},
"&:hover": {
backgroundSize: "100% $underline",
},
});
export type CustomLinkProps = Omit<ComponentProps<typeof FancyLink>, "href"> &
NextLinkProps & { NextLinkProps & {
forceNewWindow?: boolean; forceNewWindow?: boolean;
}; };
@@ -17,25 +34,18 @@ const CustomLink = ({
target, target,
rel, rel,
forceNewWindow, forceNewWindow,
className,
...rest ...rest
}: CustomLinkProps) => { }: CustomLinkProps) => {
// this component auto-detects whether or not we should use a normal HTML anchor externally or next/link internally, // this component auto-detects whether or not we should use a normal HTML anchor externally or next/link internally,
// can be overridden with `forceNewWindow={true}`. // can be overridden with `forceNewWindow={true}`.
if (forceNewWindow || isAbsoluteUrl(href.toString())) { if (forceNewWindow || isAbsoluteUrl(href.toString())) {
return ( return (
<a <FancyLink href={href.toString()} target={target || "_blank"} rel={rel || "noopener noreferrer"} {...rest} />
href={href.toString()}
target={target || "_blank"}
rel={rel || "noopener noreferrer"}
className={classNames(styles.link, className)}
{...rest}
/>
); );
} else { } else {
return ( return (
<NextLink href={href} prefetch={prefetch} passHref={passHref}> <NextLink href={href} prefetch={prefetch} passHref={passHref}>
<a className={classNames(styles.link, className)} {...rest} /> <FancyLink {...rest} />
</NextLink> </NextLink>
); );
} }

View File

@@ -1,9 +0,0 @@
.unordered,
.ordered {
margin-left: 1.5em;
padding-left: 0;
}
.item {
padding-left: 0.25em;
}

View File

@@ -1,15 +1,13 @@
import classNames from "classnames"; import { styled, css } from "../../lib/styles/stitches.config";
import styles from "./List.module.css"; const ListStyles = css({
marginLeft: "1.5em",
paddingLeft: 0,
});
export const UnorderedList = ({ className, ...rest }: JSX.IntrinsicElements["ul"]) => ( export const UnorderedList = styled("ul", ListStyles);
<ul className={classNames(styles.unordered, className)} {...rest} /> export const OrderedList = styled("ol", ListStyles);
);
export const OrderedList = ({ className, ...rest }: JSX.IntrinsicElements["ol"]) => (
<ol className={classNames(styles.ordered, className)} {...rest} />
);
// TODO: this is based on good faith that the children are all `<li>`s... export const ListItem = styled("li", {
export const ListItem = ({ className, ...rest }: JSX.IntrinsicElements["li"]) => ( paddingLeft: "0.25em",
<li className={classNames(styles.item, className)} {...rest} /> });
);

View File

@@ -1,22 +0,0 @@
.wrapper {
display: inline-block;
text-align: center;
}
.box {
display: inline-block;
height: 100%;
animation: loading 1.5s infinite ease-in-out both;
background-color: var(--medium-light);
}
@keyframes loading {
0%,
80%,
100% {
transform: scale(0);
}
40% {
transform: scale(0.6);
}
}

View File

@@ -1,7 +1,26 @@
import { memo } from "react"; import { memo } from "react";
import classNames from "classnames"; import { styled, keyframes } from "../../lib/styles/stitches.config";
import styles from "./Loading.module.css"; const pulse = keyframes({
"0%, 80%, 100%": {
transform: "scale(0)",
},
"40%": {
transform: "scale(0.6)",
},
});
const Wrapper = styled("div", {
display: "inline-block",
textAlign: "center",
});
const Box = styled("div", {
display: "inline-block",
height: "100%",
animation: `${pulse} 1.5s infinite ease-in-out both`,
backgroundColor: "$mediumLight",
});
export type LoadingProps = { export type LoadingProps = {
width: number; // of entire container, in pixels width: number; // of entire container, in pixels
@@ -19,10 +38,9 @@ const Loading = ({ width, boxes = 3, timing = 0.1, className }: LoadingProps) =>
// width of each box correlates with number of boxes (with a little padding) // width of each box correlates with number of boxes (with a little padding)
// each individual box's animation has a staggered start in corresponding order // each individual box's animation has a staggered start in corresponding order
divs.push( divs.push(
<div <Box
key={i} key={i}
className={styles.box} css={{
style={{
width: `${width / (boxes + 1)}px`, width: `${width / (boxes + 1)}px`,
animationDelay: `${i * timing}s`, animationDelay: `${i * timing}s`,
}} }}
@@ -31,15 +49,15 @@ const Loading = ({ width, boxes = 3, timing = 0.1, className }: LoadingProps) =>
} }
return ( return (
<div <Wrapper
className={classNames(styles.wrapper, className)} className={className}
style={{ style={{
width: `${width}px`, width: `${width}px`,
height: `${width / 2}px`, height: `${width / 2}px`,
}} }}
> >
{divs} {divs}
</div> </Wrapper>
); );
}; };

View File

@@ -1,34 +0,0 @@
.menu {
display: inline-flex;
padding: 0;
margin: 0;
}
.item {
list-style: none;
display: inline-flex;
margin-left: 1em;
}
@media screen and (max-width: 768px) {
.menu {
width: 100%;
justify-content: space-between;
margin-left: 1em;
}
.item {
margin-left: 0;
}
}
@media screen and (max-width: 380px) {
.menu {
margin-left: 1.4em;
}
/* the home icon is redundant when space is SUPER tight */
.item:first-of-type {
display: none;
}
}

View File

@@ -1,27 +1,57 @@
import { memo } from "react"; import { memo } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import classNames from "classnames";
import MenuItem from "../MenuItem/MenuItem"; import MenuItem from "../MenuItem/MenuItem";
import { styled } from "../../lib/styles/stitches.config";
import { menuItems } from "../../lib/config/menu"; import { menuItems } from "../../lib/config/menu";
import type { ComponentProps } from "react";
import styles from "./Menu.module.css"; const Wrapper = styled("ul", {
display: "inline-flex",
padding: 0,
margin: 0,
export type MenuProps = { "@mobile": {
className?: string; width: "100%",
}; justifyContent: "space-between",
marginLeft: "1em",
},
const Menu = ({ className }: MenuProps) => { "@superNarrow": {
marginLeft: "1.4em",
},
});
const Item = styled("li", {
listStyle: "none",
display: "inline-flex",
marginLeft: "1em",
"@mobile": {
marginLeft: 0,
},
"@superNarrow": {
// the home icon is kinda redundant when space is SUPER tight
"&:first-of-type": {
display: "none",
},
},
});
export type MenuProps = ComponentProps<typeof Wrapper>;
const Menu = ({ ...rest }: MenuProps) => {
const router = useRouter(); const router = useRouter();
return ( return (
<ul className={classNames(styles.menu, className)}> <Wrapper {...rest}>
{menuItems.map((item, index) => ( {menuItems.map((item, index) => (
<li key={index} className={styles.item}> <Item key={index}>
{/* kinda weird/hacky way to determine if the *first part* of the current path matches this href */} {/* kinda weird/hacky way to determine if the *first part* of the current path matches this href */}
<MenuItem {...item} current={item.href === `/${router.pathname.split("/")[1]}`} /> <MenuItem {...item} current={item.href === `/${router.pathname.split("/")[1]}`} />
</li> </Item>
))} ))}
</ul> </Wrapper>
); );
}; };

View File

@@ -1,47 +0,0 @@
.link {
display: inline-flex;
align-items: center;
color: var(--medium-dark);
line-height: 1;
text-decoration: none;
padding: 0.6em;
}
.link:hover,
.link.current {
border-bottom: 0.2em solid;
margin-bottom: -0.2em;
}
.link:hover {
border-color: var(--kinda-light);
}
.link.current {
border-color: var(--link-underline);
}
.icon {
width: 1.25em;
height: 1.25em;
vertical-align: -0.3em;
}
.label {
font-size: 0.95em;
font-weight: 500;
margin-top: 0.1em;
margin-left: 0.8em;
}
@media screen and (max-width: 768px) {
.icon {
width: 1.8em;
height: 1.8em;
}
/* hide text next to emojis on mobile */
.label {
display: none;
}
}

View File

@@ -1,7 +1,52 @@
import Link from "next/link"; import NextLink from "next/link";
import classNames from "classnames"; import { styled } from "../../lib/styles/stitches.config";
import styles from "./MenuItem.module.css"; const Link = styled("a", {
display: "inline-flex",
alignItems: "center",
color: "$mediumDark",
lineHeight: 1,
textDecoration: "none",
padding: "0.6em",
"&:hover": {
borderBottom: "0.2em solid",
marginBottom: "-0.2em",
borderColor: "$kindaLight",
},
variants: {
current: {
true: {
borderBottom: "0.2em solid",
marginBottom: "-0.2em",
borderColor: "$linkUnderline !important",
},
},
},
});
const Label = styled("span", {
fontSize: "0.95em",
fontWeight: 500,
marginTop: "0.1em",
marginLeft: "0.8em",
"@mobile": {
display: "none",
},
});
const Icon = styled("svg", {
width: "1.25em",
height: "1.25em",
verticalAlign: "-0.3em",
"@mobile": {
width: "1.8em",
height: "1.8em",
},
});
export type MenuItemProps = { export type MenuItemProps = {
href?: string; href?: string;
@@ -14,19 +59,21 @@ export type MenuItemProps = {
icon: any; icon: any;
}; };
const MenuItem = ({ icon: Icon, href, text, current, className }: MenuItemProps) => { const MenuItem = ({ icon: ItemIcon, href, text, current, className }: MenuItemProps) => {
const linkContent = ( const linkContent = (
<> <>
<Icon className={classNames(styles.icon, className)} /> {text && <span className={styles.label}>{text}</span>} <Icon as={ItemIcon} /> {text && <Label>{text}</Label>}
</> </>
); );
// allow both navigational links and/or other interactive react components (e.g. the theme toggle) // allow both navigational links and/or other interactive react components (e.g. the theme toggle)
if (href) { if (href) {
return ( return (
<Link href={href} prefetch={false}> <NextLink href={href} prefetch={false} passHref={true}>
<a className={classNames(styles.link, current && styles.current, className)}>{linkContent}</a> <Link className={className} current={current}>
</Link> {linkContent}
</Link>
</NextLink>
); );
} else { } else {
return linkContent; return linkContent;

View File

@@ -1,55 +0,0 @@
.meta {
display: inline-flex;
flex-wrap: wrap;
font-size: 0.825em;
line-height: 2.3;
letter-spacing: 0.04em;
color: var(--medium);
}
.meta_item {
display: inline-flex;
margin-right: 1.6em;
white-space: nowrap;
}
.icon {
width: 1.2em;
height: 1.2em;
vertical-align: -0.2em;
margin-right: 0.6em;
}
.tags {
white-space: normal;
display: inline-flex;
flex-wrap: wrap;
}
.tag {
text-transform: lowercase;
white-space: nowrap;
margin-right: 0.75em;
}
.tag::before {
content: "\0023"; /* cosmetically hashtagify tags */
padding-right: 0.125em;
color: var(--light);
}
.tag:last-of-type {
margin-right: 0;
}
.date_link,
.edit_link {
color: inherit;
text-decoration: none;
}
.views {
/* fix potential layout shift when number of hits loads */
min-width: 7em;
margin-right: 0;
}

View File

@@ -1,73 +1,121 @@
import Link from "next/link"; import Link from "next/link";
import classNames from "classnames";
import { format } from "date-fns"; import { format } from "date-fns";
import HitCounter from "../HitCounter/HitCounter"; import HitCounter from "../HitCounter/HitCounter";
import NoteTitle from "../NoteTitle/NoteTitle"; import NoteTitle from "../NoteTitle/NoteTitle";
import { DateIcon, TagIcon, EditIcon, ViewsIcon } from "../Icons"; import { DateIcon, TagIcon, EditIcon, ViewsIcon } from "../Icons";
import { styled } from "../../lib/styles/stitches.config";
import * as config from "../../lib/config"; import * as config from "../../lib/config";
import type { NoteType } from "../../types"; import type { NoteType } from "../../types";
import styles from "./NoteMeta.module.css"; const Wrapper = styled("div", {
display: "inline-flex",
flexWrap: "wrap",
fontSize: "0.825em",
lineHeight: 2.3,
letterSpacing: "0.04em",
color: "$medium",
});
const MetaItem = styled("div", {
display: "inline-flex",
marginRight: "1.6em",
whiteSpace: "nowrap",
});
const MetaLink = styled("a", {
color: "inherit",
textDecoration: "none",
});
const Icon = styled("svg", {
width: "1.2em",
height: "1.2em",
verticalAlign: "-0.2em",
marginRight: "0.6em",
});
const Tag = styled("span", {
textTransform: "lowercase",
whiteSpace: "nowrap",
marginRight: "0.75em",
"&::before": {
content: "\\0023", // cosmetically hashtagify tags
paddingRight: "0.125em",
color: "$light",
},
"&:last-of-type": {
marginRight: 0,
},
});
export type NoteMetaProps = Pick<NoteType["frontMatter"], "slug" | "date" | "title" | "htmlTitle" | "tags">; export type NoteMetaProps = Pick<NoteType["frontMatter"], "slug" | "date" | "title" | "htmlTitle" | "tags">;
const NoteMeta = ({ slug, date, title, htmlTitle, tags = [] }: NoteMetaProps) => ( const NoteMeta = ({ slug, date, title, htmlTitle, tags = [] }: NoteMetaProps) => (
<> <>
<div className={styles.meta}> <Wrapper>
<div className={styles.meta_item}> <MetaItem>
<Link <Link
href={{ href={{
pathname: "/notes/[slug]/", pathname: "/notes/[slug]/",
query: { slug }, query: { slug },
}} }}
passHref={true}
> >
<a className={styles.date_link}> <MetaLink>
<span> <span>
<DateIcon className={styles.icon} /> <Icon as={DateIcon} />
</span> </span>
<span title={format(new Date(date), "PPppp")}>{format(new Date(date), "MMMM d, yyyy")}</span> <span title={format(new Date(date), "PPppp")}>{format(new Date(date), "MMMM d, yyyy")}</span>
</a> </MetaLink>
</Link> </Link>
</div> </MetaItem>
{tags.length > 0 && ( {tags.length > 0 && (
<div className={classNames(styles.meta_item, styles.tags)}> <MetaItem
css={{
whiteSpace: "normal",
display: "inline-flex",
flexWrap: "wrap",
}}
>
<span> <span>
<TagIcon className={styles.icon} /> <Icon as={TagIcon} />
</span> </span>
{tags.map((tag) => ( {tags.map((tag) => (
<span key={tag} className={styles.tag}> <Tag key={tag}>{tag}</Tag>
{tag}
</span>
))} ))}
</div> </MetaItem>
)} )}
<div className={styles.meta_item}> <MetaItem>
<a <MetaLink
className={styles.edit_link}
href={`https://github.com/${config.githubRepo}/blob/main/notes/${slug}.mdx`} href={`https://github.com/${config.githubRepo}/blob/main/notes/${slug}.mdx`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
title={`Edit "${title}" on GitHub`} title={`Edit "${title}" on GitHub`}
> >
<span> <span>
<EditIcon className={styles.icon} /> <Icon as={EditIcon} />
</span> </span>
<span>Improve This Post</span> <span>Improve This Post</span>
</a> </MetaLink>
</div> </MetaItem>
{/* only count hits on production site */} {/* only count hits on production site */}
{process.env.NEXT_PUBLIC_VERCEL_ENV === "production" && ( {process.env.NEXT_PUBLIC_VERCEL_ENV === "production" && (
<div className={classNames(styles.meta_item, styles.views)}> <MetaItem
// fix potential layout shift when number of hits loads
css={{ minWidth: "7em", marginRight: 0 }}
>
<span> <span>
<ViewsIcon className={styles.icon} /> <Icon as={ViewsIcon} />
</span> </span>
<HitCounter slug={`notes/${slug}`} /> <HitCounter slug={`notes/${slug}`} />
</div> </MetaItem>
)} )}
</div> </Wrapper>
<NoteTitle slug={slug} htmlTitle={htmlTitle || title} /> <NoteTitle slug={slug} htmlTitle={htmlTitle || title} />
</> </>

View File

@@ -1,21 +0,0 @@
.title {
margin: 0.3em 0 0.5em -0.03em;
font-size: 2.1em;
line-height: 1.3;
font-weight: 700;
}
.title code {
margin: 0 0.075em;
}
.link {
color: var(--text);
text-decoration: none;
}
@media screen and (max-width: 768px) {
.title {
font-size: 1.8em;
}
}

View File

@@ -1,22 +1,42 @@
import Link from "next/link"; import NextLink from "next/link";
import classNames from "classnames"; import { styled } from "../../lib/styles/stitches.config";
import type { ComponentProps } from "react";
import type { NoteType } from "../../types"; import type { NoteType } from "../../types";
import styles from "./NoteTitle.module.css"; const Title = styled("h1", {
margin: "0.3em 0 0.5em -0.03em",
fontSize: "2.1em",
lineHeight: 1.3,
fontWeight: 700,
export type NoteTitleProps = Pick<NoteType["frontMatter"], "slug" | "htmlTitle"> & JSX.IntrinsicElements["h1"]; "& code": {
margin: "0 0.075em",
},
const NoteTitle = ({ slug, htmlTitle, className, ...rest }: NoteTitleProps) => ( "@mobile": {
<h1 className={classNames(styles.title, className)} {...rest}> fontSize: "1.8em",
<Link },
});
const Link = styled("a", {
color: "$text",
textDecoration: "none",
});
export type NoteTitleProps = Pick<NoteType["frontMatter"], "slug" | "htmlTitle"> & ComponentProps<typeof Title>;
const NoteTitle = ({ slug, htmlTitle, ...rest }: NoteTitleProps) => (
<Title {...rest}>
<NextLink
href={{ href={{
pathname: "/notes/[slug]/", pathname: "/notes/[slug]/",
query: { slug }, query: { slug },
}} }}
passHref={true}
> >
<a className={styles.link} dangerouslySetInnerHTML={{ __html: htmlTitle }} /> <Link dangerouslySetInnerHTML={{ __html: htmlTitle }} />
</Link> </NextLink>
</h1> </Title>
); );
export default NoteTitle; export default NoteTitle;

View File

@@ -1,51 +0,0 @@
.section {
font-size: 1.1em;
line-height: 1.1;
margin: 2.4em 0;
}
.section:first-of-type {
margin-top: 0;
}
.section:last-of-type {
margin-bottom: 0;
}
.year {
font-size: 2.2em;
margin-top: 0;
margin-bottom: 0.5em;
}
.list {
list-style-type: none;
margin: 0;
padding: 0;
}
.row {
display: flex;
line-height: 1.75;
margin-bottom: 1em;
}
.row:last-of-type {
margin-bottom: 0;
}
.date {
width: 5.25em;
flex-shrink: 0;
color: var(--medium);
}
@media screen and (max-width: 768px) {
.section {
margin: 1.8em 0;
}
.year {
font-size: 2em;
}
}

View File

@@ -1,8 +1,57 @@
import { format } from "date-fns"; import { format } from "date-fns";
import Link from "../Link/Link"; import Link from "../Link/Link";
import { styled } from "../../lib/styles/stitches.config";
import type { NoteType } from "../../types"; import type { NoteType } from "../../types";
import styles from "./NotesList.module.css"; const Section = styled("section", {
fontSize: "1.1em",
lineHeight: 1.1,
margin: "2.4em 0",
"&:first-of-type": {
marginTop: 0,
},
"&:last-of-type": {
marginBottom: 0,
},
"@mobile": {
margin: "1.8em 0",
},
});
const Year = styled("h2", {
fontSize: "2.2em",
marginTop: 0,
marginBottom: "0.5em",
"@mobile": {
fontSize: "2em",
},
});
const List = styled("ul", {
listStyleType: "none",
margin: 0,
padding: 0,
});
const Post = styled("li", {
display: "flex",
lineHeight: 1.75,
marginBottom: "1em",
"&:last-of-type": {
marginBottom: 0,
},
});
const PostDate = styled("span", {
width: "5.25em",
flexShrink: 0,
color: "$medium",
});
export type NotesListProps = { export type NotesListProps = {
notesByYear: Record<string, NoteType["frontMatter"][]>; notesByYear: Record<string, NoteType["frontMatter"][]>;
@@ -13,12 +62,12 @@ const NotesList = ({ notesByYear }: NotesListProps) => {
Object.entries(notesByYear).forEach(([year, notes]: [string, NoteType["frontMatter"][]]) => { Object.entries(notesByYear).forEach(([year, notes]: [string, NoteType["frontMatter"][]]) => {
sections.push( sections.push(
<section key={year} className={styles.section}> <Section key={year}>
<h2 className={styles.year}>{year}</h2> <Year>{year}</Year>
<ul className={styles.list}> <List>
{notes.map(({ slug, date, htmlTitle }) => ( {notes.map(({ slug, date, htmlTitle }) => (
<li key={slug} className={styles.row}> <Post key={slug}>
<span className={styles.date}>{format(new Date(date), "MMM d")}</span> <PostDate>{format(new Date(date), "MMM d")}</PostDate>
<span> <span>
<Link <Link
href={{ href={{
@@ -28,10 +77,10 @@ const NotesList = ({ notesByYear }: NotesListProps) => {
dangerouslySetInnerHTML={{ __html: htmlTitle }} dangerouslySetInnerHTML={{ __html: htmlTitle }}
/> />
</span> </span>
</li> </Post>
))} ))}
</ul> </List>
</section> </Section>
); );
}); });

View File

@@ -1,15 +0,0 @@
.link {
margin: 0 0.4em;
color: var(--text);
text-decoration: none;
}
.link:hover {
color: var(--link);
}
.icon {
width: 1.2em;
height: 1.2em;
vertical-align: -0.2em;
}

View File

@@ -1,16 +1,32 @@
import classNames from "classnames";
import { OctocatOcticon } from "../Icons"; import { OctocatOcticon } from "../Icons";
import { styled } from "../../lib/styles/stitches.config";
import type { ComponentProps } from "react";
import styles from "./OctocatLink.module.css"; const Link = styled("a", {
margin: "0 0.4em",
color: "$text",
textDecoration: "none",
export type OctocatLinkProps = JSX.IntrinsicElements["a"] & { "&:hover": {
color: "$link",
},
});
const Octocat = styled(OctocatOcticon, {
width: "1.2em",
height: "1.2em",
verticalAlign: "-0.2em",
fill: "currentColor",
});
export type OctocatLinkProps = ComponentProps<typeof Link> & {
repo: string; repo: string;
}; };
const OctocatLink = ({ repo, className, ...rest }: OctocatLinkProps) => ( const OctocatLink = ({ repo, className, ...rest }: OctocatLinkProps) => (
<a className={styles.link} href={`https://github.com/${repo}`} target="_blank" rel="noopener noreferrer" {...rest}> <Link href={`https://github.com/${repo}`} target="_blank" rel="noopener noreferrer" {...rest}>
<OctocatOcticon fill="currentColor" className={classNames(styles.icon, className)} /> <Octocat className={className} />
</a> </Link>
); );
export default OctocatLink; export default OctocatLink;

View File

@@ -1,17 +0,0 @@
.title {
margin-top: 0;
margin-bottom: 0.6em;
font-size: 2em;
text-align: center;
}
.link {
color: var(--text);
text-decoration: none;
}
@media screen and (max-width: 768px) {
.title {
font-size: 1.8em;
}
}

View File

@@ -1,22 +1,37 @@
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import Link from "next/link"; import NextLink from "next/link";
import classNames from "classnames"; import { styled } from "../../lib/styles/stitches.config";
import { baseUrl } from "../../lib/config"; import { baseUrl } from "../../lib/config";
import type { ComponentProps } from "react";
import styles from "./PageTitle.module.css"; const Title = styled("h1", {
marginTop: 0,
marginBottom: "0.6em",
fontSize: "2em",
textAlign: "center",
export type PageTitleProps = JSX.IntrinsicElements["h1"]; "@mobile": {
fontSize: "1.8em",
},
});
const PageTitle = ({ className, children, ...rest }: PageTitleProps) => { const Link = styled("a", {
color: "$text",
textDecoration: "none",
});
export type PageTitleProps = ComponentProps<typeof Title>;
const PageTitle = ({ children, ...rest }: PageTitleProps) => {
const router = useRouter(); const router = useRouter();
const canonical = `${baseUrl}${router.pathname}/`; const canonical = `${baseUrl}${router.pathname}/`;
return ( return (
<h1 className={classNames(styles.title, className)} {...rest}> <Title {...rest}>
<Link href={canonical}> <NextLink href={canonical} passHref={true}>
<a className={styles.link}>{children}</a> <Link>{children}</Link>
</Link> </NextLink>
</h1> </Title>
); );
}; };

View File

@@ -1,62 +0,0 @@
.card {
width: 100%;
padding: 1.2em 1.2em 0.8em 1.2em;
border: 1px solid;
border-radius: var(--rounded-edge-radius);
font-size: 0.85em;
color: var(--medium-dark);
border-color: var(--kinda-light);
/* light-dark theme switch fading */
transition: border 0.25s ease;
}
.name {
font-size: 1.2em;
font-weight: 600;
}
.description {
margin-top: 0.7em;
margin-bottom: 0.5em;
line-height: 1.7;
}
.meta {
display: flex;
flex-wrap: wrap;
align-items: baseline;
}
.meta_item {
margin-right: 1.5em;
font-size: 0.875em;
line-height: 2;
color: var(--medium);
}
.meta_link {
color: inherit;
text-decoration: none;
}
.meta_link:hover {
color: var(--link);
}
.octicon,
.language_color {
width: 16px;
height: 16px;
vertical-align: text-bottom;
margin-right: 0.5em;
}
.language_color {
display: inline-block;
width: 1.15em;
height: 1.15em;
border-radius: 50%;
position: relative;
vertical-align: text-top;
}

View File

@@ -1,10 +1,71 @@
import classNames from "classnames";
import { intlFormat, formatDistanceToNowStrict } from "date-fns"; import { intlFormat, formatDistanceToNowStrict } from "date-fns";
import Link from "../Link/Link"; import Link from "../Link/Link";
import { StarOcticon, ForkOcticon } from "../Icons"; import { StarOcticon, ForkOcticon } from "../Icons";
import { styled } from "../../lib/styles/stitches.config";
import type { RepositoryType } from "../../types"; import type { RepositoryType } from "../../types";
import styles from "./RepositoryCard.module.css"; const Wrapper = styled("div", {
width: "100%",
padding: "1.2em 1.2em 0.8em 1.2em",
border: "1px solid $kindaLight",
borderRadius: "$rounded",
fontSize: "0.85em",
color: "$mediumDark",
// light-dark theme switch fading
transition: "border 0.25s ease",
});
const Name = styled(Link, {
fontSize: "1.2em",
fontWeight: 600,
});
const Description = styled("p", {
marginTop: "0.7em",
marginBottom: "0.5em",
lineHeight: 1.7,
});
const Meta = styled("div", {
display: "flex",
flexWrap: "wrap",
alignItems: "baseline",
});
const MetaItem = styled("div", {
marginRight: "1.5em",
fontSize: "0.875em",
lineHeight: 2,
color: "$medium",
});
const MetaLink = styled("a", {
color: "inherit",
textDecoration: "none",
"&:hover": {
color: "$link",
},
});
const MetaIcon = styled("svg", {
width: "16px",
height: "16px",
verticalAlign: "text-bottom",
marginRight: "0.5em",
fill: "currentColor",
});
const LanguageCircle = styled("span", {
display: "inline-block",
position: "relative",
width: "1.15em",
height: "1.15em",
marginRight: "0.5em",
borderRadius: "50%",
verticalAlign: "text-top",
});
export type RepositoryCardProps = RepositoryType & { export type RepositoryCardProps = RepositoryType & {
className?: string; className?: string;
@@ -20,53 +81,48 @@ const RepositoryCard = ({
updatedAt, updatedAt,
className, className,
}: RepositoryCardProps) => ( }: RepositoryCardProps) => (
<div className={classNames(styles.card, className)}> <Wrapper className={className}>
<Link className={styles.name} href={url}> <Name href={url}>{name}</Name>
{name}
</Link>
{description && <p className={styles.description}>{description}</p>} {description && <Description>{description}</Description>}
<div className={styles.meta}> <Meta>
{language && ( {language && (
<div className={styles.meta_item}> <MetaItem>
<span className={styles.language_color} style={{ backgroundColor: language.color }} /> <LanguageCircle css={{ backgroundColor: language.color }} />
<span>{language.name}</span> <span>{language.name}</span>
</div> </MetaItem>
)} )}
{stars > 0 && ( {stars > 0 && (
<div className={styles.meta_item}> <MetaItem>
<a <MetaLink
className={styles.meta_link}
href={`${url}/stargazers`} href={`${url}/stargazers`}
title={`${stars.toLocaleString("en-US")} ${stars === 1 ? "star" : "stars"}`} title={`${stars.toLocaleString("en-US")} ${stars === 1 ? "star" : "stars"}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
<StarOcticon fill="currentColor" className={styles.octicon} /> <MetaIcon as={StarOcticon} />
<span>{stars.toLocaleString("en-US")}</span> <span>{stars.toLocaleString("en-US")}</span>
</a> </MetaLink>
</div> </MetaItem>
)} )}
{forks > 0 && ( {forks > 0 && (
<div className={styles.meta_item}> <MetaItem>
<a <MetaLink
className={styles.meta_link}
href={`${url}/network/members`} href={`${url}/network/members`}
title={`${forks.toLocaleString("en-US")} ${forks === 1 ? "fork" : "forks"}`} title={`${forks.toLocaleString("en-US")} ${forks === 1 ? "fork" : "forks"}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
<ForkOcticon fill="currentColor" className={styles.octicon} /> <MetaIcon as={ForkOcticon} />
<span>{forks.toLocaleString("en-US")}</span> <span>{forks.toLocaleString("en-US")}</span>
</a> </MetaLink>
</div> </MetaItem>
)} )}
<div <MetaItem
className={styles.meta_item}
title={intlFormat( title={intlFormat(
new Date(updatedAt), new Date(updatedAt),
{ {
@@ -83,9 +139,9 @@ const RepositoryCard = ({
)} )}
> >
<span>Updated {formatDistanceToNowStrict(new Date(updatedAt), { addSuffix: true })}</span> <span>Updated {formatDistanceToNowStrict(new Date(updatedAt), { addSuffix: true })}</span>
</div> </MetaItem>
</div> </Meta>
</div> </Wrapper>
); );
export default RepositoryCard; export default RepositoryCard;

View File

@@ -1,48 +0,0 @@
.link {
display: inline-flex;
align-items: center;
color: var(--medium-dark);
text-decoration: none;
}
.link:hover {
color: var(--link);
}
.selfie {
width: 50px;
height: 50px;
line-height: 0;
padding: 0;
}
.selfie img {
border: 1px solid var(--light) !important;
border-radius: 50%;
}
.name {
margin: 0 0.6em;
font-size: 1.2em;
font-weight: 500;
line-height: 1;
}
@media screen and (max-width: 768px) {
.selfie {
width: 70px;
height: 70px;
}
.name {
display: none;
}
.selfie img {
border-width: 2px !important;
}
.link:hover .selfie img {
border-color: var(--link-underline) !important;
}
}

View File

@@ -1,20 +1,66 @@
import { memo } from "react"; import { memo } from "react";
import Link from "next/link"; import NextLink from "next/link";
import Image from "next/image"; import NextImage from "next/image";
import classNames from "classnames"; import { styled } from "../../lib/styles/stitches.config";
import type { ComponentProps } from "react";
import styles from "./Selfie.module.css";
import selfieJpg from "../../public/static/images/selfie.jpg"; import selfieJpg from "../../public/static/images/selfie.jpg";
export type SelfieProps = { const ConstrainImage = styled("div", {
className?: string; width: "50px",
}; height: "50px",
lineHeight: 0,
padding: 0,
const Selfie = ({ className }: SelfieProps) => ( "@mobile": {
<Link href="/"> width: "70px",
<a className={classNames(styles.link, className)}> height: "70px",
<div className={styles.selfie}> },
});
const Image = styled(NextImage, {
border: "1px solid $light !important",
borderRadius: "50%",
"@mobile": {
borderWidth: "2px !important",
},
});
const Link = styled("a", {
display: "inline-flex",
alignItems: "center",
color: "$mediumDark",
textDecoration: "none",
"&:hover": {
color: "$link",
"@mobile": {
[`${Image}`]: {
borderColor: "$linkUnderline !important",
},
},
},
});
const Name = styled("span", {
margin: "0 0.6em",
fontSize: "1.2em",
fontWeight: 500,
lineHeight: 1,
"@mobile": {
display: "none",
},
});
export type SelfieProps = ComponentProps<typeof Link>;
const Selfie = ({ ...rest }: SelfieProps) => (
<NextLink href="/" passHref={true}>
<Link {...rest}>
<ConstrainImage>
<Image <Image
src={selfieJpg} src={selfieJpg}
alt="Photo of Jake Jarvis" alt="Photo of Jake Jarvis"
@@ -24,10 +70,10 @@ const Selfie = ({ className }: SelfieProps) => (
layout="intrinsic" layout="intrinsic"
priority priority
/> />
</div> </ConstrainImage>
<span className={styles.name}>Jake Jarvis</span> <Name>Jake Jarvis</Name>
</a> </Link>
</Link> </NextLink>
); );
export default memo(Selfie); export default memo(Selfie);

View File

@@ -1,28 +0,0 @@
.terminal {
width: 100%;
height: 100%;
padding: 1em;
background-color: #000000;
color: #cccccc;
font-size: 0.925em;
font-weight: 500;
line-height: 2;
white-space: pre-wrap;
user-select: none;
}
.blink {
display: inline-block;
vertical-align: text-bottom;
width: 10px;
border-bottom: 2px solid #cccccc;
animation: blink 1s step-end infinite;
}
@keyframes blink {
50% {
opacity: 0;
}
}

View File

@@ -1,17 +1,46 @@
import { forwardRef } from "react"; import { forwardRef } from "react";
import classNames from "classnames"; import { keyframes, styled } from "../../lib/styles/stitches.config";
import type { Ref } from "react"; import type { Ref, ComponentProps } from "react";
import styles from "./Terminal.module.css"; const BlackBox = styled("div", {
width: "100%",
height: "100%",
padding: "1.25em",
backgroundColor: "#000000",
color: "#cccccc",
});
export type TerminalProps = JSX.IntrinsicElements["div"]; const Monospace = styled("pre", {
display: "block",
margin: 0,
lineHeight: 1.75,
fontSize: "0.925em",
fontWeight: 500,
whiteSpace: "pre-wrap",
userSelect: "none",
});
// flashing terminal cursor underscore-looking thingy
const Underscore = styled("span", {
display: "inline-block",
verticalAlign: "text-bottom",
width: "10px",
borderBottom: "2px solid #cccccc",
// blink every second for 0.4s
animation: `${keyframes({ "40%": { opacity: 0 } })} 1s step-end infinite`,
});
export type TerminalProps = ComponentProps<typeof BlackBox>;
// a DOS-style terminal box with dynamic text // a DOS-style terminal box with dynamic text
const Terminal = forwardRef(function Terminal({ className, ...rest }: TerminalProps, ref: Ref<HTMLSpanElement>) { const Terminal = forwardRef(function Terminal({ ...rest }: TerminalProps, ref: Ref<HTMLSpanElement>) {
return ( return (
<div className={classNames("monospace", className, styles.terminal)} {...rest}> <BlackBox {...rest}>
<span ref={ref} /> <span className={styles.blink} /> <Monospace>
</div> <span ref={ref} /> <Underscore />
</Monospace>
</BlackBox>
); );
}); });

View File

@@ -1,12 +0,0 @@
.button {
border: 0;
padding: 0.6em;
margin-right: -0.6em;
background: none;
cursor: pointer;
color: var(--medium-dark);
}
.button:hover {
color: var(--warning);
}

View File

@@ -1,8 +1,20 @@
import { useEffect, useState, memo } from "react"; import { useEffect, useState, memo } from "react";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
import { styled } from "../../lib/styles/stitches.config";
import { SunIcon, MoonIcon } from "../Icons"; import { SunIcon, MoonIcon } from "../Icons";
import styles from "./ThemeToggle.module.css"; const Button = styled("button", {
border: 0,
padding: "0.6em",
marginRight: "-0.6em",
background: "none",
cursor: "pointer",
color: "$mediumDark",
"&:hover": {
color: "$warning",
},
});
export type ThemeToggleProps = { export type ThemeToggleProps = {
className?: string; className?: string;
@@ -16,20 +28,19 @@ const ThemeToggle = ({ className }: ThemeToggleProps) => {
useEffect(() => setMounted(true), []); useEffect(() => setMounted(true), []);
if (!mounted) { if (!mounted) {
return ( return (
<button className={styles.button} aria-hidden={true}> <Button aria-hidden={true}>
<SunIcon className={className} /> <SunIcon className={className} />
</button> </Button>
); );
} }
return ( return (
<button <Button
className={styles.button}
onClick={() => setTheme(resolvedTheme === "light" ? "dark" : "light")} onClick={() => setTheme(resolvedTheme === "light" ? "dark" : "light")}
title={resolvedTheme === "light" ? "Toggle Dark Mode" : "Toggle Light Mode"} title={resolvedTheme === "light" ? "Toggle Dark Mode" : "Toggle Light Mode"}
> >
{resolvedTheme === "light" ? <SunIcon className={className} /> : <MoonIcon className={className} />} {resolvedTheme === "light" ? <SunIcon className={className} /> : <MoonIcon className={className} />}
</button> </Button>
); );
}; };

View File

@@ -1,26 +0,0 @@
.display {
height: 600px;
width: 100%;
max-width: 800px;
/* fix fuzziness: https://stackoverflow.com/a/13492784 */
image-rendering: optimizeSpeed;
image-rendering: -webkit-optimize-contrast;
image-rendering: crisp-edges;
image-rendering: pixelated;
-ms-interpolation-mode: nearest-neighbor;
}
.display div {
background: none !important;
}
.display div canvas {
cursor: default !important;
}
.terminal {
height: 400px;
width: 100%;
max-width: 700px;
}

View File

@@ -2,8 +2,41 @@ import { useRef, useEffect, useState, memo } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import RFB from "@novnc/novnc/core/rfb.js"; import RFB from "@novnc/novnc/core/rfb.js";
import Terminal from "../Terminal/Terminal"; import Terminal from "../Terminal/Terminal";
import { styled } from "../../lib/styles/stitches.config";
import styles from "./VNC.module.css"; const Display = styled(
"div",
{
height: "600px",
width: "100%",
maxWidth: "800px",
// these are injected by noVNC after connection, so we can't target them directly:
"& div": {
background: "none !important",
"& canvas": {
cursor: "default !important",
},
},
},
// fix fuziness in different browsers: https://stackoverflow.com/a/13492784
// separate objects since these are duplicate properties: https://github.com/modulz/stitches/issues/758#issuecomment-913580518
{
imageRendering: "-webkit-optimize-contrast",
},
{
imageRendering: "pixelated",
MSInterpolationMode: "nearest-neighbor",
}
);
const DOS = styled(Terminal, {
height: "400px",
width: "100%",
maxWidth: "700px",
});
export type VNCProps = { export type VNCProps = {
server: string; server: string;
@@ -104,10 +137,10 @@ const VNC = ({ server }: VNCProps) => {
return ( return (
<> <>
<Terminal ref={terminalRef} className={styles.terminal} /> <DOS ref={terminalRef} />
{/* the VNC canvas is hidden until we've successfully connected to the socket */} {/* the VNC canvas is hidden until we've successfully connected to the socket */}
<div ref={screenRef} className={styles.display} style={{ display: "none" }} /> <Display ref={screenRef} style={{ display: "none" }} />
</> </>
); );
}; };

View File

@@ -1,14 +0,0 @@
.wrapper {
position: relative;
padding-top: 56.25%;
}
.wrapper > div {
position: absolute;
top: 0;
left: 0;
}
.wrapper video {
border-radius: var(--rounded-edge-radius);
}

View File

@@ -1,8 +1,21 @@
import classNames from "classnames";
import ReactPlayer from "react-player/file"; import ReactPlayer from "react-player/file";
import { styled } from "../../lib/styles/stitches.config";
import type { FilePlayerProps } from "react-player/file"; import type { FilePlayerProps } from "react-player/file";
import styles from "./Video.module.css"; const Wrapper = styled("div", {
position: "relative",
paddingTop: "56.25%",
"& > div": {
position: "absolute",
top: 0,
left: 0,
},
"& video": {
borderRadius: "$rounded",
},
});
export type VideoProps = Partial<FilePlayerProps> & { export type VideoProps = Partial<FilePlayerProps> & {
src: { src: {
@@ -59,7 +72,7 @@ const Video = ({ src, thumbnail, subs, autoplay, className, ...rest }: VideoProp
} }
return ( return (
<div className={classNames(styles.wrapper, className)}> <Wrapper className={className}>
<ReactPlayer <ReactPlayer
width="100%" width="100%"
height="100%" height="100%"
@@ -69,7 +82,7 @@ const Video = ({ src, thumbnail, subs, autoplay, className, ...rest }: VideoProp
config={config} config={config}
{...rest} {...rest}
/> />
</div> </Wrapper>
); );
}; };

View File

@@ -1,9 +0,0 @@
.wallpaper {
width: 100%;
min-height: 400px;
}
.tile {
background-repeat: repeat;
background-position: center;
}

View File

@@ -1,21 +1,43 @@
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import classNames from "classnames"; import { styled } from "../../lib/styles/stitches.config";
import type { ComponentProps } from "react";
import type { VariantProps } from "@stitches/react";
import styles from "./Wallpaper.module.css"; const Wrapper = styled("main", {
width: "100%",
minHeight: "400px",
export type WallpaperProps = JSX.IntrinsicElements["main"] & { variants: {
tile: {
true: {
backgroundRepeat: "repeat",
backgroundPosition: "center",
},
},
},
});
export type WallpaperProps = ComponentProps<typeof Wrapper> & {
image: string; image: string;
tile?: boolean; tile?: boolean;
}; };
const Wallpaper = ({ image, tile, className, ...rest }: WallpaperProps) => { const Wallpaper = ({ image, tile, ...rest }: WallpaperProps) => {
const bgRef = useRef<HTMLDivElement>(null); const bgRef = useRef<VariantProps<typeof Wrapper>>(null);
useEffect(() => { useEffect(() => {
// @ts-ignore
bgRef.current.style.backgroundImage = `url(${image})`; bgRef.current.style.backgroundImage = `url(${image})`;
}, []); // eslint-disable-line react-hooks/exhaustive-deps }, []); // eslint-disable-line react-hooks/exhaustive-deps
return <main ref={bgRef} className={classNames(styles.wallpaper, tile && styles.tile, className)} {...rest} />; return (
<Wrapper
tile={tile}
// @ts-ignore
ref={bgRef}
{...rest}
/>
);
}; };
export default Wallpaper; export default Wallpaper;

View File

@@ -1,15 +0,0 @@
.wrapper {
position: relative;
padding-top: 56.25%;
}
.wrapper > div {
position: absolute;
top: 0;
left: 0;
}
.wrapper :global(.react-player__preview),
.wrapper iframe {
border-radius: var(--rounded-edge-radius);
}

View File

@@ -1,8 +1,21 @@
import classNames from "classnames";
import ReactPlayer from "react-player/youtube"; import ReactPlayer from "react-player/youtube";
import { styled } from "../../lib/styles/stitches.config";
import type { YouTubePlayerProps } from "react-player/youtube"; import type { YouTubePlayerProps } from "react-player/youtube";
import styles from "./YouTubeEmbed.module.css"; const Wrapper = styled("div", {
position: "relative",
paddingTop: "56.25%",
"& > div": {
position: "absolute",
top: 0,
left: 0,
},
"& .react-player__preview, & iframe": {
borderRadius: "$rounded",
},
});
export type YouTubeEmbedProps = Partial<YouTubePlayerProps> & { export type YouTubeEmbedProps = Partial<YouTubePlayerProps> & {
id: string; id: string;
@@ -10,7 +23,7 @@ export type YouTubeEmbedProps = Partial<YouTubePlayerProps> & {
}; };
const YouTubeEmbed = ({ id, className, ...rest }: YouTubeEmbedProps) => ( const YouTubeEmbed = ({ id, className, ...rest }: YouTubeEmbedProps) => (
<div className={classNames(styles.wrapper, className)}> <Wrapper className={className}>
<ReactPlayer <ReactPlayer
width="100%" width="100%"
height="100%" height="100%"
@@ -19,7 +32,7 @@ const YouTubeEmbed = ({ id, className, ...rest }: YouTubeEmbedProps) => (
controls controls
{...rest} {...rest}
/> />
</div> </Wrapper>
); );
export default YouTubeEmbed; export default YouTubeEmbed;

View File

@@ -1,79 +0,0 @@
import decamelize from "decamelize";
// Light/dark theme-related CSS variables that are inlined in Layout.tsx and become available globally.
// TODO: Probably invert the object so that *each variable* has a light and dark key.
const themes = {
light: {
backgroundInner: "#ffffff",
backgroundOuter: "#fcfcfc",
backgroundHeader: "rgba(252, 252, 252, 0.7)",
text: "#202020",
mediumDark: "#515151",
medium: "#5e5e5e",
mediumLight: "#757575",
light: "#d2d2d2",
kindaLight: "#e3e3e3",
superLight: "#f4f4f4",
superDuperLight: "#fbfbfb",
link: "#0e6dc2",
linkUnderline: "rgba(14, 109, 194, 0.4)",
success: "#44a248",
error: "#ff1b1b",
warning: "#f78200",
// Syntax Highlighting (light) - modified from Monokai Light: https://github.com/mlgill/pygments-style-monokailight
codeText: "#313131",
codeBackground: "#fdfdfd",
codeComment: "#656e77",
codeKeyword: "#029cb9",
codeAttribute: "#70a800",
codeNamespace: "#f92672",
codeLiteral: "#ae81ff",
codePunctuation: "#111111",
codeVariable: "#d88200",
codeAddition: "#44a248",
codeDeletion: "#ff1b1b",
},
dark: {
backgroundInner: "#1e1e1e",
backgroundOuter: "#252525",
backgroundHeader: "rgba(37, 37, 37, 0.85)",
text: "#f1f1f1",
mediumDark: "#d7d7d7",
medium: "#b1b1b1",
mediumLight: "#959595",
light: "#646464",
kindaLight: "#535353",
superLight: "#272727",
superDuperLight: "#1f1f1f",
link: "#88c7ff",
linkUnderline: "rgba(136, 199, 255, 0.4)",
success: "#78df55",
error: "#ff5151",
warning: "#f2b702",
// Syntax Highlighting (dark) - modified from Dracula: https://github.com/dracula/pygments
codeText: "#e4e4e4",
codeBackground: "#212121",
codeComment: "#929292",
codeKeyword: "#3b9dd2",
codeAttribute: "#78df55",
codeNamespace: "#f95757",
codeLiteral: "#d588fb",
codePunctuation: "#cccccc",
codeVariable: "#fd992a",
codeAddition: "#78df55",
codeDeletion: "#ff5151",
},
};
// JS-friendly camelCase to CSS-friendly kebab-case
const camelToKebab = (camel: string) => decamelize(camel, { separator: "-" });
// converts each variable in a given theme object to CSS syntax and returns all of them as one long string
export const toCSS = (theme: Record<string, string>) =>
Object.entries(theme)
.map(([name, color]) => `--${camelToKebab(name)}:${color};`)
.join("");
export default themes;

View File

@@ -0,0 +1,24 @@
// @ts-nocheck
// Legacy
import comicNeueLatin400NormalWoff from "@fontsource/comic-neue/files/comic-neue-latin-400-normal.woff";
import comicNeueLatin400NormalWoff2 from "@fontsource/comic-neue/files/comic-neue-latin-400-normal.woff2";
import comicNeueLatin700NormalWoff from "@fontsource/comic-neue/files/comic-neue-latin-700-normal.woff";
import comicNeueLatin700NormalWoff2 from "@fontsource/comic-neue/files/comic-neue-latin-700-normal.woff2";
export const ComicNeue = [
{
fontFamily: "Comic Neue",
fontStyle: "normal",
fontDisplay: "swap",
fontWeight: 400,
src: `url(${comicNeueLatin400NormalWoff2}) format("woff2"), url(${comicNeueLatin400NormalWoff}) format("woff")`,
},
{
fontFamily: "Comic Neue",
fontStyle: "normal",
fontDisplay: "swap",
fontWeight: 700,
src: `url(${comicNeueLatin700NormalWoff2}) format("woff2"), url(${comicNeueLatin700NormalWoff}) format("woff")`,
},
];

57
lib/styles/fonts/inter.ts Normal file
View File

@@ -0,0 +1,57 @@
// @ts-nocheck
// Legacy
import interLatin400NormalWoff from "@fontsource/inter/files/inter-latin-400-normal.woff";
import interLatin400NormalWoff2 from "@fontsource/inter/files/inter-latin-400-normal.woff2";
import interLatin500NormalWoff from "@fontsource/inter/files/inter-latin-500-normal.woff";
import interLatin500NormalWoff2 from "@fontsource/inter/files/inter-latin-500-normal.woff2";
import interLatin700NormalWoff from "@fontsource/inter/files/inter-latin-700-normal.woff";
import interLatin700NormalWoff2 from "@fontsource/inter/files/inter-latin-700-normal.woff2";
// Variable
import interLatinVarFullNormalWoff2 from "@fontsource/inter/files/inter-latin-variable-full-normal.woff2";
import interLatinExtVarFullNormalWoff2 from "@fontsource/inter/files/inter-latin-ext-variable-full-normal.woff2";
// re-export hashed URL of the most prominent font so we can preload it
export { interLatinVarFullNormalWoff2 as preloadUrl };
export const Inter = [
{
fontFamily: "Inter",
fontStyle: "normal",
fontDisplay: "swap",
fontWeight: 400,
src: `url(${interLatin400NormalWoff2}) format("woff2"), url(${interLatin400NormalWoff}) format("woff")`,
},
{
fontFamily: "Inter",
fontStyle: "normal",
fontDisplay: "swap",
fontWeight: 500,
src: `url(${interLatin500NormalWoff2}) format("woff2"), url(${interLatin500NormalWoff}) format("woff")`,
},
{
fontFamily: "Inter",
fontStyle: "normal",
fontDisplay: "swap",
fontWeight: 700,
src: `url(${interLatin700NormalWoff2}) format("woff2"), url(${interLatin700NormalWoff}) format("woff")`,
},
{
fontFamily: "Inter var",
fontStyle: "oblique 0deg 10deg",
fontDisplay: "swap",
fontWeight: "100 900",
src: `url(${interLatinVarFullNormalWoff2}) format("woff2")`,
unicodeRange:
"U+00??,U+0131,U+0152-0153,U+02bb-02bc,U+02c6,U+02da,U+02dc,U+2000-206f,U+2074,U+20ac,U+2122,U+2191,U+2193,U+2212,U+2215,U+feff,U+fffd",
},
{
fontFamily: "Inter var",
fontStyle: "oblique 0deg 10deg",
fontDisplay: "swap",
fontWeight: "100 900",
src: `url(${interLatinExtVarFullNormalWoff2}) format("woff2")`,
unicodeRange: "U+0100-024f,U+0259,U+1e??,U+2020,U+20a0-20ab,U+20ad-20cf,U+2113,U+2c60-2c7f,U+a720-a7ff",
},
];

View File

@@ -0,0 +1,76 @@
// @ts-nocheck
// Legacy
import robotoMonoLatin400NormalWoff from "@fontsource/roboto-mono/files/roboto-mono-latin-400-normal.woff";
import robotoMonoLatin400NormalWoff2 from "@fontsource/roboto-mono/files/roboto-mono-latin-400-normal.woff2";
import robotoMonoLatin500NormalWoff from "@fontsource/roboto-mono/files/roboto-mono-latin-500-normal.woff";
import robotoMonoLatin500NormalWoff2 from "@fontsource/roboto-mono/files/roboto-mono-latin-500-normal.woff2";
import robotoMonoLatin700NormalWoff from "@fontsource/roboto-mono/files/roboto-mono-latin-700-normal.woff";
import robotoMonoLatin700NormalWoff2 from "@fontsource/roboto-mono/files/roboto-mono-latin-700-normal.woff2";
// Variable
import robotoMonoLatinVarWghtOnlyNormalWoff2 from "@fontsource/roboto-mono/files/roboto-mono-latin-variable-wghtOnly-normal.woff2";
import robotoMonoLatinExtVarWghtOnlyNormalWoff2 from "@fontsource/roboto-mono/files/roboto-mono-latin-ext-variable-wghtOnly-normal.woff2";
import robotoMonoLatinVarWghtOnlyItalicWoff2 from "@fontsource/roboto-mono/files/roboto-mono-latin-variable-wghtOnly-italic.woff2";
import robotoMonoLatinExtVarWghtOnlyItalicWoff2 from "@fontsource/roboto-mono/files/roboto-mono-latin-ext-variable-wghtOnly-italic.woff2";
// re-export hashed URL of the most prominent font so we can preload it
export { robotoMonoLatinVarWghtOnlyNormalWoff2 as preloadUrl };
export const RobotoMono = [
{
fontFamily: "Roboto Mono",
fontStyle: "normal",
fontDisplay: "swap",
fontWeight: 400,
src: `url(${robotoMonoLatin400NormalWoff2}) format("woff2"), url(${robotoMonoLatin400NormalWoff}) format("woff")`,
},
{
fontFamily: "Roboto Mono",
fontStyle: "normal",
fontDisplay: "swap",
fontWeight: 500,
src: `url(${robotoMonoLatin500NormalWoff2}) format("woff2"), url(${robotoMonoLatin500NormalWoff}) format("woff")`,
},
{
fontFamily: "Roboto Mono",
fontStyle: "normal",
fontDisplay: "swap",
fontWeight: 700,
src: `url(${robotoMonoLatin700NormalWoff2}) format("woff2"), url(${robotoMonoLatin700NormalWoff}) format("woff")`,
},
{
fontFamily: "Roboto Mono var",
fontStyle: "normal",
fontDisplay: "swap",
fontWeight: "100 700",
src: `url(${robotoMonoLatinVarWghtOnlyNormalWoff2}) format("woff2")`,
unicodeRange:
"U+00??,U+0131,U+0152-0153,U+02bb-02bc,U+02c6,U+02da,U+02dc,U+2000-206f,U+2074,U+20ac,U+2122,U+2191,U+2193,U+2212,U+2215,U+feff,U+fffd",
},
{
fontFamily: "Roboto Mono var",
fontStyle: "normal",
fontDisplay: "swap",
fontWeight: "100 700",
src: `url(${robotoMonoLatinExtVarWghtOnlyNormalWoff2}) format("woff2")`,
unicodeRange: "U+0100-024f,U+0259,U+1e??,U+2020,U+20a0-20ab,U+20ad-20cf,U+2113,U+2c60-2c7f,U+a720-a7ff",
},
{
fontFamily: "Roboto Mono var",
fontStyle: "italic",
fontDisplay: "swap",
fontWeight: "100 700",
src: `url(${robotoMonoLatinVarWghtOnlyItalicWoff2}) format("woff2")`,
unicodeRange:
"U+00??,U+0131,U+0152-0153,U+02bb-02bc,U+02c6,U+02da,U+02dc,U+2000-206f,U+2074,U+20ac,U+2122,U+2191,U+2193,U+2212,U+2215,U+feff,U+fffd",
},
{
fontFamily: "Roboto Mono var",
fontStyle: "italic",
fontDisplay: "swap",
fontWeight: "100 700",
src: `url(${robotoMonoLatinExtVarWghtOnlyItalicWoff2}) format("woff2")`,
unicodeRange: "U+0100-024f,U+0259,U+1e??,U+2020,U+20a0-20ab,U+20ad-20cf,U+2113,U+2c60-2c7f,U+a720-a7ff",
},
];

View File

@@ -0,0 +1,11 @@
// hex -> rgba, pulled from https://github.com/sindresorhus/hex-rgb/blob/main/index.js#L29
const hex2rgba = (hex: string, alpha: number) => {
const number = Number.parseInt(hex.replace(/^#/, ""), 16);
const red = number >> 16;
const green = (number >> 8) & 255;
const blue = number & 255;
return `rgba(${red},${green},${blue},${alpha})`;
};
export default hex2rgba;

View File

@@ -0,0 +1,87 @@
// @sindresorhus's modern-normalize.css converted to a JS object, with a bit of cruft removed:
// https://github.com/sindresorhus/modern-normalize/blob/b59ec0d3d8654cbb6843bc9ea45aef5f1d680108/modern-normalize.css
export const normalizeCss = {
"*, ::before, ::after": {
boxSizing: "border-box",
},
html: {
lineHeight: 1.15,
tabSize: 4,
WebkitTextSizeAdjust: "100%",
},
hr: {
height: 0,
color: "inherit",
},
"abbr[title]": {
textDecoration: "underline dotted",
},
"b, strong": {
fontWeight: "bolder",
},
small: {
fontSize: "80%",
},
"sub, sup": {
fontSize: "75%",
lineHeight: 0,
position: "relative",
verticalAlign: "baseline",
},
sub: {
bottom: "-0.25em",
},
sup: {
top: "-0.5em",
},
table: {
textIndent: 0,
borderColor: "inherit",
},
"button, input, optgroup, select, textarea": {
fontFamily: "inherit",
fontSize: "100%",
lineHeight: 1.15,
margin: 0,
},
"button, select": {
textTransform: "none",
},
"button, [type='button'], [type='reset'], [type='submit']": {
WebkitAppearance: "button",
},
"::-moz-focus-inner": {
borderStyle: "none",
padding: 0,
},
":-moz-focusring": {
outline: "1px dotted ButtonText",
},
":-moz-ui-invalid": {
boxShadow: "none",
},
legend: {
padding: 0,
},
progress: {
verticalAlign: "baseline",
},
"::-webkit-inner-spin-button, ::-webkit-outer-spin-button": {
height: "auto",
},
"[type='search']": {
WebkitAppearance: "textfield",
outlineOffset: "-2px",
},
"::-webkit-search-decoration": {
WebkitAppearance: "none",
},
"::-webkit-file-upload-button": {
WebkitAppearance: "button",
font: "inherit",
},
summary: {
display: "list-item",
},
};

View File

@@ -0,0 +1,184 @@
import { createStitches, defaultThemeMap } from "@stitches/react";
import hex2rgba from "./helpers/hex-to-rgba";
// modified modern-normalize.css in object form
import { normalizeCss } from "./helpers/normalize";
// web fonts
import { Inter, preloadUrl as interPreloadUrl } from "./fonts/inter";
import { RobotoMono, preloadUrl as robotoMonoPreloadUrl } from "./fonts/roboto-mono";
import { ComicNeue } from "./fonts/comic-neue";
export const { styled, css, getCssText, globalCss, keyframes, theme, createTheme } = createStitches({
theme: {
fonts: {
sans: `Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif`,
sansVar: `"Inter var", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif`,
mono: `"Roboto Mono", ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier`,
monoVar: `"Roboto Mono var", ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier`,
},
colors: {
backgroundInner: "#ffffff",
backgroundOuter: "#fcfcfc",
backgroundHeader: "rgba(252, 252, 252, 0.7)",
text: "#202020",
mediumDark: "#515151",
medium: "#5e5e5e",
mediumLight: "#757575",
light: "#d2d2d2",
kindaLight: "#e3e3e3",
superLight: "#f4f4f4",
superDuperLight: "#fbfbfb",
link: "#0e6dc2",
linkUnderline: "rgba(14, 109, 194, 0.4)",
success: "#44a248",
error: "#ff1b1b",
warning: "#f78200",
// Syntax Highlighting (light) - modified from Monokai Light: https://github.com/mlgill/pygments-style-monokailight
codeText: "#313131",
codeBackground: "#fdfdfd",
codeComment: "#656e77",
codeKeyword: "#029cb9",
codeAttribute: "#70a800",
codeNamespace: "#f92672",
codeLiteral: "#ae81ff",
codePunctuation: "#111111",
codeVariable: "#d88200",
codeAddition: "#44a248",
codeDeletion: "#ff1b1b",
},
borderWidths: {
underline: "calc(0.1em + 0.05rem)",
},
radii: {
rounded: "0.65em",
},
},
media: {
mobile: "(max-width: 768px)",
superNarrow: "(max-width: 380px)",
},
utils: {
backgroundGradientHack: ({ color = "$linkUnderline" }) => {
// allow both pre-set rgba stitches variables and hex values
const rgba = color.startsWith("#") ? hex2rgba(color, 0.4) : color;
return {
backgroundImage: `linear-gradient(${rgba}, ${rgba})`,
};
},
},
themeMap: {
...defaultThemeMap,
backgroundSize: "borderWidths",
},
});
export const darkTheme = createTheme({
colors: {
backgroundInner: "#1e1e1e",
backgroundOuter: "#252525",
backgroundHeader: "rgba(37, 37, 37, 0.85)",
text: "#f1f1f1",
mediumDark: "#d7d7d7",
medium: "#b1b1b1",
mediumLight: "#959595",
light: "#646464",
kindaLight: "#535353",
superLight: "#272727",
superDuperLight: "#1f1f1f",
link: "#88c7ff",
linkUnderline: "rgba(136, 199, 255, 0.4)",
success: "#78df55",
error: "#ff5151",
warning: "#f2b702",
// Syntax Highlighting (dark) - modified from Dracula: https://github.com/dracula/pygments
codeText: "#e4e4e4",
codeBackground: "#212121",
codeComment: "#929292",
codeKeyword: "#3b9dd2",
codeAttribute: "#78df55",
codeNamespace: "#f95757",
codeLiteral: "#d588fb",
codePunctuation: "#cccccc",
codeVariable: "#fd992a",
codeAddition: "#78df55",
codeDeletion: "#ff5151",
},
});
export const globalStyles = globalCss({
// https://github.com/sindresorhus/modern-normalize
...normalizeCss,
// @ts-ignore
"@font-face": [...Inter, ...RobotoMono, ...ComicNeue],
body: {
margin: 0,
backgroundColor: "$backgroundInner",
fontFamily: "$sans",
// light-dark theme switch fading
transition: "background 0.25s ease",
},
"code, kbd, samp, pre": {
fontFamily: "$mono",
},
// variable font support
"@supports (font-variation-settings: normal)": {
body: {
fontFamily: "$sansVar",
fontOpticalSizing: "auto",
},
"code, kbd, samp, pre": {
fontFamily: "$monoVar",
},
// Chrome doesn't automatically slant multi-axis Inter var, for some reason.
// Adding "slnt" -10 fixes Chrome but then over-italicizes in Firefox. AHHHHHHHHHH.
em: {
fontStyle: "normal",
fontVariationSettings: `"ital" 1, "slnt" -10`,
// Roboto Mono doesn't have this problem, but the above fix breaks it, of course.
"& code, & kbd, & samp, & pre": {
fontStyle: "italic !important",
fontVariationSettings: "initial !important",
},
},
},
// reduced motion preference:
// https://web.dev/prefers-reduced-motion/#(bonus)-forcing-reduced-motion-on-all-websites
"@media (prefers-reduced-motion: reduce)": {
"*, ::before, ::after": {
animationDelay: "-1ms !important",
animationDuration: "1ms !important",
animationIterationCount: "1 !important",
backgroundAttachment: "initial !important",
scrollBehavior: "auto !important",
transitionDuration: "0s !important",
transitionDelay: "0s !important",
},
},
});
// re-export hashed URLs of the most important variable fonts so we can preload them in ../../pages/_document.tsx
export const preloads = {
fonts: {
InterVar: interPreloadUrl,
RobotoMonoVar: robotoMonoPreloadUrl,
},
};

View File

@@ -30,14 +30,23 @@ module.exports = (phase, { defaultConfig }) => {
formats: ["image/avif", "image/webp"], formats: ["image/avif", "image/webp"],
minimumCacheTTL: 43200, minimumCacheTTL: 43200,
}, },
experimental: {
// use critters to automatically inline critical css:
optimizeCss: true,
},
webpack: (config) => { webpack: (config) => {
// this lets us statically import webfonts like we would images, allowing cool things like preloading them
config.module.rules.push({ config.module.rules.push({
test: /\.svg$/, test: /\.(woff|woff2|eot|ttf|otf)$/i,
issuer: { and: [/\.(js|ts)x?$/] }, issuer: { and: [/\.(js|ts|md)x?$/] },
type: "asset/resource",
generator: {
filename: "static/media/[name].[hash:8][ext]",
},
});
// allow processing SVGs from the below packages directly instead of through their different exports, and leave
// other static imports of SVGs alone.
// see: ./components/Icons/index.ts
config.module.rules.push({
test: /\.svg$/i,
issuer: { and: [/\.(js|ts|md)x?$/] },
use: [ use: [
{ {
loader: "@svgr/webpack", loader: "@svgr/webpack",
@@ -51,9 +60,6 @@ module.exports = (phase, { defaultConfig }) => {
}, },
], ],
include: [ include: [
// allow processing images from these packages directly instead of through their different exports, and leave
// other static imports of SVGs alone.
// see: ./components/Icons/index.ts
path.resolve(__dirname, "node_modules/@primer/octicons/build/svg"), path.resolve(__dirname, "node_modules/@primer/octicons/build/svg"),
path.resolve(__dirname, "node_modules/feather-icons/dist/icons"), path.resolve(__dirname, "node_modules/feather-icons/dist/icons"),
path.resolve(__dirname, "node_modules/simple-icons/icons"), path.resolve(__dirname, "node_modules/simple-icons/icons"),

View File

@@ -17,10 +17,7 @@
"dev": "cross-env NODE_OPTIONS='--inspect' next dev", "dev": "cross-env NODE_OPTIONS='--inspect' next dev",
"build": "next build", "build": "next build",
"analyze": "cross-env ANALYZE=true next build", "analyze": "cross-env ANALYZE=true next build",
"lint": "run-s lint:*", "lint": "eslint . && prettier --check ."
"lint:js": "eslint .",
"lint:css": "stylelint '**/*.css'",
"lint:prettier": "prettier --check ."
}, },
"dependencies": { "dependencies": {
"@fontsource/comic-neue": "4.5.3", "@fontsource/comic-neue": "4.5.3",
@@ -28,17 +25,15 @@
"@fontsource/roboto-mono": "4.5.3", "@fontsource/roboto-mono": "4.5.3",
"@giscus/react": "^1.1.2", "@giscus/react": "^1.1.2",
"@hcaptcha/react-hcaptcha": "^1.1.1", "@hcaptcha/react-hcaptcha": "^1.1.1",
"@next/bundle-analyzer": "12.1.1-canary.5", "@next/bundle-analyzer": "12.1.1-canary.6",
"@novnc/novnc": "github:novnc/noVNC#679b45fa3b453c7cf32f4b4455f4814818ecf161", "@novnc/novnc": "github:novnc/noVNC#679b45fa3b453c7cf32f4b4455f4814818ecf161",
"@octokit/graphql": "^4.8.0", "@octokit/graphql": "^4.8.0",
"@primer/octicons": "^16.3.1", "@primer/octicons": "^16.3.1",
"@sentry/node": "^6.18.1", "@sentry/node": "^6.18.1",
"@sentry/tracing": "^6.18.1", "@sentry/tracing": "^6.18.1",
"classnames": "^2.3.1", "@stitches/react": "^1.2.7",
"copy-to-clipboard": "^3.3.1", "copy-to-clipboard": "^3.3.1",
"critters": "^0.0.16",
"date-fns": "^2.28.0", "date-fns": "^2.28.0",
"decamelize": "^6.0.0",
"escape-goat": "^4.0.0", "escape-goat": "^4.0.0",
"fathom-client": "^3.4.1", "fathom-client": "^3.4.1",
"faunadb": "^4.5.2", "faunadb": "^4.5.2",
@@ -48,8 +43,7 @@
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"is-absolute-url": "^4.0.1", "is-absolute-url": "^4.0.1",
"markdown-to-jsx": "^7.1.6", "markdown-to-jsx": "^7.1.6",
"modern-normalize": "github:sindresorhus/modern-normalize#b59ec0d3d8654cbb6843bc9ea45aef5f1d680108", "next": "12.1.1-canary.6",
"next": "12.1.1-canary.5",
"next-compose-plugins": "^2.2.1", "next-compose-plugins": "^2.2.1",
"next-mdx-remote": "^4.0.0", "next-mdx-remote": "^4.0.0",
"next-seo": "^5.1.0", "next-seo": "^5.1.0",
@@ -86,7 +80,7 @@
"@types/node": "*", "@types/node": "*",
"@types/prop-types": "^15.7.4", "@types/prop-types": "^15.7.4",
"@types/react": "^17.0.39", "@types/react": "^17.0.39",
"@types/react-dom": "^17.0.12", "@types/react-dom": "^17.0.13",
"@types/react-is": "^17.0.3", "@types/react-is": "^17.0.3",
"@types/remove-markdown": "^0.3.1", "@types/remove-markdown": "^0.3.1",
"@types/sanitize-html": "^2.6.2", "@types/sanitize-html": "^2.6.2",
@@ -94,27 +88,19 @@
"@typescript-eslint/parser": "^5.13.0", "@typescript-eslint/parser": "^5.13.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"eslint": "~8.10.0", "eslint": "~8.10.0",
"eslint-config-next": "12.1.1-canary.5", "eslint-config-next": "12.1.1-canary.6",
"eslint-config-prettier": "~8.4.0", "eslint-config-prettier": "~8.5.0",
"eslint-plugin-mdx": "~1.16.0", "eslint-plugin-mdx": "~1.16.0",
"eslint-plugin-prettier": "~4.0.0", "eslint-plugin-prettier": "~4.0.0",
"lint-staged": "^12.3.4", "lint-staged": "^12.3.4",
"npm-run-all": "^4.1.5",
"prettier": "^2.5.1", "prettier": "^2.5.1",
"simple-git-hooks": "^2.7.0", "simple-git-hooks": "^2.7.0",
"stylelint": "~14.5.3", "typescript": "~4.5.5"
"stylelint-config-prettier": "~9.0.3",
"stylelint-prettier": "~2.0.0",
"typescript": "^4.6.2"
}, },
"simple-git-hooks": { "simple-git-hooks": {
"pre-commit": "npx lint-staged" "pre-commit": "npx lint-staged"
}, },
"lint-staged": { "lint-staged": {
"*.css": [
"stylelint",
"prettier --check"
],
"*.{js,jsx,ts,tsx,md,mdx}": [ "*.{js,jsx,ts,tsx,md,mdx}": [
"eslint", "eslint",
"prettier --check" "prettier --check"

View File

@@ -1,33 +1,16 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import Head from "next/head";
import { ThemeProvider } from "next-themes"; import { ThemeProvider } from "next-themes";
import { DefaultSeo, SocialProfileJsonLd } from "next-seo"; import { DefaultSeo, SocialProfileJsonLd } from "next-seo";
import * as Fathom from "fathom-client"; import * as Fathom from "fathom-client";
import Layout from "../components/Layout/Layout"; import Layout from "../components/Layout/Layout";
import { globalStyles, theme, darkTheme } from "../lib/styles/stitches.config";
import * as config from "../lib/config"; import * as config from "../lib/config";
import { defaultSeo, socialProfileJsonLd } from "../lib/config/seo"; import { defaultSeo, socialProfileJsonLd } from "../lib/config/seo";
import type { ReactElement, ReactNode } from "react"; import type { ReactElement, ReactNode } from "react";
import type { NextPage } from "next"; import type { NextPage } from "next";
import type { AppProps as NextAppProps } from "next/app"; import type { AppProps as NextAppProps } from "next/app";
// global webfonts -- imported here so they're processed through PostCSS
import "@fontsource/inter/latin-400.css";
import "@fontsource/inter/latin-500.css";
import "@fontsource/inter/latin-700.css";
import "@fontsource/inter/variable-full.css";
import "@fontsource/roboto-mono/latin-400.css";
import "@fontsource/roboto-mono/latin-500.css";
import "@fontsource/roboto-mono/latin-700.css";
import "@fontsource/roboto-mono/variable.css";
import "@fontsource/roboto-mono/variable-italic.css";
// global styles
import "modern-normalize/modern-normalize.css";
import "../styles/settings.css";
import "../styles/typography.css";
import "../styles/index.css";
// https://nextjs.org/docs/basic-features/layouts#with-typescript // https://nextjs.org/docs/basic-features/layouts#with-typescript
export type AppProps = NextAppProps & { export type AppProps = NextAppProps & {
Component: NextPage & { Component: NextPage & {
@@ -68,28 +51,12 @@ const App = ({ Component, pageProps }: AppProps) => {
// allow layout overrides per-page, but default to plain `<Layout />` // allow layout overrides per-page, but default to plain `<Layout />`
const getLayout = Component.getLayout || ((page) => <Layout>{page}</Layout>); const getLayout = Component.getLayout || ((page) => <Layout>{page}</Layout>);
// inject body styles defined in ../lib/styles/stitches.config.ts
globalStyles();
return ( return (
<> <>
{/* static asset preloads */} {/* all SEO config is in ../lib/config/seo.ts except for canonical URLs, which require access to next router */}
<Head>
{/* TODO: these hrefs will change (unpredictably?) at some point. find a safer way to get them from webpack. */}
<link
rel="preload"
as="font"
type="font/woff2"
href="/_next/static/media/inter-latin-variable-full-normal.79d31200.woff2"
crossOrigin="anonymous"
/>
<link
rel="preload"
as="font"
type="font/woff2"
href="/_next/static/media/roboto-mono-latin-variable-wghtOnly-normal.3689861c.woff2"
crossOrigin="anonymous"
/>
</Head>
{/* all SEO config is in ../lib/seo.ts except for canonical URLs, which require access to next router */}
<DefaultSeo <DefaultSeo
{...defaultSeo} {...defaultSeo}
canonical={canonical} canonical={canonical}
@@ -103,8 +70,16 @@ const App = ({ Component, pageProps }: AppProps) => {
/> />
<SocialProfileJsonLd {...socialProfileJsonLd} /> <SocialProfileJsonLd {...socialProfileJsonLd} />
{/* NOTE: this *must* come last in this fragment */} <ThemeProvider
<ThemeProvider>{getLayout(<Component {...pageProps} />)}</ThemeProvider> // theme classnames are generated dynamically by stitches, so have ThemeProvider pull them from there
attribute="class"
value={{
light: theme.className,
dark: darkTheme.className,
}}
>
{getLayout(<Component {...pageProps} />)}
</ThemeProvider>
</> </>
); );
}; };

View File

@@ -1,5 +1,5 @@
import { Html, Head, Main, NextScript } from "next/document"; import { Html, Head, Main, NextScript } from "next/document";
import themes, { toCSS } from "../lib/config/themes"; import { getCssText, preloads } from "../lib/styles/stitches.config";
import * as config from "../lib/config"; import * as config from "../lib/config";
// https://nextjs.org/docs/advanced-features/custom-document // https://nextjs.org/docs/advanced-features/custom-document
@@ -7,13 +7,12 @@ const Document = () => {
return ( return (
<Html lang={config.siteLocale?.replace("_", "-")}> <Html lang={config.siteLocale?.replace("_", "-")}>
<Head> <Head>
{/* convert themes object into inlined css variables */} {/* static asset preloads */}
<style <link rel="preload" as="font" type="font/woff2" href={preloads.fonts.InterVar} crossOrigin="anonymous" />
id="theme-colors" <link rel="preload" as="font" type="font/woff2" href={preloads.fonts.RobotoMonoVar} crossOrigin="anonymous" />
dangerouslySetInnerHTML={{
__html: `:root{${toCSS(themes.light)}}[data-theme="dark"]{${toCSS(themes.dark)}}`, {/* stitches SSR: https://stitches.dev/blog/using-nextjs-with-stitches#step-3-ssr */}
}} <style id="stitches" dangerouslySetInnerHTML={{ __html: getCssText() }} />
/>
</Head> </Head>
<body> <body>
<Main /> <Main />

View File

@@ -3,6 +3,18 @@ import Content from "../components/Content/Content";
import PageTitle from "../components/PageTitle/PageTitle"; import PageTitle from "../components/PageTitle/PageTitle";
import Link from "../components/Link/Link"; import Link from "../components/Link/Link";
import ContactForm from "../components/ContactForm/ContactForm"; import ContactForm from "../components/ContactForm/ContactForm";
import { styled } from "../lib/styles/stitches.config";
const Wrapper = styled(Content, {
maxWidth: "600px",
margin: "0 auto",
});
const PubKey = styled("code", {
fontSize: "0.925em",
wordSpacing: "-0.175em",
whiteSpace: "normal",
});
const Contact = () => ( const Contact = () => (
<> <>
@@ -15,38 +27,23 @@ const Contact = () => (
<PageTitle>📬 Contact Me</PageTitle> <PageTitle>📬 Contact Me</PageTitle>
<Content> <Wrapper>
<div className="wrapper"> <p>
<p> Fill out this quick form and I'll get back to you as soon as I can! You can also{" "}
Fill out this quick form and I'll get back to you as soon as I can! You can also{" "} <Link href="mailto:jake@jarv.is">email me directly</Link>, send me a{" "}
<Link href="mailto:jake@jarv.is">email me directly</Link>, send me a{" "} <Link href="https://twitter.com/messages/compose?recipient_id=229769022">direct message on Twitter</Link>, or{" "}
<Link href="https://twitter.com/messages/compose?recipient_id=229769022">direct message on Twitter</Link>, or{" "} <Link href="sms:+1-617-917-3737">text me</Link>.
<Link href="sms:+1-617-917-3737">text me</Link>. </p>
</p> <p>
<p> 🔐 You can grab my public key here:{" "}
🔐 You can grab my public key here:{" "} <Link href="/pubkey.asc" title="My Public PGP Key" rel="pgpkey authn noopener" forceNewWindow>
<Link href="/pubkey.asc" title="My Public PGP Key" rel="pgpkey authn noopener" forceNewWindow> <PubKey>6BF3 79D3 6F67 1480 2B0C 9CF2 51E6 9A39</PubKey>
<code className="pubkey">6BF3 79D3 6F67 1480 2B0C 9CF2 51E6 9A39</code> </Link>
</Link> .
. </p>
</p>
<ContactForm /> <ContactForm />
</div> </Wrapper>
</Content>
<style jsx>{`
.wrapper {
max-width: 600px;
margin: 0 auto;
}
.pubkey {
font-size: 0.925em;
word-spacing: -0.175em;
white-space: normal;
}
`}</style>
</> </>
); );

View File

@@ -3,9 +3,18 @@ import Content from "../components/Content/Content";
import PageTitle from "../components/PageTitle/PageTitle"; import PageTitle from "../components/PageTitle/PageTitle";
import Link from "../components/Link/Link"; import Link from "../components/Link/Link";
import Video from "../components/Video/Video"; import Video from "../components/Video/Video";
import { styled } from "../lib/styles/stitches.config";
import thumbnail from "../public/static/images/hillary/thumb.png"; import thumbnail from "../public/static/images/hillary/thumb.png";
const Copyright = styled("p", {
textAlign: "center",
fontSize: "0.9em",
lineHeight: 1.8,
margin: "1.25em 1em 0 1em",
color: "$mediumLight",
});
const Hillary = () => ( const Hillary = () => (
<> <>
<NextSeo <NextSeo
@@ -27,7 +36,7 @@ const Hillary = () => (
subs="/static/images/hillary/subs.en.vtt" subs="/static/images/hillary/subs.en.vtt"
/> />
<p className="copyright"> <Copyright>
Video is property of{" "} Video is property of{" "}
<Link href="https://www.hillaryclinton.com/" style={{ fontWeight: 700 }}> <Link href="https://www.hillaryclinton.com/" style={{ fontWeight: 700 }}>
Hillary for America Hillary for America
@@ -41,18 +50,8 @@ const Hillary = () => (
CNN / WarnerMedia CNN / WarnerMedia
</Link> </Link>
. &copy; 2016. . &copy; 2016.
</p> </Copyright>
</Content> </Content>
<style jsx>{`
.copyright {
text-align: center;
font-size: 0.9em;
line-height: 1.8;
margin: 1.25em 1em 0 1em;
color: var(--medium-light);
}
`}</style>
</> </>
); );

View File

@@ -1,366 +1,362 @@
import Content from "../components/Content/Content"; import Content from "../components/Content/Content";
import ColorfulLink from "../components/ColorfulLink/ColorfulLink"; import Link, { CustomLinkProps } from "../components/Link/Link";
import { styled, keyframes, darkTheme } from "../lib/styles/stitches.config";
const Wrapper = styled(Content, {
fontSize: "1em",
lineHeight: 1,
});
const ColorfulLink = ({
lightColor,
darkColor,
...rest
}: CustomLinkProps & {
lightColor: string;
darkColor: string;
}) => {
return (
<>
<Link
css={{
color: lightColor,
backgroundGradientHack: { color: lightColor },
[`.${darkTheme} &`]: {
color: darkColor,
backgroundGradientHack: { color: darkColor },
},
}}
{...rest}
/>
</>
);
};
const H1 = styled("h1", {
margin: "0 0 0.5em -0.03em",
fontSize: "1.8em",
fontWeight: 500,
letterSpacing: "-0.01em",
"@mobile": {
fontSize: "1.5em",
},
});
const H2 = styled("h2", {
margin: "0.5em 0 0.5em -0.03em",
fontSize: "1.35em",
fontWeight: 400,
letterSpacing: "-0.016em",
lineHeight: 1.4,
"@mobile": {
fontSize: "1.2em",
},
});
const Paragraph = styled("p", {
margin: "0.85em 0",
letterSpacing: "-0.004em",
lineHeight: 1.7,
"&:last-of-type": {
marginBottom: 0,
},
"@mobile": {
fontSize: "0.925em",
},
});
const hello = keyframes({
"0%": { transform: "rotate(0deg)" },
"5%": { transform: "rotate(14deg)" },
"10%": { transform: "rotate(-8deg)" },
"15%": { transform: "rotate(14deg)" },
"20%": { transform: "rotate(-4deg)" },
"25%": { transform: "rotate(10deg)" },
"30%": { transform: "rotate(0deg)" },
// pause for ~9 out of 10 seconds
"100%": { transform: "rotate(0deg)" },
});
const Wave = styled("span", {
display: "inline-block",
marginLeft: "0.1em",
fontSize: "1.2em",
animation: `${hello} 5s infinite`,
animationDelay: "1s",
transformOrigin: "65% 80%",
willChange: "transform",
});
const PGPKey = styled("sup", {
margin: "0 0.15em",
fontSize: "0.65em",
wordSpacing: "-0.3em",
});
const Quiet = styled("span", {
color: "$mediumLight",
});
const EasterEgg = styled(ColorfulLink, {
cursor: `url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='24' height='30' style='font-size:24px'><text y='50%' transform='rotate(-70 0 0) translate(-20, 6)'>🪄</text></svg>") 5 5, auto`,
});
const Index = () => ( const Index = () => (
<> <Wrapper>
<Content> <H1>
<div> Hi there! I'm Jake. <Wave>👋</Wave>
<h1> </H1>
Hi there! I'm Jake. <span className="wave">👋</span>
</h1>
<h2> <H2>
I'm a frontend web developer based in{" "} I'm a frontend web developer based in{" "}
<ColorfulLink <ColorfulLink
href="https://www.youtube-nocookie.com/embed/rLwbzGyC6t4?hl=en&amp;fs=1&amp;showinfo=1&amp;rel=0&amp;iv_load_policy=3" href="https://www.youtube-nocookie.com/embed/rLwbzGyC6t4?hl=en&amp;fs=1&amp;showinfo=1&amp;rel=0&amp;iv_load_policy=3"
title='"Boston Accent Trailer - Late Night with Seth Meyers" on YouTube' title='"Boston Accent Trailer - Late Night with Seth Meyers" on YouTube'
lightColor="#fb4d42" lightColor="#fb4d42"
darkColor="#ff5146" darkColor="#ff5146"
> >
Boston Boston
</ColorfulLink> </ColorfulLink>
. .
</h2> </H2>
<p> <Paragraph>
I specialize in{" "} I specialize in{" "}
<ColorfulLink <ColorfulLink
href="https://stackoverflow.blog/2018/01/11/brutal-lifecycle-javascript-frameworks/" href="https://stackoverflow.blog/2018/01/11/brutal-lifecycle-javascript-frameworks/"
title='"The Brutal Lifecycle of JavaScript Frameworks" by Ian Allen' title='"The Brutal Lifecycle of JavaScript Frameworks" by Ian Allen'
lightColor="#1091b3" lightColor="#1091b3"
darkColor="#6fcbe3" darkColor="#6fcbe3"
> >
modern JS frameworks modern JS frameworks
</ColorfulLink>{" "} </ColorfulLink>{" "}
and{" "} and{" "}
<ColorfulLink <ColorfulLink
href="http://vanilla-js.com/" href="http://vanilla-js.com/"
title="The best JS framework in the world by Eric Wastl" title="The best JS framework in the world by Eric Wastl"
lightColor="#f48024" lightColor="#f48024"
darkColor="#e18431" darkColor="#e18431"
> >
vanilla JavaScript vanilla JavaScript
</ColorfulLink>{" "} </ColorfulLink>{" "}
to make nifty{" "} to make nifty{" "}
<ColorfulLink href="https://jamstack.wtf/" title="WTF is JAMstack?" lightColor="#04a699" darkColor="#08bbac"> <ColorfulLink href="https://jamstack.wtf/" title="WTF is JAMstack?" lightColor="#04a699" darkColor="#08bbac">
JAMstack sites JAMstack sites
</ColorfulLink>{" "} </ColorfulLink>{" "}
with dynamic{" "} with dynamic{" "}
<ColorfulLink <ColorfulLink
href="https://nodejs.org/en/" href="https://nodejs.org/en/"
title="Node.js Official Website" title="Node.js Official Website"
lightColor="#6fbc4e" lightColor="#6fbc4e"
darkColor="#84d95f" darkColor="#84d95f"
> >
Node.js Node.js
</ColorfulLink>{" "} </ColorfulLink>{" "}
services. But I'm fluent in non-buzzwords like{" "} services. But I'm fluent in non-buzzwords like{" "}
<ColorfulLink <ColorfulLink
href="https://stitcher.io/blog/php-in-2020" href="https://stitcher.io/blog/php-in-2020"
title='"PHP in 2020" by Brent Roose' title='"PHP in 2020" by Brent Roose'
lightColor="#8892bf" lightColor="#8892bf"
darkColor="#a4afe3" darkColor="#a4afe3"
> >
PHP PHP
</ColorfulLink> </ColorfulLink>
,{" "} ,{" "}
<ColorfulLink <ColorfulLink
href="https://www.ruby-lang.org/en/" href="https://www.ruby-lang.org/en/"
title="Ruby Official Website" title="Ruby Official Website"
lightColor="#d34135" lightColor="#d34135"
darkColor="#f95a4d" darkColor="#f95a4d"
> >
Ruby Ruby
</ColorfulLink> </ColorfulLink>
, and{" "} , and{" "}
<ColorfulLink <ColorfulLink href="https://golang.org/" title="Golang Official Website" lightColor="#00acd7" darkColor="#2ad1fb">
href="https://golang.org/" Go
title="Golang Official Website" </ColorfulLink>{" "}
lightColor="#00acd7" too.
darkColor="#2ad1fb" </Paragraph>
>
Go
</ColorfulLink>{" "}
too.
</p>
<p> <Paragraph>
Whenever possible, I also apply my experience in{" "} Whenever possible, I also apply my experience in{" "}
<ColorfulLink <ColorfulLink
href="https://github.com/jakejarvis/awesome-shodan-queries" href="https://github.com/jakejarvis/awesome-shodan-queries"
title="jakejarvis/awesome-shodan-queries on GitHub" title="jakejarvis/awesome-shodan-queries on GitHub"
lightColor="#00b81a" lightColor="#00b81a"
darkColor="#57f06d" darkColor="#57f06d"
> >
application security application security
</ColorfulLink> </ColorfulLink>
,{" "} ,{" "}
<ColorfulLink <ColorfulLink
href="https://www.cloudflare.com/learning/serverless/what-is-serverless/" href="https://www.cloudflare.com/learning/serverless/what-is-serverless/"
title='"What is serverless computing?" on Cloudflare' title='"What is serverless computing?" on Cloudflare'
lightColor="#0098ec" lightColor="#0098ec"
darkColor="#43b9fb" darkColor="#43b9fb"
> >
serverless stacks serverless stacks
</ColorfulLink> </ColorfulLink>
, and{" "} , and{" "}
<ColorfulLink <ColorfulLink href="https://xkcd.com/1319/" title='"Automation" on xkcd' lightColor="#ff6200" darkColor="#f46c16">
href="https://xkcd.com/1319/" DevOps automation
title='"Automation" on xkcd' </ColorfulLink>
lightColor="#ff6200" .
darkColor="#f46c16" </Paragraph>
>
DevOps automation
</ColorfulLink>
.
</p>
<p> <Paragraph>
I fell in love with{" "} I fell in love with{" "}
<ColorfulLink <ColorfulLink
href="/previously/" href="/previously/"
title="My Terrible, Horrible, No Good, Very Bad First Websites" title="My Terrible, Horrible, No Good, Very Bad First Websites"
lightColor="#4169e1" lightColor="#4169e1"
darkColor="#8ca9ff" darkColor="#8ca9ff"
> >
frontend web design frontend web design
</ColorfulLink>{" "} </ColorfulLink>{" "}
and{" "} and{" "}
<ColorfulLink <ColorfulLink
href="/notes/my-first-code/" href="/notes/my-first-code/"
title="Jake's Bulletin Board, circa 2003" title="Jake's Bulletin Board, circa 2003"
lightColor="#9932cc" lightColor="#9932cc"
darkColor="#d588fb" darkColor="#d588fb"
> >
backend programming backend programming
</ColorfulLink>{" "} </ColorfulLink>{" "}
back when my only source of income was{" "} back when my only source of income was{" "}
<span className="birthday"> <EasterEgg
<ColorfulLink href="/birthday/"
href="/birthday/" title="🎉 Cranky Birthday Boy on VHS Tape 📼"
title="🎉 Cranky Birthday Boy on VHS Tape 📼" lightColor="#e40088"
lightColor="#e40088" darkColor="#fd40b1"
darkColor="#fd40b1" >
> the Tooth Fairy
the Tooth Fairy </EasterEgg>
</ColorfulLink> . <Quiet>I've improved a bit since then, I think...</Quiet>
</span> </Paragraph>
. <span className="quiet">I've improved a bit since then, I think...</span>
</p>
<p> <Paragraph>
Over the years, some of my side projects{" "} Over the years, some of my side projects{" "}
<ColorfulLink <ColorfulLink
href="https://tuftsdaily.com/news/2012/04/06/student-designs-iphone-joeytracker-app/" href="https://tuftsdaily.com/news/2012/04/06/student-designs-iphone-joeytracker-app/"
title='"Student designs iPhone JoeyTracker app" on The Tufts Daily' title='"Student designs iPhone JoeyTracker app" on The Tufts Daily'
lightColor="#ff1b1b" lightColor="#ff1b1b"
darkColor="#f06060" darkColor="#f06060"
> >
have have
</ColorfulLink>{" "} </ColorfulLink>{" "}
<ColorfulLink <ColorfulLink
href="/leo/" href="/leo/"
title="Powncer segment on The Lab with Leo Laporte (G4techTV)" title="Powncer segment on The Lab with Leo Laporte (G4techTV)"
lightColor="#f78200" lightColor="#f78200"
darkColor="#fd992a" darkColor="#fd992a"
> >
been been
</ColorfulLink>{" "} </ColorfulLink>{" "}
<ColorfulLink <ColorfulLink
href="https://www.google.com/books/edition/The_Facebook_Effect/RRUkLhyGZVgC?hl=en&gbpv=1&dq=%22jake%20jarvis%22&pg=PA226&printsec=frontcover&bsq=%22jake%20jarvis%22" href="https://www.google.com/books/edition/The_Facebook_Effect/RRUkLhyGZVgC?hl=en&gbpv=1&dq=%22jake%20jarvis%22&pg=PA226&printsec=frontcover&bsq=%22jake%20jarvis%22"
title='"The Facebook Effect" by David Kirkpatrick (Google Books)' title='"The Facebook Effect" by David Kirkpatrick (Google Books)'
lightColor="#f2b702" lightColor="#f2b702"
darkColor="#ffcc2e" darkColor="#ffcc2e"
> >
featured featured
</ColorfulLink>{" "} </ColorfulLink>{" "}
<ColorfulLink <ColorfulLink
href="https://money.cnn.com/2007/06/01/technology/facebookplatform.fortune/index.htm" href="https://money.cnn.com/2007/06/01/technology/facebookplatform.fortune/index.htm"
title='"The new Facebook is on a roll" on CNN Money' title='"The new Facebook is on a roll" on CNN Money'
lightColor="#5ebd3e" lightColor="#5ebd3e"
darkColor="#78df55" darkColor="#78df55"
> >
by by
</ColorfulLink>{" "} </ColorfulLink>{" "}
<ColorfulLink <ColorfulLink
href="https://www.wired.com/2007/04/our-web-servers/" href="https://www.wired.com/2007/04/our-web-servers/"
title='"Middio: A YouTube Scraper for Major Label Music Videos" on Wired' title='"Middio: A YouTube Scraper for Major Label Music Videos" on Wired'
lightColor="#009cdf" lightColor="#009cdf"
darkColor="#29bfff" darkColor="#29bfff"
> >
various various
</ColorfulLink>{" "} </ColorfulLink>{" "}
<ColorfulLink <ColorfulLink
href="https://gigaom.com/2009/10/06/fresh-faces-in-tech-10-kid-entrepreneurs-to-watch/6/" href="https://gigaom.com/2009/10/06/fresh-faces-in-tech-10-kid-entrepreneurs-to-watch/6/"
title='"Fresh Faces in Tech: 10 Kid Entrepreneurs to Watch" on Gigaom' title='"Fresh Faces in Tech: 10 Kid Entrepreneurs to Watch" on Gigaom'
lightColor="#3e49bb" lightColor="#3e49bb"
darkColor="#7b87ff" darkColor="#7b87ff"
> >
media media
</ColorfulLink>{" "} </ColorfulLink>{" "}
<ColorfulLink <ColorfulLink
href="https://adage.com/article/small-agency-diary/client-ceo-s-son/116723/" href="https://adage.com/article/small-agency-diary/client-ceo-s-son/116723/"
title='"Your Next Client? The CEO&#39;s Son" on Advertising Age' title='"Your Next Client? The CEO&#39;s Son" on Advertising Age'
lightColor="#973999" lightColor="#973999"
darkColor="#db60dd" darkColor="#db60dd"
> >
outlets outlets
</ColorfulLink> </ColorfulLink>
. .
</p> </Paragraph>
<p> <Paragraph>
You can find more of my work on{" "} You can find more of my work on{" "}
<ColorfulLink <ColorfulLink
href="https://github.com/jakejarvis" href="https://github.com/jakejarvis"
title="Jake Jarvis on GitHub" title="Jake Jarvis on GitHub"
lightColor="#8d4eff" lightColor="#8d4eff"
darkColor="#a379f0" darkColor="#a379f0"
> >
GitHub GitHub
</ColorfulLink>{" "} </ColorfulLink>{" "}
and{" "} and{" "}
<ColorfulLink <ColorfulLink
href="https://www.linkedin.com/in/jakejarvis/" href="https://www.linkedin.com/in/jakejarvis/"
title="Jake Jarvis on LinkedIn" title="Jake Jarvis on LinkedIn"
lightColor="#0073b1" lightColor="#0073b1"
darkColor="#3b9dd2" darkColor="#3b9dd2"
> >
LinkedIn LinkedIn
</ColorfulLink> </ColorfulLink>
. I'm always available to connect over{" "} . I'm always available to connect over{" "}
<ColorfulLink href="/contact/" title="Send an email" lightColor="#de0c0c" darkColor="#ff5050"> <ColorfulLink href="/contact/" title="Send an email" lightColor="#de0c0c" darkColor="#ff5050">
email email
</ColorfulLink>{" "} </ColorfulLink>{" "}
<sup className="monospace pgp_key"> <PGPKey>
<ColorfulLink <ColorfulLink
href="/pubkey.asc" href="/pubkey.asc"
rel="pgpkey authn noopener" rel="pgpkey authn noopener"
title="My Public Key" title="My Public Key"
lightColor="#757575" lightColor="#757575"
darkColor="#959595" darkColor="#959595"
style={{ background: "none" }} style={{ background: "none" }}
forceNewWindow forceNewWindow
> >
🔐 2B0C 9CF2 51E6 9A39 <code>🔐 2B0C 9CF2 51E6 9A39</code>
</ColorfulLink> </ColorfulLink>
</sup> </PGPKey>
,{" "} ,{" "}
<ColorfulLink <ColorfulLink
href="https://twitter.com/jakejarvis" href="https://twitter.com/jakejarvis"
title="Jake Jarvis on Twitter" title="Jake Jarvis on Twitter"
lightColor="#00acee" lightColor="#00acee"
darkColor="#3bc9ff" darkColor="#3bc9ff"
> >
Twitter Twitter
</ColorfulLink> </ColorfulLink>
, or{" "} , or{" "}
<ColorfulLink <ColorfulLink
href="sms:+1-617-917-3737" href="sms:+1-617-917-3737"
title="Send SMS to +1 (617) 917-3737" title="Send SMS to +1 (617) 917-3737"
lightColor="#6fcc01" lightColor="#6fcc01"
darkColor="#8edb34" darkColor="#8edb34"
> >
SMS SMS
</ColorfulLink>{" "} </ColorfulLink>{" "}
as well! as well!
</p> </Paragraph>
</div> </Wrapper>
</Content>
<style jsx>{`
div {
font-size: 1.1em;
line-height: 1;
}
h1 {
margin: 0 0 0.5em -0.03em;
font-size: 1.8em;
font-weight: 500;
letter-spacing: -0.01em;
}
h2 {
margin: 0.5em 0 0.5em -0.03em;
font-size: 1.35em;
font-weight: 400;
letter-spacing: -0.016em;
line-height: 1.4;
}
p {
margin: 0.85em 0;
letter-spacing: -0.004em;
line-height: 1.7;
}
p:last-of-type {
margin-bottom: 0;
}
.wave {
display: inline-block;
margin-left: 0.1em;
font-size: 1.2em;
animation: wave 5s infinite;
animation-delay: 1s;
transform-origin: 65% 80%;
will-change: transform;
}
.pgp_key {
margin: 0 0.15em;
font-size: 0.65em;
word-spacing: -0.3em;
}
.quiet {
color: var(--medium-light);
}
.birthday :global(a:hover) {
/* magic wand cursor easter egg */
cursor: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='24' height='30' style='font-size:24px'><text y='50%' transform='rotate(-70 0 0) translate(-20, 6)'>🪄</text></svg>")
5 5,
auto;
}
@media screen and (max-width: 768px) {
h1 {
font-size: 1.5em;
}
h2 {
font-size: 1.2em;
}
p {
font-size: 0.925em;
}
}
/* https://jarv.is/notes/css-waving-hand-emoji/ */
@keyframes wave {
0% {
transform: rotate(0deg);
}
5% {
transform: rotate(14deg);
}
10% {
transform: rotate(-8deg);
}
15% {
transform: rotate(14deg);
}
20% {
transform: rotate(-4deg);
}
25% {
transform: rotate(10deg);
}
30% {
transform: rotate(0deg);
}
// pause for 3.5 out of 5 seconds
100% {
transform: rotate(0deg);
}
}
`}</style>
</>
); );
export default Index; export default Index;

View File

@@ -3,9 +3,18 @@ import Content from "../components/Content/Content";
import PageTitle from "../components/PageTitle/PageTitle"; import PageTitle from "../components/PageTitle/PageTitle";
import Link from "../components/Link/Link"; import Link from "../components/Link/Link";
import Video from "../components/Video/Video"; import Video from "../components/Video/Video";
import { styled } from "../lib/styles/stitches.config";
import thumbnail from "../public/static/images/leo/thumb.png"; import thumbnail from "../public/static/images/leo/thumb.png";
const Copyright = styled("p", {
textAlign: "center",
fontSize: "0.9em",
lineHeight: 1.8,
margin: "1.25em 1em 0 1em",
color: "$mediumLight",
});
const Leo = () => ( const Leo = () => (
<> <>
<NextSeo <NextSeo
@@ -28,7 +37,7 @@ const Leo = () => (
subs="/static/images/leo/subs.en.vtt" subs="/static/images/leo/subs.en.vtt"
/> />
<p className="copyright"> <Copyright>
Video is property of{" "} Video is property of{" "}
<Link href="https://web.archive.org/web/20070511004304/http://www.g4techtv.ca/" style={{ fontWeight: 700 }}> <Link href="https://web.archive.org/web/20070511004304/http://www.g4techtv.ca/" style={{ fontWeight: 700 }}>
G4techTV Canada G4techTV Canada
@@ -38,18 +47,8 @@ const Leo = () => (
Leo Laporte Leo Laporte
</Link> </Link>
. &copy; 2007 G4 Media, Inc. . &copy; 2007 G4 Media, Inc.
</p> </Copyright>
</Content> </Content>
<style jsx>{`
.copyright {
text-align: center;
font-size: 0.9em;
line-height: 1.8;
margin: 1.25em 1em 0 1em;
color: var(--medium-light);
}
`}</style>
</> </>
); );

View File

@@ -1,5 +1,6 @@
/* eslint-disable camelcase */ /* eslint-disable camelcase */
import Head from "next/head";
import { NextSeo } from "next-seo"; import { NextSeo } from "next-seo";
import Content from "../components/Content/Content"; import Content from "../components/Content/Content";
import PageTitle from "../components/PageTitle/PageTitle"; import PageTitle from "../components/PageTitle/PageTitle";
@@ -23,11 +24,69 @@ import img_2012_09 from "../public/static/images/previously/2012_09.png";
import img_2018_04 from "../public/static/images/previously/2018_04.png"; import img_2018_04 from "../public/static/images/previously/2018_04.png";
import img_2020_03 from "../public/static/images/previously/2020_03.png"; import img_2020_03 from "../public/static/images/previously/2020_03.png";
import "@fontsource/comic-neue/latin-400.css";
import "@fontsource/comic-neue/latin-700.css";
const Previously = () => ( const Previously = () => (
<> <>
<Head>
{/* a complete sh*tshow of overrides, mainly to compensate for font change */}
<style
dangerouslySetInnerHTML={{
__html: `
body {
font-family: "Comic Neue", "Comic Sans MS", "Comic Sans", sans-serif !important;
font-weight: 600 !important;
}
/* left header */
header nav > a:first-of-type span:last-of-type {
font-size: 1.4em !important;
font-weight: 700 !important;
}
/* right header */
header nav ul a span {
font-size: 1.1em !important;
font-weight: 700 !important;
line-height: 1.1;
}
/* content */
main > div > div {
font-size: 1.1em !important;
text-align: center;
}
main > div > div p {
font-size: 0.95em;
}
main > div > div strong {
font-weight: 900;
}
main > div > div code {
font-size: 0.85em;
font-weight: 400;
}
main > div > div figure:last-of-type {
margin-bottom: 0;
}
/* footer */
footer > div {
font-size: 0.95em !important;
}
/* components */
figcaption,
.iframe_caption {
margin-top: 0.2em;
font-size: 0.9em;
line-height: 1.5;
color: var(--colors-medium);
text-align: center;
}
hr {
margin: 1em auto !important;
}
iframe {
margin-bottom: 0.6em !important;
}`,
}}
/>
</Head>
<NextSeo <NextSeo
title="Previously on..." title="Previously on..."
description="An incredibly embarrassing and somewhat painful trip down this site's memory lane..." description="An incredibly embarrassing and somewhat painful trip down this site's memory lane..."
@@ -152,62 +211,6 @@ const Previously = () => (
) )
</Figure> </Figure>
</Content> </Content>
{/* a complete sh*tshow of overrides, mainly to compensate for font change */}
<style jsx global>{`
body {
font-family: "Comic Neue", "Comic Sans MS", "Comic Sans", var(--font-family-sans-variable);
font-weight: 600 !important;
}
/* left header */
header nav > a:first-of-type span:last-of-type {
font-size: 1.4em !important;
font-weight: 700 !important;
}
/* right header */
header nav ul a span {
font-size: 1.1em !important;
font-weight: 700 !important;
line-height: 1.1;
}
/* content */
main > div > div {
font-size: 1.1em !important;
text-align: center;
}
main > div > div p {
font-size: 0.95em;
}
main > div > div strong {
font-weight: 900;
}
main > div > div code {
font-size: 0.85em;
font-weight: 400;
}
main > div > div figure:last-of-type {
margin-bottom: 0;
}
/* footer */
footer > div {
font-size: 0.95em !important;
}
/* components */
figcaption,
.iframe_caption {
margin-top: 0.2em;
font-size: 0.9em;
line-height: 1.5;
color: var(--medium);
text-align: center;
}
hr {
margin: 1em auto !important;
}
iframe {
margin-bottom: 0.6em !important;
}
`}</style>
</> </>
); );

View File

@@ -5,10 +5,42 @@ import PageTitle from "../components/PageTitle/PageTitle";
import Link from "../components/Link/Link"; import Link from "../components/Link/Link";
import RepositoryCard from "../components/RepositoryCard/RepositoryCard"; import RepositoryCard from "../components/RepositoryCard/RepositoryCard";
import { OctocatOcticon } from "../components/Icons"; import { OctocatOcticon } from "../components/Icons";
import { styled } from "../lib/styles/stitches.config";
import { authorSocial } from "../lib/config"; import { authorSocial } from "../lib/config";
import type { GetStaticProps } from "next"; import type { GetStaticProps } from "next";
import type { RepositoryType } from "../types"; import type { RepositoryType } from "../types";
const Wrapper = styled("div", {
display: "flex",
flexFlow: "row wrap",
justifyContent: "space-between",
alignItems: "flex-start",
width: "100%",
fontSize: "1.1em",
lineHeight: 1.1,
});
const Card = styled(RepositoryCard, {
flexGrow: 1,
margin: "0.6em",
width: "370px",
});
const ViewMore = styled("p", {
textAlign: "center",
marginBottom: 0,
fontSize: "1.1em",
fontWeight: 500,
});
const GitHubLogo = styled(OctocatOcticon, {
width: "1.2em",
height: "1.2em",
verticalAlign: "-0.2em",
margin: "0 0.15em",
fill: "$text",
});
const Projects = ({ repos }) => ( const Projects = ({ repos }) => (
<> <>
<NextSeo <NextSeo
@@ -21,56 +53,18 @@ const Projects = ({ repos }) => (
<PageTitle>💾 Projects</PageTitle> <PageTitle>💾 Projects</PageTitle>
<Content> <Content>
<div className="wrapper"> <Wrapper>
{repos.map((repo: RepositoryType) => ( {repos.map((repo: RepositoryType) => (
<div key={repo.name} className="card"> <Card key={repo.name} {...repo} />
<RepositoryCard {...repo} />
</div>
))} ))}
</div> </Wrapper>
<p className="view_more"> <ViewMore>
<Link href={`https://github.com/${authorSocial.github}`}> <Link href={`https://github.com/${authorSocial.github}`}>
View more on{" "} View more on <GitHubLogo /> GitHub...
<OctocatOcticon
fill="currentColor"
style={{
color: "var(--text)",
width: "1.2em",
height: "1.2em",
verticalAlign: "-0.2em",
margin: "0 0.15em",
}}
/>{" "}
GitHub...
</Link> </Link>
</p> </ViewMore>
</Content> </Content>
<style jsx>{`
.wrapper {
display: flex;
flex-flow: row wrap;
justify-content: space-between;
align-items: flex-start;
width: 100%;
font-size: 1.1em;
line-height: 1.1;
}
.card {
flex-grow: 1;
margin: 0.5em;
width: 370px;
}
.view_more {
text-align: center;
margin-bottom: 0;
font-size: 1.1em;
font-weight: 500;
}
`}</style>
</> </>
); );

View File

@@ -54,7 +54,7 @@ Y2K.getLayout = (page: ReactElement) => {
<Wallpaper <Wallpaper
image={randomTile} image={randomTile}
tile tile
style={{ css={{
display: "flex", display: "flex",
justifyContent: "center", justifyContent: "center",
alignItems: "center", alignItems: "center",

View File

@@ -29,9 +29,9 @@
# TECHNOLOGY # TECHNOLOGY
- Next.js - Next.js & React
- React
- Vercel - Vercel
- Stitches
- Giscus - Giscus
- Fathom Analytics - Fathom Analytics
- ...and more: https://jarv.is/uses/ - ...and more: https://jarv.is/uses/

View File

@@ -1,19 +0,0 @@
body {
background-color: var(--background-inner);
/* light-dark theme switch fading */
transition: background 0.25s ease;
}
/* https://web.dev/prefers-reduced-motion/#(bonus)-forcing-reduced-motion-on-all-websites */
@media (prefers-reduced-motion: reduce) {
*,
::before,
::after {
animation-delay: -1ms !important;
animation-duration: 1ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0s !important;
transition-delay: 0s !important;
}
}

View File

@@ -1,12 +0,0 @@
:root {
--rounded-edge-radius: 0.65em;
--link-underline-size: calc(0.1em + 0.05rem);
--font-family-sans: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial,
sans-serif;
--font-family-sans-variable: InterVariable, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
Helvetica, Arial, sans-serif;
--font-family-mono: "Roboto Mono", ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier;
--font-family-mono-variable: "Roboto MonoVariable", ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", Menlo,
Courier;
}

View File

@@ -1,51 +0,0 @@
body {
font-family: var(--font-family-sans);
font-kerning: normal;
font-variant-ligatures: common-ligatures;
}
code,
kbd,
samp,
pre,
.monospace {
font-family: var(--font-family-mono);
}
/*
* override above font-family if browser supports variable fonts:
* https://caniuse.com/#feat=variable-fonts
*/
@supports (font-variation-settings: normal) {
body {
font-family: var(--font-family-sans-variable);
font-optical-sizing: auto;
}
code,
kbd,
samp,
pre,
.monospace {
font-family: var(--font-family-mono-variable);
}
/*
* Chrome doesn't automatically slant multi-axis Inter var, for some reason.
* Adding "slnt" -10 fixes Chrome but then over-italicizes in Firefox. AHHHHHHHHHH.
*/
em {
font-style: normal;
font-variation-settings: "ital" 1, "slnt" -10;
}
/* Roboto Mono doesn't have this problem, but the above fix breaks it, of course. */
em code,
em kbd,
em samp,
em pre,
em .monospace {
font-style: italic !important;
font-variation-settings: initial !important;
}
}

925
yarn.lock

File diff suppressed because it is too large Load Diff