tfrere HF Staff commited on
Commit
54b202e
·
1 Parent(s): 0034f24

refactor: split Hero into HeroArticle + HeroPaper with shared heroUtils

Browse files

- Extract shared logic (normalizeAuthors, slugify, stripHtml, link icons)
into heroUtils.ts
- HeroArticle.astro: article template with banner, flex-row meta bar, PDF
access control - self-contained CSS, no external overrides
- HeroPaper.astro: paper template with centered inline meta, external link
pills with auto-detected icons - self-contained CSS, zero !important
- index.astro conditionally renders the right component based on template
- Remove ~90 lines of :global([data-template="paper"]) CSS overrides from
_layout.css (hero/meta rules only; content-grid rules kept)
- Delete old monolithic Hero.astro (843 lines, 24 :global overrides,
13 !important)

Made-with: Cursor

app/src/components/{Hero.astro → HeroArticle.astro} RENAMED
@@ -1,21 +1,12 @@
1
  ---
2
  import HtmlEmbed from "./HtmlEmbed.astro";
3
-
4
- interface Props {
5
- title: string; // may contain HTML (e.g., <br/>)
6
- titleRaw?: string; // plain title for slug/PDF (optional)
7
- description?: string;
8
- authors?: Array<
9
- string | { name: string; url?: string; affiliationIndices?: number[] }
10
- >;
11
- affiliations?: Array<{ id: number; name: string; url?: string }>;
12
- affiliation?: string; // legacy single affiliation
13
- published?: string;
14
- doi?: string;
15
- pdfProOnly?: boolean; // Gate PDF download to Pro users only
16
- showPdf?: boolean; // Show or hide PDF section in metadata
17
- links?: Array<{ label: string; url: string; icon?: string }>;
18
- }
19
 
