mirror of
https://github.com/Patiun/com.gregkilmer.www.git
synced 2026-11-08 04:08:24 +00:00
Content Updates with projects and experience
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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.",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user