mirror of
https://github.com/Patiun/com.gregkilmer.www.git
synced 2026-11-08 04:08:24 +00:00
Content update and Professional details
This commit is contained in:
@@ -2,16 +2,14 @@
|
|||||||
/**
|
/**
|
||||||
* InterestHero.astro
|
* InterestHero.astro
|
||||||
* ---------------------------------------------------------------------------
|
* ---------------------------------------------------------------------------
|
||||||
* Shared header for every interest sub-page, implementing the spec's
|
* Shared header for every interest sub-page. Compact hero on the animated
|
||||||
* per-interest template (Header: title + tagline; Overview: narrative). Uses the
|
* Milky Way field: a back-to-hub breadcrumb, a small icon mark, the title, and
|
||||||
* warm "interest" surface tone via InterestLayout.
|
* the italic tagline.
|
||||||
*
|
*
|
||||||
* The decorative icon and the tagline (in mono, as a pull quote) give each page
|
* Tuned for the dark starfield: text uses bright cream tones (not the dim
|
||||||
* a consistent rhythm while the page supplies its own title + overview copy.
|
* --color-text-subtle/--color-text which vanish on the deep sky). The title is
|
||||||
*
|
* h2-sized (the page is a sub-page, not Home) so it doesn't dominate. An
|
||||||
* An optional `image` prop renders a wide celestial-framed banner behind / above
|
* optional banner image sits above the heading.
|
||||||
* the heading so each interest page leads with a vibe-matched photo. A faint
|
|
||||||
* starfield + alchemical accent reinforces the esoteric mood.
|
|
||||||
*/
|
*/
|
||||||
import Icon from '@components/Icon.astro';
|
import Icon from '@components/Icon.astro';
|
||||||
import Reveal from '@components/motion/Reveal.astro';
|
import Reveal from '@components/motion/Reveal.astro';
|
||||||
@@ -46,20 +44,57 @@ const {
|
|||||||
Carries the per-page morph name (one .vt-sky per page snapshot). -->
|
Carries the per-page morph name (one .vt-sky per page snapshot). -->
|
||||||
<AnimatedBackground client:idle className="vt-sky" />
|
<AnimatedBackground client:idle className="vt-sky" />
|
||||||
|
|
||||||
<div class="container-prose relative z-10 flex flex-col items-start gap-6 py-8">
|
{/* Two-column layout: text on the left, image on the right. Putting the image
|
||||||
<Reveal>
|
BESIDE the text (instead of stacked above it) means the hero height is
|
||||||
<a
|
driven by the text column and stays consistent across interest pages
|
||||||
href="/interests"
|
regardless of image presence. On small screens it stacks (image below). */}
|
||||||
class="inline-flex items-center gap-1.5 font-mono text-xs uppercase tracking-[0.18em] transition-colors"
|
<div class="container-prose relative z-10 grid items-center gap-6 py-6 sm:grid-cols-[1.4fr_1fr]">
|
||||||
style="color: var(--color-text-subtle);"
|
{/* --- Left: text column --- */}
|
||||||
>
|
<div class="flex flex-col items-start gap-4">
|
||||||
<Icon name="arrow-left" class="h-4 w-4" />
|
<Reveal>
|
||||||
{eyebrow}
|
<a
|
||||||
</a>
|
href="/interests"
|
||||||
</Reveal>
|
class="inline-flex items-center gap-1.5 font-mono text-xs uppercase tracking-[0.18em] transition-colors"
|
||||||
|
style="color: var(--color-cream); opacity: 0.8;"
|
||||||
|
>
|
||||||
|
<Icon name="arrow-left" class="h-4 w-4" />
|
||||||
|
{eyebrow}
|
||||||
|
</a>
|
||||||
|
</Reveal>
|
||||||
|
|
||||||
|
<Reveal delay={60}>
|
||||||
|
<span
|
||||||
|
class="inline-flex h-12 w-12 items-center justify-center rounded-xl"
|
||||||
|
style="background: color-mix(in srgb, var(--color-accent) 18%, transparent); color: var(--color-accent);"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<Icon name={icon} class="h-6 w-6" />
|
||||||
|
</span>
|
||||||
|
</Reveal>
|
||||||
|
|
||||||
|
<Reveal delay={100}>
|
||||||
|
<h2
|
||||||
|
class="font-sans font-bold tracking-tight"
|
||||||
|
style="color: var(--color-cream); font-size: clamp(1.75rem, 2vw + 1rem, 2.5rem); line-height: 1.15;"
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</h2>
|
||||||
|
</Reveal>
|
||||||
|
|
||||||
|
<Reveal delay={140}>
|
||||||
|
<p
|
||||||
|
class="max-w-xl border-l-2 pl-4 font-sans text-base italic sm:text-lg"
|
||||||
|
style="border-color: var(--color-accent); color: var(--color-cream); opacity: 0.92;"
|
||||||
|
>
|
||||||
|
{tagline}
|
||||||
|
</p>
|
||||||
|
</Reveal>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* --- Right: image column (only when an image is provided). A fixed
|
||||||
|
aspect keeps every interest hero the same height. --- */}
|
||||||
{image && (
|
{image && (
|
||||||
<Reveal delay={40} class="w-full">
|
<Reveal delay={80} class="w-full">
|
||||||
<figure
|
<figure
|
||||||
class="relative w-full overflow-hidden rounded-2xl border"
|
class="relative w-full overflow-hidden rounded-2xl border"
|
||||||
style="border-color: color-mix(in srgb, var(--color-accent) 40%, transparent);"
|
style="border-color: color-mix(in srgb, var(--color-accent) 40%, transparent);"
|
||||||
@@ -69,7 +104,7 @@ const {
|
|||||||
alt={imageAlt}
|
alt={imageAlt}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
decoding="async"
|
decoding="async"
|
||||||
class="aspect-[21/9] w-full object-cover"
|
class="aspect-[4/3] w-full object-cover"
|
||||||
/>
|
/>
|
||||||
<StarBurst
|
<StarBurst
|
||||||
class="pointer-events-none absolute left-3 top-3 h-5 w-5 drop-shadow-[0_0_6px_color-mix(in_srgb,var(--color-accent)_60%,transparent)]"
|
class="pointer-events-none absolute left-3 top-3 h-5 w-5 drop-shadow-[0_0_6px_color-mix(in_srgb,var(--color-accent)_60%,transparent)]"
|
||||||
@@ -78,28 +113,5 @@ const {
|
|||||||
</figure>
|
</figure>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Reveal delay={60}>
|
|
||||||
<span
|
|
||||||
class="inline-flex h-16 w-16 items-center justify-center rounded-2xl"
|
|
||||||
style="background: color-mix(in srgb, var(--color-accent) 18%, transparent); color: var(--color-accent);"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<Icon name={icon} class="h-8 w-8" />
|
|
||||||
</span>
|
|
||||||
</Reveal>
|
|
||||||
|
|
||||||
<Reveal delay={120}>
|
|
||||||
<h1 class="text-balance">{title}</h1>
|
|
||||||
</Reveal>
|
|
||||||
|
|
||||||
<Reveal delay={160}>
|
|
||||||
<p
|
|
||||||
class="max-w-2xl border-l-2 pl-5 font-sans text-lg italic sm:text-xl"
|
|
||||||
style="border-color: var(--color-accent); color: var(--color-text);"
|
|
||||||
>
|
|
||||||
{tagline}
|
|
||||||
</p>
|
|
||||||
</Reveal>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -2,15 +2,23 @@
|
|||||||
/**
|
/**
|
||||||
* SectionNav.astro
|
* SectionNav.astro
|
||||||
* ---------------------------------------------------------------------------
|
* ---------------------------------------------------------------------------
|
||||||
* Fixed LEFT-SIDE vertical rail for the Professional page. Shows the six
|
* Fixed LEFT-SIDE vertical rail for the Professional page. Six sections as a
|
||||||
* sections as a stacked icon+label list, highlights the section currently in
|
* stacked icon+label list; highlights the current section; click to smooth-
|
||||||
* view, and lets the user click any item to smooth-scroll to it. A vertical
|
* scroll. A vertical progress line LEFT of the icons fills as you scroll.
|
||||||
* progress line beside the items fills as the user scrolls down the page.
|
|
||||||
*
|
*
|
||||||
* Stylistically it mirrors the page's `.card` language (same raised surface,
|
* Three bugs previously fixed here:
|
||||||
* border, radius) so it reads as a sibling of the content cards rather than a
|
* 1. Contrast: the rail used surface-relative tokens (--color-surface-raised /
|
||||||
* separate chrome element. Icons + labels use bright tokens so they pop on the
|
* --color-cream) which collapsed to the same tone on light surfaces (the
|
||||||
* dark surface. Hidden below `lg`.
|
* 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";
|
import Icon from "@components/Icon.astro";
|
||||||
const sections = [
|
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"
|
class="pro-section-nav fixed left-4 top-1/2 z-40 hidden -translate-y-1/2 flex-col lg:flex"
|
||||||
data-pro-nav
|
data-pro-nav
|
||||||
>
|
>
|
||||||
{/* Rail container — matches the .card aesthetic: raised surface, hairline
|
{/* Rail container. FIXED dark colors (not surface tokens) so it's always
|
||||||
border, 1rem radius. Bright icons + labels inside. */}
|
legible regardless of the page's data-surface tone. Layout: a left gutter
|
||||||
|
for the progress line, then the icon+label list. */}
|
||||||
<div
|
<div
|
||||||
class="card relative flex flex-col gap-1 p-3"
|
class="relative flex gap-2 rounded-2xl border p-3 shadow-2xl"
|
||||||
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);"
|
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. */}
|
{/* --- Left gutter: the progress track + fill. Sits LEFT of the icons. --- */}
|
||||||
<div
|
<div class="relative my-1 w-2 shrink-0">
|
||||||
class="pointer-events-none absolute left-[1.55rem] top-6 bottom-6 w-px"
|
{/* Track (full height of the gutter). */}
|
||||||
style="background: color-mix(in srgb, var(--color-border) 80%, transparent);"
|
<div
|
||||||
aria-hidden="true"
|
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);"
|
||||||
</div>
|
aria-hidden="true"
|
||||||
{/* The fill bar — height set by JS as the user scrolls. */}
|
>
|
||||||
<div
|
</div>
|
||||||
class="pointer-events-none absolute left-[1.55rem] top-6 w-px origin-top"
|
{/* Fill — height set by JS. Anchored to the top of the track. */}
|
||||||
style="background: var(--color-accent); height: 0%; box-shadow: 0 0 8px color-mix(in srgb, var(--color-accent) 60%, transparent);"
|
<div
|
||||||
data-pro-fill
|
class="pointer-events-none absolute left-1/2 top-0 w-px -translate-x-1/2 origin-top"
|
||||||
aria-hidden="true"
|
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>
|
</div>
|
||||||
|
|
||||||
{sections.map((s) => (
|
{/* --- The icon+label list. --- */}
|
||||||
<a
|
<div class="flex flex-col gap-1">
|
||||||
href={`#${s.id}`}
|
{sections.map((s) => (
|
||||||
data-section-link={s.id}
|
<a
|
||||||
class="group relative flex items-center gap-3 rounded-lg px-2.5 py-2 font-sans text-sm font-semibold transition-all"
|
href={`#${s.id}`}
|
||||||
style="color: var(--color-cream); opacity: 0.85;"
|
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"
|
||||||
<span
|
style="color: #e8e6f5;"
|
||||||
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 inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-lg transition-colors"
|
||||||
<span class="relative z-10 tracking-wide">{s.label}</span>
|
style="background: rgba(232, 181, 74, 0.14); color: #f4cd72;"
|
||||||
</a>
|
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>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<style>
|
<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"] {
|
.pro-section-nav a[data-active="true"] {
|
||||||
opacity: 1 !important;
|
opacity: 1 !important;
|
||||||
color: var(--color-accent) !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] {
|
.pro-section-nav a[data-active="true"] [data-section-dot] {
|
||||||
background: color-mix(in srgb, var(--color-accent) 80%, transparent) !important;
|
background: var(--color-accent) !important;
|
||||||
color: var(--color-ink) !important;
|
color: #0b1026 !important;
|
||||||
box-shadow: 0 0 12px color-mix(in srgb, var(--color-accent) 65%, transparent);
|
box-shadow: 0 0 12px rgba(232, 181, 74, 0.65);
|
||||||
}
|
|
||||||
/* Hover lift on inactive items. */
|
|
||||||
.pro-section-nav a:hover {
|
|
||||||
opacity: 1 !important;
|
|
||||||
background: color-mix(in srgb, var(--color-accent) 8%, transparent);
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Spy: highlight the link for whichever section is in view + fill the rail.
|
|
||||||
function setupSectionSpy() {
|
function setupSectionSpy() {
|
||||||
const nav = document.querySelector<HTMLElement>("[data-pro-nav]");
|
const nav = document.querySelector<HTMLElement>("[data-pro-nav]");
|
||||||
if (!nav) return;
|
if (!nav) return;
|
||||||
@@ -107,40 +124,81 @@ const sections = [
|
|||||||
|
|
||||||
const setActive = (id: string) => {
|
const setActive = (id: string) => {
|
||||||
for (const l of links) {
|
for (const l of links) {
|
||||||
const active = l.dataset.sectionLink === id;
|
l.setAttribute("data-active", l.dataset.sectionLink === id ? "true" : "false");
|
||||||
l.setAttribute("data-active", active ? "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(
|
const io = new IntersectionObserver(
|
||||||
(entries) => {
|
(entries) => {
|
||||||
|
if (suppressIo) return;
|
||||||
const visible = entries
|
const visible = entries
|
||||||
.filter((e) => e.isIntersecting)
|
.filter((e) => e.isIntersecting)
|
||||||
.sort((a, b) => b.intersectionRatio - a.intersectionRatio)[0];
|
.sort((a, b) => b.intersectionRatio - a.intersectionRatio)[0];
|
||||||
if (visible) setActive(visible.target.id);
|
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));
|
sections.forEach((s) => io.observe(s));
|
||||||
|
|
||||||
// Progress fill: fraction of the page scrolled.
|
// Progress fill ONLY. The scroll handler must never set active state — that
|
||||||
const trackTop = 24; // px — matches top-6 on the track/fill
|
// was the other half of the flicker. Last-section-at-bottom is handled by
|
||||||
const trackBottom = 24; // px — matches bottom-6
|
// a dedicated, debounced check below instead of on every scroll tick.
|
||||||
const updateFill = () => {
|
const updateFill = () => {
|
||||||
if (!fill) return;
|
if (!fill) return;
|
||||||
|
const track = fill.parentElement; // the .w-2 gutter
|
||||||
|
if (!track) return;
|
||||||
const doc = document.documentElement;
|
const doc = document.documentElement;
|
||||||
const max = doc.scrollHeight - doc.clientHeight;
|
const max = doc.scrollHeight - doc.clientHeight;
|
||||||
const frac = max > 0 ? Math.max(0, Math.min(1, window.scrollY / max)) : 0;
|
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
|
fill.style.height = `${(frac * track.clientHeight).toFixed(1)}px`;
|
||||||
// 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`;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 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();
|
updateFill();
|
||||||
window.addEventListener("scroll", updateFill, { passive: true });
|
window.addEventListener("scroll", () => {
|
||||||
|
updateFill();
|
||||||
|
checkBottom();
|
||||||
|
}, { passive: true });
|
||||||
window.addEventListener("resize", updateFill);
|
window.addEventListener("resize", updateFill);
|
||||||
}
|
}
|
||||||
setupSectionSpy();
|
setupSectionSpy();
|
||||||
|
|||||||
@@ -32,25 +32,10 @@ const rendered = await Promise.all(
|
|||||||
);
|
);
|
||||||
---
|
---
|
||||||
|
|
||||||
<ol class="relative flex flex-col gap-10">
|
<ol class="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>
|
|
||||||
|
|
||||||
{rendered.map(({ entry, Content }, i) => (
|
{rendered.map(({ entry, Content }, i) => (
|
||||||
<li class="relative pl-12">
|
<li class="relative">
|
||||||
<Reveal delay={i * 80}>
|
<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">
|
<div class="card flex flex-col gap-3">
|
||||||
{/* Header row: icon + company name inline, period on the right. */}
|
{/* Header row: icon + company name inline, period on the right. */}
|
||||||
|
|||||||
@@ -14,14 +14,16 @@ import BaseLayout from "@layouts/BaseLayout.astro";
|
|||||||
import Reveal from "@components/motion/Reveal.astro";
|
import Reveal from "@components/motion/Reveal.astro";
|
||||||
import Icon from "@components/Icon.astro";
|
import Icon from "@components/Icon.astro";
|
||||||
import SectionHeader from "@components/layout/SectionHeader.astro";
|
import SectionHeader from "@components/layout/SectionHeader.astro";
|
||||||
import Placeholder from "@components/layout/Placeholder.astro";
|
|
||||||
import SectionNav from "@components/professional/SectionNav.astro";
|
import SectionNav from "@components/professional/SectionNav.astro";
|
||||||
import Timeline from "@components/professional/Timeline.astro";
|
import Timeline from "@components/professional/Timeline.astro";
|
||||||
import SkillGrid from "@components/professional/SkillGrid.astro";
|
import SkillGrid from "@components/professional/SkillGrid.astro";
|
||||||
import ProjectCard from "@components/professional/ProjectCard.astro";
|
import ProjectCard from "@components/professional/ProjectCard.astro";
|
||||||
|
import EducationHistory from "@components/professional/EducationHistory.astro";
|
||||||
|
import Certifications from "@components/professional/Certifications.astro";
|
||||||
import AlchemicalDivider from "@components/mystic/AlchemicalDivider.astro";
|
import AlchemicalDivider from "@components/mystic/AlchemicalDivider.astro";
|
||||||
import Astrolabe from "@components/mystic/Astrolabe.astro";
|
import Astrolabe from "@components/mystic/Astrolabe.astro";
|
||||||
import CircuitField from "@components/mystic/CircuitField.tsx";
|
import CircuitField from "@components/mystic/CircuitField.tsx";
|
||||||
|
import CelestialFrame from "@components/mystic/CelestialFrame.astro";
|
||||||
import { getCollection } from "astro:content";
|
import { getCollection } from "astro:content";
|
||||||
|
|
||||||
const experience = (await getCollection("experience")).sort(
|
const experience = (await getCollection("experience")).sort(
|
||||||
@@ -37,24 +39,43 @@ const projects = (await getCollection("projects")).sort(
|
|||||||
title="Professional"
|
title="Professional"
|
||||||
description="Software developer and team lead: cloud and mobile development, SAP Business Technology Platform, AI/ML, full-stack work. Resume and portfolio for Greg Kilmer."
|
description="Software developer and team lead: cloud and mobile development, SAP Business Technology Platform, AI/ML, full-stack work. Resume and portfolio for Greg Kilmer."
|
||||||
>
|
>
|
||||||
<!-- Page header -->
|
{/* Page header + overlapping headshot.
|
||||||
<section class="relative overflow-hidden pt-28 sm:pt-32">
|
The headshot is anchored to the HERO SECTION (not container-prose) so its
|
||||||
{
|
straddle point is the true hero/next-section boundary. It's positioned on
|
||||||
/* Circuit-board field. Now self-contained (nebula gradient + canvas inside
|
the right, aligned with the content column's right edge via the same
|
||||||
one root div), mirroring AnimatedBackground's structure so the morph from
|
container-prose centering math (a centered max-width column). translate-y-1/2
|
||||||
a star hero is element-to-element equivalent — no separate gradient
|
puts its center on the boundary: half in the hero, half over the next
|
||||||
flash. Shares view-transition-name "sky" via vt-sky. */
|
section, in front of both (z-50). */}
|
||||||
}
|
<section class="relative overflow-visible pt-28 pb-28 sm:pt-32 sm:pb-36">
|
||||||
<CircuitField client:idle className="vt-sky opacity-100" />
|
{/* Background layer — clipped to the hero so the circuit canvas/astrolabe
|
||||||
{/* Slowly rotating astrolabe ornament behind the header. */}
|
don't bleed into the content below. */}
|
||||||
<Astrolabe
|
<div class="pointer-events-none absolute inset-0 overflow-hidden">
|
||||||
class="pointer-events-none absolute z-0 -right-48 -top-24 h-[34rem] w-[34rem] animate-spin-slow"
|
<CircuitField client:idle className="vt-sky opacity-100" />
|
||||||
style="color: var(--color-accent); opacity:.13;"
|
<Astrolabe
|
||||||
/>
|
class="absolute z-0 -right-48 -top-24 h-[34rem] w-[34rem] animate-spin-slow"
|
||||||
<div
|
style="color: var(--color-accent); opacity:.13;"
|
||||||
class="container-prose relative z-10 flex flex-col gap-10 lg:ml-56 lg:max-w-[calc(72rem+14rem)]"
|
/>
|
||||||
>
|
</div>
|
||||||
<Reveal>
|
|
||||||
|
{/* Headshot — anchored to the hero section's bottom edge (the true boundary
|
||||||
|
with the next section), aligned right with the content column. Wrapped in
|
||||||
|
a centered max-width box that mirrors container-prose so the headshot's
|
||||||
|
right edge lines up with the content's right edge. */}
|
||||||
|
<div class="pointer-events-none absolute inset-x-0 bottom-0 z-50 mx-auto max-w-[72rem] px-5 lg:ml-56 lg:max-w-[calc(72rem+14rem)] sm:px-8">
|
||||||
|
<div class="pointer-events-auto ml-auto w-44 translate-y-1/2 sm:w-52 md:w-60">
|
||||||
|
<CelestialFrame rounded="2xl" emphasis="strong">
|
||||||
|
<img
|
||||||
|
src="/images/about-portrait.jpg"
|
||||||
|
alt="Greg Kilmer headshot"
|
||||||
|
class="h-full w-full object-cover"
|
||||||
|
style="aspect-ratio: 4/5;"
|
||||||
|
/>
|
||||||
|
</CelestialFrame>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container-prose relative z-10 lg:ml-56 lg:max-w-[calc(72rem+14rem)]">
|
||||||
|
<Reveal class="max-w-2xl">
|
||||||
<SectionHeader
|
<SectionHeader
|
||||||
tone="hero"
|
tone="hero"
|
||||||
kicker="Professional"
|
kicker="Professional"
|
||||||
@@ -80,7 +101,7 @@ const projects = (await getCollection("projects")).sort(
|
|||||||
It keeps container-prose's max-width but lets it sit beside the rail. */
|
It keeps container-prose's max-width but lets it sit beside the rail. */
|
||||||
}
|
}
|
||||||
<div
|
<div
|
||||||
class="container-prose flex flex-col gap-20 py-16 sm:gap-24 sm:py-20 lg:ml-56 lg:max-w-[calc(72rem+14rem)]"
|
class="container-prose flex flex-col gap-20 pt-28 pb-16 sm:gap-24 sm:pt-40 sm:pb-20 lg:ml-56 lg:max-w-[calc(72rem+14rem)]"
|
||||||
>
|
>
|
||||||
{/* ===== 1. Summary ===== */}
|
{/* ===== 1. Summary ===== */}
|
||||||
<section id="summary" class="scroll-mt-28 flex flex-col gap-6">
|
<section id="summary" class="scroll-mt-28 flex flex-col gap-6">
|
||||||
@@ -185,7 +206,7 @@ const projects = (await getCollection("projects")).sort(
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* ===== 5. Education ===== */}
|
{/* ===== 5. Education ===== */}
|
||||||
<section id="education" class="scroll-mt-28 flex flex-col gap-6">
|
<section id="education" class="scroll-mt-28 flex flex-col gap-8">
|
||||||
<Reveal>
|
<Reveal>
|
||||||
<SectionHeader
|
<SectionHeader
|
||||||
kicker="05"
|
kicker="05"
|
||||||
@@ -194,16 +215,22 @@ const projects = (await getCollection("projects")).sort(
|
|||||||
subtitle="My foundation."
|
subtitle="My foundation."
|
||||||
/>
|
/>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
<div class="grid gap-4 sm:grid-cols-2">
|
<Reveal delay={60}>
|
||||||
<Placeholder
|
<div class="flex flex-col gap-3">
|
||||||
label="Education history"
|
<h3 class="font-mono text-xs uppercase tracking-[0.22em]" style="color: var(--color-accent);">
|
||||||
hint="Add degrees, institutions, and dates."
|
Education history
|
||||||
/>
|
</h3>
|
||||||
<Placeholder
|
<EducationHistory />
|
||||||
label="Certifications"
|
</div>
|
||||||
hint="SAP, cloud, or other relevant certifications."
|
</Reveal>
|
||||||
/>
|
<Reveal delay={120}>
|
||||||
</div>
|
<div class="flex flex-col gap-3">
|
||||||
|
<h3 class="font-mono text-xs uppercase tracking-[0.22em]" style="color: var(--color-accent);">
|
||||||
|
Certifications
|
||||||
|
</h3>
|
||||||
|
<Certifications />
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* ===== 6. Contact ===== */}
|
{/* ===== 6. Contact ===== */}
|
||||||
|
|||||||
Reference in New Issue
Block a user