Refresh the dark dashboard around the coolLabs purple brand.

Dark mode keeps a black canvas, paints cards at #121212, and tints the sidebar purple-black. Active navigation, secondary buttons, and the view switcher use brand purple; warning yellow stays for real warnings.

Co-authored-by: Andras Bacsai <andrasbacsai@users.noreply.github.com>
This commit is contained in:
Cursor AgentandAndras Bacsai committed 2026-10-22 07:13:38 +00:00
1 parent 91ea7d851e
commit 8df39bc637
19 files changed
+432 -140

No files matched your search

+83 -54
View File
@@ -29,11 +29,14 @@ 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.
- **Dark gray, not black.** Dark mode is a pure neutral dark gray (page
canvas about sRGB 10,10,10). 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.
- **Dark gray, not black.** Dark mode page canvas and the top bar are neutral
sRGB 10,10,10 (`#0a0a0a`). Cards sit one step up (`#121212`). Light mode is
pure neutral gray.
- **One accent.** Coolify purple in both themes (`#6b16ed`, hover `#7317ff`,
pressed `#5a12c7`, depth `#4a0fa3`, light text and icons `#a391ff`). Yellow
is only for real warnings. The dark sidebar, including the logo cell, is
purple-black (`#120e1a`) with a faint brand wash. The page canvas stays
neutral.
- **Quiet text.** Sentence case. A clear text ladder: primary, dim, faint.
No em dashes in UI copy.
@@ -94,8 +97,8 @@ follow the theme.
.dark {
/* Surfaces (dark: pure neutral dark gray) */
--ui-canvas: oklch(14.48% 0 0); /* sRGB 10,10,10 */
--ui-panel: oklch(19.13% 0 0); /* sRGB 20,20,20: header bar, card and table headers */
--ui-canvas: #0a0a0a; /* page and top bar */
--ui-panel: #121212; /* cards and table headers, not the top bar */
--ui-card-bg: rgb(255 255 255 / 6%);
--ui-card-border: rgb(255 255 255 / 8%);
--ui-well-bg: rgb(0 0 0 / 34%);
@@ -114,8 +117,15 @@ follow the theme.
--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);
/* Accent stays purple. Yellow is the warning color, not the accent. */
--ui-accent: #6b16ed;
--ui-accent-top: #7317ff;
--ui-accent-bottom: #5a12c7;
--ui-accent-edge: #4a0fa3;
--ui-accent-fg: #ffffff;
--ui-accent-light: #a391ff;
--ui-sidebar: #120e1a;
--ui-card: #121212;
--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);
@@ -131,16 +141,16 @@ Rules for the tokens:
- Floating surfaces (menus, popovers, tooltips, modals) must be opaque. Use
`--ui-menu-bg`, never a translucent value, or the content behind shows
through.
- Keep the dark surfaces pure neutral (chroma 0). A brand tint on the
surfaces was tried and removed.
- Keep the page canvas, cards, menus, and logs pure neutral (chroma 0). The
dark sidebar and the logo cell above it are the tinted surface.
- **Dark lightness level.** The canvas is at 14.48% and the header panel at
19.13%. 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.
- Primary buttons, focus rings, and active navigation use purple in both
themes. Warning yellow is not an accent.
## P3. Scales
@@ -182,9 +192,14 @@ Class names below are examples. Keep the values and structure.
- 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.
- 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 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.
The logo cell stays the solid sidebar color.
- Keep the chrome borders faint. A strong border on near-black looks like a
line drawn on the page.
@@ -205,21 +220,28 @@ Class names below are examples. Keep the values and structure.
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%); }
.dark .nav-tile {
border-color: rgb(163 145 255 / 0.13);
background: rgb(107 22 237 / 0.07);
color: #b9adff;
}
.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);
color: #fff;
border-color: #8a4dff;
background: linear-gradient(180deg, var(--ui-accent-top), var(--ui-accent-bottom));
--ui-depth-color: var(--ui-accent-edge);
box-shadow: 0 2px 0 var(--ui-accent-edge), 0 0 16px rgb(115 23 255 / 0.50);
}
```
- 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.
- Rows with a submenu end in a 12px chevron. In dark mode the chevron is
hidden until the row is hovered, focused, or current.
- Group rows under section labels (11px in light mode; 10.5px uppercase muted
lavender in dark mode), and separate groups with a 1px divider.
### Card (outer box, title row, well)
@@ -444,10 +466,10 @@ The interface is compact and product-focused:
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;
- the readable Coolify yellow accent in dark mode;
- solid active-item fills (neutral black/white opacity), not accent gradients;
active state is a flat selected surface with no accent rail;
- the Coolify purple brand accent in both themes (`#6b16ed`); yellow only for
real warnings, such as "Update available";
- the active sidebar item is a purple gradient tile with a soft glow. The row
itself stays transparent;
- sentence-case labels and headings;
- never use the em dash (`—`) in UI copy. Prefer a period, colon, comma, or
ASCII hyphen (`-`) for empty cells and separators.
@@ -527,19 +549,18 @@ 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:
Accent behavior uses Coolify purple in both themes. Brand tokens live in
`@theme` as `--color-brand` (`#6b16ed`), `--color-brand-hover` (`#7317ff`),
`--color-brand-press` (`#5a12c7`), `--color-brand-depth` (`#4a0fa3`), and
`--color-brand-light` (`#a391ff`). Dark mode sets `--color-accent` to
`--color-brand`. `--color-warning` stays yellow and is only for real warnings.
- **Light mode:** Coolify purple (`coollabs`) for active controls, focus,
primary actions, and navigation accents.
- **Dark mode:** Coolify yellow (`warning`) for the same states because the
original purple did not provide sufficient text and ring contrast.
Do not hard-code blue focus rings or leave yellow accent utilities active in
light mode. Primary action patterns should normally follow:
Do not hard-code blue focus rings, and do not use `dark:text-warning` as a
stand-in for the accent. Selected chips follow:
```html
bg-coollabs/10 text-coollabs ring-coollabs/25
dark:bg-warning/15 dark:text-warning dark:ring-warning/25
dark:bg-brand/15 dark:text-brand-light dark:ring-brand-light/25
```
The filled top-level action/tab treatment uses the same palette at a restrained
@@ -554,16 +575,20 @@ lift off the content:
- **Content canvas** is the base layer: `bg-app` in dark (deepest,
`--color-app` `oklch(14.48% 0 0)`, sRGB 10,10,10), `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.13% 0 0)`, sRGB 20,20,20, 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`.
- **Topbar chrome** uses the canvas color in dark (`#0a0a0a`) and `bg-white`
in light. It has no bottom border until the page scrolls under it. Scrolling
adds `is-scrolled` on `<html>` (see `layouts/app.blade.php`) and the dark bar
becomes `rgb(10 10 10 / 0.72)` with blur and a lavender hairline. In dark
mode, top-level cards and table header rows use `--color-card` (`#121212`),
not the top bar.
- **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`.
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
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;
a global rule forces that utility to the solid hairline grey.
Dark surface tokens are exact oklch equivalents of chosen sRGB steps. oklch
lightness compresses toward pure black below ~15%, so check the rendered sRGB
@@ -571,22 +596,26 @@ value. The dark ladder is `--color-app` 10, `--color-panel` 20,
`--coollabs-elevated` 22, `--coollabs-base` 28,
`--coollabs-recessed` 34 (sRGB, r=g=b), which reads as distinct surfaces.
Temperature: in both modes 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 chroma 0, so menus, listbox panels, modals, logs, and the
command palette match the canvas. Do not give one surface a cast (blue, warm,
or purple). The light page canvas uses `bg-neutral-50` (not
Temperature: the page canvas, cards, menus, listboxes, modals, logs, and the
command palette stay **pure neutral gray** (r=g=b). Do not tint those. The
dark sidebar (`--color-sidebar`, `#120e1a`) is the exception: it carries a
faint brand-purple wash. 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.
The only intentional color is the purple brand accent. Yellow is for warnings.
- **Layer cards** use the outer box and inset well from section 5.
- **Top-level pages** (Projects, Servers, and the other section roots) do not
repeat the page title in the breadcrumb when that title is the visible `h1`.
Nested pages keep the full trail (project, environment, resource).
- **Pagination** is hidden when there is only one page (both arrows disabled).
- **Project cards** spell out their counts: "1 environment · 35 resources".
- **The view switcher** shows every view. The current one is a purple pill.
- **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
well, so it is `--color-card` (`#121212`). A table (a shell whose first child is a
`border-b` header row) has a `--color-card` 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.
+270 -23
View File
@@ -39,6 +39,13 @@
--color-coollabs-100: oklch(52.34% 0.287 289.16);
--color-coollabs-200: oklch(43.76% 0.2369 289.82);
--color-coollabs-300: oklch(38.04% 0.2031 290.47);
/* coolLabs brand purple. Primary actions, focus, and active navigation
use these in both themes. Warning yellow is not an accent. */
--color-brand: #6b16ed;
--color-brand-hover: #7317ff;
--color-brand-press: #5a12c7;
--color-brand-depth: #4a0fa3;
--color-brand-light: #a391ff;
--color-coolgray-100: oklch(20.9% 0 0);
--color-coolgray-200: oklch(24.35% 0 0);
--color-coolgray-300: oklch(26.03% 0 0);
@@ -102,7 +109,33 @@
}
.dark .button:not(.button-highlighted):not(.button-error):not([isHighlighted]):not(:disabled) {
--button-depth-color: rgb(255 255 255 / 0.08);
--button-depth-color: rgb(107 22 237 / 0.35);
background-color: rgb(107 22 237 / 0.14);
border-color: rgb(163 145 255 / 0.24);
color: #ece8ff;
}
.dark .button:not(.button-highlighted):not(.button-error):not([isHighlighted]):not(:disabled):hover {
background-color: rgb(107 22 237 / 0.18);
border-color: rgb(163 145 255 / 0.34);
}
.dark .button:not(.button-highlighted):not(.button-error):not([isHighlighted]):not(:disabled) svg {
color: var(--color-brand-light);
opacity: 1;
}
.dark .button.button-highlighted:not(:disabled),
.dark .button[isHighlighted]:not(:disabled) {
background: var(--color-brand);
border-color: transparent;
color: #fff;
--button-depth-color: var(--color-brand-depth);
}
.dark .button.button-highlighted:not(:disabled):hover,
.dark .button[isHighlighted]:not(:disabled):hover {
background: var(--color-brand-hover);
}
.button.button-error:not(:disabled) {
@@ -1261,7 +1294,13 @@ html:not(.dark) .application-console-shell[data-console-theme="system"] .termina
}
.dark {
--color-accent: oklch(88.13% 0.1507 91.7);
/* Purple is the accent in dark mode too. Warning yellow stays on
--color-warning for real warnings only (update available, attention). */
--color-accent: var(--color-brand);
/* Cards sit one step above the #0a0a0a canvas. The top bar uses the canvas,
not this color. The sidebar uses --color-sidebar. */
--color-card: #121212;
--color-sidebar: #120e1a;
/* Dark surface ladder in hex (sRGB), not oklch: oklch lightness compresses
to near-black below ~15%, so oklch(15%) rendered as sRGB 11 and cards
could not lift off the content canvas (sRGB 10). These give clear,
@@ -1842,25 +1881,28 @@ 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 mode: top-level cards are #121212, a step above the canvas. The top bar
matches the canvas instead (see the brand shell below). */
.dark :is(.application-settings-section, .empty-state):not(:is(.application-settings-section, .application-settings-section-body) *) {
background: var(--color-panel);
background: var(--color-card);
box-shadow: none;
}
/* 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. */
A tile is a card without a well. A table (a shell with a header row) takes
the card color on the header row and a darker mix on the rows. */
.dark [class~="rounded-xl"][class~="dark:bg-white/[0.05]"] {
background: var(--color-panel);
background-color: var(--color-card) !important;
border-color: rgb(255 255 255 / 0.08) !important;
box-shadow: none;
}
.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);
background: color-mix(in srgb, var(--color-card) 66%, black) !important;
}
.dark [class~="rounded-xl"][class~="dark:bg-white/[0.05]"] > [class~="border-b"]:first-child {
background: var(--color-panel);
background: var(--color-card) !important;
}
/* Brief accent ring when a settings nav sub-item scrolls a section into view.
@@ -5552,10 +5594,17 @@ html:where(:not([data-nav-items="flat"])) :is(.menu-item-active, .menu-subitem-a
--button-depth-color: color-mix(in oklab, var(--color-coollabs) 45%, transparent) !important;
}
html.dark:where(:not([data-nav-items="flat"])) :is(.menu-item-active, .menu-subitem-active) .menu-item-icon {
color: var(--color-warning);
border-color: color-mix(in oklab, var(--color-warning) 40%, transparent) !important;
--button-depth-color: color-mix(in oklab, var(--color-warning) 35%, transparent) !important;
html.dark:where(:not([data-nav-items="flat"])) :is(.menu-item-active, .menu-subitem-active) .menu-item-icon:not(.text-error) {
color: #fff;
border-color: #8a4dff !important;
background: linear-gradient(180deg, var(--color-brand-hover) 0%, var(--color-brand-press) 100%) !important;
--button-depth-color: var(--color-brand-depth) !important;
box-shadow: 0 2px 0 var(--color-brand-depth), 0 0 16px rgb(115 23 255 / 0.50) !important;
}
html.dark:where(:not([data-nav-items="flat"])) :is(.menu-item-active, .menu-subitem-active):active .menu-item-icon:not(.text-error) {
transform: translateY(2px);
box-shadow: none !important;
}
/* Danger Zone icons (<x-reicon name="danger">) stay red, also when active. */
@@ -5644,9 +5693,11 @@ html:where(:not([data-nav-items="flat"])) :is(.menu-item, .menu-subitem):not(.si
box-shadow: none;
}
html.dark:where(:not([data-nav-items="flat"])) :is(.menu-item, .menu-subitem):not(.sidebar-toggle) .menu-item-icon {
border-color: oklch(100% 0 0 / 0.09);
background: oklch(100% 0 0 / 0.04);
html.dark:where(:not([data-nav-items="flat"])) .app-sidebar :is(.menu-item, .menu-subitem):not(.sidebar-toggle):not(.menu-search):not(.nav-admin):not(.menu-item-active):not(.menu-subitem-active) .menu-item-icon {
border-color: rgb(163 145 255 / 0.13);
background: rgb(107 22 237 / 0.07);
color: #b9adff;
--button-depth-color: rgb(255 255 255 / 0.05);
}
html:where(:not([data-nav-items="flat"])) :is(.menu-item, .menu-subitem):not(.sidebar-toggle):hover .menu-item-icon,
@@ -5655,10 +5706,17 @@ html:where(:not([data-nav-items="flat"])) :is(.menu-item-active, .menu-subitem-a
background: #fff;
}
html.dark:where(:not([data-nav-items="flat"])) :is(.menu-item, .menu-subitem):not(.sidebar-toggle):hover .menu-item-icon,
html.dark:where(:not([data-nav-items="flat"])) :is(.menu-item-active, .menu-subitem-active) .menu-item-icon {
border-color: oklch(100% 0 0 / 0.18);
background: oklch(100% 0 0 / 0.08);
html.dark:where(:not([data-nav-items="flat"])) :is(.menu-item, .menu-subitem):not(.sidebar-toggle):not(.menu-item-active):not(.menu-subitem-active):hover .menu-item-icon {
border-color: rgb(255 255 255 / 0.14);
background: rgb(255 255 255 / 0.06);
color: #fff;
}
/* Admin stays a neutral tile unless it is the current page. */
html.dark .app-sidebar .nav-admin:not(.menu-item-active) .menu-item-icon {
color: oklch(72% 0 0);
background: rgb(255 255 255 / 0.03);
border-color: rgb(255 255 255 / 0.07);
}
/* Search keeps its original input look while the sidebar is expanded (always on
@@ -5948,6 +6006,188 @@ html.dark .canvas-zoom-max {
}
}
/* Brand shell (dark): black canvas and top bar, purple-black sidebar, scroll hairline.
Light mode keeps the neutral shell. Floating navigation keeps its own frame. */
html.dark .app-sidebar nav.sidebar-nav,
html.dark .mobile-sidebar-sheet [role="dialog"] {
background-color: var(--color-sidebar);
background-image: linear-gradient(180deg, rgb(107 22 237 / 0.09), transparent 520px);
border-right-color: rgb(163 145 255 / 0.10);
--color-nav-text: oklch(80% 0.02 300);
--color-nav-muted: oklch(58% 0.04 300);
}
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%);
border-right-color: rgb(163 145 255 / 0.10);
}
html.dark .app-sidebar .sidebar-footer {
background-color: transparent;
}
html.dark .app-sidebar .menu-search {
background-color: rgb(163 145 255 / 0.06) !important;
box-shadow: inset 0 0 0 1px rgb(163 145 255 / 0.10) !important;
}
html.dark .app-sidebar .menu-search:hover {
background-color: rgb(163 145 255 / 0.10) !important;
}
html.dark .app-sidebar .menu-search kbd {
border-color: rgb(163 145 255 / 0.18) !important;
}
html.dark .app-sidebar nav:not(.sidebar-collapsed) .nav-section {
font-family: var(--font-mono);
font-size: 10.5px;
font-weight: 500;
letter-spacing: 0.08em;
text-transform: uppercase;
color: rgb(163 145 255 / 0.55);
opacity: 1;
}
html.dark .app-sidebar .nav-chevron {
transition: opacity 120ms ease;
}
html.dark .app-sidebar .menu-item:not(:hover):not(:focus-visible):not(.menu-item-active) > .nav-chevron {
opacity: 0;
}
html.dark:not([data-nav-style="floating"]) .app-topbar {
background-color: #0a0a0a;
backdrop-filter: none;
transition: background-color 180ms ease;
}
html.dark:not([data-nav-style="floating"]) .app-topbar > div:not(.app-topbar-brand) {
border-bottom-color: transparent;
transition: border-color 180ms ease, box-shadow 180ms ease, background-color 180ms ease;
}
html.dark.is-scrolled:not([data-nav-style="floating"]) .app-topbar {
background-color: rgb(10 10 10 / 0.72);
backdrop-filter: blur(12px) saturate(140%);
}
html.dark.is-scrolled:not([data-nav-style="floating"]) .app-topbar > .app-topbar-brand {
background-color: var(--color-sidebar);
}
html.dark.is-scrolled:not([data-nav-style="floating"]) .app-topbar > div:not(.app-topbar-brand) {
border-bottom-color: rgb(163 145 255 / 0.12);
box-shadow: 0 10px 30px -14px rgb(0 0 0 / 0.95), 0 1px 0 rgb(107 22 237 / 0.06);
}
html.dark .view-toggle {
gap: 2px;
padding: 2px;
background-color: rgb(107 22 237 / 0.06);
border-color: rgb(163 145 255 / 0.16);
}
html.dark .view-toggle > :is(a, button) {
color: oklch(80% 0.02 300);
}
html.dark .view-toggle > :is(a, button) svg {
color: var(--color-brand-light);
}
html.dark .view-toggle > :is(a, button):not(.is-selected):hover {
background-color: rgb(107 22 237 / 0.14);
color: #fff;
}
html.dark .view-toggle > .is-selected {
background: linear-gradient(180deg, var(--color-brand-hover), var(--color-brand-press));
color: #fff;
box-shadow: 0 1px 0 var(--color-brand-depth), inset 0 0 0 1px #8a4dff;
}
html.dark .view-toggle > .is-selected svg {
color: #fff;
}
html.dark .view-toggle .bg-coollabs {
background-color: rgb(163 145 255 / 0.18);
color: #d9d0ff;
}
html.dark .resource-glyph {
border-color: rgb(163 145 255 / 0.20);
background: rgb(107 22 237 / 0.10);
color: var(--color-brand-light);
}
html.dark .card-icon-button {
color: var(--color-brand-light);
}
html.dark .card-icon-button:hover {
background-color: rgb(107 22 237 / 0.16);
color: #fff;
}
html.dark .app-main :is(article.group, a.group).rounded-xl {
background-color: var(--color-card) !important;
border-color: rgb(255 255 255 / 0.08) !important;
box-shadow: none;
}
html.dark .app-main :is(article.group, a.group).rounded-xl:hover {
border-color: rgb(163 145 255 / 0.30) !important;
box-shadow: none;
}
.project-card-stats {
color: oklch(40% 0 0);
font-size: 11.5px;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.project-card-stats b {
color: oklch(20% 0 0);
font-weight: 600;
}
.project-card-stats-dot {
margin-inline: 5px;
color: oklch(70% 0 0);
}
html.dark .project-card-stats {
color: oklch(72% 0 0);
}
html.dark .project-card-stats b {
color: #f2f2f2;
}
html.dark .project-card-stats-dot {
color: oklch(45% 0 0);
}
.dark .application-settings-form :is(.input, select, textarea):focus,
.dark .application-settings-form :is(.input, select, textarea):focus-visible,
.dark .application-settings-workspace :is(.input, select, textarea):focus,
.dark .application-settings-workspace :is(.input, select, textarea):focus-visible {
border-color: var(--color-brand);
box-shadow: 0 0 0 3px rgb(107 22 237 / 0.25) !important;
}
/* One page of results: the bar is noise. !important beats the flex utility and Alpine x-show. */
.pager[hidden] {
display: none !important;
}
/* Dark mode (both navigation styles): the floating dotted backdrop shows through
see-through surfaces (dark:bg-white/[x] with or without `!`, .button, …). Give the
outermost ones the same color as an opaque mix with the app background, like the
@@ -5979,7 +6219,8 @@ html.dark .canvas-zoom-max {
html.dark .app-main [class*="dark:bg-white/[0.09]"]:not(:is([class*="dark:bg-"]:not(.app-main), .button, .view-toggle, .application-settings-section, .application-settings-section-body, .box, .coolbox, .data-table, .resource-view, .empty-state, .loading-state-card, .runtime-log-shell, .logs-viewer) *) { background-color: color-mix(in oklab, #fff 9%, var(--color-app)) !important; }
html.dark .app-main [class*="dark:bg-white/[0.14]"]:not(:is([class*="dark:bg-"]:not(.app-main), .button, .view-toggle, .application-settings-section, .application-settings-section-body, .box, .coolbox, .data-table, .resource-view, .empty-state, .loading-state-card, .runtime-log-shell, .logs-viewer) *) { background-color: color-mix(in oklab, #fff 14%, var(--color-app)) !important; }
html.dark .app-main [class*="dark:bg-white/[0.1]"]:not(:is([class*="dark:bg-"]:not(.app-main), .button, .view-toggle, .application-settings-section, .application-settings-section-body, .box, .coolbox, .data-table, .resource-view, .empty-state, .loading-state-card, .runtime-log-shell, .logs-viewer) *) { background-color: color-mix(in oklab, #fff 10%, var(--color-app)) !important; }
html.dark .app-main :is(.button, .view-toggle):not(:is([class*="dark:bg-"]:not(.app-main), .button, .view-toggle, .application-settings-section, .application-settings-section-body, .box, .coolbox, .data-table, .resource-view, .empty-state, .loading-state-card, .runtime-log-shell, .logs-viewer) *) { background-color: color-mix(in oklab, #fff 6%, var(--color-app)) !important; }
html.dark .app-main .button:not(.button-highlighted):not(.button-error):not([isHighlighted]):not(:disabled):not(:is([class*="dark:bg-"]:not(.app-main), .button, .view-toggle, .application-settings-section, .application-settings-section-body, .box, .coolbox, .data-table, .resource-view, .empty-state, .loading-state-card, .runtime-log-shell, .logs-viewer) *) { background-color: rgb(107 22 237 / 0.14) !important; border-color: rgb(163 145 255 / 0.24) !important; color: #ece8ff !important; }
html.dark .app-main .view-toggle:not(:is([class*="dark:bg-"]:not(.app-main), .button, .view-toggle, .application-settings-section, .application-settings-section-body, .box, .coolbox, .data-table, .resource-view, .empty-state, .loading-state-card, .runtime-log-shell, .logs-viewer) *) { background-color: rgb(107 22 237 / 0.06) !important; border-color: rgb(163 145 255 / 0.16) !important; }
html.dark .app-main [class*="dark:hover:bg-white/10"]:not(:is([class*="dark:bg-"]:not(.app-main), .button, .view-toggle, .application-settings-section, .application-settings-section-body, .box, .coolbox, .data-table, .resource-view, .empty-state, .loading-state-card, .runtime-log-shell, .logs-viewer) *):hover { background-color: color-mix(in oklab, #fff 10%, var(--color-app)) !important; }
html.dark .app-main [class*="dark:hover:bg-white/[0.025]"]:not(:is([class*="dark:bg-"]:not(.app-main), .button, .view-toggle, .application-settings-section, .application-settings-section-body, .box, .coolbox, .data-table, .resource-view, .empty-state, .loading-state-card, .runtime-log-shell, .logs-viewer) *):hover { background-color: color-mix(in oklab, #fff 2.5%, var(--color-app)) !important; }
html.dark .app-main [class*="dark:hover:bg-white/[0.035]"]:not(:is([class*="dark:bg-"]:not(.app-main), .button, .view-toggle, .application-settings-section, .application-settings-section-body, .box, .coolbox, .data-table, .resource-view, .empty-state, .loading-state-card, .runtime-log-shell, .logs-viewer) *):hover { background-color: color-mix(in oklab, #fff 3.5%, var(--color-app)) !important; }
@@ -5991,7 +6232,13 @@ html.dark .canvas-zoom-max {
html.dark .app-main [class*="dark:hover:bg-white/[0.08]"]:not(:is([class*="dark:bg-"]:not(.app-main), .button, .view-toggle, .application-settings-section, .application-settings-section-body, .box, .coolbox, .data-table, .resource-view, .empty-state, .loading-state-card, .runtime-log-shell, .logs-viewer) *):hover { background-color: color-mix(in oklab, #fff 8%, var(--color-app)) !important; }
html.dark .app-main [class*="dark:hover:bg-white/[0.12]"]:not(:is([class*="dark:bg-"]:not(.app-main), .button, .view-toggle, .application-settings-section, .application-settings-section-body, .box, .coolbox, .data-table, .resource-view, .empty-state, .loading-state-card, .runtime-log-shell, .logs-viewer) *):hover { background-color: color-mix(in oklab, #fff 12%, var(--color-app)) !important; }
html.dark .app-main [class*="dark:hover:bg-white/[0.1]"]:not(:is([class*="dark:bg-"]:not(.app-main), .button, .view-toggle, .application-settings-section, .application-settings-section-body, .box, .coolbox, .data-table, .resource-view, .empty-state, .loading-state-card, .runtime-log-shell, .logs-viewer) *):hover { background-color: color-mix(in oklab, #fff 10%, var(--color-app)) !important; }
html.dark .app-main .button:not(:disabled):not(:is([class*="dark:bg-"]:not(.app-main), .button, .view-toggle, .application-settings-section, .application-settings-section-body, .box, .coolbox, .data-table, .resource-view, .empty-state, .loading-state-card, .runtime-log-shell, .logs-viewer) *):hover { background-color: color-mix(in oklab, #fff 10%, var(--color-app)) !important; }
html.dark .app-main .button:not(.button-highlighted):not(.button-error):not([isHighlighted]):not(:disabled):not(:is([class*="dark:bg-"]:not(.app-main), .button, .view-toggle, .application-settings-section, .application-settings-section-body, .box, .coolbox, .data-table, .resource-view, .empty-state, .loading-state-card, .runtime-log-shell, .logs-viewer) *):hover { background-color: rgb(107 22 237 / 0.18) !important; border-color: rgb(163 145 255 / 0.34) !important; }
/* Collection shells are #121212. Same selector as the 5% mix, plus .rounded-xl, so it wins. */
html.dark .app-main [class*="dark:bg-white/[0.05]"].rounded-xl:not(:is([class*="dark:bg-"]:not(.app-main), .button, .view-toggle, .application-settings-section, .application-settings-section-body, .box, .coolbox, .data-table, .resource-view, .empty-state, .loading-state-card, .runtime-log-shell, .logs-viewer) *) {
background-color: #121212 !important;
border-color: rgb(255 255 255 / 0.08) !important;
}
}
/* Resource menu in the collapsed main sidebar: icons only. */
+4 -4
View File
@@ -142,7 +142,7 @@
}
@utility loading-indicator {
@apply text-coollabs dark:text-warning;
@apply text-coollabs dark:text-brand-light;
}
/* Compact icon-only control (gear, chevrons, etc.) */
@@ -162,7 +162,7 @@
/* Active resource tab (used with aria-current="page") */
@utility app-tab-active {
@apply bg-coollabs/10 text-coollabs shadow-sm ring-1 ring-coollabs/25 hover:bg-coollabs/15 dark:bg-warning/15 dark:text-warning dark:ring-warning/25 dark:hover:bg-warning/20;
@apply bg-coollabs/10 text-coollabs shadow-sm ring-1 ring-coollabs/25 hover:bg-coollabs/15 dark:bg-brand/15 dark:text-brand-light dark:ring-brand-light/25 dark:hover:bg-brand/20;
}
@utility auth-tooltip {
@@ -271,7 +271,7 @@
}
@utility heading-item-active {
@apply text-black rounded-sm dark:bg-coolgray-200 dark:text-warning;
@apply text-black rounded-sm dark:bg-coolgray-200 dark:text-brand-light;
}
@utility icon {
@@ -299,7 +299,7 @@
}
@utility kbd-custom {
@apply px-2 text-xs rounded-sm border border-dashed border-neutral-700 dark:text-warning;
@apply px-2 text-xs rounded-sm border border-dashed border-neutral-700 dark:text-brand-light;
}
@utility box {
@@ -35,7 +35,7 @@
@if ($context === 'service-schedule')
<button type="button" @click="activeSection = '{{ $item['key'] }}'"
:class="activeSection === '{{ $item['key'] }}'
? 'bg-coollabs/10 text-coollabs ring-1 ring-coollabs/25 dark:bg-warning/15 dark:text-warning dark:ring-warning/25'
? 'bg-coollabs/10 text-coollabs ring-1 ring-coollabs/25 dark:bg-brand/15 dark:text-brand-light dark:ring-brand-light/25'
: 'text-neutral-500 hover:bg-black/5 hover:text-neutral-900 dark:text-fg-dim dark:hover:bg-white/[0.06] dark:hover:text-fg'"
class="inline-flex h-8 shrink-0 cursor-pointer items-center rounded-md px-3 text-[13px] font-medium transition-colors">
{{ $item['label'] }}
@@ -43,7 +43,7 @@
@else
<a @class([
'inline-flex h-8 shrink-0 items-center rounded-md px-3 text-[13px] font-medium transition-colors',
'bg-coollabs/10 text-coollabs ring-1 ring-coollabs/25 dark:bg-warning/15 dark:text-warning dark:ring-warning/25' => $section === $item['key'],
'bg-coollabs/10 text-coollabs ring-1 ring-coollabs/25 dark:bg-brand/15 dark:text-brand-light dark:ring-brand-light/25' => $section === $item['key'],
'text-neutral-500 hover:bg-black/5 hover:text-neutral-900 dark:text-fg-dim dark:hover:bg-white/[0.06] dark:hover:text-fg' => $section !== $item['key'],
])
{{ wireNavigate() }} href="{{ route($routes[$item['key']], $parameters) }}">
@@ -1,6 +1,6 @@
@props(['listUrl' => null, 'canvasUrl' => null, 'onCanvas' => false, 'views' => null])
@php
$idle = 'text-neutral-400 hover:bg-neutral-100 hover:text-black dark:text-fg dark:hover:bg-white/[0.06] dark:hover:text-fg';
$idle = 'text-neutral-400 hover:bg-neutral-100 hover:text-black dark:hover:text-white';
$canvasItem = 'relative flex h-7.5 shrink-0 items-center gap-1.5 whitespace-nowrap rounded-md px-2.5 text-[12px] font-medium transition-colors';
$views ??= ['table' => ['Table view', 'unordered-list'], 'grid' => ['Grid view', 'grid']];
// One list view (no table / grid choice): nothing to switch to on the list page.
@@ -22,11 +22,14 @@
List view
</a>
@else
{{-- Only the views you can switch to, with their names; the current one is not shown. --}}
{{-- Every view stays visible. The current one is the selected pill. --}}
@unless ($singleList)
@foreach ($views as $mode => [$label, $icon])
<button type="button" x-on:click="setViewMode(@js($mode))" x-show="viewMode !== @js($mode)" x-cloak
class="{{ $canvasItem }} {{ $idle }}" title="{{ $label }}">
<button type="button" x-on:click="setViewMode(@js($mode))"
class="{{ $canvasItem }}"
:class="viewMode === @js($mode) ? 'is-selected' : @js($idle)"
:aria-pressed="(viewMode === @js($mode)).toString()"
title="{{ $label }}">
<x-reicon :name="$icon" class="size-3.5" />
{{ $label }}
</button>
@@ -9,7 +9,7 @@
'nextDisabled' => 'page >= totalPages',
])
<footer {{ $attributes->class('flex min-h-11 items-center justify-between border-t border-neutral-200 px-4 text-[11px] text-neutral-500 dark:border-white/[0.08] dark:text-fg-faint') }}>
<footer x-bind:hidden="Number(totalPages) <= 1" {{ $attributes->class('pager flex min-h-11 items-center justify-between border-t border-neutral-200 px-4 text-[11px] text-neutral-500 dark:border-white/[0.08] dark:text-fg-faint') }}>
<div class="flex items-center gap-3">
<span class="inline-flex h-7 items-center whitespace-nowrap tabular-nums" x-text="{{ $summary }}"></span>
<x-page-size-select :model="$pageSizeModel" :storage-key="$storageKey" :options="$options" />
@@ -120,7 +120,7 @@
@foreach ($items as $item)
<a @class([
'app-tab shrink-0',
'bg-coollabs/10 text-coollabs shadow-sm ring-1 ring-coollabs/25 hover:bg-coollabs/15 dark:bg-warning/15 dark:text-warning dark:ring-warning/25 dark:hover:bg-warning/20' => $item['active'],
'bg-coollabs/10 text-coollabs shadow-sm ring-1 ring-coollabs/25 hover:bg-coollabs/15 dark:bg-brand/15 dark:text-brand-light dark:ring-brand-light/25 dark:hover:bg-brand/20' => $item['active'],
])
{{ wireNavigate() }} href="{{ route($item['route'], $parameters) }}">
@if ($item['icon'] ?? null)
@@ -8,7 +8,7 @@
// Persistent affordance shown while a deploy/start is running. Either re-opens
// the in-page live-log dialog (services/databases, via $wire) or links to the
// running deployment's log page (applications, via href) so the log is never lost.
$deployingIndicatorClasses = 'inline-flex shrink-0 items-center gap-1.5 rounded-md px-2 py-1 text-[11px] font-medium ring-1 transition-colors bg-coollabs/10 text-coollabs ring-coollabs/25 hover:bg-coollabs/15 hover:no-underline dark:bg-warning/15 dark:text-warning dark:ring-warning/25 dark:hover:bg-warning/20';
$deployingIndicatorClasses = 'inline-flex shrink-0 items-center gap-1.5 rounded-md px-2 py-1 text-[11px] font-medium ring-1 transition-colors bg-coollabs/10 text-coollabs ring-coollabs/25 hover:bg-coollabs/15 hover:no-underline dark:bg-brand/15 dark:text-brand-light dark:ring-brand-light/25 dark:hover:bg-brand/20';
@endphp
@if ($href)
+4 -4
View File
@@ -271,7 +271,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-coollabs dark:text-fg-faint" ::class="collapsed && 'lg:hidden'" />
<x-reicon name="chevron-down" class="nav-chevron size-3 -rotate-90 text-coollabs dark:text-fg-faint" ::class="collapsed && 'lg:hidden'" />
</button>
@else
<a title="{{ $item['label'] }}" href="{{ $item['href'] }}"
@@ -294,9 +294,9 @@
<li x-show="!submenu" @if ($activeSubmenu ?? null) x-cloak @endif @class(['xl:hidden' => $navContext ?? []])
:class="{ 'xl:hidden': navLevel !== -1 }">
<a title="Admin" {{ wireNavigate() }}
class="{{ request()->is('admin') ? 'menu-item-active menu-item' : 'menu-item' }}"
class="nav-admin {{ request()->is('admin') ? 'menu-item-active menu-item' : 'menu-item' }}"
:class="collapsed && 'lg:justify-center lg:px-0'" href="{{ route('admin.index') }}">
<x-reicon name="fire" class="menu-item-icon text-pink-500" />
<x-reicon name="fire" class="menu-item-icon" />
<span class="menu-item-label" :class="collapsed && 'lg:hidden'">Admin</span>
</a>
</li>
@@ -324,7 +324,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-neutral-50 px-2 py-2 dark:bg-app 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-transparent 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"
@@ -13,12 +13,14 @@
</button>
</div>
{{-- Only the view you can switch to, with its name (as components/canvas-view-toggle.blade.php). --}}
{{-- Every view stays visible. The current one is the selected pill. --}}
<div class="view-toggle w-fit">
@foreach (['list' => ['List view', 'unordered-list'], 'grid' => ['Grid view', 'grid']] as $mode => [$viewLabel, $icon])
<button type="button" @click="viewMode = '{{ $mode }}'; localStorage.setItem('{{ $storageKey }}', '{{ $mode }}')"
x-show="viewMode !== '{{ $mode }}'" x-cloak title="{{ $viewLabel }}"
class="relative flex h-7.5 items-center gap-1.5 rounded-md px-2.5 text-[12px] font-medium text-neutral-400 transition-colors hover:bg-neutral-100 hover:text-black dark:text-fg dark:hover:bg-white/[0.06] dark:hover:text-fg">
title="{{ $viewLabel }}"
class="relative flex h-7.5 items-center gap-1.5 rounded-md px-2.5 text-[12px] font-medium transition-colors"
:class="viewMode === '{{ $mode }}' ? 'is-selected' : 'text-neutral-400 hover:bg-neutral-100 hover:text-black dark:hover:text-white'"
:aria-pressed="(viewMode === '{{ $mode }}').toString()">
<x-reicon :name="$icon" class="size-3.5" />
{{ $viewLabel }}
</button>
@@ -20,7 +20,7 @@
$hasLoading = filled($wireTarget);
@endphp
<footer {{ $attributes->class('flex min-h-11 items-center justify-between border-t border-neutral-200 px-4 text-[11px] text-neutral-500 dark:border-white/[0.08] dark:text-fg-faint') }}>
<footer @if ((int) $lastPage <= 1) hidden @endif {{ $attributes->class('pager flex min-h-11 items-center justify-between border-t border-neutral-200 px-4 text-[11px] text-neutral-500 dark:border-white/[0.08] dark:text-fg-faint') }}>
<div class="flex items-center gap-3">
<span class="inline-flex h-7 items-center whitespace-nowrap tabular-nums">{{ $from }}–{{ $to }} of {{ $total }}</span>
@isset($pageSize)
@@ -104,15 +104,27 @@
default => null,
};
@endphp
<div class="flex min-w-0 items-center gap-0.5 text-[13px]">
<div class="flex min-w-0 items-center gap-0.5 text-[13px]" data-page-context="{{ $dashboardContext }}"
x-data="{
titleRepeats: false,
syncTitle() {
const expected = this.$el.dataset.pageContext || '';
const heading = document.querySelector('.app-main h1');
const title = heading && heading.offsetHeight > 0 && heading.getBoundingClientRect().top < 140
? heading.textContent.trim()
: '';
this.titleRepeats = expected !== '' && title === expected;
},
}"
x-init="syncTitle()" @livewire:navigated.window="$nextTick(() => syncTitle())">
{{-- Team --}}
<div class="shrink-0" x-data="{ collapsed: false }">
<livewire:switch-team />
</div>
@if (!$currentProject && $dashboardContext)
<span class="shrink-0 px-0.5 text-neutral-300 dark:text-fg-faint">/</span>
<span
<span x-show="!titleRepeats" x-cloak class="shrink-0 px-0.5 text-neutral-300 dark:text-fg-faint">/</span>
<span x-show="!titleRepeats" x-cloak
class="flex h-8 min-w-0 items-center truncate px-2 font-semibold text-black opacity-70 dark:text-fg">{{ $dashboardContext }}</span>
@endif
+8
View File
@@ -19,6 +19,14 @@
this.sidebarReady = true;
});
});
if (!window.__coolifyMarkScrolled) {
window.__coolifyMarkScrolled = () => {
document.documentElement.classList.toggle('is-scrolled', window.scrollY > 2);
};
window.addEventListener('scroll', window.__coolifyMarkScrolled, { passive: true });
document.addEventListener('livewire:navigated', () => requestAnimationFrame(window.__coolifyMarkScrolled));
}
window.__coolifyMarkScrolled();
},
toggleSidebar() {
this.collapsed = !this.collapsed;
+12 -17
View File
@@ -38,7 +38,7 @@
default => ['Ready', 'success'],
}];
});
$iconButton = 'flex size-6.5 items-center justify-center rounded-md text-neutral-400 transition-colors hover:bg-neutral-100 hover:text-black dark:text-fg-faint dark:hover:bg-white/[0.06] dark:hover:text-fg';
$iconButton = 'card-icon-button flex size-6.5 items-center justify-center rounded-md text-neutral-400 transition-colors hover:bg-neutral-100 hover:text-black';
// Same look as $iconButton, sized to the 20px section title it sits next to.
$titleIconButton = 'flex size-5 items-center justify-center rounded-md text-neutral-400 transition-colors hover:bg-neutral-100 hover:text-black dark:text-fg-faint dark:hover:bg-white/[0.06] dark:hover:text-fg';
$tableShell = 'overflow-x-auto rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.05]';
@@ -127,7 +127,7 @@
<div class="flex min-w-0 items-start gap-3">
<div
class="flex size-8 shrink-0 items-center justify-center rounded-lg border border-neutral-200 bg-neutral-50 text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.04] dark:text-fg-dim">
class="resource-glyph flex size-8 shrink-0 items-center justify-center rounded-lg border border-neutral-200 bg-neutral-50 text-neutral-500">
@if ($project->icon_path)
<img src="{{ project_icon_url($project) }}"
alt="{{ $project->name }} icon"
@@ -150,18 +150,13 @@
</div>
<div class="mt-auto flex items-center justify-between gap-3 border-t border-neutral-100 pt-2.5 dark:border-white/[0.06]">
<div class="relative z-10 flex min-w-0 items-center gap-3 text-[11px] font-medium text-neutral-500 dark:text-fg-dim">
<span class="inline-flex items-center gap-1" data-tooltip="Environments"
aria-label="Environments">
<x-reicon name="layers" class="size-3.5 text-neutral-400 dark:text-fg-faint" />
{{ $project->environments->count() }}
</span>
<span class="inline-flex items-center gap-1" data-tooltip="Resources"
aria-label="Resources">
<x-reicon name="grid" class="size-3.5 text-neutral-400 dark:text-fg-faint" />
{{ $resourceCount }}
</span>
</div>
<p class="project-card-stats relative z-10 min-w-0">
<b>{{ $project->environments->count() }}</b>
{{ $project->environments->count() === 1 ? 'environment' : 'environments' }}
<span class="project-card-stats-dot" aria-hidden="true">&middot;</span>
<b>{{ $resourceCount }}</b>
{{ $resourceCount === 1 ? 'resource' : 'resources' }}
</p>
<div class="relative z-10 flex shrink-0 items-center gap-0.5">
@if ($firstEnvironment)
@@ -210,7 +205,7 @@
class="absolute inset-0" aria-label="Open {{ $project->name }}"></a>
<div class="flex min-w-0 items-center gap-3">
<div
class="flex size-8 shrink-0 items-center justify-center rounded-lg border border-neutral-200 bg-neutral-50 text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.035] dark:text-fg-dim">
class="resource-glyph flex size-8 shrink-0 items-center justify-center rounded-lg border border-neutral-200 bg-neutral-50 text-neutral-500">
@if ($project->icon_path)
<img src="{{ project_icon_url($project) }}" alt="{{ $project->name }} icon"
class="h-full w-full rounded-lg object-cover">
@@ -336,7 +331,7 @@
<div class="relative z-10 flex min-w-0 items-start gap-3">
<div
class="flex size-8 shrink-0 items-center justify-center rounded-lg border border-neutral-200 bg-neutral-50 text-neutral-500 dark:border-white/[0.1] dark:bg-white/[0.04] dark:text-fg-dim">
class="resource-glyph flex size-8 shrink-0 items-center justify-center rounded-lg border border-neutral-200 bg-neutral-50 text-neutral-500">
<x-reicon name="servers" class="size-4" />
</div>
<div class="min-w-0 flex-1 self-center">
@@ -380,7 +375,7 @@
class="grid grid-cols-[minmax(0,1fr)_9.5rem] gap-4 @2xl:min-w-[480px] @2xl:grid-cols-[minmax(0,1fr)_minmax(0,1.5fr)_9.5rem] hover:no-underline {{ $tableRow }}">
<div class="flex min-w-0 items-center gap-3">
<div
class="flex size-8 shrink-0 items-center justify-center rounded-lg border border-neutral-200 bg-neutral-50 text-neutral-500 dark:border-white/[0.1] dark:bg-white/[0.035] dark:text-fg-dim">
class="resource-glyph flex size-8 shrink-0 items-center justify-center rounded-lg border border-neutral-200 bg-neutral-50 text-neutral-500">
<x-reicon name="servers" class="size-4" />
</div>
<p class="truncate text-[13px] font-semibold text-black dark:text-fg">
@@ -89,7 +89,7 @@
:aria-label="`Open ${project.name}`"></a>
<div class="flex items-start gap-3">
<div
class="flex size-8 shrink-0 items-center justify-center rounded-lg border border-neutral-200 bg-neutral-50 text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.04] dark:text-fg-dim">
class="resource-glyph flex size-8 shrink-0 items-center justify-center rounded-lg border border-neutral-200 bg-neutral-50 text-neutral-500">
<template x-if="project.iconUrl">
<img :src="project.iconUrl" :alt="`${project.name} icon`" class="h-full w-full rounded-lg object-cover">
</template>
@@ -108,28 +108,23 @@
</div>
<div class="mt-auto flex items-center justify-between gap-3 border-t border-neutral-100 pt-2.5 dark:border-white/[0.06]">
<div class="relative z-10 flex min-w-0 items-center gap-3 text-[11px] font-medium text-neutral-500 dark:text-fg-dim">
<span class="inline-flex items-center gap-1" data-tooltip="Environments"
aria-label="Environments">
<x-reicon name="layers" class="size-3.5 text-neutral-400 dark:text-fg-faint" />
<span x-text="project.environmentCount"></span>
</span>
<span class="inline-flex items-center gap-1" data-tooltip="Resources"
aria-label="Resources">
<x-reicon name="grid" class="size-3.5 text-neutral-400 dark:text-fg-faint" />
<span x-text="project.resourceCount"></span>
</span>
</div>
<p class="project-card-stats relative z-10 min-w-0">
<b x-text="project.environmentCount"></b>
<span x-text="Number(project.environmentCount) === 1 ? 'environment' : 'environments'"></span>
<span class="project-card-stats-dot" aria-hidden="true">&middot;</span>
<b x-text="project.resourceCount"></b>
<span x-text="Number(project.resourceCount) === 1 ? 'resource' : 'resources'"></span>
</p>
<div class="relative z-10 flex shrink-0 items-center gap-0.5">
<a x-show="project.addResourceHref" :href="project.addResourceHref"
{{ wireNavigate() }}
class="flex size-7.5 items-center justify-center rounded-md text-neutral-400 transition-colors hover:bg-neutral-100 hover:text-black dark:text-fg-faint dark:hover:bg-white/[0.06] dark:hover:text-fg"
class="card-icon-button flex size-7.5 items-center justify-center rounded-md text-neutral-400 transition-colors hover:bg-neutral-100 hover:text-black"
title="Add resource" :aria-label="`Add resource to ${project.name}`">
<x-reicon name="plus" class="size-3" />
</a>
<a x-show="project.settingsHref" :href="project.settingsHref" {{ wireNavigate() }}
class="flex size-7.5 items-center justify-center rounded-md text-neutral-400 transition-colors hover:bg-neutral-100 hover:text-black dark:text-fg-faint dark:hover:bg-white/[0.06] dark:hover:text-fg"
class="card-icon-button flex size-7.5 items-center justify-center rounded-md text-neutral-400 transition-colors hover:bg-neutral-100 hover:text-black"
title="Project settings" :aria-label="`Open settings for ${project.name}`">
<x-reicon name="settings" class="size-3" />
</a>
@@ -159,7 +154,7 @@
class="projects-table-grid group min-h-14 items-center border-b border-neutral-200 px-4 py-2.5 transition-colors last:border-b-0 hover:bg-neutral-50 dark:border-white/[0.07] dark:hover:bg-white/[0.025]">
<div class="flex min-w-0 items-center gap-3">
<div
class="flex size-8 shrink-0 items-center justify-center rounded-lg border border-neutral-200 bg-neutral-50 text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.035] dark:text-fg-dim">
class="resource-glyph flex size-8 shrink-0 items-center justify-center rounded-lg border border-neutral-200 bg-neutral-50 text-neutral-500">
<template x-if="project.iconUrl">
<img :src="project.iconUrl" :alt="`${project.name} icon`" class="h-full w-full rounded-lg object-cover">
</template>
@@ -4,7 +4,7 @@
@foreach (['general' => 'General', 'executions' => 'Executions', ...(auth()->user()->can('update', $resource) ? ['danger' => 'Danger Zone'] : [])] as $sectionKey => $sectionLabel)
<button type="button" @click="activeSection = '{{ $sectionKey }}'"
:class="activeSection === '{{ $sectionKey }}'
? 'bg-coollabs/10 text-coollabs ring-1 ring-coollabs/25 dark:bg-warning/15 dark:text-warning dark:ring-warning/25'
? 'bg-coollabs/10 text-coollabs ring-1 ring-coollabs/25 dark:bg-brand/15 dark:text-brand-light dark:ring-brand-light/25'
: 'text-neutral-500 hover:bg-black/5 hover:text-neutral-900 dark:text-fg-dim dark:hover:bg-white/[0.06] dark:hover:text-fg'"
class="inline-flex h-8 shrink-0 cursor-pointer items-center rounded-md px-3 text-[13px] font-medium transition-colors">
{{ $sectionLabel }}
@@ -137,7 +137,7 @@
<div class="relative z-10 flex items-start gap-3">
<div
class="flex size-8 shrink-0 items-center justify-center rounded-lg border border-neutral-200 bg-neutral-50 text-neutral-500 dark:border-white/[0.1] dark:bg-white/[0.04] dark:text-fg-dim">
class="resource-glyph flex size-8 shrink-0 items-center justify-center rounded-lg border border-neutral-200 bg-neutral-50 text-neutral-500">
<x-reicon name="servers" class="size-4" />
</div>
<div class="min-w-0 flex-1 self-center">
@@ -199,7 +199,7 @@
])>
<div class="flex min-w-0 items-center gap-3">
<div
class="flex size-8 shrink-0 items-center justify-center rounded-lg border border-neutral-200 bg-neutral-50 text-neutral-500 dark:border-white/[0.1] dark:bg-white/[0.035] dark:text-fg-dim">
class="resource-glyph flex size-8 shrink-0 items-center justify-center rounded-lg border border-neutral-200 bg-neutral-50 text-neutral-500">
<x-reicon name="servers" class="size-4" />
</div>
<div class="min-w-0">
@@ -7,14 +7,14 @@
<button type="button" x-on:click="selected = 'monthly'"
class="app-tab h-6! px-2.5!"
:class="selected === 'monthly'
? 'bg-coollabs/10 text-coollabs ring-1 ring-coollabs/25 dark:bg-warning/15 dark:text-warning dark:ring-warning/25'
? 'bg-coollabs/10 text-coollabs ring-1 ring-coollabs/25 dark:bg-brand/15 dark:text-brand-light dark:ring-brand-light/25'
: ''">
Monthly
</button>
<button type="button" x-on:click="selected = 'yearly'"
class="app-tab h-6! px-2.5!"
:class="selected === 'yearly'
? 'bg-coollabs/10 text-coollabs ring-1 ring-coollabs/25 dark:bg-warning/15 dark:text-warning dark:ring-warning/25'
? 'bg-coollabs/10 text-coollabs ring-1 ring-coollabs/25 dark:bg-brand/15 dark:text-brand-light dark:ring-brand-light/25'
: ''">
Yearly
</button>
@@ -172,7 +172,8 @@ test('root user sees the admin link in the navbar', function () {
expect($navbar)
->toContain('href="'.route('admin.index').'"')
->toContain('title="Admin"')
->toContain('text-pink-500');
->toContain('nav-admin')
->not->toContain('text-pink-500');
});
test('non-root user does not see the admin link in the navbar', function () {