diff --git a/apps/dokploy/components/dashboard/application/deployments/show-deployment.tsx b/apps/dokploy/components/dashboard/application/deployments/show-deployment.tsx
index 96e32d8c..380b22d9 100644
--- a/apps/dokploy/components/dashboard/application/deployments/show-deployment.tsx
+++ b/apps/dokploy/components/dashboard/application/deployments/show-deployment.tsx
@@ -111,7 +111,7 @@ export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => {
{
filteredLogs.length > 0 ? filteredLogs.map((log: LogLine, index: number) => (
diff --git a/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx b/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx
index db1c774b..3f30c292 100644
--- a/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx
+++ b/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx
@@ -1,309 +1,291 @@
-import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select";
import { api } from "@/utils/api";
import { Download as DownloadIcon, Loader2 } from "lucide-react";
import React, { useEffect, useRef } from "react";
+import { LineCountFilter } from "./line-count-filter";
+import { SinceLogsFilter, type TimeFilter } from "./since-logs-filter";
+import { StatusLogsFilter } from "./status-logs-filter";
import { TerminalLine } from "./terminal-line";
import { type LogLine, getLogType, parseLogs } from "./utils";
interface Props {
- containerId: string;
- serverId?: string | null;
+ containerId: string;
+ serverId?: string | null;
}
-type TimeFilter = "all" | "1h" | "6h" | "24h" | "168h" | "720h";
-type TypeFilter = "all" | "error" | "warning" | "success" | "info" | "debug";
+export const priorities = [
+ {
+ label: "Info",
+ value: "info",
+ },
+ {
+ label: "Success",
+ value: "success",
+ },
+ {
+ label: "Warning",
+ value: "warning",
+ },
+ {
+ label: "Debug",
+ value: "debug",
+ },
+ {
+ label: "Error",
+ value: "error",
+ },
+];
export const DockerLogsId: React.FC = ({ containerId, serverId }) => {
- const { data } = api.docker.getConfig.useQuery(
- {
- containerId,
- serverId: serverId ?? undefined,
- },
- {
- enabled: !!containerId,
- }
- );
+ const { data } = api.docker.getConfig.useQuery(
+ {
+ containerId,
+ serverId: serverId ?? undefined,
+ },
+ {
+ enabled: !!containerId,
+ },
+ );
- const [rawLogs, setRawLogs] = React.useState("");
- const [filteredLogs, setFilteredLogs] = React.useState([]);
- const [autoScroll, setAutoScroll] = React.useState(true);
- const [lines, setLines] = React.useState(100);
- const [search, setSearch] = React.useState("");
+ const [rawLogs, setRawLogs] = React.useState("");
+ const [filteredLogs, setFilteredLogs] = React.useState([]);
+ const [autoScroll, setAutoScroll] = React.useState(true);
+ const [lines, setLines] = React.useState(100);
+ const [search, setSearch] = React.useState("");
+ const [showTimestamp, setShowTimestamp] = React.useState(true);
+ const [since, setSince] = React.useState("all");
+ const [typeFilter, setTypeFilter] = React.useState([]);
+ const scrollRef = useRef(null);
+ const [isLoading, setIsLoading] = React.useState(false);
- const [since, setSince] = React.useState("all");
- const [typeFilter, setTypeFilter] = React.useState("all");
- const scrollRef = useRef(null);
- const [isLoading, setIsLoading] = React.useState(false);
+ const scrollToBottom = () => {
+ if (autoScroll && scrollRef.current) {
+ scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
+ }
+ };
- const scrollToBottom = () => {
- if (autoScroll && scrollRef.current) {
- scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
- }
- };
+ const handleScroll = () => {
+ if (!scrollRef.current) return;
- const handleScroll = () => {
- if (!scrollRef.current) return;
+ const { scrollTop, scrollHeight, clientHeight } = scrollRef.current;
+ const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 10;
+ setAutoScroll(isAtBottom);
+ };
- const { scrollTop, scrollHeight, clientHeight } = scrollRef.current;
- const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 10;
- setAutoScroll(isAtBottom);
- };
+ const handleSearch = (e: React.ChangeEvent) => {
+ setSearch(e.target.value || "");
+ };
- const handleSearch = (e: React.ChangeEvent) => {
- setSearch(e.target.value || "");
- };
+ const handleLines = (lines: number) => {
+ setRawLogs("");
+ setFilteredLogs([]);
+ setLines(lines);
+ };
- const handleLines = (e: React.ChangeEvent) => {
- setRawLogs("");
- setFilteredLogs([]);
- setLines(Number(e.target.value) || 1);
- };
+ const handleSince = (value: TimeFilter) => {
+ setRawLogs("");
+ setFilteredLogs([]);
+ setSince(value);
+ };
- const handleSince = (value: TimeFilter) => {
- setRawLogs("");
- setFilteredLogs([]);
- setSince(value);
- };
+ useEffect(() => {
+ if (!containerId) return;
- const handleTypeFilter = (value: TypeFilter) => {
- setTypeFilter(value);
- };
+ let isCurrentConnection = true;
+ let noDataTimeout: NodeJS.Timeout;
+ setIsLoading(true);
+ setRawLogs("");
+ setFilteredLogs([]);
- useEffect(() => {
- if (!containerId) return;
-
- let isCurrentConnection = true;
- let noDataTimeout: NodeJS.Timeout;
- setIsLoading(true);
- setRawLogs("");
- setFilteredLogs([]);
+ const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
+ const params = new globalThis.URLSearchParams({
+ containerId,
+ tail: lines.toString(),
+ since,
+ search,
+ });
- const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
- const params = new globalThis.URLSearchParams({
- containerId,
- tail: lines.toString(),
- since,
- search,
- });
+ if (serverId) {
+ params.append("serverId", serverId);
+ }
- if (serverId) {
- params.append("serverId", serverId);
- }
+ const wsUrl = `${protocol}//${
+ window.location.host
+ }/docker-container-logs?${params.toString()}`;
+ console.log("Connecting to WebSocket:", wsUrl);
+ const ws = new WebSocket(wsUrl);
- const wsUrl = `${protocol}//${
- window.location.host
- }/docker-container-logs?${params.toString()}`;
- console.log("Connecting to WebSocket:", wsUrl);
- const ws = new WebSocket(wsUrl);
+ const resetNoDataTimeout = () => {
+ if (noDataTimeout) clearTimeout(noDataTimeout);
+ noDataTimeout = setTimeout(() => {
+ if (isCurrentConnection) {
+ setIsLoading(false);
+ }
+ }, 2000); // Wait 2 seconds for data before showing "No logs found"
+ };
- const resetNoDataTimeout = () => {
- if (noDataTimeout) clearTimeout(noDataTimeout);
- noDataTimeout = setTimeout(() => {
- if (isCurrentConnection) {
- setIsLoading(false);
- }
- }, 2000); // Wait 2 seconds for data before showing "No logs found"
- };
+ ws.onopen = () => {
+ if (!isCurrentConnection) {
+ ws.close();
+ return;
+ }
+ console.log("WebSocket connected");
+ resetNoDataTimeout();
+ };
- ws.onopen = () => {
- if (!isCurrentConnection) {
- ws.close();
- return;
- }
- console.log("WebSocket connected");
- resetNoDataTimeout();
- };
+ ws.onmessage = (e) => {
+ if (!isCurrentConnection) return;
+ setRawLogs((prev) => prev + e.data);
+ setIsLoading(false);
+ if (noDataTimeout) clearTimeout(noDataTimeout);
+ };
- ws.onmessage = (e) => {
- if (!isCurrentConnection) return;
- setRawLogs((prev) => prev + e.data);
- setIsLoading(false);
- if (noDataTimeout) clearTimeout(noDataTimeout);
- };
+ ws.onerror = (error) => {
+ if (!isCurrentConnection) return;
+ console.error("WebSocket error:", error);
+ setIsLoading(false);
+ if (noDataTimeout) clearTimeout(noDataTimeout);
+ };
- ws.onerror = (error) => {
- if (!isCurrentConnection) return;
- console.error("WebSocket error:", error);
- setIsLoading(false);
- if (noDataTimeout) clearTimeout(noDataTimeout);
- };
+ ws.onclose = (e) => {
+ if (!isCurrentConnection) return;
+ console.log("WebSocket closed:", e.reason);
+ setIsLoading(false);
+ if (noDataTimeout) clearTimeout(noDataTimeout);
+ };
- ws.onclose = (e) => {
- if (!isCurrentConnection) return;
- console.log("WebSocket closed:", e.reason);
- setIsLoading(false);
- if (noDataTimeout) clearTimeout(noDataTimeout);
- };
+ return () => {
+ isCurrentConnection = false;
+ if (noDataTimeout) clearTimeout(noDataTimeout);
+ if (ws.readyState === WebSocket.OPEN) {
+ ws.close();
+ }
+ };
+ }, [containerId, serverId, lines, search, since]);
- return () => {
- isCurrentConnection = false;
- if (noDataTimeout) clearTimeout(noDataTimeout);
- if (ws.readyState === WebSocket.OPEN) {
- ws.close();
- }
- };
- }, [containerId, serverId, lines, search, since]);
+ const handleDownload = () => {
+ const logContent = filteredLogs
+ .map(
+ ({ timestamp, message }: { timestamp: Date | null; message: string }) =>
+ `${timestamp?.toISOString() || "No timestamp"} ${message}`,
+ )
+ .join("\n");
- const handleDownload = () => {
- const logContent = filteredLogs
- .map(
- ({ timestamp, message }: { timestamp: Date | null; message: string }) =>
- `${timestamp?.toISOString() || "No timestamp"} ${message}`
- )
- .join("\n");
+ const blob = new Blob([logContent], { type: "text/plain" });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ const appName = data.Name.replace("/", "") || "app";
+ const isoDate = new Date().toISOString();
+ a.href = url;
+ a.download = `${appName}-${isoDate.slice(0, 10).replace(/-/g, "")}_${isoDate
+ .slice(11, 19)
+ .replace(/:/g, "")}.log.txt`;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+ };
- const blob = new Blob([logContent], { type: "text/plain" });
- const url = URL.createObjectURL(blob);
- const a = document.createElement("a");
- const appName = data.Name.replace("/", "") || "app";
- const isoDate = new Date().toISOString();
- a.href = url;
- a.download = `${appName}-${isoDate.slice(0, 10).replace(/-/g, "")}_${isoDate
- .slice(11, 19)
- .replace(/:/g, "")}.log.txt`;
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
- URL.revokeObjectURL(url);
- };
+ const handleFilter = (logs: LogLine[]) => {
+ return logs.filter((log) => {
+ const logType = getLogType(log.message).type;
- const handleFilter = (logs: LogLine[]) => {
- return logs.filter((log) => {
- const logType = getLogType(log.message).type;
+ if (typeFilter.length === 0) {
+ return true;
+ }
- const matchesType = typeFilter === "all" || logType === typeFilter;
+ return typeFilter.includes(logType);
+ });
+ };
- return matchesType;
- });
- };
+ useEffect(() => {
+ setRawLogs("");
+ setFilteredLogs([]);
+ }, [containerId]);
- useEffect(() => {
- setRawLogs("");
- setFilteredLogs([]);
- }, [containerId]);
+ useEffect(() => {
+ const logs = parseLogs(rawLogs);
+ const filtered = handleFilter(logs);
+ setFilteredLogs(filtered);
+ }, [rawLogs, search, lines, since, typeFilter]);
- useEffect(() => {
- const logs = parseLogs(rawLogs);
- const filtered = handleFilter(logs);
- setFilteredLogs(filtered);
- }, [rawLogs, search, lines, since, typeFilter]);
+ useEffect(() => {
+ scrollToBottom();
- useEffect(() => {
- scrollToBottom();
+ if (autoScroll && scrollRef.current) {
+ scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
+ }
+ }, [filteredLogs, autoScroll]);
- if (autoScroll && scrollRef.current) {
- scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
- }
- }, [filteredLogs, autoScroll]);
+ return (
+
+
+
+
+
+
- return (
-
-
-
-
-
-
-
-
- {filteredLogs.length > 0 ? (
- filteredLogs.map((filteredLog: LogLine, index: number) => (
-
- ))
- ) : isLoading ? (
-
-
-
- ) : (
-
- No logs found
-
- )}
-
-
-
-
- );
-};
\ No newline at end of file
+
+
+
+ {filteredLogs.length > 0 ? (
+ filteredLogs.map((filteredLog: LogLine, index: number) => (
+
+ ))
+ ) : isLoading ? (
+
+
+
+ ) : (
+
+ No logs found
+
+ )}
+
+
+
+
+ );
+};
diff --git a/apps/dokploy/components/dashboard/docker/logs/line-count-filter.tsx b/apps/dokploy/components/dashboard/docker/logs/line-count-filter.tsx
new file mode 100644
index 00000000..dd7b63af
--- /dev/null
+++ b/apps/dokploy/components/dashboard/docker/logs/line-count-filter.tsx
@@ -0,0 +1,173 @@
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@/components/ui/popover";
+import { Separator } from "@/components/ui/separator";
+import { cn } from "@/lib/utils";
+import { Command as CommandPrimitive } from "cmdk";
+import { debounce } from "lodash";
+import { CheckIcon, Hash } from "lucide-react";
+import React, { useCallback, useRef } from "react";
+
+const lineCountOptions = [
+ { label: "100 lines", value: 100 },
+ { label: "300 lines", value: 300 },
+ { label: "500 lines", value: 500 },
+ { label: "1000 lines", value: 1000 },
+ { label: "5000 lines", value: 5000 },
+] as const;
+
+interface LineCountFilterProps {
+ value: number;
+ onValueChange: (value: number) => void;
+ title?: string;
+}
+
+export function LineCountFilter({
+ value,
+ onValueChange,
+ title = "Limit to",
+}: LineCountFilterProps) {
+ const [open, setOpen] = React.useState(false);
+ const [inputValue, setInputValue] = React.useState("");
+ const pendingValueRef = useRef(null);
+
+ const isPresetValue = lineCountOptions.some(
+ (option) => option.value === value,
+ );
+
+ const debouncedValueChange = useCallback(
+ debounce((numValue: number) => {
+ if (numValue > 0 && numValue !== value) {
+ onValueChange(numValue);
+ pendingValueRef.current = null;
+ }
+ }, 500),
+ [onValueChange, value],
+ );
+
+ const handleInputChange = (input: string) => {
+ setInputValue(input);
+
+ // Extract numbers from input and convert
+ const numValue = Number.parseInt(input.replace(/[^0-9]/g, ""));
+ if (!Number.isNaN(numValue)) {
+ pendingValueRef.current = numValue;
+ debouncedValueChange(numValue);
+ }
+ };
+
+ const handleSelect = (selectedValue: string) => {
+ const preset = lineCountOptions.find((opt) => opt.label === selectedValue);
+ if (preset) {
+ if (preset.value !== value) {
+ onValueChange(preset.value);
+ }
+ setInputValue("");
+ setOpen(false);
+ return;
+ }
+
+ const numValue = Number.parseInt(selectedValue);
+ if (
+ !Number.isNaN(numValue) &&
+ numValue > 0 &&
+ numValue !== value &&
+ numValue !== pendingValueRef.current
+ ) {
+ onValueChange(numValue);
+ setInputValue("");
+ setOpen(false);
+ }
+ };
+
+ React.useEffect(() => {
+ return () => {
+ debouncedValueChange.cancel();
+ };
+ }, [debouncedValueChange]);
+
+ const displayValue = isPresetValue
+ ? lineCountOptions.find((option) => option.value === value)?.label
+ : `${value} lines`;
+
+ return (
+
+
+
+
+
+
+
+
+ {
+ if (e.key === "Enter") {
+ e.preventDefault();
+ const numValue = Number.parseInt(
+ inputValue.replace(/[^0-9]/g, ""),
+ );
+ if (
+ !Number.isNaN(numValue) &&
+ numValue > 0 &&
+ numValue !== value &&
+ numValue !== pendingValueRef.current
+ ) {
+ handleSelect(inputValue);
+ }
+ }
+ }}
+ />
+
+
+
+ {lineCountOptions.map((option) => {
+ const isSelected = value === option.value;
+ return (
+ handleSelect(option.label)}
+ className="relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 aria-selected:bg-accent aria-selected:text-accent-foreground"
+ >
+
+
+
+ {option.label}
+
+ );
+ })}
+
+
+
+
+
+ );
+}
+
+export default LineCountFilter;
diff --git a/apps/dokploy/components/dashboard/docker/logs/since-logs-filter.tsx b/apps/dokploy/components/dashboard/docker/logs/since-logs-filter.tsx
new file mode 100644
index 00000000..b7caafe7
--- /dev/null
+++ b/apps/dokploy/components/dashboard/docker/logs/since-logs-filter.tsx
@@ -0,0 +1,125 @@
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import {
+ Command,
+ CommandGroup,
+ CommandItem,
+ CommandList,
+} from "@/components/ui/command";
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@/components/ui/popover";
+import { Separator } from "@/components/ui/separator";
+import { Switch } from "@/components/ui/switch";
+import { cn } from "@/lib/utils";
+import { CheckIcon } from "lucide-react";
+import React from "react";
+
+export type TimeFilter = "all" | "1h" | "6h" | "24h" | "168h" | "720h";
+
+const timeRanges: Array<{ label: string; value: TimeFilter }> = [
+ {
+ label: "All time",
+ value: "all",
+ },
+ {
+ label: "Last hour",
+ value: "1h",
+ },
+ {
+ label: "Last 6 hours",
+ value: "6h",
+ },
+ {
+ label: "Last 24 hours",
+ value: "24h",
+ },
+ {
+ label: "Last 7 days",
+ value: "168h",
+ },
+ {
+ label: "Last 30 days",
+ value: "720h",
+ },
+] as const;
+
+interface SinceLogsFilterProps {
+ value: TimeFilter;
+ onValueChange: (value: TimeFilter) => void;
+ showTimestamp: boolean;
+ onTimestampChange: (show: boolean) => void;
+ title?: string;
+}
+
+export function SinceLogsFilter({
+ value,
+ onValueChange,
+ showTimestamp,
+ onTimestampChange,
+ title = "Time range",
+}: SinceLogsFilterProps) {
+ const selectedLabel =
+ timeRanges.find((range) => range.value === value)?.label ??
+ "Select time range";
+
+ return (
+
+
+
+
+
+
+
+
+ {timeRanges.map((range) => {
+ const isSelected = value === range.value;
+ return (
+ {
+ if (!isSelected) {
+ onValueChange(range.value);
+ }
+ }}
+ >
+
+
+
+ {range.label}
+
+ );
+ })}
+
+
+
+
+
+ Show timestamps
+
+
+
+
+ );
+}
diff --git a/apps/dokploy/components/dashboard/docker/logs/status-logs-filter.tsx b/apps/dokploy/components/dashboard/docker/logs/status-logs-filter.tsx
new file mode 100644
index 00000000..3ef11517
--- /dev/null
+++ b/apps/dokploy/components/dashboard/docker/logs/status-logs-filter.tsx
@@ -0,0 +1,170 @@
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import {
+ Command,
+ CommandGroup,
+ CommandItem,
+ CommandList,
+} from "@/components/ui/command";
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@/components/ui/popover";
+import { Separator } from "@/components/ui/separator";
+import { cn } from "@/lib/utils";
+import { CheckIcon } from "lucide-react";
+import type React from "react";
+
+interface StatusLogsFilterProps {
+ value?: string[];
+ setValue?: (value: string[]) => void;
+ title?: string;
+ options: {
+ label: string;
+ value: string;
+ icon?: React.ComponentType<{ className?: string }>;
+ }[];
+}
+
+export function StatusLogsFilter({
+ value = [],
+ setValue,
+ title,
+ options,
+}: StatusLogsFilterProps) {
+ const selectedValues = new Set(value as string[]);
+ const allSelected = selectedValues.size === 0;
+
+ const getSelectedBadges = () => {
+ if (allSelected) {
+ return (
+
+ All
+
+ );
+ }
+
+ if (selectedValues.size >= 1) {
+ const selected = options.find((opt) => selectedValues.has(opt.value));
+ return (
+ <>
+
+ {selected?.label}
+
+ {selectedValues.size > 1 && (
+
+ +{selectedValues.size - 1}
+
+ )}
+ >
+ );
+ }
+
+ return null;
+ };
+
+ return (
+
+
+
+
+
+
+
+
+ {
+ setValue?.([]); // Empty array means "All"
+ }}
+ >
+
+
+
+ All
+
+ {options.map((option) => {
+ const isSelected = selectedValues.has(option.value);
+ return (
+ {
+ const newValues = new Set(selectedValues);
+ if (isSelected) {
+ newValues.delete(option.value);
+ } else {
+ newValues.add(option.value);
+ }
+ setValue?.(Array.from(newValues));
+ }}
+ >
+
+
+
+ {option.icon && (
+
+ )}
+
+ {option.label}
+
+
+ );
+ })}
+
+
+
+
+
+ );
+}
diff --git a/apps/dokploy/components/ui/badge.tsx b/apps/dokploy/components/ui/badge.tsx
index 911b0071..9c41234d 100644
--- a/apps/dokploy/components/ui/badge.tsx
+++ b/apps/dokploy/components/ui/badge.tsx
@@ -14,14 +14,14 @@ const badgeVariants = cva(
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
- red: "border-transparent select-none items-center whitespace-nowrap font-medium bg-red-500/15 text-destructive text-xs h-4 px-1 py-1 rounded-md",
+ red: "border-transparent select-none items-center whitespace-nowrap font-medium bg-red-600/20 dark:bg-red-500/15 text-destructive text-xs h-4 px-1 py-1 rounded-md",
yellow:
- "border-transparent select-none items-center whitespace-nowrap font-medium bg-yellow-500/15 text-yellow-500 text-xs h-4 px-1 py-1 rounded-md",
+ "border-transparent select-none items-center whitespace-nowrap font-medium bg-yellow-600/20 dark:bg-yellow-500/15 dark:text-yellow-500 text-yellow-600 text-xs h-4 px-1 py-1 rounded-md",
orange:
- "border-transparent select-none items-center whitespace-nowrap font-medium bg-orange-500/15 text-orange-500 text-xs h-4 px-1 py-1 rounded-md",
+ "border-transparent select-none items-center whitespace-nowrap font-medium bg-orange-600/20 dark:bg-orange-500/15 dark:text-orange-500 text-orange-600 text-xs h-4 px-1 py-1 rounded-md",
green:
- "border-transparent select-none items-center whitespace-nowrap font-medium bg-emerald-500/15 text-emerald-500 text-xs h-4 px-1 py-1 rounded-md",
- blue: "border-transparent select-none items-center whitespace-nowrap font-medium bg-blue-500/15 text-blue-500 text-xs h-4 px-1 py-1 rounded-md",
+ "border-transparent select-none items-center whitespace-nowrap font-medium bg-emerald-600/20 dark:bg-emerald-500/15 dark:text-emerald-500 text-emerald-600 text-xs h-4 px-1 py-1 rounded-md",
+ blue: "border-transparent select-none items-center whitespace-nowrap font-medium bg-blue-600/20 dark:bg-blue-500/15 dark:text-blue-500 text-blue-600 text-xs h-4 px-1 py-1 rounded-md",
blank:
"border-transparent select-none items-center whitespace-nowrap font-medium dark:bg-white/15 bg-black/15 text-foreground text-xs h-4 px-1 py-1 rounded-md",
outline: "text-foreground",