mirror of
https://github.com/Mail-0/Zero.git
synced 2026-06-30 07:46:15 +00:00
badges by color
This commit is contained in:
92
components/icons/animated/moon.tsx
Normal file
92
components/icons/animated/moon.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
"use client";
|
||||
|
||||
import { forwardRef, useCallback, useImperativeHandle, useRef } from "react";
|
||||
import type { Transition, Variants } from "motion/react";
|
||||
import { motion, useAnimation } from "motion/react";
|
||||
import type { HTMLAttributes } from "react";
|
||||
|
||||
export interface MoonIconHandle {
|
||||
startAnimation: () => void;
|
||||
stopAnimation: () => void;
|
||||
}
|
||||
|
||||
const svgVariants: Variants = {
|
||||
normal: {
|
||||
rotate: 0,
|
||||
},
|
||||
animate: {
|
||||
rotate: [0, -10, 10, -5, 5, 0],
|
||||
},
|
||||
};
|
||||
|
||||
const svgTransition: Transition = {
|
||||
duration: 1.2,
|
||||
ease: "easeInOut",
|
||||
};
|
||||
|
||||
const MoonIcon = forwardRef<MoonIconHandle, HTMLAttributes<HTMLDivElement>>(
|
||||
({ onMouseEnter, onMouseLeave, ...props }, ref) => {
|
||||
const controls = useAnimation();
|
||||
const isControlledRef = useRef(false);
|
||||
|
||||
useImperativeHandle(ref, () => {
|
||||
isControlledRef.current = true;
|
||||
|
||||
return {
|
||||
startAnimation: () => controls.start("animate"),
|
||||
stopAnimation: () => controls.start("normal"),
|
||||
};
|
||||
});
|
||||
|
||||
const handleMouseEnter = useCallback(
|
||||
(e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (!isControlledRef.current) {
|
||||
controls.start("animate");
|
||||
} else {
|
||||
onMouseEnter?.(e);
|
||||
}
|
||||
},
|
||||
[controls, onMouseEnter],
|
||||
);
|
||||
|
||||
const handleMouseLeave = useCallback(
|
||||
(e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (!isControlledRef.current) {
|
||||
controls.start("normal");
|
||||
} else {
|
||||
onMouseLeave?.(e);
|
||||
}
|
||||
},
|
||||
[controls, onMouseLeave],
|
||||
);
|
||||
return (
|
||||
<div
|
||||
className="flex cursor-pointer select-none items-center justify-center rounded-md p-2 transition-colors duration-200 hover:bg-accent"
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
{...props}
|
||||
>
|
||||
<motion.svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="15"
|
||||
height="15"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
variants={svgVariants}
|
||||
animate={controls}
|
||||
transition={svgTransition}
|
||||
>
|
||||
<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" />
|
||||
</motion.svg>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
MoonIcon.displayName = "MoonIcon";
|
||||
|
||||
export { MoonIcon };
|
||||
101
components/icons/animated/sun.tsx
Normal file
101
components/icons/animated/sun.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
"use client";
|
||||
|
||||
import { forwardRef, useCallback, useImperativeHandle, useRef } from "react";
|
||||
import { motion, useAnimation } from "motion/react";
|
||||
import type { Variants } from "motion/react";
|
||||
import type { HTMLAttributes } from "react";
|
||||
|
||||
export interface SunIconHandle {
|
||||
startAnimation: () => void;
|
||||
stopAnimation: () => void;
|
||||
}
|
||||
|
||||
const pathVariants: Variants = {
|
||||
normal: { opacity: 1 },
|
||||
animate: (i: number) => ({
|
||||
opacity: [0, 1],
|
||||
transition: { delay: i * 0.1, duration: 0.3 },
|
||||
}),
|
||||
};
|
||||
|
||||
const SunIcon = forwardRef<SunIconHandle, HTMLAttributes<HTMLDivElement>>(
|
||||
({ onMouseEnter, onMouseLeave, ...props }, ref) => {
|
||||
const controls = useAnimation();
|
||||
const isControlledRef = useRef(false);
|
||||
|
||||
useImperativeHandle(ref, () => {
|
||||
isControlledRef.current = true;
|
||||
|
||||
return {
|
||||
startAnimation: () => controls.start("animate"),
|
||||
stopAnimation: () => controls.start("normal"),
|
||||
};
|
||||
});
|
||||
|
||||
const handleMouseEnter = useCallback(
|
||||
(e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (!isControlledRef.current) {
|
||||
controls.start("animate");
|
||||
} else {
|
||||
onMouseEnter?.(e);
|
||||
}
|
||||
},
|
||||
[controls, onMouseEnter],
|
||||
);
|
||||
|
||||
const handleMouseLeave = useCallback(
|
||||
(e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (!isControlledRef.current) {
|
||||
controls.start("normal");
|
||||
} else {
|
||||
onMouseLeave?.(e);
|
||||
}
|
||||
},
|
||||
[controls, onMouseLeave],
|
||||
);
|
||||
return (
|
||||
<div
|
||||
className="flex cursor-pointer select-none items-center justify-center rounded-md p-2 transition-colors duration-200 hover:bg-accent"
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
{...props}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="15"
|
||||
height="15"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="4" />
|
||||
{[
|
||||
"M12 2v2",
|
||||
"m19.07 4.93-1.41 1.41",
|
||||
"M20 12h2",
|
||||
"m17.66 17.66 1.41 1.41",
|
||||
"M12 20v2",
|
||||
"m6.34 17.66-1.41 1.41",
|
||||
"M2 12h2",
|
||||
"m4.93 4.93 1.41 1.41",
|
||||
].map((d, index) => (
|
||||
<motion.path
|
||||
key={d}
|
||||
d={d}
|
||||
animate={controls}
|
||||
variants={pathVariants}
|
||||
custom={index + 1}
|
||||
/>
|
||||
))}
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
SunIcon.displayName = "SunIcon";
|
||||
|
||||
export { SunIcon };
|
||||
@@ -153,7 +153,7 @@ const Thread = ({ message: initialMessage, selectMode, onSelect, isCompact }: Th
|
||||
{messagesCount !== 1 ? (
|
||||
<span className="ml-0.5 text-xs opacity-70">{messagesCount}</span>
|
||||
) : null}
|
||||
{message.unread ? <span className="ml-0.5 size-2 rounded-full bg-blue-500" /> : null}
|
||||
{message.unread ? <span className="ml-0.5 size-2 rounded-full bg-[#006FFE]" /> : null}
|
||||
</p>
|
||||
</div>
|
||||
<p
|
||||
@@ -263,11 +263,19 @@ export function MailList({ items, isCompact, folder }: MailListProps) {
|
||||
function MailLabels({ labels }: { labels: string[] }) {
|
||||
if (!labels.length) return null;
|
||||
|
||||
const visibleLabels = labels.filter(
|
||||
(label) => !["unread", "inbox"].includes(label.toLowerCase()),
|
||||
);
|
||||
|
||||
if (!visibleLabels.length) return null;
|
||||
|
||||
return (
|
||||
<div className={cn("mt-1.5 flex select-none items-center gap-2")}>
|
||||
{labels.map((label) => (
|
||||
<Badge key={label} className="rounded-md" variant={getDefaultBadgeStyle(label)}>
|
||||
<p className="text-xs font-medium lowercase opacity-70">{label.replace(/_/g, " ")}</p>
|
||||
{visibleLabels.map((label) => (
|
||||
<Badge key={label} className="rounded-full" variant={getDefaultBadgeStyle(label)}>
|
||||
<p className="text-xs font-medium lowercase">
|
||||
{label.replace(/^category_/i, "").replace(/_/g, " ")}
|
||||
</p>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
@@ -275,14 +283,19 @@ function MailLabels({ labels }: { labels: string[] }) {
|
||||
}
|
||||
|
||||
function getDefaultBadgeStyle(label: string): ComponentProps<typeof Badge>["variant"] {
|
||||
return "outline";
|
||||
const normalizedLabel = label.toLowerCase().replace(/^category_/i, "");
|
||||
|
||||
// TODO: styling for each tag type
|
||||
switch (true) {
|
||||
case label.toLowerCase() === "work":
|
||||
switch (normalizedLabel) {
|
||||
case "important":
|
||||
return "important";
|
||||
case "promotions":
|
||||
return "promotions";
|
||||
case "personal":
|
||||
return "personal";
|
||||
case "updates":
|
||||
return "updates";
|
||||
case "work":
|
||||
return "default";
|
||||
case label.toLowerCase().startsWith("category_"):
|
||||
return "outline";
|
||||
default:
|
||||
return "secondary";
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
@@ -14,6 +14,14 @@ const badgeVariants = cva(
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
||||
outline: "text-foreground",
|
||||
important:
|
||||
"border-0 bg-amber-100 text-amber-700 hover:bg-amber-200 dark:bg-amber-900/20 dark:text-amber-500 dark:hover:bg-amber-900/30",
|
||||
promotions:
|
||||
"border-0 bg-red-100 text-red-700 hover:bg-red-200 dark:bg-red-900/20 dark:text-red-500 dark:hover:bg-red-900/30",
|
||||
personal:
|
||||
"border-0 bg-green-100 text-green-700 hover:bg-green-200 dark:bg-green-900/20 dark:text-green-500 dark:hover:bg-green-900/30",
|
||||
updates:
|
||||
"border-0 bg-purple-100 text-purple-700 hover:bg-purple-200 dark:bg-purple-900/20 dark:text-purple-500 dark:hover:bg-purple-900/30",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
|
||||
@@ -1,27 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import { LaptopMinimalIcon, MoonIcon, SunIcon } from "lucide-react";
|
||||
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||
import { LaptopMinimalIcon } from "lucide-react";
|
||||
|
||||
import { useTheme } from "next-themes";
|
||||
|
||||
import { Select, SelectContent, SelectItem } from "@/components/ui/select";
|
||||
import { MoonIcon } from "../icons/animated/moon";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { SunIcon } from "../icons/animated/sun";
|
||||
|
||||
export function SidebarThemeSwitch() {
|
||||
const { theme, systemTheme, resolvedTheme, setTheme } = useTheme();
|
||||
|
||||
async function handleThemeChange(newTheme: string) {
|
||||
let nextResolvedTheme = newTheme;
|
||||
|
||||
if (newTheme === "system" && systemTheme) {
|
||||
nextResolvedTheme = systemTheme;
|
||||
}
|
||||
async function handleThemeToggle() {
|
||||
const newTheme = theme === "dark" ? "light" : "dark";
|
||||
|
||||
function update() {
|
||||
setTheme(newTheme);
|
||||
}
|
||||
|
||||
if (document.startViewTransition && nextResolvedTheme !== resolvedTheme) {
|
||||
if (document.startViewTransition && newTheme !== resolvedTheme) {
|
||||
document.documentElement.style.viewTransitionName = "theme-transition";
|
||||
await document.startViewTransition(update).finished;
|
||||
document.documentElement.style.viewTransitionName = "";
|
||||
@@ -31,23 +28,13 @@ export function SidebarThemeSwitch() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Select value={theme} onValueChange={handleThemeChange}>
|
||||
<SelectPrimitive.Trigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="text-muted-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground"
|
||||
>
|
||||
{theme === "dark" && <MoonIcon />}
|
||||
{theme === "light" && <SunIcon />}
|
||||
{theme === "system" && <LaptopMinimalIcon />}
|
||||
</Button>
|
||||
</SelectPrimitive.Trigger>
|
||||
<SelectContent onCloseAutoFocus={(e) => e.preventDefault()}>
|
||||
<SelectItem value="light">Light</SelectItem>
|
||||
<SelectItem value="dark">Dark</SelectItem>
|
||||
<SelectItem value="system">System</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={handleThemeToggle}
|
||||
className="!cursor-pointer text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
||||
>
|
||||
{theme === "dark" ? <MoonIcon /> : <SunIcon />}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user