Content update and Professional details

This commit is contained in:
Greg Kilmer
2026-07-04 18:58:16 -04:00
parent 35d561525f
commit cf9eeb865e
6 changed files with 396 additions and 161 deletions
@@ -0,0 +1,77 @@
---
/**
* Certifications.astro
* ---------------------------------------------------------------------------
* Card grid for the Certifications section of the Professional page. Each entry
* is a cert with name, issuer, year, and optional status. Seeded with SAP/cloud
* placeholders (Greg to fill); swap the array below.
*
* Card aesthetic matches EducationHistory / ProjectCard.
*/
import Icon from "@components/Icon.astro";
interface CertEntry {
icon: string;
name: string;
issuer: string;
year: string;
status?: "Active" | "Expired" | "In progress";
}
// Placeholder entries — replace with Greg's real certifications.
const entries: CertEntry[] = [
/*{
icon: "microsoft-azure-devops",
name: "SAP BTP Certification",
issuer: "SAP",
year: "[Year]",
status: "Active",
},*/
{
icon: "cloud",
name: "Planet 9 Devlopment",
issuer: "Neptune",
year: "2019",
status: "Active",
},
/*{
icon: "robot",
name: "[AI / ML cert]",
issuer: "[Issuer]",
year: "[Year]",
},*/
];
---
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{
entries.map((c) => (
<article class="card flex flex-col gap-3">
<div class="flex items-start justify-between gap-3">
<span
class="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-lg"
style="background: color-mix(in srgb, var(--color-secondary) 18%, transparent); color: var(--color-secondary);"
aria-hidden="true"
>
<Icon name={c.icon} class="h-5 w-5" />
</span>
{c.status && <span class="pill">{c.status}</span>}
</div>
<div class="flex flex-col gap-1">
<h3 class="font-sans text-base" style="color: var(--color-text);">
{c.name}
</h3>
<p class="text-sm font-medium" style="color: var(--color-accent);">
{c.issuer}
</p>
<span
class="font-mono text-xs uppercase tracking-[0.18em]"
style="color: var(--color-text-subtle);"
>
{c.year}
</span>
</div>
</article>
))
}
</div>
@@ -0,0 +1,76 @@
---
/**
* EducationHistory.astro
* ---------------------------------------------------------------------------
* Card grid for the Education section of the Professional page. Each entry is a
* degree/diploma with institution, period, and optional detail. Data is seeded
* with placeholder rows (Greg to fill); swap the array below with real entries.
*
* Mirrors the Timeline/ProjectCard card aesthetic: raised surface, hairline
* border, gold accent for the credential, mono period label.
*/
import Icon from "@components/Icon.astro";
interface EduEntry {
icon: string;
credential: string;
institution: string;
period: string;
detail?: string;
}
// Placeholder entries — replace with Greg's real education history.
const entries: EduEntry[] = [
{
icon: "school",
credential: "B.S., Computer Science",
institution: "Drexel University",
period: "2014 – 2018",
detail:
"Foundational coursework in software engineering, algorithms, and systems. Focuses in AI and human-computer interaction.",
},
];
---
<div class="grid gap-4 sm:grid-cols-2">
{
entries.map((e) => (
<article class="card flex flex-col gap-3">
<div class="flex items-start gap-3">
<span
class="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-lg"
style="background: color-mix(in srgb, var(--color-accent) 16%, transparent); color: var(--color-accent);"
aria-hidden="true"
>
<Icon name={e.icon} class="h-5 w-5" />
</span>
<div class="flex flex-col gap-1">
<h3 class="font-sans text-lg" style="color: var(--color-text);">
{e.credential}
</h3>
<p
class="font-sans font-semibold"
style="color: var(--color-accent);"
>
{e.institution}
</p>
<span
class="font-mono text-xs uppercase tracking-[0.18em]"
style="color: var(--color-text-subtle);"
>
{e.period}
</span>
</div>
</div>
{e.detail && (
<p
class="text-sm leading-relaxed"
style="color: var(--color-text-muted);"
>
{e.detail}
</p>
)}
</article>
))
}
</div>
+126 -68
View File
@@ -2,15 +2,23 @@
/**
* SectionNav.astro
* ---------------------------------------------------------------------------
* 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.
* Fixed LEFT-SIDE vertical rail for the Professional page. Six sections as a
* stacked icon+label list; highlights the current section; click to smooth-
* scroll. A vertical progress line LEFT of the icons fills as you scroll.
*
* 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`.
* Three bugs previously fixed here:
* 1. Contrast: the rail used surface-relative tokens (--color-surface-raised /
* --color-cream) which collapsed to the same tone on light surfaces (the
* interest-surface cream made icons/text invisible). The rail now uses
* FIXED dark colors regardless of the page's surface tone.
* 2. Progress line clipping: the line sat at the icon column's horizontal
* center, running THROUGH the circles. It now sits in a dedicated left
* gutter, clear of the icons.
* 3. Last section (Contact) not activating: the IntersectionObserver band was
* too thin for the short final section to land in. The rootMargin is
* widened + click handlers set active immediately so feedback is instant.
*
* Hidden below `lg`.
*/
import Icon from "@components/Icon.astro";
const sections = [
@@ -28,70 +36,79 @@ const 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. */}
{/* Rail container. FIXED dark colors (not surface tokens) so it's always
legible regardless of the page's data-surface tone. Layout: a left gutter
for the progress line, then the icon+label list. */}
<div
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);"
class="relative flex gap-2 rounded-2xl border p-3 shadow-2xl"
style="border-color: rgba(232, 181, 74, 0.22); background: linear-gradient(180deg, rgba(12, 14, 40, 0.97), rgba(8, 10, 32, 0.97)); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); box-shadow: 0 18px 40px -24px rgba(11, 16, 38, 0.7);"
>
{/* 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"
>
{/* --- Left gutter: the progress track + fill. Sits LEFT of the icons. --- */}
<div class="relative my-1 w-2 shrink-0">
{/* Track (full height of the gutter). */}
<div
class="pointer-events-none absolute left-1/2 top-0 h-full w-px -translate-x-1/2"
style="background: rgba(255, 255, 255, 0.12);"
aria-hidden="true"
>
</div>
{/* Fill — height set by JS. Anchored to the top of the track. */}
<div
class="pointer-events-none absolute left-1/2 top-0 w-px -translate-x-1/2 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>
</div>
{sections.map((s) => (
<a
href={`#${s.id}`}
data-section-link={s.id}
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;"
>
<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}
{/* --- The icon+label list. --- */}
<div class="flex flex-col gap-1">
{sections.map((s) => (
<a
href={`#${s.id}`}
data-section-link={s.id}
class="group relative flex items-center gap-2.5 rounded-lg px-2.5 py-2 font-sans text-sm font-semibold transition-all"
style="color: #e8e6f5;"
>
<Icon name={s.icon} class="h-[1.15rem] w-[1.15rem]" />
</span>
<span class="relative z-10 tracking-wide">{s.label}</span>
</a>
))}
<span
class="relative z-10 inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-lg transition-colors"
style="background: rgba(232, 181, 74, 0.14); color: #f4cd72;"
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>
</div>
</nav>
<style>
/* Active link: full opacity + accent text + subtle accent wash. */
/* Inactive items: readable but slightly dimmed. */
.pro-section-nav a[data-section-link] {
opacity: 0.82;
}
.pro-section-nav a[data-section-link]:hover {
opacity: 1;
background: rgba(232, 181, 74, 0.08);
}
/* Active link: bright accent text + 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);
background: rgba(232, 181, 74, 0.14);
}
/* Active dot: filled accent, bright icon, glow ring. */
/* Active dot: filled accent, dark 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);
background: var(--color-accent) !important;
color: #0b1026 !important;
box-shadow: 0 0 12px rgba(232, 181, 74, 0.65);
}
</style>
<script>
// 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;
@@ -107,40 +124,81 @@ const sections = [
const setActive = (id: string) => {
for (const l of links) {
const active = l.dataset.sectionLink === id;
l.setAttribute("data-active", active ? "true" : "false");
l.setAttribute("data-active", l.dataset.sectionLink === id ? "true" : "false");
}
};
// Highlight the most-visible section.
// Manual-scroll suppression. When the user clicks a nav link we set active
// immediately AND suppress IO updates until the smooth-scroll settles. This
// is what stops the flicker: without it, the IO callback and the scroll
// handler fight during the animated scroll, toggling active state rapidly.
let suppressIo = false;
let suppressTimer: number | undefined;
// Click → set active immediately + suppress IO until scrolling settles.
for (const l of links) {
l.addEventListener("click", () => {
const id = l.dataset.sectionLink!;
setActive(id);
suppressIo = true;
window.clearTimeout(suppressTimer);
// Re-enable IO once the smooth-scroll has had time to finish.
suppressTimer = window.setTimeout(() => {
suppressIo = false;
}, 900);
});
}
// The IntersectionObserver is the SINGLE source of truth for the active
// section during free scrolling. (Previously the scroll handler also set
// active, which caused the two systems to thrash.)
const io = new IntersectionObserver(
(entries) => {
if (suppressIo) return;
const visible = entries
.filter((e) => e.isIntersecting)
.sort((a, b) => b.intersectionRatio - a.intersectionRatio)[0];
if (visible) setActive(visible.target.id);
},
{ rootMargin: "-30% 0px -55% 0px", threshold: [0, 0.25, 0.5] },
// Band spans roughly the middle 50% of the viewport vertically.
{ rootMargin: "-25% 0px -25% 0px", threshold: [0, 0.1, 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
// Progress fill ONLY. The scroll handler must never set active state — that
// was the other half of the flicker. Last-section-at-bottom is handled by
// a dedicated, debounced check below instead of on every scroll tick.
const updateFill = () => {
if (!fill) return;
const track = fill.parentElement; // the .w-2 gutter
if (!track) 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`;
fill.style.height = `${(frac * track.clientHeight).toFixed(1)}px`;
};
// Debounced at-bottom check: pin the last section active when truly at the
// bottom, but only after scrolling has paused (not on every tick).
let bottomTimer: number | undefined;
const checkBottom = () => {
window.clearTimeout(bottomTimer);
bottomTimer = window.setTimeout(() => {
if (suppressIo) return;
const doc = document.documentElement;
const atBottom =
window.innerHeight + window.scrollY >= doc.scrollHeight - 4;
if (atBottom && sections.length) {
setActive(sections[sections.length - 1].id);
}
}, 120);
};
updateFill();
window.addEventListener("scroll", updateFill, { passive: true });
window.addEventListener("scroll", () => {
updateFill();
checkBottom();
}, { passive: true });
window.addEventListener("resize", updateFill);
}
setupSectionSpy();
+2 -17
View File
@@ -32,25 +32,10 @@ const rendered = await Promise.all(
);
---
<ol class="relative flex flex-col gap-10">
{/* The vertical line. */}
<span
class="absolute left-[1.15rem] top-2 bottom-2 w-px"
style="background: var(--color-border);"
aria-hidden="true"
></span>
<ol class="flex flex-col gap-10">
{rendered.map(({ entry, Content }, i) => (
<li class="relative pl-12">
<li class="relative">
<Reveal delay={i * 80}>
{/* 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-[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>
<div class="card flex flex-col gap-3">
{/* Header row: icon + company name inline, period on the right. */}