diff options
Diffstat (limited to 'examples/portfolio/src/components')
-rw-r--r-- | examples/portfolio/src/components/CallToAction.astro | 56 | ||||
-rw-r--r-- | examples/portfolio/src/components/ContactCTA.astro | 46 | ||||
-rw-r--r-- | examples/portfolio/src/components/Footer.astro | 74 | ||||
-rw-r--r-- | examples/portfolio/src/components/Grid.astro | 65 | ||||
-rw-r--r-- | examples/portfolio/src/components/Hero.astro | 54 | ||||
-rw-r--r-- | examples/portfolio/src/components/Icon.astro | 56 | ||||
-rw-r--r-- | examples/portfolio/src/components/IconPaths.ts | 38 | ||||
-rw-r--r-- | examples/portfolio/src/components/MainHead.astro | 47 | ||||
-rw-r--r-- | examples/portfolio/src/components/Nav.astro | 354 | ||||
-rw-r--r-- | examples/portfolio/src/components/Pill.astro | 16 | ||||
-rw-r--r-- | examples/portfolio/src/components/PortfolioPreview.astro | 64 | ||||
-rw-r--r-- | examples/portfolio/src/components/Skills.astro | 62 | ||||
-rw-r--r-- | examples/portfolio/src/components/ThemeToggle.astro | 95 |
13 files changed, 1027 insertions, 0 deletions
diff --git a/examples/portfolio/src/components/CallToAction.astro b/examples/portfolio/src/components/CallToAction.astro new file mode 100644 index 000000000..a1ca69750 --- /dev/null +++ b/examples/portfolio/src/components/CallToAction.astro @@ -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> diff --git a/examples/portfolio/src/components/ContactCTA.astro b/examples/portfolio/src/components/ContactCTA.astro new file mode 100644 index 000000000..6986bd740 --- /dev/null +++ b/examples/portfolio/src/components/ContactCTA.astro @@ -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> diff --git a/examples/portfolio/src/components/Footer.astro b/examples/portfolio/src/components/Footer.astro new file mode 100644 index 000000000..9d1878dad --- /dev/null +++ b/examples/portfolio/src/components/Footer.astro @@ -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> diff --git a/examples/portfolio/src/components/Grid.astro b/examples/portfolio/src/components/Grid.astro new file mode 100644 index 000000000..24a5ea79d --- /dev/null +++ b/examples/portfolio/src/components/Grid.astro @@ -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> diff --git a/examples/portfolio/src/components/Hero.astro b/examples/portfolio/src/components/Hero.astro new file mode 100644 index 000000000..30460420a --- /dev/null +++ b/examples/portfolio/src/components/Hero.astro @@ -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> diff --git a/examples/portfolio/src/components/Icon.astro b/examples/portfolio/src/components/Icon.astro new file mode 100644 index 000000000..92cff492a --- /dev/null +++ b/examples/portfolio/src/components/Icon.astro @@ -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> diff --git a/examples/portfolio/src/components/IconPaths.ts b/examples/portfolio/src/components/IconPaths.ts new file mode 100644 index 000000000..f2e959f62 --- /dev/null +++ b/examples/portfolio/src/components/IconPaths.ts @@ -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"/>`, +}; diff --git a/examples/portfolio/src/components/MainHead.astro b/examples/portfolio/src/components/MainHead.astro new file mode 100644 index 000000000..b4c7263ff --- /dev/null +++ b/examples/portfolio/src/components/MainHead.astro @@ -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> diff --git a/examples/portfolio/src/components/Nav.astro b/examples/portfolio/src/components/Nav.astro new file mode 100644 index 000000000..1d1938aec --- /dev/null +++ b/examples/portfolio/src/components/Nav.astro @@ -0,0 +1,354 @@ +--- +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' }, +]; + +/** Test if a link is pointing to the current page. */ +const isCurrentPage = (href: string) => { + let pathname = Astro.url.pathname.replace(import.meta.env.BASE_URL, ''); + if (pathname.at(0) !== '/') pathname = '/' + pathname; + if (pathname.at(-1) !== '/') pathname += '/'; + return pathname === href || (href !== '/' && pathname.startsWith(href)); +}; +--- + +<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={isCurrentPage(href) ? 'page' : null} class="link" 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={isCurrentPage(href) ? 'page' : null} class="link" 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[aria-current] { + 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[aria-current='page'] { + 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[aria-current='page'] { + color: SelectedItem; + } + } +</style> diff --git a/examples/portfolio/src/components/Pill.astro b/examples/portfolio/src/components/Pill.astro new file mode 100644 index 000000000..2c410faa0 --- /dev/null +++ b/examples/portfolio/src/components/Pill.astro @@ -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> diff --git a/examples/portfolio/src/components/PortfolioPreview.astro b/examples/portfolio/src/components/PortfolioPreview.astro new file mode 100644 index 000000000..f26bae0e2 --- /dev/null +++ b/examples/portfolio/src/components/PortfolioPreview.astro @@ -0,0 +1,64 @@ +--- +import type { CollectionEntry } from 'astro:content'; + +interface Props { + project: CollectionEntry<'work'>; +} + +const { data, id } = Astro.props.project; +--- + +<a class="card" href={`/work/${id}`}> + <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> diff --git a/examples/portfolio/src/components/Skills.astro b/examples/portfolio/src/components/Skills.astro new file mode 100644 index 000000000..5df5bb0d3 --- /dev/null +++ b/examples/portfolio/src/components/Skills.astro @@ -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> diff --git a/examples/portfolio/src/components/ThemeToggle.astro b/examples/portfolio/src/components/ThemeToggle.astro new file mode 100644 index 000000000..88f7bf67c --- /dev/null +++ b/examples/portfolio/src/components/ThemeToggle.astro @@ -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> |