Kaynağa Gözat

feat(templates): 每日榜浅色报告风改版与片尾重做

- github-trending 主题切为浅色报告风:原深色渐变+玻璃卡片观感偏暗、
  与周榜区分不足;现在纸质底+报告蓝、方形数据卡(顶部色条)+双编辑
  线,与周榜圆角纸片的杂志风形成对比
- 每日推荐保留深色舞台:拆出独立 GITHUB_DAILY_PICK_PALETTE(沿用原
  深色盘),自持深色 Tag,不再复用 trending 的浅色 Tag
- 三榜封面日期放大加粗(32~38px → 46~52px),日期一眼可读;封面恢复
  代码/科技纹理装饰(CoverDecor,上轮封面改版时丢失)
- github-trending 封面改浅色左对齐刊头:Daily Briefing kicker +
  数据条(项目数/总 stars/语言标签),不再用背景图+深色蒙版
- 片尾 Outro 重做:原纯深底一行文案过单调;现在按模板明暗配色,新增
  本期项目回顾 chips(逐个弹入)+ 节目 kicker + 与封面呼应的纹理
- 内容场景新增"今日 +N"涨星强调标签
- core 建议标题带上封面日期行(meta.subtitle),发布标题始终含日期

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lkatzey 4 hafta önce
ebeveyn
işleme
9fe38130c1

+ 16 - 2
packages/core/src/document.ts

@@ -78,12 +78,26 @@ export interface DocumentRunResult {
   stateError?: string;
 }
 
