|
@@ -11,6 +11,7 @@ import GithubWeeklyScene from "./github-weekly/index";
|
|
|
import GithubDailyPickScene from "./github-daily-pick/index";
|
|
import GithubDailyPickScene from "./github-daily-pick/index";
|
|
|
import { THEMES } from "./base/theme/colors";
|
|
import { THEMES } from "./base/theme/colors";
|
|
|
import { Watermark } from "./base/components/watermark";
|
|
import { Watermark } from "./base/components/watermark";
|
|
|
|
|
+import { Rise, Pop, CountUp } from "./base/components/motion";
|
|
|
|
|
|
|
|
// Re-export the render-time contract so legacy imports (`import { RemotionScene
|
|
// Re-export the render-time contract so legacy imports (`import { RemotionScene
|
|
|
// } from "../Root"`) keep resolving during the migration. New code should import
|
|
// } from "../Root"`) keep resolving during the migration. New code should import
|
|
@@ -251,6 +252,11 @@ const CoverScene: React.FC<{
|
|
|
const topLangs = [...langStats.entries()]
|
|
const topLangs = [...langStats.entries()]
|
|
|
.sort((a, b) => b[1].count - a[1].count)
|
|
.sort((a, b) => b[1].count - a[1].count)
|
|
|
.slice(0, 6);
|
|
.slice(0, 6);
|
|
|
|
|
+ // Portrait-only entrance reinforcement (same rule as the trending cover):
|
|
|
|
|
+ // stagger kicker → masthead → week range → language pills → dek in.
|
|
|
|
|
+ // Layout is unchanged; landscape passes through untouched.
|
|
|
|
|
+ const ent = (delay: number) => (isPortrait ? { delay } : {});
|
|
|
|
|
+ const pop = (delay: number) => (isPortrait ? { delay } : { delay: -9999 });
|
|
|
|
|
|
|
|
return (
|
|
return (
|
|
|
<AbsoluteFill style={{
|
|
<AbsoluteFill style={{
|
|
@@ -280,6 +286,7 @@ const CoverScene: React.FC<{
|
|
|
padding: isPortrait ? "72px 64px 150px" : "58px 80px 130px",
|
|
padding: isPortrait ? "72px 64px 150px" : "58px 80px 130px",
|
|
|
}}>
|
|
}}>
|
|
|
{/* Issue kicker above the masthead — journal convention */}
|
|
{/* Issue kicker above the masthead — journal convention */}
|
|
|
|
|
+ <Rise {...ent(0)}>
|
|
|
<div style={{
|
|
<div style={{
|
|
|
fontSize: isPortrait ? 30 : 26, fontWeight: 600, color: primary,
|
|
fontSize: isPortrait ? 30 : 26, fontWeight: 600, color: primary,
|
|
|
fontFamily: "Noto Sans SC", letterSpacing: "0.42em",
|
|
fontFamily: "Noto Sans SC", letterSpacing: "0.42em",
|
|
@@ -287,7 +294,9 @@ const CoverScene: React.FC<{
|
|
|
}}>
|
|
}}>
|
|
|
Weekly Issue
|
|
Weekly Issue
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ </Rise>
|
|
|
{/* Masthead title */}
|
|
{/* Masthead title */}
|
|
|
|
|
+ <Rise {...ent(5)}>
|
|
|
<div style={{
|
|
<div style={{
|
|
|
fontSize: isPortrait ? 112 : 104, fontWeight: 800, color: "#1e293b",
|
|
fontSize: isPortrait ? 112 : 104, fontWeight: 800, color: "#1e293b",
|
|
|
fontFamily: "Noto Sans SC", lineHeight: 1.1, letterSpacing: "0.02em",
|
|
fontFamily: "Noto Sans SC", lineHeight: 1.1, letterSpacing: "0.02em",
|
|
@@ -296,9 +305,11 @@ const CoverScene: React.FC<{
|
|
|
}}>
|
|
}}>
|
|
|
{title}
|
|
{title}
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ </Rise>
|
|
|
{/* Week range — the issue date, set large so the week reads at a
|
|
{/* Week range — the issue date, set large so the week reads at a
|
|
|
glance (journal issue-line convention). */}
|
|
glance (journal issue-line convention). */}
|
|
|
{subtitle && (
|
|
{subtitle && (
|
|
|
|
|
+ <Rise {...ent(11)}>
|
|
|
<div style={{
|
|
<div style={{
|
|
|
fontSize: isPortrait ? 52 : 46, fontWeight: 700, color: "#1e293b",
|
|
fontSize: isPortrait ? 52 : 46, fontWeight: 700, color: "#1e293b",
|
|
|
fontFamily: "Noto Sans SC", letterSpacing: "0.06em",
|
|
fontFamily: "Noto Sans SC", letterSpacing: "0.06em",
|
|
@@ -306,6 +317,7 @@ const CoverScene: React.FC<{
|
|
|
}}>
|
|
}}>
|
|
|
{subtitle}
|
|
{subtitle}
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ </Rise>
|
|
|
)}
|
|
)}
|
|
|
{/* Main tags — languages aggregated across the week's repos */}
|
|
{/* Main tags — languages aggregated across the week's repos */}
|
|
|
{topLangs.length > 0 && (
|
|
{topLangs.length > 0 && (
|
|
@@ -314,8 +326,9 @@ const CoverScene: React.FC<{
|
|
|
alignItems: "center", justifyContent: "center",
|
|
alignItems: "center", justifyContent: "center",
|
|
|
gap: 14, marginTop: 30,
|
|
gap: 14, marginTop: 30,
|
|
|
}}>
|
|
}}>
|
|
|
- {topLangs.map(([lang, stats]) => (
|
|
|
|
|
- <span key={lang} style={{
|
|
|
|
|
|
|
+ {topLangs.map(([lang, stats], i) => (
|
|
|
|
|
+ <Pop key={lang} {...pop(17 + i * 4)} origin="center">
|
|
|
|
|
+ <span style={{
|
|
|
display: "inline-flex", alignItems: "center", gap: 9,
|
|
display: "inline-flex", alignItems: "center", gap: 9,
|
|
|
fontSize: isPortrait ? 28 : 24, fontWeight: 500, color: "#334155",
|
|
fontSize: isPortrait ? 28 : 24, fontWeight: 500, color: "#334155",
|
|
|
fontFamily: "Noto Sans SC",
|
|
fontFamily: "Noto Sans SC",
|
|
@@ -326,16 +339,20 @@ const CoverScene: React.FC<{
|
|
|
<ColorDot color={stats.color || primary} size={isPortrait ? 14 : 12} />
|
|
<ColorDot color={stats.color || primary} size={isPortrait ? 14 : 12} />
|
|
|
{lang}
|
|
{lang}
|
|
|
</span>
|
|
</span>
|
|
|
|
|
+ </Pop>
|
|
|
))}
|
|
))}
|
|
|
</div>
|
|
</div>
|
|
|
)}
|
|
)}
|
|
|
{/* Trend summary — set like a journal dek */}
|
|
{/* Trend summary — set like a journal dek */}
|
|
|
{trendSummary && (
|
|
{trendSummary && (
|
|
|
<>
|
|
<>
|
|
|
|
|
+ <Rise {...ent(24)}>
|
|
|
<div style={{
|
|
<div style={{
|
|
|
width: "52%", height: 1, marginTop: 34,
|
|
width: "52%", height: 1, marginTop: 34,
|
|
|
background: "rgba(30,41,59,0.18)",
|
|
background: "rgba(30,41,59,0.18)",
|
|
|
}} />
|
|
}} />
|
|
|
|
|
+ </Rise>
|
|
|
|
|
+ <Rise {...ent(27)}>
|
|
|
<div style={{
|
|
<div style={{
|
|
|
fontSize: isPortrait ? 34 : 29, fontWeight: 500, color: "#44403c",
|
|
fontSize: isPortrait ? 34 : 29, fontWeight: 500, color: "#44403c",
|
|
|
fontFamily: "Noto Sans SC", lineHeight: 1.5,
|
|
fontFamily: "Noto Sans SC", lineHeight: 1.5,
|
|
@@ -343,6 +360,7 @@ const CoverScene: React.FC<{
|
|
|
}}>
|
|
}}>
|
|
|
{trendSummary}
|
|
{trendSummary}
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ </Rise>
|
|
|
</>
|
|
</>
|
|
|
)}
|
|
)}
|
|
|
</div>
|
|
</div>
|
|
@@ -496,6 +514,13 @@ const CoverScene: React.FC<{
|
|
|
const repos = (coverRepos ?? []).map((s) => ghExt(s)!.repo);
|
|
const repos = (coverRepos ?? []).map((s) => ghExt(s)!.repo);
|
|
|
const totalStars = repos.reduce((sum, r) => sum + (r.stars ?? 0), 0);
|
|
const totalStars = repos.reduce((sum, r) => sum + (r.stars ?? 0), 0);
|
|
|
|
|
|
|
|
|
|
+ // Portrait-only entrance reinforcement: stagger the existing elements in
|
|
|
|
|
+ // (kicker → masthead → summary → data strip → language chips). The layout
|
|
|
|
|
+ // itself is unchanged; on landscape every wrapper passes through.
|
|
|
|
|
+ const ent = (delay: number) => (isPortrait ? { delay } : {});
|
|
|
|
|
+ const pop = (delay: number) =>
|
|
|
|
|
+ isPortrait ? { delay } : { delay: -9999 }; // landscape: spring settles instantly at frame 0
|
|
|
|
|
+
|
|
|
return (
|
|
return (
|
|
|
<AbsoluteFill style={{
|
|
<AbsoluteFill style={{
|
|
|
background: `linear-gradient(150deg, ${theme.bg} 0%, ${theme.bgLight} 60%, #dbe4ee 100%)`,
|
|
background: `linear-gradient(150deg, ${theme.bg} 0%, ${theme.bgLight} 60%, #dbe4ee 100%)`,
|
|
@@ -526,6 +551,7 @@ const CoverScene: React.FC<{
|
|
|
maxWidth: "100%",
|
|
maxWidth: "100%",
|
|
|
}}>
|
|
}}>
|
|
|
{/* Doc-type kicker — "DAILY BRIEFING", not the weekly's "WEEKLY ISSUE" */}
|
|
{/* Doc-type kicker — "DAILY BRIEFING", not the weekly's "WEEKLY ISSUE" */}
|
|
|
|
|
+ <Rise {...ent(0)}>
|
|
|
<div style={{
|
|
<div style={{
|
|
|
display: "inline-flex", alignItems: "baseline", gap: 18,
|
|
display: "inline-flex", alignItems: "baseline", gap: 18,
|
|
|
marginBottom: 24,
|
|
marginBottom: 24,
|
|
@@ -548,8 +574,10 @@ const CoverScene: React.FC<{
|
|
|
</span>
|
|
</span>
|
|
|
)}
|
|
)}
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ </Rise>
|
|
|
{/* Masthead title — flush left with a heavy primary sidebar rule,
|
|
{/* Masthead title — flush left with a heavy primary sidebar rule,
|
|
|
no underline (the weekly underlines its centered masthead) */}
|
|
no underline (the weekly underlines its centered masthead) */}
|
|
|
|
|
+ <Rise {...ent(5)}>
|
|
|
<div style={{
|
|
<div style={{
|
|
|
fontSize: isPortrait ? 108 : 98, fontWeight: 800, color: "#1e293b",
|
|
fontSize: isPortrait ? 108 : 98, fontWeight: 800, color: "#1e293b",
|
|
|
fontFamily: "Noto Sans SC", lineHeight: 1.12,
|
|
fontFamily: "Noto Sans SC", lineHeight: 1.12,
|
|
@@ -559,8 +587,10 @@ const CoverScene: React.FC<{
|
|
|
}}>
|
|
}}>
|
|
|
{title}
|
|
{title}
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ </Rise>
|
|
|
{/* Trend summary — set as the report's abstract line */}
|
|
{/* Trend summary — set as the report's abstract line */}
|
|
|
{trendSummary && (
|
|
{trendSummary && (
|
|
|
|
|
+ <Rise {...ent(12)}>
|
|
|
<div style={{
|
|
<div style={{
|
|
|
fontSize: isPortrait ? 34 : 28, fontWeight: 500, color: "#475569",
|
|
fontSize: isPortrait ? 34 : 28, fontWeight: 500, color: "#475569",
|
|
|
fontFamily: "Noto Sans SC", lineHeight: 1.5,
|
|
fontFamily: "Noto Sans SC", lineHeight: 1.5,
|
|
@@ -571,6 +601,7 @@ const CoverScene: React.FC<{
|
|
|
}}>
|
|
}}>
|
|
|
{trendSummary}
|
|
{trendSummary}
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ </Rise>
|
|
|
)}
|
|
)}
|
|
|
{/* Data strip — repo count + aggregate stars, then language tags.
|
|
{/* Data strip — repo count + aggregate stars, then language tags.
|
|
|
Numbers-first row a report leads with; chips are SQUARE-cornered
|
|
Numbers-first row a report leads with; chips are SQUARE-cornered
|
|
@@ -581,6 +612,7 @@ const CoverScene: React.FC<{
|
|
|
alignItems: "stretch",
|
|
alignItems: "stretch",
|
|
|
gap: 12, marginTop: 44,
|
|
gap: 12, marginTop: 44,
|
|
|
}}>
|
|
}}>
|
|
|
|
|
+ <Pop {...pop(18)} origin="left center">
|
|
|
<span style={{
|
|
<span style={{
|
|
|
display: "inline-flex", alignItems: "baseline", gap: 8,
|
|
display: "inline-flex", alignItems: "baseline", gap: 8,
|
|
|
padding: "12px 22px",
|
|
padding: "12px 22px",
|
|
@@ -591,12 +623,16 @@ const CoverScene: React.FC<{
|
|
|
}}>
|
|
}}>
|
|
|
<span style={{
|
|
<span style={{
|
|
|
fontSize: isPortrait ? 38 : 32, fontWeight: 800, color: primary,
|
|
fontSize: isPortrait ? 38 : 32, fontWeight: 800, color: primary,
|
|
|
- }}>{repos.length}</span>
|
|
|
|
|
|
|
+ }}>
|
|
|
|
|
+ {isPortrait ? <CountUp value={repos.length} format={(n) => String(n)} delay={20} duration={26} /> : repos.length}
|
|
|
|
|
+ </span>
|
|
|
<span style={{
|
|
<span style={{
|
|
|
fontSize: isPortrait ? 26 : 22, fontWeight: 500, color: "#64748b",
|
|
fontSize: isPortrait ? 26 : 22, fontWeight: 500, color: "#64748b",
|
|
|
}}>个项目</span>
|
|
}}>个项目</span>
|
|
|
</span>
|
|
</span>
|
|
|
|
|
+ </Pop>
|
|
|
{totalStars > 0 && (
|
|
{totalStars > 0 && (
|
|
|
|
|
+ <Pop {...pop(23)} origin="left center">
|
|
|
<span style={{
|
|
<span style={{
|
|
|
display: "inline-flex", alignItems: "baseline", gap: 8,
|
|
display: "inline-flex", alignItems: "baseline", gap: 8,
|
|
|
padding: "12px 22px",
|
|
padding: "12px 22px",
|
|
@@ -607,14 +643,18 @@ const CoverScene: React.FC<{
|
|
|
}}>
|
|
}}>
|
|
|
<span style={{
|
|
<span style={{
|
|
|
fontSize: isPortrait ? 38 : 32, fontWeight: 800, color: primary,
|
|
fontSize: isPortrait ? 38 : 32, fontWeight: 800, color: primary,
|
|
|
- }}>{formatCount(totalStars)}</span>
|
|
|
|
|
|
|
+ }}>
|
|
|
|
|
+ {isPortrait ? <CountUp value={totalStars} format={formatCount} delay={25} duration={32} /> : formatCount(totalStars)}
|
|
|
|
|
+ </span>
|
|
|
<span style={{
|
|
<span style={{
|
|
|
fontSize: isPortrait ? 26 : 22, fontWeight: 500, color: "#64748b",
|
|
fontSize: isPortrait ? 26 : 22, fontWeight: 500, color: "#64748b",
|
|
|
}}>总 stars</span>
|
|
}}>总 stars</span>
|
|
|
</span>
|
|
</span>
|
|
|
|
|
+ </Pop>
|
|
|
)}
|
|
)}
|
|
|
- {topLangs.map(([lang, stats]) => (
|
|
|
|
|
- <span key={lang} style={{
|
|
|
|
|
|
|
+ {topLangs.map(([lang, stats], i) => (
|
|
|
|
|
+ <Pop key={lang} {...pop(28 + i * 4)} origin="left center">
|
|
|
|
|
+ <span style={{
|
|
|
display: "inline-flex", alignItems: "center", gap: 8,
|
|
display: "inline-flex", alignItems: "center", gap: 8,
|
|
|
fontSize: isPortrait ? 26 : 22, fontWeight: 500, color: "#334155",
|
|
fontSize: isPortrait ? 26 : 22, fontWeight: 500, color: "#334155",
|
|
|
fontFamily: "Noto Sans SC",
|
|
fontFamily: "Noto Sans SC",
|
|
@@ -626,6 +666,7 @@ const CoverScene: React.FC<{
|
|
|
<ColorDot color={stats.color || primary} size={isPortrait ? 13 : 11} />
|
|
<ColorDot color={stats.color || primary} size={isPortrait ? 13 : 11} />
|
|
|
{lang}
|
|
{lang}
|
|
|
</span>
|
|
</span>
|
|
|
|
|
+ </Pop>
|
|
|
))}
|
|
))}
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|