init
This commit is contained in:
		
							
								
								
									
										56
									
								
								src/components/CallToAction.astro
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								src/components/CallToAction.astro
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,56 @@
 | 
			
		||||
---
 | 
			
		||||
interface Props {
 | 
			
		||||
	href: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const { href } = Astro.props;
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
<a href={href}><slot /></a>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
	a {
 | 
			
		||||
		position: relative;
 | 
			
		||||
		display: flex;
 | 
			
		||||
		place-content: center;
 | 
			
		||||
		text-align: center;
 | 
			
		||||
		padding: 0.56em 2em;
 | 
			
		||||
		gap: 0.8em;
 | 
			
		||||
		color: var(--accent-text-over);
 | 
			
		||||
		text-decoration: none;
 | 
			
		||||
		line-height: 1.1;
 | 
			
		||||
		border-radius: 999rem;
 | 
			
		||||
		overflow: hidden;
 | 
			
		||||
		background: var(--gradient-accent-orange);
 | 
			
		||||
		box-shadow: var(--shadow-md);
 | 
			
		||||
		white-space: nowrap;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@media (min-width: 20em) {
 | 
			
		||||
		a {
 | 
			
		||||
			font-size: var(--text-lg);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/* Overlay for hover effects. */
 | 
			
		||||
	a::after {
 | 
			
		||||
		content: '';
 | 
			
		||||
		position: absolute;
 | 
			
		||||
		inset: 0;
 | 
			
		||||
		pointer-events: none;
 | 
			
		||||
		transition: background-color var(--theme-transition);
 | 
			
		||||
		mix-blend-mode: overlay;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	a:focus::after,
 | 
			
		||||
	a:hover::after {
 | 
			
		||||
		background-color: hsla(var(--gray-999-basis), 0.3);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@media (min-width: 50em) {
 | 
			
		||||
		a {
 | 
			
		||||
			padding: 1.125rem 2.5rem;
 | 
			
		||||
			font-size: var(--text-xl);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										46
									
								
								src/components/ContactCTA.astro
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/components/ContactCTA.astro
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,46 @@
 | 
			
		||||
---
 | 
			
		||||
import CallToAction from './CallToAction.astro';
 | 
			
		||||
import Icon from './Icon.astro';
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
<aside>
 | 
			
		||||
	<h2>Interested in working together?</h2>
 | 
			
		||||
	<CallToAction href="mailto:me@example.com">
 | 
			
		||||
		Send Me a Message
 | 
			
		||||
		<Icon icon="paper-plane-tilt" size="1.2em" />
 | 
			
		||||
	</CallToAction>
 | 
			
		||||
</aside>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
	aside {
 | 
			
		||||
		display: flex;
 | 
			
		||||
		flex-direction: column;
 | 
			
		||||
		align-items: center;
 | 
			
		||||
		gap: 3rem;
 | 
			
		||||
		border-top: 1px solid var(--gray-800);
 | 
			
		||||
		border-bottom: 1px solid var(--gray-800);
 | 
			
		||||
		padding: 5rem 1.5rem;
 | 
			
		||||
		background-color: var(--gray-999_40);
 | 
			
		||||
		box-shadow: var(--shadow-sm);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	h2 {
 | 
			
		||||
		font-size: var(--text-xl);
 | 
			
		||||
		text-align: center;
 | 
			
		||||
		max-width: 15ch;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@media (min-width: 50em) {
 | 
			
		||||
		aside {
 | 
			
		||||
			padding: 7.5rem;
 | 
			
		||||
			flex-direction: row;
 | 
			
		||||
			flex-wrap: wrap;
 | 
			
		||||
			justify-content: space-between;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		h2 {
 | 
			
		||||
			font-size: var(--text-3xl);
 | 
			
		||||
			text-align: left;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										74
									
								
								src/components/Footer.astro
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								src/components/Footer.astro
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,74 @@
 | 
			
		||||
---
 | 
			
		||||
import Icon from './Icon.astro';
 | 
			
		||||
const currentYear = new Date().getFullYear();
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
<footer>
 | 
			
		||||
	<div class="group">
 | 
			
		||||
		<p>
 | 
			
		||||
			Designed & Developed in Portland with <a href="https://astro.build/">Astro</a>
 | 
			
		||||
			<Icon icon="rocket-launch" size="1.2em" />
 | 
			
		||||
		</p>
 | 
			
		||||
		<p>© {currentYear} Jeanine White</p>
 | 
			
		||||
	</div>
 | 
			
		||||
	<p class="socials">
 | 
			
		||||
		<a href="https://twitter.com/me"> Twitter</a>
 | 
			
		||||
		<a href="https://github.com/me"> GitHub</a>
 | 
			
		||||
		<a href="https://codepen.io/me"> CodePen</a>
 | 
			
		||||
	</p>
 | 
			
		||||
</footer>
 | 
			
		||||
<style>
 | 
			
		||||
	footer {
 | 
			
		||||
		display: flex;
 | 
			
		||||
		flex-direction: column;
 | 
			
		||||
		gap: 3rem;
 | 
			
		||||
		margin-top: auto;
 | 
			
		||||
		padding: 3rem 2rem 3rem;
 | 
			
		||||
		text-align: center;
 | 
			
		||||
		color: var(--gray-400);
 | 
			
		||||
		font-size: var(--text-sm);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	footer a {
 | 
			
		||||
		color: var(--gray-400);
 | 
			
		||||
		text-decoration: 1px solid underline transparent;
 | 
			
		||||
		text-underline-offset: 0.25em;
 | 
			
		||||
		transition: text-decoration-color var(--theme-transition);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	footer a:hover,
 | 
			
		||||
	footer a:focus {
 | 
			
		||||
		text-decoration-color: currentColor;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.group {
 | 
			
		||||
		display: flex;
 | 
			
		||||
		flex-direction: column;
 | 
			
		||||
		gap: 0.5rem;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.socials {
 | 
			
		||||
		display: flex;
 | 
			
		||||
		justify-content: center;
 | 
			
		||||
		gap: 1rem;
 | 
			
		||||
		flex-wrap: wrap;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@media (min-width: 50em) {
 | 
			
		||||
		footer {
 | 
			
		||||
			flex-direction: row;
 | 
			
		||||
			justify-content: space-between;
 | 
			
		||||
			padding: 2.5rem 5rem;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		.group {
 | 
			
		||||
			flex-direction: row;
 | 
			
		||||
			gap: 1rem;
 | 
			
		||||
			flex-wrap: wrap;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		.socials {
 | 
			
		||||
			justify-content: flex-end;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										65
									
								
								src/components/Grid.astro
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								src/components/Grid.astro
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,65 @@
 | 
			
		||||
---
 | 
			
		||||
interface Props {
 | 
			
		||||
	variant?: 'offset' | 'small';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const { variant } = Astro.props;
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
<ul class:list={['grid', { offset: variant === 'offset', small: variant === 'small' }]}>
 | 
			
		||||
	<slot />
 | 
			
		||||
</ul>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
	.grid {
 | 
			
		||||
		display: grid;
 | 
			
		||||
		grid-auto-rows: 1fr;
 | 
			
		||||
		gap: 1rem;
 | 
			
		||||
		list-style: none;
 | 
			
		||||
		padding: 0;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.grid.small {
 | 
			
		||||
		grid-template-columns: 1fr 1fr;
 | 
			
		||||
		gap: 1.5rem;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/* If last row contains only one item, make it span both columns. */
 | 
			
		||||
	.grid.small > :global(:last-child:nth-child(odd)) {
 | 
			
		||||
		grid-column: 1 / 3;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@media (min-width: 50em) {
 | 
			
		||||
		.grid {
 | 
			
		||||
			grid-template-columns: 1fr 1fr;
 | 
			
		||||
			gap: 4rem;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		.grid.offset {
 | 
			
		||||
			--row-offset: 7.5rem;
 | 
			
		||||
			padding-bottom: var(--row-offset);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/* Shift first item in each row vertically to create staggered effect. */
 | 
			
		||||
		.grid.offset > :global(:nth-child(odd)) {
 | 
			
		||||
			transform: translateY(var(--row-offset));
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/* If last row contains only one item, display it in the second column. */
 | 
			
		||||
		.grid.offset > :global(:last-child:nth-child(odd)) {
 | 
			
		||||
			grid-column: 2 / 3;
 | 
			
		||||
			transform: none;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		.grid.small {
 | 
			
		||||
			display: flex;
 | 
			
		||||
			flex-wrap: wrap;
 | 
			
		||||
			justify-content: center;
 | 
			
		||||
			gap: 2rem;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		.grid.small > :global(*) {
 | 
			
		||||
			flex-basis: 20rem;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										54
									
								
								src/components/Hero.astro
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								src/components/Hero.astro
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,54 @@
 | 
			
		||||
---
 | 
			
		||||
interface Props {
 | 
			
		||||
	title: string;
 | 
			
		||||
	tagline?: string;
 | 
			
		||||
	align?: 'start' | 'center';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const { align = 'center', tagline, title } = Astro.props;
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
<div class:list={['hero stack gap-4', align]}>
 | 
			
		||||
	<div class="stack gap-2">
 | 
			
		||||
		<h1 class="title">{title}</h1>
 | 
			
		||||
		{tagline && <p class="tagline">{tagline}</p>}
 | 
			
		||||
	</div>
 | 
			
		||||
	<slot />
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
	.hero {
 | 
			
		||||
		font-size: var(--text-lg);
 | 
			
		||||
		text-align: center;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.title,
 | 
			
		||||
	.tagline {
 | 
			
		||||
		max-width: 37ch;
 | 
			
		||||
		margin-inline: auto;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.title {
 | 
			
		||||
		font-size: var(--text-3xl);
 | 
			
		||||
		color: var(--gray-0);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@media (min-width: 50em) {
 | 
			
		||||
		.hero {
 | 
			
		||||
			font-size: var(--text-xl);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		.start {
 | 
			
		||||
			text-align: start;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		.start .title,
 | 
			
		||||
		.start .tagline {
 | 
			
		||||
			margin-inline: unset;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		.title {
 | 
			
		||||
			font-size: var(--text-5xl);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										56
									
								
								src/components/Icon.astro
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								src/components/Icon.astro
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,56 @@
 | 
			
		||||
---
 | 
			
		||||
import type { HTMLAttributes } from 'astro/types';
 | 
			
		||||
import { iconPaths } from './IconPaths';
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
	icon: keyof typeof iconPaths;
 | 
			
		||||
	color?: string;
 | 
			
		||||
	gradient?: boolean;
 | 
			
		||||
	size?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const { color = 'currentcolor', gradient, icon, size } = Astro.props;
 | 
			
		||||
const iconPath = iconPaths[icon];
 | 
			
		||||
 | 
			
		||||
const attrs: HTMLAttributes<'svg'> = {};
 | 
			
		||||
if (size) attrs.style = { '--size': size };
 | 
			
		||||
 | 
			
		||||
const gradientId = 'icon-gradient-' + Math.round(Math.random() * 10e12).toString(36);
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
<svg
 | 
			
		||||
	xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
	width="40"
 | 
			
		||||
	height="40"
 | 
			
		||||
	viewBox="0 0 256 256"
 | 
			
		||||
	aria-hidden="true"
 | 
			
		||||
	stroke={gradient ? `url(#${gradientId})` : color}
 | 
			
		||||
	fill={gradient ? `url(#${gradientId})` : color}
 | 
			
		||||
	{...attrs}
 | 
			
		||||
>
 | 
			
		||||
	<g set:html={iconPath} />
 | 
			
		||||
	{
 | 
			
		||||
		gradient && (
 | 
			
		||||
			<linearGradient
 | 
			
		||||
				id={gradientId}
 | 
			
		||||
				x1="23"
 | 
			
		||||
				x2="235"
 | 
			
		||||
				y1="43"
 | 
			
		||||
				y2="202"
 | 
			
		||||
				gradientUnits="userSpaceOnUse"
 | 
			
		||||
			>
 | 
			
		||||
				<stop stop-color="var(--gradient-stop-1)" />
 | 
			
		||||
				<stop offset=".5" stop-color="var(--gradient-stop-2)" />
 | 
			
		||||
				<stop offset="1" stop-color="var(--gradient-stop-3)" />
 | 
			
		||||
			</linearGradient>
 | 
			
		||||
		)
 | 
			
		||||
	}
 | 
			
		||||
</svg>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
	svg {
 | 
			
		||||
		vertical-align: middle;
 | 
			
		||||
		width: var(--size, 1em);
 | 
			
		||||
		height: var(--size, 1em);
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										38
									
								
								src/components/IconPaths.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								src/components/IconPaths.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,38 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Icons adapted from https://phosphoricons.com/
 | 
			
		||||
 *
 | 
			
		||||
 * Want to add more?
 | 
			
		||||
 * 1. Find the icon you want on Phosphor Icons.
 | 
			
		||||
 * 2. Click “Copy SVG”.
 | 
			
		||||
 * 3. Paste the SVG code in your editor.
 | 
			
		||||
 * 4. Remove the `<svg>` wrapper so you only have elements like `<path>`, `<circle>`, `<rect>` etc.
 | 
			
		||||
 * 5. Remove any `stroke="#000000"` attributes
 | 
			
		||||
 * 6. Replace any `fill="#000000"` attributes with `stroke="none"`
 | 
			
		||||
 *    (or add `stroke="none"` on shapes with no `fill` or `stroke` specified).
 | 
			
		||||
 */
 | 
			
		||||
export const iconPaths = {
 | 
			
		||||
	'terminal-window': `<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="m80 96 40 32-40 32m56 0h40"/><rect width="192" height="160" x="32" y="48" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16.97" rx="8.5"/>`,
 | 
			
		||||
	trophy: `<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M56 56v55.1c0 39.7 31.8 72.6 71.5 72.9a72 72 0 0 0 72.5-72V56a8 8 0 0 0-8-8H64a8 8 0 0 0-8 8Zm40 168h64m-32-40v40"/><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M198.2 128h9.8a32 32 0 0 0 32-32V80a8 8 0 0 0-8-8h-32M58 128H47.9a32 32 0 0 1-32-32V80a8 8 0 0 1 8-8h32"/>`,
 | 
			
		||||
	strategy: `<circle cx="68" cy="188" r="28" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="m40 72 40 40m0-40-40 40m136 56 40 40m0-40-40 40M136 80V40h40"/><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="m136 40 16 16c40 40 8 88-24 96"/>`,
 | 
			
		||||
	'paper-plane-tilt': `<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M210.3 35.9 23.9 88.4a8 8 0 0 0-1.2 15l85.6 40.5a7.8 7.8 0 0 1 3.8 3.8l40.5 85.6a8 8 0 0 0 15-1.2l52.5-186.4a7.9 7.9 0 0 0-9.8-9.8Zm-99.4 109.2 45.2-45.2"/>`,
 | 
			
		||||
	'arrow-right': `<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M40 128h176m-72-72 72 72-72 72"/>`,
 | 
			
		||||
	'arrow-left': `<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M216 128H40m72-72-72 72 72 72"/>`,
 | 
			
		||||
	code: `<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="m64 88-48 40 48 40m128-80 48 40-48 40M160 40 96 216"/>`,
 | 
			
		||||
	'microphone-stage': `<circle cx="168" cy="88" r="64" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="m213.3 133.3-90.6-90.6M100 156l-12 12m16.8-70.1L28.1 202.5a7.9 7.9 0 0 0 .8 10.4l14.2 14.2a7.9 7.9 0 0 0 10.4.8l104.6-76.7"/>`,
 | 
			
		||||
	'pencil-line': `<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M96 216H48a8 8 0 0 1-8-8v-44.7a7.9 7.9 0 0 1 2.3-5.6l120-120a8 8 0 0 1 11.4 0l44.6 44.6a8 8 0 0 1 0 11.4Zm40-152 56 56"/><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M216 216H96l-55.5-55.5M164 92l-96 96"/>`,
 | 
			
		||||
	'rocket-launch': `<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M94.1 184.6c-11.4 33.9-56.6 33.9-56.6 33.9s0-45.2 33.9-56.6m124.5-56.5L128 173.3 82.7 128l67.9-67.9C176.3 34.4 202 34.7 213 36.3a7.8 7.8 0 0 1 6.7 6.7c1.6 11 1.9 36.7-23.8 62.4Z"/><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M184.6 116.7v64.6a8 8 0 0 1-2.4 5.6l-32.3 32.4a8 8 0 0 1-13.5-4.1l-8.4-41.9m11.3-101.9H74.7a8 8 0 0 0-5.6 2.4l-32.4 32.3a8 8 0 0 0 4.1 13.5l41.9 8.4"/>`,
 | 
			
		||||
	list: `<path stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M40 128h176M40 64h176M40 192h176"/>`,
 | 
			
		||||
	heart: `<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M128 216S28 160 28 92a52 52 0 0 1 100-20h0a52 52 0 0 1 100 20c0 68-100 124-100 124Z"/>`,
 | 
			
		||||
	'moon-stars': `<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M216 112V64m24 24h-48m-24-64v32m16-16h-32m65 113A92 92 0 0 1 103 39h0a92 92 0 1 0 114 114Z"/>`,
 | 
			
		||||
	sun: `<circle cx="128" cy="128" r="60" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M128 36V16M63 63 49 49m-13 79H16m47 65-14 14m79 13v20m65-47 14 14m13-79h20m-47-65 14-14"/>`,
 | 
			
		||||
	'twitter-logo': `<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M128 88c0-22 18.5-40.3 40.5-40a40 40 0 0 1 36.2 24H240l-32.3 32.3A127.9 127.9 0 0 1 80 224c-32 0-40-12-40-12s32-12 48-36c0 0-64-32-48-120 0 0 40 40 88 48Z"/>`,
 | 
			
		||||
	'codepen-logo': `<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="m232 101-104 59-104-59 100.1-56.8a8.3 8.3 0 0 1 7.8 0Z"/><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="m232 165-100.1 56.8a8.3 8.3 0 0 1-7.8 0L24 165l104-59Zm0-64v64M24 101v64m104-5v62.8m0-179.6V106"/>`,
 | 
			
		||||
	'github-logo': `<g stroke-linecap="round" stroke-linejoin="round"><path fill="none" stroke-width="14.7" d="M55.7 167.2c13.9 1 21.3 13.1 22.2 14.6 4.2 7.2 10.4 9.6 18.3 7.1l1.1-3.4a60.3 60.3 0 0 1-25.8-11.9c-12-10.1-18-25.6-18-46.3"/><path fill="none" stroke-width="16" d="M61.4 205.1a24.5 24.5 0 0 1-3-6.1c-3.2-7.9-7.1-10.6-7.8-11.1l-1-.6c-2.4-1.6-9.5-6.5-7.2-13.9 1.4-4.5 6-7.2 12.3-7.2h.8c4 .3 7.6 1.5 10.7 3.2-9.1-10.1-13.6-24.3-13.6-42.3 0-11.3 3.5-21.7 10.1-30.4A46.7 46.7 0 0 1 65 67.3a8.3 8.3 0 0 1 5-4.7c2.8-.9 13.3-2.7 33.2 9.9a105 105 0 0 1 50.5 0c19.9-12.6 30.4-10.8 33.2-9.9 2.3.7 4.1 2.4 5 4.7 5 12.7 4 23.2 2.6 29.4 6.7 8.7 10 18.9 10 30.4 0 42.6-25.8 54.7-43.6 58.7 1.4 4.1 2.2 8.8 2.2 13.7l-.1 23.4v2.3"/><path fill="none" stroke-width="16" d="M160.9 185.7c1.4 4.1 2.2 8.8 2.2 13.7l-.1 23.4v2.3A98.6 98.6 0 1 0 61.4 205c-1.4-2.1-11.3-17.5-11.8-17.8-2.4-1.6-9.5-6.5-7.2-13.9 1.4-4.5 6-7.2 12.3-7.2h.8c4 .3 7.6 1.5 10.7 3.2-9.1-10.1-13.6-24.3-13.6-42.3 0-11.3 3.5-21.7 10.1-30.4A46.4 46.4 0 0 1 65 67.3a8.3 8.3 0 0 1 5-4.7c2.8-.9 13.3-2.7 33.2 9.9a105 105 0 0 1 50.5 0c19.9-12.6 30.4-10.8 33.2-9.9 2.3.7 4.1 2.4 5 4.7 5 12.7 4 23.2 2.6 29.4 6.7 8.7 10 18.9 10 30.4.1 42.6-25.8 54.7-43.6 58.6z"/><path fill="none" stroke-width="18.7" d="m170.1 203.3 17.3-12 17.2-18.7 9.5-26.6v-27.9l-9.5-27.5" /><path fill="none" stroke-width="22.7" d="m92.1 57.3 23.3-4.6 18.7-1.4 29.3 5.4m-110 32.6-8 16-4 21.4.6 20.3 3.4 13" /><path fill="none" stroke-width="13.3" d="M28.8 133a100 100 0 0 0 66.9 94.4v-8.7c-22.4 1.8-33-11.5-35.6-19.8-3.4-8.6-7.8-11.4-8.5-11.8"/></g>`,
 | 
			
		||||
	'twitch-logo': `<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M165 200h-42a8 8 0 0 0-5 2l-46 38v-40H48a8 8 0 0 1-8-8V48a8 8 0 0 1 8-8h160a8 8 0 0 1 8 8v108a8 8 0 0 1-3 6l-43 36a8 8 0 0 1-5 2Zm3-112v48m-48-48v48"/>`,
 | 
			
		||||
	'youtube-logo': `<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="m160 128-48-32v64l48-32z"/><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M24 128c0 30 3 47 5 56a16 16 0 0 0 10 11c34 13 89 13 89 13s56 0 89-13a16 16 0 0 0 10-11c2-9 5-26 5-56s-3-47-5-56a16 16 0 0 0-10-11c-33-13-89-13-89-13s-55 0-89 13a16 16 0 0 0-10 11c-2 9-5 26-5 56Z"/>`,
 | 
			
		||||
	'dribbble-logo': `<circle cx="128" cy="128" r="96" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M71 205a160 160 0 0 1 137-77l16 1m-36-76a160 160 0 0 1-124 59 165 165 0 0 1-30-3"/><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M86 42a161 161 0 0 1 74 177"/>`,
 | 
			
		||||
	'discord-logo': `<circle stroke="none" cx="96" cy="144" r="12"/><circle stroke="none" cx="160" cy="144" r="12"/><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M74 80a175 175 0 0 1 54-8 175 175 0 0 1 54 8m0 96a175 175 0 0 1-54 8 175 175 0 0 1-54-8"/><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="m155 182 12 24a8 8 0 0 0 9 4c25-6 46-16 61-30a8 8 0 0 0 3-8L206 59a8 8 0 0 0-5-5 176 176 0 0 0-30-9 8 8 0 0 0-9 5l-8 24m-53 108-12 24a8 8 0 0 1-9 4c-25-6-46-16-61-30a8 8 0 0 1-3-8L50 59a8 8 0 0 1 5-5 176 176 0 0 1 30-9 8 8 0 0 1 9 5l8 24"/>`,
 | 
			
		||||
	'linkedin-logo': `<rect width="184" height="184" x="36" y="36" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" rx="8"/><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M120 112v64m-32-64v64m32-36a28 28 0 0 1 56 0v36"/><circle stroke="none" cx="88" cy="80" r="12"/>`,
 | 
			
		||||
	'instagram-logo': `<circle cx="128" cy="128" r="40" fill="none" stroke-miterlimit="10" stroke-width="16"/><rect width="184" height="184" x="36" y="36" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" rx="48"/><circle cx="180" cy="76" r="12" stroke="none" />`,
 | 
			
		||||
	'tiktok-logo': `<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M168 106a96 96 0 0 0 56 18V84a56 56 0 0 1-56-56h-40v128a28 28 0 1 1-40-25V89a68 68 0 1 0 80 67Z"/>`,
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										47
									
								
								src/components/MainHead.astro
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								src/components/MainHead.astro
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,47 @@
 | 
			
		||||
---
 | 
			
		||||
import '../styles/global.css';
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
	title?: string | undefined;
 | 
			
		||||
	description?: string | undefined;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const {
 | 
			
		||||
	title = 'Jeanine White: Personal Site',
 | 
			
		||||
	description = 'The personal site of Jeanine White',
 | 
			
		||||
} = Astro.props;
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
<meta charset="UTF-8" />
 | 
			
		||||
<meta name="description" property="og:description" content={description} />
 | 
			
		||||
<meta name="viewport" content="width=device-width" />
 | 
			
		||||
<meta name="generator" content={Astro.generator} />
 | 
			
		||||
<title>{title}</title>
 | 
			
		||||
 | 
			
		||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
 | 
			
		||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
 | 
			
		||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
 | 
			
		||||
<link
 | 
			
		||||
	href="https://fonts.googleapis.com/css2?family=Public+Sans:ital,wght@0,400;0,700;1,400&family=Rubik:wght@500;600&display=swap"
 | 
			
		||||
	rel="stylesheet"
 | 
			
		||||
/>
 | 
			
		||||
<script is:inline>
 | 
			
		||||
	// This code is inlined in the head to make dark mode instant & blocking.
 | 
			
		||||
	const getThemePreference = () => {
 | 
			
		||||
		if (typeof localStorage !== 'undefined' && localStorage.getItem('theme')) {
 | 
			
		||||
			return localStorage.getItem('theme');
 | 
			
		||||
		}
 | 
			
		||||
		return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
 | 
			
		||||
	};
 | 
			
		||||
	const isDark = getThemePreference() === 'dark';
 | 
			
		||||
	document.documentElement.classList[isDark ? 'add' : 'remove']('theme-dark');
 | 
			
		||||
 | 
			
		||||
	if (typeof localStorage !== 'undefined') {
 | 
			
		||||
		// Watch the document element and persist user preference when it changes.
 | 
			
		||||
		const observer = new MutationObserver(() => {
 | 
			
		||||
			const isDark = document.documentElement.classList.contains('theme-dark');
 | 
			
		||||
			localStorage.setItem('theme', isDark ? 'dark' : 'light');
 | 
			
		||||
		});
 | 
			
		||||
		observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
 | 
			
		||||
	}
 | 
			
		||||
</script>
 | 
			
		||||
							
								
								
									
										367
									
								
								src/components/Nav.astro
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										367
									
								
								src/components/Nav.astro
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,367 @@
 | 
			
		||||
---
 | 
			
		||||
import Icon from './Icon.astro';
 | 
			
		||||
import ThemeToggle from './ThemeToggle.astro';
 | 
			
		||||
import type { iconPaths } from './IconPaths';
 | 
			
		||||
 | 
			
		||||
/** Main menu items */
 | 
			
		||||
const textLinks: { label: string; href: string }[] = [
 | 
			
		||||
	{ label: 'Home', href: '/' },
 | 
			
		||||
	{ label: 'Work', href: '/work/' },
 | 
			
		||||
	{ label: 'About', href: '/about/' },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
/** Icon links to social media — edit these with links to your profiles! */
 | 
			
		||||
const iconLinks: { label: string; href: string; icon: keyof typeof iconPaths }[] = [
 | 
			
		||||
	{ label: 'Twitter', href: 'https://twitter.com/me', icon: 'twitter-logo' },
 | 
			
		||||
	{ label: 'Twitch', href: 'https://twitch.tv/me', icon: 'twitch-logo' },
 | 
			
		||||
	{ label: 'GitHub', href: 'https://github.com/me', icon: 'github-logo' },
 | 
			
		||||
	{ label: 'CodePen', href: 'https://codepen.io/me', icon: 'codepen-logo' },
 | 
			
		||||
	{ label: 'dribbble', href: 'https://dribbble.com/me', icon: 'dribbble-logo' },
 | 
			
		||||
	{ label: 'YouTube', href: 'https://www.youtube.com/@me/', icon: 'youtube-logo' },
 | 
			
		||||
];
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
<nav>
 | 
			
		||||
	<div class="menu-header">
 | 
			
		||||
		<a href="/" class="site-title">
 | 
			
		||||
			<Icon icon="terminal-window" color="var(--accent-regular)" size="1.6em" gradient />
 | 
			
		||||
			Jeanine White
 | 
			
		||||
		</a>
 | 
			
		||||
		<menu-button>
 | 
			
		||||
			<template>
 | 
			
		||||
				<button class="menu-button" aria-expanded="false">
 | 
			
		||||
					<span class="sr-only">Menu</span>
 | 
			
		||||
					<Icon icon="list" />
 | 
			
		||||
				</button>
 | 
			
		||||
			</template>
 | 
			
		||||
		</menu-button>
 | 
			
		||||
	</div>
 | 
			
		||||
	<noscript>
 | 
			
		||||
		<ul class="nav-items">
 | 
			
		||||
			{
 | 
			
		||||
				textLinks.map(({ label, href }) => (
 | 
			
		||||
					<li>
 | 
			
		||||
						<a
 | 
			
		||||
							aria-current={Astro.url.pathname === href}
 | 
			
		||||
							class:list={[
 | 
			
		||||
								'link',
 | 
			
		||||
								{
 | 
			
		||||
									active:
 | 
			
		||||
										Astro.url.pathname === href ||
 | 
			
		||||
										(href !== '/' && Astro.url.pathname.startsWith(href)),
 | 
			
		||||
								},
 | 
			
		||||
							]}
 | 
			
		||||
							href={href}
 | 
			
		||||
						>
 | 
			
		||||
							{label}
 | 
			
		||||
						</a>
 | 
			
		||||
					</li>
 | 
			
		||||
				))
 | 
			
		||||
			}
 | 
			
		||||
		</ul>
 | 
			
		||||
	</noscript>
 | 
			
		||||
	<noscript>
 | 
			
		||||
		<div class="menu-footer">
 | 
			
		||||
			<div class="socials">
 | 
			
		||||
				{
 | 
			
		||||
					iconLinks.map(({ href, icon, label }) => (
 | 
			
		||||
						<a href={href} class="social">
 | 
			
		||||
							<span class="sr-only">{label}</span>
 | 
			
		||||
							<Icon icon={icon} />
 | 
			
		||||
						</a>
 | 
			
		||||
					))
 | 
			
		||||
				}
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	</noscript>
 | 
			
		||||
	<div id="menu-content" hidden>
 | 
			
		||||
		<ul class="nav-items">
 | 
			
		||||
			{
 | 
			
		||||
				textLinks.map(({ label, href }) => (
 | 
			
		||||
					<li>
 | 
			
		||||
						<a
 | 
			
		||||
							aria-current={Astro.url.pathname === href}
 | 
			
		||||
							class:list={[
 | 
			
		||||
								'link',
 | 
			
		||||
								{
 | 
			
		||||
									active:
 | 
			
		||||
										Astro.url.pathname === href ||
 | 
			
		||||
										(href !== '/' && Astro.url.pathname.startsWith(href)),
 | 
			
		||||
								},
 | 
			
		||||
							]}
 | 
			
		||||
							href={href}
 | 
			
		||||
						>
 | 
			
		||||
							{label}
 | 
			
		||||
						</a>
 | 
			
		||||
					</li>
 | 
			
		||||
				))
 | 
			
		||||
			}
 | 
			
		||||
		</ul>
 | 
			
		||||
		<div class="menu-footer">
 | 
			
		||||
			<div class="socials">
 | 
			
		||||
				{
 | 
			
		||||
					iconLinks.map(({ href, icon, label }) => (
 | 
			
		||||
						<a href={href} class="social">
 | 
			
		||||
							<span class="sr-only">{label}</span>
 | 
			
		||||
							<Icon icon={icon} />
 | 
			
		||||
						</a>
 | 
			
		||||
					))
 | 
			
		||||
				}
 | 
			
		||||
			</div>
 | 
			
		||||
 | 
			
		||||
			<div class="theme-toggle">
 | 
			
		||||
				<ThemeToggle />
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
</nav>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
	class MenuButton extends HTMLElement {
 | 
			
		||||
		constructor() {
 | 
			
		||||
			super();
 | 
			
		||||
 | 
			
		||||
			// Inject menu toggle button when JS runs.
 | 
			
		||||
			this.appendChild(this.querySelector('template')!.content.cloneNode(true));
 | 
			
		||||
			const btn = this.querySelector('button')!;
 | 
			
		||||
 | 
			
		||||
			// Hide menu (shown by default to support no-JS browsers).
 | 
			
		||||
			const menu = document.getElementById('menu-content')!;
 | 
			
		||||
			menu.hidden = true;
 | 
			
		||||
			// Add "menu-content" class in JS to avoid covering content in non-JS browsers.
 | 
			
		||||
			menu.classList.add('menu-content');
 | 
			
		||||
 | 
			
		||||
			/** Set whether the menu is currently expanded or collapsed. */
 | 
			
		||||
			const setExpanded = (expand: boolean) => {
 | 
			
		||||
				btn.setAttribute('aria-expanded', expand ? 'true' : 'false');
 | 
			
		||||
				menu.hidden = !expand;
 | 
			
		||||
			};
 | 
			
		||||
 | 
			
		||||
			// Toggle menu visibility when the menu button is clicked.
 | 
			
		||||
			btn.addEventListener('click', () => setExpanded(menu.hidden));
 | 
			
		||||
 | 
			
		||||
			// Hide menu button for large screens.
 | 
			
		||||
			const handleViewports = (e: MediaQueryList | MediaQueryListEvent) => {
 | 
			
		||||
				setExpanded(e.matches);
 | 
			
		||||
				btn.hidden = e.matches;
 | 
			
		||||
			};
 | 
			
		||||
			const mediaQueries = window.matchMedia('(min-width: 50em)');
 | 
			
		||||
			handleViewports(mediaQueries);
 | 
			
		||||
			mediaQueries.addEventListener('change', handleViewports);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	customElements.define('menu-button', MenuButton);
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
	nav {
 | 
			
		||||
		z-index: 9999;
 | 
			
		||||
		position: relative;
 | 
			
		||||
		font-family: var(--font-brand);
 | 
			
		||||
		font-weight: 500;
 | 
			
		||||
		margin-bottom: 3.5rem;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.menu-header {
 | 
			
		||||
		display: flex;
 | 
			
		||||
		justify-content: space-between;
 | 
			
		||||
		gap: 0.5rem;
 | 
			
		||||
		padding: 1.5rem;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.site-title {
 | 
			
		||||
		display: flex;
 | 
			
		||||
		gap: 0.5rem;
 | 
			
		||||
		align-items: center;
 | 
			
		||||
		line-height: 1.1;
 | 
			
		||||
		color: var(--gray-0);
 | 
			
		||||
		text-decoration: none;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.menu-button {
 | 
			
		||||
		position: relative;
 | 
			
		||||
		display: flex;
 | 
			
		||||
		border: 0;
 | 
			
		||||
		border-radius: 999rem;
 | 
			
		||||
		padding: 0.5rem;
 | 
			
		||||
		font-size: 1.5rem;
 | 
			
		||||
		color: var(--gray-300);
 | 
			
		||||
		background: radial-gradient(var(--gray-900), var(--gray-800) 150%);
 | 
			
		||||
		box-shadow: var(--shadow-md);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.menu-button[aria-expanded='true'] {
 | 
			
		||||
		color: var(--gray-0);
 | 
			
		||||
		background: linear-gradient(180deg, var(--gray-600), transparent),
 | 
			
		||||
			radial-gradient(var(--gray-900), var(--gray-800) 150%);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.menu-button[hidden] {
 | 
			
		||||
		display: none;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.menu-button::before {
 | 
			
		||||
		position: absolute;
 | 
			
		||||
		inset: -1px;
 | 
			
		||||
		content: '';
 | 
			
		||||
		background: var(--gradient-stroke);
 | 
			
		||||
		border-radius: 999rem;
 | 
			
		||||
		z-index: -1;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.menu-content {
 | 
			
		||||
		position: absolute;
 | 
			
		||||
		left: 0;
 | 
			
		||||
		right: 0;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.nav-items {
 | 
			
		||||
		margin: 0;
 | 
			
		||||
		display: flex;
 | 
			
		||||
		flex-direction: column;
 | 
			
		||||
		gap: 1rem;
 | 
			
		||||
		font-size: var(--text-md);
 | 
			
		||||
		line-height: 1.2;
 | 
			
		||||
		list-style: none;
 | 
			
		||||
		padding: 2rem;
 | 
			
		||||
		background-color: var(--gray-999);
 | 
			
		||||
		border-bottom: 1px solid var(--gray-800);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.link {
 | 
			
		||||
		display: inline-block;
 | 
			
		||||
		color: var(--gray-300);
 | 
			
		||||
		text-decoration: none;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.link.active {
 | 
			
		||||
		color: var(--gray-0);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.menu-footer {
 | 
			
		||||
		--icon-size: var(--text-xl);
 | 
			
		||||
		--icon-padding: 0.5rem;
 | 
			
		||||
 | 
			
		||||
		display: flex;
 | 
			
		||||
		justify-content: space-between;
 | 
			
		||||
		gap: 0.75rem;
 | 
			
		||||
		padding: 1.5rem 2rem 1.5rem 1.5rem;
 | 
			
		||||
		background-color: var(--gray-999);
 | 
			
		||||
		border-radius: 0 0 0.75rem 0.75rem;
 | 
			
		||||
		box-shadow: var(--shadow-lg);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.socials {
 | 
			
		||||
		display: flex;
 | 
			
		||||
		flex-wrap: wrap;
 | 
			
		||||
		gap: 0.625rem;
 | 
			
		||||
		font-size: var(--icon-size);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.social {
 | 
			
		||||
		display: flex;
 | 
			
		||||
		padding: var(--icon-padding);
 | 
			
		||||
		text-decoration: none;
 | 
			
		||||
		color: var(--accent-dark);
 | 
			
		||||
		transition: color var(--theme-transition);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.social:hover,
 | 
			
		||||
	.social:focus {
 | 
			
		||||
		color: var(--accent-text-over);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.theme-toggle {
 | 
			
		||||
		display: flex;
 | 
			
		||||
		align-items: center;
 | 
			
		||||
		height: calc(var(--icon-size) + 2 * var(--icon-padding));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@media (min-width: 50em) {
 | 
			
		||||
		nav {
 | 
			
		||||
			display: grid;
 | 
			
		||||
			grid-template-columns: 1fr auto 1fr;
 | 
			
		||||
			align-items: center;
 | 
			
		||||
			padding: 2.5rem 5rem;
 | 
			
		||||
			gap: 1rem;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		.menu-header {
 | 
			
		||||
			padding: 0;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		.site-title {
 | 
			
		||||
			font-size: var(--text-lg);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		.menu-content {
 | 
			
		||||
			display: contents;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		.nav-items {
 | 
			
		||||
			position: relative;
 | 
			
		||||
			flex-direction: row;
 | 
			
		||||
			font-size: var(--text-sm);
 | 
			
		||||
			border-radius: 999rem;
 | 
			
		||||
			border: 0;
 | 
			
		||||
			padding: 0.5rem 0.5625rem;
 | 
			
		||||
			background: radial-gradient(var(--gray-900), var(--gray-800) 150%);
 | 
			
		||||
			box-shadow: var(--shadow-md);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		.nav-items::before {
 | 
			
		||||
			position: absolute;
 | 
			
		||||
			inset: -1px;
 | 
			
		||||
			content: '';
 | 
			
		||||
			background: var(--gradient-stroke);
 | 
			
		||||
			border-radius: 999rem;
 | 
			
		||||
			z-index: -1;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		.link {
 | 
			
		||||
			padding: 0.5rem 1rem;
 | 
			
		||||
			border-radius: 999rem;
 | 
			
		||||
			transition:
 | 
			
		||||
				color var(--theme-transition),
 | 
			
		||||
				background-color var(--theme-transition);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		.link:hover,
 | 
			
		||||
		.link:focus {
 | 
			
		||||
			color: var(--gray-100);
 | 
			
		||||
			background-color: var(--accent-subtle-overlay);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		.link.active {
 | 
			
		||||
			color: var(--accent-text-over);
 | 
			
		||||
			background-color: var(--accent-regular);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		.menu-footer {
 | 
			
		||||
			--icon-padding: 0.375rem;
 | 
			
		||||
 | 
			
		||||
			justify-self: flex-end;
 | 
			
		||||
			align-items: center;
 | 
			
		||||
			padding: 0;
 | 
			
		||||
			background-color: transparent;
 | 
			
		||||
			box-shadow: none;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		.socials {
 | 
			
		||||
			display: none;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@media (min-width: 60em) {
 | 
			
		||||
		.socials {
 | 
			
		||||
			display: flex;
 | 
			
		||||
			justify-content: flex-end;
 | 
			
		||||
			gap: 0;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	@media (forced-colors: active) {
 | 
			
		||||
		.link.active {
 | 
			
		||||
			color: SelectedItem;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										16
									
								
								src/components/Pill.astro
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/components/Pill.astro
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
			
		||||
<div class="pill"><slot /></div>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
	.pill {
 | 
			
		||||
		display: flex;
 | 
			
		||||
		padding: 0.5rem 1rem;
 | 
			
		||||
		gap: 0.5rem;
 | 
			
		||||
		color: var(--accent-text-over);
 | 
			
		||||
		border: 1px solid var(--accent-regular);
 | 
			
		||||
		background-color: var(--accent-regular);
 | 
			
		||||
		border-radius: 999rem;
 | 
			
		||||
		font-size: var(--text-md);
 | 
			
		||||
		line-height: 1.35;
 | 
			
		||||
		white-space: nowrap;
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										64
									
								
								src/components/PortfolioPreview.astro
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								src/components/PortfolioPreview.astro
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,64 @@
 | 
			
		||||
---
 | 
			
		||||
import type { CollectionEntry } from 'astro:content';
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
	project: CollectionEntry<'work'>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const { data, slug } = Astro.props.project;
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
<a class="card" href={`/work/${slug}`}>
 | 
			
		||||
	<span class="title">{data.title}</span>
 | 
			
		||||
	<img src={data.img} alt={data.img_alt || ''} loading="lazy" decoding="async" />
 | 
			
		||||
</a>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
	.card {
 | 
			
		||||
		display: grid;
 | 
			
		||||
		grid-template: auto 1fr / auto 1fr;
 | 
			
		||||
		height: 11rem;
 | 
			
		||||
		background: var(--gradient-subtle);
 | 
			
		||||
		border: 1px solid var(--gray-800);
 | 
			
		||||
		border-radius: 0.75rem;
 | 
			
		||||
		overflow: hidden;
 | 
			
		||||
		box-shadow: var(--shadow-sm);
 | 
			
		||||
		text-decoration: none;
 | 
			
		||||
		font-family: var(--font-brand);
 | 
			
		||||
		font-size: var(--text-lg);
 | 
			
		||||
		font-weight: 500;
 | 
			
		||||
		transition: box-shadow var(--theme-transition);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.card:hover {
 | 
			
		||||
		box-shadow: var(--shadow-md);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.title {
 | 
			
		||||
		grid-area: 1 / 1 / 2 / 2;
 | 
			
		||||
		z-index: 1;
 | 
			
		||||
		margin: 0.5rem;
 | 
			
		||||
		padding: 0.5rem 1rem;
 | 
			
		||||
		background: var(--gray-999);
 | 
			
		||||
		color: var(--gray-200);
 | 
			
		||||
		border-radius: 0.375rem;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	img {
 | 
			
		||||
		grid-area: 1 / 1 / 3 / 3;
 | 
			
		||||
		width: 100%;
 | 
			
		||||
		height: 100%;
 | 
			
		||||
		object-fit: cover;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@media (min-width: 50em) {
 | 
			
		||||
		.card {
 | 
			
		||||
			height: 22rem;
 | 
			
		||||
			border-radius: 1.5rem;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		.title {
 | 
			
		||||
			border-radius: 0.9375rem;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										62
									
								
								src/components/Skills.astro
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								src/components/Skills.astro
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,62 @@
 | 
			
		||||
---
 | 
			
		||||
import Icon from './Icon.astro';
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
<section class="box skills">
 | 
			
		||||
	<div class="stack gap-2 lg:gap-4">
 | 
			
		||||
		<Icon icon="terminal-window" color="var(--accent-regular)" size="2.5rem" gradient />
 | 
			
		||||
		<h2>Full Stack</h2>
 | 
			
		||||
		<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod.</p>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div class="stack gap-2 lg:gap-4">
 | 
			
		||||
		<Icon icon="trophy" color="var(--accent-regular)" size="2.5rem" gradient />
 | 
			
		||||
		<h2>Industry Leader</h2>
 | 
			
		||||
		<p>Neque viverra justo nec ultrices dui. Est ultricies integer quis auctor elit.</p>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div class="stack gap-2 lg:gap-4">
 | 
			
		||||
		<Icon icon="strategy" color="var(--accent-regular)" size="2.5rem" gradient />
 | 
			
		||||
		<h2>Strategy-Minded</h2>
 | 
			
		||||
		<p>Urna porttitor rhoncus dolor purus non enim praesent ornare.</p>
 | 
			
		||||
	</div>
 | 
			
		||||
</section>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
	.box {
 | 
			
		||||
		border: 1px solid var(--gray-800);
 | 
			
		||||
		border-radius: 0.75rem;
 | 
			
		||||
		padding: 1.5rem;
 | 
			
		||||
		background-color: var(--gray-999_40);
 | 
			
		||||
		box-shadow: var(--shadow-sm);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.skills {
 | 
			
		||||
		display: flex;
 | 
			
		||||
		flex-direction: column;
 | 
			
		||||
		gap: 3rem;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.skills h2 {
 | 
			
		||||
		font-size: var(--text-lg);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.skills p {
 | 
			
		||||
		color: var(--gray-400);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@media (min-width: 50em) {
 | 
			
		||||
		.box {
 | 
			
		||||
			border-radius: 1.5rem;
 | 
			
		||||
			padding: 2.5rem;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		.skills {
 | 
			
		||||
			display: grid;
 | 
			
		||||
			grid-template-columns: repeat(3, 1fr);
 | 
			
		||||
			gap: 5rem;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		.skills h2 {
 | 
			
		||||
			font-size: var(--text-2xl);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										95
									
								
								src/components/ThemeToggle.astro
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								src/components/ThemeToggle.astro
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,95 @@
 | 
			
		||||
---
 | 
			
		||||
import Icon from './Icon.astro';
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
<theme-toggle>
 | 
			
		||||
	<button>
 | 
			
		||||
		<span class="sr-only">Dark theme</span>
 | 
			
		||||
		<span class="icon light"><Icon icon="sun" /></span>
 | 
			
		||||
		<span class="icon dark"><Icon icon="moon-stars" /></span>
 | 
			
		||||
	</button>
 | 
			
		||||
</theme-toggle>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
	button {
 | 
			
		||||
		display: flex;
 | 
			
		||||
		border: 0;
 | 
			
		||||
		border-radius: 999rem;
 | 
			
		||||
		padding: 0;
 | 
			
		||||
		background-color: var(--gray-999);
 | 
			
		||||
		box-shadow: inset 0 0 0 1px var(--accent-overlay);
 | 
			
		||||
		cursor: pointer;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.icon {
 | 
			
		||||
		z-index: 1;
 | 
			
		||||
		position: relative;
 | 
			
		||||
		display: flex;
 | 
			
		||||
		padding: 0.5rem;
 | 
			
		||||
		width: 2rem;
 | 
			
		||||
		height: 2rem;
 | 
			
		||||
		font-size: 1rem;
 | 
			
		||||
		color: var(--accent-overlay);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.icon.light::before {
 | 
			
		||||
		content: '';
 | 
			
		||||
		z-index: -1;
 | 
			
		||||
		position: absolute;
 | 
			
		||||
		inset: 0;
 | 
			
		||||
		background-color: var(--accent-regular);
 | 
			
		||||
		border-radius: 999rem;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	:global(.theme-dark) .icon.light::before {
 | 
			
		||||
		transform: translateX(100%);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	:global(.theme-dark) .icon.dark,
 | 
			
		||||
	:global(html:not(.theme-dark)) .icon.light,
 | 
			
		||||
	button[aria-pressed='false'] .icon.light {
 | 
			
		||||
		color: var(--accent-text-over);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@media (prefers-reduced-motion: no-preference) {
 | 
			
		||||
		.icon,
 | 
			
		||||
		.icon.light::before {
 | 
			
		||||
			transition:
 | 
			
		||||
				transform var(--theme-transition),
 | 
			
		||||
				color var(--theme-transition);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@media (forced-colors: active) {
 | 
			
		||||
		.icon.light::before {
 | 
			
		||||
			background-color: SelectedItem;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
	class ThemeToggle extends HTMLElement {
 | 
			
		||||
		constructor() {
 | 
			
		||||
			super();
 | 
			
		||||
 | 
			
		||||
			const button = this.querySelector('button')!;
 | 
			
		||||
 | 
			
		||||
			/** Set the theme to dark/light mode. */
 | 
			
		||||
			const setTheme = (dark: boolean) => {
 | 
			
		||||
				document.documentElement.classList[dark ? 'add' : 'remove']('theme-dark');
 | 
			
		||||
				button.setAttribute('aria-pressed', String(dark));
 | 
			
		||||
			};
 | 
			
		||||
 | 
			
		||||
			// Toggle the theme when a user clicks the button.
 | 
			
		||||
			button.addEventListener('click', () => setTheme(!this.isDark()));
 | 
			
		||||
 | 
			
		||||
			// Initialize button state to reflect current theme.
 | 
			
		||||
			setTheme(this.isDark());
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		isDark() {
 | 
			
		||||
			return document.documentElement.classList.contains('theme-dark');
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	customElements.define('theme-toggle', ThemeToggle);
 | 
			
		||||
</script>
 | 
			
		||||
		Reference in New Issue
	
	Block a user