跳转到主要内容
Vibestrap 故意只发浅色主题。理由:双主题维护成本高,营销页作为 单主题更有定见,对标的高端脚手架(Stripe、Loops、Resend、Clerk、 tuwa.ai)也都不支持暗黑。 但很多开发者真的有暗黑偏好。如果你的成品需要,按下面五步加回。

Step 1 — 重新安装 next-themes

pnpm add next-themes

Step 2 — 用 ThemeProvider 包起来

编辑 src/components/providers.tsx
'use client';

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ThemeProvider } from 'next-themes';
import { useState } from 'react';
import { Toaster } from 'sonner';

export function Providers({ children }: { children: React.ReactNode }) {
  const [queryClient] = useState(
    () =>
      new QueryClient({
        defaultOptions: {
          queries: { staleTime: 60 * 1000, refetchOnWindowFocus: false },
        },
      })
  );
  return (
    <ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
      <QueryClientProvider client={queryClient}>
        {children}
        <Toaster position="top-center" richColors />
      </QueryClientProvider>
    </ThemeProvider>
  );
}
注意:<Toaster> 上的 theme="light" 要去掉——sonner 会自动跟随 next-themes

Step 3 — 在 globals.css 里加回 .dark

src/app/globals.css 顶部(@plugin 之后)加 variant 指令:
@custom-variant dark (&:is(.dark *));
然后在 :root { ... } 之后加一段 .dark { ... },定义反色调色板。 基于编辑式 cream-mono 基线的话,大概是这样:
.dark {
  --background: oklch(0.16 0.01 85);
  --foreground: oklch(0.96 0.012 85);
  --card: oklch(0.20 0.012 85);
  --card-foreground: oklch(0.96 0.012 85);
  --popover: oklch(0.20 0.012 85);
  --popover-foreground: oklch(0.96 0.012 85);
  --primary: oklch(0.96 0.012 85);
  --primary-foreground: oklch(0.16 0.01 85);
  --secondary: oklch(0.26 0.012 85);
  --secondary-foreground: oklch(0.96 0.012 85);
  --muted: oklch(0.26 0.012 85);
  --muted-foreground: oklch(0.7 0.005 80);
  --accent: oklch(0.26 0.012 85);
  --accent-foreground: oklch(0.96 0.012 85);
  --border: oklch(1 0 0 / 10%);
  --input: oklch(1 0 0 / 14%);
  --ring: oklch(0.55 0.2 250);
}
按你的皮肤微调——上面是 cream-mono 编辑式默认值的对应反相。

Step 4 — 恢复切换组件

新建 src/components/layout/theme-toggle.tsx
'use client';

import { Moon, Sun } from 'lucide-react';
import { useTheme } from 'next-themes';
import { Button } from '@/components/ui/button';

export function ThemeToggle() {
  const { setTheme, resolvedTheme } = useTheme();
  const next = resolvedTheme === 'dark' ? 'light' : 'dark';
  return (
    <Button
      variant="ghost"
      size="icon"
      aria-label="Toggle theme"
      onClick={() => setTheme(next)}
    >
      <Sun className="rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
      <Moon className="absolute rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
    </Button>
  );
}
挂到 src/components/layout/header.tsx
import { ThemeToggle } from './theme-toggle';
// …
<div className="flex items-center gap-1">
  <LocaleSwitcher />
  <ThemeToggle />
  <UserNav />
</div>

Step 5 — 在需要的地方补回 dark: Tailwind variant

Vibestrap 中原本用过 dark: 补色的位置:
  • Markdown 长文页——在 <div className="prose ..."> 外层加回 dark:prose-invert。涉及 src/app/[locale]/(marketing)/{terms,privacy,refund,license,about,changelog,blog/[slug]}/page.tsx 几个文件。
  • src/components/ai/* 的状态色——text-emerald-600 等需要配对 dark:text-emerald-400 才能在暗色下读得舒服。
  • src/components/ui/alert.tsx——destructive 变体的可读性。
  • src/components/turnstile-field.tsx——用 resolvedTheme 传给 Turnstile widget,让它的内嵌 UI 跟随主题。
可以用这条命令找需要补的位置:
grep -rn "text-emerald-600\|text-amber-600\|prose " src/
…然后在每条旁边加上对应的 dark:* 类。

验收

五步走完跑一遍闸:
pnpm typecheck && pnpm lint && pnpm check:i18n && pnpm build
浏览器里切换主题确认:
  • 营销页两种主题都干净。
  • 状态色 / accent 在暗色下可读。
  • Sonner toast 跟主题。
  • Turnstile widget 颜色一致。

我们为什么不发

继承的维护税:每次改组件都得问”暗色下也行吗”。多数买家面向的消费 受众不会在营销页切暗黑。编辑式 / cream-mono 美学(Vibestrap 的默认 皮)本就是浅色为主。 如果你照上面走完发现哪里有漏洞,告诉我们一声——我们改这页文档,而不 是把暗黑模式加回主分支。