033c9ec315
- vitest: dictionary parity, slug-map bijection, content-schema validation, JSON-LD (23) - playwright: both locales, language-switch mapping (incl. no-JS), CV downloads, form happy-path + honeypot, axe a11y on 5 templates x 2 themes (23) - fix: raise ink-faint + light accent to meet WCAG AA 4.5:1 (axe-verified) - fix: gate reveal animations behind html.js so content is visible without JS (progressive enhancement - was hidden at opacity 0); scan reduced-motion path - fix: validate contact form before the anti-bot time-trap (no false success) - chore: ESLint node globals, typed diagram props, resvg fontFiles, prettier pass Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
99 lines
3.3 KiB
Plaintext
99 lines
3.3 KiB
Plaintext
---
|
||
import type { Locale } from '@i18n/locales';
|
||
import { useDict } from '@i18n/t';
|
||
import { homeAnchor } from '@i18n/slugMap';
|
||
import { getExperience } from '@lib/content';
|
||
import SectionLabel from '@components/ui/SectionLabel.astro';
|
||
|
||
interface Props {
|
||
locale: Locale;
|
||
eyebrow?: boolean; // false on the standalone experience page (it has its own H1)
|
||
}
|
||
const { locale, eyebrow = true } = Astro.props;
|
||
const d = useDict(locale);
|
||
const items = getExperience(locale);
|
||
const firstAlongside = items.findIndex((i) => i.alongside);
|
||
|
||
const period = (from: number, to: number | null) => `${from}–${to ?? d.home.present}`;
|
||
---
|
||
|
||
<section
|
||
id={homeAnchor('experience', locale)}
|
||
class:list={['mx-auto max-w-[--content-max] px-6', eyebrow && 'pt-24']}
|
||
data-reveal
|
||
>
|
||
{
|
||
eyebrow && (
|
||
<>
|
||
<SectionLabel number="03" text={d.home.experienceLabel} />
|
||
<h2 class="text-h2 mt-4">{d.home.experienceHeading}</h2>
|
||
</>
|
||
)
|
||
}
|
||
|
||
<div class="relative mt-10 pl-8">
|
||
<span
|
||
class="timeline-spine bg-line-strong absolute top-2 bottom-2 left-[5px] w-px origin-top"
|
||
aria-hidden="true"></span>
|
||
|
||
{
|
||
items.map((item, i) => (
|
||
<>
|
||
{i === firstAlongside && (
|
||
<p class="text-mono-label text-ink-faint mt-6 mb-3 font-mono font-medium tracking-[0.08em] uppercase">
|
||
{d.home.earlierRoles}
|
||
</p>
|
||
)}
|
||
|
||
{item.emphasis === 'featured' ? (
|
||
<div class="relative pb-10">
|
||
<span
|
||
class="bg-accent ring-surface-0 absolute top-1.5 -left-8 h-2.5 w-2.5 rounded-full ring-4"
|
||
aria-hidden="true"
|
||
/>
|
||
<h3 class="text-h4 text-ink font-semibold">
|
||
{item.role} — {item.employer}
|
||
</h3>
|
||
<p class="text-mono-meta text-ink-faint mt-1 font-mono">
|
||
{period(item.period.from, item.period.to)} · {item.location}
|
||
{item.progression &&
|
||
` · ${item.progression.map((p) => `${p.from}–${p.to} ${p.label}`).join(' → ')}`}
|
||
</p>
|
||
{item.summary && <p class="text-body text-ink-muted mt-3">{item.summary}</p>}
|
||
{item.highlights && (
|
||
<ul class="mt-3 flex flex-col gap-2">
|
||
{item.highlights.map((h) => (
|
||
<li class="text-body text-ink-muted flex gap-3">
|
||
<span
|
||
class="bg-accent mt-2 h-1 w-1 shrink-0 rounded-full"
|
||
aria-hidden="true"
|
||
/>
|
||
<span>{h}</span>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
)}
|
||
</div>
|
||
) : (
|
||
<div class="relative py-2.5">
|
||
<span
|
||
class="bg-ink-faint ring-surface-0 absolute top-4 -left-8 h-2 w-2 rounded-full ring-4"
|
||
aria-hidden="true"
|
||
/>
|
||
<p class="text-small text-ink-muted">
|
||
<span class="text-ink-faint font-mono">
|
||
{period(item.period.from, item.period.to)}
|
||
</span>
|
||
<span class="mx-2">·</span>
|
||
<span class="text-ink">{item.role}</span>
|
||
<span class="mx-2">·</span>
|
||
{item.employer}
|
||
</p>
|
||
</div>
|
||
)}
|
||
</>
|
||
))
|
||
}
|
||
</div>
|
||
</section>
|