146 lines
4.8 KiB
TypeScript
146 lines
4.8 KiB
TypeScript
"use client";
|
|
|
|
import { Check, ChevronDown, Monitor, Palette } from "lucide-react";
|
|
import type { LucideIcon } from "lucide-react";
|
|
|
|
import { useProjects } from "@/components/projects/projects-context";
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuLabel,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
} from "@/components/ui/dropdown-menu";
|
|
import {
|
|
LIGHT_OPTIONS,
|
|
DARK_OPTIONS,
|
|
type ThemeOption,
|
|
} from "@/components/theme/theme-toggle";
|
|
import { useTheme, type ThemeName } from "@/components/theme/theme-provider";
|
|
|
|
const DEFAULT_LABEL = "Default theme";
|
|
|
|
/** One named-theme row. Uses DropdownMenuItem (a real menu item) so the
|
|
* menu closes on selection, matching the app's other menus. */
|
|
function ThemeOptionRow({
|
|
option,
|
|
active,
|
|
onSelect,
|
|
}: {
|
|
option: ThemeOption;
|
|
active: boolean;
|
|
onSelect: () => void;
|
|
}) {
|
|
const Icon: LucideIcon = option.icon;
|
|
return (
|
|
<DropdownMenuItem
|
|
onClick={onSelect}
|
|
className="gap-2.5 px-2 py-1.5 text-sm cursor-pointer"
|
|
>
|
|
<Icon className="size-4 text-muted-foreground" />
|
|
<span>{option.label}</span>
|
|
<span
|
|
className="ml-auto size-3 rounded-full ring-1 ring-foreground/15"
|
|
style={{ backgroundColor: option.swatch }}
|
|
aria-hidden
|
|
/>
|
|
{active && <Check className="size-4 shrink-0" />}
|
|
</DropdownMenuItem>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* The per-project theme picker, rendered to the right of a project's name
|
|
* (see components/board/kanban-board.tsx).
|
|
*
|
|
* "Default theme" (project.theme = null) means "whatever the user assigned
|
|
* in the global theme menu" -- the same value this picker's checkmark and
|
|
* label track, live. Picking a named theme stores it on the project; the
|
|
* project page then scopes the app to that theme while it's open
|
|
* (components/theme/project-theme-scope.tsx).
|
|
*/
|
|
export function ProjectThemePicker({ projectId }: { projectId: string }) {
|
|
const { projects, setProjectTheme } = useProjects();
|
|
const { resolvedTheme } = useTheme();
|
|
const project = projects.find((p) => p.id === projectId);
|
|
if (!project) return null;
|
|
|
|
const theme: ThemeName | null = project.theme;
|
|
const activeOption = theme
|
|
? [...LIGHT_OPTIONS, ...DARK_OPTIONS].find((o) => o.value === theme)
|
|
: undefined;
|
|
|
|
return (
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger
|
|
render={
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-8 gap-1.5 rounded-lg px-2 text-[13px] text-muted-foreground hover:text-foreground"
|
|
aria-label="Choose this project's theme"
|
|
>
|
|
<Palette className="size-3.5" />
|
|
<span className="max-w-36 truncate">
|
|
{theme ? activeOption?.label ?? DEFAULT_LABEL : DEFAULT_LABEL}
|
|
</span>
|
|
<ChevronDown className="size-3.5" />
|
|
</Button>
|
|
}
|
|
/>
|
|
<DropdownMenuContent align="start" className="w-60">
|
|
<DropdownMenuItem
|
|
onClick={() => setProjectTheme(projectId, null)}
|
|
className="gap-2.5 px-2 py-1.5 text-sm cursor-pointer"
|
|
>
|
|
<Monitor className="size-4 text-muted-foreground" />
|
|
<span className="min-w-0">
|
|
<span className="block truncate">Default theme</span>
|
|
<span className="block truncate text-xs text-muted-foreground">
|
|
Follows the global theme menu
|
|
</span>
|
|
</span>
|
|
<span
|
|
className="ml-auto size-3 shrink-0 rounded-full ring-1 ring-foreground/15"
|
|
style={{
|
|
background: "linear-gradient(90deg, #4f56e0 50%, #7c83f2 50%)",
|
|
}}
|
|
aria-hidden
|
|
/>
|
|
{theme === null && <Check className="size-4 shrink-0" />}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuLabel>Light</DropdownMenuLabel>
|
|
{LIGHT_OPTIONS.map((option) => (
|
|
<ThemeOptionRow
|
|
key={option.value}
|
|
option={option}
|
|
active={theme === option.value}
|
|
onSelect={() => setProjectTheme(projectId, option.value)}
|
|
/>
|
|
))}
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuLabel>Dark</DropdownMenuLabel>
|
|
{DARK_OPTIONS.map((option) => (
|
|
<ThemeOptionRow
|
|
key={option.value}
|
|
option={option}
|
|
active={theme === option.value}
|
|
onSelect={() => setProjectTheme(projectId, option.value)}
|
|
/>
|
|
))}
|
|
<DropdownMenuSeparator />
|
|
<p className="px-2 py-1.5 text-xs leading-snug text-muted-foreground">
|
|
{theme === null
|
|
? `Currently following your global theme (now: ${
|
|
resolvedTheme.charAt(0).toUpperCase() + resolvedTheme.slice(1)
|
|
}).`
|
|
: `This theme applies only while "${project.title}" is open.`}
|
|
</p>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
);
|
|
}
|