Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions astro-docs/src/components/CtaLink.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
---
/**
* CtaLink — the house CTA pill for external destinations, in its two shapes:
* `primary` = solid fill (Events "Register to attend"), `ghost` = outline
* (resource link-outs). The ONE copy of the pill — EventLinks, ResourceEmbed
* and ConsentEmbed each carried a near-verbatim clone before — so a style
* tweak lands here once. Always opens in a new tab with
* `rel="noopener noreferrer"` and the shared external-link glyph; the slot
* carries the visible label.
*
* Consumes the `--ev-*` tokens the mounting surface seeds (EventsLayout /
* ResourcesLayout), each with a light-theme fallback so the pill never renders
* with an undefined token when reused outside those roots (ConsentEmbed mounts
* on any embed surface — Epic 10/11 reuse).
*/
import ExternalLinkIcon from './ExternalLinkIcon.astro';

interface Props {
href: string;
variant: 'primary' | 'ghost';
}

const { href, variant } = Astro.props;
---

<a
class={`cta cta--${variant}`}
href={href}
target="_blank"
rel="noopener noreferrer"
>
<slot />
<ExternalLinkIcon />
</a>

<style>
.cta {
display: inline-flex;
align-items: center;
gap: 0.45rem;
padding: 0.5rem 1rem;
border-radius: 9999px;
font-size: 0.9rem;
font-weight: 800;
text-decoration: none;
}

