style(ui): refine canvas layouts and rollback settings

This commit is contained in:
Andras Bacsai committed 2026-10-22 15:54:02 +02:00
1 parent c454f84545
commit eeaeacbe3f
6 files changed
+124 -40

No files matched your search

+64 -6
View File
@@ -195,8 +195,10 @@ Class names below are examples. Keep the values and structure.
table rows are the darker content layer below them.
- In light mode the sidebar is the canvas color and has a 1px
`--ui-chrome-border` right edge. In dark mode the sidebar column, including
the logo cell, is `#120e1a` with a brand-purple wash at the top (about 9%,
fading out) and a faint lavender right edge (`rgb(163 145 255 / 0.10)`).
the logo cell, is `#120e1a` with a faint lavender right edge
(`rgb(163 145 255 / 0.10)`). The nav has a brand-purple wash that starts at
9% and fades out; the logo cell has a flat 9% wash, so the two read as one
surface.
- The top bar matches the canvas. It has no bottom border at rest. After the
page scrolls under it, dark mode draws a lavender hairline
(`rgb(163 145 255 / 0.12)`), a translucent bar, blur, and a soft shadow.
@@ -204,6 +206,38 @@ Class names below are examples. Keep the values and structure.
- Keep the chrome borders faint. A strong border on near-black looks like a
line drawn on the page.
### App header (logo cell)
- The logo cell is the top of the sidebar column, in the 48px top bar. It is
as wide as the sidebar: 224px expanded, 64px collapsed.
- Expanded: the wordmark (15px semibold, tight tracking, `--ui-fg`), then the
version on the same baseline (10.5px medium, `--ui-fg-faint`), with 6px
between them and 16px side padding. An update badge sits at the right end.
- Collapsed: only the 20px logo mark, centered. The cell gets a bottom border
so that it lines up with the top bar edge.
- The update badge is a small pill: 18px high, 6px side padding, 9.5px
semibold text, fully rounded, a 10% fill, and a 25% inset ring of the same
color. It is purple in light mode and warning yellow in dark mode. While an
update runs, the same pill says "Updating". It hides when the sidebar is
collapsed.
- The mobile drawer starts with the same wordmark and version in a 48px row
with a bottom border, and a 32px close button on the right. It has no update
badge.
### Mobile header
- Below the desktop breakpoint (1024px) a sticky top bar replaces the desktop
top bar and sidebar. It is about 56px high (12px vertical padding, 16px side
padding, 24px from 640px), translucent (95% of `--ui-panel`) with a small
blur, and has a faint bottom border.
- Left: the logo mark (18px) in a 32px tile with an 8px radius and a neutral
fill (`rgb(255 255 255 / 6%)` in dark mode), then the team switcher. The
team name truncates.
- Right, with 4px gaps: status indicators (configuration warnings, active
deployments), the update badge, the user menu, and a 36px sidebar button
(the same sidebar glyph as the desktop collapse toggle). The sidebar button
opens the drawer from the right.
### Sidebar navigation
```css
@@ -622,8 +656,9 @@ lift off the content:
not the top bar.
- **Desktop sidebar and the topbar brand block** (Coolify name and version)
use the canvas color in light mode. In dark mode they use `--color-sidebar`
(`#120e1a`): a vertical brand wash on the nav, a radial wash on the logo
cell, and a lavender right edge. Section labels are 10.5px uppercase mono in
(`#120e1a`): a vertical brand wash on the nav, a flat 9% wash on the logo
cell (the same color as the top of the nav wash), and a lavender right
edge. Section labels are 10.5px uppercase mono in
muted lavender. Chevrons are hidden until the row is hovered, focused, or
current. The Admin icon stays neutral. The footer is transparent so the
wash continues. Do not use `dark:border-white/[0.06]` on these chrome edges;
@@ -690,8 +725,31 @@ color. Row-hover states keep the lighter `dark:hover:bg-white/[0.025]`.
rows; yellow washes look muddy on dark UI.
- Nested items sit behind a thin 1px guide line (`.nav-children`) and use the
same selected pill, not a thick box border.
- The update badge sits on the version row and uses a tiny fully rounded
primary-action pill.
- The logo cell (`.app-topbar-brand` in `layouts/app.blade.php`) is `w-56`
expanded and `w-16` collapsed. Expanded, it shows the `Coolify` wordmark
(`text-[15px] font-semibold tracking-tight`), `<x-version>`
(`text-[10.5px]`, `text-fg-faint` in dark), and the update badge
(`livewire:upgrade`, instance admins on self-hosted only) at `ml-auto`.
Collapsed, it shows only `/coolify-logo.svg` at `size-5` and adds
`border-b`.
- The update badge sits on the version row: an 18px fully rounded pill with
9.5px semibold text. It is `bg-coollabs/10 text-coollabs ring-coollabs/25` in
light mode and the warning pill (`dark:bg-warning/15 dark:text-warning
dark:ring-warning/25`) in dark mode, because an update is a real warning.
While an upgrade runs it says "Updating". Both open the upgrade modal.
- The mobile drawer header (`data-mobile-sidebar-brand`) repeats the wordmark
and version in an `h-12` row with a bottom border and a close button. It
does not show the update badge.
- The mobile top bar (`MOBILE TOP BAR` in `layouts/app.blade.php`, `lg:hidden`)
is `sticky top-0 z-40`, `px-4 py-3 sm:px-6`, `bg-white/95 dark:bg-panel/95
backdrop-blur-sm` with `border-neutral-200/60 dark:border-white/[0.06]`.
Left: `/coolify-logo.svg` (18px) in a `size-8 rounded-lg bg-neutral-100
dark:bg-white/[0.06]` tile, then `livewire:switch-team`. Right (`gap-1`):
`#configuration-warning-hud-slot-mobile`, `deployments-indicator
variant="mobile"`, the update badge (`livewire:upgrade key="mobile-upgrade"`,
instance admins on self-hosted only), `x-top-user-menu`, and a `size-9`
button that toggles the drawer. Keep these in the same order as the desktop
top bar.
- Submenu chevrons (the right chevron on rows that open a submenu and the back
chevron) are `text-coollabs` in light mode and grey in dark mode
(`dark:text-fg-faint` right, `dark:text-fg-dim` back).
+39 -1
View File
@@ -6023,9 +6023,10 @@ html.dark .mobile-sidebar-sheet [role="dialog"] {
--color-nav-muted: oklch(58% 0.04 300);
}
/* Same color as the top of the sidebar gradient, so the brand and sidebar read as one surface. */
html.dark .app-topbar-brand {
background-color: var(--color-sidebar);
background-image: radial-gradient(140% 260% at 0% 0%, rgb(107 22 237 / 0.12), transparent 70%);
background-image: linear-gradient(rgb(107 22 237 / 0.09), rgb(107 22 237 / 0.09));
border-right-color: rgb(163 145 255 / 0.10);
}
@@ -6102,6 +6103,43 @@ html.dark.is-scrolled:not([data-nav-style="floating"]) .app-topbar > div:not(.ap
box-shadow: 0 10px 30px -14px rgb(0 0 0 / 0.95), 0 1px 0 rgb(107 22 237 / 0.06);
}
/* Canvases (default navigation): the canvas runs under a clear top bar, and the
canvas root paints the dots (set by the canvas JS) behind it, like the
floating navigation does. */
@media (min-width: 1024px) {
html:not([data-nav-style="floating"]) .app-main.app-main-full:has(:is([data-ec-root], .cv-root)) {
padding-top: 0;
}
html:not([data-nav-style="floating"]) .app-main-full :is([data-ec-root], .cv-root) {
padding-top: calc(3rem + 0.5rem);
}
/* The deploy canvas (.cv-root) paints its own dots. */
html:not([data-nav-style="floating"]) .app-main-full [data-ec-root] {
background-image: var(--canvas-dots-image, radial-gradient(circle, rgb(0 0 0 / 0.13) 1px, transparent 1px));
background-size: var(--canvas-dots-size, 20px 20px);
background-position: var(--canvas-dots-position, 0 0);
}
html.dark:not([data-nav-style="floating"]) .app-main-full [data-ec-root] {
background-image: var(--canvas-dots-image, radial-gradient(circle, rgb(255 255 255 / 0.09) 1px, transparent 1px));
}
html:not([data-nav-style="floating"]) .app-main-full [data-ec-viewport] {
background-image: none;
}
html:not([data-nav-style="floating"]):has(.app-main-full :is([data-ec-root], .cv-root)) .app-topbar {
background-color: transparent;
backdrop-filter: none;
}
html:not([data-nav-style="floating"]):has(.app-main-full :is([data-ec-root], .cv-root)) .app-topbar > div:not(.app-topbar-brand) {
border-bottom-color: transparent;
}
}
html.dark .view-toggle {
gap: 2px;
padding: 2px;
@@ -233,14 +233,6 @@
],
['id' => 'healthcheck-timing-section', 'label' => 'Timing and retries'],
],
'project.application.rollback' => [
['id' => 'rollback-retention-section', 'label' => 'Image retention'],
['id' => 'rollback-images-section', 'label' => 'Available images'],
],
'project.application.resource-limits' => [
['id' => 'cpu-limits-section', 'label' => 'CPU'],
['id' => 'memory-limits-section', 'label' => 'Memory'],
],
'project.application.resource-operations' => [
['id' => 'clone-destination-section', 'label' => 'Clone destination'],
['id' => 'clone-environment-section', 'label' => 'Clone environment'],
+1 -1
View File
@@ -103,7 +103,7 @@
x-transition:leave-start="translate-x-0" x-transition:leave-end="translate-x-full"
class="relative flex h-full w-72 max-w-[85vw] min-w-0 flex-col overflow-hidden rounded-l-2xl border-l border-neutral-200 bg-white shadow-[-8px_0_30px_-6px_rgba(0,0,0,0.18)] dark:border-white/[0.12] dark:bg-panel dark:shadow-[-8px_0_30px_-4px_rgba(0,0,0,0.5)]">
<div data-mobile-sidebar-brand
class="flex h-12 shrink-0 items-center justify-between gap-1.5 border-b border-neutral-200 px-4 dark:border-white/[0.06]">
class="flex h-12 shrink-0 items-center justify-between gap-1.5 border-b border-neutral-200 px-4 dark:border-white/[0.06] dark:bg-panel">
<div class="flex min-w-0 items-baseline gap-1.5">
<a href="/" {{ wireNavigate() }} title="Coolify"
class="text-[15px] font-semibold tracking-tight text-black transition-opacity hover:opacity-80 dark:text-white">
@@ -1,15 +1,22 @@
<div class="flex flex-col gap-6" x-init="$wire.loadImages">
<form wire:submit="saveSettings" class="application-settings-form flex flex-col">
<x-unsaved-bar action="saveSettings" />
<x-application.settings-section id="rollback-retention-section" title="Image retention"
helper="Keep previously built Docker images available for fast rollbacks.">
<x-application.settings-section title="Rollback images"
helper="Keep previously built Docker images available for fast rollbacks. Rollback uses an existing image without rebuilding the application. Images in the Docker registry are pulled to the server first." flush>
<x-slot:actions>
@can('view', $application)
<x-forms.button wire:click="loadImages(true)">
Reload images
</x-forms.button>
@endcan
</x-slot:actions>
<form wire:submit="saveSettings"
class="flex flex-col gap-4 border-b border-neutral-200 px-4 py-3.5 dark:border-white/[0.07]">
<x-unsaved-bar action="saveSettings" />
@if ($serverRetentionDisabled)
<div class="mb-4">
<x-callout type="warning" title="Disabled by the server">
Image retention is disabled at the server level. This setting has no effect until a server
administrator enables it.
</x-callout>
</div>
<x-callout type="warning" title="Disabled by the server">
Image retention is disabled at the server level. This setting has no effect until a server
administrator enables it.
</x-callout>
@endif
<div class="max-w-sm">
@@ -18,18 +25,7 @@
helper="Set to 0 to keep only the running image. Pull request images are always removed during cleanup."
canGate="update" :canResource="$application" :disabled="$serverRetentionDisabled" />
</div>
</x-application.settings-section>
</form>
<x-application.settings-section id="rollback-images-section" title="Available images"
helper="Rollback uses an existing image without rebuilding the application. Images in the Docker registry are pulled to the server first." flush>
<x-slot:actions>
@can('view', $application)
<x-forms.button wire:click="loadImages(true)">
Reload images
</x-forms.button>
@endcan
</x-slot:actions>
</form>
<div wire:target="loadImages" wire:loading.remove>
@forelse ($images as $image)
@@ -180,7 +180,7 @@
<label for="service-force-https-{{ $appId }}-trigger" class="mb-0! whitespace-nowrap text-[12px]!">Redirect HTTP to HTTPS</label>
<x-helper helper="Disable only when Cloudflare Tunnel or another proxy connects to Coolify over HTTP. Keep enabled when Cloudflare uses Full or Full (Strict) SSL." />
<x-forms.listbox canGate="update" :canResource="$service" id="forceHttpsRedirects.{{ $appId }}"
htmlId="service-force-https-{{ $appId }}" preserveValue
htmlId="service-force-https-{{ $appId }}" preserveValue portal
onChange="updateForceHttps"
:onChangeArgs="[(int) $appId]"
:options="[
@@ -193,7 +193,7 @@
<label for="service-permanent-redirect-{{ $appId }}-trigger" class="mb-0! whitespace-nowrap text-[12px]!">Redirect type</label>
<x-helper helper="Applies to the HTTP to HTTPS and www redirects. Permanent (301/308) is better for SEO, but browsers cache it. Use Temporary (302/307) while you test a redirect." />
<x-forms.listbox canGate="update" :canResource="$service" id="permanentRedirects.{{ $appId }}"
htmlId="service-permanent-redirect-{{ $appId }}" preserveValue
htmlId="service-permanent-redirect-{{ $appId }}" preserveValue portal
onChange="updatePermanentRedirect"
:onChangeArgs="[(int) $appId]"
:options="[