|
@@ -6,6 +6,7 @@ import KnowledgeScene from "./knowledge/index";
|
|
|
import OpinionScene from "./opinion/index";
|
|
import OpinionScene from "./opinion/index";
|
|
|
import MarketingScene from "./marketing/index";
|
|
import MarketingScene from "./marketing/index";
|
|
|
import GithubTrendingScene, { ColorDot } from "./github-trending/index";
|
|
import GithubTrendingScene, { ColorDot } from "./github-trending/index";
|
|
|
|
|
+import GithubWeeklyScene from "./github-weekly/index";
|
|
|
import { THEMES } from "./base/theme/colors";
|
|
import { THEMES } from "./base/theme/colors";
|
|
|
|
|
|
|
|
// Re-export the render-time contract so legacy imports (`import { RemotionScene
|
|
// Re-export the render-time contract so legacy imports (`import { RemotionScene
|
|
@@ -14,10 +15,13 @@ import { THEMES } from "./base/theme/colors";
|
|
|
export type { RenderScene as RemotionScene, RenderProps as RemotionProps } from "@pipeline/shared";
|
|
export type { RenderScene as RemotionScene, RenderProps as RemotionProps } from "@pipeline/shared";
|
|
|
import type { RenderSegmentExtension } from "@pipeline/shared";
|
|
import type { RenderSegmentExtension } from "@pipeline/shared";
|
|
|
|
|
|
|
|
-/** Narrow a scene's per-template extension to the github-trending variant, or
|
|
|
|
|
- * undefined. Template-specific data lives in `extension`, not on the scene root. */
|
|
|
|
|
|
|
+/** Narrow a scene's per-template extension to a github board variant (trending
|
|
|
|
|
+ * or weekly — same shape), or undefined. Template-specific data lives in
|
|
|
|
|
+ * `extension`, not on the scene root. */
|
|
|
const ghExt = (s: { extension?: RenderScene["extension"] }): RenderSegmentExtension | undefined =>
|
|
const ghExt = (s: { extension?: RenderScene["extension"] }): RenderSegmentExtension | undefined =>
|
|
|
- s.extension?.type === "github-trending" ? s.extension : undefined;
|
|
|
|
|
|
|
+ s.extension?.type === "github-trending" || s.extension?.type === "github-weekly"
|
|
|
|
|
+ ? s.extension
|
|
|
|
|
+ : undefined;
|
|
|
|
|
|
|
|
/** Default visual length (seconds) for a scene with no audio (e.g. a silent
|
|
/** Default visual length (seconds) for a scene with no audio (e.g. a silent
|
|
|
* cover). Visual duration is the TEMPLATE's call — it may exceed the audio to
|
|
* cover). Visual duration is the TEMPLATE's call — it may exceed the audio to
|
|
@@ -42,6 +46,7 @@ const SCENE_MAP: Record<TemplateType, React.FC<any>> = {
|
|
|
opinion: OpinionScene,
|
|
opinion: OpinionScene,
|
|
|
marketing: MarketingScene,
|
|
marketing: MarketingScene,
|
|
|
"github-trending": GithubTrendingScene,
|
|
"github-trending": GithubTrendingScene,
|
|
|
|
|
+ "github-weekly": GithubWeeklyScene,
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
const RATIO_ID: Record<AspectRatio, string> = {
|
|
const RATIO_ID: Record<AspectRatio, string> = {
|
|
@@ -54,13 +59,13 @@ const ASPECT_RATIOS: Record<AspectRatio, { width: number; height: number }> = {
|
|
|
"9:16": { width: 1080, height: 1920 },
|
|
"9:16": { width: 1080, height: 1920 },
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
-const TEMPLATES: TemplateType[] = ["news", "knowledge", "opinion", "marketing", "github-trending"];
|
|
|
|
|
|
|
+const TEMPLATES: TemplateType[] = ["news", "knowledge", "opinion", "marketing", "github-trending", "github-weekly"];
|
|
|
|
|
|
|
|
const TemplateComposition: React.FC<RenderProps> = (props) => {
|
|
const TemplateComposition: React.FC<RenderProps> = (props) => {
|
|
|
const { fps } = useVideoConfig();
|
|
const { fps } = useVideoConfig();
|
|
|
const SceneComponent = SCENE_MAP[props.template];
|
|
const SceneComponent = SCENE_MAP[props.template];
|
|
|
- // github-trending: the cover aggregates the repos' languages into tag
|
|
|
|
|
- // chips. Content scenes carry repo data in extension.
|
|
|
|
|
|
|
+ // github boards: the cover aggregates the repos' languages into tag chips.
|
|
|
|
|
+ // Content scenes carry repo data in extension.
|
|
|
const coverRepos = props.scenes.filter(
|
|
const coverRepos = props.scenes.filter(
|
|
|
(s) => s.kind === "content" && ghExt(s)
|
|
(s) => s.kind === "content" && ghExt(s)
|
|
|
);
|
|
);
|
|
@@ -140,7 +145,9 @@ const TemplateComposition: React.FC<RenderProps> = (props) => {
|
|
|
);
|
|
);
|
|
|
})}
|
|
})}
|
|
|
<GlobalProgressBar totalFrames={totalFrames} color={THEMES[props.template].primary} />
|
|
<GlobalProgressBar totalFrames={totalFrames} color={THEMES[props.template].primary} />
|
|
|
- {props.template === "github-trending" && <ChapterToc layout={layout} />}
|
|
|
|
|
|
|
+ {(props.template === "github-trending" || props.template === "github-weekly") && (
|
|
|
|
|
+ <ChapterToc layout={layout} template={props.template} />
|
|
|
|
|
+ )}
|
|
|
</AbsoluteFill>
|
|
</AbsoluteFill>
|
|
|
);
|
|
);
|
|
|
};
|
|
};
|
|
@@ -200,6 +207,123 @@ const CoverScene: React.FC<{
|
|
|
const { width, height } = useVideoConfig();
|
|
const { width, height } = useVideoConfig();
|
|
|
const isPortrait = height > width;
|
|
const isPortrait = height > width;
|
|
|
|
|
|
|
|
|
|
+ // github-weekly cover: the LIGHT magazine-weekly masthead — paper
|
|
|
|
|
+ // background, editorial rules top and bottom, the masthead title + week
|
|
|
|
|
+ // range + language tags + trend line typeset like a journal front page. No
|
|
|
|
|
+ // background image (the paper IS the identity); the light gradient stands
|
|
|
|
|
+ // alone so no asset dependency exists for this board.
|
|
|
|
|
+ if (template === "github-weekly") {
|
|
|
|
|
+ const theme = THEMES[template];
|
|
|
|
|
+ const primary = theme.primary;
|
|
|
|
|
+ // Aggregate languages across the week's repos → the cover's "main tags".
|
|
|
|
|
+ // Count-weighted, top repo's languageColor wins per language.
|
|
|
|
|
+ const langStats = new Map<string, { count: number; color?: string }>();
|
|
|
|
|
+ for (const s of coverRepos ?? []) {
|
|
|
|
|
+ const repo = ghExt(s)!.repo;
|
|
|
|
|
+ if (!repo.language) continue;
|
|
|
|
|
+ const stats = langStats.get(repo.language) ?? { count: 0, color: repo.languageColor };
|
|
|
|
|
+ stats.count += 1;
|
|
|
|
|
+ langStats.set(repo.language, stats);
|
|
|
|
|
+ }
|
|
|
|
|
+ const topLangs = [...langStats.entries()]
|
|
|
|
|
+ .sort((a, b) => b[1].count - a[1].count)
|
|
|
|
|
+ .slice(0, 6);
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <AbsoluteFill style={{
|
|
|
|
|
+ background: `linear-gradient(150deg, ${theme.bg} 0%, ${theme.bgLight} 60%, #efe5d1 100%)`,
|
|
|
|
|
+ }}>
|
|
|
|
|
+ {/* Faint paper ruling — a journal's faint column grid */}
|
|
|
|
|
+ <div style={{
|
|
|
|
|
+ position: "absolute", inset: 0,
|
|
|
|
|
+ backgroundImage: `
|
|
|
|
|
+ linear-gradient(rgba(30,41,59,0.035) 1px, transparent 1px),
|
|
|
|
|
+ linear-gradient(90deg, rgba(30,41,59,0.035) 1px, transparent 1px)
|
|
|
|
|
+ `,
|
|
|
|
|
+ backgroundSize: "96px 96px",
|
|
|
|
|
+ }} />
|
|
|
|
|
+ {/* Editorial double rule, top and bottom */}
|
|
|
|
|
+ <div style={{ position: "absolute", top: 0, left: 0, width: "100%", height: 8, background: primary }} />
|
|
|
|
|
+ <div style={{ position: "absolute", top: 14, left: 0, width: "100%", height: 1, background: `${primary}66` }} />
|
|
|
|
|
+ <div style={{ position: "absolute", bottom: 0, left: 0, width: "100%", height: 6, background: `${theme.accent}` }} />
|
|
|
|
|
+ <div style={{ position: "absolute", bottom: 12, left: 0, width: "100%", height: 1, background: `${theme.accent}55` }} />
|
|
|
|
|
+
|
|
|
|
|
+ <div style={{
|
|
|
|
|
+ position: "absolute", inset: 0, display: "flex", flexDirection: "column",
|
|
|
|
|
+ alignItems: "center",
|
|
|
|
|
+ justifyContent: "center",
|
|
|
|
|
+ padding: isPortrait ? "72px 64px 150px" : "58px 80px 130px",
|
|
|
|
|
+ }}>
|
|
|
|
|
+ {/* Issue kicker above the masthead — journal convention */}
|
|
|
|
|
+ <div style={{
|
|
|
|
|
+ fontSize: isPortrait ? 30 : 26, fontWeight: 600, color: primary,
|
|
|
|
|
+ fontFamily: "Noto Sans SC", letterSpacing: "0.42em",
|
|
|
|
|
+ textTransform: "uppercase", marginBottom: 18,
|
|
|
|
|
+ }}>
|
|
|
|
|
+ Weekly Issue
|
|
|
|
|
+ </div>
|
|
|
|
|
+ {/* Masthead title */}
|
|
|
|
|
+ <div style={{
|
|
|
|
|
+ fontSize: isPortrait ? 112 : 104, fontWeight: 800, color: "#1e293b",
|
|
|
|
|
+ fontFamily: "Noto Sans SC", lineHeight: 1.1, letterSpacing: "0.02em",
|
|
|
|
|
+ borderBottom: `3px solid ${primary}`,
|
|
|
|
|
+ paddingBottom: 20,
|
|
|
|
|
+ }}>
|
|
|
|
|
+ {title}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ {/* Week range */}
|
|
|
|
|
+ {subtitle && (
|
|
|
|
|
+ <div style={{
|
|
|
|
|
+ fontSize: isPortrait ? 38 : 34, fontWeight: 600, color: "#57534e",
|
|
|
|
|
+ fontFamily: "Noto Sans SC", letterSpacing: "0.08em",
|
|
|
|
|
+ marginTop: 26,
|
|
|
|
|
+ }}>
|
|
|
|
|
+ {subtitle}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ {/* Main tags — languages aggregated across the week's repos */}
|
|
|
|
|
+ {topLangs.length > 0 && (
|
|
|
|
|
+ <div style={{
|
|
|
|
|
+ display: "flex", flexWrap: "wrap",
|
|
|
|
|
+ alignItems: "center", justifyContent: "center",
|
|
|
|
|
+ gap: 14, marginTop: 30,
|
|
|
|
|
+ }}>
|
|
|
|
|
+ {topLangs.map(([lang, stats]) => (
|
|
|
|
|
+ <span key={lang} style={{
|
|
|
|
|
+ display: "inline-flex", alignItems: "center", gap: 9,
|
|
|
|
|
+ fontSize: isPortrait ? 28 : 24, fontWeight: 500, color: "#334155",
|
|
|
|
|
+ fontFamily: "Noto Sans SC",
|
|
|
|
|
+ padding: "10px 24px", borderRadius: 999,
|
|
|
|
|
+ background: "rgba(255,255,255,0.75)",
|
|
|
|
|
+ border: `1px solid ${(stats.color || primary)}55`,
|
|
|
|
|
+ }}>
|
|
|
|
|
+ <ColorDot color={stats.color || primary} size={isPortrait ? 14 : 12} />
|
|
|
|
|
+ {lang}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ {/* Trend summary — set like a journal dek */}
|
|
|
|
|
+ {trendSummary && (
|
|
|
|
|
+ <>
|
|
|
|
|
+ <div style={{
|
|
|
|
|
+ width: "52%", height: 1, marginTop: 34,
|
|
|
|
|
+ background: "rgba(30,41,59,0.18)",
|
|
|
|
|
+ }} />
|
|
|
|
|
+ <div style={{
|
|
|
|
|
+ fontSize: isPortrait ? 34 : 29, fontWeight: 500, color: "#44403c",
|
|
|
|
|
+ fontFamily: "Noto Sans SC", lineHeight: 1.5,
|
|
|
|
|
+ maxWidth: "90%", marginTop: 18,
|
|
|
|
|
+ }}>
|
|
|
|
|
+ {trendSummary}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </AbsoluteFill>
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// github-trending cover: the renderer-provided background image (default.png
|
|
// github-trending cover: the renderer-provided background image (default.png
|
|
|
// fallback — no template-specific bg exists yet) under a dark scrim, with a
|
|
// fallback — no template-specific bg exists yet) under a dark scrim, with a
|
|
|
// DARK title card fully centered on top — no repo list. Shows only the
|
|
// DARK title card fully centered on top — no repo list. Shows only the
|
|
@@ -456,16 +580,20 @@ const GlobalProgressBar: React.FC<{
|
|
|
|
|
|
|
|
const ChapterToc: React.FC<{
|
|
const ChapterToc: React.FC<{
|
|
|
layout: { scene: RenderScene; startFrame: number; endFrame: number; durationFrames: number }[];
|
|
layout: { scene: RenderScene; startFrame: number; endFrame: number; durationFrames: number }[];
|
|
|
-}> = ({ layout }) => {
|
|
|
|
|
- // github-trending only: a chapter table of contents pinned just above the
|
|
|
|
|
|
|
+ template: TemplateType;
|
|
|
|
|
+}> = ({ layout, template }) => {
|
|
|
|
|
+ // github boards only: a chapter table of contents pinned just above the
|
|
|
// global progress bar. Lists every repo's short name; the chapter whose
|
|
// global progress bar. Lists every repo's short name; the chapter whose
|
|
|
// [startFrame, endFrame) contains the current frame is highlighted. A faint
|
|
// [startFrame, endFrame) contains the current frame is highlighted. A faint
|
|
|
- // dark gradient strip behind the row keeps labels legible over light
|
|
|
|
|
- // backgrounds (e.g. the cover) and visually groups the TOC with the bar.
|
|
|
|
|
|
|
+ // gradient strip behind the row keeps labels legible — dark strip + white
|
|
|
|
|
+ // text for github-trending, light strip + slate text for github-weekly's
|
|
|
|
|
+ // paper theme.
|
|
|
const frame = useCurrentFrame();
|
|
const frame = useCurrentFrame();
|
|
|
const { width, height } = useVideoConfig();
|
|
const { width, height } = useVideoConfig();
|
|
|
const isPortrait = height > width;
|
|
const isPortrait = height > width;
|
|
|
- const accent = THEMES["github-trending"].primary;
|
|
|
|
|
|
|
+ const theme = THEMES[template];
|
|
|
|
|
+ const onLight = template === "github-weekly";
|
|
|
|
|
+ const accent = theme.primary;
|
|
|
|
|
|
|
|
const chapters = layout.filter((l) => l.scene.kind === "content" && ghExt(l.scene));
|
|
const chapters = layout.filter((l) => l.scene.kind === "content" && ghExt(l.scene));
|
|
|
const active = chapters.findIndex((c) => frame >= c.startFrame && frame < c.endFrame);
|
|
const active = chapters.findIndex((c) => frame >= c.startFrame && frame < c.endFrame);
|
|
@@ -479,7 +607,9 @@ const ChapterToc: React.FC<{
|
|
|
left: 0,
|
|
left: 0,
|
|
|
width: "100%",
|
|
width: "100%",
|
|
|
height: 72,
|
|
height: 72,
|
|
|
- background: "linear-gradient(to top, rgba(0,0,0,0.42), transparent)",
|
|
|
|
|
|
|
+ background: onLight
|
|
|
|
|
+ ? "linear-gradient(to top, rgba(250,247,242,0.9), transparent)"
|
|
|
|
|
+ : "linear-gradient(to top, rgba(0,0,0,0.42), transparent)",
|
|
|
pointerEvents: "none",
|
|
pointerEvents: "none",
|
|
|
}}
|
|
}}
|
|
|
/>
|
|
/>
|
|
@@ -507,7 +637,7 @@ const ChapterToc: React.FC<{
|
|
|
{i > 0 && (
|
|
{i > 0 && (
|
|
|
<span
|
|
<span
|
|
|
style={{
|
|
style={{
|
|
|
- color: "rgba(255,255,255,0.3)",
|
|
|
|
|
|
|
+ color: onLight ? "rgba(30,41,59,0.35)" : "rgba(255,255,255,0.3)",
|
|
|
fontSize: isPortrait ? 22 : 18,
|
|
fontSize: isPortrait ? 22 : 18,
|
|
|
flexShrink: 0,
|
|
flexShrink: 0,
|
|
|
}}
|
|
}}
|
|
@@ -521,7 +651,11 @@ const ChapterToc: React.FC<{
|
|
|
minWidth: 0,
|
|
minWidth: 0,
|
|
|
fontSize: isPortrait ? 26 : 22,
|
|
fontSize: isPortrait ? 26 : 22,
|
|
|
fontWeight: isActive ? 700 : 500,
|
|
fontWeight: isActive ? 700 : 500,
|
|
|
- color: isActive ? accent : "rgba(255,255,255,0.6)",
|
|
|
|
|
|
|
+ color: isActive
|
|
|
|
|
+ ? accent
|
|
|
|
|
+ : onLight
|
|
|
|
|
+ ? "rgba(51,65,85,0.62)"
|
|
|
|
|
+ : "rgba(255,255,255,0.6)",
|
|
|
whiteSpace: "nowrap",
|
|
whiteSpace: "nowrap",
|
|
|
overflow: "hidden",
|
|
overflow: "hidden",
|
|
|
textOverflow: "ellipsis",
|
|
textOverflow: "ellipsis",
|