/* Primary CTA — white on teal-700 (light) / near-black on teal-400 (dark). */
.cta--primary {
background: var(--ev-cta-bg, #046151);
color: var(--ev-cta-fg, #ffffff);
}

/* Ghost CTA — transparent fill, accent text + ≥3:1 accent border. */
.cta--ghost {
background: transparent;
color: var(--ev-accent-text, #046151);
border: 1.5px solid var(--ev-accent-border, #046151);
}

.cta:focus-visible {
outline: 2px solid var(--ev-focus-ring, #017b64);
outline-offset: 2px;
}
</style>
33 changes: 33 additions & 0 deletions astro-docs/src/components/ExternalLinkIcon.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
/**
* The house 14×14 external-link glyph — the ONE copy. It was inlined verbatim
* in EventLinks, DocLinks, ResourceEmbed and ConsentEmbed; every external-link
* affordance renders this component instead so a glyph tweak lands once.
* Decorative (`aria-hidden`): the surrounding link's text names the
* destination. Colored by the parent via `currentColor`.
*/
---

<svg
width="14"
height="14"
viewBox="0 0 14 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<path
d="M5.25 2.625H2.625A.875.875 0 0 0 1.75 3.5v7.875a.875.875 0 0 0 .875.875H10.5a.875.875 0 0 0 .875-.875V8.75M8.75 1.75h3.5v3.5M7 7l5.25-5.25"
stroke="currentColor"
stroke-width="1.25"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>

<style>
/* The glyph rides inside inline-flex pills/rows — never let it shrink. */
svg {
flex-shrink: 0;
}
</style>
79 changes: 78 additions & 1 deletion astro-docs/src/components/events/EventCard.astro
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import type { CollectionEntry } from 'astro:content';

import { blogAuthors } from '../../data/team';
import { urlForEntry } from '../../lib/cross-links';
import { formatDisplay } from '../../schema/event-date';
import EventAttribution from './EventAttribution.astro';
import EventLinks from './EventLinks.astro';
Expand All @@ -16,9 +17,16 @@ import {
interface Props {
event: CollectionEntry<'events'>;
timeframe: 'upcoming' | 'past';
/**
* This event's produced Resources — derived (never hand-authored) and
* pre-sorted by `EventList` via `indexResourcesByOriginEvent`, so each card
* renders its slice without re-scanning the full resource set. Sparse — an
* empty list renders no block.
*/
producedResources: CollectionEntry<'resources'>[];
}

const { event, timeframe } = Astro.props;
const { event, timeframe, producedResources } = Astro.props;
const {
title,
participation,
Expand Down Expand Up @@ -74,6 +82,32 @@ const place = online ? 'Online' : location;
</div>

<EventLinks links={links} timeframe={timeframe} />

{
/*
9.4 Event→Resources: a DISTINCT, separately-labelled block from the
external `resourceUrl` CTA above (complement, not a duplicate "watch"
link). Each item is a same-tab INTERNAL cross-link to the Resources
library (coarse `/resources/` target, meaningful resource-title label,
no external glyph, no target=_blank). Renders only when non-empty.
*/
producedResources.length > 0 && (
<div class="event-produced">
<p class="event-produced-label" id={`produced-${event.id}`}>
Resources from this event
</p>
<ul class="event-produced-list" aria-labelledby={`produced-${event.id}`}>
{producedResources.map((resource) => (
<li class="event-produced-item">
<a class="event-produced-link" href={urlForEntry(resource)}>
{resource.data.title}
</a>
</li>
))}
</ul>
</div>
)
}
</div>
</article>

Expand Down Expand Up @@ -219,4 +253,47 @@ const place = online ? 'Online' : location;
font-weight: 400;
color: var(--ev-fg-muted);
}

/*
Event→Resources block (9.4). A distinct, labelled list of same-tab INTERNAL
cross-links — separate from the external `resourceUrl` CTA in EventLinks.
*/
.event-produced {
margin-top: 0.5rem;
padding-top: 0.5rem;
border-top: 1px solid var(--ev-line, #e5e7eb);
}

.event-produced-label {
margin: 0 0 0.35rem;
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--ev-fg-faint, #6b7280);
}

.event-produced-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.25rem;
}

.event-produced-link {
color: var(--ev-accent-text, #046151);
font-size: 0.95rem;
font-weight: 600;
text-decoration: underline;
text-underline-offset: 2px;
border-radius: 0.25rem;
}

.event-produced-link:focus-visible {
outline: 2px solid var(--ev-focus-ring, #017b64);
outline-offset: 2px;
}
</style>
67 changes: 7 additions & 60 deletions astro-docs/src/components/events/EventLinks.astro
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
---
import CtaLink from '../CtaLink.astro';
import type { EventCardLink } from './eventMeta';

interface Props {
Expand All @@ -9,41 +10,20 @@ interface Props {
const { links, timeframe } = Astro.props;

// Upcoming "Register to attend" is the primary (solid) CTA; past "View
// resource" is the ghost (outline) CTA. Both are full pills with a visible
// external-link glyph. Purely presentational — the resolved links (label/url)
// are unchanged (see resolveEventLinks + its tests).
const ctaClass = timeframe === 'upcoming' ? 'cta cta--primary' : 'cta cta--ghost';
// resource" is the ghost (outline) CTA — the shared CtaLink pill owns both
// shapes. Purely presentational — the resolved links (label/url) are
// unchanged (see resolveEventLinks + its tests).
const variant = timeframe === 'upcoming' ? 'primary' : 'ghost';
---

{
links.length > 0 && (
<ul class="event-links-list">
{links.map((link) => (
<li class="event-links-item">
<a
class={ctaClass}
href={link.url}
target="_blank"
rel="noopener noreferrer"
>
<CtaLink href={link.url} variant={variant}>
{link.label}
<svg
width="14"
height="14"
viewBox="0 0 14 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<path
d="M5.25 2.625H2.625A.875.875 0 0 0 1.75 3.5v7.875a.875.875 0 0 0 .875.875H10.5a.875.875 0 0 0 .875-.875V8.75M8.75 1.75h3.5v3.5M7 7l5.25-5.25"
stroke="currentColor"
stroke-width="1.25"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</a>
</CtaLink>
</li>
))}
</ul>
Expand All @@ -64,37 +44,4 @@ const ctaClass = timeframe === 'upcoming' ? 'cta cta--primary' : 'cta cta--ghost
.event-links-item {
display: flex;
}

.cta {
display: inline-flex;
align-items: center;
gap: 0.45rem;
padding: 0.5rem 1rem;
border-radius: 9999px;
font-size: 0.9rem;
font-weight: 800;
text-decoration: none;
}

.cta svg {
flex-shrink: 0;
}

/* Primary CTA — white on teal-700 (light) / near-black on teal-400 (dark). */
.cta--primary {
background: var(--ev-cta-bg);
color: var(--ev-cta-fg);
}

/* Ghost CTA — transparent fill, accent text + ≥3:1 accent border. */
.cta--ghost {
background: transparent;
color: var(--ev-accent-text);
border: 1.5px solid var(--ev-accent-border);
}

.cta:focus-visible {
outline: 2px solid var(--ev-focus-ring);
outline-offset: 2px;
}
</style>
22 changes: 20 additions & 2 deletions astro-docs/src/components/events/EventList.astro
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
---
import type { CollectionEntry } from 'astro:content';

import { indexResourcesByOriginEvent } from '../../lib/cross-links';
import { classify, type EventDate } from '../../schema/event-date';
import { type ParticipationType } from '../../schema/events';
import EventCard from './EventCard.astro';
Expand All @@ -17,13 +18,17 @@ type TypeFilter = ParticipationType | 'all';
interface Props {
allEvents: EventEntry[];
events: EventEntry[];
allResources: CollectionEntry<'resources'>[];
eventsById: Map<string, EventEntry>;
currentType?: TypeFilter;
buildDate?: Date;
}

const {
allEvents,
events,
allResources,
eventsById,
currentType = 'all',
buildDate = new Date(),
} = Astro.props;
Expand Down Expand Up @@ -51,6 +56,11 @@ past.sort((a, b) =>
),
);

// Derived produced-Resources (never hand-authored), grouped ONCE for the whole
// page via the single shared cross-link module — each card receives its
// pre-sorted slice instead of re-scanning the full resource set per card.
const resourcesByEvent = indexResourcesByOriginEvent(allResources, eventsById);

const typeCounts = allEvents.reduce<Record<ParticipationType, number>>(
(counts, e) => {
counts[e.data.participation] += 1;
Expand Down Expand Up @@ -93,7 +103,11 @@ const typeCounts = allEvents.reduce<Record<ParticipationType, number>>(
upcoming.length > 0 ? (
<div class="event-stack">
{upcoming.map((event) => (
<EventCard event={event} timeframe="upcoming" />
<EventCard
event={event}
timeframe="upcoming"
producedResources={resourcesByEvent.get(event.id) ?? []}
/>
))}
</div>
) : (
Expand All @@ -108,7 +122,11 @@ const typeCounts = allEvents.reduce<Record<ParticipationType, number>>(
past.length > 0 ? (
<div class="event-stack">
{past.map((event) => (
<EventCard event={event} timeframe="past" />
<EventCard
event={event}
timeframe="past"
producedResources={resourcesByEvent.get(event.id) ?? []}
/>
))}
</div>
) : (
Expand Down
Loading
Loading