badges by color

This commit is contained in:
Nizzy
2025-02-16 16:58:30 -05:00
parent fb0c9f6a21
commit dccedbd673
5 changed files with 240 additions and 39 deletions

View 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 };

View 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 };

View File

@@ -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";
}

View File

@@ -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: {

View File

@@ -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>
);
}