20
  const {
21
  title,
@@ -28,95 +19,25 @@ const {
28
  doi,
29
  pdfProOnly = false,
30
  showPdf = true,
31
- links = [],
32
- } = Astro.props as Props;
33
-
34
- type Author = { name: string; url?: string; affiliationIndices?: number[] };
35
-
36
- function normalizeAuthors(
37
- input: Array<
38
- | string
39
- | {
40
- name?: string;
41
- url?: string;
42
- link?: string;
43
- affiliationIndices?: number[];
44
- }
45
- >,
46
- ): Author[] {
47
- return (Array.isArray(input) ? input : [])
48
- .map((a) => {
49
- if (typeof a === "string") {
50
- return { name: a } as Author;
51
- }
52
- const name = (a?.name ?? "").toString();
53
- const url = (a?.url ?? a?.link) as string | undefined;
54
- const affiliationIndices = Array.isArray((a as any)?.affiliationIndices)
55
- ? (a as any).affiliationIndices
56
- : undefined;
57
- return { name, url, affiliationIndices } as Author;
58
- })
59
- .filter((a) => a.name && a.name.trim().length > 0);
60
- }
61
 
62
  const normalizedAuthors: Author[] = normalizeAuthors(authors as any);
63
 
64
- // Determine if affiliation superscripts should be shown (only when there are multiple distinct affiliations referenced by authors)
65
  const authorAffiliationIndexSet = new Set<number>();
66
  for (const author of normalizedAuthors) {
67
  const indices = Array.isArray(author.affiliationIndices)
68
  ? author.affiliationIndices
69
  : [];
70
  for (const idx of indices) {
71
- if (typeof idx === "number") {
72
- authorAffiliationIndexSet.add(idx);
73
- }
74
  }
75
  }
76
  const shouldShowAffiliationSupers = authorAffiliationIndexSet.size > 1;
77
  const hasMultipleAffiliations =
78
  Array.isArray(affiliations) && affiliations.length > 1;
79
 
80
- function stripHtml(text: string): string {
81
- return String(text || "").replace(/<[^>]*>/g, "");
82
- }
83
-
84
- function slugify(text: string): string {
85
- return (
86
- String(text || "")
87
- .normalize("NFKD")
88
- .replace(/\p{Diacritic}+/gu, "")
89
- .toLowerCase()
90
- .replace(/[^a-z0-9]+/g, "-")
91
- .replace(/^-+|-+$/g, "")
92
- .slice(0, 120) || "article"
93
- );
94
- }
95
-
96
  const pdfBase = titleRaw ? stripHtml(titleRaw) : stripHtml(title);
97
  const pdfFilename = `${slugify(pdfBase)}.pdf`;
98
-
99
- const LINK_ICONS: Record<string, string> = {
100
- github: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0 0 24 12c0-6.63-5.37-12-12-12Z"/></svg>`,
101
- paper: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>`,
102
- arxiv: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>`,
103
- code: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>`,
104
- demo: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>`,
105
- data: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/></svg>`,
106
- dataset: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/></svg>`,
107
- model: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></svg>`,
108
- video: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"/></svg>`,
109
- blog: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg>`,
110
- };
111
-
112
- function getLinkIcon(label: string, icon?: string): string | null {
113
- if (icon && LINK_ICONS[icon.toLowerCase()]) return LINK_ICONS[icon.toLowerCase()];
114
- const key = label.toLowerCase().trim();
115
- for (const [keyword, svg] of Object.entries(LINK_ICONS)) {
116
- if (key === keyword || key.includes(keyword)) return svg;
117
- }
118
- return null;
119
- }
120
  ---
121
 
122
  <section class="hero">
@@ -265,57 +186,29 @@ function getLinkIcon(label: string, icon?: string): string | null {
265
  </div>
266
  )}
267
  </div>
268
- {links && links.length > 0 && (
269
- <nav class="hero-links" aria-label="External links">
270
- {links.map((link) => {
271
- const iconSvg = getLinkIcon(link.label, link.icon);
272
- return (
273
- <a href={link.url} class="hero-link" target="_blank" rel="noopener noreferrer">
274
- {iconSvg && <span class="hero-link-icon" set:html={iconSvg} />}
275
- {link.label}
276
- </a>
277
- );
278
- })}
279
- </nav>
280
- )}
281
  </header>
282
 
283
  {showPdf && (
284
  <script is:inline>
285
  // PDF access control for Pro users only
286
-
287
- // ⚙️ Configuration for local development
288
- const LOCAL_IS_PRO = true; // Set to true to test Pro access locally
289
-
290
  const FALLBACK_TIMEOUT_MS = 3000;
291
- const MAX_RETRY_ATTEMPTS = 100; // Maximum 1s of retries (100 * 10ms)
292
  let userPlanChecked = false;
293
  let initialized = false;
294
 
295
- /**
296
- * Check if user has Pro access
297
- * Isolated logic for Pro user verification
298
- * Expected plan structure: { user: "pro", org: "enterprise" }
299
- */
300
  function isProUser(plan) {
301
  if (!plan) return false;
302
  return plan.user === "pro";
303
  }
304
 
305
- /**
306
- * Update UI based on user's Pro status
307
- */
308
  function updatePdfAccess(isPro, pdfProOnly) {
309
  const loadingEl = document.querySelector(".pdf-loading");
310
  const proOnlyEl = document.querySelector(".pdf-pro-only");
311
  const lockedEl = document.querySelector(".pdf-locked");
312
  const proOnlyLabel = document.querySelector(".pro-only-label");
313
  const proBadgeWrapper = document.querySelector(".pro-badge-wrapper");
314
-
315
- // Hide loading state
316
  if (loadingEl) loadingEl.style.display = "none";
317
-
318
- // If PDF Pro gating is disabled, just show the download button
319
  if (!pdfProOnly) {
320
  if (proOnlyEl) proOnlyEl.style.display = "block";
321
  if (proOnlyLabel) proOnlyLabel.style.display = "none";
@@ -323,8 +216,6 @@ function getLinkIcon(label: string, icon?: string): string | null {
323
  if (proBadgeWrapper) proBadgeWrapper.style.display = "none";
324
  return;
325
  }
326
-
327
- // Show appropriate state based on Pro status
328
  if (isPro) {
329
  if (proOnlyEl) proOnlyEl.style.display = "block";
330
  if (proOnlyLabel) proOnlyLabel.style.display = "none";
@@ -338,46 +229,22 @@ function getLinkIcon(label: string, icon?: string): string | null {
338
  }
339
  }
340
 
341
- /**
342
- * Handle user plan response
343
- */
344
  function handleUserPlan(plan, pdfProOnly) {
345
  userPlanChecked = true;
346
- const isPro = isProUser(plan);
347
- updatePdfAccess(isPro, pdfProOnly);
348
-
349
-
350
  }
351
 
352
- /**
353
- * Fallback behavior when no parent window responds
354
- * Uses LOCAL_IS_PRO configuration for local development
355
- */
356
  function handleFallback(pdfProOnly) {
357
- if (LOCAL_IS_PRO) {
358
- handleUserPlan({ user: "pro" }, pdfProOnly);
359
- } else {
360
- handleUserPlan({ user: "free" }, pdfProOnly);
361
- }
362
  }
363
 
364
- /**
365
- * Initialize PDF access control
366
- */
367
  function initPdfAccess(retryCount = 0) {
368
- // Prevent multiple initializations
369
  if (initialized) return;
370
-
371
- // Check if PDF Pro gating is enabled
372
  const pdfContainer = document.querySelector("#pdf-download-container");
373
-
374
  if (!pdfContainer) {
375
- // Retry with limit to avoid infinite loop
376
  if (retryCount < MAX_RETRY_ATTEMPTS) {
377
  setTimeout(() => initPdfAccess(retryCount + 1), 10);
378
  } else {
379
- console.warn("[PDF Access] Container not found after max retries");
380
- // Try one more time after a longer delay as fallback
381
  setTimeout(() => {
382
  const container = document.querySelector("#pdf-download-container");
383
  if (container && !initialized) {
@@ -389,50 +256,27 @@ function getLinkIcon(label: string, icon?: string): string | null {
389
  }
390
  return;
391
  }
392
-
393
  initialized = true;
394
  const pdfProOnly = pdfContainer.getAttribute("data-pdf-pro-only") === "true";
395
-
396
- // If PDF Pro gating is disabled, show the download button immediately
397
  if (!pdfProOnly) {
398
  updatePdfAccess(true, pdfProOnly);
399
  } else {
400
- // Listen for messages from parent window (Hugging Face Spaces)
401
- // Use a named function to avoid duplicate listeners
402
- const messageHandler = (event) => {
403
- if (event.data.type === "USER_PLAN") {
404
- handleUserPlan(event.data.plan, pdfProOnly);
405
- }
406
- };
407
-
408
- window.addEventListener("message", messageHandler);
409
-
410
- // Request user plan on page load
411
  if (window.parent && window.parent !== window) {
412
- // We're in an iframe, request user plan
413
  window.parent.postMessage({ type: "USER_PLAN_REQUEST" }, "*");
414
-
415
- // Fallback if no response after timeout
416
- setTimeout(() => {
417
- if (!userPlanChecked) {
418
- handleFallback(pdfProOnly);
419
- }
420
- }, FALLBACK_TIMEOUT_MS);
421
  } else {
422
- // Not in iframe (local development), use fallback immediately
423
  handleFallback(pdfProOnly);
424
  }
425
  }
426
  }
427
 
428
- // Initialize when DOM is ready
429
  (function() {
430
  if (document.readyState === "loading") {
431
- document.addEventListener("DOMContentLoaded", () => {
432
- setTimeout(() => initPdfAccess(), 0);
433
- });
434
  } else {
435
- // DOM already loaded, but wait a bit for Astro to render
436
  setTimeout(() => initPdfAccess(), 0);
437
  }
438
  })();
@@ -440,7 +284,6 @@ function getLinkIcon(label: string, icon?: string): string | null {
440
  )}
441
 
442
  <style>
443
- /* Hero (full-width) */
444
  .hero {
445
  width: 100%;
446
  padding: 64px 16px 16px;
@@ -480,15 +323,11 @@ function getLinkIcon(label: string, icon?: string): string | null {
480
  }
481
 
482
  @media (max-width: 768px) {
483
- .hero-banner {
484
- aspect-ratio: auto;
485
- }
486
- .hero-desc {
487
- max-width: 90%;
488
- }
489
  }
490
 
491
- /* Meta (byline-like header) */
492
  .meta {
493
  border-top: 1px solid var(--border-color);
494
  border-bottom: 1px solid var(--border-color);
@@ -503,8 +342,9 @@ function getLinkIcon(label: string, icon?: string): string | null {
503
  margin: 0 auto;
504
  padding: 0 var(--content-padding-x);
505
  gap: 8px;
 
 
506
  }
507
- /* Subtle underline for links in meta; keep buttons without underline */
508
  .meta-container a:not(.button) {
509
  color: var(--primary-color);
510
  text-decoration: underline;
@@ -546,7 +386,7 @@ function getLinkIcon(label: string, icon?: string): string | null {
546
  }
547
  .authors li {
548
  white-space: nowrap;
549
- padding:0;
550
  }
551
  .affiliations {
552
  margin: 0;
@@ -556,15 +396,8 @@ function getLinkIcon(label: string, icon?: string): string | null {
556
  margin: 0;
557
  }
558
 
559
- header.meta .meta-container {
560
- flex-wrap: wrap;
561
- row-gap: 12px;
562
- }
563
-
564
  @media (max-width: 768px) {
565
- .meta-container-cell:nth-child(even) {
566
- text-align: right;
567
- }
568
  .meta-container-cell:last-child:nth-child(odd) {
569
  flex-grow: 0;
570
  flex-basis: auto;
@@ -574,27 +407,20 @@ function getLinkIcon(label: string, icon?: string): string | null {
574
  }
575
 
576
  @media print {
577
- .meta-container-cell--pdf {
578
- display: none !important;
579
- }
580
  }
581
 
582
- /* PDF access control styles */
583
  .pdf-header-wrapper {
584
  display: flex;
585
  align-items: center;
586
  gap: 6px;
587
  line-height: 1;
588
  }
589
-
590
- .pdf-header-wrapper h3 {
591
- line-height: 1;
592
- }
593
-
594
  #pdf-download-container {
595
  min-height: calc(var(--button-padding-y) * 2 + var(--button-font-size) + 2px);
596
  }
597
-
598
  .pdf-loading {
599
  color: var(--muted-color);
600
  font-size: var(--button-font-size);
@@ -607,30 +433,20 @@ function getLinkIcon(label: string, icon?: string): string | null {
607
  height: calc(var(--button-padding-y) * 2 + var(--button-font-size) + 2px);
608
  vertical-align: top;
609
  }
610
-
611
- .pdf-pro-only {
612
- margin: 0;
613
- line-height: 0;
614
- }
615
-
616
- .pdf-pro-only .button {
617
- margin: 0;
618
- }
619
-
620
  .pro-badge-wrapper {
621
  display: inline-flex;
622
  align-items: center;
623
  gap: 5px;
624
  font-style: normal;
625
  }
626
-
627
  .pro-badge-prefix {
628
  font-size: 0.85em;
629
  opacity: 0.5;
630
  font-weight: 400;
631
  font-style: normal;
632
  }
633
-
634
  .pro-badge {
635
  display: inline-block;
636
  border: 1px solid rgba(0, 0, 0, 0.025);
@@ -644,14 +460,11 @@ function getLinkIcon(label: string, icon?: string): string | null {
644
  letter-spacing: 0.025em;
645
  text-transform: uppercase;
646
  }
647
-
648
- /* Dark mode pro badge */
649
  :global(.dark) .pro-badge,
650
  :global([data-theme="dark"]) .pro-badge {
651
  background: linear-gradient(to bottom right, #ec4899, #22c55e, #eab308);
652
  border-color: rgba(255, 255, 255, 0.15);
653
  }
654
-
655
  .pro-only-label {
656
  display: inline-flex;
657
  flex-direction: row;
@@ -662,13 +475,7 @@ function getLinkIcon(label: string, icon?: string): string | null {
662
  font-weight: 400;
663
  line-height: 1;
664
  }
665
-
666
- .pro-only-dash {
667
- display: inline-flex;
668
- align-items: center;
669
- line-height: 1;
670
- }
671
-
672
  .pro-only-icon {
673
  width: 11px;
674
  height: 11px;
@@ -676,24 +483,15 @@ function getLinkIcon(label: string, icon?: string): string | null {
676
  display: inline-flex;
677
  align-items: center;
678
  }
679
-
680
- .pro-only-text {
681
- display: inline-flex;
682
- align-items: center;
683
- line-height: 1;
684
- }
685
-
686
- .pdf-locked {
687
- display: block;
688
- }
689
-
690
  .button-locked {
691
  display: inline-flex;
692
  align-items: center;
693
  gap: 6px;
694
- background: linear-gradient(135deg,
695
- var(--primary-color) 0%,
696
- oklch(from var(--primary-color) calc(l - 0.1) calc(c + 0.05) calc(h - 60)) 100%);
697
  border-radius: var(--button-radius);
698
  padding: var(--button-padding-y) var(--button-padding-x);
699
  font-size: var(--button-font-size);
@@ -705,24 +503,9 @@ function getLinkIcon(label: string, icon?: string): string | null {
705
  font-weight: normal;
706
  border-color: rgba(0, 0, 0, 0.15);
707
  }
708
-
709
- .button-locked:active {
710
- transform: translateY(0);
711
- }
712
-
713
- .lock-icon {
714
- font-size: 1em;
715
- flex-shrink: 0;
716
- position: relative;
717
- z-index: 1;
718
- }
719
-
720
- .locked-title {
721
- position: relative;
722
- z-index: 1;
723
- }
724
-
725
- /* Dark mode locked button - inherits from light mode variables */
726
 
727
  @media (max-width: 768px) {
728
  .meta-container-cell--pdf {
@@ -731,113 +514,4 @@ function getLinkIcon(label: string, icon?: string): string | null {
731
  align-items: flex-end;
732
  }
733
  }
734
-
735
- /* Hero external links (paper template) */
736
- .hero-links {
737
- display: none;
738
- }
739
-
740
- /* Paper template overrides (must be scoped to win over default styles) */
741
- :global([data-template="paper"]) .hero {
742
- padding: 80px 16px 0;
743
- }
744
- :global([data-template="paper"]) .hero-title {
745
- font-size: clamp(34px, 5vw, 54px);
746
- font-weight: 800;
747
- line-height: 1.12;
748
- letter-spacing: -0.02em;
749
- max-width: 860px;
750
- margin: 0 auto 20px;
751
- }
752
- :global([data-template="paper"]) .hero-banner {
753
- display: none;
754
- }
755
- :global([data-template="paper"]) .hero-desc {
756
- max-width: 680px;
757
- margin: 0 auto;
758
- font-size: 1.15em;
759
- font-style: normal;
760
- line-height: 1.6;
761
- color: var(--muted-color);
762
- }
763
- :global([data-template="paper"]) .meta {
764
- border-top: none;
765
- border-bottom: none;
766
- padding: 24px 0 0;
767
- }
768
- :global([data-template="paper"]) .meta-container {
769
- display: block !important;
770
- text-align: center;
771
- max-width: 860px;
772
- }
773
- :global([data-template="paper"]) .meta-container-cell {
774
- display: inline !important;
775
- max-width: none !important;
776
- gap: 0 !important;
777
- flex-direction: unset !important;
778
- }
779
- :global([data-template="paper"]) .meta-container-cell h3 {
780
- display: none !important;
781
- }
782
- :global([data-template="paper"]) .meta-container-cell + .meta-container-cell::before {
783
- content: " · ";
784
- color: var(--muted-color);
785
- font-weight: 400;
786
- }
787
- :global([data-template="paper"]) .authors,
788
- :global([data-template="paper"]) .authors li,
789
- :global([data-template="paper"]) .meta-container-cell--affiliations .affiliations,
790
- :global([data-template="paper"]) .meta-container-cell--affiliations .affiliations li,
791
- :global([data-template="paper"]) .meta-container-cell p {
792
- display: inline !important;
793
- padding: 0 !important;
794
- margin: 0 !important;
795
- list-style: none !important;
796
- }
797
- :global([data-template="paper"]) .meta-container-cell--affiliations .affiliations,
798
- :global([data-template="paper"]) .meta-container-cell--affiliations p,
799
- :global([data-template="paper"]) .meta-container-cell--published p {
800
- color: var(--muted-color);
801
- }
802
- :global([data-template="paper"]) .meta-container-cell--doi {
803
- display: none !important;
804
- }
805
- :global([data-template="paper"]) .meta-container-cell--pdf {
806
- display: none !important;
807
- }
808
- :global([data-template="paper"]) .hero-links {
809
- display: flex;
810
- justify-content: center;
811
- flex-wrap: wrap;
812
- gap: 8px;
813
- margin-top: 20px;
814
- padding: 0 var(--content-padding-x);
815
- }
816
- :global([data-template="paper"]) .hero-link {
817
- display: inline-flex;
818
- align-items: center;
819
- gap: 6px;
820
- padding: 6px 16px;
821
- font-size: 0.85em;
822
- font-weight: 500;
823
- color: var(--text-color);
824
- background: var(--surface-bg);
825
- border: 1px solid var(--border-color);
826
- border-radius: 20px;
827
- text-decoration: none;
828
- transition: border-color 0.15s, background 0.15s;
829
- }
830
- :global([data-template="paper"]) .hero-link-icon {
831
- display: inline-flex;
832
- align-items: center;
833
- line-height: 0;
834
- }
835
- :global([data-template="paper"]) .hero-link-icon :global(svg) {
836
- width: 14px;
837
- height: 14px;
838
- }
839
- :global([data-template="paper"]) .hero-link:hover {
840
- border-color: var(--primary-color);
841
- background: var(--hover-bg, var(--surface-bg));
842
- }
843
  </style>
 
1
  ---
2
  import HtmlEmbed from "./HtmlEmbed.astro";
3
+ import {
4
+ type HeroProps,
5
+ type Author,
6
+ normalizeAuthors,
7
+ stripHtml,
8
+ slugify,
9
+ } from "./heroUtils";
 
 
 
 
 
 
 
 
 
10
 
11
  const {
12
  title,
 
19
  doi,
20
  pdfProOnly = false,
21
  showPdf = true,
22
+ } = Astro.props as HeroProps;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
  const normalizedAuthors: Author[] = normalizeAuthors(authors as any);
25
 
 
26
  const authorAffiliationIndexSet = new Set<number>();
27
  for (const author of normalizedAuthors) {
28
  const indices = Array.isArray(author.affiliationIndices)
29
  ? author.affiliationIndices
30
  : [];
31
  for (const idx of indices) {
32
+ if (typeof idx === "number") authorAffiliationIndexSet.add(idx);
 
 
33
  }
34
  }
35
  const shouldShowAffiliationSupers = authorAffiliationIndexSet.size > 1;
36
  const hasMultipleAffiliations =
37
  Array.isArray(affiliations) && affiliations.length > 1;
38
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  const pdfBase = titleRaw ? stripHtml(titleRaw) : stripHtml(title);
40
  const pdfFilename = `${slugify(pdfBase)}.pdf`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  ---
42
 
43
  <section class="hero">
 
186
  </div>
187
  )}
188
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
189
  </header>
190
 
191
  {showPdf && (
192
  <script is:inline>
193
  // PDF access control for Pro users only
194
+ const LOCAL_IS_PRO = true;
 
 
 
195
  const FALLBACK_TIMEOUT_MS = 3000;
196
+ const MAX_RETRY_ATTEMPTS = 100;
197
  let userPlanChecked = false;
198
  let initialized = false;
199
 
 
 
 
 
 
200
  function isProUser(plan) {
201
  if (!plan) return false;
202
  return plan.user === "pro";
203
  }
204
 
 
 
 
205
  function updatePdfAccess(isPro, pdfProOnly) {
206
  const loadingEl = document.querySelector(".pdf-loading");
207
  const proOnlyEl = document.querySelector(".pdf-pro-only");
208
  const lockedEl = document.querySelector(".pdf-locked");
209
  const proOnlyLabel = document.querySelector(".pro-only-label");
210
  const proBadgeWrapper = document.querySelector(".pro-badge-wrapper");
 
 
211
  if (loadingEl) loadingEl.style.display = "none";
 
 
212
  if (!pdfProOnly) {
213
  if (proOnlyEl) proOnlyEl.style.display = "block";
214
  if (proOnlyLabel) proOnlyLabel.style.display = "none";
 
216
  if (proBadgeWrapper) proBadgeWrapper.style.display = "none";
217
  return;
218
  }
 
 
219
  if (isPro) {
220
  if (proOnlyEl) proOnlyEl.style.display = "block";
221
  if (proOnlyLabel) proOnlyLabel.style.display = "none";
 
229
  }
230
  }
231
 
 
 
 
232
  function handleUserPlan(plan, pdfProOnly) {
233
  userPlanChecked = true;
234
+ updatePdfAccess(isProUser(plan), pdfProOnly);
 
 
 
235
  }
236
 
 
 
 
 
237
  function handleFallback(pdfProOnly) {
238
+ handleUserPlan(LOCAL_IS_PRO ? { user: "pro" } : { user: "free" }, pdfProOnly);
 
 
 
 
239
  }
240
 
 
 
 
241
  function initPdfAccess(retryCount = 0) {
 
242
  if (initialized) return;
 
 
243
  const pdfContainer = document.querySelector("#pdf-download-container");
 
244
  if (!pdfContainer) {
 
245
  if (retryCount < MAX_RETRY_ATTEMPTS) {
246
  setTimeout(() => initPdfAccess(retryCount + 1), 10);
247
  } else {
 
 
248
  setTimeout(() => {
249
  const container = document.querySelector("#pdf-download-container");
250
  if (container && !initialized) {
 
256
  }
257
  return;
258
  }
 
259
  initialized = true;
260
  const pdfProOnly = pdfContainer.getAttribute("data-pdf-pro-only") === "true";
 
 
261
  if (!pdfProOnly) {
262
  updatePdfAccess(true, pdfProOnly);
263
  } else {
264
+ window.addEventListener("message", (event) => {
265
+ if (event.data.type === "USER_PLAN") handleUserPlan(event.data.plan, pdfProOnly);
266
+ });
 
 
 
 
 
 
 
 
267
  if (window.parent && window.parent !== window) {
 
268
  window.parent.postMessage({ type: "USER_PLAN_REQUEST" }, "*");
269
+ setTimeout(() => { if (!userPlanChecked) handleFallback(pdfProOnly); }, FALLBACK_TIMEOUT_MS);
 
 
 
 
 
 
270
  } else {
 
271
  handleFallback(pdfProOnly);
272
  }
273
  }
274
  }
275
 
 
276
  (function() {
277
  if (document.readyState === "loading") {
278
+ document.addEventListener("DOMContentLoaded", () => setTimeout(() => initPdfAccess(), 0));
 
 
279
  } else {
 
280
  setTimeout(() => initPdfAccess(), 0);
281
  }
282
  })();
 
284
  )}
285
 
286
  <style>
 
287
  .hero {
288
  width: 100%;
289
  padding: 64px 16px 16px;
 
323
  }
324
 
325
  @media (max-width: 768px) {
326
+ .hero-banner { aspect-ratio: auto; }
327
+ .hero-desc { max-width: 90%; }
 
 
 
 
328
  }
329
 
330
+ /* Meta bar */
331
  .meta {
332
  border-top: 1px solid var(--border-color);
333
  border-bottom: 1px solid var(--border-color);
 
342
  margin: 0 auto;
343
  padding: 0 var(--content-padding-x);
344
  gap: 8px;
345
+ flex-wrap: wrap;
346
+ row-gap: 12px;
347
  }
 
348
  .meta-container a:not(.button) {
349
  color: var(--primary-color);
350
  text-decoration: underline;
 
386
  }
387
  .authors li {
388
  white-space: nowrap;
389
+ padding: 0;
390
  }
391
  .affiliations {
392
  margin: 0;
 
396
  margin: 0;
397
  }
398
 
 
 
 
 
 
399
  @media (max-width: 768px) {
400
+ .meta-container-cell:nth-child(even) { text-align: right; }
 
 
401
  .meta-container-cell:last-child:nth-child(odd) {
402
  flex-grow: 0;
403
  flex-basis: auto;
 
407
  }
408
 
409
  @media print {
410
+ .meta-container-cell--pdf { display: none !important; }
 
 
411
  }
412
 
413
+ /* PDF styles */
414
  .pdf-header-wrapper {
415
  display: flex;
416
  align-items: center;
417
  gap: 6px;
418
  line-height: 1;
419
  }
420
+ .pdf-header-wrapper h3 { line-height: 1; }
 
 
 
 
421
  #pdf-download-container {
422
  min-height: calc(var(--button-padding-y) * 2 + var(--button-font-size) + 2px);
423
  }
 
424
  .pdf-loading {
425
  color: var(--muted-color);
426
  font-size: var(--button-font-size);
 
433
  height: calc(var(--button-padding-y) * 2 + var(--button-font-size) + 2px);
434
  vertical-align: top;
435
  }
436
+ .pdf-pro-only { margin: 0; line-height: 0; }
437
+ .pdf-pro-only .button { margin: 0; }
 
 
 
 
 
 
 
 
438
  .pro-badge-wrapper {
439
  display: inline-flex;
440
  align-items: center;
441
  gap: 5px;
442
  font-style: normal;
443
  }
 
444
  .pro-badge-prefix {
445
  font-size: 0.85em;
446
  opacity: 0.5;
447
  font-weight: 400;
448
  font-style: normal;
449
  }
 
450
  .pro-badge {
451
  display: inline-block;
452
  border: 1px solid rgba(0, 0, 0, 0.025);
 
460
  letter-spacing: 0.025em;
461
  text-transform: uppercase;
462
  }
 
 
463
  :global(.dark) .pro-badge,
464
  :global([data-theme="dark"]) .pro-badge {
465
  background: linear-gradient(to bottom right, #ec4899, #22c55e, #eab308);
466
  border-color: rgba(255, 255, 255, 0.15);
467
  }
 
468
  .pro-only-label {
469
  display: inline-flex;
470
  flex-direction: row;
 
475
  font-weight: 400;
476
  line-height: 1;
477
  }
478
+ .pro-only-dash { display: inline-flex; align-items: center; line-height: 1; }
 
 
 
 
 
 
479
  .pro-only-icon {
480
  width: 11px;
481
  height: 11px;
 
483
  display: inline-flex;
484
  align-items: center;
485
  }
486
+ .pro-only-text { display: inline-flex; align-items: center; line-height: 1; }
487
+ .pdf-locked { display: block; }
 
 
 
 
 
 
 
 
 
488
  .button-locked {
489
  display: inline-flex;
490
  align-items: center;
491
  gap: 6px;
492
+ background: linear-gradient(135deg,
493
+ var(--primary-color) 0%,
494
+ oklch(from var(--primary-color) calc(l - 0.1) calc(c + 0.05) calc(h - 60)) 100%);
495
  border-radius: var(--button-radius);
496
  padding: var(--button-padding-y) var(--button-padding-x);
497
  font-size: var(--button-font-size);
 
503
  font-weight: normal;
504
  border-color: rgba(0, 0, 0, 0.15);
505
  }
506
+ .button-locked:active { transform: translateY(0); }
507
+ .lock-icon { font-size: 1em; flex-shrink: 0; position: relative; z-index: 1; }
508
+ .locked-title { position: relative; z-index: 1; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
509
 
510
  @media (max-width: 768px) {
511
  .meta-container-cell--pdf {
 
514
  align-items: flex-end;
515
  }
516
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
517
  </style>
app/src/components/HeroPaper.astro ADDED
@@ -0,0 +1,209 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ import {
3
+ type HeroProps,
4
+ type Author,
5
+ normalizeAuthors,
6
+ getLinkIcon,
7
+ } from "./heroUtils";
8
+
9
+ const {
10
+ title,
11
+ description,
12
+ authors = [],
13
+ affiliations = [],
14
+ affiliation,
15
+ published,
16
+ links = [],
17
+ } = Astro.props as HeroProps;
18
+
19
+ const normalizedAuthors: Author[] = normalizeAuthors(authors as any);
20
+
21
+ const authorAffiliationIndexSet = new Set<number>();
22
+ for (const author of normalizedAuthors) {
23
+ const indices = Array.isArray(author.affiliationIndices)
24
+ ? author.affiliationIndices
25
+ : [];
26
+ for (const idx of indices) {
27
+ if (typeof idx === "number") authorAffiliationIndexSet.add(idx);
28
+ }
29
+ }
30
+ const shouldShowAffiliationSupers = authorAffiliationIndexSet.size > 1;
31
+
32
+ // Resolve affiliations: prefer array, fallback to legacy string
33
+ const resolvedAffiliations =
34
+ Array.isArray(affiliations) && affiliations.length > 0
35
+ ? affiliations
36
+ : affiliation
37
+ ? [{ id: 1, name: affiliation }]
38
+ : [];
39
+ ---
40
+
41
+ <section class="hero-paper">
42
+ <h1 class="hero-paper__title" set:html={title} />
43
+ {description && <p class="hero-paper__desc">{description}</p>}
44
+ </section>
45
+
46
+ <script is:inline>
47
+ (() => {
48
+ const el = document.querySelector(".hero-paper__title");
49
+ if (!el) return;
50
+ const len = (el.textContent || "").length;
51
+ if (len > 100) el.dataset.titleSize = "sm";
52
+ else if (len > 60) el.dataset.titleSize = "md";
53
+ })();
54
+ </script>
55
+
56
+ <header class="hero-paper-meta" aria-label="Article meta information">
57
+ <div class="hero-paper-meta__inner">
58
+ {normalizedAuthors.length > 0 && (
59
+ <span class="hero-paper-meta__segment">
60
+ {normalizedAuthors.map((a, i) => (
61
+ <>
62
+ {a.url ? <a href={a.url}>{a.name}</a> : <span>{a.name}</span>}
63
+ {shouldShowAffiliationSupers &&
64
+ Array.isArray(a.affiliationIndices) &&
65
+ a.affiliationIndices.length > 0 && (
66
+ <sup>{a.affiliationIndices.join(",")}</sup>
67
+ )}
68
+ {i < normalizedAuthors.length - 1 && <span set:html=",&nbsp;" />}
69
+ </>
70
+ ))}
71
+ </span>
72
+ )}
73
+ {resolvedAffiliations.length > 0 && (
74
+ <span class="hero-paper-meta__segment hero-paper-meta__segment--muted">
75
+ {resolvedAffiliations.map((af, i) => (
76
+ <>
77
+ {af.url ? (
78
+ <a href={af.url} target="_blank" rel="noopener noreferrer">{af.name}</a>
79
+ ) : (
80
+ <span>{af.name}</span>
81
+ )}
82
+ {i < resolvedAffiliations.length - 1 && <span set:html=",&nbsp;" />}
83
+ </>
84
+ ))}
85
+ </span>
86
+ )}
87
+ {published && (
88
+ <span class="hero-paper-meta__segment hero-paper-meta__segment--muted">
89
+ {published}
90
+ </span>
91
+ )}
92
+ </div>
93
+
94
+ {links && links.length > 0 && (
95
+ <nav class="hero-paper-links" aria-label="External links">
96
+ {links.map((link) => {
97
+ const iconSvg = getLinkIcon(link.label, link.icon);
98
+ return (
99
+ <a href={link.url} class="hero-paper-link" target="_blank" rel="noopener noreferrer">
100
+ {iconSvg && <span class="hero-paper-link__icon" set:html={iconSvg} />}
101
+ {link.label}
102
+ </a>
103
+ );
104
+ })}
105
+ </nav>
106
+ )}
107
+ </header>
108
+
109
+ <style>
110
+ /* Hero section */
111
+ .hero-paper {
112
+ width: 100%;
113
+ padding: 80px 16px 0;
114
+ text-align: center;
115
+ }
116
+ .hero-paper__title {
117
+ font-size: clamp(34px, 5vw, 54px);
118
+ font-weight: 800;
119
+ line-height: 1.12;
120
+ letter-spacing: -0.02em;
121
+ max-width: 860px;
122
+ margin: 0 auto 20px;
123
+ text-wrap: balance;
124
+ }
125
+ .hero-paper__title[data-title-size="md"] {
126
+ font-size: clamp(28px, 4vw, 44px);
127
+ }
128
+ .hero-paper__title[data-title-size="sm"] {
129
+ font-size: clamp(24px, 3.2vw, 38px);
130
+ }
131
+ .hero-paper__desc {
132
+ max-width: 680px;
133
+ margin: 0 auto;
134
+ font-size: 1.15em;
135
+ line-height: 1.6;
136
+ color: var(--muted-color);
137
+ }
138
+
139
+ /* Meta section - inline centered */
140
+ .hero-paper-meta {
141
+ padding: 24px 0 0;
142
+ font-size: 0.9rem;
143
+ text-align: center;
144
+ }
145
+ .hero-paper-meta__inner {
146
+ max-width: 860px;
147
+ margin: 0 auto;
148
+ padding: 0 var(--content-padding-x);
149
+ }
150
+ .hero-paper-meta__segment {
151
+ display: inline;
152
+ }
153
+ .hero-paper-meta__segment + .hero-paper-meta__segment::before {
154
+ content: " · ";
155
+ color: var(--muted-color);
156
+ font-weight: 400;
157
+ }
158
+ .hero-paper-meta__segment--muted,
159
+ .hero-paper-meta__segment--muted a {
160
+ color: var(--muted-color);
161
+ }
162
+ .hero-paper-meta__segment a {
163
+ text-decoration: underline;
164
+ text-underline-offset: 2px;
165
+ text-decoration-thickness: 0.06em;
166
+ text-decoration-color: var(--link-underline);
167
+ transition: text-decoration-color 0.15s ease-in-out;
168
+ }
169
+ .hero-paper-meta__segment a:hover {
170
+ text-decoration-color: var(--link-underline-hover);
171
+ }
172
+
173
+ /* External links */
174
+ .hero-paper-links {
175
+ display: flex;
176
+ justify-content: center;
177
+ flex-wrap: wrap;
178
+ gap: 8px;
179
+ margin-top: 20px;
180
+ padding: 0 var(--content-padding-x);
181
+ }
182
+ .hero-paper-link {
183
+ display: inline-flex;
184
+ align-items: center;
185
+ gap: 6px;
186
+ padding: 6px 16px;
187
+ font-size: 0.85em;
188
+ font-weight: 500;
189
+ color: var(--text-color);
190
+ background: var(--surface-bg);
191
+ border: 1px solid var(--border-color);
192
+ border-radius: 20px;
193
+ text-decoration: none;
194
+ transition: border-color 0.15s, background 0.15s;
195
+ }
196
+ .hero-paper-link:hover {
197
+ border-color: var(--primary-color);
198
+ background: var(--hover-bg, var(--surface-bg));
199
+ }
200
+ .hero-paper-link__icon {
201
+ display: inline-flex;
202
+ align-items: center;
203
+ line-height: 0;
204
+ }
205
+ .hero-paper-link__icon :global(svg) {
206
+ width: 14px;
207
+ height: 14px;
208
+ }
209
+ </style>
app/src/components/heroUtils.ts ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export type Author = {
2
+ name: string;
3
+ url?: string;
4
+ affiliationIndices?: number[];
5
+ };
6
+
7
+ export interface HeroProps {
8
+ title: string;
9
+ titleRaw?: string;
10
+ description?: string;
11
+ authors?: Array<
12
+ string | { name: string; url?: string; affiliationIndices?: number[] }
13
+ >;
14
+ affiliations?: Array<{ id: number; name: string; url?: string }>;
15
+ affiliation?: string;
16
+ published?: string;
17
+ doi?: string;
18
+ pdfProOnly?: boolean;
19
+ showPdf?: boolean;
20
+ links?: Array<{ label: string; url: string; icon?: string }>;
21
+ }
22
+
23
+ export function normalizeAuthors(
24
+ input: Array<
25
+ | string
26
+ | {
27
+ name?: string;
28
+ url?: string;
29
+ link?: string;
30
+ affiliationIndices?: number[];
31
+ }
32
+ >,
33
+ ): Author[] {
34
+ return (Array.isArray(input) ? input : [])
35
+ .map((a) => {
36
+ if (typeof a === "string") {
37
+ return { name: a } as Author;
38
+ }
39
+ const name = (a?.name ?? "").toString();
40
+ const url = (a?.url ?? a?.link) as string | undefined;
41
+ const affiliationIndices = Array.isArray((a as any)?.affiliationIndices)
42
+ ? (a as any).affiliationIndices
43
+ : undefined;
44
+ return { name, url, affiliationIndices } as Author;
45
+ })
46
+ .filter((a) => a.name && a.name.trim().length > 0);
47
+ }
48
+
49
+ export function stripHtml(text: string): string {
50
+ return String(text || "").replace(/<[^>]*>/g, "");
51
+ }
52
+
53
+ export function slugify(text: string): string {
54
+ return (
55
+ String(text || "")
56
+ .normalize("NFKD")
57
+ .replace(/\p{Diacritic}+/gu, "")
58
+ .toLowerCase()
59
+ .replace(/[^a-z0-9]+/g, "-")
60
+ .replace(/^-+|-+$/g, "")
61
+ .slice(0, 120) || "article"
62
+ );
63
+ }
64
+
65
+ export const LINK_ICONS: Record<string, string> = {
66
+ github: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0 0 24 12c0-6.63-5.37-12-12-12Z"/></svg>`,
67
+ paper: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>`,
68
+ arxiv: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>`,
69
+ code: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>`,
70
+ demo: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>`,
71
+ data: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/></svg>`,
72
+ dataset: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/></svg>`,
73
+ model: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></svg>`,
74
+ video: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"/></svg>`,
75
+ blog: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg>`,
76
+ };
77
+
78
+ export function getLinkIcon(label: string, icon?: string): string | null {
79
+ if (icon && LINK_ICONS[icon.toLowerCase()])
80
+ return LINK_ICONS[icon.toLowerCase()];
81
+ const key = label.toLowerCase().trim();
82
+ for (const [keyword, svg] of Object.entries(LINK_ICONS)) {
83
+ if (key === keyword || key.includes(keyword)) return svg;
84
+ }
85
+ return null;
86
+ }
app/src/pages/index.astro CHANGED
@@ -1,7 +1,8 @@
1
  ---
2
  import * as ArticleMod from "../content/article.mdx";
3
 
4
- import Hero from "../components/Hero.astro";
 
5
  import Footer from "../components/Footer.astro";
6
  import ThemeToggle from "../components/ThemeToggle.astro";
7
  import Seo from "../components/Seo.astro";
@@ -334,19 +335,32 @@ const links: Array<{ label: string; url: string; icon?: string }> =
334
  </head>
335
  <body>
336
  <ThemeToggle />
337
- <Hero
338
- title={docTitleHtml}
339
- titleRaw={docTitle}
340
- description={subtitle || description}
341
- authors={normalizedAuthors as any}
342
- affiliations={normalizedAffiliations as any}
343
- affiliation={articleFM?.affiliation}
344
- published={articleFM?.published}
345
- doi={doi}
346
- pdfProOnly={articleFM?.pdfProOnly}
347
- showPdf={template === "paper" ? false : articleFM?.showPdf}
348
- links={links}
349
- />
 
 
 
 
 
 
 
 
 
 
 
 
 
350
 
351
  {template === "paper" && (
352
  <div class="paper-hero-banner">
 
1
  ---
2
  import * as ArticleMod from "../content/article.mdx";
3
 
4
+ import HeroArticle from "../components/HeroArticle.astro";
5
+ import HeroPaper from "../components/HeroPaper.astro";
6
  import Footer from "../components/Footer.astro";
7
  import ThemeToggle from "../components/ThemeToggle.astro";
8
  import Seo from "../components/Seo.astro";
 
335
  </head>
336
  <body>
337
  <ThemeToggle />
338
+ {template === "paper" ? (
339
+ <HeroPaper
340
+ title={docTitleHtml}
341
+ titleRaw={docTitle}
342
+ description={subtitle || description}
343
+ authors={normalizedAuthors as any}
344
+ affiliations={normalizedAffiliations as any}
345
+ affiliation={articleFM?.affiliation}
346
+ published={articleFM?.published}
347
+ links={links}
348
+ />
349
+ ) : (
350
+ <HeroArticle
351
+ title={docTitleHtml}
352
+ titleRaw={docTitle}
353
+ description={subtitle || description}
354
+ authors={normalizedAuthors as any}
355
+ affiliations={normalizedAffiliations as any}
356
+ affiliation={articleFM?.affiliation}
357
+ published={articleFM?.published}
358
+ doi={doi}
359
+ pdfProOnly={articleFM?.pdfProOnly}
360
+ showPdf={articleFM?.showPdf}
361
+ links={links}
362
+ />
363
+ )}
364
 
365
  {template === "paper" && (
366
  <div class="paper-hero-banner">
app/src/styles/_layout.css CHANGED
@@ -259,98 +259,6 @@
259
  /* Reference: academic project pages like diffusion-cot.github.io */
260
  /* ============================================================================ */
261
 
262
- /* ---- Hero: hide inline banner, large title, venue-style subtitle ---- */
263
-
264
- [data-template="paper"] .hero {
265
- padding: 80px 16px 0;
266
- }
267
-
268
- [data-template="paper"] .hero-title {
269
- font-size: clamp(34px, 5vw, 54px);
270
- font-weight: 800;
271
- line-height: 1.12;
272
- letter-spacing: -0.02em;
273
- max-width: 860px;
274
- margin: 0 auto 20px;
275
- }
276
-
277
- [data-template="paper"] .hero-banner {
278
- display: none;
279
- }
280
-
281
- [data-template="paper"] .hero-desc {
282
- max-width: 680px;
283
- margin: 0 auto;
284
- font-size: 1.15em;
285
- font-style: normal;
286
- line-height: 1.6;
287
- color: var(--muted-color);
288
- }
289
-
290
- /* ---- Meta bar: centered academic author/affiliation block ---- */
291
-
292
- [data-template="paper"] .meta {
293
- border-top: none;
294
- border-bottom: none;
295
- padding: 24px 0 0;
296
- }
297
-
298
- [data-template="paper"] .meta-container {
299
- flex-direction: row;
300
- flex-wrap: wrap;
301
- align-items: baseline;
302
- justify-content: center;
303
- gap: 4px 6px;
304
- max-width: 860px;
305
- }
306
-
307
- [data-template="paper"] .meta-container-cell {
308
- text-align: center;
309
- max-width: none;
310
- flex: 0 0 auto;
311
- }
312
-
313
- [data-template="paper"] .meta-container-cell h3 {
314
- display: none;
315
- }
316
-
317
- [data-template="paper"] .meta-container-cell + .meta-container-cell::before {
318
- content: "·";
319
- color: var(--muted-color);
320
- margin-right: 2px;
321
- font-weight: 400;
322
- }
323
-
324
- [data-template="paper"] .authors {
325
- justify-content: center;
326
- gap: 0;
327
- font-size: 1em;
328
- }
329
-
330
- [data-template="paper"] .meta-container-cell--affiliations .affiliations {
331
- display: flex;
332
- flex-wrap: wrap;
333
- justify-content: center;
334
- gap: 0 20px;
335
- list-style-position: inside;
336
- padding-left: 0;
337
- font-size: 0.92em;
338
- color: var(--muted-color);
339
- }
340
-
341
- [data-template="paper"] .meta-container-cell--published p {
342
- color: var(--muted-color);
343
- font-size: 0.92em;
344
- }
345
-
346
- [data-template="paper"] .meta-container-cell--doi {
347
- display: none;
348
- }
349
-
350
- [data-template="paper"] .meta-container-cell--pdf {
351
- display: none;
352
- }
353
-
354
  /* ---- Paper hero banner: placed between meta and content in index.astro ---- */
355
 
356
  .paper-hero-banner {
@@ -376,7 +284,8 @@
376
  }
377
 
378
  /* When no banner, separator is directly below meta */
379
- [data-template="paper"] .meta + .content-grid {
 
380
  margin-top: 32px;
381
  border-top: 1px solid var(--border-color);
382
  padding-top: 40px;
 
259
  /* Reference: academic project pages like diffusion-cot.github.io */
260
  /* ============================================================================ */
261
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
262
  /* ---- Paper hero banner: placed between meta and content in index.astro ---- */
263
 
264
  .paper-hero-banner {
 
284
  }
285
 
286
  /* When no banner, separator is directly below meta */
287
+ [data-template="paper"] .meta + .content-grid,
288
+ [data-template="paper"] .hero-paper-meta + .content-grid {
289
  margin-top: 32px;
290
  border-top: 1px solid var(--border-color);
291
  padding-top: 40px;