-/** Feishu title suggestion: video title joined with the trend one-liner. */
+/** github board family — their cover date line (meta.subtitle) is
+ *  deterministic (assemble), so it can be echoed into the title suggestion. */
+const GITHUB_TEMPLATES = new Set([
+  "github-trending",
+  "github-weekly",
+  "github-daily-pick",
+  "github-weekly-recap",
+]);
+
+/** Feishu title suggestion: video title joined with the cover date line and
+ *  the trend one-liner. The date comes from meta.subtitle — the same string
+ *  the cover shows (daily: "2026年8月25日"; weekly: "2026.08.24 - 08.30") —
+ *  so the suggested posting title always carries the day/week. */
 function buildTitleSuggestion(doc?: VideoDocument): string | undefined {
   const t = doc?.meta?.title;
   if (!t) return undefined;
+  const date = GITHUB_TEMPLATES.has(doc.template) ? doc.meta?.subtitle : undefined;
+  const withDate = date ? `${t} ${date}` : t;
   const trend = doc?.meta?.trendSummary;
-  return trend ? `${t}|${trend}` : t;
+  return trend ? `${withDate}|${trend}` : withDate;
 }
 
 /**

+ 328 - 36
packages/templates/src/Root.tsx

@@ -1,6 +1,7 @@
 import React, { useEffect, useState } from "react";
 import { Composition, Sequence, AbsoluteFill, Audio, staticFile, Img, useCurrentFrame, useVideoConfig, interpolate, continueRender, delayRender } from "remotion";
 import type { TemplateType, AspectRatio, RenderScene, RenderProps } from "@pipeline/shared";
+import { formatCount } from "@pipeline/shared";
 import NewsScene from "./news/index";
 import KnowledgeScene from "./knowledge/index";
 import OpinionScene from "./opinion/index";
@@ -9,6 +10,7 @@ import GithubTrendingScene, { ColorDot } from "./github-trending/index";
 import GithubWeeklyScene from "./github-weekly/index";
 import GithubDailyPickScene from "./github-daily-pick/index";
 import { THEMES } from "./base/theme/colors";
+import { Watermark } from "./base/components/watermark";
 
 // Re-export the render-time contract so legacy imports (`import { RemotionScene
 // } from "../Root"`) keep resolving during the migration. New code should import
@@ -134,8 +136,11 @@ const TemplateComposition: React.FC<RenderProps> = (props) => {
             >
               {sceneAudio}
               <OutroScene
+                template={props.template}
                 text={scene.captionOrigin ?? ""}
                 cta={scene.title}
+                channelName={props.channelName}
+                outroRepos={coverRepos}
                 totalFrames={totalFrames}
               />
             </Sequence>
@@ -172,22 +177,22 @@ const TemplateComposition: React.FC<RenderProps> = (props) => {
 
 // --- Cover Scene ---
 
-// Faint, sparse code/data motifs on the github-trending cover background.
-// Low-opacity and edge-anchored so the cover stays clean — the motifs read
-// only as subtle texture, never competing with the title card. Colors adapt
-// to the backdrop: near-black on the light gradient, white on the dark
-// scrim over the background image.
+// Faint, sparse code/data motifs on the github board covers (and the outro —
+// the video closes the way it opened). Edge-anchored so the masthead stays
+// clean: the motifs read as subtle texture, never competing with the
+// headline. Colors adapt to the backdrop: near-black on the paper covers,
+// white on the dark stage.
 const CoverDecor: React.FC<{ accent: string; onDark?: boolean }> = ({ accent, onDark }) => {
   const tok = (css: React.CSSProperties): React.CSSProperties => ({
     position: "absolute",
     fontFamily: "monospace",
     fontWeight: 700,
     color: onDark ? "#ffffff" : "#0f172a",
-    opacity: onDark ? 0.08 : 0.06,
+    opacity: onDark ? 0.08 : 0.1,
     letterSpacing: "0.02em",
     ...css,
   });
-  const chartOpacity = onDark ? 0.16 : 0.08;
+  const chartOpacity = onDark ? 0.16 : 0.15;
   return (
     <div style={{ position: "absolute", inset: 0, overflow: "hidden", pointerEvents: "none" }}>
       <span style={tok({ top: 44, left: 52, fontSize: 56 })}>{"</>"}</span>
@@ -221,7 +226,6 @@ const CoverScene: React.FC<{
   totalFrames: number;
 }> = ({ template, title, subtitle, cardList, backgroundAsset, coverRepos, trendSummary }) => {
   const accent = THEMES[template].primaryLight;
-  const isGithubTrending = template === "github-trending" || template === "github-daily-pick";
   const isGithubWeekly = template === "github-weekly" || template === "github-weekly-recap";
   const { width, height } = useVideoConfig();
   const isPortrait = height > width;
@@ -261,6 +265,8 @@ const CoverScene: React.FC<{
           `,
           backgroundSize: "96px 96px",
         }} />
+        {/* Code/data motifs — the cover's tech texture */}
+        <CoverDecor accent={primary} />
         {/* 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` }} />
@@ -290,11 +296,12 @@ const CoverScene: React.FC<{
           }}>
             {title}
           </div>
-          {/* Week range */}
+          {/* Week range — the issue date, set large so the week reads at a
+              glance (journal issue-line convention). */}
           {subtitle && (
             <div style={{
-              fontSize: isPortrait ? 38 : 34, fontWeight: 600, color: "#57534e",
-              fontFamily: "Noto Sans SC", letterSpacing: "0.08em",
+              fontSize: isPortrait ? 52 : 46, fontWeight: 700, color: "#1e293b",
+              fontFamily: "Noto Sans SC", letterSpacing: "0.06em",
               marginTop: 26,
             }}>
               {subtitle}
@@ -343,14 +350,12 @@ const CoverScene: React.FC<{
     );
   }
 
-  // github-trending / github-daily-pick cover: the renderer-provided
-  // background image (default.png 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 masthead title, the date, the day's main
-  // language tags (aggregated across today's repos / the featured repo), and
-  // the trend summary line. Light gradient + CoverDecor is the no-image
-  // fallback (e.g. studio demo props).
-  if (isGithubTrending) {
+  // github-daily-pick cover: the renderer-provided background image
+  // (default.png fallback) under a dark scrim, with a DARK title card fully
+  // centered on top — the deep-dive board keeps the dark stage. Shows only the
+  // masthead title, the date, the featured repo's language tags, and the trend
+  // summary line. Light gradient + CoverDecor is the no-image fallback.
+  if (template === "github-daily-pick") {
     const primary = THEMES[template].primary;
     // Aggregate languages across today's repos → the cover's "main tags".
     // Count-weighted, top repo's languageColor wins per language.
@@ -413,11 +418,11 @@ const CoverScene: React.FC<{
             }}>
               {title}
             </div>
-            {/* Date */}
+            {/* Date — the issue line, set large so the day reads at a glance */}
             {subtitle && (
               <div style={{
-                fontSize: isPortrait ? 36 : 32, fontWeight: 600, color: accent,
-                fontFamily: "Noto Sans SC",
+                fontSize: isPortrait ? 52 : 46, fontWeight: 700, color: accent,
+                fontFamily: "Noto Sans SC", letterSpacing: "0.04em",
               }}>
                 {subtitle}
               </div>
@@ -466,6 +471,168 @@ const CoverScene: React.FC<{
     );
   }
 
+  // github-trending cover: the LIGHT daily board — paper masthead matching the
+  // (now light) content scenes: editorial rules top/bottom, masthead title +
+  // date + language tags + trend summary typeset on paper. No background image
+  // (the paper IS the identity, same choice as github-weekly).
+  if (template === "github-trending") {
+    const theme = THEMES[template];
+    const primary = theme.primary;
+    // Aggregate languages across today'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);
+    // Report identity signals: how many repos make up today's briefing and
+    // their combined star base — the two numbers a data report leads with.
+    const repos = (coverRepos ?? []).map((s) => ghExt(s)!.repo);
+    const totalStars = repos.reduce((sum, r) => sum + (r.stars ?? 0), 0);
+
+    return (
+      <AbsoluteFill style={{
+        background: `linear-gradient(150deg, ${theme.bg} 0%, ${theme.bgLight} 60%, #dbe4ee 100%)`,
+      }}>
+        {/* Fine ledger ruling — denser than the weekly's journal grid */}
+        <div style={{
+          position: "absolute", inset: 0,
+          backgroundImage: `
+            linear-gradient(rgba(30,41,59,0.045) 1px, transparent 1px),
+            linear-gradient(90deg, rgba(30,41,59,0.045) 1px, transparent 1px)
+          `,
+          backgroundSize: "48px 48px",
+        }} />
+        {/* Code/data motifs — the cover's tech texture */}
+        <CoverDecor accent={primary} />
+        {/* Report rules: heavy primary band top, thin accent rule bottom —
+            NOT the weekly's double editorial rules */}
+        <div style={{ position: "absolute", top: 0, left: 0, width: "100%", height: 10, background: primary }} />
+        <div style={{ position: "absolute", bottom: 0, left: 0, width: "100%", height: 5, background: `${theme.accent}` }} />
+
+        {/* LEFT-ALIGNED report header — the daily board's identity vs the
+            weekly magazine's centered masthead */}
+        <div style={{
+          position: "absolute", inset: 0, display: "flex", flexDirection: "column",
+          justifyContent: "center",
+          alignItems: "flex-start",
+          padding: isPortrait ? "72px 64px 150px" : "58px 96px 130px",
+          maxWidth: "100%",
+        }}>
+          {/* Doc-type kicker — "DAILY BRIEFING", not the weekly's "WEEKLY ISSUE" */}
+          <div style={{
+            display: "inline-flex", alignItems: "baseline", gap: 18,
+            marginBottom: 24,
+          }}>
+            <span style={{
+              fontSize: isPortrait ? 26 : 22, fontWeight: 700,
+              color: "#ffffff", background: primary,
+              padding: "8px 20px", letterSpacing: "0.3em",
+              fontFamily: "Noto Sans SC", textTransform: "uppercase",
+            }}>
+              Daily Briefing
+            </span>
+            {subtitle && (
+              <span style={{
+                fontSize: isPortrait ? 40 : 36, fontWeight: 600,
+                color: "#334155", fontFamily: "Noto Sans SC",
+                letterSpacing: "0.04em",
+              }}>
+                {subtitle}
+              </span>
+            )}
+          </div>
+          {/* Masthead title — flush left with a heavy primary sidebar rule,
+              no underline (the weekly underlines its centered masthead) */}
+          <div style={{
+            fontSize: isPortrait ? 108 : 98, fontWeight: 800, color: "#1e293b",
+            fontFamily: "Noto Sans SC", lineHeight: 1.12,
+            letterSpacing: "-0.01em",
+            borderLeft: `10px solid ${primary}`,
+            paddingLeft: isPortrait ? 36 : 42,
+          }}>
+            {title}
+          </div>
+          {/* Trend summary — set as the report's abstract line */}
+          {trendSummary && (
+            <div style={{
+              fontSize: isPortrait ? 34 : 28, fontWeight: 500, color: "#475569",
+              fontFamily: "Noto Sans SC", lineHeight: 1.5,
+              maxWidth: isPortrait ? "92%" : "72%",
+              marginTop: 26,
+              borderLeft: `3px solid ${primary}55`,
+              paddingLeft: isPortrait ? 22 : 24,
+            }}>
+              {trendSummary}
+            </div>
+          )}
+          {/* Data strip — repo count + aggregate stars, then language tags.
+              Numbers-first row a report leads with; chips are SQUARE-cornered
+              with a top data bar (report legend), unlike the weekly's round
+              paper pills. */}
+          <div style={{
+            display: "flex", flexWrap: "wrap",
+            alignItems: "stretch",
+            gap: 12, marginTop: 44,
+          }}>
+            <span style={{
+              display: "inline-flex", alignItems: "baseline", gap: 8,
+              padding: "12px 22px",
+              background: "#ffffff",
+              border: `1px solid ${primary}44`,
+              borderTop: `3px solid ${primary}`,
+              fontFamily: "Noto Sans SC",
+            }}>
+              <span style={{
+                fontSize: isPortrait ? 38 : 32, fontWeight: 800, color: primary,
+              }}>{repos.length}</span>
+              <span style={{
+                fontSize: isPortrait ? 26 : 22, fontWeight: 500, color: "#64748b",
+              }}>个项目</span>
+            </span>
+            {totalStars > 0 && (
+              <span style={{
+                display: "inline-flex", alignItems: "baseline", gap: 8,
+                padding: "12px 22px",
+                background: "#ffffff",
+                border: `1px solid ${primary}44`,
+                borderTop: `3px solid ${primary}`,
+                fontFamily: "Noto Sans SC",
+              }}>
+                <span style={{
+                  fontSize: isPortrait ? 38 : 32, fontWeight: 800, color: primary,
+                }}>{formatCount(totalStars)}</span>
+                <span style={{
+                  fontSize: isPortrait ? 26 : 22, fontWeight: 500, color: "#64748b",
+                }}>总 stars</span>
+              </span>
+            )}
+            {topLangs.map(([lang, stats]) => (
+              <span key={lang} style={{
+                display: "inline-flex", alignItems: "center", gap: 8,
+                fontSize: isPortrait ? 26 : 22, fontWeight: 500, color: "#334155",
+                fontFamily: "Noto Sans SC",
+                padding: "12px 20px",
+                background: "rgba(255,255,255,0.75)",
+                border: `1px solid ${(stats.color || primary)}44`,
+                borderTop: `3px solid ${(stats.color || primary)}`,
+              }}>
+                <ColorDot color={stats.color || primary} size={isPortrait ? 13 : 11} />
+                {lang}
+              </span>
+            ))}
+          </div>
+        </div>
+      </AbsoluteFill>
+    );
+  }
+
   return (
     <AbsoluteFill style={{ backgroundColor: "#0f172a" }}>
       {backgroundAsset && (
@@ -529,40 +696,165 @@ const CoverScene: React.FC<{
 
 // --- Outro Scene ---
 
+/** The github outro's recap chips — repo short names (board boards) or act
+ *  titles (daily-pick), one chip per content scene. */
+const OutroChips: React.FC<{
+  entries: string[];
+  accent: string;
+  onLight?: boolean;
+  isPortrait: boolean;
+}> = ({ entries, accent, onLight, isPortrait }) => {
+  const frame = useCurrentFrame();
+  const shown = Math.min(
+    entries.length,
+    Math.floor(frame / 4) + 1
+  );
+  return (
+    <div style={{
+      display: "flex", flexWrap: "wrap",
+      alignItems: "center", justifyContent: "center",
+      gap: isPortrait ? 16 : 12,
+      maxWidth: isPortrait ? "92%" : "80%",
+    }}>
+      {entries.slice(0, shown).map((label, i) => (
+        <span key={i} style={{
+          display: "inline-flex", alignItems: "center",
+          fontSize: isPortrait ? 30 : 26, fontWeight: 500,
+          fontFamily: "Noto Sans SC",
+          color: onLight ? "#334155" : "#e2e8f0",
+          padding: "12px 26px", borderRadius: 999,
+          background: onLight ? "rgba(255,255,255,0.8)" : "rgba(255,255,255,0.06)",
+          border: `1px solid ${accent}55`,
+        }}>
+          {label}
+        </span>
+      ))}
+    </div>
+  );
+};
+
 const OutroScene: React.FC<{
+  template: TemplateType;
   text: string;
   cta?: string;
+  channelName?: string;
+  /** Content scenes — the outro's recap chips list their repo/act labels. */
+  outroRepos?: RenderScene[];
   totalFrames: number;
-}> = ({ text, cta }) => {
+}> = ({ template, text, cta, channelName, outroRepos }) => {
   const frame = useCurrentFrame();
-  const opacity = interpolate(frame, [0, 15], [0, 1], { extrapolateRight: "clamp" });
+  const { width, height } = useVideoConfig();
+  const isPortrait = height > width;
+  const theme = THEMES[template];
+  const isGithub = template.startsWith("github");
+  const onLight =
+    template === "github-trending" ||
+    template === "github-weekly" ||
+    template === "github-weekly-recap";
+  const accent = theme.primary;
+
+  // Staggered entrance: the closing line rises first, chips pop in one by one,
+  // the CTA lands last.
+  const rise = (delay: number) =>
+    interpolate(frame, [delay, delay + 14], [36, 0], {
+      extrapolateLeft: "clamp", extrapolateRight: "clamp",
+    });
+  const fade = (delay: number) =>
+    interpolate(frame, [delay, delay + 14], [0, 1], {
+      extrapolateLeft: "clamp", extrapolateRight: "clamp",
+    });
+
+  // Recap entries: repo short names for the board boards, act titles for the
+  // daily deep-dive (same labeling rule as ChapterToc).
+  const entries = (outroRepos ?? [])
+    .map((s) => {
+      const ext = ghExt(s);
+      if (!ext) return "";
+      return template === "github-daily-pick"
+        ? (s.title || ext.repo.name || "")
+        : (ext.repo.name || ext.repo.fullName || s.title || "");
+    })
+    .filter(Boolean);
 
   return (
-    <AbsoluteFill style={{ opacity, backgroundColor: "#0f172a" }}>
+    <AbsoluteFill style={{
+      backgroundColor: onLight ? theme.bg : "#0f172a",
+      background: onLight
+        ? `linear-gradient(150deg, ${theme.bg} 0%, ${theme.bgLight} 55%, ${theme.bgLight} 100%)`
+        : `linear-gradient(150deg, #0f172a 0%, ${theme.bgLight} 100%)`,
+    }}>
+      {/* Close the way we opened — the cover's tech texture bookends the video */}
+      <CoverDecor accent={accent} onDark={!onLight} />
+      {/* Editorial rules matching the paper boards' covers */}
+      {onLight ? (
+        <>
+          <div style={{ position: "absolute", top: 0, left: 0, width: "100%", height: 8, background: accent }} />
+          <div style={{ position: "absolute", top: 14, left: 0, width: "100%", height: 1, background: `${accent}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", top: 0, left: 0, width: "100%", height: 6,
+          background: `linear-gradient(90deg, ${accent}, ${theme.primaryLight})`,
+        }} />
+      )}
+
       <div style={{
         position: "absolute", inset: 0, display: "flex",
         flexDirection: "column", justifyContent: "center", alignItems: "center",
-        padding: 80, textAlign: "center",
+        padding: isPortrait ? "100px 64px 170px" : "80px",
+        textAlign: "center",
       }}>
+        {/* Kicker — the show's masthead, echoes the cover */}
+        {isGithub && (
+          <div style={{
+            fontSize: isPortrait ? 28 : 24, fontWeight: 600,
+            color: onLight ? accent : theme.primaryLight,
+            fontFamily: "Noto Sans SC", letterSpacing: "0.42em",
+            textTransform: "uppercase", marginBottom: 22,
+            opacity: fade(0), transform: `translateY(${rise(0)}px)`,
+          }}>
+            {template === "github-trending" ? "GitHub Daily" : template === "github-weekly" ? "GitHub Weekly" : "GitHub Pick"}
+          </div>
+        )}
+        {/* Closing line */}
         <div style={{
-          fontSize: 48, fontWeight: 700, color: "white",
-          fontFamily: "Noto Sans SC", lineHeight: 1.4, marginBottom: 40,
-          maxWidth: "80%",
+          fontSize: isPortrait ? 56 : 50, fontWeight: 700,
+          color: onLight ? "#1e293b" : "white",
+          fontFamily: "Noto Sans SC", lineHeight: 1.4,
+          maxWidth: "84%", marginBottom: isGithub ? 44 : 40,
+          opacity: fade(4), transform: `translateY(${rise(4)}px)`,
         }}>
           {text}
         </div>
+        {/* Today/this week's projects — the recap chip strip */}
+        {entries.length > 0 && (
+          <OutroChips
+            entries={entries}
+            accent={accent}
+            onLight={onLight}
+            isPortrait={isPortrait}
+          />
+        )}
+        {/* CTA — the landing action, last in */}
         {cta && (
           <div style={{
-            fontSize: 28, fontWeight: 600, color: "#fbbf24",
+            marginTop: 52,
+            fontSize: isPortrait ? 32 : 28, fontWeight: 600,
+            color: onLight ? "#ffffff" : "#0f172a",
             fontFamily: "Noto Sans SC",
-            padding: "16px 48px",
-            borderRadius: 12,
-            border: "2px solid #fbbf24",
+            padding: "16px 52px",
+            borderRadius: 999,
+            background: onLight ? accent : theme.primaryLight,
+            boxShadow: onLight ? "0 8px 24px rgba(0,0,0,0.12)" : "0 8px 24px rgba(0,0,0,0.4)",
+            opacity: fade(16), transform: `translateY(${rise(16)}px)`,
           }}>
             {cta}
           </div>
         )}
       </div>
+      <Watermark text={channelName} light={onLight} />
     </AbsoluteFill>
   );
 };
@@ -606,13 +898,13 @@ const ChapterToc: React.FC<{
   // global progress bar. Lists every repo's short name; the chapter whose
   // [startFrame, endFrame) contains the current frame is highlighted. A faint
   // 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.
+  // text for github-daily-pick (the dark deep-dive board), light strip +
+  // slate text for the paper boards (github-trending / github-weekly / recap).
   const frame = useCurrentFrame();
   const { width, height } = useVideoConfig();
   const isPortrait = height > width;
   const theme = THEMES[template];
-  const onLight = template === "github-weekly" || template === "github-weekly-recap";
+  const onLight = template !== "github-daily-pick";
   const accent = theme.primary;
 
   const chapters = layout.filter((l) => l.scene.kind === "content" && ghExt(l.scene));

+ 34 - 13
packages/templates/src/base/theme/colors.ts

@@ -40,14 +40,14 @@ export const THEMES = {
     gradient: ["#0a0a0a", "#3b0764"],
   },
   "github-trending": {
-    primary: "#22c55e",
-    primaryLight: "#4ade80",
-    accent: "#3b82f6",
-    bg: "#0f172a",
-    bgLight: "#1e293b",
-    text: "#ffffff",
-    textMuted: "#94a3b8",
-    gradient: ["#0f172a", "#1e3a8a"],
+    primary: "#2b5797",
+    primaryLight: "#4479bd",
+    accent: "#173e63",
+    bg: "#f7f9fb",
+    bgLight: "#e8eef4",
+    text: "#1e293b",
+    textMuted: "#64748b",
+    gradient: ["#f7f9fb", "#e8eef4"],
   },
   "github-weekly": {
     primary: "#d97706",
@@ -85,12 +85,33 @@ export type ThemeName = keyof typeof THEMES;
 export type ThemeColors = (typeof THEMES)[ThemeName];
 
 /**
- * Per-scene rotating background palette for the github-trending template.
- * Indexed by `contentSceneIndex % PALETTE.length`. Each entry has a two-stop
- * dark gradient and a lighter accent for tags/labels — all dark enough that
- * white text reads cleanly on top.
+ * Per-scene rotating background palette for the github-trending template —
+ * the LIGHT daily counterpart of GITHUB_WEEKLY_PALETTE. Indexed by
+ * `contentSceneIndex % PALETTE.length`. Paper-toned two-stop gradients with
+ * a deep editorial accent per entry; all light enough that dark slate text
+ * reads cleanly on top.
  */
 export const GITHUB_TRENDING_PALETTE = [
+  { from: "#f5f8fb", to: "#e6edf4", accent: "#2b5797" }, // report blue
+  { from: "#f6f7f9", to: "#e8ebef", accent: "#334155" }, // slate / graphite
+  { from: "#f4f7f8", to: "#e3edf0", accent: "#0f766e" }, // mist / petrol
+  { from: "#f8f6f2", to: "#efe8da", accent: "#a16207" }, // vellum / bronze
+  { from: "#f5f6f8", to: "#e6ebf3", accent: "#1e40af" }, // cool paper / navy
+  { from: "#f7f5f2", to: "#eae7df", accent: "#57534e" }, // warm grey / stone
+  { from: "#f4f6f5", to: "#e5eeea", accent: "#166534" }, // sage / forest
+  { from: "#f7f4f5", to: "#eee2e4", accent: "#9f1239" }, // tinted / rosewood
+] as const;
+
+export type GithubTrendingPaletteEntry = (typeof GITHUB_TRENDING_PALETTE)[number];
+
+/**
+ * Per-scene rotating background palette for the github-daily-pick template —
+ * the DARK deep-dive counterpart of GITHUB_TRENDING_PALETTE (the daily pick
+ * reads as a focused spotlight on ONE project, so it keeps the dark stage).
+ * Each entry has a two-stop dark gradient and a lighter accent for
+ * tags/labels — all dark enough that white text reads cleanly on top.
+ */
+export const GITHUB_DAILY_PICK_PALETTE = [
   { from: "#0f172a", to: "#1e3a8a", accent: "#60a5fa" }, // navy
   { from: "#042f2e", to: "#134e4a", accent: "#2dd4bf" }, // teal
   { from: "#1e1b4b", to: "#4c1d95", accent: "#a78bfa" }, // indigo
@@ -101,7 +122,7 @@ export const GITHUB_TRENDING_PALETTE = [
   { from: "#3f1d11", to: "#7c2d12", accent: "#fb7185" }, // rust
 ] as const;
 
-export type GithubTrendingPaletteEntry = (typeof GITHUB_TRENDING_PALETTE)[number];
+export type GithubDailyPickPaletteEntry = (typeof GITHUB_DAILY_PICK_PALETTE)[number];
 
 /**
  * Per-scene rotating palette for the github-weekly template — the LIGHT

+ 24 - 3
packages/templates/src/github-daily-pick/index.tsx

@@ -9,11 +9,11 @@ import {
 } from "remotion";
 import type { RemotionScene } from "../Root";
 import type { RenderSegmentExtension } from "@pipeline/shared";
-import { GITHUB_TRENDING_PALETTE } from "../base/theme/colors";
+import { GITHUB_DAILY_PICK_PALETTE } from "../base/theme/colors";
 import { SubtitleBar } from "../base/components/subtitle-bar";
 import { Watermark } from "../base/components/watermark";
 import { formatCount } from "@pipeline/shared";
-import { Tag, ColorDot } from "../github-trending/index";
+import { ColorDot } from "../github-trending/index";
 
 interface GithubDailyPickSceneProps {
   scene: RemotionScene;
@@ -24,6 +24,27 @@ interface GithubDailyPickSceneProps {
   channelName?: string;
 }
 
+/** Dark-stage tag chip — the deep-dive board keeps the dark look, so it can't
+ *  reuse github-trending's (now light) Tag. Dark glass chip, light accent. */
+const Tag: React.FC<{ accent: string; children: React.ReactNode; size?: "default" | "large" }> = ({ accent, children, size = "default" }) => (
+  <div style={{
+    display: "inline-flex",
+    alignItems: "center",
+    gap: 10,
+    padding: "12px 24px",
+    borderRadius: 10,
+    background: "rgba(255,255,255,0.08)",
+    border: `1px solid ${accent}55`,
+    color: "white",
+    fontFamily: "Noto Sans SC",
+    fontSize: size === "large" ? 32 : 28,
+    fontWeight: 500,
+    backdropFilter: "blur(4px)",
+  }}>
+    {children}
+  </div>
+);
+
 /**
  * github-daily-pick content scene — one ACT chapter of the day's six-act
  * recommendation (钩子/痛点/亮相/亮点/上手/总结). Unlike the board templates (headline = repo fullName), here the scene
@@ -41,7 +62,7 @@ const GithubDailyPickScene: React.FC<GithubDailyPickSceneProps> = ({
   const frame = useCurrentFrame();
   const { width, height, durationInFrames } = useVideoConfig();
   const isPortrait = height > width;
-  const palette = GITHUB_TRENDING_PALETTE[sceneIndex % GITHUB_TRENDING_PALETTE.length];
+  const palette = GITHUB_DAILY_PICK_PALETTE[sceneIndex % GITHUB_DAILY_PICK_PALETTE.length];
   const github: RenderSegmentExtension | undefined =
     scene.extension?.type === "github-daily-pick" ? scene.extension : undefined;
 

+ 49 - 49
packages/templates/src/github-trending/index.tsx

@@ -65,6 +65,7 @@ const GithubTrendingScene: React.FC<GithubTrendingSceneProps> = ({
   const starsLabel = repo?.stars != null ? `${formatCount(repo.stars)} stars` : "";
   const license = repo?.license || "";
   const fullName = repo?.fullName || scene.title || "";
+  const gainLabel = repo?.todayStars != null ? `今日 +${formatCount(repo.todayStars)}` : "";
 
   if (!github) {
     return (
@@ -73,12 +74,12 @@ const GithubTrendingScene: React.FC<GithubTrendingSceneProps> = ({
           position: "absolute", inset: 0, display: "flex",
           alignItems: "center", justifyContent: "center", padding: pad,
         }}>
-          <div style={{ fontSize: 48, fontWeight: 700, color: "white", fontFamily: "Noto Sans SC", textAlign: "center" }}>
+          <div style={{ fontSize: 48, fontWeight: 700, color: "#1e293b", fontFamily: "Noto Sans SC", textAlign: "center" }}>
             {scene.title || scene.captionOrigin}
           </div>
         </div>
         {(scene.caption?.length ?? 0) > 0 && <SubtitleBar caption={scene.caption} />}
-        <Watermark text={channelName} />
+        <Watermark text={channelName} light />
       </AbsoluteFill>
     );
   }
@@ -88,7 +89,7 @@ const GithubTrendingScene: React.FC<GithubTrendingSceneProps> = ({
       <div style={{
         fontSize,
         fontWeight: 800,
-        color: "white",
+        color: "#1e293b",
         fontFamily: "Noto Sans SC",
         lineHeight: 1.15,
         letterSpacing: "-0.01em",
@@ -102,6 +103,11 @@ const GithubTrendingScene: React.FC<GithubTrendingSceneProps> = ({
             {language}
           </Tag>
         )}
+        {gainLabel && (
+          <Tag accent={palette.accent} size={tagSize} emphasized>
+            {gainLabel}
+          </Tag>
+        )}
         {starsLabel && <Tag accent={palette.accent} size={tagSize}>{starsLabel}</Tag>}
         {license && <Tag accent={palette.accent} size={tagSize}>{license}</Tag>}
       </div>
@@ -113,21 +119,19 @@ const GithubTrendingScene: React.FC<GithubTrendingSceneProps> = ({
       opacity: fadeOpacity,
       background: `linear-gradient(135deg, ${palette.from} 0%, ${palette.to} 100%)`,
     }}>
-      {/* Subtle grid overlay (matches other templates' aesthetic) */}
+      {/* Subtle paper ruling (the light counterpart of the old dark grid) */}
       <div style={{
         position: "absolute", inset: 0,
         backgroundImage: `
-          linear-gradient(rgba(255,255,255,0.03) 1px, transparent 1px),
-          linear-gradient(90deg, rgba(255,255,255,0.03) 1px, transparent 1px)
+          linear-gradient(rgba(30,41,59,0.03) 1px, transparent 1px),
+          linear-gradient(90deg, rgba(30,41,59,0.03) 1px, transparent 1px)
         `,
         backgroundSize: "80px 80px",
       }} />
 
-      {/* Top accent line */}
-      <div style={{
-        position: "absolute", top: 0, left: 0, width: "100%", height: 4,
-        background: `linear-gradient(90deg, ${palette.accent}, ${palette.accent}88)`,
-      }} />
+      {/* Top accent rule — a double editorial rule, magazine style */}
+      <div style={{ position: "absolute", top: 0, left: 0, width: "100%", height: 6, background: palette.accent }} />
+      <div style={{ position: "absolute", top: 10, left: 0, width: "100%", height: 1, background: `${palette.accent}55` }} />
 
       {isPortrait ? (
         // Portrait: flex column. Header takes natural height regardless of
@@ -146,7 +150,7 @@ const GithubTrendingScene: React.FC<GithubTrendingSceneProps> = ({
           {renderHeader(92, "large", 16)}
           <div style={{
             height: 1,
-            background: "rgba(255,255,255,0.18)",
+            background: "rgba(30,41,59,0.18)",
             marginTop: 24,
           }} />
           <PortraitContent
@@ -157,7 +161,7 @@ const GithubTrendingScene: React.FC<GithubTrendingSceneProps> = ({
           />
         </div>
       ) : (
-        // Landscape: absolute positioning (unchanged — single-line 80px header).
+        // Landscape: absolute positioning (single-line 80px header).
         <>
           <div style={{
             position: "absolute",
@@ -174,7 +178,7 @@ const GithubTrendingScene: React.FC<GithubTrendingSceneProps> = ({
             left: pad,
             right: pad,
             height: 1,
-            background: "rgba(255,255,255,0.18)",
+            background: "rgba(30,41,59,0.18)",
           }} />
 
           <LandscapeContent
@@ -197,27 +201,33 @@ const GithubTrendingScene: React.FC<GithubTrendingSceneProps> = ({
           fontSize={isPortrait ? 56 : undefined}
         />
       )}
-      <Watermark text={channelName} />
+      <Watermark text={channelName} light />
     </AbsoluteFill>
   );
 };
 
-// --- Sub-components ---
+// --- Sub-components (light report components — the daily board reads as a
+//     data briefing: square-cornered legend chips with a top data bar, in
+//     contrast to github-weekly's round paper pills) ---
 
-export const Tag: React.FC<{ accent: string; children: React.ReactNode; size?: "default" | "large" }> = ({ accent, children, size = "default" }) => (
+export const Tag: React.FC<{
+  accent: string;
+  children: React.ReactNode;
+  size?: "default" | "large";
+  emphasized?: boolean;
+}> = ({ accent, children, size = "default", emphasized }) => (
   <div style={{
     display: "inline-flex",
     alignItems: "center",
     gap: 10,
-    padding: "12px 24px",
-    borderRadius: 10,
-    background: "rgba(255,255,255,0.08)",
-    border: `1px solid ${accent}55`,
-    color: "white",
+    padding: "12px 22px",
+    background: emphasized ? accent : "rgba(255,255,255,0.8)",
+    border: `1px solid ${emphasized ? accent : `${accent}44`}`,
+    borderTop: `3px solid ${accent}`,
+    color: emphasized ? "#ffffff" : "#334155",
     fontFamily: "Noto Sans SC",
     fontSize: size === "large" ? 32 : 28,
-    fontWeight: 500,
-    backdropFilter: "blur(4px)",
+    fontWeight: emphasized ? 700 : 500,
   }}>
     {children}
   </div>
@@ -229,7 +239,7 @@ export const ColorDot: React.FC<{ color: string; size?: number }> = ({ color, si
     width: size,
     height: size,
     borderRadius: "50%",
-    background: color || "#ccc",
+    background: color || "#999",
   }} />
 );
 
@@ -238,14 +248,14 @@ const ImageFrame: React.FC<{ children: React.ReactNode; flex?: string | number }
     flex,
     width: flex ? undefined : "100%",
     height: "auto",
-    background: "#0b1220",
-    border: "1px solid rgba(255,255,255,0.10)",
-    borderRadius: 16,
+    background: "#ffffff",
+    border: "1px solid rgba(30,41,59,0.12)",
+    borderRadius: 12,
     overflow: "hidden",
     display: "flex",
     alignItems: "center",
     justifyContent: "center",
-    boxShadow: "0 12px 32px rgba(0,0,0,0.45), 0 2px 8px rgba(0,0,0,0.35)",
+    boxShadow: "0 10px 28px rgba(120,100,60,0.14), 0 2px 6px rgba(120,100,60,0.10)",
   }}>
     {children}
   </div>
@@ -265,9 +275,10 @@ const Section: React.FC<{
   return (
     <div style={{
       flex: flex ?? 1,
-      background: "rgba(255,255,255,0.05)",
-      border: "1px solid rgba(255,255,255,0.10)",
-      borderRadius: 12,
+      background: "rgba(255,255,255,0.88)",
+      border: "1px solid rgba(30,41,59,0.12)",
+      borderTop: `3px solid ${accent}`,
+      borderRadius: 4,
       padding: isLarge ? "24px 32px" : "22px 28px",
       display: "flex",
       flexDirection: "column",
@@ -279,36 +290,25 @@ const Section: React.FC<{
         alignItems: "center",
         gap: 12,
       }}>
+        {/* Report-style label: primary color, wide tracking, no accent bar —
+            the top border bar carries the accent instead. */}
         <span style={{
-          display: "inline-block",
-          width: isLarge ? 6 : 5,
-          height: isLarge ? 32 : 26,
-          background: accent,
-          borderRadius: 2,
-        }} />
-        <span style={{
-          fontSize: isLarge ? 36 : 28,
+          fontSize: isLarge ? 34 : 28,
           fontWeight: 700,
           color: accent,
           fontFamily: "Noto Sans SC",
-          letterSpacing: "0.04em",
+          letterSpacing: "0.14em",
         }}>
           {title}
         </span>
       </div>
       <div style={{
         // Size to content (capped at N lines by -webkit-line-clamp), not by
-        // flex distribution. With flex:1 the body's flex-basis was 0% and its
-        // height was purely the parent's allocation — when the section was
-        // short (images consuming vertical space) the body could end up
-        // shorter than a single line of text, and `overflow: hidden` clipped
-        // the bottom of that line. `0 0 auto` makes body exactly content-
-        // sized: never shrinks, never grows. Empty space below short text is
-        // fine (section background shows through).
+        // flex distribution — mirrors the github-weekly Section rationale.
         flex: "0 0 auto",
         fontSize: isLarge ? 60 : 32,
         lineHeight: isLarge ? 1.35 : 1.4,
-        color: "white",
+        color: "#334155",
         fontFamily: "Noto Sans SC",
         fontWeight: 400,
         display: "-webkit-box",