refactor(v5): rename Home to Dashboard and add coollabs brand styles

Rename HomeController, Home page, and V5HomeProps type to Dashboard
equivalents. Update route name from 'home' to 'dashboard'.

Add coollabs brand color tokens to CSS theme and a 'coolify' button
variant using those colors. Add Sheet UI component for slide-over panels.
Update navbar with active-route detection and Sheet-based mobile drawer.
Switch cluster action buttons to use the new 'coolify' variant.
This commit is contained in:
Andras Bacsai
2026-06-16 22:23:50 +02:00
parent 4c41817cda
commit 5d57b131ff
10 changed files with 235 additions and 46 deletions
+7 -2
View File
@@ -29,6 +29,11 @@
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-warning: var(--warning);
--color-coollabs-50: #f5f0ff;
--color-coollabs: #6b16ed;
--color-coollabs-100: #7317ff;
--color-coollabs-200: #5a12c7;
--color-coollabs-300: #4a0fa3;
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
@@ -76,7 +81,7 @@
--warning: #fcd452;
--border: oklch(0.92 0.004 286.32);
--input: oklch(0.92 0.004 286.32);
--ring: oklch(0.705 0.015 286.067);
--ring: var(--warning);
--chart-1: oklch(0.871 0.006 286.286);
--chart-2: oklch(0.552 0.016 285.938);
--chart-3: oklch(0.442 0.017 285.786);
@@ -112,7 +117,7 @@
--warning: #fcd452;
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.552 0.016 285.938);
--ring: var(--warning);
--chart-1: oklch(0.871 0.006 286.286);
--chart-2: oklch(0.552 0.016 285.938);
--chart-3: oklch(0.442 0.017 285.786);
+4 -4
View File
@@ -14,7 +14,7 @@ import {
DialogTitle,
} from '@/components/ui/dialog';
import { csrfToken } from '@/lib/csrf';
import type { V5Cluster, V5HomeProps } from '@/types';
import type { V5Cluster, V5DashboardProps } from '@/types';
type ClusterFormErrors = {
name?: string[];
@@ -50,7 +50,7 @@ export default function Clusters({
projects = [],
selectedProjectUuid = null,
selectedEnvironmentUuid = null,
}: V5HomeProps) {
}: V5DashboardProps) {
const [clusterList, setClusterList] = useState<V5Cluster[]>(clusters);
const [selectedClusterId, setSelectedClusterId] = useState<string>(clusters[0]?.id ?? '');
const [name, setName] = useState('');
@@ -134,7 +134,7 @@ export default function Clusters({
</div>
<Button
type="button"
variant="outline"
variant="coolify"
size="sm"
aria-label="Create cluster"
onClick={() => setIsCreateDialogOpen(true)}
@@ -323,7 +323,7 @@ export default function Clusters({
<DialogFooter>
<DialogClose render={<Button type="button" variant="outline" />}>Cancel</DialogClose>
<Button type="submit" disabled={isSubmitting}>
<Button type="submit" variant="coolify" disabled={isSubmitting}>
{isSubmitting ? 'Creating...' : 'Create cluster'}
</Button>
</DialogFooter>
@@ -1,17 +1,17 @@
import { Head } from '@inertiajs/react';
import { AppNavbar } from '@/components/app-navbar';
import type { V5HomeProps } from '@/types';
import type { V5DashboardProps } from '@/types';
export default function Home({
export default function Dashboard({
flux,
projects = [],
selectedProjectUuid = null,
selectedEnvironmentUuid = null,
}: V5HomeProps) {
}: V5DashboardProps) {
return (
<>
<Head title="Magic" />
<Head title="Dashboard" />
<div className="h-dvh overflow-hidden bg-background text-foreground">
<AppNavbar
@@ -23,7 +23,7 @@ export default function Home({
<main className="flex h-full min-h-0 items-center justify-center overflow-hidden px-6 pt-16">
<section className="flex w-full max-w-5xl flex-col items-center justify-center gap-3 rounded-lg border border-dashed border-border bg-card px-8 py-24 text-center">
<p className="text-sm font-medium text-foreground">Magic</p>
<p className="text-sm font-medium text-foreground">Dashboard</p>
<p className="max-w-md text-sm text-muted-foreground">This is where the magic happens.</p>
</section>
</main>
+57 -9
View File
@@ -1,9 +1,11 @@
import { Link } from '@inertiajs/react';
import { Link, usePage } from '@inertiajs/react';
import { useMemo, useState } from 'react';
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Sheet, SheetClose, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet';
import { csrfToken } from '@/lib/csrf';
import type { SelectItemOption, V5HomeProps, V5Project } from '@/types';
import { cn } from '@/lib/utils';
import type { SelectItemOption, V5DashboardProps, V5Project } from '@/types';
function persistSelection(projectUuid: string, environmentUuid: string): void {
void fetch('/v5/selection', {
@@ -21,7 +23,7 @@ function persistSelection(projectUuid: string, environmentUuid: string): void {
});
}
type AppNavbarProps = V5HomeProps;
type AppNavbarProps = V5DashboardProps;
export function AppNavbar({
flux,
@@ -30,6 +32,7 @@ export function AppNavbar({
selectedProjectUuid = null,
selectedEnvironmentUuid = null,
}: AppNavbarProps) {
const { url } = usePage();
const firstProject = projects[0] ?? null;
const [projectUuid, setProjectUuid] = useState<string>(selectedProjectUuid ?? firstProject?.uuid ?? '');
const selectedProject = useMemo<V5Project | null>(
@@ -73,26 +76,27 @@ export function AppNavbar({
label: environment.name,
value: environment.uuid,
}));
const isClustersPage = url.startsWith('/v5/clusters');
return (
<header className="fixed inset-x-0 top-0 z-40 border-b border-border bg-background">
<nav className="flex h-16 items-center gap-4 px-6" aria-label="Main navigation">
<nav className="relative flex h-16 items-center gap-3 px-4 sm:px-6" aria-label="Main navigation">
<Link
href="/v5"
className="flex shrink-0 items-center rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
aria-label="Coolify home"
aria-label="Coolify dashboard"
>
<img src="/coolify-logo.svg" alt="Coolify" className="size-8" />
</Link>
<div className="flex min-w-0 items-center gap-2">
<div className="absolute left-1/2 flex min-w-0 -translate-x-1/2 items-center justify-center gap-1 md:static md:flex-1 md:translate-x-0 md:justify-start md:gap-2">
<Select
items={projectItems}
value={selectedProject?.uuid ?? ''}
onValueChange={selectProject}
disabled={projects.length === 0}
>
<SelectTrigger aria-label="Select a project" variant="ghost" className="max-w-[10rem]">
<SelectTrigger aria-label="Select a project" variant="ghost" className="max-w-[38vw] md:max-w-[10rem]">
<SelectValue placeholder="Select a project" />
</SelectTrigger>
<SelectContent position="popper" align="start" sideOffset={4}>
@@ -114,7 +118,7 @@ export function AppNavbar({
onValueChange={selectEnvironment}
disabled={!selectedProject || (selectedProject.environments ?? []).length === 0}
>
<SelectTrigger aria-label="Select an environment" variant="ghost" className="max-w-[10rem]">
<SelectTrigger aria-label="Select an environment" variant="ghost" className="max-w-[30vw] md:max-w-[10rem]">
<SelectValue placeholder="Select an environment" />
</SelectTrigger>
<SelectContent position="popper" align="start" sideOffset={4}>
@@ -132,7 +136,7 @@ export function AppNavbar({
<div className="ml-auto flex items-center gap-3">
<Link
href="/v5/clusters"
className="rounded-md px-3 py-1 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
className="hidden rounded-md px-3 py-1 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground md:inline-flex"
>
Clusters
</Link>
@@ -143,6 +147,50 @@ export function AppNavbar({
>
Flux: {flux?.label ?? 'Unknown'} · {clusters.length} clusters
</div>
<Sheet>
<SheetTrigger
className="inline-flex rounded-md p-2 text-warning transition-colors hover:bg-muted hover:text-warning md:hidden"
aria-label="Open mobile menu"
>
<svg xmlns="http://www.w3.org/2000/svg" className="size-6" viewBox="0 0 24 24" aria-hidden="true">
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M4 6h16M4 12h16M4 18h16"
/>
</svg>
</SheetTrigger>
<SheetContent side="right" className="w-72 max-w-[85vw] bg-background">
<SheetHeader>
<SheetTitle>Coolify</SheetTitle>
<SheetDescription className="sr-only">Move between Coolify v5 pages.</SheetDescription>
</SheetHeader>
<nav className="flex flex-col gap-1 px-4" aria-label="Mobile navigation">
<SheetClose
render={<Link href="/v5" />}
className={cn(
'rounded-md px-3 py-2 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-warning',
!isClustersPage && 'bg-accent text-accent-foreground',
)}
>
Dashboard
</SheetClose>
<SheetClose
render={<Link href="/v5/clusters" />}
className={cn(
'rounded-md px-3 py-2 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-warning',
isClustersPage && 'bg-accent text-accent-foreground',
)}
>
Clusters
</SheetClose>
</nav>
</SheetContent>
</Sheet>
</div>
</nav>
</header>
+2
View File
@@ -10,6 +10,8 @@ const buttonVariants = cva(
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/80',
coolify:
'border-coollabs bg-coollabs-50 text-coollabs-200 hover:bg-coollabs hover:text-white dark:border-coollabs-100 dark:bg-coollabs/20 dark:text-white dark:hover:bg-coollabs-100 dark:hover:text-white',
outline:
'border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50',
secondary:
+87
View File
@@ -0,0 +1,87 @@
import { Dialog as SheetPrimitive } from '@base-ui/react/dialog';
import { XIcon } from '@phosphor-icons/react';
import type * as React from 'react';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
function Sheet({ ...props }: SheetPrimitive.Root.Props) {
return <SheetPrimitive.Root data-slot="sheet" {...props} />;
}
function SheetTrigger({ ...props }: SheetPrimitive.Trigger.Props) {
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />;
}
function SheetClose({ ...props }: SheetPrimitive.Close.Props) {
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />;
}
function SheetPortal({ ...props }: SheetPrimitive.Portal.Props) {
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />;
}
function SheetOverlay({ className, ...props }: SheetPrimitive.Backdrop.Props) {
return (
<SheetPrimitive.Backdrop
data-slot="sheet-overlay"
className={cn(
'fixed inset-0 z-50 bg-black/10 text-xs/relaxed transition-opacity duration-150 data-ending-style:opacity-0 data-starting-style:opacity-0 supports-backdrop-filter:backdrop-blur-xs',
className,
)}
{...props}
/>
);
}
function SheetContent({
className,
children,
side = 'right',
showCloseButton = true,
...props
}: SheetPrimitive.Popup.Props & {
side?: 'top' | 'right' | 'bottom' | 'left';
showCloseButton?: boolean;
}) {
return (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Popup
data-slot="sheet-content"
data-side={side}
className={cn(
'fixed z-50 flex flex-col bg-popover bg-clip-padding text-xs/relaxed text-popover-foreground shadow-lg transition duration-200 ease-in-out data-ending-style:opacity-0 data-starting-style:opacity-0 data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t data-[side=bottom]:data-ending-style:translate-y-[2.5rem] data-[side=bottom]:data-starting-style:translate-y-[2.5rem] data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r data-[side=left]:data-ending-style:translate-x-[-2.5rem] data-[side=left]:data-starting-style:translate-x-[-2.5rem] data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-l data-[side=right]:data-ending-style:translate-x-[2.5rem] data-[side=right]:data-starting-style:translate-x-[2.5rem] data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=top]:data-ending-style:translate-y-[-2.5rem] data-[side=top]:data-starting-style:translate-y-[-2.5rem] data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm',
className,
)}
{...props}
>
{children}
{showCloseButton && (
<SheetPrimitive.Close data-slot="sheet-close" render={<Button variant="ghost" className="absolute top-3 right-3" size="icon-sm" />}>
<XIcon />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
)}
</SheetPrimitive.Popup>
</SheetPortal>
);
}
function SheetHeader({ className, ...props }: React.ComponentProps<'div'>) {
return <div data-slot="sheet-header" className={cn('flex flex-col gap-0.5 p-4', className)} {...props} />;
}
function SheetFooter({ className, ...props }: React.ComponentProps<'div'>) {
return <div data-slot="sheet-footer" className={cn('mt-auto flex flex-col gap-2 p-4', className)} {...props} />;
}
function SheetTitle({ className, ...props }: SheetPrimitive.Title.Props) {
return <SheetPrimitive.Title data-slot="sheet-title" className={cn('font-heading text-sm font-medium text-foreground', className)} {...props} />;
}
function SheetDescription({ className, ...props }: SheetPrimitive.Description.Props) {
return <SheetPrimitive.Description data-slot="sheet-description" className={cn('text-xs/relaxed text-muted-foreground', className)} {...props} />;
}
export { Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger };
+1 -1
View File
@@ -38,7 +38,7 @@ export type V5Project = {
environments: V5Environment[];
};
export type V5HomeProps = {
export type V5DashboardProps = {
flux: FluxStatus | null;
clusters?: V5Cluster[];
projects?: V5Project[];