From 5a1bd6e6ae10d5c07a5c92814c37cb9d6ab0a676 Mon Sep 17 00:00:00 2001 From: Oro Date: Sat, 28 Mar 2026 11:51:35 +0100 Subject: [PATCH 01/11] feat: layout switching --- .../components/dashboard/projects/show.tsx | 17 ++- apps/dokploy/components/ui/cards-layout.tsx | 117 ++++++++++++++++++ apps/dokploy/package.json | 2 +- .../environment/[environmentId].tsx | 12 +- pnpm-lock.yaml | 2 +- 5 files changed, 140 insertions(+), 10 deletions(-) create mode 100644 apps/dokploy/components/ui/cards-layout.tsx diff --git a/apps/dokploy/components/dashboard/projects/show.tsx b/apps/dokploy/components/dashboard/projects/show.tsx index 1d3055e5e2..0374ef8ee4 100644 --- a/apps/dokploy/components/dashboard/projects/show.tsx +++ b/apps/dokploy/components/dashboard/projects/show.tsx @@ -37,6 +37,12 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; +import { + CardsLayout, + getDefaultLayout, + LayoutSwitcher, + type Layout, +} from "@/components/ui/cards-layout"; import { DropdownMenu, DropdownMenuContent, @@ -57,6 +63,7 @@ import { HandleProject } from "./handle-project"; import { ProjectEnvironment } from "./project-environment"; export const ShowProjects = () => { + const [layout, setLayout] = useState(() => getDefaultLayout()); const utils = api.useUtils(); const router = useRouter(); const { data: isCloud } = api.settings.isCloud.useQuery(); @@ -279,6 +286,7 @@ export const ShowProjects = () => { + {filteredProjects?.length === 0 && (
@@ -288,7 +296,7 @@ export const ShowProjects = () => {
)} -
+ {filteredProjects?.map((project) => { const emptyServices = project?.environments .map( @@ -326,10 +334,7 @@ export const ShowProjects = () => { const hasNoEnvironments = !accessibleEnvironment; return ( -
+
{
); })} -
+
)} diff --git a/apps/dokploy/components/ui/cards-layout.tsx b/apps/dokploy/components/ui/cards-layout.tsx new file mode 100644 index 0000000000..22ac66277e --- /dev/null +++ b/apps/dokploy/components/ui/cards-layout.tsx @@ -0,0 +1,117 @@ +import { LayoutGridIcon, LayoutListIcon } from "lucide-react"; +import React, { useEffect } from "react"; +import { Toggle } from "@/components/ui/toggle"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { cn } from "@/lib/utils"; + +export enum Layout { + GRID = "grid", + LIST = "list", +} + +export type CardsLayoutProps = React.HTMLAttributes & { + layout: Layout; +}; + +/** + * LayoutSwitcher component that allows users to toggle between grid and list layouts for displaying cards. It uses a Toggle button to switch layouts and a Tooltip to provide context on the action. The selected layout is saved in localStorage to persist user preference across sessions. + * @param layout The layout to use for the cards, either "grid" or "list". + * @param setLayout Function to update the layout state in the parent component. + * @example + * const [layout, setLayout] = useState(() => getDefaultLayout()); + * @returns JSX.Element + */ +const LayoutSwitcher = ({ + layout, + setLayout, +}: { + layout: Layout; + setLayout: (layout: Layout) => void; +}) => { + const iconClass = "w-5 h-5"; + return ( + + + + setLayout(pressed ? Layout.GRID : Layout.LIST) + } + > + {layout === Layout.GRID ? ( + + ) : ( + + )} + + + +

Switch to {layout === Layout.GRID ? "list" : "grid"} view

+
+
+ ); +}; +/** + * Retrieves the default layout for the cards, either from localStorage or defaults to "grid". + * @returns The default layout, either "grid" or "list". + */ +export function getDefaultLayout(): Layout { + if (typeof window !== "undefined") { + const savedLayout = localStorage.getItem("servicesLayout") as Layout; + if (savedLayout === Layout.GRID || savedLayout === Layout.LIST) { + return savedLayout; + } + } + return Layout.GRID; // Default layout +} + +/** + * CardsLayout component that wraps its children in a grid or list layout based on the provided layout prop. It also saves the user's layout preference in localStorage. + * + * @param layout The layout to use for the cards, either "grid" or "list". + * @example Layout state should be managed this way in the parent component: + * const [layout, setLayout] = useState(() => getDefaultLayout()); + * + * + * {children} + * + * + * The LayoutSwitcher component can be used to toggle between grid and list layouts, and it will update the layout state in the parent component accordingly. + * + * + * + * This implementation ensures that the user's layout preference is persisted across sessions and provides an easy way to switch between different layouts. + * @returns JSX.Element + */ +const CardsLayout = React.forwardRef( + ({ layout, className, children, ...props }, ref) => { + useEffect(() => { + localStorage.setItem("servicesLayout", layout); + }, [layout]); + return ( +
+
+ {children} +
+
+ ); + }, +); + +CardsLayout.displayName = "CardsLayout"; +LayoutSwitcher.displayName = "LayoutSwitcher"; + +export { CardsLayout, LayoutSwitcher }; diff --git a/apps/dokploy/package.json b/apps/dokploy/package.json index 09ec864c41..88f0e3e7f6 100644 --- a/apps/dokploy/package.json +++ b/apps/dokploy/package.json @@ -79,7 +79,7 @@ "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-switch": "^1.2.5", "@radix-ui/react-tabs": "^1.1.12", - "@radix-ui/react-toggle": "^1.1.9", + "@radix-ui/react-toggle": "^1.1.10", "@radix-ui/react-tooltip": "^1.2.7", "@react-email/components": "^0.0.21", "@stepperize/react": "4.0.1", diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId].tsx index 44be4cae77..612efc5dc1 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId].tsx @@ -60,6 +60,12 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; +import { + CardsLayout, + getDefaultLayout, + LayoutSwitcher, + type Layout, +} from "@/components/ui/cards-layout"; import { Checkbox } from "@/components/ui/checkbox"; import { Command, @@ -285,6 +291,7 @@ const EnvironmentPage = ( ) => { const utils = api.useUtils(); const [isBulkActionLoading, setIsBulkActionLoading] = useState(false); + const [layout, setLayout] = useState(() => getDefaultLayout()); const { projectId, environmentId } = props; const { data: auth } = api.user.get.useQuery(); const { data: permissions } = api.user.getPermissions.useQuery(); @@ -1445,6 +1452,7 @@ const EnvironmentPage = ( )} +
@@ -1468,7 +1476,7 @@ const EnvironmentPage = ( ) : (
-
+ {filteredServices?.map((service) => ( ))} -
+
)} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cb61d48e33..96bb103141 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -213,7 +213,7 @@ importers: specifier: ^1.1.12 version: 1.1.13(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@radix-ui/react-toggle': - specifier: ^1.1.9 + specifier: ^1.1.10 version: 1.1.10(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@radix-ui/react-tooltip': specifier: ^1.2.7 From 2aafa6b688d656587862934ae7b41843972defa8 Mon Sep 17 00:00:00 2001 From: Oro Date: Sat, 28 Mar 2026 12:10:18 +0100 Subject: [PATCH 02/11] feat: color switcher --- .../components/ui/color-theme-picker.tsx | 72 +++ .../components/ui/color-theme-provider.tsx | 71 +++ apps/dokploy/pages/_app.tsx | 13 +- .../pages/dashboard/settings/profile.tsx | 27 + apps/dokploy/styles/globals.css | 542 +++++++++++------- 5 files changed, 524 insertions(+), 201 deletions(-) create mode 100644 apps/dokploy/components/ui/color-theme-picker.tsx create mode 100644 apps/dokploy/components/ui/color-theme-provider.tsx diff --git a/apps/dokploy/components/ui/color-theme-picker.tsx b/apps/dokploy/components/ui/color-theme-picker.tsx new file mode 100644 index 0000000000..b0ce069bc4 --- /dev/null +++ b/apps/dokploy/components/ui/color-theme-picker.tsx @@ -0,0 +1,72 @@ +import { Check } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { + COLOR_THEMES, + type ColorTheme, + useColorTheme, +} from "./color-theme-provider"; + +const THEME_META: Record = { + zinc: { label: "Zinc", hex: "#71717a" }, + blue: { label: "Blue", hex: "#3b82f6" }, + violet: { label: "Violet", hex: "#8b5cf6" }, + pink: { label: "Pink", hex: "#ec4899" }, + rose: { label: "Rose", hex: "#f43f5e" }, + red: { label: "Red", hex: "#ef4444" }, + orange: { label: "Orange", hex: "#f97316" }, + amber: { label: "Amber", hex: "#f59e0b" }, + green: { label: "Green", hex: "#22c55e" }, + teal: { label: "Teal", hex: "#14b8a6" }, +}; + +export function ColorThemePicker() { + const { colorTheme, setColorTheme } = useColorTheme(); + + return ( +
+ {COLOR_THEMES.map((theme) => { + const isActive = colorTheme === theme; + const { label, hex } = THEME_META[theme]; + return ( + + ); + })} +
+ ); +} diff --git a/apps/dokploy/components/ui/color-theme-provider.tsx b/apps/dokploy/components/ui/color-theme-provider.tsx new file mode 100644 index 0000000000..9cccdc4805 --- /dev/null +++ b/apps/dokploy/components/ui/color-theme-provider.tsx @@ -0,0 +1,71 @@ +import { createContext, useContext, useLayoutEffect, useState } from "react"; + +export const COLOR_THEMES = [ + "zinc", + "blue", + "violet", + "pink", + "rose", + "red", + "orange", + "amber", + "green", + "teal", +] as const; + +export type ColorTheme = (typeof COLOR_THEMES)[number]; + +const STORAGE_KEY = "dokploy-color-theme"; +const DEFAULT_THEME: ColorTheme = "zinc"; + +interface ColorThemeContextValue { + colorTheme: ColorTheme; + setColorTheme: (theme: ColorTheme) => void; +} + +const ColorThemeContext = createContext({ + colorTheme: DEFAULT_THEME, + setColorTheme: () => {}, +}); + +export function ColorThemeProvider({ + children, +}: { + children: React.ReactNode; +}) { + const [colorTheme, setColorThemeState] = useState(DEFAULT_THEME); + + useLayoutEffect(() => { + const stored = localStorage.getItem(STORAGE_KEY) as ColorTheme | null; + const theme = + stored && (COLOR_THEMES as readonly string[]).includes(stored) + ? stored + : DEFAULT_THEME; + setColorThemeState(theme); + applyTheme(theme); + }, []); + + function setColorTheme(theme: ColorTheme) { + setColorThemeState(theme); + localStorage.setItem(STORAGE_KEY, theme); + applyTheme(theme); + } + + return ( + + {children} + + ); +} + +function applyTheme(theme: ColorTheme) { + if (theme === DEFAULT_THEME) { + document.documentElement.removeAttribute("data-color-theme"); + } else { + document.documentElement.setAttribute("data-color-theme", theme); + } +} + +export function useColorTheme() { + return useContext(ColorThemeContext); +} diff --git a/apps/dokploy/pages/_app.tsx b/apps/dokploy/pages/_app.tsx index 86af79bd0e..3b0fb6b191 100644 --- a/apps/dokploy/pages/_app.tsx +++ b/apps/dokploy/pages/_app.tsx @@ -9,6 +9,7 @@ import NextTopLoader from "nextjs-toploader"; import type { ReactElement, ReactNode } from "react"; import { SearchCommand } from "@/components/dashboard/search-command"; import { WhitelabelingProvider } from "@/components/proprietary/whitelabeling/whitelabeling-provider"; +import { ColorThemeProvider } from "@/components/ui/color-theme-provider"; import { Toaster } from "@/components/ui/sonner"; import { api } from "@/utils/api"; @@ -48,11 +49,13 @@ const MyApp = ({ disableTransitionOnChange forcedTheme={Component.theme} > - - - - - {getLayout()} + + + + + + {getLayout()} + ); diff --git a/apps/dokploy/pages/dashboard/settings/profile.tsx b/apps/dokploy/pages/dashboard/settings/profile.tsx index b02d59c0e3..ab0697415d 100644 --- a/apps/dokploy/pages/dashboard/settings/profile.tsx +++ b/apps/dokploy/pages/dashboard/settings/profile.tsx @@ -7,6 +7,15 @@ import { ShowApiKeys } from "@/components/dashboard/settings/api/show-api-keys"; import { LinkingAccount } from "@/components/dashboard/settings/linking-account/linking-account"; import { ProfileForm } from "@/components/dashboard/settings/profile/profile-form"; import { DashboardLayout } from "@/components/layouts/dashboard-layout"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { ColorThemePicker } from "@/components/ui/color-theme-picker"; +import { ModeToggle } from "@/components/ui/modeToggle"; import { appRouter } from "@/server/api/root"; import { api } from "@/utils/api"; @@ -18,6 +27,24 @@ const Page = () => {
+ + + Appearance + + Customize the look and feel of the interface. + + + +
+ Color theme + +
+
+ Mode + +
+
+
{isCloud && } {permissions?.api.read && }
diff --git a/apps/dokploy/styles/globals.css b/apps/dokploy/styles/globals.css index 8bd768e1ab..be610de6e5 100644 --- a/apps/dokploy/styles/globals.css +++ b/apps/dokploy/styles/globals.css @@ -3,262 +3,412 @@ @tailwind utilities; @layer base { - :root { - --terminal-paste: rgba(0, 0, 0, 0.2); - --background: 0 0% 100%; - --foreground: 240 10% 3.9%; - - --card: 0 0% 100%; - --card-foreground: 240 10% 3.9%; - - --popover: 0 0% 100%; - --popover-foreground: 240 10% 3.9%; - - --primary: 240 5.9% 10%; - --primary-foreground: 0 0% 98%; - - --secondary: 240 4.8% 95.9%; - --secondary-foreground: 240 5.9% 10%; - - --muted: 240 4.8% 95.9%; - --muted-foreground: 240 3.8% 46.1%; - - --accent: 240 4.8% 95.9%; - --accent-foreground: 240 5.9% 10%; - - --destructive: 0 84.2% 50.2%; - --destructive-foreground: 0 0% 98%; - - --border: 240 5.9% 90%; - --input: 240 5.9% 90%; - --ring: 240 10% 3.9%; - - --radius: 0.5rem; - --overlay: rgba(0, 0, 0, 0.2); - - --chart-1: 173 58% 39%; - --chart-2: 12 76% 61%; - --chart-3: 197 37% 24%; - --chart-4: 43 74% 66%; - --chart-5: 27 87% 67%; - --sidebar-background: 0 0% 98%; - --sidebar-foreground: 240 5.3% 26.1%; - --sidebar-primary: 240 5.9% 10%; - --sidebar-primary-foreground: 0 0% 98%; - --sidebar-accent: 240 4.8% 95.9%; - --sidebar-accent-foreground: 240 5.9% 10%; - --sidebar-border: 220 13% 91%; - --sidebar-ring: 217.2 91.2% 59.8%; - } - - .dark { - --terminal-paste: rgba(255, 255, 255, 0.2); - --background: 0 0% 0%; - --foreground: 0 0% 98%; - - --card: 240 4% 10%; - --card-foreground: 0 0% 98%; - - --popover: 240 10% 3.9%; - --popover-foreground: 0 0% 98%; - - --primary: 0 0% 98%; - --primary-foreground: 240 5.9% 10%; - - --secondary: 240 3.7% 15.9%; - --secondary-foreground: 0 0% 98%; - - --muted: 240 4% 10%; - --muted-foreground: 240 5% 64.9%; - - --accent: 240 3.7% 15.9%; - --accent-foreground: 0 0% 98%; - - --destructive: 0 84.2% 50.2%; - --destructive-foreground: 0 0% 98%; - - --border: 240 3.7% 15.9%; - --input: 240 4% 10%; - --ring: 240 4.9% 83.9%; - - --overlay: rgba(0, 0, 0, 0.5); - - --chart-1: 220 70% 50%; - --chart-5: 160 60% 45%; - --chart-3: 30 80% 55%; - --chart-4: 280 65% 60%; - --chart-2: 340 75% 55%; - --sidebar-background: 240 5.9% 10%; - --sidebar-foreground: 240 4.8% 95.9%; - --sidebar-primary: 224.3 76.3% 48%; - --sidebar-primary-foreground: 0 0% 100%; - --sidebar-accent: 240 3.7% 15.9%; - --sidebar-accent-foreground: 240 4.8% 95.9%; - --sidebar-border: 240 3.7% 15.9%; - --sidebar-ring: 217.2 91.2% 59.8%; - } + :root { + --terminal-paste: rgba(0, 0, 0, 0.2); + --background: 0 0% 100%; + --foreground: 240 10% 3.9%; + + --card: 0 0% 100%; + --card-foreground: 240 10% 3.9%; + + --popover: 0 0% 100%; + --popover-foreground: 240 10% 3.9%; + + --primary: 240 5.9% 10%; + --primary-foreground: 0 0% 98%; + + --secondary: 240 4.8% 95.9%; + --secondary-foreground: 240 5.9% 10%; + + --muted: 240 4.8% 95.9%; + --muted-foreground: 240 3.8% 46.1%; + + --accent: 240 4.8% 95.9%; + --accent-foreground: 240 5.9% 10%; + + --destructive: 0 84.2% 50.2%; + --destructive-foreground: 0 0% 98%; + + --border: 240 5.9% 90%; + --input: 240 5.9% 90%; + --ring: 240 10% 3.9%; + + --radius: 0.5rem; + --overlay: rgba(0, 0, 0, 0.2); + + --chart-1: 173 58% 39%; + --chart-2: 12 76% 61%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + --sidebar-background: 0 0% 98%; + --sidebar-foreground: 240 5.3% 26.1%; + --sidebar-primary: 240 5.9% 10%; + --sidebar-primary-foreground: 0 0% 98%; + --sidebar-accent: 240 4.8% 95.9%; + --sidebar-accent-foreground: 240 5.9% 10%; + --sidebar-border: 220 13% 91%; + --sidebar-ring: 217.2 91.2% 59.8%; + } + + .dark { + --terminal-paste: rgba(255, 255, 255, 0.2); + --background: 0 0% 0%; + --foreground: 0 0% 98%; + + --card: 240 4% 10%; + --card-foreground: 0 0% 98%; + + --popover: 240 10% 3.9%; + --popover-foreground: 0 0% 98%; + + --primary: 0 0% 98%; + --primary-foreground: 240 5.9% 10%; + + --secondary: 240 3.7% 15.9%; + --secondary-foreground: 0 0% 98%; + + --muted: 240 4% 10%; + --muted-foreground: 240 5% 64.9%; + + --accent: 240 3.7% 15.9%; + --accent-foreground: 0 0% 98%; + + --destructive: 0 84.2% 50.2%; + --destructive-foreground: 0 0% 98%; + + --border: 240 3.7% 15.9%; + --input: 240 4% 10%; + --ring: 240 4.9% 83.9%; + + --overlay: rgba(0, 0, 0, 0.5); + + --chart-1: 220 70% 50%; + --chart-5: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-2: 340 75% 55%; + --sidebar-background: 240 5.9% 10%; + --sidebar-foreground: 240 4.8% 95.9%; + --sidebar-primary: 224.3 76.3% 48%; + --sidebar-primary-foreground: 0 0% 100%; + --sidebar-accent: 240 3.7% 15.9%; + --sidebar-accent-foreground: 240 4.8% 95.9%; + --sidebar-border: 240 3.7% 15.9%; + --sidebar-ring: 217.2 91.2% 59.8%; + } } @layer base { - * { - @apply border-border; - } - - body { - @apply bg-background text-foreground; - } - - /* Custom scrollbar styling */ - ::-webkit-scrollbar { - width: 0.3125rem; - } - - ::-webkit-scrollbar-track { - background: transparent; - } - - ::-webkit-scrollbar-thumb { - background: hsl(var(--border)); - border-radius: 0.3125rem; - } - - * { - scrollbar-width: thin; - scrollbar-color: hsl(var(--border)) transparent; - } + * { + @apply border-border; + } + + body { + @apply bg-background text-foreground; + } + + /* Custom scrollbar styling */ + ::-webkit-scrollbar { + width: 0.3125rem; + } + + ::-webkit-scrollbar-track { + background: transparent; + } + + ::-webkit-scrollbar-thumb { + background: hsl(var(--border)); + border-radius: 0.3125rem; + } + + * { + scrollbar-width: thin; + scrollbar-color: hsl(var(--border)) transparent; + } } .xterm-viewport { - border-radius: 0.75rem /* 12px */ !important; + border-radius: 0.75rem /* 12px */ !important; } .xterm .xterm-viewport { - overflow-y: auto !important; + overflow-y: auto !important; } .xterm .xterm-screen { - overflow: hidden; + overflow: hidden; } @layer utilities { - /* Chrome, Safari and Opera */ - .no-scrollbar::-webkit-scrollbar { - display: none; - } - - .no-scrollbar { - -ms-overflow-style: none; /* IE and Edge */ - scrollbar-width: none; /* Firefox */ - } + /* Chrome, Safari and Opera */ + .no-scrollbar::-webkit-scrollbar { + display: none; + } + + .no-scrollbar { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ + } } /* Codemirror */ .cm-editor { - @apply w-full h-full rounded-md overflow-hidden border border-solid border-border outline-none; + @apply w-full h-full rounded-md overflow-hidden border border-solid border-border outline-none; } .cm-editor .cm-scroller { - font-family: inherit; - line-height: inherit; + font-family: inherit; + line-height: inherit; } .cm-editor.cm-focused { - @apply outline-none; + @apply outline-none; } /* fix: placeholder bg */ .cm-editor .cm-activeLine:has(.cm-placeholder) { - background-color: transparent; + background-color: transparent; } .compose-file-editor .cm-editor { - @apply min-h-[25rem]; + @apply min-h-[25rem]; } -@keyframes heartbeat { - 0% { - transform: scale(1); - opacity: 0.7; - } - 25% { - transform: scale(1.1); - opacity: 1; - } - 50% { - transform: scale(1); - opacity: 0.7; - } - 75% { - transform: scale(1.1); - opacity: 1; - } - 100% { - transform: scale(1); - opacity: 0.7; - } +/* ── Color themes ───────────────────────────────────────────────────────── */ +/* Each theme overrides --primary, --primary-foreground, --ring, */ +/* --sidebar-primary and --sidebar-ring on the element. */ + +/* zinc (default — no override needed, matches the base :root values) */ + +/* pink */ +[data-color-theme="pink"] { + --primary: 330.4 81.2% 60.4%; + --primary-foreground: 355.7 100% 97.3%; + --ring: 330.4 81.2% 60.4%; + --sidebar-primary: 330.4 81.2% 60.4%; + --sidebar-ring: 330.4 81.2% 60.4%; +} +.dark[data-color-theme="pink"] { + --primary: 330.4 81.2% 65%; + --primary-foreground: 355.7 100% 97.3%; + --ring: 330.4 81.2% 65%; + --sidebar-primary: 330.4 81.2% 65%; + --sidebar-ring: 330.4 81.2% 65%; } -.animate-heartbeat { - animation: heartbeat 2.5s infinite; +/* teal */ +[data-color-theme="teal"] { + --primary: 173.4 80.4% 36%; + --primary-foreground: 0 0% 98%; + --ring: 173.4 80.4% 36%; + --sidebar-primary: 173.4 80.4% 36%; + --sidebar-ring: 173.4 80.4% 36%; +} +.dark[data-color-theme="teal"] { + --primary: 172 66% 50.4%; + --primary-foreground: 172 40% 10%; + --ring: 172 66% 50.4%; + --sidebar-primary: 172 66% 50.4%; + --sidebar-ring: 172 66% 50.4%; } -@media (prefers-color-scheme: dark) { - .swagger-ui { - background-color: white; - } - .swagger-ui .info { - margin: 0px !important; - padding-top: 1rem !important; - } +/* blue */ +[data-color-theme="blue"] { + --primary: 221.2 83.2% 53.3%; + --primary-foreground: 210 40% 98%; + --ring: 221.2 83.2% 53.3%; + --sidebar-primary: 221.2 83.2% 53.3%; + --sidebar-ring: 221.2 83.2% 53.3%; +} +.dark[data-color-theme="blue"] { + --primary: 217.2 91.2% 59.8%; + --primary-foreground: 222.2 47.4% 11.2%; + --ring: 217.2 91.2% 59.8%; + --sidebar-primary: 217.2 91.2% 59.8%; + --sidebar-ring: 217.2 91.2% 59.8%; } -/* Docker Logs Scrollbar */ -@layer utilities { - .custom-logs-scrollbar { - scrollbar-width: thin; - scrollbar-color: hsl(var(--muted-foreground)) transparent; - } +/* violet */ +[data-color-theme="violet"] { + --primary: 262.1 83.3% 57.8%; + --primary-foreground: 210 20% 98%; + --ring: 262.1 83.3% 57.8%; + --sidebar-primary: 262.1 83.3% 57.8%; + --sidebar-ring: 262.1 83.3% 57.8%; +} +.dark[data-color-theme="violet"] { + --primary: 263.4 70% 50.4%; + --primary-foreground: 210 20% 98%; + --ring: 263.4 70% 50.4%; + --sidebar-primary: 263.4 70% 50.4%; + --sidebar-ring: 263.4 70% 50.4%; +} + +/* rose */ +[data-color-theme="rose"] { + --primary: 346.8 77.2% 49.8%; + --primary-foreground: 355.7 100% 97.3%; + --ring: 346.8 77.2% 49.8%; + --sidebar-primary: 346.8 77.2% 49.8%; + --sidebar-ring: 346.8 77.2% 49.8%; +} +.dark[data-color-theme="rose"] { + --primary: 349.7 89.2% 60.2%; + --primary-foreground: 355.7 100% 97.3%; + --ring: 349.7 89.2% 60.2%; + --sidebar-primary: 349.7 89.2% 60.2%; + --sidebar-ring: 349.7 89.2% 60.2%; +} + +/* red */ +[data-color-theme="red"] { + --primary: 0 72.2% 50.6%; + --primary-foreground: 0 85.7% 97.3%; + --ring: 0 72.2% 50.6%; + --sidebar-primary: 0 72.2% 50.6%; + --sidebar-ring: 0 72.2% 50.6%; +} +.dark[data-color-theme="red"] { + --primary: 0 72.2% 50.6%; + --primary-foreground: 0 85.7% 97.3%; + --ring: 0 72.2% 50.6%; + --sidebar-primary: 0 72.2% 50.6%; + --sidebar-ring: 0 72.2% 50.6%; +} + +/* orange */ +[data-color-theme="orange"] { + --primary: 24.6 95% 53.1%; + --primary-foreground: 60 9.1% 97.8%; + --ring: 24.6 95% 53.1%; + --sidebar-primary: 24.6 95% 53.1%; + --sidebar-ring: 24.6 95% 53.1%; +} +.dark[data-color-theme="orange"] { + --primary: 20.5 90.2% 48.2%; + --primary-foreground: 60 9.1% 97.8%; + --ring: 20.5 90.2% 48.2%; + --sidebar-primary: 20.5 90.2% 48.2%; + --sidebar-ring: 20.5 90.2% 48.2%; +} + +/* amber */ +[data-color-theme="amber"] { + --primary: 37.7 92.1% 50.2%; + --primary-foreground: 26 83.3% 14.1%; + --ring: 37.7 92.1% 50.2%; + --sidebar-primary: 37.7 92.1% 50.2%; + --sidebar-ring: 37.7 92.1% 50.2%; +} +.dark[data-color-theme="amber"] { + --primary: 43.3 96.4% 56.3%; + --primary-foreground: 26 83.3% 14.1%; + --ring: 43.3 96.4% 56.3%; + --sidebar-primary: 43.3 96.4% 56.3%; + --sidebar-ring: 43.3 96.4% 56.3%; +} - .custom-logs-scrollbar::-webkit-scrollbar { - width: 8px; - height: 8px; - } +/* green */ +[data-color-theme="green"] { + --primary: 142.1 76.2% 36.3%; + --primary-foreground: 355.7 100% 97.3%; + --ring: 142.1 76.2% 36.3%; + --sidebar-primary: 142.1 76.2% 36.3%; + --sidebar-ring: 142.1 76.2% 36.3%; +} +.dark[data-color-theme="green"] { + --primary: 142.1 70.6% 45.3%; + --primary-foreground: 144.9 80.4% 10%; + --ring: 142.1 70.6% 45.3%; + --sidebar-primary: 142.1 70.6% 45.3%; + --sidebar-ring: 142.1 70.6% 45.3%; +} - .custom-logs-scrollbar::-webkit-scrollbar-track { - background: transparent; - } +@keyframes heartbeat { + 0% { + transform: scale(1); + opacity: 0.7; + } + 25% { + transform: scale(1.1); + opacity: 1; + } + 50% { + transform: scale(1); + opacity: 0.7; + } + 75% { + transform: scale(1.1); + opacity: 1; + } + 100% { + transform: scale(1); + opacity: 0.7; + } +} - .custom-logs-scrollbar::-webkit-scrollbar-thumb { - background-color: hsl(var(--muted-foreground) / 0.3); - border-radius: 20px; - } +.animate-heartbeat { + animation: heartbeat 2.5s infinite; +} +@media (prefers-color-scheme: dark) { + .swagger-ui { + background-color: white; + } + + .swagger-ui .info { + margin: 0px !important; + padding-top: 1rem !important; + } +} - .custom-logs-scrollbar::-webkit-scrollbar-thumb:hover { - background-color: hsl(var(--muted-foreground) / 0.5); - } +/* Docker Logs Scrollbar */ +@layer utilities { + .custom-logs-scrollbar { + scrollbar-width: thin; + scrollbar-color: hsl(var(--muted-foreground)) transparent; + } + + .custom-logs-scrollbar::-webkit-scrollbar { + width: 8px; + height: 8px; + } + + .custom-logs-scrollbar::-webkit-scrollbar-track { + background: transparent; + } + + .custom-logs-scrollbar::-webkit-scrollbar-thumb { + background-color: hsl(var(--muted-foreground) / 0.3); + border-radius: 20px; + } + + .custom-logs-scrollbar::-webkit-scrollbar-thumb:hover { + background-color: hsl(var(--muted-foreground) / 0.5); + } } .xterm-bg-257.xterm-fg-257 { - background-color: var(--terminal-paste) !important; - color: currentColor !important; + background-color: var(--terminal-paste) !important; + color: currentColor !important; } .cm-content, .cm-lineWrapping { - @apply font-mono; + @apply font-mono; } /* HubSpot Widget - Force light color-scheme to prevent white background */ #hubspot-messages-iframe-container, #hubspot-messages-iframe-container * { - background-color: transparent !important; - color-scheme: light !important; + background-color: transparent !important; + color-scheme: light !important; } #hubspot-messages-iframe-container .hs-shadow-container { - display: none !important; + display: none !important; } #hubspot-conversations-iframe { - color-scheme: light !important; + color-scheme: light !important; } From 20b3ccfbb022881149eeb5d967fef2e4c65d2073 Mon Sep 17 00:00:00 2001 From: Oro Date: Sat, 28 Mar 2026 12:28:29 +0100 Subject: [PATCH 03/11] feat: color enhancements --- .../components/dashboard/projects/show.tsx | 2 +- apps/dokploy/components/layouts/side.tsx | 8 +- apps/dokploy/components/ui/tabs.tsx | 2 +- .../environment/[environmentId].tsx | 115 +++++++++--------- 4 files changed, 63 insertions(+), 64 deletions(-) diff --git a/apps/dokploy/components/dashboard/projects/show.tsx b/apps/dokploy/components/dashboard/projects/show.tsx index 0374ef8ee4..1473266ebc 100644 --- a/apps/dokploy/components/dashboard/projects/show.tsx +++ b/apps/dokploy/components/dashboard/projects/show.tsx @@ -347,7 +347,7 @@ export const ShowProjects = () => { } }} > - + diff --git a/apps/dokploy/components/layouts/side.tsx b/apps/dokploy/components/layouts/side.tsx index c173be620e..e9c25c120d 100644 --- a/apps/dokploy/components/layouts/side.tsx +++ b/apps/dokploy/components/layouts/side.tsx @@ -959,18 +959,18 @@ export default function Page({ children }: Props) { {item.icon && ( )} - {item.title} + {item.title} ) : ( diff --git a/apps/dokploy/components/ui/tabs.tsx b/apps/dokploy/components/ui/tabs.tsx index ffbbaed192..ca0b1164a4 100644 --- a/apps/dokploy/components/ui/tabs.tsx +++ b/apps/dokploy/components/ui/tabs.tsx @@ -27,7 +27,7 @@ const TabsTrigger = React.forwardRef< lastDeployDate) { lastDeployDate = deployDate; @@ -235,8 +234,8 @@ export const extractServicesFromEnvironment = ( for (const deployment of deployments) { const deployDate = new Date( deployment.finishedAt || - deployment.startedAt || - deployment.createdAt, + deployment.startedAt || + deployment.createdAt, ); if (!lastDeployDate || deployDate > lastDeployDate) { lastDeployDate = deployDate; @@ -968,8 +967,8 @@ const EnvironmentPage = ( className={cn( "data-[state=checked]:bg-primary", selectedServices.length > 0 && - selectedServices.length < filteredServices.length && - "bg-primary/50", + selectedServices.length < filteredServices.length && + "bg-primary/50", )} onCheckedChange={handleSelectAll} /> @@ -1054,17 +1053,17 @@ const EnvironmentPage = (

{selectedServicesWithRunningStatus.length > 0 && ( - - Warning:{" "} - {selectedServicesWithRunningStatus.length}{" "} - of the selected services are currently - running. Please stop these services first - before deleting:{" "} - {selectedServicesWithRunningStatus - .map((s) => s.name) - .join(", ")} - - )} + + Warning:{" "} + {selectedServicesWithRunningStatus.length}{" "} + of the selected services are currently + running. Please stop these services first + before deleting:{" "} + {selectedServicesWithRunningStatus + .map((s) => s.name) + .join(", ")} + + )}
} type="destructive" @@ -1421,37 +1420,37 @@ const EnvironmentPage = ( {(availableServers.length > 0 || hasServicesWithoutServer) && ( - - )} + + )} @@ -1483,7 +1482,7 @@ const EnvironmentPage = ( href={`/dashboard/project/${projectId}/environment/${environmentId}/services/${service.type}/${service.id}`} className="block" > - + {service.serverId && (
From 267904c62ba22736ff75575fdfe7996756eb12f0 Mon Sep 17 00:00:00 2001 From: Oro Date: Sat, 28 Mar 2026 13:21:41 +0100 Subject: [PATCH 04/11] chore: revert useless dependency update (handled by dependabot?) --- apps/dokploy/package.json | 2 +- pnpm-lock.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/dokploy/package.json b/apps/dokploy/package.json index 88f0e3e7f6..09ec864c41 100644 --- a/apps/dokploy/package.json +++ b/apps/dokploy/package.json @@ -79,7 +79,7 @@ "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-switch": "^1.2.5", "@radix-ui/react-tabs": "^1.1.12", - "@radix-ui/react-toggle": "^1.1.10", + "@radix-ui/react-toggle": "^1.1.9", "@radix-ui/react-tooltip": "^1.2.7", "@react-email/components": "^0.0.21", "@stepperize/react": "4.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 96bb103141..cb61d48e33 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -213,7 +213,7 @@ importers: specifier: ^1.1.12 version: 1.1.13(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@radix-ui/react-toggle': - specifier: ^1.1.10 + specifier: ^1.1.9 version: 1.1.10(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@radix-ui/react-tooltip': specifier: ^1.2.7 From b9ab81e87146995c034173f1208c16f6a7b44717 Mon Sep 17 00:00:00 2001 From: Oro Date: Sat, 28 Mar 2026 13:23:46 +0100 Subject: [PATCH 05/11] chore: format --- apps/dokploy/components/layouts/side.tsx | 19 +- .../environment/[environmentId].tsx | 110 ++-- apps/dokploy/styles/globals.css | 550 +++++++++--------- 3 files changed, 346 insertions(+), 333 deletions(-) diff --git a/apps/dokploy/components/layouts/side.tsx b/apps/dokploy/components/layouts/side.tsx index e9c25c120d..01360e6a7a 100644 --- a/apps/dokploy/components/layouts/side.tsx +++ b/apps/dokploy/components/layouts/side.tsx @@ -959,7 +959,10 @@ export default function Page({ children }: Props) { {item.icon && ( )} - {item.title} + + {item.title} + ) : ( diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId].tsx index a80e6ed392..f8b20865d5 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId].tsx @@ -112,13 +112,13 @@ export type Services = { serverName?: string | null; name: string; type: - | "mariadb" - | "application" - | "postgres" - | "mysql" - | "mongo" - | "redis" - | "compose"; + | "mariadb" + | "application" + | "postgres" + | "mysql" + | "mongo" + | "redis" + | "compose"; description?: string | null; id: string; createdAt: string; @@ -144,8 +144,8 @@ export const extractServicesFromEnvironment = ( for (const deployment of deployments) { const deployDate = new Date( deployment.finishedAt || - deployment.startedAt || - deployment.createdAt, + deployment.startedAt || + deployment.createdAt, ); if (!lastDeployDate || deployDate > lastDeployDate) { lastDeployDate = deployDate; @@ -234,8 +234,8 @@ export const extractServicesFromEnvironment = ( for (const deployment of deployments) { const deployDate = new Date( deployment.finishedAt || - deployment.startedAt || - deployment.createdAt, + deployment.startedAt || + deployment.createdAt, ); if (!lastDeployDate || deployDate > lastDeployDate) { lastDeployDate = deployDate; @@ -967,8 +967,8 @@ const EnvironmentPage = ( className={cn( "data-[state=checked]:bg-primary", selectedServices.length > 0 && - selectedServices.length < filteredServices.length && - "bg-primary/50", + selectedServices.length < filteredServices.length && + "bg-primary/50", )} onCheckedChange={handleSelectAll} /> @@ -1053,17 +1053,17 @@ const EnvironmentPage = (

{selectedServicesWithRunningStatus.length > 0 && ( - - Warning:{" "} - {selectedServicesWithRunningStatus.length}{" "} - of the selected services are currently - running. Please stop these services first - before deleting:{" "} - {selectedServicesWithRunningStatus - .map((s) => s.name) - .join(", ")} - - )} + + Warning:{" "} + {selectedServicesWithRunningStatus.length}{" "} + of the selected services are currently + running. Please stop these services first + before deleting:{" "} + {selectedServicesWithRunningStatus + .map((s) => s.name) + .join(", ")} + + )}
} type="destructive" @@ -1420,37 +1420,37 @@ const EnvironmentPage = ( {(availableServers.length > 0 || hasServicesWithoutServer) && ( - - )} + + )} diff --git a/apps/dokploy/styles/globals.css b/apps/dokploy/styles/globals.css index be610de6e5..c0bee63ed4 100644 --- a/apps/dokploy/styles/globals.css +++ b/apps/dokploy/styles/globals.css @@ -3,176 +3,176 @@ @tailwind utilities; @layer base { - :root { - --terminal-paste: rgba(0, 0, 0, 0.2); - --background: 0 0% 100%; - --foreground: 240 10% 3.9%; - - --card: 0 0% 100%; - --card-foreground: 240 10% 3.9%; - - --popover: 0 0% 100%; - --popover-foreground: 240 10% 3.9%; - - --primary: 240 5.9% 10%; - --primary-foreground: 0 0% 98%; - - --secondary: 240 4.8% 95.9%; - --secondary-foreground: 240 5.9% 10%; - - --muted: 240 4.8% 95.9%; - --muted-foreground: 240 3.8% 46.1%; - - --accent: 240 4.8% 95.9%; - --accent-foreground: 240 5.9% 10%; - - --destructive: 0 84.2% 50.2%; - --destructive-foreground: 0 0% 98%; - - --border: 240 5.9% 90%; - --input: 240 5.9% 90%; - --ring: 240 10% 3.9%; - - --radius: 0.5rem; - --overlay: rgba(0, 0, 0, 0.2); - - --chart-1: 173 58% 39%; - --chart-2: 12 76% 61%; - --chart-3: 197 37% 24%; - --chart-4: 43 74% 66%; - --chart-5: 27 87% 67%; - --sidebar-background: 0 0% 98%; - --sidebar-foreground: 240 5.3% 26.1%; - --sidebar-primary: 240 5.9% 10%; - --sidebar-primary-foreground: 0 0% 98%; - --sidebar-accent: 240 4.8% 95.9%; - --sidebar-accent-foreground: 240 5.9% 10%; - --sidebar-border: 220 13% 91%; - --sidebar-ring: 217.2 91.2% 59.8%; - } - - .dark { - --terminal-paste: rgba(255, 255, 255, 0.2); - --background: 0 0% 0%; - --foreground: 0 0% 98%; - - --card: 240 4% 10%; - --card-foreground: 0 0% 98%; - - --popover: 240 10% 3.9%; - --popover-foreground: 0 0% 98%; - - --primary: 0 0% 98%; - --primary-foreground: 240 5.9% 10%; - - --secondary: 240 3.7% 15.9%; - --secondary-foreground: 0 0% 98%; - - --muted: 240 4% 10%; - --muted-foreground: 240 5% 64.9%; - - --accent: 240 3.7% 15.9%; - --accent-foreground: 0 0% 98%; - - --destructive: 0 84.2% 50.2%; - --destructive-foreground: 0 0% 98%; - - --border: 240 3.7% 15.9%; - --input: 240 4% 10%; - --ring: 240 4.9% 83.9%; - - --overlay: rgba(0, 0, 0, 0.5); - - --chart-1: 220 70% 50%; - --chart-5: 160 60% 45%; - --chart-3: 30 80% 55%; - --chart-4: 280 65% 60%; - --chart-2: 340 75% 55%; - --sidebar-background: 240 5.9% 10%; - --sidebar-foreground: 240 4.8% 95.9%; - --sidebar-primary: 224.3 76.3% 48%; - --sidebar-primary-foreground: 0 0% 100%; - --sidebar-accent: 240 3.7% 15.9%; - --sidebar-accent-foreground: 240 4.8% 95.9%; - --sidebar-border: 240 3.7% 15.9%; - --sidebar-ring: 217.2 91.2% 59.8%; - } + :root { + --terminal-paste: rgba(0, 0, 0, 0.2); + --background: 0 0% 100%; + --foreground: 240 10% 3.9%; + + --card: 0 0% 100%; + --card-foreground: 240 10% 3.9%; + + --popover: 0 0% 100%; + --popover-foreground: 240 10% 3.9%; + + --primary: 240 5.9% 10%; + --primary-foreground: 0 0% 98%; + + --secondary: 240 4.8% 95.9%; + --secondary-foreground: 240 5.9% 10%; + + --muted: 240 4.8% 95.9%; + --muted-foreground: 240 3.8% 46.1%; + + --accent: 240 4.8% 95.9%; + --accent-foreground: 240 5.9% 10%; + + --destructive: 0 84.2% 50.2%; + --destructive-foreground: 0 0% 98%; + + --border: 240 5.9% 90%; + --input: 240 5.9% 90%; + --ring: 240 10% 3.9%; + + --radius: 0.5rem; + --overlay: rgba(0, 0, 0, 0.2); + + --chart-1: 173 58% 39%; + --chart-2: 12 76% 61%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + --sidebar-background: 0 0% 98%; + --sidebar-foreground: 240 5.3% 26.1%; + --sidebar-primary: 240 5.9% 10%; + --sidebar-primary-foreground: 0 0% 98%; + --sidebar-accent: 240 4.8% 95.9%; + --sidebar-accent-foreground: 240 5.9% 10%; + --sidebar-border: 220 13% 91%; + --sidebar-ring: 217.2 91.2% 59.8%; + } + + .dark { + --terminal-paste: rgba(255, 255, 255, 0.2); + --background: 0 0% 0%; + --foreground: 0 0% 98%; + + --card: 240 4% 10%; + --card-foreground: 0 0% 98%; + + --popover: 240 10% 3.9%; + --popover-foreground: 0 0% 98%; + + --primary: 0 0% 98%; + --primary-foreground: 240 5.9% 10%; + + --secondary: 240 3.7% 15.9%; + --secondary-foreground: 0 0% 98%; + + --muted: 240 4% 10%; + --muted-foreground: 240 5% 64.9%; + + --accent: 240 3.7% 15.9%; + --accent-foreground: 0 0% 98%; + + --destructive: 0 84.2% 50.2%; + --destructive-foreground: 0 0% 98%; + + --border: 240 3.7% 15.9%; + --input: 240 4% 10%; + --ring: 240 4.9% 83.9%; + + --overlay: rgba(0, 0, 0, 0.5); + + --chart-1: 220 70% 50%; + --chart-5: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-2: 340 75% 55%; + --sidebar-background: 240 5.9% 10%; + --sidebar-foreground: 240 4.8% 95.9%; + --sidebar-primary: 224.3 76.3% 48%; + --sidebar-primary-foreground: 0 0% 100%; + --sidebar-accent: 240 3.7% 15.9%; + --sidebar-accent-foreground: 240 4.8% 95.9%; + --sidebar-border: 240 3.7% 15.9%; + --sidebar-ring: 217.2 91.2% 59.8%; + } } @layer base { - * { - @apply border-border; - } + * { + @apply border-border; + } - body { - @apply bg-background text-foreground; - } + body { + @apply bg-background text-foreground; + } - /* Custom scrollbar styling */ - ::-webkit-scrollbar { - width: 0.3125rem; - } + /* Custom scrollbar styling */ + ::-webkit-scrollbar { + width: 0.3125rem; + } - ::-webkit-scrollbar-track { - background: transparent; - } + ::-webkit-scrollbar-track { + background: transparent; + } - ::-webkit-scrollbar-thumb { - background: hsl(var(--border)); - border-radius: 0.3125rem; - } + ::-webkit-scrollbar-thumb { + background: hsl(var(--border)); + border-radius: 0.3125rem; + } - * { - scrollbar-width: thin; - scrollbar-color: hsl(var(--border)) transparent; - } + * { + scrollbar-width: thin; + scrollbar-color: hsl(var(--border)) transparent; + } } .xterm-viewport { - border-radius: 0.75rem /* 12px */ !important; + border-radius: 0.75rem /* 12px */ !important; } .xterm .xterm-viewport { - overflow-y: auto !important; + overflow-y: auto !important; } .xterm .xterm-screen { - overflow: hidden; + overflow: hidden; } @layer utilities { - /* Chrome, Safari and Opera */ - .no-scrollbar::-webkit-scrollbar { - display: none; - } + /* Chrome, Safari and Opera */ + .no-scrollbar::-webkit-scrollbar { + display: none; + } - .no-scrollbar { - -ms-overflow-style: none; /* IE and Edge */ - scrollbar-width: none; /* Firefox */ - } + .no-scrollbar { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ + } } /* Codemirror */ .cm-editor { - @apply w-full h-full rounded-md overflow-hidden border border-solid border-border outline-none; + @apply w-full h-full rounded-md overflow-hidden border border-solid border-border outline-none; } .cm-editor .cm-scroller { - font-family: inherit; - line-height: inherit; + font-family: inherit; + line-height: inherit; } .cm-editor.cm-focused { - @apply outline-none; + @apply outline-none; } /* fix: placeholder bg */ .cm-editor .cm-activeLine:has(.cm-placeholder) { - background-color: transparent; + background-color: transparent; } .compose-file-editor .cm-editor { - @apply min-h-[25rem]; + @apply min-h-[25rem]; } /* ── Color themes ───────────────────────────────────────────────────────── */ @@ -183,232 +183,232 @@ /* pink */ [data-color-theme="pink"] { - --primary: 330.4 81.2% 60.4%; - --primary-foreground: 355.7 100% 97.3%; - --ring: 330.4 81.2% 60.4%; - --sidebar-primary: 330.4 81.2% 60.4%; - --sidebar-ring: 330.4 81.2% 60.4%; + --primary: 330.4 81.2% 60.4%; + --primary-foreground: 355.7 100% 97.3%; + --ring: 330.4 81.2% 60.4%; + --sidebar-primary: 330.4 81.2% 60.4%; + --sidebar-ring: 330.4 81.2% 60.4%; } .dark[data-color-theme="pink"] { - --primary: 330.4 81.2% 65%; - --primary-foreground: 355.7 100% 97.3%; - --ring: 330.4 81.2% 65%; - --sidebar-primary: 330.4 81.2% 65%; - --sidebar-ring: 330.4 81.2% 65%; + --primary: 330.4 81.2% 65%; + --primary-foreground: 355.7 100% 97.3%; + --ring: 330.4 81.2% 65%; + --sidebar-primary: 330.4 81.2% 65%; + --sidebar-ring: 330.4 81.2% 65%; } /* teal */ [data-color-theme="teal"] { - --primary: 173.4 80.4% 36%; - --primary-foreground: 0 0% 98%; - --ring: 173.4 80.4% 36%; - --sidebar-primary: 173.4 80.4% 36%; - --sidebar-ring: 173.4 80.4% 36%; + --primary: 173.4 80.4% 36%; + --primary-foreground: 0 0% 98%; + --ring: 173.4 80.4% 36%; + --sidebar-primary: 173.4 80.4% 36%; + --sidebar-ring: 173.4 80.4% 36%; } .dark[data-color-theme="teal"] { - --primary: 172 66% 50.4%; - --primary-foreground: 172 40% 10%; - --ring: 172 66% 50.4%; - --sidebar-primary: 172 66% 50.4%; - --sidebar-ring: 172 66% 50.4%; + --primary: 172 66% 50.4%; + --primary-foreground: 172 40% 10%; + --ring: 172 66% 50.4%; + --sidebar-primary: 172 66% 50.4%; + --sidebar-ring: 172 66% 50.4%; } /* blue */ [data-color-theme="blue"] { - --primary: 221.2 83.2% 53.3%; - --primary-foreground: 210 40% 98%; - --ring: 221.2 83.2% 53.3%; - --sidebar-primary: 221.2 83.2% 53.3%; - --sidebar-ring: 221.2 83.2% 53.3%; + --primary: 221.2 83.2% 53.3%; + --primary-foreground: 210 40% 98%; + --ring: 221.2 83.2% 53.3%; + --sidebar-primary: 221.2 83.2% 53.3%; + --sidebar-ring: 221.2 83.2% 53.3%; } .dark[data-color-theme="blue"] { - --primary: 217.2 91.2% 59.8%; - --primary-foreground: 222.2 47.4% 11.2%; - --ring: 217.2 91.2% 59.8%; - --sidebar-primary: 217.2 91.2% 59.8%; - --sidebar-ring: 217.2 91.2% 59.8%; + --primary: 217.2 91.2% 59.8%; + --primary-foreground: 222.2 47.4% 11.2%; + --ring: 217.2 91.2% 59.8%; + --sidebar-primary: 217.2 91.2% 59.8%; + --sidebar-ring: 217.2 91.2% 59.8%; } /* violet */ [data-color-theme="violet"] { - --primary: 262.1 83.3% 57.8%; - --primary-foreground: 210 20% 98%; - --ring: 262.1 83.3% 57.8%; - --sidebar-primary: 262.1 83.3% 57.8%; - --sidebar-ring: 262.1 83.3% 57.8%; + --primary: 262.1 83.3% 57.8%; + --primary-foreground: 210 20% 98%; + --ring: 262.1 83.3% 57.8%; + --sidebar-primary: 262.1 83.3% 57.8%; + --sidebar-ring: 262.1 83.3% 57.8%; } .dark[data-color-theme="violet"] { - --primary: 263.4 70% 50.4%; - --primary-foreground: 210 20% 98%; - --ring: 263.4 70% 50.4%; - --sidebar-primary: 263.4 70% 50.4%; - --sidebar-ring: 263.4 70% 50.4%; + --primary: 263.4 70% 50.4%; + --primary-foreground: 210 20% 98%; + --ring: 263.4 70% 50.4%; + --sidebar-primary: 263.4 70% 50.4%; + --sidebar-ring: 263.4 70% 50.4%; } /* rose */ [data-color-theme="rose"] { - --primary: 346.8 77.2% 49.8%; - --primary-foreground: 355.7 100% 97.3%; - --ring: 346.8 77.2% 49.8%; - --sidebar-primary: 346.8 77.2% 49.8%; - --sidebar-ring: 346.8 77.2% 49.8%; + --primary: 346.8 77.2% 49.8%; + --primary-foreground: 355.7 100% 97.3%; + --ring: 346.8 77.2% 49.8%; + --sidebar-primary: 346.8 77.2% 49.8%; + --sidebar-ring: 346.8 77.2% 49.8%; } .dark[data-color-theme="rose"] { - --primary: 349.7 89.2% 60.2%; - --primary-foreground: 355.7 100% 97.3%; - --ring: 349.7 89.2% 60.2%; - --sidebar-primary: 349.7 89.2% 60.2%; - --sidebar-ring: 349.7 89.2% 60.2%; + --primary: 349.7 89.2% 60.2%; + --primary-foreground: 355.7 100% 97.3%; + --ring: 349.7 89.2% 60.2%; + --sidebar-primary: 349.7 89.2% 60.2%; + --sidebar-ring: 349.7 89.2% 60.2%; } /* red */ [data-color-theme="red"] { - --primary: 0 72.2% 50.6%; - --primary-foreground: 0 85.7% 97.3%; - --ring: 0 72.2% 50.6%; - --sidebar-primary: 0 72.2% 50.6%; - --sidebar-ring: 0 72.2% 50.6%; + --primary: 0 72.2% 50.6%; + --primary-foreground: 0 85.7% 97.3%; + --ring: 0 72.2% 50.6%; + --sidebar-primary: 0 72.2% 50.6%; + --sidebar-ring: 0 72.2% 50.6%; } .dark[data-color-theme="red"] { - --primary: 0 72.2% 50.6%; - --primary-foreground: 0 85.7% 97.3%; - --ring: 0 72.2% 50.6%; - --sidebar-primary: 0 72.2% 50.6%; - --sidebar-ring: 0 72.2% 50.6%; + --primary: 0 72.2% 50.6%; + --primary-foreground: 0 85.7% 97.3%; + --ring: 0 72.2% 50.6%; + --sidebar-primary: 0 72.2% 50.6%; + --sidebar-ring: 0 72.2% 50.6%; } /* orange */ [data-color-theme="orange"] { - --primary: 24.6 95% 53.1%; - --primary-foreground: 60 9.1% 97.8%; - --ring: 24.6 95% 53.1%; - --sidebar-primary: 24.6 95% 53.1%; - --sidebar-ring: 24.6 95% 53.1%; + --primary: 24.6 95% 53.1%; + --primary-foreground: 60 9.1% 97.8%; + --ring: 24.6 95% 53.1%; + --sidebar-primary: 24.6 95% 53.1%; + --sidebar-ring: 24.6 95% 53.1%; } .dark[data-color-theme="orange"] { - --primary: 20.5 90.2% 48.2%; - --primary-foreground: 60 9.1% 97.8%; - --ring: 20.5 90.2% 48.2%; - --sidebar-primary: 20.5 90.2% 48.2%; - --sidebar-ring: 20.5 90.2% 48.2%; + --primary: 20.5 90.2% 48.2%; + --primary-foreground: 60 9.1% 97.8%; + --ring: 20.5 90.2% 48.2%; + --sidebar-primary: 20.5 90.2% 48.2%; + --sidebar-ring: 20.5 90.2% 48.2%; } /* amber */ [data-color-theme="amber"] { - --primary: 37.7 92.1% 50.2%; - --primary-foreground: 26 83.3% 14.1%; - --ring: 37.7 92.1% 50.2%; - --sidebar-primary: 37.7 92.1% 50.2%; - --sidebar-ring: 37.7 92.1% 50.2%; + --primary: 37.7 92.1% 50.2%; + --primary-foreground: 26 83.3% 14.1%; + --ring: 37.7 92.1% 50.2%; + --sidebar-primary: 37.7 92.1% 50.2%; + --sidebar-ring: 37.7 92.1% 50.2%; } .dark[data-color-theme="amber"] { - --primary: 43.3 96.4% 56.3%; - --primary-foreground: 26 83.3% 14.1%; - --ring: 43.3 96.4% 56.3%; - --sidebar-primary: 43.3 96.4% 56.3%; - --sidebar-ring: 43.3 96.4% 56.3%; + --primary: 43.3 96.4% 56.3%; + --primary-foreground: 26 83.3% 14.1%; + --ring: 43.3 96.4% 56.3%; + --sidebar-primary: 43.3 96.4% 56.3%; + --sidebar-ring: 43.3 96.4% 56.3%; } /* green */ [data-color-theme="green"] { - --primary: 142.1 76.2% 36.3%; - --primary-foreground: 355.7 100% 97.3%; - --ring: 142.1 76.2% 36.3%; - --sidebar-primary: 142.1 76.2% 36.3%; - --sidebar-ring: 142.1 76.2% 36.3%; + --primary: 142.1 76.2% 36.3%; + --primary-foreground: 355.7 100% 97.3%; + --ring: 142.1 76.2% 36.3%; + --sidebar-primary: 142.1 76.2% 36.3%; + --sidebar-ring: 142.1 76.2% 36.3%; } .dark[data-color-theme="green"] { - --primary: 142.1 70.6% 45.3%; - --primary-foreground: 144.9 80.4% 10%; - --ring: 142.1 70.6% 45.3%; - --sidebar-primary: 142.1 70.6% 45.3%; - --sidebar-ring: 142.1 70.6% 45.3%; + --primary: 142.1 70.6% 45.3%; + --primary-foreground: 144.9 80.4% 10%; + --ring: 142.1 70.6% 45.3%; + --sidebar-primary: 142.1 70.6% 45.3%; + --sidebar-ring: 142.1 70.6% 45.3%; } @keyframes heartbeat { - 0% { - transform: scale(1); - opacity: 0.7; - } - 25% { - transform: scale(1.1); - opacity: 1; - } - 50% { - transform: scale(1); - opacity: 0.7; - } - 75% { - transform: scale(1.1); - opacity: 1; - } - 100% { - transform: scale(1); - opacity: 0.7; - } + 0% { + transform: scale(1); + opacity: 0.7; + } + 25% { + transform: scale(1.1); + opacity: 1; + } + 50% { + transform: scale(1); + opacity: 0.7; + } + 75% { + transform: scale(1.1); + opacity: 1; + } + 100% { + transform: scale(1); + opacity: 0.7; + } } .animate-heartbeat { - animation: heartbeat 2.5s infinite; + animation: heartbeat 2.5s infinite; } @media (prefers-color-scheme: dark) { - .swagger-ui { - background-color: white; - } + .swagger-ui { + background-color: white; + } - .swagger-ui .info { - margin: 0px !important; - padding-top: 1rem !important; - } + .swagger-ui .info { + margin: 0px !important; + padding-top: 1rem !important; + } } /* Docker Logs Scrollbar */ @layer utilities { - .custom-logs-scrollbar { - scrollbar-width: thin; - scrollbar-color: hsl(var(--muted-foreground)) transparent; - } + .custom-logs-scrollbar { + scrollbar-width: thin; + scrollbar-color: hsl(var(--muted-foreground)) transparent; + } - .custom-logs-scrollbar::-webkit-scrollbar { - width: 8px; - height: 8px; - } + .custom-logs-scrollbar::-webkit-scrollbar { + width: 8px; + height: 8px; + } - .custom-logs-scrollbar::-webkit-scrollbar-track { - background: transparent; - } + .custom-logs-scrollbar::-webkit-scrollbar-track { + background: transparent; + } - .custom-logs-scrollbar::-webkit-scrollbar-thumb { - background-color: hsl(var(--muted-foreground) / 0.3); - border-radius: 20px; - } + .custom-logs-scrollbar::-webkit-scrollbar-thumb { + background-color: hsl(var(--muted-foreground) / 0.3); + border-radius: 20px; + } - .custom-logs-scrollbar::-webkit-scrollbar-thumb:hover { - background-color: hsl(var(--muted-foreground) / 0.5); - } + .custom-logs-scrollbar::-webkit-scrollbar-thumb:hover { + background-color: hsl(var(--muted-foreground) / 0.5); + } } .xterm-bg-257.xterm-fg-257 { - background-color: var(--terminal-paste) !important; - color: currentColor !important; + background-color: var(--terminal-paste) !important; + color: currentColor !important; } .cm-content, .cm-lineWrapping { - @apply font-mono; + @apply font-mono; } /* HubSpot Widget - Force light color-scheme to prevent white background */ #hubspot-messages-iframe-container, #hubspot-messages-iframe-container * { - background-color: transparent !important; - color-scheme: light !important; + background-color: transparent !important; + color-scheme: light !important; } #hubspot-messages-iframe-container .hs-shadow-container { - display: none !important; + display: none !important; } #hubspot-conversations-iframe { - color-scheme: light !important; + color-scheme: light !important; } From a8babc1a185ae64b051b33e1808b56db333be3e6 Mon Sep 17 00:00:00 2001 From: Oro Date: Sat, 28 Mar 2026 13:24:47 +0100 Subject: [PATCH 06/11] chore: re-add libsql --- .vscode/settings.json | 3 +++ .../project/[projectId]/environment/[environmentId].tsx | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 99357f2369..698574862a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,5 +4,8 @@ "editor.codeActionsOnSave": { "source.fixAll.biome": "explicit", "source.organizeImports.biome": "explicit" + }, + "[typescriptreact]": { + "editor.defaultFormatter": "biomejs.biome" } } diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId].tsx index f8b20865d5..e003c69e85 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId].tsx @@ -118,7 +118,8 @@ export type Services = { | "mysql" | "mongo" | "redis" - | "compose"; + | "compose" + | "libsql"; description?: string | null; id: string; createdAt: string; From 52b9f94124ad4bc7f6fd77ee73db093c943331a3 Mon Sep 17 00:00:00 2001 From: Oro Date: Sat, 28 Mar 2026 13:25:43 +0100 Subject: [PATCH 07/11] chore: rollback vscode settings (not in scope) --- .vscode/settings.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 698574862a..99357f2369 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,8 +4,5 @@ "editor.codeActionsOnSave": { "source.fixAll.biome": "explicit", "source.organizeImports.biome": "explicit" - }, - "[typescriptreact]": { - "editor.defaultFormatter": "biomejs.biome" } } From 35af1a7515ff272b8033cfbec1c9ae820d92484b Mon Sep 17 00:00:00 2001 From: Oro <52313840+orochibraru@users.noreply.github.com> Date: Sat, 28 Mar 2026 14:41:07 +0100 Subject: [PATCH 08/11] fix: include `className` in `cn` Fixes class extension for the card component Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- apps/dokploy/components/ui/cards-layout.tsx | 30 +++++++-------------- 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/apps/dokploy/components/ui/cards-layout.tsx b/apps/dokploy/components/ui/cards-layout.tsx index 22ac66277e..626434708a 100644 --- a/apps/dokploy/components/ui/cards-layout.tsx +++ b/apps/dokploy/components/ui/cards-layout.tsx @@ -89,26 +89,16 @@ export function getDefaultLayout(): Layout { * @returns JSX.Element */ const CardsLayout = React.forwardRef( - ({ layout, className, children, ...props }, ref) => { - useEffect(() => { - localStorage.setItem("servicesLayout", layout); - }, [layout]); - return ( -
-
- {children} -
-
- ); - }, +
); CardsLayout.displayName = "CardsLayout"; From 472c57db49ed4ce03d8e62c9981702f856b9f19f Mon Sep 17 00:00:00 2001 From: Oro Date: Sat, 28 Mar 2026 14:43:55 +0100 Subject: [PATCH 09/11] fix: AI broken component (thanks greptile) --- apps/dokploy/components/ui/cards-layout.tsx | 31 ++++++++++++++------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/apps/dokploy/components/ui/cards-layout.tsx b/apps/dokploy/components/ui/cards-layout.tsx index 626434708a..9ad8568dcc 100644 --- a/apps/dokploy/components/ui/cards-layout.tsx +++ b/apps/dokploy/components/ui/cards-layout.tsx @@ -89,16 +89,27 @@ export function getDefaultLayout(): Layout { * @returns JSX.Element */ const CardsLayout = React.forwardRef( -
+ ({ layout, className, children, ...props }, ref) => { + useEffect(() => { + localStorage.setItem("servicesLayout", layout); + }, [layout]); + return ( +
+
+ {children} +
+
+ ); + }, ); CardsLayout.displayName = "CardsLayout"; From dca7d51779d3a0af09447b9f7102054ab3b71fb9 Mon Sep 17 00:00:00 2001 From: Oro Date: Sat, 28 Mar 2026 14:44:31 +0100 Subject: [PATCH 10/11] fix: add additional breakpoints --- apps/dokploy/components/ui/cards-layout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dokploy/components/ui/cards-layout.tsx b/apps/dokploy/components/ui/cards-layout.tsx index 9ad8568dcc..4947a4a556 100644 --- a/apps/dokploy/components/ui/cards-layout.tsx +++ b/apps/dokploy/components/ui/cards-layout.tsx @@ -98,7 +98,7 @@ const CardsLayout = React.forwardRef(
Date: Sat, 28 Mar 2026 14:46:02 +0100 Subject: [PATCH 11/11] fix: added pressed element --- apps/dokploy/components/ui/cards-layout.tsx | 29 ++++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/apps/dokploy/components/ui/cards-layout.tsx b/apps/dokploy/components/ui/cards-layout.tsx index 4947a4a556..84182fc3c7 100644 --- a/apps/dokploy/components/ui/cards-layout.tsx +++ b/apps/dokploy/components/ui/cards-layout.tsx @@ -17,6 +17,20 @@ export type CardsLayoutProps = React.HTMLAttributes & { layout: Layout; }; +/** + * Retrieves the default layout for the cards, either from localStorage or defaults to "grid". + * @returns The default layout, either "grid" or "list". + */ +export function getDefaultLayout(): Layout { + if (typeof window !== "undefined") { + const savedLayout = localStorage.getItem("servicesLayout") as Layout; + if (savedLayout === Layout.GRID || savedLayout === Layout.LIST) { + return savedLayout; + } + } + return Layout.GRID; // Default layout +} + /** * LayoutSwitcher component that allows users to toggle between grid and list layouts for displaying cards. It uses a Toggle button to switch layouts and a Tooltip to provide context on the action. The selected layout is saved in localStorage to persist user preference across sessions. * @param layout The layout to use for the cards, either "grid" or "list". @@ -39,6 +53,7 @@ const LayoutSwitcher = ({ setLayout(pressed ? Layout.GRID : Layout.LIST) } @@ -56,19 +71,7 @@ const LayoutSwitcher = ({ ); }; -/** - * Retrieves the default layout for the cards, either from localStorage or defaults to "grid". - * @returns The default layout, either "grid" or "list". - */ -export function getDefaultLayout(): Layout { - if (typeof window !== "undefined") { - const savedLayout = localStorage.getItem("servicesLayout") as Layout; - if (savedLayout === Layout.GRID || savedLayout === Layout.LIST) { - return savedLayout; - } - } - return Layout.GRID; // Default layout -} + /** * CardsLayout component that wraps its children in a grid or list layout based on the provided layout prop. It also saves the user's layout preference in localStorage.