From 54febeffd643ecb5bff07fca363aab5465f637b5 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Wed, 21 Oct 2026 13:18:59 +0200 Subject: [PATCH] feat(ui): restyle cards, sidebar, and dark surfaces - Cards use a soft outer box with an inset well; empty states match - Smaller sidebar icon tiles; sidebar and brand block use the canvas color with faint borders - Darker inputs in the dark well; floating panels stay opaque - Faint purple tint on dark surfaces, menus, and logs - Cloudflare icon for Cloudflare Tunnel - DESIGN.md: add a portable design language part Co-Authored-By: Claude Opus 5.5 --- DESIGN.md | 521 ++++++++++++++++-- resources/css/app.css | 203 +++++-- resources/views/components/empty.blade.php | 62 +-- resources/views/components/navbar.blade.php | 14 +- resources/views/components/reicon.blade.php | 2 + .../views/components/server/sidebar.blade.php | 2 +- resources/views/layouts/app.blade.php | 4 +- 7 files changed, 671 insertions(+), 137 deletions(-) diff --git a/DESIGN.md b/DESIGN.md index 02757f53f7..a1e10a63e6 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -1,9 +1,402 @@ # Coolify UI design system -This document defines Coolify's UI design system for its Livewire + Blade + -Alpine + Tailwind v4 frontend. The visual system covers the global shell, -project and environment pages, application navigation, settings surfaces, -tables, modals, toasts, terminals, and metrics. +This file has two parts: + +- **Part 1, Portable design language:** the look and feel of the Coolify UI, + with no dependency on Coolify code. It has the principles, a copy-paste token + sheet in plain CSS, and the anatomy of each component with exact values. Use + it to give another app the same feel. It works with any stack (plain CSS, + Tailwind, React, Vue, Blade). +- **Part 2, Coolify implementation:** how Coolify applies the language with its + Livewire + Blade + Alpine + Tailwind v4 frontend: the components, routes, page + shells, and the rules for this repository. + +When you change a shared value in Coolify, update Part 1 and Part 2 in the same +change. + +--- + +# Part 1. Portable design language + +## P1. The feel + +- **Compact and calm.** 13–14px UI text, 32px controls, small radii, short + labels. The data is the hero, not the chrome. +- **Layers, not boxes.** Depth comes from a few surface steps (canvas, card, + well, control), 1px borders, and very soft shadows. Never use thick borders or + heavy floating shadows. +- **Card and well.** Content sits in a darker inset panel (the well) inside a + soft outer box with a small title row. +- **Tactile buttons.** Buttons and navigation icon tiles have a 2px bottom + edge. They lift 1px on hover and press down on click. +- **Near-black, not black.** Dark mode is a very dark gray with a faint brand + tint. Light mode is pure neutral gray. +- **One accent per theme.** Purple in light mode, yellow in dark mode (purple + has too little contrast on dark surfaces). Everything else is neutral. +- **Quiet text.** Sentence case. A clear text ladder: primary, dim, faint. + No em dashes in UI copy. + +## P2. Token sheet + +Copy this sheet into the app's global CSS. Put the dark values on the selector +the app uses for dark mode (here `.dark` on ``). Use the tokens in every +component; do not hard-code a gray in a component, because then it does not +follow the theme or the tint. + +```css +:root { + /* Type */ + --ui-font-sans: 'Geist Sans', Inter, system-ui, sans-serif; + --ui-font-mono: 'Geist Mono', ui-monospace, SFMono-Regular, Menlo, monospace; + + /* Surfaces (light: pure neutral) */ + --ui-canvas: oklch(98.5% 0 0); /* page background and sidebar */ + --ui-panel: #ffffff; /* top header bar */ + --ui-card-bg: rgb(0 0 0 / 0.75%); /* card outer box */ + --ui-card-border: rgb(0 0 0 / 9%); + --ui-well-bg: #ffffff; /* card body (inset well) */ + --ui-well-border: rgb(0 0 0 / 8%); + --ui-well-shadow: 0 1px 2px rgb(0 0 0 / 6%); + --ui-control-bg: oklch(96% 0 0); /* inputs, select triggers */ + --ui-control-border: oklch(14.5% 0 0 / 0.1); + --ui-menu-bg: oklch(96% 0 0); /* dropdown, listbox, popover (opaque) */ + --ui-menu-border: oklch(14.5% 0 0 / 0.1); + --ui-hover: oklch(92.2% 0 0); /* menu option and row hover */ + --ui-divider: oklch(92.2% 0 0); + --ui-chrome-border: rgb(0 0 0 / 6%); /* sidebar and header edges */ + + /* Text */ + --ui-fg: oklch(14.5% 0 0); + --ui-fg-dim: oklch(40% 0 0); /* card titles */ + --ui-fg-faint: oklch(50% 0 0); /* labels, descriptions, meta */ + + /* Accent and status */ + --ui-accent: oklch(49.65% 0.2709 289.33); /* purple */ + --ui-accent-top: oklch(52.34% 0.287 289.16); /* primary button gradient */ + --ui-accent-bottom: oklch(43.76% 0.2369 289.82); + --ui-accent-edge: oklch(38.04% 0.2031 290.47); /* primary button depth edge */ + --ui-accent-fg: #ffffff; + --ui-success: oklch(48% 0.135 149.58); + --ui-warning: oklch(50% 0.105 80); + --ui-error: oklch(52% 0.2 27.33); + + /* Depth */ + --ui-card-shadow: 0 1px 2px rgb(0 0 0 / 5%); + --ui-menu-shadow: 0 4px 12px rgb(0 0 0 / 0.12), 0 2px 4px rgb(0 0 0 / 0.08); + --ui-modal-shadow: 0 24px 64px rgb(0 0 0 / 0.55), 0 4px 16px rgb(0 0 0 / 0.4); + --ui-depth-color: rgb(0 0 0 / 0.22); /* button and tile bottom edge */ + + /* Motion */ + --ui-ease-out: cubic-bezier(0.23, 1, 0.32, 1); + --ui-ease-drawer: cubic-bezier(0.32, 0.72, 0, 1); +} + +.dark { + /* Surfaces (dark: near-black with a faint purple tint, hue 300) */ + --ui-canvas: oklch(14.6% 0.018 300); + --ui-panel: oklch(19.2% 0.03 300); + --ui-card-bg: rgb(255 255 255 / 6%); + --ui-card-border: rgb(255 255 255 / 8%); + --ui-well-bg: rgb(0 0 0 / 34%); + --ui-well-border: rgb(255 255 255 / 7%); + --ui-well-shadow: 0 1px 2px rgb(0 0 0 / 25%); + --ui-control-bg: rgb(255 255 255 / 3%); + --ui-control-border: rgb(255 255 255 / 8%); + --ui-menu-bg: oklch(25.2% 0.018 300); + --ui-menu-border: oklch(32% 0.012 300); + --ui-hover: oklch(29.31% 0.018 300); + --ui-divider: rgb(255 255 255 / 7%); + --ui-chrome-border: rgb(255 255 255 / 4%); + + /* Text */ + --ui-fg: oklch(96.12% 0 0); + --ui-fg-dim: oklch(76.99% 0 0); + --ui-fg-faint: oklch(65% 0 0); + + /* Accent and status: yellow accent in dark mode */ + --ui-accent: oklch(88.13% 0.1507 91.7); + --ui-success: oklch(72.27% 0.192 149.58); + --ui-warning: oklch(88.13% 0.1507 91.7); + --ui-error: oklch(66% 0.2 27.33); + + --ui-depth-color: rgb(255 255 255 / 0.08); +} +``` + +Rules for the tokens: + +- Translucent surfaces (`--ui-card-bg`, `--ui-well-bg`, `--ui-control-bg`) take + the color of what is below them. That is why the cards and inputs show the + dark tint without their own tint. +- Floating surfaces (menus, popovers, tooltips, modals) must be opaque. Use + `--ui-menu-bg`, never a translucent value, or the content behind shows + through. +- To change the tint, change only the chroma and hue of the dark surfaces. + Keep it small: chroma 0.012 for borders, 0.018 for surfaces, 0.03 for the + header. On near-black, pick the hue by the rendered sRGB value: red must be a + little above green, or the tint looks blue (hue 300 renders the canvas as + sRGB 11,9,17). +- The primary buttons use purple in both themes. Only focus rings, active + navigation, and other accent states change to yellow in dark mode. + +## P3. Scales + +| Scale | Values | Use | +|---|---|---| +| Text | 11px | meta, section labels, card descriptions, header links | +| | 12px | badges, small descriptions, code snippets | +| | 13px | navigation, buttons, table cells, card titles (semibold) | +| | 14px | inputs, menu options, empty-state titles | +| | 24px | page titles (with a 13px faint summary below) | +| Weight | 500 / 600 | 500 for labels and buttons, 600 for titles | +| Height | 24px | badges | +| | 32px | buttons, inputs, select triggers, menu options | +| | 36px | navigation rows with tiles | +| | 40px / 48px | table header / table rows | +| Radius | 6px | menu options | +| | 8px | buttons, inputs, card wells, navigation tiles | +| | 10px | menus and popovers (6px options + 4px inset) | +| | 12px | card outer box | +| | full | badges, pills | +| Spacing | 6px | card outer padding | +| | 16px | well padding, page gutters on small screens | +| | 24px | gap between cards (use a flex or grid gap, not margins) | +| Motion | 80ms | button lift and press | +| | 120ms | color, border, and focus changes | +| | 150ms | menu swaps, row hover | + +Nested boxes normally follow `outer radius = inner radius + inset`. The card +(12px outer, 8px well, 6px inset) is an accepted exception. + +## P4. Components + +Class names below are examples. Keep the values and structure. + +### App shell + +- Three layers: the canvas (page and sidebar, `--ui-canvas`), the top header + bar (`--ui-panel`, 48px high), and the cards on the canvas. +- The sidebar is the canvas color and has a 1px `--ui-chrome-border` right + edge. The brand block (product name and version) at the top of the sidebar + column also uses the canvas color, with a 1px bottom border. +- Keep the chrome borders faint. A strong border on near-black looks like a + line drawn on the page. + +### Sidebar navigation + +```css +.nav-section { padding: 4px 10px; font-size: 11px; font-weight: 500; color: var(--ui-fg-faint); } +.nav-item { + display: flex; align-items: center; gap: 10px; + height: 36px; padding: 0 10px 0 4px; + font-size: 13px; font-weight: 500; color: var(--ui-fg-dim); + background: transparent; /* the row itself never fills */ +} +.nav-item:hover, .nav-item.is-active { color: var(--ui-fg); } +.nav-tile { /* 30px tile: 16px icon + 6px padding + 1px border */ + box-sizing: content-box; width: 16px; height: 16px; padding: 6px; + border: 1px solid rgb(0 0 0 / 9%); border-radius: 8px; background: #fafafa; + box-shadow: 0 2px 0 var(--ui-depth-color); + transition: transform 80ms, box-shadow 80ms, border-color 120ms, background-color 120ms; +} +.dark .nav-tile { border-color: rgb(255 255 255 / 9%); background: rgb(255 255 255 / 4%); } +.nav-item:hover .nav-tile { transform: translateY(-1px); box-shadow: 0 3px 0 var(--ui-depth-color); } +.nav-item:active .nav-tile { transform: translateY(2px); box-shadow: none; } +.nav-item.is-active .nav-tile { + color: var(--ui-accent); + border-color: color-mix(in oklab, var(--ui-accent) 45%, transparent); + --ui-depth-color: color-mix(in oklab, var(--ui-accent) 45%, transparent); +} +``` + +- Use outline icons (1.5px stroke) at 16px. A brand item can use a one-color + brand mark when it is clearer than an outline icon. +- Rows with a submenu end in a 12px chevron: accent in light mode, faint gray + in dark mode. +- Group rows under 11px section labels, and separate groups with a 1px divider. + +### Card (outer box, title row, well) + +```css +.card { + display: flex; flex-direction: column; + border: 1px solid var(--ui-card-border); border-radius: 12px; padding: 6px; + background: var(--ui-card-bg); box-shadow: var(--ui-card-shadow); + overflow: visible; /* menus and tooltips must escape */ +} +.card-header { + display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; + gap: 16px; padding: 4px 8px 10px; +} +.card-title { font-size: 13px; font-weight: 600; line-height: 20px; color: var(--ui-fg-dim); } +.card-description { margin-top: 2px; font-size: 11px; line-height: 16px; color: var(--ui-fg-faint); } +.card-header a { font-size: 11px; font-weight: 500; color: var(--ui-fg-faint); } +.card-header a:hover { color: var(--ui-fg); } +.card-well { + position: relative; padding: 16px; + border: 1px solid var(--ui-well-border); border-radius: 8px; + background: var(--ui-well-bg); box-shadow: var(--ui-well-shadow); +} +.card-well.is-flush { padding: 0; } /* tables and lists that touch the edge */ +``` + +- Card actions (buttons, badges, a filter) go in the title row on the right. +- Keep one collection in one card: its toolbar at the top of a flush well, its + table below, and its pagination in the same well. + +### Inputs and select triggers + +```css +.input { + display: block; width: 100%; height: 32px; padding: 0 12px; + border: 1px solid var(--ui-control-border); border-radius: 8px; + background: var(--ui-control-bg); color: var(--ui-fg); font-size: 14px; + transition: border-color 120ms var(--ui-ease-out), box-shadow 120ms var(--ui-ease-out); +} +.input::placeholder { color: var(--ui-fg-faint); } +.input:focus-visible { outline: none; border-color: var(--ui-accent); } +.label { display: block; margin-bottom: 6px; font-size: 13px; font-weight: 500; color: var(--ui-fg-dim); } +``` + +- Use a custom listbox, not the native `