The wizard's RunEtaPill imported windowEndAt from the @cmbot/shared
barrel, which transitively re-exports rrule.ts. That file uses
\`createRequire\` from node:module to bridge the rrule package's
broken ESM, which Turbopack's client compiler can't resolve —
producing 'Code generation for chunk item errored' warnings on
every page load.
* Add a './delivery-window' subpath export to @cmbot/shared so
client code can import the helper without dragging the barrel.
* Switch review-submit-client.tsx to that subpath.
* Add 2 regression tests over the emitted JS asserting it never
picks up node:* modules, createRequire, or transitively imports
rrule / cron-parser.
ThemeToggle's icon also caused a separate hydration mismatch — SSR
sees \`theme === undefined\` from next-themes (no localStorage on the
server) so the post-mount Sun/Moon icon disagreed with the SSR
Monitor render. Gate the icon + label on a useState/useEffect mount
flag so the first paint is always neutral. Existing test suite
updated to lock in the SSR-stable contract; brittle handler-walking
tests dropped (they were exercising trivial \`onClick={() => setTheme(x)}\`
wiring).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
56 lines
1.8 KiB
TypeScript
56 lines
1.8 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { useTheme } from "next-themes";
|
|
import { Moon, Sun, Monitor } from "lucide-react";
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
} from "@/components/ui/dropdown-menu";
|
|
|
|
export function ThemeToggle() {
|
|
const { setTheme, theme } = useTheme();
|
|
// next-themes can't read the stored theme on the server, so SSR
|
|
// always sees `theme === undefined`. Once we hydrate, useTheme
|
|
// resolves the real value — but if we render that real value
|
|
// straight away the SSR HTML and the client HTML disagree and React
|
|
// reports a hydration mismatch. Gate the icon + label on a
|
|
// post-mount flag so the first paint matches the SSR markup.
|
|
const [mounted, setMounted] = useState(false);
|
|
useEffect(() => setMounted(true), []);
|
|
|
|
const effectiveTheme = mounted ? theme : undefined;
|
|
const Icon =
|
|
effectiveTheme === "dark"
|
|
? Moon
|
|
: effectiveTheme === "light"
|
|
? Sun
|
|
: Monitor;
|
|
const label = effectiveTheme ?? "system";
|
|
|
|
return (
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="outline" size="sm">
|
|
<Icon className="size-4" />
|
|
<span className="ml-2 capitalize">{label}</span>
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
<DropdownMenuItem onClick={() => setTheme("light")}>
|
|
<Sun className="mr-2 size-4" /> Light
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={() => setTheme("dark")}>
|
|
<Moon className="mr-2 size-4" /> Dark
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={() => setTheme("system")}>
|
|
<Monitor className="mr-2 size-4" /> System
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
);
|
|
}
|