mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-04 10:42:23 +00:00
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:
1 parent
91ea7d851e
commit
8df39bc637
19 files changed
+432
-140
No files matched your search
@@ -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
@@ -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. */
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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">·</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">·</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 () {
|
||||
|
||||
Reference in new issue
Block a user