burtenshaw
fix: layer hero title over graphic
46cc191
---
import HtmlEmbed from "./HtmlEmbed.astro";
import {
type HeroProps,
type Author,
normalizeAuthors,
stripHtml,
slugify,
} from "./heroUtils";
const {
title,
titleRaw,
description,
authors = [],
affiliations = [],
affiliation,
published,
doi,
pdfProOnly = false,
showPdf = true,
} = Astro.props as HeroProps;
const normalizedAuthors: Author[] = normalizeAuthors(authors as any);
const authorAffiliationIndexSet = new Set<number>();
for (const author of normalizedAuthors) {
const indices = Array.isArray(author.affiliationIndices)
? author.affiliationIndices
: [];
for (const idx of indices) {
if (typeof idx === "number") authorAffiliationIndexSet.add(idx);
}
}
const shouldShowAffiliationSupers = authorAffiliationIndexSet.size > 1;
const hasMultipleAffiliations =
Array.isArray(affiliations) && affiliations.length > 1;
const pdfBase = titleRaw ? stripHtml(titleRaw) : stripHtml(title);
const pdfFilename = `${slugify(pdfBase)}.pdf`;
---
<section class="hero">
<h1 class="hero-title" set:html={title} />
<div class="hero-banner">
<HtmlEmbed src="banner.html" frameless />
</div>
{description && <p class="hero-desc">{description}</p>}
</section>
<script is:inline>
(() => {
const el = document.querySelector(".hero-title");
if (!el) return;
const len = (el.textContent || "").length;
if (len > 100) el.dataset.titleSize = "sm";
else if (len > 60) el.dataset.titleSize = "md";
})();
</script>
<header class="meta" aria-label="Article meta information">
<div class="meta-container">
{
normalizedAuthors.length > 0 && (
<div class="meta-container-cell">
<h3>Author{normalizedAuthors.length > 1 ? "s" : ""}</h3>
<ul class="authors">
{normalizedAuthors.map((a, i) => {
const supers =
shouldShowAffiliationSupers &&
Array.isArray(a.affiliationIndices) &&
a.affiliationIndices.length ? (
<sup>{a.affiliationIndices.join(", ")}</sup>
) : null;
return (
<li>
{a.url ? <a href={a.url}>{a.name}</a> : a.name}{supers}{i < normalizedAuthors.length - 1 && <span set:html=",&nbsp;" />}
</li>
);
})}
</ul>
</div>
)
}
{
Array.isArray(affiliations) && affiliations.length > 0 && (
<div class="meta-container-cell meta-container-cell--affiliations">
<h3>Affiliation{affiliations.length > 1 ? "s" : ""}</h3>
{hasMultipleAffiliations ? (
<ol class="affiliations">
{affiliations.map((af) => (
<li value={af.id}>
{af.url ? (
<a href={af.url} target="_blank" rel="noopener noreferrer">
{af.name}
</a>
) : (
af.name
)}
</li>
))}
</ol>
) : (
<p>
{affiliations[0]?.url ? (
<a
href={affiliations[0].url}
target="_blank"
rel="noopener noreferrer"
>
{affiliations[0].name}
</a>
) : (
affiliations[0]?.name
)}
</p>
)}
</div>
)
}
{
(!affiliations || affiliations.length === 0) && affiliation && (
<div class="meta-container-cell meta-container-cell--affiliations">
<h3>Affiliation</h3>
<p>{affiliation}</p>
</div>
)
}
{
published && (
<div class="meta-container-cell meta-container-cell--published">
<h3>Published</h3>
<p>{published}</p>
</div>
)
}
{doi && (
<div class="meta-container-cell meta-container-cell--doi">
<h3>DOI</h3>
<p><a href={`https://doi.org/${doi}`} target="_blank" rel="noopener noreferrer">{doi}</a></p>
</div>
)}
{showPdf && (
<div class="meta-container-cell meta-container-cell--pdf">
<div class="pdf-header-wrapper">
<h3>PDF</h3>
<span class="pro-badge-wrapper" style="display: none;">
<span class="pro-badge-prefix">- you are</span>
<span class="pro-badge">PRO</span>
</span>
<span class="pro-only-label" style="display: none;">
<span class="pro-only-dash">-</span>
<span class="pro-only-text">pro only</span>
<svg class="pro-only-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
</svg>
</span>
</div>
<div id="pdf-download-container" data-pdf-pro-only={pdfProOnly.toString()}>
<p class="pdf-loading">Checking access...</p>
<p class="pdf-pro-only" style="display: none;">
<a
class="button"
href={`/${pdfFilename}`}
download={pdfFilename}
aria-label={`Download PDF ${pdfFilename}`}
>
Download PDF
</a>
</p>
<div class="pdf-locked" style="display: none;">
<a
class="button button-locked"
href="https://huggingface.co/subscribe/pro"
target="_blank"
rel="noopener noreferrer"
>
<svg class="lock-icon" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" role="img" width="1em" height="1em" viewBox="0 0 12 12" fill="none">
<path d="M6.48 1.26c0 1.55.67 2.58 1.5 3.24.86.68 1.9 1 2.58 1.07v.86A5.3 5.3 0 0 0 7.99 7.5a3.95 3.95 0 0 0-1.51 3.24h-.96c0-1.55-.67-2.58-1.5-3.24a5.3 5.3 0 0 0-2.58-1.07v-.86A5.3 5.3 0 0 0 4.01 4.5a3.95 3.95 0 0 0 1.51-3.24h.96Z" fill="currentColor"></path>
</svg>
<span class="locked-title">Subscribe to Pro</span>
</a>
</div>
</div>
</div>
)}
</div>
</header>
{showPdf && (
<script is:inline>
// PDF access control for Pro users only
const LOCAL_IS_PRO = true;
const FALLBACK_TIMEOUT_MS = 3000;
const MAX_RETRY_ATTEMPTS = 100;
let userPlanChecked = false;
let initialized = false;
function isProUser(plan) {
if (!plan) return false;
return plan.user === "pro";
}
function updatePdfAccess(isPro, pdfProOnly) {
const loadingEl = document.querySelector(".pdf-loading");
const proOnlyEl = document.querySelector(".pdf-pro-only");
const lockedEl = document.querySelector(".pdf-locked");
const proOnlyLabel = document.querySelector(".pro-only-label");
const proBadgeWrapper = document.querySelector(".pro-badge-wrapper");
if (loadingEl) loadingEl.style.display = "none";
if (!pdfProOnly) {
if (proOnlyEl) proOnlyEl.style.display = "block";
if (proOnlyLabel) proOnlyLabel.style.display = "none";
if (lockedEl) lockedEl.style.display = "none";
if (proBadgeWrapper) proBadgeWrapper.style.display = "none";
return;
}
if (isPro) {
if (proOnlyEl) proOnlyEl.style.display = "block";
if (proOnlyLabel) proOnlyLabel.style.display = "none";
if (lockedEl) lockedEl.style.display = "none";
if (proBadgeWrapper) proBadgeWrapper.style.display = "inline-flex";
} else {
if (proOnlyEl) proOnlyEl.style.display = "none";
if (proOnlyLabel) proOnlyLabel.style.display = "inline-flex";
if (lockedEl) lockedEl.style.display = "block";
if (proBadgeWrapper) proBadgeWrapper.style.display = "none";
}
}
function handleUserPlan(plan, pdfProOnly) {
userPlanChecked = true;
updatePdfAccess(isProUser(plan), pdfProOnly);
}
function handleFallback(pdfProOnly) {
handleUserPlan(LOCAL_IS_PRO ? { user: "pro" } : { user: "free" }, pdfProOnly);
}
function initPdfAccess(retryCount = 0) {
if (initialized) return;
const pdfContainer = document.querySelector("#pdf-download-container");
if (!pdfContainer) {
if (retryCount < MAX_RETRY_ATTEMPTS) {
setTimeout(() => initPdfAccess(retryCount + 1), 10);
} else {
setTimeout(() => {
const container = document.querySelector("#pdf-download-container");
if (container && !initialized) {
initialized = true;
const pdfProOnly = container.getAttribute("data-pdf-pro-only") === "true";
updatePdfAccess(!pdfProOnly, pdfProOnly);
}
}, 100);
}
return;
}
initialized = true;
const pdfProOnly = pdfContainer.getAttribute("data-pdf-pro-only") === "true";
if (!pdfProOnly) {
updatePdfAccess(true, pdfProOnly);
} else {
window.addEventListener("message", (event) => {
if (event.data.type === "USER_PLAN") handleUserPlan(event.data.plan, pdfProOnly);
});
if (window.parent && window.parent !== window) {
window.parent.postMessage({ type: "USER_PLAN_REQUEST" }, "*");
setTimeout(() => { if (!userPlanChecked) handleFallback(pdfProOnly); }, FALLBACK_TIMEOUT_MS);
} else {
handleFallback(pdfProOnly);
}
}
}
(function() {
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => setTimeout(() => initPdfAccess(), 0));
} else {
setTimeout(() => initPdfAccess(), 0);
}
})();
</script>
)}
<style>
.hero {
position: relative;
width: 100%;
min-height: 100svh;
padding: clamp(56px, 9vh, 96px) 16px clamp(32px, 7vh, 72px);
text-align: center;
display: flex;
flex-direction: column;
justify-content: center;
gap: 16px;
overflow: hidden;
isolation: isolate;
}
.hero-title {
position: relative;
z-index: 2;
font-size: clamp(34px, 5vw, 54px);
font-weight: 800;
line-height: 1.12;
letter-spacing: 0;
margin: 0 auto;
max-width: 780px;
text-wrap: balance;
text-shadow: 0 2px 28px color-mix(in srgb, var(--page-bg) 92%, transparent);
}
.hero-title[data-title-size="md"] {
font-size: clamp(28px, 4vw, 44px);
}
.hero-title[data-title-size="sm"] {
font-size: clamp(24px, 3.2vw, 38px);
}
.hero-banner {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
overflow: hidden;
z-index: 0;
}
.hero-banner::after {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
z-index: 1;
background:
radial-gradient(ellipse at center, color-mix(in srgb, var(--page-bg) 70%, transparent) 0%, color-mix(in srgb, var(--page-bg) 36%, transparent) 28%, transparent 54%),
linear-gradient(to bottom, color-mix(in srgb, var(--page-bg) 30%, transparent) 0%, transparent 34%),
linear-gradient(to top, var(--page-bg) 0%, transparent 22%);
}
.hero-banner :global(.html-embed) {
height: 100%;
}
.hero-banner :global(.html-embed__card) {
height: 100%;
}
.hero-desc {
position: relative;
z-index: 2;
color: var(--muted-color);
font-style: italic;
margin: 0 auto;
max-width: min(680px, 90%);
text-shadow: 0 1px 18px color-mix(in srgb, var(--page-bg) 96%, transparent);
}
@media (max-width: 768px) {
.hero {
min-height: 100svh;
padding-top: clamp(48px, 9vh, 72px);
}
}
/* Meta bar */
.meta {
border-top: 1px solid var(--border-color);
border-bottom: 1px solid var(--border-color);
padding: 1rem 0;
font-size: 0.9rem;
}
.meta-container {
max-width: 980px;
display: flex;
flex-direction: row;
justify-content: space-between;
margin: 0 auto;
padding: 0 var(--content-padding-x);
gap: 8px;
flex-wrap: wrap;
row-gap: 12px;
}
.meta-container a:not(.button) {
color: var(--primary-color);
text-decoration: underline;
text-underline-offset: 2px;
text-decoration-thickness: 0.06em;
text-decoration-color: var(--link-underline);
transition: text-decoration-color 0.15s ease-in-out;
}
.meta-container a:hover {
text-decoration-color: var(--link-underline-hover);
}
.meta-container a.button,
.meta-container .button {
text-decoration: none;
}
.meta-container-cell {
display: flex;
flex-direction: column;
gap: 8px;
max-width: 400px;
}
.meta-container-cell h3 {
margin: 0;
font-size: 12px;
font-weight: 400;
color: var(--muted-color);
text-transform: uppercase;
letter-spacing: 0.02em;
}
.meta-container-cell p {
margin: 0;
}
.authors {
margin: 0;
list-style-type: none;
padding-left: 0;
display: flex;
flex-wrap: wrap;
}
.authors li {
white-space: nowrap;
padding: 0;
}
.affiliations {
margin: 0;
padding-left: 1.25em;
}
.affiliations li {
margin: 0;
}
@media (max-width: 768px) {
.meta-container-cell:nth-child(even) { text-align: right; }
.meta-container-cell:last-child:nth-child(odd) {
flex-grow: 0;
flex-basis: auto;
margin-left: auto;
text-align: right;
}
}
@media print {
.meta-container-cell--pdf { display: none !important; }
}
/* PDF styles */
.pdf-header-wrapper {
display: flex;
align-items: center;
gap: 6px;
line-height: 1;
}
.pdf-header-wrapper h3 { line-height: 1; }
#pdf-download-container {
min-height: calc(var(--button-padding-y) * 2 + var(--button-font-size) + 2px);
}
.pdf-loading {
color: var(--muted-color);
font-size: var(--button-font-size);
line-height: 1;
padding: var(--button-padding-y) var(--button-padding-x);
margin: 0;
display: inline-block;
box-sizing: border-box;
border: 1px solid transparent;
height: calc(var(--button-padding-y) * 2 + var(--button-font-size) + 2px);
vertical-align: top;
}
.pdf-pro-only { margin: 0; line-height: 0; }
.pdf-pro-only .button { margin: 0; }
.pro-badge-wrapper {
display: inline-flex;
align-items: center;
gap: 5px;
font-style: normal;
}
.pro-badge-prefix {
font-size: 0.85em;
opacity: 0.5;
font-weight: 400;
font-style: normal;
}
.pro-badge {
display: inline-block;
border: 1px solid rgba(0, 0, 0, 0.025);
background: linear-gradient(to bottom right, #f9a8d4, #86efac, #fde047);
color: black;
padding: 1px 5px;
border-radius: 3px;
font-size: 0.5rem;
font-weight: 700;
font-style: normal;
letter-spacing: 0.025em;
text-transform: uppercase;
}
:global(.dark) .pro-badge,
:global([data-theme="dark"]) .pro-badge {
background: linear-gradient(to bottom right, #ec4899, #22c55e, #eab308);
border-color: rgba(255, 255, 255, 0.15);
}
.pro-only-label {
display: inline-flex;
flex-direction: row;
align-items: center;
gap: 5px;
font-size: 0.85em;
opacity: 0.5;
font-weight: 400;
line-height: 1;
}
.pro-only-dash { display: inline-flex; align-items: center; line-height: 1; }
.pro-only-icon {
width: 11px;
height: 11px;
flex-shrink: 0;
display: inline-flex;
align-items: center;
}
.pro-only-text { display: inline-flex; align-items: center; line-height: 1; }
.pdf-locked { display: block; }
.button-locked {
display: inline-flex;
align-items: center;
gap: 6px;
background: linear-gradient(135deg,
var(--primary-color) 0%,
oklch(from var(--primary-color) calc(l - 0.1) calc(c + 0.05) calc(h - 60)) 100%);
border-radius: var(--button-radius);
padding: var(--button-padding-y) var(--button-padding-x);
font-size: var(--button-font-size);
line-height: 1;
color: var(--on-primary);
position: relative;
overflow: hidden;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
font-weight: normal;
border-color: rgba(0, 0, 0, 0.15);
}
.button-locked:active { transform: translateY(0); }
.lock-icon { font-size: 1em; flex-shrink: 0; position: relative; z-index: 1; }
.locked-title { position: relative; z-index: 1; }
@media (max-width: 768px) {
.meta-container-cell--pdf {
display: flex;
flex-direction: column;
align-items: flex-end;
}
}
</style>