1
mirror of https://github.com/jakejarvis/jarv.is.git synced 2025-04-26 09:05:22 -04:00

CSS modules ➡️ Stitches 🧵 (#799)

This commit is contained in:
Jake Jarvis 2022-03-03 09:18:26 -05:00 committed by GitHub
parent ac7ac71c10
commit c2dde042b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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": [
"dbaeumer.vscode-eslint",
"divlo.vscode-styled-jsx-languageserver",
"divlo.vscode-styled-jsx-syntax",
"editorconfig.editorconfig",
"esbenp.prettier-vscode",
"silvenon.mdx",
"stylelint.vscode-stylelint",
"wix.vscode-import-cost"
]
}

View File

@ -11,9 +11,6 @@
},
"prettier.requireConfig": true,
"prettier.configPath": ".prettierrc.json",
"stylelint.packageManager": "yarn",
"stylelint.reportNeedlessDisables": true,
"stylelint.reportInvalidScopeDisables": true,
"npm.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)
[![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!

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";
export type BlockquoteProps = JSX.IntrinsicElements["blockquote"];
const Blockquote = ({ className, ...rest }: BlockquoteProps) => (
<blockquote className={classNames(styles.blockquote, className)} {...rest} />
);
const Blockquote = styled("blockquote", {
marginLeft: 0,
paddingLeft: "1.25em",
borderLeft: "0.25em solid $link",
color: "$mediumDark",
});
export default Blockquote;

View File

@ -7,6 +7,7 @@ export type CaptchaProps = {
size?: "normal" | "compact" | "invisible";
theme?: "light" | "dark";
id?: string;
className?: string;
// callbacks pulled verbatim from node_modules/@hcaptcha/react-hcaptcha/types/index.d.ts
/* eslint-disable @typescript-eslint/no-explicit-any */
@ -20,7 +21,7 @@ export type CaptchaProps = {
/* 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();
return (
@ -30,6 +31,7 @@ const Captcha = ({ size = "normal", theme, id, ...rest }: CaptchaProps) => {
<link rel="preconnect" href="https://newassets.hcaptcha.com" />
</Head>
<div className={className}>
<HCaptcha
sitekey={process.env.NEXT_PUBLIC_HCAPTCHA_SITE_KEY}
reCaptchaCompat={false}
@ -39,6 +41,7 @@ const Captcha = ({ size = "normal", theme, id, ...rest }: CaptchaProps) => {
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 { 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;
};
@ -15,26 +115,19 @@ const CodeBlock = ({ forceBlock, className, children, ...rest }: CodeBlockProps)
// 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}`
return (
<div className={styles.block}>
<CopyButton source={children} className={styles.copy_btn} />
<code
className={classNames(
styles.code,
prismEnabled && styles.highlight,
className?.replace("code-highlight", "").trim()
)}
{...rest}
>
<Block>
<CornerCopyButton source={children} />
<Code highlight={prismEnabled} className={className?.replace("code-highlight", "").trim()} {...rest}>
{children}
</code>
</div>
</Code>
</Block>
);
} else {
// inline code in paragraphs, headings, etc. (not highlighted)
return (
<code className={classNames(styles.code, styles.inline, className)} {...rest}>
<InlineCode className={className} {...rest}>
{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 { useTheme } from "next-themes";
import classNames from "classnames";
import { Giscus } from "@giscus/react";
import { styled } from "../../lib/styles/stitches.config";
import { giscusConfig } from "../../lib/config";
import type { ComponentProps } from "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;
};
const Comments = ({ title, className, ...rest }: CommentsProps) => {
const Comments = ({ title, ...rest }: CommentsProps) => {
const { resolvedTheme } = useTheme();
return (
<div className={classNames(styles.wrapper, className)} {...rest}>
<Wrapper {...rest}>
<Giscus
{...(giscusConfig as GiscusProps)}
term={title}
@ -24,7 +32,7 @@ const Comments = ({ title, className, ...rest }: CommentsProps) => {
emitMetadata="0"
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 classNames from "classnames";
import { Formik, Form, Field } from "formik";
import TextareaAutosize from "react-textarea-autosize";
import Link from "../Link/Link";
import Captcha from "../Captcha/Captcha";
import { SendIcon, CheckOcticon, XOcticon } from "../Icons";
import { styled, css } from "../../lib/styles/stitches.config";
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 = {
name: string;
@ -100,11 +213,11 @@ const ContactForm = ({ className }: ContactFormProps) => {
<Form className={className} name="contact">
<Field name="name">
{({ field, meta }) => (
<input
<Input
type="text"
className={classNames(styles.input, meta.error && meta.touched && styles.missing)}
placeholder="Name"
disabled={success}
missing={meta.error && meta.touched}
{...field}
/>
)}
@ -112,12 +225,12 @@ const ContactForm = ({ className }: ContactFormProps) => {
<Field name="email">
{({ field, meta }) => (
<input
<Input
type="email"
inputMode="email"
className={classNames(styles.input, meta.error && meta.touched && styles.missing)}
placeholder="Email"
disabled={success}
missing={meta.error && meta.touched}
{...field}
/>
)}
@ -125,17 +238,17 @@ const ContactForm = ({ className }: ContactFormProps) => {
<Field name="message">
{({ field, meta }) => (
<TextareaAutosize
className={classNames(styles.input, styles.textarea, meta.error && meta.touched && styles.missing)}
<TextArea
placeholder="Write something..."
minRows={5}
disabled={success}
missing={meta.error && meta.touched}
{...field}
/>
)}
</Field>
<div className={styles.markdown_tip}>
<MarkdownTip>
Basic{" "}
<Link href="https://commonmark.org/help/" title="Markdown reference sheet" style={{ fontWeight: 600 }}>
Markdown syntax
@ -145,45 +258,32 @@ const ContactForm = ({ className }: ContactFormProps) => {
links
</Link>
](https://jarv.is), and <code>`code`</code>.
</div>
</MarkdownTip>
<div className={styles.captcha}>
<Captcha onVerify={(token) => setFieldValue("h-captcha-response", token)} />
</div>
<HCaptcha onVerify={(token) => setFieldValue("h-captcha-response", token)} />
<div className={styles.action_row}>
<button
className={classNames(styles.btn_submit, success && styles.hidden)}
<ActionRow>
<SubmitButton
type="submit"
title="Send Message"
aria-label="Send Message"
onClick={() => setSubmitted(true)}
disabled={isSubmitting}
hidden={success}
>
{isSubmitting ? (
<span>Sending...</span>
) : (
<>
<SendIcon className={classNames(styles.send_icon)} /> <span>Send</span>
<SubmitIcon /> <span>Send</span>
</>
)}
</button>
</SubmitButton>
<span
className={classNames(
success && styles.result_success,
!success && styles.result_error,
(!submitted || !feedback || isSubmitting) && styles.hidden
)}
>
{success ? (
<CheckOcticon className={styles.result_icon} fill="CurrentColor" />
) : (
<XOcticon className={styles.result_icon} fill="CurrentColor" />
)}{" "}
{feedback}
</span>
</div>
<Result status={success ? "success" : "error"} hidden={!submitted || !feedback || isSubmitting}>
<ResultIcon as={success ? CheckOcticon : XOcticon} /> {feedback}
</Result>
</ActionRow>
</Form>
)}
</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"];
const Content = ({ className, ...rest }: ContentProps) => (
<div className={classNames(styles.content, className)} {...rest} />
);
"@mobile": {
fontSize: "0.925em",
lineHeight: 1.85,
},
});
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 classNames from "classnames";
import copy from "copy-to-clipboard";
import innerText from "react-innertext";
import { ClipboardOcticon, CheckOcticon } from "../Icons";
import { styled } from "../../lib/styles/stitches.config";
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 = {
source: ReactNode;
@ -45,20 +63,17 @@ const CopyButton = forwardRef(function CopyButton(
}, [timeout, copied]);
return (
<button
className={classNames(styles.button, copied && styles.success, className)}
<Button
className={className}
title="Copy to clipboard"
aria-label="Copy to clipboard"
onClick={handleCopy}
disabled={!!copied}
success={copied}
ref={ref}
>
{copied ? (
<CheckOcticon className={styles.icon} fill="currentColor" />
) : (
<ClipboardOcticon className={styles.icon} fill="currentColor" />
)}
</button>
<Icon as={copied ? CheckOcticon : ClipboardOcticon} />
</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 innerText from "react-innertext";
import classNames from "classnames";
import { styled } from "../../lib/styles/stitches.config";
import type { PropsWithChildren } from "react";
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"> &
PropsWithChildren<{
@ -13,10 +28,10 @@ export type FigureProps = Omit<NextImageProps, "alt"> &
const Figure = ({ children, alt, className, ...imageProps }: FigureProps) => {
return (
<figure className={classNames(styles.figure, className)}>
<Wrapper className={className}>
<Image alt={alt || innerText(children)} {...imageProps} />
<figcaption className={styles.caption}>{children}</figcaption>
</figure>
<Caption>{children}</Caption>
</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 Link from "next/link";
import classNames from "classnames";
import NextLink from "next/link";
import { HeartIcon, NextjsLogo } from "../Icons";
import { keyframes, styled } from "../../lib/styles/stitches.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) => (
<footer className={classNames(styles.footer, className)} {...rest}>
<div className={styles.row}>
<div className={styles.license}>
const Row = styled("div", {
display: "flex",
width: "100%",
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{" "}
<Link href="/license/" prefetch={false}>
<a className={styles.link} title="Creative Commons Attribution 4.0 International">
licensed under CC-BY-4.0
</a>
</Link>
<NextLink href="/license/" prefetch={false} passHref={true}>
<Link title="Creative Commons Attribution 4.0 International">licensed under CC-BY-4.0</Link>
</NextLink>
,{" "}
<Link href="/previously/" prefetch={false}>
<a className={styles.link} title="Previously on...">
2001
</a>
</Link>{" "}
<NextLink href="/previously/" prefetch={false} passHref={true}>
<Link title="Previously on...">2001</Link>
</NextLink>{" "}
{new Date().getFullYear()}.
</div>
<div className={styles.powered_by}>
<div>
Made with{" "}
<span className={styles.heart} title="Love">
<HeartIcon className={styles.icon} />
</span>{" "}
<Heart title="Love">
<Icon as={HeartIcon} />
</Heart>{" "}
and{" "}
<a
className={classNames(styles.link, styles.nextjs)}
<NextjsLink
href="https://nextjs.org/"
title="Powered by Next.js"
aria-label="Next.js"
target="_blank"
rel="noopener noreferrer"
>
<NextjsLogo className={styles.icon} fill="currentColor" />
</a>
<Icon as={NextjsLogo} fill="currentColor" />
</NextjsLink>
.{" "}
<a
className={classNames(styles.link, styles.view_source)}
<ViewSourceLink
href={`https://github.com/${config.githubRepo}`}
title="View Source on GitHub"
target="_blank"
rel="noopener noreferrer"
>
View source.
</a>
</ViewSourceLink>
</div>
</div>
</footer>
</Row>
</Wrapper>
);
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 classNames from "classnames";
import Selfie from "../Selfie/Selfie";
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;
};
const Header = ({ sticky, className, ...rest }: HeaderProps) => (
<header className={classNames(styles.header, sticky && styles.sticky, className)} {...rest}>
<nav className={styles.nav}>
<Selfie className={styles.selfie} />
<Menu className={styles.menu} />
</nav>
</header>
const Header = ({ sticky, ...rest }: HeaderProps) => (
<Wrapper sticky={sticky} {...rest}>
<Nav>
<Selfie />
<ResponsiveMenu />
</Nav>
</Wrapper>
);
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 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";
};
const Heading = ({ as: Component, id, className, children, ...rest }: HeadingProps) => {
const Heading = ({ as, id, children, ...rest }: HeadingProps) => {
return (
<Component className={classNames(styles.heading, styles[Component], className)} id={id} {...rest}>
<H as={as} underline={as === "h2"} id={id} {...rest}>
{children}
{/* add anchor link to H2s and H3s. ID is already generated by rehype-slug. `#` character inserted via CSS. */}
{id && (Component === "h2" || Component === "h3") && (
<a
className={styles.anchor}
href={`#${id}`}
title={`Jump to "${innerText(children)}"`}
tabIndex={-1}
aria-hidden={true}
/>
{id && (as === "h2" || as === "h3") && (
<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";
export type HorizontalRuleProps = JSX.IntrinsicElements["hr"];
const HorizontalRule = ({ className, ...rest }: HorizontalRuleProps) => (
<hr className={classNames(styles.hr, className)} {...rest} />
);
const HorizontalRule = styled("hr", {
margin: "1.5em auto",
height: "0.175em",
border: 0,
backgroundColor: "$light",
});
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;
height: number;
width?: number; // defaults to 100%
@ -10,17 +17,16 @@ export type IFrameProps = JSX.IntrinsicElements["iframe"] & {
noScroll?: boolean;
};
const IFrame = ({ src, title, height, width, allowScripts, noScroll, className, ...rest }: IFrameProps) => (
<iframe
className={classNames(styles.frame, className)}
const IFrame = ({ src, title, height, width, allowScripts, noScroll, ...rest }: IFrameProps) => (
<RoundedIFrame
src={src}
title={title}
sandbox={allowScripts ? "allow-same-origin allow-scripts allow-popups" : undefined}
scrolling={noScroll ? "no" : undefined}
loading="lazy"
style={{
css={{
height: `${height}px`,
maxWidth: width ? `${width}px` : undefined,
maxWidth: width ? `${width}px` : null,
}}
{...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 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 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 = ({
src,
@ -15,7 +26,7 @@ const CustomImage = ({
priority,
className,
...rest
}: NextImageProps) => {
}: NextImageProps & ComponentProps<typeof RoundedImage>) => {
// passed directly into next/image: https://nextjs.org/docs/api-reference/next/image
const imageProps: Partial<NextImageProps> = {
width: typeof width === "string" ? Number.parseInt(width) : width,
@ -40,10 +51,10 @@ const CustomImage = ({
}
return (
<div className={classNames(styles.wrapper, className)}>
<Wrapper className={className}>
{/* @ts-ignore */}
<NextImage className={styles.image} {...imageProps} {...rest} />
</div>
<RoundedImage {...imageProps} {...rest} />
</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 { useTheme } from "next-themes";
import classNames from "classnames";
import Header from "../Header/Header";
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.
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();
return (
<>
<Head>
{/* 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>
<div className={classNames(styles.flex, className)} {...rest}>
<Flex {...rest}>
<Header sticky={stickyHeader} />
{/* passing `container={false}` to Layout allows 100% control of the content area on a per-page basis */}
{container ? (
<main className={styles.default}>
<div className={styles.container}>{children}</div>
</main>
<Default>
<Container>{children}</Container>
</Default>
) : (
<>{children}</>
)}
<Footer className={styles.footer} />
</div>
<FlexedFooter />
</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 classNames from "classnames";
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 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 & {
forceNewWindow?: boolean;
};
@ -17,25 +34,18 @@ const CustomLink = ({
target,
rel,
forceNewWindow,
className,
...rest
}: CustomLinkProps) => {
// 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}`.
if (forceNewWindow || isAbsoluteUrl(href.toString())) {
return (
<a
href={href.toString()}
target={target || "_blank"}
rel={rel || "noopener noreferrer"}
className={classNames(styles.link, className)}
{...rest}
/>
<FancyLink href={href.toString()} target={target || "_blank"} rel={rel || "noopener noreferrer"} {...rest} />
);
} else {
return (
<NextLink href={href} prefetch={prefetch} passHref={passHref}>
<a className={classNames(styles.link, className)} {...rest} />
<FancyLink {...rest} />
</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"]) => (
<ul className={classNames(styles.unordered, className)} {...rest} />
);
export const OrderedList = ({ className, ...rest }: JSX.IntrinsicElements["ol"]) => (
<ol className={classNames(styles.ordered, className)} {...rest} />
);
export const UnorderedList = styled("ul", ListStyles);
export const OrderedList = styled("ol", ListStyles);
// TODO: this is based on good faith that the children are all `<li>`s...
export const ListItem = ({ className, ...rest }: JSX.IntrinsicElements["li"]) => (
<li className={classNames(styles.item, className)} {...rest} />
);
export const ListItem = styled("li", {
paddingLeft: "0.25em",
});

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 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 = {
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)
// each individual box's animation has a staggered start in corresponding order
divs.push(
<div
<Box
key={i}
className={styles.box}
style={{
css={{
width: `${width / (boxes + 1)}px`,
animationDelay: `${i * timing}s`,
}}
@ -31,15 +49,15 @@ const Loading = ({ width, boxes = 3, timing = 0.1, className }: LoadingProps) =>
}
return (
<div
className={classNames(styles.wrapper, className)}
<Wrapper
className={className}
style={{
width: `${width}px`,
height: `${width / 2}px`,
}}
>
{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 { useRouter } from "next/router";
import classNames from "classnames";
import MenuItem from "../MenuItem/MenuItem";
import { styled } from "../../lib/styles/stitches.config";
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 = {
className?: string;
};
"@mobile": {
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();
return (
<ul className={classNames(styles.menu, className)}>
<Wrapper {...rest}>
{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 */}
<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 classNames from "classnames";
import NextLink from "next/link";
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 = {
href?: string;
@ -14,19 +59,21 @@ export type MenuItemProps = {
icon: any;
};
const MenuItem = ({ icon: Icon, href, text, current, className }: MenuItemProps) => {
const MenuItem = ({ icon: ItemIcon, href, text, current, className }: MenuItemProps) => {
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)
if (href) {
return (
<Link href={href} prefetch={false}>
<a className={classNames(styles.link, current && styles.current, className)}>{linkContent}</a>
<NextLink href={href} prefetch={false} passHref={true}>
<Link className={className} current={current}>
{linkContent}
</Link>
</NextLink>
);
} else {
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 classNames from "classnames";
import { format } from "date-fns";
import HitCounter from "../HitCounter/HitCounter";
import NoteTitle from "../NoteTitle/NoteTitle";
import { DateIcon, TagIcon, EditIcon, ViewsIcon } from "../Icons";
import { styled } from "../../lib/styles/stitches.config";
import * as config from "../../lib/config";
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">;
const NoteMeta = ({ slug, date, title, htmlTitle, tags = [] }: NoteMetaProps) => (
<>
<div className={styles.meta}>
<div className={styles.meta_item}>
<Wrapper>
<MetaItem>
<Link
href={{
pathname: "/notes/[slug]/",
query: { slug },
}}
passHref={true}
>
<a className={styles.date_link}>
<MetaLink>
<span>
<DateIcon className={styles.icon} />
<Icon as={DateIcon} />
</span>
<span title={format(new Date(date), "PPppp")}>{format(new Date(date), "MMMM d, yyyy")}</span>
</a>
</MetaLink>
</Link>
</div>
</MetaItem>
{tags.length > 0 && (
<div className={classNames(styles.meta_item, styles.tags)}>
<MetaItem
css={{
whiteSpace: "normal",
display: "inline-flex",
flexWrap: "wrap",
}}
>
<span>
<TagIcon className={styles.icon} />
<Icon as={TagIcon} />
</span>
{tags.map((tag) => (
<span key={tag} className={styles.tag}>
{tag}
</span>
<Tag key={tag}>{tag}</Tag>
))}
</div>
</MetaItem>
)}
<div className={styles.meta_item}>
<a
className={styles.edit_link}
<MetaItem>
<MetaLink
href={`https://github.com/${config.githubRepo}/blob/main/notes/${slug}.mdx`}
target="_blank"
rel="noopener noreferrer"
title={`Edit "${title}" on GitHub`}
>
<span>
<EditIcon className={styles.icon} />
<Icon as={EditIcon} />
</span>
<span>Improve This Post</span>
</a>
</div>
</MetaLink>
</MetaItem>
{/* only count hits on production site */}
{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>
<ViewsIcon className={styles.icon} />
<Icon as={ViewsIcon} />
</span>
<HitCounter slug={`notes/${slug}`} />
</div>
</MetaItem>
)}
</div>
</Wrapper>
<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 classNames from "classnames";
import NextLink from "next/link";
import { styled } from "../../lib/styles/stitches.config";
import type { ComponentProps } from "react";
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) => (
<h1 className={classNames(styles.title, className)} {...rest}>
<Link
"@mobile": {
fontSize: "1.8em",
},
});
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={{
pathname: "/notes/[slug]/",
query: { slug },
}}
passHref={true}
>
<a className={styles.link} dangerouslySetInnerHTML={{ __html: htmlTitle }} />
</Link>
</h1>
<Link dangerouslySetInnerHTML={{ __html: htmlTitle }} />
</NextLink>
</Title>
);
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 Link from "../Link/Link";
import { styled } from "../../lib/styles/stitches.config";
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 = {
notesByYear: Record<string, NoteType["frontMatter"][]>;
@ -13,12 +62,12 @@ const NotesList = ({ notesByYear }: NotesListProps) => {
Object.entries(notesByYear).forEach(([year, notes]: [string, NoteType["frontMatter"][]]) => {
sections.push(
<section key={year} className={styles.section}>
<h2 className={styles.year}>{year}</h2>
<ul className={styles.list}>
<Section key={year}>
<Year>{year}</Year>
<List>
{notes.map(({ slug, date, htmlTitle }) => (
<li key={slug} className={styles.row}>
<span className={styles.date}>{format(new Date(date), "MMM d")}</span>
<Post key={slug}>
<PostDate>{format(new Date(date), "MMM d")}</PostDate>
<span>
<Link
href={{
@ -28,10 +77,10 @@ const NotesList = ({ notesByYear }: NotesListProps) => {
dangerouslySetInnerHTML={{ __html: htmlTitle }}
/>
</span>
</li>
</Post>
))}
</ul>
</section>
</List>
</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 { 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;
};
const OctocatLink = ({ repo, className, ...rest }: OctocatLinkProps) => (
<a className={styles.link} href={`https://github.com/${repo}`} target="_blank" rel="noopener noreferrer" {...rest}>
<OctocatOcticon fill="currentColor" className={classNames(styles.icon, className)} />
</a>
<Link href={`https://github.com/${repo}`} target="_blank" rel="noopener noreferrer" {...rest}>
<Octocat className={className} />
</Link>
);
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 Link from "next/link";
import classNames from "classnames";
import NextLink from "next/link";
import { styled } from "../../lib/styles/stitches.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 canonical = `${baseUrl}${router.pathname}/`;
return (
<h1 className={classNames(styles.title, className)} {...rest}>
<Link href={canonical}>
<a className={styles.link}>{children}</a>
</Link>
</h1>
<Title {...rest}>
<NextLink href={canonical} passHref={true}>
<Link>{children}</Link>
</NextLink>
</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 Link from "../Link/Link";
import { StarOcticon, ForkOcticon } from "../Icons";
import { styled } from "../../lib/styles/stitches.config";
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 & {
className?: string;
@ -20,53 +81,48 @@ const RepositoryCard = ({
updatedAt,
className,
}: RepositoryCardProps) => (
<div className={classNames(styles.card, className)}>
<Link className={styles.name} href={url}>
{name}
</Link>
<Wrapper className={className}>
<Name href={url}>{name}</Name>
{description && <p className={styles.description}>{description}</p>}
{description && <Description>{description}</Description>}
<div className={styles.meta}>
<Meta>
{language && (
<div className={styles.meta_item}>
<span className={styles.language_color} style={{ backgroundColor: language.color }} />
<MetaItem>
<LanguageCircle css={{ backgroundColor: language.color }} />
<span>{language.name}</span>
</div>
</MetaItem>
)}
{stars > 0 && (
<div className={styles.meta_item}>
<a
className={styles.meta_link}
<MetaItem>
<MetaLink
href={`${url}/stargazers`}
title={`${stars.toLocaleString("en-US")} ${stars === 1 ? "star" : "stars"}`}
target="_blank"
rel="noopener noreferrer"
>
<StarOcticon fill="currentColor" className={styles.octicon} />
<MetaIcon as={StarOcticon} />
<span>{stars.toLocaleString("en-US")}</span>
</a>
</div>
</MetaLink>
</MetaItem>
)}
{forks > 0 && (
<div className={styles.meta_item}>
<a
className={styles.meta_link}
<MetaItem>
<MetaLink
href={`${url}/network/members`}
title={`${forks.toLocaleString("en-US")} ${forks === 1 ? "fork" : "forks"}`}
target="_blank"
rel="noopener noreferrer"
>
<ForkOcticon fill="currentColor" className={styles.octicon} />
<MetaIcon as={ForkOcticon} />
<span>{forks.toLocaleString("en-US")}</span>
</a>
</div>
</MetaLink>
</MetaItem>
)}
<div
className={styles.meta_item}
<MetaItem
title={intlFormat(
new Date(updatedAt),
{
@ -83,9 +139,9 @@ const RepositoryCard = ({
)}
>
<span>Updated {formatDistanceToNowStrict(new Date(updatedAt), { addSuffix: true })}</span>
</div>
</div>
</div>
</MetaItem>
</Meta>
</Wrapper>
);
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 Link from "next/link";
import Image from "next/image";
import classNames from "classnames";
import styles from "./Selfie.module.css";
import NextLink from "next/link";
import NextImage from "next/image";
import { styled } from "../../lib/styles/stitches.config";
import type { ComponentProps } from "react";
import selfieJpg from "../../public/static/images/selfie.jpg";
export type SelfieProps = {
className?: string;
};
const ConstrainImage = styled("div", {
width: "50px",
height: "50px",
lineHeight: 0,
padding: 0,
const Selfie = ({ className }: SelfieProps) => (
<Link href="/">
<a className={classNames(styles.link, className)}>
<div className={styles.selfie}>
"@mobile": {
width: "70px",
height: "70px",
},
});
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
src={selfieJpg}
alt="Photo of Jake Jarvis"
@ -24,10 +70,10 @@ const Selfie = ({ className }: SelfieProps) => (
layout="intrinsic"
priority
/>
</div>
<span className={styles.name}>Jake Jarvis</span>
</a>
</ConstrainImage>
<Name>Jake Jarvis</Name>
</Link>
</NextLink>
);
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 classNames from "classnames";
import type { Ref } from "react";
import { keyframes, styled } from "../../lib/styles/stitches.config";
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
const Terminal = forwardRef(function Terminal({ className, ...rest }: TerminalProps, ref: Ref<HTMLSpanElement>) {
const Terminal = forwardRef(function Terminal({ ...rest }: TerminalProps, ref: Ref<HTMLSpanElement>) {
return (
<div className={classNames("monospace", className, styles.terminal)} {...rest}>
<span ref={ref} /> <span className={styles.blink} />
</div>
<BlackBox {...rest}>
<Monospace>
<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 { useTheme } from "next-themes";
import { styled } from "../../lib/styles/stitches.config";
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 = {
className?: string;
@ -16,20 +28,19 @@ const ThemeToggle = ({ className }: ThemeToggleProps) => {
useEffect(() => setMounted(true), []);
if (!mounted) {
return (
<button className={styles.button} aria-hidden={true}>
<Button aria-hidden={true}>
<SunIcon className={className} />
</button>
</Button>
);
}
return (
<button
className={styles.button}
<Button
onClick={() => setTheme(resolvedTheme === "light" ? "dark" : "light")}
title={resolvedTheme === "light" ? "Toggle Dark Mode" : "Toggle Light Mode"}
>
{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 RFB from "@novnc/novnc/core/rfb.js";
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 = {
server: string;
@ -104,10 +137,10 @@ const VNC = ({ server }: VNCProps) => {
return (
<>
<Terminal ref={terminalRef} className={styles.terminal} />
<DOS ref={terminalRef} />
{/* 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 { styled } from "../../lib/styles/stitches.config";
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> & {
src: {
@ -59,7 +72,7 @@ const Video = ({ src, thumbnail, subs, autoplay, className, ...rest }: VideoProp
}
return (
<div className={classNames(styles.wrapper, className)}>
<Wrapper className={className}>
<ReactPlayer
width="100%"
height="100%"
@ -69,7 +82,7 @@ const Video = ({ src, thumbnail, subs, autoplay, className, ...rest }: VideoProp
config={config}
{...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 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;
tile?: boolean;
};
const Wallpaper = ({ image, tile, className, ...rest }: WallpaperProps) => {
const bgRef = useRef<HTMLDivElement>(null);
const Wallpaper = ({ image, tile, ...rest }: WallpaperProps) => {
const bgRef = useRef<VariantProps<typeof Wrapper>>(null);
useEffect(() => {
// @ts-ignore
bgRef.current.style.backgroundImage = `url(${image})`;
}, []); // 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;

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 { styled } from "../../lib/styles/stitches.config";
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> & {
id: string;
@ -10,7 +23,7 @@ export type YouTubeEmbedProps = Partial<YouTubePlayerProps> & {
};
const YouTubeEmbed = ({ id, className, ...rest }: YouTubeEmbedProps) => (
<div className={classNames(styles.wrapper, className)}>
<Wrapper className={className}>
<ReactPlayer
width="100%"
height="100%"
@ -19,7 +32,7 @@ const YouTubeEmbed = ({ id, className, ...rest }: YouTubeEmbedProps) => (
controls
{...rest}
/>
</div>
</Wrapper>
);
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"],
minimumCacheTTL: 43200,
},
experimental: {
// use critters to automatically inline critical css:
optimizeCss: true,
},
webpack: (config) => {
// this lets us statically import webfonts like we would images, allowing cool things like preloading them
config.module.rules.push({
test: /\.svg$/,
issuer: { and: [/\.(js|ts)x?$/] },
test: /\.(woff|woff2|eot|ttf|otf)$/i,
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: [
{
loader: "@svgr/webpack",
@ -51,9 +60,6 @@ module.exports = (phase, { defaultConfig }) => {
},
],
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/feather-icons/dist/icons"),
path.resolve(__dirname, "node_modules/simple-icons/icons"),

View File

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

View File

@ -1,33 +1,16 @@
import { useEffect } from "react";
import { useRouter } from "next/router";
import Head from "next/head";
import { ThemeProvider } from "next-themes";
import { DefaultSeo, SocialProfileJsonLd } from "next-seo";
import * as Fathom from "fathom-client";
import Layout from "../components/Layout/Layout";
import { globalStyles, theme, darkTheme } from "../lib/styles/stitches.config";
import * as config from "../lib/config";
import { defaultSeo, socialProfileJsonLd } from "../lib/config/seo";
import type { ReactElement, ReactNode } from "react";
import type { NextPage } from "next";
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
export type AppProps = NextAppProps & {
Component: NextPage & {
@ -68,28 +51,12 @@ const App = ({ Component, pageProps }: AppProps) => {
// allow layout overrides per-page, but default to plain `<Layout />`
const getLayout = Component.getLayout || ((page) => <Layout>{page}</Layout>);
// inject body styles defined in ../lib/styles/stitches.config.ts
globalStyles();
return (
<>
{/* static asset preloads */}
<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 */}
{/* all SEO config is in ../lib/config/seo.ts except for canonical URLs, which require access to next router */}
<DefaultSeo
{...defaultSeo}
canonical={canonical}
@ -103,8 +70,16 @@ const App = ({ Component, pageProps }: AppProps) => {
/>
<SocialProfileJsonLd {...socialProfileJsonLd} />
{/* NOTE: this *must* come last in this fragment */}
<ThemeProvider>{getLayout(<Component {...pageProps} />)}</ThemeProvider>
<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 themes, { toCSS } from "../lib/config/themes";
import { getCssText, preloads } from "../lib/styles/stitches.config";
import * as config from "../lib/config";
// https://nextjs.org/docs/advanced-features/custom-document
@ -7,13 +7,12 @@ const Document = () => {
return (
<Html lang={config.siteLocale?.replace("_", "-")}>
<Head>
{/* convert themes object into inlined css variables */}
<style
id="theme-colors"
dangerouslySetInnerHTML={{
__html: `:root{${toCSS(themes.light)}}[data-theme="dark"]{${toCSS(themes.dark)}}`,
}}
/>
{/* static asset preloads */}
<link rel="preload" as="font" type="font/woff2" href={preloads.fonts.InterVar} crossOrigin="anonymous" />
<link rel="preload" as="font" type="font/woff2" href={preloads.fonts.RobotoMonoVar} crossOrigin="anonymous" />
{/* stitches SSR: https://stitches.dev/blog/using-nextjs-with-stitches#step-3-ssr */}
<style id="stitches" dangerouslySetInnerHTML={{ __html: getCssText() }} />
</Head>
<body>
<Main />

View File

@ -3,6 +3,18 @@ import Content from "../components/Content/Content";
import PageTitle from "../components/PageTitle/PageTitle";
import Link from "../components/Link/Link";
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 = () => (
<>
@ -15,8 +27,7 @@ const Contact = () => (
<PageTitle>📬 Contact Me</PageTitle>
<Content>
<div className="wrapper">
<Wrapper>
<p>
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{" "}
@ -26,27 +37,13 @@ const Contact = () => (
<p>
🔐 You can grab my public key here:{" "}
<Link href="/pubkey.asc" title="My Public PGP Key" rel="pgpkey authn noopener" forceNewWindow>
<code className="pubkey">6BF3 79D3 6F67 1480 2B0C 9CF2 51E6 9A39</code>
<PubKey>6BF3 79D3 6F67 1480 2B0C 9CF2 51E6 9A39</PubKey>
</Link>
.
</p>
<ContactForm />
</div>
</Content>
<style jsx>{`
.wrapper {
max-width: 600px;
margin: 0 auto;
}
.pubkey {
font-size: 0.925em;
word-spacing: -0.175em;
white-space: normal;
}
`}</style>
</Wrapper>
</>
);

View File

@ -3,9 +3,18 @@ import Content from "../components/Content/Content";
import PageTitle from "../components/PageTitle/PageTitle";
import Link from "../components/Link/Link";
import Video from "../components/Video/Video";
import { styled } from "../lib/styles/stitches.config";
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 = () => (
<>
<NextSeo
@ -27,7 +36,7 @@ const Hillary = () => (
subs="/static/images/hillary/subs.en.vtt"
/>
<p className="copyright">
<Copyright>
Video is property of{" "}
<Link href="https://www.hillaryclinton.com/" style={{ fontWeight: 700 }}>
Hillary for America
@ -41,18 +50,8 @@ const Hillary = () => (
CNN / WarnerMedia
</Link>
. &copy; 2016.
</p>
</Copyright>
</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,15 +1,118 @@
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 = () => (
<>
<Content>
<div>
<h1>
Hi there! I'm Jake. <span className="wave">👋</span>
</h1>
<Wrapper>
<H1>
Hi there! I'm Jake. <Wave>👋</Wave>
</H1>
<h2>
<H2>
I'm a frontend web developer based in{" "}
<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"
@ -20,9 +123,9 @@ const Index = () => (
Boston
</ColorfulLink>
.
</h2>
</H2>
<p>
<Paragraph>
I specialize in{" "}
<ColorfulLink
href="https://stackoverflow.blog/2018/01/11/brutal-lifecycle-javascript-frameworks/"
@ -73,18 +176,13 @@ const Index = () => (
Ruby
</ColorfulLink>
, and{" "}
<ColorfulLink
href="https://golang.org/"
title="Golang Official Website"
lightColor="#00acd7"
darkColor="#2ad1fb"
>
<ColorfulLink href="https://golang.org/" title="Golang Official Website" lightColor="#00acd7" darkColor="#2ad1fb">
Go
</ColorfulLink>{" "}
too.
</p>
</Paragraph>
<p>
<Paragraph>
Whenever possible, I also apply my experience in{" "}
<ColorfulLink
href="https://github.com/jakejarvis/awesome-shodan-queries"
@ -104,18 +202,13 @@ const Index = () => (
serverless stacks
</ColorfulLink>
, and{" "}
<ColorfulLink
href="https://xkcd.com/1319/"
title='"Automation" on xkcd'
lightColor="#ff6200"
darkColor="#f46c16"
>
<ColorfulLink href="https://xkcd.com/1319/" title='"Automation" on xkcd' lightColor="#ff6200" darkColor="#f46c16">
DevOps automation
</ColorfulLink>
.
</p>
</Paragraph>
<p>
<Paragraph>
I fell in love with{" "}
<ColorfulLink
href="/previously/"
@ -135,20 +228,18 @@ const Index = () => (
backend programming
</ColorfulLink>{" "}
back when my only source of income was{" "}
<span className="birthday">
<ColorfulLink
<EasterEgg
href="/birthday/"
title="🎉 Cranky Birthday Boy on VHS Tape 📼"
lightColor="#e40088"
darkColor="#fd40b1"
>
the Tooth Fairy
</ColorfulLink>
</span>
. <span className="quiet">I've improved a bit since then, I think...</span>
</p>
</EasterEgg>
. <Quiet>I've improved a bit since then, I think...</Quiet>
</Paragraph>
<p>
<Paragraph>
Over the years, some of my side projects{" "}
<ColorfulLink
href="https://tuftsdaily.com/news/2012/04/06/student-designs-iphone-joeytracker-app/"
@ -207,9 +298,9 @@ const Index = () => (
outlets
</ColorfulLink>
.
</p>
</Paragraph>
<p>
<Paragraph>
You can find more of my work on{" "}
<ColorfulLink
href="https://github.com/jakejarvis"
@ -232,7 +323,7 @@ const Index = () => (
<ColorfulLink href="/contact/" title="Send an email" lightColor="#de0c0c" darkColor="#ff5050">
email
</ColorfulLink>{" "}
<sup className="monospace pgp_key">
<PGPKey>
<ColorfulLink
href="/pubkey.asc"
rel="pgpkey authn noopener"
@ -242,9 +333,9 @@ const Index = () => (
style={{ background: "none" }}
forceNewWindow
>
🔐 2B0C 9CF2 51E6 9A39
<code>🔐 2B0C 9CF2 51E6 9A39</code>
</ColorfulLink>
</sup>
</PGPKey>
,{" "}
<ColorfulLink
href="https://twitter.com/jakejarvis"
@ -264,103 +355,8 @@ const Index = () => (
SMS
</ColorfulLink>{" "}
as well!
</p>
</div>
</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>
</>
</Paragraph>
</Wrapper>
);
export default Index;

View File

@ -3,9 +3,18 @@ import Content from "../components/Content/Content";
import PageTitle from "../components/PageTitle/PageTitle";
import Link from "../components/Link/Link";
import Video from "../components/Video/Video";
import { styled } from "../lib/styles/stitches.config";
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 = () => (
<>
<NextSeo
@ -28,7 +37,7 @@ const Leo = () => (
subs="/static/images/leo/subs.en.vtt"
/>
<p className="copyright">
<Copyright>
Video is property of{" "}
<Link href="https://web.archive.org/web/20070511004304/http://www.g4techtv.ca/" style={{ fontWeight: 700 }}>
G4techTV Canada
@ -38,18 +47,8 @@ const Leo = () => (
Leo Laporte
</Link>
. &copy; 2007 G4 Media, Inc.
</p>
</Copyright>
</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 */
import Head from "next/head";
import { NextSeo } from "next-seo";
import Content from "../components/Content/Content";
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_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 = () => (
<>
<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
title="Previously on..."
description="An incredibly embarrassing and somewhat painful trip down this site's memory lane..."
@ -152,62 +211,6 @@ const Previously = () => (
)
</Figure>
</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 RepositoryCard from "../components/RepositoryCard/RepositoryCard";
import { OctocatOcticon } from "../components/Icons";
import { styled } from "../lib/styles/stitches.config";
import { authorSocial } from "../lib/config";
import type { GetStaticProps } from "next";
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 }) => (
<>
<NextSeo
@ -21,56 +53,18 @@ const Projects = ({ repos }) => (
<PageTitle>💾 Projects</PageTitle>
<Content>
<div className="wrapper">
<Wrapper>
{repos.map((repo: RepositoryType) => (
<div key={repo.name} className="card">
<RepositoryCard {...repo} />
</div>
<Card key={repo.name} {...repo} />
))}
</div>
</Wrapper>
<p className="view_more">
<ViewMore>
<Link href={`https://github.com/${authorSocial.github}`}>
View more on{" "}
<OctocatOcticon
fill="currentColor"
style={{
color: "var(--text)",
width: "1.2em",
height: "1.2em",
verticalAlign: "-0.2em",
margin: "0 0.15em",
}}
/>{" "}
GitHub...
View more on <GitHubLogo /> GitHub...
</Link>
</p>
</ViewMore>
</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
image={randomTile}
tile
style={{
css={{
display: "flex",
justifyContent: "center",
alignItems: "center",

View File

@ -29,9 +29,9 @@
# TECHNOLOGY
- Next.js
- React
- Next.js & React
- Vercel
- Stitches
- Giscus
- Fathom Analytics
- ...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