mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-12 15:53:46 -04:00
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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
0d3f6d5dbe
commit
54febeffd6
@@ -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 `<html>`). 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 `<select>`.
|
||||
- Show a text input and a select trigger with the same height, fill, border,
|
||||
and radius.
|
||||
|
||||
### Buttons
|
||||
|
||||
```css
|
||||
.button {
|
||||
display: inline-flex; align-items: center; justify-content: center; gap: 6px;
|
||||
height: 32px; padding: 0 10px; border-radius: 6px;
|
||||
border: 1px solid rgb(0 0 0 / 9%); background: #ffffff; color: var(--ui-fg);
|
||||
font-size: 13px; font-weight: 500; white-space: nowrap;
|
||||
box-shadow: 0 2px 0 var(--ui-depth-color);
|
||||
transition: color 120ms, background-color 120ms, border-color 120ms,
|
||||
box-shadow 80ms, transform 80ms;
|
||||
transition-timing-function: var(--ui-ease-out);
|
||||
}
|
||||
.dark .button:not(.button-primary) { border-color: rgb(255 255 255 / 8%); background: rgb(255 255 255 / 6%); }
|
||||
.button:hover { transform: translateY(-1px); box-shadow: 0 3px 0 var(--ui-depth-color); }
|
||||
.button:active { transform: translateY(2px); box-shadow: none; }
|
||||
.button:focus-visible { outline: none; box-shadow: 0 2px 0 var(--ui-depth-color), 0 0 0 1px var(--ui-accent); }
|
||||
.button:disabled { box-shadow: none; transform: none; opacity: 0.5; cursor: not-allowed; }
|
||||
.button-primary {
|
||||
--ui-depth-color: var(--ui-accent-edge);
|
||||
border-color: var(--ui-accent-bottom); color: var(--ui-accent-fg);
|
||||
background: linear-gradient(to bottom, var(--ui-accent-top), var(--ui-accent-bottom));
|
||||
}
|
||||
```
|
||||
|
||||
- Use one primary button per area. Other actions are neutral buttons.
|
||||
- Destructive actions use the error color for text and focus ring.
|
||||
|
||||
### Menus, listboxes, and popovers
|
||||
|
||||
```css
|
||||
.menu {
|
||||
min-width: max(100%, 13rem); max-height: 16rem; overflow-y: auto;
|
||||
display: flex; flex-direction: column; gap: 1px; padding: 4px;
|
||||
border: 1px solid var(--ui-menu-border); border-radius: 10px;
|
||||
background: var(--ui-menu-bg); box-shadow: var(--ui-menu-shadow);
|
||||
}
|
||||
.menu-option {
|
||||
display: flex; align-items: center; justify-content: space-between; gap: 8px;
|
||||
min-height: 32px; padding: 6px 8px; border-radius: 6px;
|
||||
font-size: 14px; color: var(--ui-fg); white-space: nowrap; cursor: pointer;
|
||||
}
|
||||
.menu-option:hover { background: var(--ui-hover); }
|
||||
```
|
||||
|
||||
- Mark the selected option with a check mark on the right, not with a fill.
|
||||
- Keep trigger labels static (`Filter`, `Sort`); show the selection in the menu.
|
||||
- Render menus above everything; never clip them at a card or modal edge.
|
||||
|
||||
### Tables
|
||||
|
||||
- Header row: 40px, 13px medium, `--ui-fg-faint`, a faint fill
|
||||
(`rgb(0 0 0 / 2%)` light, `rgb(255 255 255 / 2%)` dark), and a 1px
|
||||
`--ui-divider` bottom border.
|
||||
- Data rows: about 48px, 10px 16px padding, a 1px divider, and a faint hover
|
||||
fill. Put the row action at the far right.
|
||||
- Toolbar on top (search left; filter, sort, and add right), pagination in the
|
||||
footer. Hide the footer when there is only one page.
|
||||
- Use a table, not cards, for a collection that can grow.
|
||||
|
||||
### Status badge
|
||||
|
||||
```css
|
||||
.badge {
|
||||
display: inline-flex; align-items: center; gap: 6px; height: 24px; padding: 0 8px;
|
||||
border: 1px solid rgb(0 0 0 / 9%); border-radius: 999px; background: rgb(0 0 0 / 4%);
|
||||
font-size: 12px; font-weight: 500; line-height: 1; color: var(--ui-fg-dim);
|
||||
}
|
||||
.dark .badge { border-color: rgb(255 255 255 / 12%); background: rgb(255 255 255 / 7%); color: var(--ui-fg); }
|
||||
.badge-dot { width: 6px; height: 6px; border-radius: 999px; background: currentColor; }
|
||||
```
|
||||
|
||||
- The pill stays neutral. Only the 6px dot has the status color (success,
|
||||
warning, error, neutral). Use a pulsing dot for a state in progress.
|
||||
|
||||
### Empty state
|
||||
|
||||
- On its own: the card outer box and well, with centered text, at least 160px
|
||||
high (128px small, 384px large).
|
||||
- Inside a card: only the centered text, because the card is the frame.
|
||||
- Optional 44px icon tile, a 14px medium title (`--ui-fg`), a 13px faint
|
||||
description (max width 28rem), and an optional action. No dashed border.
|
||||
|
||||
### Modals and toasts
|
||||
|
||||
- Modals use the card anatomy (outer box, title row, well), size to their
|
||||
content on desktop, and put the actions on the right below a divider. Use
|
||||
`--ui-modal-shadow`.
|
||||
- Toasts are compact cards (max width 26rem) with a status icon tile, a title,
|
||||
an optional description, and a dismiss button. They close after 4 seconds
|
||||
and pause on hover.
|
||||
|
||||
### Logs and code
|
||||
|
||||
- A monospace viewport on an opaque surface a little darker than the card
|
||||
(dark: `oklch(15.91% 0.018 300)`), with one compact toolbar on top.
|
||||
- Code snippets: 12px mono, 8px radius, 1px border, the control fill.
|
||||
|
||||
## P5. Traps
|
||||
|
||||
- **Translucent fills stack.** A control fill of `rgb(255 255 255 / 3%)` looks
|
||||
right on the dark well but is invisible on a menu. If you change a control
|
||||
token for one area, set the opaque value again on the floating panels in it.
|
||||
- **Global overrides of utility classes.** If the app has a global rule that
|
||||
replaces a border or fill utility (for example, to make borders crisper),
|
||||
that rule also hits new elements. Check the computed value, not the class.
|
||||
- **Dark default tokens.** Define every text token for both themes. A token
|
||||
that has only a dark value makes light-mode text too pale.
|
||||
- **`overflow: hidden` on a card** clips menus and tooltips. Keep cards
|
||||
visible; clip only an inner scroll area.
|
||||
- **Near-black hues.** oklch lightness below about 15% compresses toward black,
|
||||
and a hue can render bluer than expected. Check the rendered sRGB value.
|
||||
|
||||
## P6. Adopting the language in another app
|
||||
|
||||
1. Copy the P2 token sheet and load the fonts (Geist Sans, Geist Mono).
|
||||
2. Set the canvas on the page and the sidebar, and the panel on the header bar.
|
||||
3. Build the card, input, button, menu, badge, and empty-state components from
|
||||
P4 before the pages. Use only the tokens in them.
|
||||
4. Put every page section in a card. Stack cards with a 24px gap.
|
||||
5. Use a table for collections, and the status badge for states.
|
||||
6. Check in light and dark mode, with the sidebar expanded and collapsed, and
|
||||
with a menu open inside a card. Measure the computed colors of the canvas,
|
||||
card, well, input, and menu; they must match the token sheet.
|
||||
|
||||
---
|
||||
|
||||
# Part 2. Coolify implementation
|
||||
|
||||
This part defines how Coolify applies the design language with its Livewire +
|
||||
Blade + Alpine + Tailwind v4 frontend. It covers the global shell, project and
|
||||
environment pages, application navigation, settings surfaces, tables, modals,
|
||||
toasts, terminals, and metrics. In Coolify, the tokens of Part 1 map to the
|
||||
`--coollabs-*`, `--color-*`, `--card-*`, and `--well-*` variables in
|
||||
`resources/css/app.css`.
|
||||
|
||||
Use this file as the source of truth for frontend design work. Update it in the
|
||||
same change whenever a new shared visual pattern or component is introduced.
|
||||
@@ -33,9 +426,9 @@ The interface is compact and product-focused:
|
||||
|
||||
- near-neutral layered surfaces instead of large bordered boxes;
|
||||
- 13–14px UI typography and 32px controls;
|
||||
- crisp hairline rings plus a restrained card lift (single 1px ring +
|
||||
`0 1px 2px rgb(0 0 0 / 0.05)`) so cards and tables separate from the canvas,
|
||||
never heavy borders or a strong floating shadow;
|
||||
- layer cards are a soft outer box around a darker inset well (see section 5),
|
||||
with 1px borders and a restrained `0 1px 2px rgb(0 0 0 / 0.05)` lift, never
|
||||
heavy borders or a strong floating shadow;
|
||||
- full-width data tables for dense collections;
|
||||
- outline Reicon glyphs through `<x-reicon>`;
|
||||
- the Coolify purple brand accent in light mode;
|
||||
@@ -104,12 +497,12 @@ The surface ladder is defined in `resources/css/app.css`.
|
||||
|
||||
| Token | Light | Dark | Use |
|
||||
|---|---|---|---|
|
||||
| `--coollabs-canvas` | 97% off-white | 14.48% neutral | page canvas (kept below card fills so cards lift) |
|
||||
| `--coollabs-elevated` | 98% neutral | 20.02% neutral | shells and card headers |
|
||||
| `--coollabs-base` | white | 22.64% neutral | nested card bodies |
|
||||
| `--coollabs-recessed` | 96% neutral | 25.2% neutral | inputs and listboxes |
|
||||
| `--coollabs-canvas` | 97% off-white | 14.6% (tinted, = `--color-app`) | page canvas (kept below card fills so cards lift) |
|
||||
| `--coollabs-elevated` | 98% neutral | 20.02% neutral | shells and modal headers |
|
||||
| `--coollabs-base` | white | 22.64% neutral | nested modal bodies and legacy panels |
|
||||
| `--coollabs-recessed` | 96% neutral | 25.2% neutral | inputs and listboxes (`rgb(255 255 255 / 3%)` inside the dark card well) |
|
||||
| `--coollabs-fill` | 92.2% neutral | 29.31% neutral | dividers and passive fills |
|
||||
| `--coollabs-line` | translucent dark | 32% neutral | control borders |
|
||||
| `--coollabs-line` | translucent dark | 32% neutral | control borders (`rgb(255 255 255 / 8%)` inside the dark card well) |
|
||||
| `--coollabs-hairline` | 85.5% neutral | 32% neutral | shell rings (crisp enough to read as a card edge, ~1.5:1) |
|
||||
| `--coollabs-subtle` | 50% neutral | 70.8% neutral | labels and muted titles (light darkened for WCAG AA 4.5:1) |
|
||||
|
||||
@@ -133,15 +526,22 @@ opacity rather than a fully saturated fill.
|
||||
|
||||
### Shell layering
|
||||
|
||||
The app shell is three distinct surface layers, not one flat color. Chrome
|
||||
lifts, content is the base, cards lift off the content:
|
||||
The app shell is three distinct surface layers, not one flat color. The
|
||||
content canvas is the base, the topbar breadcrumb bar lifts off it, and cards
|
||||
lift off the content:
|
||||
|
||||
- **Content canvas** is the base layer: `bg-app` in dark (deepest,
|
||||
`--color-app` `oklch(14.48% 0 0)`, sRGB 10), `bg-neutral-50` in light. The
|
||||
`--color-app` `oklch(14.6% 0.018 300)`, sRGB 11,9,17), `bg-neutral-50` in light. The
|
||||
`<main>` content area and page body use it.
|
||||
- **Sidebar and topbar chrome** use `bg-panel` in dark (`--color-panel`
|
||||
`oklch(19.13% 0 0)`, sRGB 20, a clear step lighter than the content canvas)
|
||||
and `bg-white` in light, so the chrome reads as a separate panel from the content.
|
||||
- **Topbar chrome** uses `bg-panel` in dark (`--color-panel`
|
||||
`oklch(19.2% 0.03 300)`, sRGB 21,16,30, a clear step lighter than the content canvas)
|
||||
and `bg-white` in light, so the header reads as a separate panel.
|
||||
- **Desktop sidebar and the topbar brand block** (Coolify name and version)
|
||||
use the content canvas color (`bg-neutral-50` / `dark:bg-app`). The sidebar
|
||||
has a 1px right border, and the brand block has a 1px bottom border. These
|
||||
chrome borders stay faint (`border-black/[0.06]`, `dark:border-white/[0.04]`);
|
||||
do not use `dark:border-white/[0.06]`, which a global rule forces to the
|
||||
solid hairline grey. The mobile drawer keeps `bg-panel` / `bg-white`.
|
||||
|
||||
Dark surface tokens are exact oklch equivalents of chosen sRGB steps. oklch
|
||||
lightness compresses toward pure black below ~15%, so do not pick dark values by
|
||||
@@ -149,17 +549,23 @@ round oklch percentages. The dark ladder is `--color-app` 10,
|
||||
`--coollabs-elevated` 22, `--coollabs-base` 28, `--coollabs-recessed` 34 (sRGB),
|
||||
which reads as distinct surfaces.
|
||||
|
||||
Temperature: every panel is **pure neutral gray** (r=g=b), one consistent
|
||||
temperature across the sidebar, tables, cards, inputs, dividers, borders, and
|
||||
text, in both modes. Do not give one surface a cool (blue) or warm cast while
|
||||
the others stay neutral. The light page canvas uses `bg-neutral-50` (not
|
||||
Temperature: in light mode every panel is **pure neutral gray** (r=g=b). In
|
||||
dark mode, every surface token (`--color-app`, `--color-panel`, `--coollabs-*`,
|
||||
`--color-surface`, `--color-raised`, `--color-selected`, `--color-log`) has a
|
||||
very small Coolify purple tint (hue 300; chroma 0.012 on borders, 0.018 on
|
||||
surfaces, 0.03 on the topbar panel), so menus, listbox panels, modals, logs,
|
||||
and the command palette match the canvas. The table above lists the lightness only.
|
||||
The translucent cards, wells, and inputs show this tint through them. Keep the
|
||||
tint this small, and do not give one surface a different cast (blue or warm)
|
||||
from the others. The light page canvas uses `bg-neutral-50` (not
|
||||
`bg-gray-50`, which is faintly cool) so it matches the neutral cards and chrome.
|
||||
The only intentional color is the purple/yellow brand accent.
|
||||
- **Cards, tables, and collection tiles** lift off the content canvas with
|
||||
`dark:bg-white/[0.05]` plus the crisp `--coollabs-hairline` ring; in light
|
||||
they are `bg-white` with the ring and the restrained card lift.
|
||||
- **Layer cards** use the outer box and inset well from section 5.
|
||||
- **Tables and collection tiles** outside a layer card lift off the content
|
||||
canvas with `dark:bg-white/[0.05]` plus the crisp `--coollabs-hairline` ring;
|
||||
in light they are `bg-white` with the ring and the restrained card lift.
|
||||
|
||||
Do not paint the content area with the same `bg-panel` as the sidebar, and do
|
||||
Do not paint the content area with the same `bg-panel` as the topbar, and do
|
||||
not drop card fills below `dark:bg-white/[0.05]`; both make surfaces read as one
|
||||
color. Row-hover states keep the lighter `dark:hover:bg-white/[0.025]`.
|
||||
|
||||
@@ -169,16 +575,26 @@ color. Row-hover states keep the lighter `dark:hover:bg-white/[0.025]`.
|
||||
|
||||
### Global shell
|
||||
|
||||
- Main sidebar groups are compact, use outline Reicons, and keep a 32px row
|
||||
height.
|
||||
- Active sidebar rows are rounded pills (`rounded-md`) with a solid neutral
|
||||
selected fill (`bg-black/5` light, `bg-white/6` dark) and no accent rail.
|
||||
Hover rows use the same radius. Do not use accent-tinted gradients on nav
|
||||
- Main sidebar groups are compact and use outline Reicons. With Appearance →
|
||||
Navigation items → Flat, rows are 32px high.
|
||||
- With the default tile items (not Appearance → Navigation items → Flat), each
|
||||
icon sits in a 30px 3D tile (16px icon, 6px padding, 1px border) in a 36px row.
|
||||
- With tile items, the row has no fill: the current item shows through its
|
||||
accent tile (purple light, yellow dark) and its brighter label.
|
||||
- With Flat items, active rows are rounded pills (`rounded-md`) with a solid
|
||||
neutral selected fill (`bg-black/5` light, `bg-white/6` dark) and no accent
|
||||
rail. Hover rows use the same radius. Do not use accent-tinted gradients on nav
|
||||
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.
|
||||
- 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).
|
||||
- Brand items use a one-color brand glyph in `<x-reicon>` instead of an outline
|
||||
icon when the brand mark is clearer (for example `cloudflare` for Cloudflare
|
||||
Tunnel, drawn from `public/svgs/cloudflared.svg`).
|
||||
|
||||
### Resource navigation
|
||||
|
||||
@@ -364,17 +780,33 @@ replace a manual shell when modifying it instead of creating another variant.
|
||||
|
||||
Anatomy:
|
||||
|
||||
- 8px shell radius;
|
||||
- elevated header strip;
|
||||
- no divider below the header;
|
||||
- nested base-color body with its own fill ring;
|
||||
- soft outer box: 12px radius, 1px border, 6px padding, faint fill
|
||||
(`rgb(0 0 0 / 0.75%)` light, `rgb(255 255 255 / 6%)` dark);
|
||||
- small title row on the outer box (13px semibold title, 11px description and
|
||||
header links), no divider below it;
|
||||
- body is an inset well: 8px radius, 1px border, white in light and
|
||||
`rgb(0 0 0 / 34%)` in dark;
|
||||
- 16px body padding;
|
||||
- optional `flush` mode for full-bleed tables;
|
||||
- card-level actions belong in the header slot.
|
||||
- card-level actions belong in the header slot;
|
||||
- theme colors are CSS variables on the card (`--card-bg`, `--card-border`,
|
||||
`--well-bg`, `--well-border`, `--well-shadow`, `--card-fg*`), overridden in
|
||||
`.dark .application-settings-section`.
|
||||
|
||||
Header actions use an 8px top/right inset while the title keeps its 16px left
|
||||
inset. Do not leave a larger empty strip between the final action and the
|
||||
card's top-right corner.
|
||||
The title row has `4px 8px 10px` padding inside the 6px outer padding, so
|
||||
header actions sit 10px from the top and 14px from the right edge.
|
||||
|
||||
The card keeps `overflow: visible` so listbox panels and tooltips can escape
|
||||
it. Do not add `overflow: hidden` to the card or the well.
|
||||
|
||||
Inside the dark well, controls use a darker translucent fill and border
|
||||
(`--coollabs-recessed: rgb(255 255 255 / 3%)`, `--coollabs-line:
|
||||
rgb(255 255 255 / 8%)`), set on `.dark .application-settings-section-body`.
|
||||
The global opaque values look too light on the dark well. Floating panels in
|
||||
the well (`.listbox-panel`, `.searchable-listbox-panel`, `[role="listbox"]`,
|
||||
`[role="menu"]`) set the opaque values again, so they are not see-through. A
|
||||
new floating panel that uses `--coollabs-recessed` in a card needs the same
|
||||
reset.
|
||||
|
||||
Do not split one collection into a summary card followed by a table or log
|
||||
card. Keep its status/action in the header, its view switcher or toolbar at the
|
||||
@@ -397,8 +829,14 @@ Examples:
|
||||
|
||||
Do not give visibly inset parent and child boxes the same radius. Flush or
|
||||
edge-to-edge children are exempt because there is no visible inset to add.
|
||||
The layer card is an intentional exception: its 12px outer box and 8px well
|
||||
with a 6px inset match the reference design.
|
||||
|
||||
Use an empty state when the section has no usable controls:
|
||||
Use an empty state when the section has no usable controls. On its own,
|
||||
`x-empty` is the layer-card outer box and inset well with a centered 14px title
|
||||
and a 13px muted description (minimum height 160px; `sm` 128px, `lg` 384px).
|
||||
Inside a layer card, the card is the frame, so it shows only the centered text.
|
||||
Do not use a dashed border for empty states.
|
||||
|
||||
```blade
|
||||
<x-empty size="sm" title="Nothing here" description="Explain what enables it.">
|
||||
@@ -633,8 +1071,7 @@ targets.
|
||||
|
||||
`x-modal-input` and confirmation dialogs reuse the layer-card shell:
|
||||
|
||||
- compact elevated header;
|
||||
- nested base-color body;
|
||||
- the same outer box, title row, and inset well as layer cards;
|
||||
- content-width desktop sizing;
|
||||
- shared 32px controls;
|
||||
- no redundant description below a self-explanatory title;
|
||||
|
||||
+152
-51
@@ -1268,15 +1268,25 @@ html:not(.dark) .application-console-shell[data-console-theme="system"] .termina
|
||||
visible steps: content 10 -> elevated 22 -> base 28 -> recessed 34.
|
||||
Pure neutral (r=g=b), matching the neutral borders/text and the light
|
||||
ladder, so every panel shares one temperature (no blue cast). */
|
||||
--coollabs-canvas: oklch(14.48% 0 0);
|
||||
--coollabs-elevated: oklch(20.02% 0 0);
|
||||
--coollabs-recessed: oklch(25.2% 0 0);
|
||||
--coollabs-base: oklch(22.64% 0 0);
|
||||
--coollabs-fill: oklch(29.31% 0 0);
|
||||
--coollabs-line: oklch(32% 0 0);
|
||||
/* A very small Coolify purple tint (hue 300) so dark mode is not pure
|
||||
black. The translucent cards, wells, and inputs pick it up too. */
|
||||
--color-app: oklch(14.6% 0.018 300);
|
||||
--color-panel: oklch(19.2% 0.03 300);
|
||||
--coollabs-canvas: var(--color-app);
|
||||
/* The opaque surfaces (menus, listbox panels, modals, tooltips) get the
|
||||
same tint so they match the tinted canvas. */
|
||||
--coollabs-elevated: oklch(20.02% 0.018 300);
|
||||
--coollabs-recessed: oklch(25.2% 0.018 300);
|
||||
--coollabs-base: oklch(22.64% 0.018 300);
|
||||
--coollabs-fill: oklch(29.31% 0.018 300);
|
||||
--color-surface: oklch(20.02% 0.018 300);
|
||||
--color-raised: oklch(22.64% 0.018 300);
|
||||
--color-selected: oklch(26.86% 0.018 300);
|
||||
--color-log: oklch(15.91% 0.018 300);
|
||||
--coollabs-line: oklch(32% 0.012 300);
|
||||
/* Crisped from 26.9% (1.36:1 on canvas) to 32% (1.63:1) so dark cards
|
||||
and tables show a visible edge instead of blending into the canvas. */
|
||||
--coollabs-hairline: oklch(32% 0 0);
|
||||
--coollabs-hairline: oklch(32% 0.012 300);
|
||||
--coollabs-subtle: oklch(70.8% 0 0);
|
||||
/* Neutral (was #a8a8b0 / #7a7a84, both cool-tinted). Sidebar text now shares
|
||||
the same neutral temperature as the content text, so the sidebar no longer
|
||||
@@ -1776,23 +1786,45 @@ html[data-theme="custom"] textarea:disabled {
|
||||
margin-top: 1.75rem;
|
||||
}
|
||||
|
||||
/* Layer card root: elevated shell with a hairline ring. overflow stays
|
||||
visible so floating panels (listbox, tooltips) can escape the card. */
|
||||
.application-settings-section {
|
||||
/* Layer card root: soft outer box around a darker inset well (the body).
|
||||
overflow stays visible so floating panels (listbox, tooltips) can escape
|
||||
the card. */
|
||||
.application-settings-section,
|
||||
.empty-state {
|
||||
--card-bg: rgb(0 0 0 / 0.75%);
|
||||
--card-border: rgb(0 0 0 / 9%);
|
||||
--well-bg: #ffffff;
|
||||
--well-border: rgb(0 0 0 / 8%);
|
||||
--well-shadow: 0 1px 2px rgb(0 0 0 / 6%);
|
||||
/* Light mode has no dim/faint fg tokens; these keep WCAG AA on the card. */
|
||||
--card-fg: oklch(14.5% 0 0);
|
||||
--card-fg-dim: oklch(40% 0 0);
|
||||
--card-fg-faint: var(--coollabs-subtle);
|
||||
margin: 0;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
background: var(--coollabs-elevated);
|
||||
/* crisp ring + a restrained lift so the card separates from the canvas
|
||||
(shadcn-style), not a heavy floating shadow */
|
||||
box-shadow: 0 0 0 1px var(--coollabs-hairline), 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
||||
border: 1px solid var(--card-border);
|
||||
border-radius: 0.75rem;
|
||||
padding: 0.375rem;
|
||||
background: var(--card-bg);
|
||||
box-shadow: 0 1px 2px rgb(0 0 0 / 5%);
|
||||
/* keep anchored scrolls (scrollIntoView) clear of the fixed topbar and page padding */
|
||||
scroll-margin-top: 7rem;
|
||||
}
|
||||
|
||||
.dark .application-settings-section,
|
||||
.dark .empty-state {
|
||||
--card-bg: rgb(255 255 255 / 6%);
|
||||
--card-border: rgb(255 255 255 / 8%);
|
||||
--well-bg: rgb(0 0 0 / 34%);
|
||||
--well-border: rgb(255 255 255 / 7%);
|
||||
--well-shadow: 0 1px 2px rgb(0 0 0 / 25%);
|
||||
--card-fg: var(--color-fg);
|
||||
--card-fg-dim: var(--color-fg-dim);
|
||||
--card-fg-faint: var(--color-fg-faint);
|
||||
}
|
||||
|
||||
/* Brief accent ring when a settings nav sub-item scrolls a section into view.
|
||||
Use a real border on ::after (not animated multi-layer box-shadow) so the
|
||||
ring is the same weight on every side — box-shadow rings look thicker on
|
||||
@@ -1827,63 +1859,83 @@ html[data-theme="custom"] textarea:disabled {
|
||||
animation: application-settings-section-highlight 500ms ease-out forwards;
|
||||
}
|
||||
|
||||
/* Modals reuse the layer-card shell but size to content on large screens */
|
||||
/* Modals reuse the layer-card shell. The card tint is translucent, so paint it
|
||||
over the page canvas to keep the page behind the modal hidden. */
|
||||
.application-settings-section.application-settings-form {
|
||||
background: linear-gradient(var(--card-bg), var(--card-bg)), var(--coollabs-canvas);
|
||||
}
|
||||
|
||||
/* Modals size to content on large screens */
|
||||
@media (min-width: 1024px) {
|
||||
.application-settings-section.application-settings-form {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* Layer card header: compact strip on the shell, subtle title color */
|
||||
/* Layer card header: small title row on the outer box */
|
||||
.application-settings-section > :is(header, .application-settings-section-header) {
|
||||
display: flex;
|
||||
min-height: 3rem;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.5rem 0.5rem 1rem;
|
||||
border-radius: 8px 8px 0 0;
|
||||
background: var(--coollabs-elevated);
|
||||
gap: 1rem;
|
||||
padding: 0.25rem 0.5rem 0.625rem;
|
||||
font-weight: 500;
|
||||
color: var(--coollabs-subtle);
|
||||
color: var(--card-fg-faint);
|
||||
}
|
||||
|
||||
.application-settings-section > :is(header, .application-settings-section-header) :is(h2, h3) {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 1.25rem;
|
||||
letter-spacing: 0;
|
||||
/* Card title is a heading, so give it a readable strength instead of the
|
||||
muted --coollabs-subtle (5.66:1 light / 6.98:1 dark). ~8.7:1 in both
|
||||
modes (comfortable AAA) while descriptions/labels stay muted. */
|
||||
color: oklch(40% 0 0);
|
||||
color: var(--card-fg-dim);
|
||||
}
|
||||
|
||||
.dark .application-settings-section > :is(header, .application-settings-section-header) :is(h2, h3) {
|
||||
color: var(--color-fg-dim);
|
||||
.application-settings-section > :is(header, .application-settings-section-header) :is(h2, h3) .info-helper {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.application-settings-section > .application-settings-section-header p {
|
||||
margin-top: 0.125rem;
|
||||
font-size: 0.75rem;
|
||||
font-size: 11px;
|
||||
line-height: 1rem;
|
||||
color: var(--coollabs-subtle);
|
||||
color: var(--card-fg-faint);
|
||||
}
|
||||
|
||||
/* Layer card body: nested rounded panel with base bg. Ring uses the same
|
||||
--coollabs-hairline as the card shell (line ~1456) so the body edge and the
|
||||
header edge read as one continuous border instead of two different grays
|
||||
(fill was lighter than hairline, so the header sides and body sides did not
|
||||
match). */
|
||||
.application-settings-section > :is(header, .application-settings-section-header) > div:nth-child(2) > a:not(.button) {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--card-fg-faint);
|
||||
}
|
||||
|
||||
.application-settings-section > :is(header, .application-settings-section-header) > div:nth-child(2) > a:not(.button):hover {
|
||||
color: var(--card-fg);
|
||||
}
|
||||
|
||||
/* Layer card body: the inset well */
|
||||
.application-settings-section-body {
|
||||
position: relative;
|
||||
border-radius: 8px;
|
||||
background: var(--coollabs-base);
|
||||
box-shadow: 0 0 0 1px var(--coollabs-hairline);
|
||||
border: 1px solid var(--well-border);
|
||||
border-radius: 0.5rem;
|
||||
background: var(--well-bg);
|
||||
box-shadow: var(--well-shadow);
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
/* The dark well is darker than the canvas, so controls inside it use a darker,
|
||||
translucent fill and border (like haruvo) instead of the global opaque ones. */
|
||||
.dark .application-settings-section-body {
|
||||
--coollabs-recessed: rgb(255 255 255 / 3%);
|
||||
--coollabs-line: rgb(255 255 255 / 8%);
|
||||
}
|
||||
|
||||
/* Floating panels in the well must stay opaque: restore the global dark values. */
|
||||
.dark .application-settings-section-body :is(.listbox-panel, .searchable-listbox-panel, [role="listbox"], [role="menu"]) {
|
||||
--coollabs-recessed: oklch(25.2% 0.018 300);
|
||||
--coollabs-line: oklch(32% 0.012 300);
|
||||
}
|
||||
|
||||
.settings-mobile-header {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
@@ -2615,8 +2667,54 @@ input[type="search"]::-webkit-search-results-decoration {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
/* Standalone empty state: the same outer box and inset well as a layer card */
|
||||
.empty-state {
|
||||
background: var(--coollabs-base);
|
||||
border: 1px solid var(--card-border);
|
||||
border-radius: 0.75rem;
|
||||
padding: 0.375rem;
|
||||
background: var(--card-bg);
|
||||
box-shadow: 0 1px 2px rgb(0 0 0 / 5%);
|
||||
}
|
||||
|
||||
.empty-state-well {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid var(--well-border);
|
||||
border-radius: 0.5rem;
|
||||
background: var(--well-bg);
|
||||
box-shadow: var(--well-shadow);
|
||||
padding: 2rem 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-state-title {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.25rem;
|
||||
color: var(--card-fg);
|
||||
}
|
||||
|
||||
.empty-state-description {
|
||||
margin-top: 0.25rem;
|
||||
max-width: 28rem;
|
||||
line-height: 1.25rem;
|
||||
color: var(--coollabs-subtle);
|
||||
}
|
||||
|
||||
/* Inside a layer card, the card is the frame: show only the centered text. */
|
||||
.application-settings-section .empty-state,
|
||||
.application-settings-section .empty-state-well {
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
padding-inline: 0;
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.application-settings-section .empty-state {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.loading-state-card {
|
||||
@@ -5338,13 +5436,13 @@ a.command-palette-item:focus-visible {
|
||||
}
|
||||
}
|
||||
|
||||
/* Navigation items: each sidebar icon sits in a framed 32px tile with the 3D
|
||||
/* Navigation items: each sidebar icon sits in a framed 30px tile with the 3D
|
||||
depth of .button, like the canvas tool rail. Only the tile reacts; the row
|
||||
has no background. Applies to both navigation styles. Appearance →
|
||||
Navigation items → Flat sets data-nav-items="flat" on <html> and turns these
|
||||
rules off, which brings back the plain rows. */
|
||||
html:where(:not([data-nav-items="flat"])) .menu-item:not(.sidebar-toggle) {
|
||||
height: 2.5rem;
|
||||
height: 2.25rem;
|
||||
padding-left: 0.25rem;
|
||||
}
|
||||
|
||||
@@ -5459,14 +5557,14 @@ html:where(:not([data-nav-items="flat"])) :is(.menu-item, .menu-subitem):not(.si
|
||||
}
|
||||
|
||||
html:where(:not([data-nav-items="flat"])) .menu-back-icon {
|
||||
width: calc(1rem + 0.875rem + 2px);
|
||||
width: calc(1rem + 0.75rem + 2px);
|
||||
}
|
||||
|
||||
html:where(:not([data-nav-items="flat"])) :is(.menu-item, .menu-subitem):not(.sidebar-toggle) .menu-item-icon {
|
||||
box-sizing: content-box;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
padding: 0.4375rem;
|
||||
padding: 0.375rem;
|
||||
border: 1px solid oklch(0% 0 0 / 0.09);
|
||||
border-radius: 8px;
|
||||
background: #fafafa;
|
||||
@@ -5556,10 +5654,10 @@ html:where(:not([data-nav-items="flat"])) nav:not(.sidebar-collapsed) .menu-sear
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
/* The collapsed rail is 64px wide with 40px of content: center the 32px tile. */
|
||||
/* The collapsed rail is 64px wide with 40px of content: center the 30px tile. */
|
||||
html:where(:not([data-nav-items="flat"])) .sidebar-collapsed .menu-item:not(.sidebar-toggle) {
|
||||
padding-left: 0.25rem;
|
||||
padding-right: 0.25rem;
|
||||
padding-left: 0.3125rem;
|
||||
padding-right: 0.3125rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5616,7 +5714,6 @@ html.dark .canvas-zoom-max {
|
||||
@media (min-width: 1024px) {
|
||||
html[data-nav-style="floating"] {
|
||||
--floating-gap: 0.5rem;
|
||||
/* Same radius as the content cards (.application-settings-section). */
|
||||
--floating-radius: 8px;
|
||||
--floating-line: oklch(0% 0 0 / 0.09);
|
||||
/* Bottom edge of the top bar panel plus one gap. */
|
||||
@@ -5674,6 +5771,10 @@ html.dark .canvas-zoom-max {
|
||||
border-bottom-width: 0;
|
||||
}
|
||||
|
||||
html[data-nav-style="floating"] .app-topbar > .app-topbar-brand {
|
||||
border-radius: calc(var(--floating-radius) - 1px) 0 0 calc(var(--floating-radius) - 1px);
|
||||
}
|
||||
|
||||
html[data-nav-style="floating"] .app-sidebar {
|
||||
top: var(--floating-top);
|
||||
bottom: var(--floating-gap);
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
|
||||
@php
|
||||
$minHeight = match ($size) {
|
||||
'sm' => 'min-h-44',
|
||||
'sm' => 'min-h-32',
|
||||
'lg' => 'min-h-96',
|
||||
default => 'min-h-80',
|
||||
default => 'min-h-40',
|
||||
};
|
||||
|
||||
$iconBox = match ($size) {
|
||||
@@ -24,16 +24,7 @@
|
||||
default => 'size-5',
|
||||
};
|
||||
|
||||
$titleClass = match ($size) {
|
||||
'sm' => 'text-[14px] font-semibold text-black dark:text-fg',
|
||||
'lg' => 'text-base font-semibold text-black dark:text-fg',
|
||||
default => 'text-[15px] font-semibold text-black dark:text-fg',
|
||||
};
|
||||
|
||||
$descriptionClass = match ($size) {
|
||||
'sm' => 'mt-1 max-w-sm text-[12px] leading-5 text-neutral-500 dark:text-fg-dim',
|
||||
default => 'mt-1 max-w-sm text-[13px] leading-5 text-neutral-500 dark:text-fg-dim',
|
||||
};
|
||||
$descriptionClass = $size === 'sm' ? 'text-[12px]' : 'text-[13px]';
|
||||
|
||||
$hasIconSlot = isset($icon) && $icon instanceof \Illuminate\View\ComponentSlot && ! $icon->isEmpty();
|
||||
$hasIconName = filled($iconName);
|
||||
@@ -48,31 +39,30 @@
|
||||
}
|
||||
@endphp
|
||||
|
||||
{{-- Empty state: dashed card, icon badge, title, description, optional actions. --}}
|
||||
<div
|
||||
{{ $attributes->merge([
|
||||
'class' => "empty-state flex w-full flex-col items-center justify-center rounded-xl border border-dashed border-neutral-300 px-6 py-10 text-center dark:border-white/[0.1] {$minHeight}",
|
||||
]) }}>
|
||||
@if ($hasIcon)
|
||||
<div
|
||||
class="{{ $iconBox }} flex items-center justify-center rounded-xl border border-neutral-200 bg-white text-neutral-400 shadow-sm dark:border-white/[0.08] dark:bg-white/[0.035] dark:text-fg-faint">
|
||||
@if ($hasIconSlot)
|
||||
{{ $icon }}
|
||||
@else
|
||||
<x-reicon :name="$iconName" class="{{ $iconSize }}" />
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
{{-- Empty state: card frame around an inset well with centered text. Inside a layer card, the card is the frame. --}}
|
||||
<div {{ $attributes->merge(['class' => 'empty-state w-full']) }}>
|
||||
<div class="empty-state-well {{ $minHeight }}">
|
||||
@if ($hasIcon)
|
||||
<div
|
||||
class="{{ $iconBox }} flex items-center justify-center rounded-xl border border-neutral-200 bg-white text-neutral-400 shadow-sm dark:border-white/[0.08] dark:bg-white/[0.035] dark:text-fg-faint">
|
||||
@if ($hasIconSlot)
|
||||
{{ $icon }}
|
||||
@else
|
||||
<x-reicon :name="$iconName" class="{{ $iconSize }}" />
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<h2 class="{{ $titleClass }}">{{ $title }}</h2>
|
||||
<h2 class="empty-state-title">{{ $title }}</h2>
|
||||
|
||||
@if ($description)
|
||||
<p class="{{ $descriptionClass }}">{{ $description }}</p>
|
||||
@endif
|
||||
@if ($description)
|
||||
<p class="empty-state-description {{ $descriptionClass }}">{{ $description }}</p>
|
||||
@endif
|
||||
|
||||
@if ($footer)
|
||||
<div class="mt-4 flex flex-wrap items-center justify-center gap-2">
|
||||
{{ $footer }}
|
||||
</div>
|
||||
@endif
|
||||
@if ($footer)
|
||||
<div class="mt-4 flex flex-wrap items-center justify-center gap-2">
|
||||
{{ $footer }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
@props(['desktop' => false])
|
||||
|
||||
<nav class="sidebar-nav flex min-h-0 flex-col flex-1 bg-white border-r border-neutral-200 dark:border-white/[0.06] dark:bg-panel pt-2"
|
||||
<nav @class([
|
||||
'sidebar-nav flex min-h-0 flex-col flex-1 border-r border-black/[0.06] dark:border-white/[0.04] pt-2',
|
||||
'bg-neutral-50 dark:bg-app' => $desktop,
|
||||
'bg-white dark:bg-panel' => ! $desktop,
|
||||
])
|
||||
:class="collapsed ? 'px-2 lg:px-3 sidebar-collapsed' : 'px-2 lg:px-3'"
|
||||
@mouseover="
|
||||
if (!collapsed) return;
|
||||
@@ -189,7 +193,7 @@
|
||||
title="{{ $navMenu['backTitle'] }}" aria-label="{{ $navMenu['backTitle'] }}"
|
||||
class="menu-item menu-back bg-transparent! hover:bg-neutral-100! dark:hover:bg-white/[0.06]!" :class="collapsed && 'lg:justify-center lg:px-0'">
|
||||
<span class="menu-back-icon flex shrink-0 items-center justify-center">
|
||||
<x-reicon name="chevron-down" class="size-3 rotate-90 text-neutral-500 dark:text-fg-dim" />
|
||||
<x-reicon name="chevron-down" class="size-3 rotate-90 text-coollabs dark:text-fg-dim" />
|
||||
</span>
|
||||
<span class="flex-1 truncate pr-6 text-center font-medium text-black dark:text-fg"
|
||||
:class="collapsed && 'lg:hidden'">{{ $navMenu['label'] }}</span>
|
||||
@@ -214,7 +218,7 @@
|
||||
class="menu-item menu-back bg-transparent! hover:bg-neutral-100! dark:hover:bg-white/[0.06]!" :class="collapsed && 'lg:justify-center lg:px-0'">
|
||||
{{-- As wide as a menu icon (with its tile), so the chevron lines up with the icons below. --}}
|
||||
<span class="menu-back-icon flex shrink-0 items-center justify-center">
|
||||
<x-reicon name="chevron-down" class="size-3 rotate-90 text-neutral-500 dark:text-fg-dim" />
|
||||
<x-reicon name="chevron-down" class="size-3 rotate-90 text-coollabs dark:text-fg-dim" />
|
||||
</span>
|
||||
<span class="flex-1 truncate pr-6 text-center font-medium text-black dark:text-fg"
|
||||
:class="collapsed && 'lg:hidden'">{{ $submenuItem['label'] }}</span>
|
||||
@@ -268,7 +272,7 @@
|
||||
:class="collapsed && 'lg:justify-center lg:px-0'">
|
||||
<x-reicon :name="$item['icon']" class="menu-item-icon" />
|
||||
<span class="menu-item-label flex-1 text-left" :class="collapsed && 'lg:hidden'">{{ $item['label'] }}</span>
|
||||
<x-reicon name="chevron-down" class="size-3 -rotate-90 text-fg-faint" ::class="collapsed && 'lg:hidden'" />
|
||||
<x-reicon name="chevron-down" class="size-3 -rotate-90 text-coollabs dark:text-fg-faint" ::class="collapsed && 'lg:hidden'" />
|
||||
</button>
|
||||
@else
|
||||
<a title="{{ $item['label'] }}" href="{{ $item['href'] }}"
|
||||
@@ -321,7 +325,7 @@
|
||||
</ul>
|
||||
{{-- Sidebar footer (desktop only; mobile uses a temporary slide-over). It stays visible;
|
||||
only the menu above it scrolls on short or zoomed-in screens. --}}
|
||||
<div class="sidebar-footer shrink-0 -mx-2 hidden flex-col gap-1 bg-white px-2 py-2 dark:bg-panel lg:-mx-3 lg:flex lg:px-3">
|
||||
<div class="sidebar-footer shrink-0 -mx-2 hidden flex-col gap-1 bg-neutral-50 px-2 py-2 dark:bg-app lg:-mx-3 lg:flex lg:px-3">
|
||||
<div class="flex items-center gap-1" :class="collapsed ? 'flex-col justify-center' : 'justify-between'">
|
||||
<x-top-user-menu sidebar />
|
||||
<button type="button" @click="toggleSidebar()" title="Toggle sidebar" aria-label="Toggle sidebar"
|
||||
|
||||
@@ -84,9 +84,11 @@
|
||||
'bandage' => '<path d="M9.34458 8.23084C9.03703 7.92329 8.53839 7.92329 8.23084 8.23084C7.92329 8.53839 7.92329 9.03703 8.23084 9.34458C8.53839 9.65213 9.03703 9.65213 9.34458 9.34458C9.65213 9.03703 9.65213 8.53839 9.34458 8.23084Z" fill="currentColor"/><path d="M12.1289 9.15895C11.8214 8.8514 11.3227 8.8514 11.0152 9.15895C10.7076 9.4665 10.7076 9.96514 11.0152 10.2727C11.3227 10.5802 11.8214 10.5802 12.1289 10.2727C12.4365 9.96514 12.4365 9.4665 12.1289 9.15895Z" fill="currentColor"/><path d="M15.8414 12.8714C16.1489 13.179 16.1489 13.6776 15.8414 13.9852C15.5338 14.2927 15.0352 14.2927 14.7277 13.9852C14.4201 13.6776 14.4201 13.179 14.7277 12.8714C15.0352 12.5639 15.5338 12.5639 15.8414 12.8714Z" fill="currentColor"/><path d="M16.7695 16.7695C17.0771 16.462 17.0771 15.9633 16.7695 15.6558C16.462 15.3482 15.9633 15.3482 15.6558 15.6558C15.3482 15.9633 15.3482 16.462 15.6558 16.7695C15.9633 17.0771 16.462 17.0771 16.7695 16.7695Z" fill="currentColor"/><path d="M13.057 13.057C13.3646 12.7495 13.3646 12.2509 13.057 11.9433C12.7495 11.6358 12.2509 11.6358 11.9433 11.9433C11.6358 12.2509 11.6358 12.7495 11.9433 13.057C12.2509 13.3646 12.7495 13.3646 13.057 13.057Z" fill="currentColor"/><path d="M13.9852 14.7277C14.2927 15.0352 14.2927 15.5338 13.9852 15.8414C13.6776 16.1489 13.179 16.1489 12.8714 15.8414C12.5639 15.5338 12.5639 15.0352 12.8714 14.7277C13.179 14.4201 13.6776 14.4201 13.9852 14.7277Z" fill="currentColor"/><path d="M9.15895 11.0152C9.4665 10.7076 9.96514 10.7076 10.2727 11.0152C10.5802 11.3227 10.5802 11.8214 10.2727 12.1289C9.96514 12.4365 9.4665 12.4365 9.15895 12.1289C8.8514 11.8214 8.8514 11.3227 9.15895 11.0152Z" fill="currentColor"/><path fill-rule="evenodd" clip-rule="evenodd" d="M14.3986 2.68287C16.3091 0.772375 19.4066 0.772376 21.3171 2.68287C23.2276 4.59337 23.2276 7.6909 21.3171 9.6014L18.9185 12L21.3171 14.3986C23.2276 16.3091 23.2276 19.4066 21.3171 21.3171C19.4066 23.2276 16.3091 23.2276 14.3986 21.3171L12 18.9185L9.6014 21.3171C7.6909 23.2276 4.59337 23.2276 2.68287 21.3171C0.772376 19.4066 0.772376 16.3091 2.68287 14.3986L5.08148 12L2.68287 9.6014C0.772376 7.6909 0.772375 4.59337 2.68287 2.68287C4.59337 0.772376 7.6909 0.772376 9.6014 2.68287L12 5.08148L14.3986 2.68287ZM20.2565 15.4593C21.5812 16.784 21.5812 18.9318 20.2565 20.2565C18.9318 21.5812 16.784 21.5812 15.4593 20.2565L3.74353 8.54074C2.41882 7.21603 2.41882 5.06824 3.74353 3.74353C5.06824 2.41882 7.21603 2.41882 8.54074 3.74353L20.2565 15.4593ZM20.2565 8.54074L17.8579 10.9393L13.0607 6.14214L15.4593 3.74353C16.784 2.41882 18.9318 2.41882 20.2565 3.74353C21.5812 5.06825 21.5812 7.21603 20.2565 8.54074ZM3.74353 15.4593L6.14214 13.0607L10.9393 17.8579L8.54074 20.2565C7.21603 21.5812 5.06825 21.5812 3.74353 20.2565C2.41882 18.9318 2.41882 16.784 3.74353 15.4593Z" fill="currentColor"/>',
|
||||
'broom' => '<g transform="scale(0.09375)"><path d="M235.29,216.7C212.86,205.69,200,182.12,200,152V134.69a15.94,15.94,0,0,0-10.09-14.87l-28.65-11.46A8,8,0,0,1,156.79,98l22.32-56.67C184,28.79,178,14.21,165.34,9.51a24,24,0,0,0-30.7,13.71L112.25,80.08a8,8,0,0,1-10.41,4.5L73.11,73.08a15.91,15.91,0,0,0-17.38,3.66C34.68,98.4,24,123.71,24,152a111.53,111.53,0,0,0,31.15,77.53A8.06,8.06,0,0,0,61,232H232a8,8,0,0,0,8-7.51A8.21,8.21,0,0,0,235.29,216.7ZM115.11,216a87.52,87.52,0,0,1-24.26-41.71,8.21,8.21,0,0,0-9.25-6.18A8,8,0,0,0,75.28,178a105.33,105.33,0,0,0,18.36,38H64.44A95.62,95.62,0,0,1,40,152a85.92,85.92,0,0,1,7.73-36.3l137.8,55.13c3,18.06,10.55,33.5,21.89,45.19Z" fill="currentColor"/></g>',
|
||||
'shield-star' => '<g transform="scale(0.09375)"><path d="M80.57,117A8,8,0,0,1,91,112.57l29,11.61V96a8,8,0,0,1,16,0v28.18l29-11.61A8,8,0,1,1,171,127.43l-30.31,12.12L158.4,163.2a8,8,0,1,1-12.8,9.6L128,149.33,110.4,172.8a8,8,0,1,1-12.8-9.6l17.74-23.65L85,127.43A8,8,0,0,1,80.57,117ZM224,56v56c0,52.72-25.52,84.67-46.93,102.19-23.06,18.86-46,25.27-47,25.53a8,8,0,0,1-4.2,0c-1-.26-23.91-6.67-47-25.53C57.52,196.67,32,164.72,32,112V56A16,16,0,0,1,48,40H208A16,16,0,0,1,224,56Zm-16,0L48,56l0,56c0,37.3,13.82,67.51,41.07,89.81A128.25,128.25,0,0,0,128,223.62a129.3,129.3,0,0,0,39.41-22.2C194.34,179.16,208,149.07,208,112Z" fill="currentColor"/></g>',
|
||||
'cloudflare' => '<g transform="translate(0 8.24) scale(0.498) translate(-51 -6.7)" fill="currentColor"><path d="M84.2,20.4a2.85546,2.85546,0,0,0-.3-2.6,3.09428,3.09428,0,0,0-2.1-1.1l-17.4-.2c-.1,0-.2-.1-.3-.1a.1875.1875,0,0,1,0-.3c.1-.2.2-.3.4-.3L82,15.6a6.29223,6.29223,0,0,0,5.1-3.8l1-2.6c0-.1.1-.2,0-.3A11.39646,11.39646,0,0,0,66.2,7.7a5.45941,5.45941,0,0,0-3.6-1A5.20936,5.20936,0,0,0,58,11.3a5.46262,5.46262,0,0,0,.1,1.8A7.30177,7.30177,0,0,0,51,20.4a4.102,4.102,0,0,0,.1,1.1.3193.3193,0,0,0,.3.3H83.5c.2,0,.4-.1.4-.3Z"/><path d="M89.7,9.2h-.5c-.1,0-.2.1-.3.2l-.7,2.4a2.85546,2.85546,0,0,0,.3,2.6,3.09428,3.09428,0,0,0,2.1,1.1l3.7.2c.1,0,.2.1.3.1a.1875.1875,0,0,1,0,.3c-.1.2-.2.3-.4.3l-3.8.2a6.29223,6.29223,0,0,0-5.1,3.8l-.2.9c-.1.1,0,.3.2.3H98.5a.26517.26517,0,0,0,.3-.3,10.87184,10.87184,0,0,0,.4-2.6,9.56045,9.56045,0,0,0-9.5-9.5"/></g>',
|
||||
'network' => '<g transform="scale(0.09375)"><path d="M240,120a8,8,0,0,1-8,8H200v32h8a16,16,0,0,1,16,16v32a16,16,0,0,1-16,16H176a16,16,0,0,1-16-16V176a16,16,0,0,1,16-16h8V128H72v32h8a16,16,0,0,1,16,16v32a16,16,0,0,1-16,16H48a16,16,0,0,1-16-16V176a16,16,0,0,1,16-16h8V128H24a8,8,0,0,1,0-16h96V88h-8A16,16,0,0,1,96,72V40a16,16,0,0,1,16-16h32a16,16,0,0,1,16,16V72a16,16,0,0,1-16,16h-8v24h96A8,8,0,0,1,240,120Z" fill="currentColor"/></g>',
|
||||
];
|
||||
|
||||
// 'cloudflare' is the Cloudflare cloud from public/svgs/cloudflared.svg in one color.
|
||||
// 'danger' is the shield-alert glyph in the error color, for Danger Zone menu items.
|
||||
$isDanger = $name === 'danger';
|
||||
$svg = $icons[$isDanger ? 'shield-alert' : $name] ?? '';
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
'label' => 'Cloudflare Tunnel',
|
||||
'route' => 'server.cloudflare-tunnel',
|
||||
'active' => $activeMenu === 'cloudflare-tunnel',
|
||||
'icon' => 'globe',
|
||||
'icon' => 'cloudflare',
|
||||
'group' => 'Networking',
|
||||
'visible' => ! $server->isLocalhost(),
|
||||
],
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
:class="{ 'z-[1000]': resourceActionsOpen }"
|
||||
class="app-topbar hidden lg:flex fixed top-0 inset-x-0 z-50 h-12 items-center bg-white/95 dark:bg-panel/95 backdrop-blur">
|
||||
{{-- Brand (width tracks sidebar) --}}
|
||||
<div class="flex items-center gap-2 h-full shrink-0 border-r border-neutral-200 dark:border-white/[0.06] transition-[width] duration-200"
|
||||
<div class="app-topbar-brand flex items-center gap-2 h-full shrink-0 border-r border-b border-black/[0.06] bg-neutral-50 dark:border-white/[0.04] dark:bg-app transition-[width] duration-200"
|
||||
:class="collapsed ? 'w-16 justify-center px-0' : 'w-56 px-4'">
|
||||
<div class="flex items-baseline gap-1.5 min-w-0">
|
||||
<a href="/" {{ wireNavigate() }} title="Coolify"
|
||||
@@ -59,7 +59,7 @@
|
||||
</div>
|
||||
{{-- Collapse toggle + team switcher --}}
|
||||
<div
|
||||
class="flex h-full items-center gap-0.5 min-w-0 flex-1 border-b border-neutral-200 pl-3 pr-4 dark:border-white/[0.06]">
|
||||
class="flex h-full items-center gap-0.5 min-w-0 flex-1 border-b border-black/[0.06] pl-3 pr-4 dark:border-white/[0.04]">
|
||||
<div class="relative flex min-w-0 flex-1 items-center">
|
||||
<x-top-breadcrumb />
|
||||
<div id="server-topbar-context" class="min-w-0"></div>
|
||||
|
||||
Reference in New Issue
Block a user