fix(ui): lighten dark mode and match card, table, and tile headers to the topbar

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andras Bacsai
2026-10-21 18:46:52 +02:00
co-authored by Claude Opus 5.5
parent ab71f1af61
commit 8255f85a50
2 changed files with 102 additions and 45 deletions
+66 -30
View File
@@ -29,8 +29,9 @@ change.
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.
- **Dark gray, not black.** Dark mode is a dark gray with a faint brand tint
(page canvas about sRGB 20,17,26, never near-black). 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.
@@ -92,9 +93,9 @@ follow the theme or the tint.
}
.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);
/* Surfaces (dark: dark gray with a faint purple tint, hue 300) */
--ui-canvas: oklch(18.6% 0.018 300); /* sRGB 20,17,26 */
--ui-panel: oklch(23.2% 0.03 300); /* sRGB 31,26,41: header bar, card and table headers */
--ui-card-bg: rgb(255 255 255 / 6%);
--ui-card-border: rgb(255 255 255 / 8%);
--ui-well-bg: rgb(0 0 0 / 34%);
@@ -102,9 +103,9 @@ follow the theme or the tint.
--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-menu-bg: oklch(29.2% 0.018 300);
--ui-menu-border: oklch(36% 0.012 300);
--ui-hover: oklch(33.31% 0.018 300);
--ui-divider: rgb(255 255 255 / 7%);
--ui-chrome-border: rgb(255 255 255 / 4%);
@@ -135,7 +136,13 @@ Rules for the tokens:
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).
sRGB 20,17,26).
- **Dark lightness level.** The canvas is at 18.6% and the header panel at
23.2%. Do not make dark mode darker than this; the page looks too heavy. To
change the level, move every dark surface and border token by the same
amount (canvas, panel, menu, hover, borders, and the Coolify surface ladder),
so the steps between layers stay the same. If you move only some of them, a
hover fill can become darker than its menu.
- The primary buttons use purple in both themes. Only focus rings, active
navigation, and other accent states change to yellow in dark mode.
@@ -176,6 +183,9 @@ Class names below are examples. Keep the values and structure.
- Three layers: the canvas (page and sidebar, `--ui-canvas`), the top header
bar (`--ui-panel`, 48px high), and the cards on the canvas.
- In dark mode, the header bar, the outer box of a top-level card, and the
header row of a table use the same color (`--ui-panel`). The card wells and
table rows are the darker content layer below them.
- 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.
@@ -224,6 +234,7 @@ Class names below are examples. Keep the values and structure.
background: var(--ui-card-bg); box-shadow: var(--ui-card-shadow);
overflow: visible; /* menus and tooltips must escape */
}
.dark .card { background: var(--ui-panel); } /* top-level card: same as the header bar */
.card-header {
display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
gap: 16px; padding: 4px 8px 10px;
@@ -313,14 +324,20 @@ Class names below are examples. Keep the values and structure.
### 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.
- Header row: 40px, 13px medium, `--ui-fg-faint`, and a 1px `--ui-divider`
bottom border. Fill: `rgb(0 0 0 / 2%)` in light mode, `--ui-panel` in dark
mode (the same as the header bar and the card outer box).
- Dark mode rows use the card well color
(`color-mix(in srgb, var(--ui-panel) 66%, black)`, the same as
`--ui-well-bg` over the panel), so a table looks like a card: header on
top, content below.
- 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.
- Grid view tiles of a collection are cards without a well. In dark mode they
are `--ui-panel`, the same as the header bar.
### Status badge
@@ -497,13 +514,22 @@ The surface ladder is defined in `resources/css/app.css`.
| Token | Light | Dark | Use |
|---|---|---|---|
| `--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 (`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-canvas` | 97% off-white | 18.6% (tinted, = `--color-app`) | page canvas (kept below card fills so cards lift) |
| `--color-panel` | white | 23.2% | topbar, top-level card outer box, table header row |
| `--coollabs-elevated` | 98% neutral | 24% | shells and modal headers |
| `--coollabs-base` | white | 26.6% | nested modal bodies and legacy panels |
| `--coollabs-recessed` | 96% neutral | 29.2% | inputs and listboxes (`rgb(255 255 255 / 3%)` inside the dark card well) |
| `--coollabs-fill` | 92.2% neutral | 33.31% | dividers, passive fills, menu hover |
| `--coollabs-line` | translucent dark | 36% | control borders (`rgb(255 255 255 / 8%)` inside the dark card well) |
| `--coollabs-hairline` | 85.5% neutral | 36% | shell rings (crisp enough to read as a card edge, ~1.6:1) |
The dark level is fixed: canvas 18.6%, panel 23.2%. Also `--color-surface`
24.02%, `--color-raised` 26.64%, `--color-selected` 30.86%, and `--color-log`
19.91%. All dark surfaces have hue 300 (chroma 0.018, borders 0.012, panel
0.03). Do not make dark mode darker. To change the level, move the full ladder
by the same amount, including the opaque reset values for floating panels in
the card well (`.dark .application-settings-section-body :is(.listbox-panel,
...)`).
| `--coollabs-subtle` | 50% neutral | 70.8% neutral | labels and muted titles (light darkened for WCAG AA 4.5:1) |
Accent behavior is intentionally theme-aware:
@@ -531,11 +557,12 @@ 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.6% 0.018 300)`, sRGB 11,9,17), `bg-neutral-50` in light. The
`--color-app` `oklch(18.6% 0.018 300)`, sRGB 20,17,26), `bg-neutral-50` in light. The
`<main>` content area and page body use it.
- **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.
`oklch(23.2% 0.03 300)`, sRGB 31,26,41, a clear step lighter than the content canvas)
and `bg-white` in light, so the header reads as a separate panel. In dark
mode, top-level layer cards and table header rows use the same `--color-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
@@ -544,10 +571,10 @@ lift off the content:
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
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.
lightness compresses toward pure black below ~15%, so check the rendered sRGB
value. The dark ladder is `--color-app` 20,17,26, `--color-panel` 31,26,41,
`--coollabs-elevated` 32,29,39, `--coollabs-base` 39,36,45,
`--coollabs-recessed` 45,42,52 (sRGB), which reads as distinct surfaces.
Temperature: in light mode every panel is **pure neutral gray** (r=g=b). In
dark mode, every surface token (`--color-app`, `--color-panel`, `--coollabs-*`,
@@ -561,9 +588,16 @@ 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.
- **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.
- **Tables and collection tiles** outside a layer card use the
`rounded-xl border … dark:bg-white/[0.05]` shell plus the crisp
`--coollabs-hairline` ring; in light they are `bg-white` with the ring and
the restrained card lift. In dark mode these shells are painted like layer
cards. A tile, a separate pagination bar, or an empty box is a card without a
well, so it is `--color-panel`. A table (a shell whose first child is a
`border-b` header row) has a `--color-panel` header row and rows in the well
color (`color-mix(in srgb, var(--color-panel) 66%, black)`).
These rules match the Tailwind classes in `resources/css/app.css`, because
the table shells are copied markup without a shared class.
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
@@ -781,7 +815,9 @@ replace a manual shell when modifying it instead of creating another variant.
Anatomy:
- soft outer box: 12px radius, 1px border, 6px padding, faint fill
(`rgb(0 0 0 / 0.75%)` light, `rgb(255 255 255 / 6%)` dark);
(`rgb(0 0 0 / 0.75%)` light). In dark mode a top-level card is
`--color-panel`, the same as the topbar; a nested card keeps
`rgb(255 255 255 / 6%)` over its parent;
- 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
+36 -15
View File
@@ -1270,23 +1270,23 @@ html:not(.dark) .application-console-shell[data-console-theme="system"] .termina
ladder, so every panel shares one temperature (no blue cast). */
/* 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);
--color-app: oklch(18.6% 0.018 300);
--color-panel: oklch(23.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
--coollabs-elevated: oklch(24% 0.018 300);
--coollabs-recessed: oklch(29.2% 0.018 300);
--coollabs-base: oklch(26.6% 0.018 300);
--coollabs-fill: oklch(33.31% 0.018 300);
--color-surface: oklch(24.02% 0.018 300);
--color-raised: oklch(26.64% 0.018 300);
--color-selected: oklch(30.86% 0.018 300);
--color-log: oklch(19.91% 0.018 300);
--coollabs-line: oklch(36% 0.012 300);
/* Kept about 1.6:1 on the canvas so dark cards
and tables show a visible edge instead of blending into the canvas. */
--coollabs-hairline: oklch(32% 0.012 300);
--coollabs-hairline: oklch(36% 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
@@ -1848,6 +1848,27 @@ html[data-theme="custom"]:not(.dark) :is(.application-settings-section, .applica
background: linear-gradient(var(--well-bg), var(--well-bg)), var(--card-underlay);
}
/* Dark mode: top-level cards use the topbar color (see DESIGN.md). */
.dark :is(.application-settings-section, .empty-state):not(:is(.application-settings-section, .application-settings-section-body) *) {
background: var(--color-panel);
}
/* Dark mode: collection tiles, pagination bars, and tables match the cards.
A tile is a card without a well, so it takes the topbar color. A table (a
shell with a header row) takes the topbar color on the header row and the
box content (well) color on the rows. */
.dark [class~="rounded-xl"][class~="dark:bg-white/[0.05]"] {
background: var(--color-panel);
}
.dark [class~="rounded-xl"][class~="dark:bg-white/[0.05]"]:has(> [class~="border-b"]:first-child) {
background: color-mix(in srgb, var(--color-panel) 66%, black);
}
.dark [class~="rounded-xl"][class~="dark:bg-white/[0.05]"] > [class~="border-b"]:first-child {
background: var(--color-panel);
}
/* 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
@@ -1963,8 +1984,8 @@ html[data-theme="custom"]:not(.dark) :is(.application-settings-section, .applica
/* 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);
--coollabs-recessed: oklch(29.2% 0.018 300);
--coollabs-line: oklch(36% 0.012 300);
}
.settings-mobile-header {