Content Updates with projects and experience

This commit is contained in:
Greg Kilmer
2026-07-04 18:03:38 -04:00
parent 1e8f5f1f01
commit 35d561525f
29 changed files with 347 additions and 148 deletions
+89 -22
View File
@@ -2,14 +2,15 @@
/**
* SectionNav.astro
* ---------------------------------------------------------------------------
* Sticky in-page sub-navigation for the Professional page. Jumps between the
* six anchor sections (Summary, Experience, Skills, Projects, Education,
* Contact) via smooth scroll. Hidden on small screens where the page reads as a
* simple scroll. Active state is computed client-side via IntersectionObserver.
* Fixed LEFT-SIDE vertical rail for the Professional page. Shows the six
* sections as a stacked icon+label list, highlights the section currently in
* view, and lets the user click any item to smooth-scroll to it. A vertical
* progress line beside the items fills as the user scrolls down the page.
*
* Sits BELOW the hero and is OPAQUE (a solid dark nebula surface) so it remains
* readable while scrolling over content. Sticky so it tracks the viewport once
* the user scrolls past it.
* Stylistically it mirrors the page's `.card` language (same raised surface,
* border, radius) so it reads as a sibling of the content cards rather than a
* separate chrome element. Icons + labels use bright tokens so they pop on the
* dark surface. Hidden below `lg`.
*/
import Icon from "@components/Icon.astro";
const sections = [
@@ -22,28 +23,79 @@ const sections = [
];
---
<nav aria-label="Professional sections" class="hidden lg:block">
<nav
aria-label="Professional sections"
class="pro-section-nav fixed left-4 top-1/2 z-40 hidden -translate-y-1/2 flex-col lg:flex"
data-pro-nav
>
{/* Rail container — matches the .card aesthetic: raised surface, hairline
border, 1rem radius. Bright icons + labels inside. */}
<div
class="sticky top-20 z-30 flex flex-wrap gap-1 rounded-xl border p-1.5 shadow-lg"
style="border-color: color-mix(in srgb, var(--color-accent) 28%, var(--color-border)); background: linear-gradient(180deg, rgba(12, 14, 40, 0.96), rgba(8, 10, 32, 0.96)); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);"
class="card relative flex flex-col gap-1 p-3"
style="border-color: var(--color-border); background: var(--color-surface-raised); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); box-shadow: 0 18px 40px -24px rgba(11, 16, 38, 0.6);"
>
{/* Vertical progress track behind the dots. */}
<div
class="pointer-events-none absolute left-[1.55rem] top-6 bottom-6 w-px"
style="background: color-mix(in srgb, var(--color-border) 80%, transparent);"
aria-hidden="true"
>
</div>
{/* The fill bar — height set by JS as the user scrolls. */}
<div
class="pointer-events-none absolute left-[1.55rem] top-6 w-px origin-top"
style="background: var(--color-accent); height: 0%; box-shadow: 0 0 8px color-mix(in srgb, var(--color-accent) 60%, transparent);"
data-pro-fill
aria-hidden="true"
>
</div>
{sections.map((s) => (
<a
href={`#${s.id}`}
data-section-link={s.id}
class="inline-flex items-center gap-1.5 rounded-lg px-3 py-1.5 font-sans text-sm font-medium transition-colors"
style="color: var(--color-cream); opacity: 0.7;"
class="group relative flex items-center gap-3 rounded-lg px-2.5 py-2 font-sans text-sm font-semibold transition-all"
style="color: var(--color-cream); opacity: 0.85;"
>
<Icon name={s.icon} class="h-4 w-4" />
{s.label}
<span
class="relative z-10 inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-lg transition-colors"
style="background: color-mix(in srgb, var(--color-accent) 14%, transparent); color: var(--color-cream);"
data-section-dot={s.id}
>
<Icon name={s.icon} class="h-[1.15rem] w-[1.15rem]" />
</span>
<span class="relative z-10 tracking-wide">{s.label}</span>
</a>
))}
</div>
</nav>
<style>
/* Active link: full opacity + accent text + subtle accent wash. */
.pro-section-nav a[data-active="true"] {
opacity: 1 !important;
color: var(--color-accent) !important;
background: color-mix(in srgb, var(--color-accent) 14%, transparent);
}
/* Active dot: filled accent, bright icon, glow ring. */
.pro-section-nav a[data-active="true"] [data-section-dot] {
background: color-mix(in srgb, var(--color-accent) 80%, transparent) !important;
color: var(--color-ink) !important;
box-shadow: 0 0 12px color-mix(in srgb, var(--color-accent) 65%, transparent);
}
/* Hover lift on inactive items. */
.pro-section-nav a:hover {
opacity: 1 !important;
background: color-mix(in srgb, var(--color-accent) 8%, transparent);
}
</style>
<script>
// Highlight the link for whichever section is currently in view.
// Spy: highlight the link for whichever section is in view + fill the rail.
function setupSectionSpy() {
const nav = document.querySelector<HTMLElement>("[data-pro-nav]");
if (!nav) return;
const links = Array.from(
document.querySelectorAll<HTMLAnchorElement>("[data-section-link]"),
);
@@ -51,20 +103,16 @@ const sections = [
const sections = links
.map((l) => document.getElementById(l.dataset.sectionLink!))
.filter((el): el is HTMLElement => Boolean(el));
const fill = nav.querySelector<HTMLElement>("[data-pro-fill]");
const setActive = (id: string) => {
for (const l of links) {
const active = l.dataset.sectionLink === id;
l.style.color = active
? "var(--color-accent)"
: "var(--color-cream)";
l.style.opacity = active ? "1" : "0.7";
l.style.background = active
? "color-mix(in srgb, var(--color-accent) 16%, transparent)"
: "transparent";
l.setAttribute("data-active", active ? "true" : "false");
}
};
// Highlight the most-visible section.
const io = new IntersectionObserver(
(entries) => {
const visible = entries
@@ -75,6 +123,25 @@ const sections = [
{ rootMargin: "-30% 0px -55% 0px", threshold: [0, 0.25, 0.5] },
);
sections.forEach((s) => io.observe(s));
// Progress fill: fraction of the page scrolled.
const trackTop = 24; // px — matches top-6 on the track/fill
const trackBottom = 24; // px — matches bottom-6
const updateFill = () => {
if (!fill) return;
const doc = document.documentElement;
const max = doc.scrollHeight - doc.clientHeight;
const frac = max > 0 ? Math.max(0, Math.min(1, window.scrollY / max)) : 0;
// Fill height as a percentage of the track length (which spans the card
// minus top/bottom padding).
const cardEl = fill.parentElement;
if (!cardEl) return;
const trackPx = cardEl.clientHeight - trackTop - trackBottom;
fill.style.height = `${(frac * trackPx).toFixed(1)}px`;
};
updateFill();
window.addEventListener("scroll", updateFill, { passive: true });
window.addEventListener("resize", updateFill);
}
setupSectionSpy();
document.addEventListener("astro:page-load", setupSectionSpy);
+2 -1
View File
@@ -55,7 +55,8 @@ const techStack = [
{
icon: "git",
label: "Tools",
value: "Git, Gitea, CI/CD pipelines",
value:
"PostgreSQL, VSCode, Unity, XCode, Android Studio, Git, Gitea, CI/CD pipelines",
hint: "Add the rest of your toolchain.",
},
{
+16 -11
View File
@@ -43,23 +43,28 @@ const rendered = await Promise.all(
{rendered.map(({ entry, Content }, i) => (
<li class="relative pl-12">
<Reveal delay={i * 80}>
{/* Marker */}
{/* Marker dot on the timeline line (kept small; the icon now lives inline
with the company name inside the card so it never clips the card). */}
<span
class="absolute left-0 top-0 inline-flex h-9 w-9 items-center justify-center rounded-full border-4"
style="background: var(--color-surface-raised); border-color: var(--color-surface);"
class="absolute left-[1.15rem] top-7 h-3 w-3 -translate-x-1/2 rounded-full"
style="background: var(--color-accent); box-shadow: 0 0 0 4px var(--color-surface);"
aria-hidden="true"
>
<span
class="inline-flex h-9 w-9 items-center justify-center rounded-full"
style="background: color-mix(in srgb, var(--color-accent) 16%, transparent);"
>
<Icon name={entry.data.icon} class="h-4 w-4" />
</span>
</span>
<div class="card flex flex-col gap-3">
<div class="flex flex-wrap items-baseline justify-between gap-x-4 gap-y-1">
<h3 style="color: var(--color-text);">{entry.data.company}</h3>
{/* Header row: icon + company name inline, period on the right. */}
<div class="flex flex-wrap items-center justify-between gap-x-4 gap-y-1">
<h3 class="flex items-center gap-3" style="color: var(--color-text);">
<span
class="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-full"
style="background: color-mix(in srgb, var(--color-accent) 16%, transparent);"
aria-hidden="true"
>
<Icon name={entry.data.icon} class="h-5 w-5" style="color: var(--color-accent);" />
</span>
{entry.data.company}
</h3>
<span class="font-mono text-xs uppercase tracking-[0.18em]" style="color: var(--color-text-subtle);">
{entry.data.period}
</span>