feat(ui): WCAG contrast pass, neutral oklch surface system, and mobile nav overhaul (#11659)

This commit is contained in:
Andras Bacsai 2026-09-08 09:23:55 +02:00 committed by GitHub
commit caa07574b1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
95 changed files with 1574 additions and 859 deletions

View file

@ -34,7 +34,9 @@ ## 1. Visual direction
- near-neutral layered surfaces instead of large bordered boxes;
- 1314px UI typography and 32px controls;
- hairline rings instead of heavy borders;
- crisp hairline rings plus a restrained card lift (single 1px ring +
`0 1px 2px rgb(0 0 0 / 0.05)`) so cards and tables separate from the canvas,
never heavy borders or a strong floating shadow;
- full-width data tables for dense collections;
- outline Reicon glyphs through `<x-reicon>`;
- the Coolify purple brand accent in light mode;
@ -89,14 +91,14 @@ ## 3. Tokens and color behavior
| Token | Light | Dark | Use |
|---|---|---|---|
| `--coollabs-canvas` | near white | 10% neutral | page canvas |
| `--coollabs-canvas` | 97% off-white | 10% neutral | page canvas (kept below card fills so cards lift) |
| `--coollabs-elevated` | 98% neutral | 15% neutral | shells and card headers |
| `--coollabs-base` | white | 17% neutral | nested card bodies |
| `--coollabs-recessed` | 96% neutral | 20% neutral | inputs and listboxes |
| `--coollabs-fill` | 92.2% neutral | 26.9% neutral | dividers and passive fills |
| `--coollabs-line` | translucent dark | 32% neutral | control borders |
| `--coollabs-hairline` | 93.5% neutral | 26.9% neutral | shell rings |
| `--coollabs-subtle` | 55.6% neutral | 70.8% neutral | labels and muted titles |
| `--coollabs-hairline` | 85.5% neutral | 32% neutral | shell rings (crisp enough to read as a card edge, ~1.5:1) |
| `--coollabs-subtle` | 50% neutral | 70.8% neutral | labels and muted titles (light darkened for WCAG AA 4.5:1) |
Accent behavior is intentionally theme-aware:
@ -116,6 +118,38 @@ ## 3. Tokens and color behavior
The filled top-level action/tab treatment uses the same palette at a restrained
opacity rather than a fully saturated fill.
### Shell layering
The app shell is three distinct surface layers, not one flat color. Chrome
lifts, content is the base, cards lift off the content:
- **Content canvas** is the base layer: `bg-app` in dark (deepest,
`--color-app` `#0a0a0b`), `bg-gray-50` in light. The `<main>` content area
and page body use it.
- **Sidebar and topbar chrome** use `bg-panel` in dark (`--color-panel`
`#141418`, a clear step lighter than the content canvas) and `bg-white` in
light, so the chrome reads as a separate panel from the content.
Dark surface tokens are hex, not oklch. oklch lightness compresses toward pure
black below ~15% (oklch(10%) renders as sRGB 3, oklch(15%) as sRGB 11), so oklch
values there give no visible step between layers. The dark ladder is
`--color-app` 10, `--coollabs-elevated` 22, `--coollabs-base` 28,
`--coollabs-recessed` 34 (sRGB), which reads as distinct surfaces.
Temperature: every panel is **pure neutral gray** (r=g=b), one consistent
temperature across the sidebar, tables, cards, inputs, dividers, borders, and
text, in both modes. Do not give one surface a cool (blue) or warm cast while
the others stay neutral. The light page canvas uses `bg-neutral-50` (not
`bg-gray-50`, which is faintly cool) so it matches the neutral cards and chrome.
The only intentional color is the purple/yellow brand accent.
- **Cards, tables, and collection tiles** lift off the content canvas with
`dark:bg-white/[0.05]` plus the crisp `--coollabs-hairline` ring; in light
they are `bg-white` with the ring and the restrained card lift.
Do not paint the content area with the same `bg-panel` as the sidebar, and do
not drop card fills below `dark:bg-white/[0.05]`; both make surfaces read as one
color. Row-hover states keep the lighter `dark:hover:bg-white/[0.025]`.
---
## 4. Page shells and navigation
@ -419,6 +453,12 @@ ### Dropdowns
the option content left-aligned and size the panel to its content or trigger;
do not create an unnecessarily wide menu.
Every dropdown, menu, listbox panel, and the command palette uses the shared
`--shadow-dropdown` token (`0 4px 12px rgb(0 0 0 / 0.12), 0 2px 4px
rgb(0 0 0 / 0.08)`) for a restrained, consistent lift. Do not hand-roll a
heavier `shadow-lg` / `0 18px 50px` / `0.45`-alpha drop shadow on a menu.
Reserve the stronger `--shadow-modal` for actual modals, dialogs, and toasts.
Toolbar filter and sort buttons keep static labels (`Filter`, `Sort`). The
selected option is indicated inside the menu, not repeated on the trigger.

View file

@ -20,51 +20,104 @@ @theme {
--font-geist-sans: 'Geist Sans', Inter, sans-serif;
--font-logs: 'Geist Mono', 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
--color-base: #101010;
--color-warning: #fcd452;
--color-warning-50: #fefce8;
--color-warning-100: #fef9c3;
--color-warning-200: #fef08a;
--color-warning-300: #fde047;
--color-warning-400: #fcd452;
--color-warning-500: #facc15;
--color-warning-600: #ca8a04;
--color-warning-700: #a16207;
--color-warning-800: #854d0e;
--color-warning-900: #713f12;
--color-success: #22C55E;
--color-error: #dc2626;
--color-coollabs-50: #f5f0ff;
--color-coollabs: #6b16ed;
--color-coollabs-100: #7317ff;
--color-coollabs-200: #5a12c7;
--color-coollabs-300: #4a0fa3;
--color-coolgray-100: #181818;
--color-coolgray-200: #202020;
--color-coolgray-300: #242424;
--color-coolgray-400: #282828;
--color-coolgray-500: #323232;
--color-base: oklch(17.3% 0 0);
--color-warning: oklch(88.13% 0.1507 91.7);
--color-warning-50: oklch(98.73% 0.0262 102.21);
--color-warning-100: oklch(97.29% 0.0693 103.19);
--color-warning-200: oklch(94.51% 0.1243 101.54);
--color-warning-300: oklch(90.52% 0.1657 98.11);
--color-warning-400: oklch(88.13% 0.1507 91.7);
--color-warning-500: oklch(86.06% 0.1731 91.94);
--color-warning-600: oklch(68.06% 0.1423 75.83);
--color-warning-700: oklch(55.38% 0.1207 66.44);
--color-warning-800: oklch(47.62% 0.1034 61.91);
--color-warning-900: oklch(42.1% 0.0897 57.71);
--color-success: oklch(72.27% 0.192 149.58);
--color-error: oklch(57.71% 0.2152 27.33);
--color-coollabs-50: oklch(96.33% 0.0206 301.15);
--color-coollabs: oklch(49.65% 0.2709 289.33);
--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);
--color-coolgray-100: oklch(20.9% 0 0);
--color-coolgray-200: oklch(24.35% 0 0);
--color-coolgray-300: oklch(26.03% 0 0);
--color-coolgray-400: oklch(27.68% 0 0);
--color-coolgray-500: oklch(31.71% 0 0);
/* Graphite design language (ported from ref/frontend). Layered neutral
surfaces + translucent hairlines. See DESIGN.md. */
--color-app: #0c0c0d;
/* Canvas: neutral near-black (oklch), not pure black */
--color-panel: oklch(10% 0 0);
--color-surface: #161618;
--color-raised: #1c1c1e;
--color-selected: #26262a;
--color-fg: #f2f2f2;
--color-fg-dim: #b4b4b8;
--color-fg-faint: #6e6e74;
--color-accent: #6b16ed;
--color-accent-foreground: #ffffff;
--color-hairline: rgba(255, 255, 255, 0.08);
--color-nav-text: #525252;
--color-nav-muted: #666666;
--color-nav-active: #171717;
--color-log: #0d0d0d;
/* Content canvas: the darkest shell layer (sRGB ~10). Hex, not oklch:
oklch lightness compresses to near-black below ~15%, so oklch(7.5%)
renders as sRGB 1 with no visible step. */
--color-app: oklch(14.48% 0 0);
/* Sidebar + topbar chrome (sRGB ~20): a clear step lighter than the content
canvas so the shell chrome separates from the content area. */
--color-panel: oklch(19.13% 0 0);
/* Pure neutral (r=g=b). Were cool-tinted (#161618/#1c1c1e/#26262a, b>r),
which clashed with the neutral panel ladder. */
--color-surface: oklch(20.02% 0 0);
--color-raised: oklch(22.64% 0 0);
--color-selected: oklch(26.86% 0 0);
--color-fg: oklch(96.12% 0 0);
--color-fg-dim: oklch(76.99% 0 0);
/* Tertiary text. Raised from #6e6e74 (3.78:1 on base, failed WCAG AA)
to #7e7e84 so 13-14px text clears 4.5:1 on the dark surface ladder. */
--color-fg-faint: oklch(59.31% 0 0);
--color-accent: oklch(49.65% 0.2709 289.33);
--color-accent-foreground: oklch(100.0% 0 0);
--color-hairline: oklch(100.0% 0 0 / 0.08);
--color-nav-text: oklch(43.86% 0 0);
--color-nav-muted: oklch(51.03% 0 0);
--color-nav-active: oklch(20.46% 0 0);
--color-log: oklch(15.91% 0 0);
--shadow-modal: 0 24px 64px rgba(0, 0, 0, 0.55), 0 4px 16px rgba(0, 0, 0, 0.4);
/* One restrained lift shared by every dropdown/menu/listbox panel and the
command palette. Lighter and consistent, replacing the old scattered
0.45 / 0.35 / shadow-lg / shadow-sm mix. Modals keep --shadow-modal. */
--shadow-dropdown: 0 4px 12px rgb(0 0 0 / 0.12), 0 2px 4px rgb(0 0 0 / 0.08);
/* Shared motion curves (stronger than the built-in CSS easings). Use these
for consistency: --ease-out for UI enter/press, --ease-drawer for
drawers/sheets, --ease-in-out for on-screen movement. */
--ease-out: cubic-bezier(0.23, 1, 0.32, 1);
--ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
--ease-drawer: cubic-bezier(0.32, 0.72, 0, 1);
}
/* Shared press feedback: every button-like control dips slightly on press and
eases back, so pressable elements feel responsive and consistent app-wide. */
.button,
.icon-button,
.app-tab,
.split-action-main,
.split-action-caret {
transition-property: color, background-color, border-color, box-shadow, transform;
transition-timing-function: var(--ease-out);
transition-duration: 120ms;
}
.button:not(:disabled):active,
.icon-button:not(:disabled):active,
.app-tab:active,
.split-action-main:not(:disabled):active,
.split-action-caret:not(:disabled):active {
transform: scale(0.97);
}
/* Focus border/ring on any form control should ease, not snap. The accent
focus indicator is a border-color and/or box-shadow (ring) change, which the
utilities' `transition-colors` did not fully animate. Target the elements
themselves (covers bespoke search fields too) plus the div-based controls. */
input,
textarea,
select,
.listbox-trigger,
.chip-input {
transition-property: color, background-color, border-color, box-shadow;
transition-timing-function: var(--ease-out);
transition-duration: 120ms;
}
/*
@ -258,7 +311,7 @@ @layer components {
*/
html,
body {
@apply w-full min-h-full bg-gray-50 dark:bg-app dark:text-fg-dim;
@apply w-full min-h-full bg-neutral-50 dark:bg-app dark:text-fg-dim;
}
body {
@ -505,25 +558,24 @@ .terminal-fullscreen-shell::before {
.application-console-shell[data-console-theme="system"],
.terminal-fullscreen-shell[data-console-theme="system"] {
--console-theme-background: #fff;
--console-theme-border: #d4d4d8;
--console-theme-background: oklch(100.0% 0 0);
--console-theme-border: oklch(87.11% 0.0055 286.29);
--console-theme-opacity: 1;
}
html:not(.dark) .application-console-shell[data-console-theme="system"] .application-console-header {
background: transparent;
color: #52525b;
border-color: rgb(0 0 0 / 0.1);
backdrop-filter: none;
}
html:not(.dark) .application-console-shell[data-console-theme="system"] .application-console-header [class*="text-white"] {
/* Session toolbar (selected-target trigger + label) sits over the white
system console in light mode. Without this its text-white/* stayed white on
white and the selected server was invisible (issue #11532). */
html:not(.dark) .application-console-shell[data-console-theme="system"] .terminal-session-toolbar[class*="text-white"],
html:not(.dark) .application-console-shell[data-console-theme="system"] .terminal-session-toolbar [class*="text-white"],
html:not(.dark) .terminal-fullscreen-shell[data-console-theme="system"] .terminal-session-toolbar[class*="text-white"],
html:not(.dark) .terminal-fullscreen-shell[data-console-theme="system"] .terminal-session-toolbar [class*="text-white"] {
color: #52525b !important;
}
html.dark .application-console-shell[data-console-theme="system"],
html.dark .terminal-fullscreen-shell[data-console-theme="system"] {
--console-theme-background: #121214;
--console-theme-background: oklch(18.31% 0.004 285.99);
--console-theme-border: rgb(255 255 255 / 0.08);
}
@ -533,6 +585,19 @@ html[data-theme="custom"] .terminal-fullscreen-shell[data-console-theme="system"
--console-theme-border: var(--coollabs-line);
}
/* Docked System console only: the fill behind the terminal panel is the
elevated (header) surface, so the panel's rounded-corner notch reads as the
header colour the card's figure/ground. The panel paints its own base
surface over this fill, so only the notch shows it. The fullscreen shell has
no panel, so it is intentionally excluded and keeps its plain terminal fill. */
.application-console-shell[data-console-theme="system"] {
--console-theme-background: var(--coollabs-elevated);
}
html.dark .application-console-shell[data-console-theme="system"] {
--console-theme-background: var(--coollabs-elevated);
}
.console-theme-selector {
scrollbar-color: rgb(161 161 170) transparent;
scrollbar-width: thin;
@ -656,6 +721,10 @@ .terminal-page-console {
flex: 1 1 0%;
flex-direction: column;
overflow: hidden;
/* Breathing room so the console's box-shadow ring + lift are not clipped by
this overflow-hidden, viewport-height container (the card gets this from
the scrollable page around it). */
padding: 6px;
}
.terminal-page-console > .application-console-shell {
@ -712,11 +781,71 @@ body.terminal-is-fullscreen .terminal-fullscreen-shell [data-terminal-mobile-too
touch-action: manipulation;
}
.application-console-header {
/* Session header band: an attached card-style header on top of the console,
mirroring the page card header (elevated surface + hairline divider). The
System theme uses the neutral card surface in both modes; colorful themes
fall back to a translucent dark band so the white controls stay legible. */
.terminal-session-toolbar {
position: relative;
z-index: 2;
background: rgb(0 0 0 / 0.24);
backdrop-filter: blur(20px);
flex-shrink: 0;
min-height: 3rem;
padding: 0.5rem 0.75rem 0.5rem 1rem;
/* No border-radius: the shell is overflow-hidden + rounded and clips the
header's top corners, so rounding here only risks a sub-pixel seam. */
border-bottom: 1px solid var(--terminal-header-border, rgb(255 255 255 / 0.1));
background: var(--terminal-header-bg, rgb(0 0 0 / 0.28));
-webkit-backdrop-filter: blur(12px);
backdrop-filter: blur(12px);
}
/* System header has no bottom border: the body panel's ring is the divider, and
its rounded top corners tuck under the header the same merge the
target-picker card uses (elevated header meeting a base body panel). */
.application-console-shell[data-console-theme="system"] .terminal-session-toolbar {
--terminal-header-bg: var(--coollabs-elevated);
--terminal-header-border: transparent;
/* Shell no longer clips (overflow: visible), so the header rounds its own
top corners matching the card header. */
border-radius: 8px 8px 0 0;
/* Drop the z-index:2 stacking context: it painted the header OVER the body
panel, hiding the panel's ring (the divider). The header only had z-2 to
beat ::before, but its dropdowns sit in their own .relative wrappers, so
z:auto is safe and lets the body panel's ring paint over the header
exactly how the card body overlaps the card header. */
z-index: auto;
-webkit-backdrop-filter: none;
backdrop-filter: none;
}
/* System console body: the terminal panel, a full rounded box-shadow ring like
the card body. Its base surface is the terminal fill; the elevated surface
behind it (::before) shows only through the rounded-corner notch, reading as
the header colour the card's figure/ground. */
.application-console-shell[data-console-theme="system"] .terminal-session-panel {
background: var(--coollabs-base);
/* The body panel is a full rounded ring (like the card body). position:
relative puts it (and its ring) above ::before AND, because it comes
after the now-z:auto header in the DOM, above the header so the ring's
top edge is the divider and its rounded top corners connect down into the
side ring, which coincides with the shell's ring (one hairline, no
doubling). A box-shadow ring rounds and connects cleanly; a border-top
cannot (square corners, or a disconnected transparent edge). */
position: relative;
border-radius: 8px;
box-shadow: 0 0 0 1px var(--coollabs-hairline);
}
/* Connected System console frame = the target-picker card recipe: a hairline
box-shadow ring + a restrained lift. overflow stays visible so the ring is
not clipped (the .terminal-page-console padding gives it room inside the
viewport-locked, overflow-hidden page). Colorful themes keep the themed,
overflow-hidden shell. */
.application-console-shell[data-console-theme="system"] {
overflow: visible;
box-shadow:
0 0 0 1px var(--coollabs-hairline),
0 1px 2px 0 rgb(0 0 0 / 0.05);
}
.terminal-loading-label {
@ -824,7 +953,7 @@ .terminal-session-panel {
}
html:not(.dark) .application-console-shell[data-console-theme="system"] .terminal-session-panel {
background: transparent;
background: var(--coollabs-base);
}
.terminal-target-list {
@ -851,7 +980,7 @@ html:not(.dark) .application-console-shell[data-console-theme="system"] .termina
color: #52525b;
background: #fff;
border-color: #d4d4d8;
box-shadow: 0 18px 50px rgb(0 0 0 / 0.18);
box-shadow: var(--shadow-dropdown);
}
html:not(.dark) .application-console-shell[data-console-theme="system"] .terminal-target-picker [class*="text-white"] {
@ -924,75 +1053,166 @@ .terminal-fullscreen-btn:hover {
* utility classes placed in the card markup.
*/
:root {
--coollabs-canvas: oklch(98.75% 0 0);
/* Page canvas is a faint off-white so white/elevated cards lift off it
(shadcn pattern), instead of card and page reading as one flat color. */
--coollabs-canvas: oklch(97% 0 0);
--coollabs-elevated: oklch(98% 0 0);
--coollabs-recessed: oklch(96% 0 0);
--coollabs-base: #ffffff;
--coollabs-base: oklch(100.0% 0 0);
--coollabs-fill: oklch(92.2% 0 0);
--coollabs-line: oklch(14.5% 0 0 / 0.1);
--coollabs-hairline: oklch(93.5% 0 0);
--coollabs-subtle: oklch(55.6% 0 0);
/* Card/table edge ring. Crisped from 93.5% (1.17:1 on canvas, invisible)
to 85.5% (1.51:1) so surfaces read as bordered cards, not flat fills. */
--coollabs-hairline: oklch(85.5% 0 0);
/* Muted labels/titles. Darkened from 55.6% (failed WCAG AA on elevated
4.46, recessed 4.24, fill 3.76) to 50% so muted text clears 4.5:1 on
every light surface above white. Dark stays 70.8% (already passes). */
--coollabs-subtle: oklch(50% 0 0);
}
.dark {
--color-accent: #fcd452;
--coollabs-canvas: oklch(10% 0 0);
/* elevated (card shells/headers) and recessed (input fills) sit a touch
above the canvas so they read lighter than near-black */
--coollabs-elevated: oklch(15% 0 0);
--coollabs-recessed: oklch(20% 0 0);
--coollabs-base: oklch(17% 0 0);
--coollabs-fill: oklch(26.9% 0 0);
--color-accent: oklch(88.13% 0.1507 91.7);
/* 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,
visible steps: content 10 -> elevated 22 -> base 28 -> recessed 34.
Pure neutral (r=g=b), matching the neutral borders/text and the light
ladder, so every panel shares one temperature (no blue cast). */
--coollabs-canvas: oklch(14.48% 0 0);
--coollabs-elevated: oklch(20.02% 0 0);
--coollabs-recessed: oklch(25.2% 0 0);
--coollabs-base: oklch(22.64% 0 0);
--coollabs-fill: oklch(29.31% 0 0);
--coollabs-line: oklch(32% 0 0);
--coollabs-hairline: oklch(26.9% 0 0);
/* Crisped from 26.9% (1.36:1 on canvas) to 32% (1.63:1) so dark cards
and tables show a visible edge instead of blending into the canvas. */
--coollabs-hairline: oklch(32% 0 0);
--coollabs-subtle: oklch(70.8% 0 0);
--color-nav-text: #a8a8b0;
--color-nav-muted: #7a7a84;
--color-nav-active: #f2f2f2;
/* Neutral (was #a8a8b0 / #7a7a84, both cool-tinted). Sidebar text now shares
the same neutral temperature as the content text, so the sidebar no longer
reads cooler than the panels/cards. */
--color-nav-text: oklch(73.8% 0 0);
/* Raised from 58.97% (4.47:1 on panel, just under AA) to 61% (4.86:1) so the
11px sidebar section labels clear 4.5:1. */
--color-nav-muted: oklch(61% 0 0);
--color-nav-active: oklch(96.12% 0 0);
/* Tertiary text. Base token (59.31%) only clears 4.5:1 on app/panel; it
failed on surface/raised/selected (4.46/4.20/3.73). Raised to 64% so the
313 dark:text-fg-faint usages clear AA across the whole surface ladder,
while staying clearly dimmer than fg-dim (76.99%). */
--color-fg-faint: oklch(65% 0 0);
/* Error text. The base error red (57.71%) failed on every dark surface
(4.10/3.75/3.53). Brightened for dark mode so text-error clears AA on the
card ladder while staying unmistakably red. */
--color-error: oklch(66% 0.2 27.33);
}
/* Light-mode semantic text.
warning / success / error double as the bright brand accent and as solid
fills, which only appear under `dark:` so in light mode these tokens are
used almost entirely as text, translucent tints (bg-warning/10) and borders.
The bright values (yellow 88%, green 72%) and the base red (57.71%) fail AA
as text on white and the light surface ladder (warning 1.43, success 2.28,
error 4.43 on canvas). Scoped to `:not(.dark)` so dark keeps the brand
colors and the custom theme keeps its own overrides. Every value clears
4.5:1 on white, canvas, elevated, recessed and fill, and is in sRGB gamut. */
html:not(.dark) {
--color-warning: oklch(50% 0.105 80);
--color-success: oklch(48% 0.135 149.58);
--color-error: oklch(52% 0.2 27.33);
}
/* Theme surfaces are derived from one brand color. Changing
--theme-base-color is enough to generate a complete dark surface ladder. */
html[data-theme="custom"] {
--theme-base-color: #6b16ed;
--theme-bright-color: color-mix(in srgb, var(--theme-base-color) 85%, white);
--theme-base-color: oklch(49.65% 0.2709 289.33);
--theme-bright-color: color-mix(in srgb, var(--theme-base-color) 85%, oklch(100% 0 0));
--theme-scrollbar-thumb: color-mix(in srgb, var(--theme-bright-color) 70%, var(--theme-accent-foreground));
--theme-border-color: color-mix(in oklab, var(--theme-base-color) 42%, #52525b);
--theme-placeholder-color: color-mix(in srgb, white 20%, var(--theme-base-color));
--theme-border-color: color-mix(in oklab, var(--theme-base-color) 42%, oklch(44.19% 0.0146 285.79));
/* Accent text/icons sit directly on dark surfaces. Force the lightness into a
readable band (and tame chroma) so any picked hue keeps enough contrast. */
--theme-fg-on-surface: oklch(from var(--theme-base-color) clamp(0.72, l, 0.86) min(c, 0.13) h);
/* Placeholders read as a light, near-neutral hint of the hue - never the raw
(possibly dark) base color, which is invisible on the dark input. */
--theme-placeholder-color: oklch(from var(--theme-base-color) 0.62 min(c, 0.03) h);
--color-accent: var(--theme-bright-color);
--color-coollabs: var(--theme-bright-color);
--color-coollabs-100: color-mix(in oklab, var(--theme-bright-color) 88%, white);
--color-coollabs-200: color-mix(in oklab, var(--theme-bright-color) 88%, black);
--color-coollabs-300: color-mix(in oklab, var(--theme-bright-color) 72%, black);
--color-coollabs-100: color-mix(in oklab, var(--theme-bright-color) 88%, oklch(100% 0 0));
--color-coollabs-200: color-mix(in oklab, var(--theme-bright-color) 88%, oklch(0% 0 0));
--color-coollabs-300: color-mix(in oklab, var(--theme-bright-color) 72%, oklch(0% 0 0));
/* Legacy dark-theme accent utilities use warning as the brand color. */
--color-warning: var(--theme-bright-color);
--color-accent-foreground: var(--theme-accent-foreground);
--color-app: color-mix(in oklab, var(--theme-base-color) 12%, #09090a);
--color-panel: color-mix(in oklab, var(--theme-base-color) 14%, #0c0c0d);
--color-surface: color-mix(in oklab, var(--theme-base-color) 18%, #101011);
--color-raised: color-mix(in oklab, var(--theme-base-color) 24%, #141416);
--color-selected: color-mix(in oklab, var(--theme-base-color) 32%, #18181a);
--color-app: color-mix(in oklab, var(--theme-base-color) 12%, oklch(14.03% 0.0022 286.11));
--color-panel: color-mix(in oklab, var(--theme-base-color) 14%, oklch(15.48% 0.0021 286.15));
--color-surface: color-mix(in oklab, var(--theme-base-color) 18%, oklch(17.35% 0.0020 286.18));
--color-raised: color-mix(in oklab, var(--theme-base-color) 24%, oklch(19.21% 0.0040 286.02));
--color-selected: color-mix(in oklab, var(--theme-base-color) 32%, oklch(20.99% 0.0039 286.06));
--color-coolgray-100: var(--color-surface);
--color-coolgray-200: var(--color-raised);
--color-coolgray-300: var(--color-selected);
--color-coolgray-400: color-mix(in oklab, var(--theme-base-color) 28%, #1b1b1e);
--color-coolgray-500: color-mix(in oklab, var(--theme-base-color) 32%, #202024);
--color-coolgray-400: color-mix(in oklab, var(--theme-base-color) 28%, oklch(22.34% 0.0058 285.92));
--color-coolgray-500: color-mix(in oklab, var(--theme-base-color) 32%, oklch(24.52% 0.0075 285.83));
--coollabs-canvas: var(--color-app);
--coollabs-elevated: var(--color-surface);
--coollabs-recessed: var(--color-raised);
--coollabs-base: color-mix(in oklab, var(--theme-base-color) 22%, #111112);
--coollabs-base: color-mix(in oklab, var(--theme-base-color) 22%, oklch(17.81% 0.0020 286.19));
--color-content-surface: var(--coollabs-base);
--coollabs-fill: color-mix(in oklab, var(--theme-base-color) 32%, #18181a);
--coollabs-fill: color-mix(in oklab, var(--theme-base-color) 32%, oklch(20.99% 0.0039 286.06));
--coollabs-line: var(--theme-border-color);
--coollabs-hairline: color-mix(in srgb, var(--theme-border-color) 55%, var(--color-panel));
--color-nav-text: #e4dfea;
--color-nav-muted: #d8d2df;
--color-nav-active: #ffffff;
--color-log: color-mix(in oklab, var(--theme-base-color) 20%, #080809);
--color-log-toolbar: color-mix(in oklab, var(--theme-base-color) 26%, #101011);
--color-fg-dim: #e4dfea;
--color-fg-faint: #d8d2df;
--coollabs-subtle: #d8d2df;
--color-nav-text: oklch(91.08% 0.0157 306.4);
--color-nav-muted: oklch(87.24% 0.0188 306.63);
--color-nav-active: oklch(100.0% 0 0);
--color-log: color-mix(in oklab, var(--theme-base-color) 20%, oklch(13.49% 0.0024 286.07));
--color-log-toolbar: color-mix(in oklab, var(--theme-base-color) 26%, oklch(17.35% 0.0020 286.18));
--color-fg-dim: oklch(91.08% 0.0157 306.4);
--color-fg-faint: oklch(87.24% 0.0188 306.63);
--coollabs-subtle: oklch(87.24% 0.0188 306.63);
}
/* Light custom theme: the same single brand color, but surfaces tint toward
white and every foreground token flips to a dark, readable band. This selector
outranks the plain html:not(.dark) light palette, so it wins where they meet.
Accent fills (--theme-bright-color, --color-warning/coollabs) are inherited from
the block above so the JS-computed --theme-accent-foreground stays consistent. */
html[data-theme="custom"]:not(.dark) {
--theme-scrollbar-thumb: color-mix(in srgb, var(--theme-base-color) 45%, oklch(60% 0 0));
/* Accent text/icons on light surfaces: force lightness into a dark band. */
--theme-fg-on-surface: oklch(from var(--theme-base-color) clamp(0.30, l, 0.48) min(c, 0.16) h);
/* Placeholder: medium-dark, near-neutral hint of the hue. */
--theme-placeholder-color: oklch(from var(--theme-base-color) 0.50 min(c, 0.03) h);
/* Surfaces hold a fixed high lightness (stays light, keeps dark text readable)
while injecting the base hue's chroma, so the tint is actually visible.
color-mix into white coupled lightness to the base and washed the tint out
(a 4% mix of any color into near-white is imperceptible). */
--color-app: oklch(from var(--theme-base-color) 96.5% min(c, 0.035) h);
--color-panel: oklch(from var(--theme-base-color) 98.5% min(c, 0.025) h);
--color-surface: oklch(from var(--theme-base-color) 98% min(c, 0.03) h);
--color-raised: oklch(from var(--theme-base-color) 95.5% min(c, 0.04) h);
--color-selected: oklch(from var(--theme-base-color) 92% min(c, 0.05) h);
--color-coolgray-100: var(--color-surface);
--color-coolgray-200: var(--color-raised);
--color-coolgray-300: var(--color-selected);
--color-coolgray-400: oklch(from var(--theme-base-color) 90% min(c, 0.05) h);
--color-coolgray-500: oklch(from var(--theme-base-color) 87% min(c, 0.055) h);
--coollabs-canvas: var(--color-app);
--coollabs-elevated: oklch(from var(--theme-base-color) 98.5% min(c, 0.025) h);
--coollabs-recessed: var(--color-raised);
--coollabs-base: oklch(from var(--theme-base-color) 99.3% min(c, 0.018) h);
--color-content-surface: var(--coollabs-base);
--coollabs-fill: oklch(from var(--theme-base-color) 91% min(c, 0.05) h);
--coollabs-line: oklch(from var(--theme-base-color) 82% min(c, 0.055) h);
--coollabs-hairline: oklch(from var(--theme-base-color) 88% min(c, 0.04) h);
--color-nav-text: oklch(from var(--theme-base-color) 0.40 min(c, 0.04) h);
--color-nav-muted: oklch(from var(--theme-base-color) 0.50 min(c, 0.03) h);
--color-nav-active: oklch(from var(--theme-base-color) 0.22 min(c, 0.05) h);
--color-log: oklch(from var(--theme-base-color) 96.5% min(c, 0.035) h);
--color-log-toolbar: oklch(from var(--theme-base-color) 93% min(c, 0.045) h);
--color-fg: oklch(from var(--theme-base-color) 0.22 min(c, 0.04) h);
--color-fg-dim: oklch(from var(--theme-base-color) 0.40 min(c, 0.04) h);
--color-fg-faint: oklch(from var(--theme-base-color) 0.50 min(c, 0.03) h);
--coollabs-subtle: oklch(from var(--theme-base-color) 0.50 min(c, 0.03) h);
}
html[data-theme="custom"] .control-selected,
@ -1051,7 +1271,55 @@ .dark [class~="dark:border-white/[0.06]"] {
html[data-theme="custom"] input::placeholder,
html[data-theme="custom"] textarea::placeholder {
color: var(--theme-placeholder-color) !important;
opacity: 0.7;
/* The token is already a readable light tone; no extra opacity dampening,
which would drop it back below the contrast floor. */
opacity: 1;
}
/* Accent text/icons (links, badges, "update available" pill, check marks) use the
brand color as foreground on dark surfaces. Route them to the clamped, always
readable token; fills (bg-*) keep the brighter accent + its computed foreground. */
html[data-theme="custom"] :is(
[class~="text-coollabs"],
[class~="text-warning"],
[class~="text-accent"],
[class~="dark:text-coollabs"],
[class~="dark:text-warning"]
) {
color: var(--theme-fg-on-surface) !important;
}
html[data-theme="custom"] [class~="hover:text-coollabs"]:hover,
html[data-theme="custom"] [class~="hover:text-warning"]:hover,
html[data-theme="custom"] [class~="dark:hover:text-warning"]:hover,
html[data-theme="custom"] [class~="dark:hover:text-coollabs"]:hover {
color: var(--theme-fg-on-surface) !important;
}
/* Light custom theme reaches raw neutral utilities too. Dark mode tints
everything through the pervasive dark:bg-*/dark:border-* utilities (which
resolve to --color-* vars the block overrides); light mode has no equivalent,
so components fall back to hardcoded neutrals. Remap the dominant light
surface/border utilities to the tinted custom vars so the color reaches the
whole UI, not just design-system classes. */
html[data-theme="custom"]:not(.dark) :is([class~="bg-white"], [class~="bg-white/95"], [class~="bg-white/80"]) {
background-color: var(--coollabs-elevated) !important;
}
html[data-theme="custom"]:not(.dark) [class~="bg-neutral-50"] {
background-color: var(--coollabs-canvas) !important;
}
html[data-theme="custom"]:not(.dark) :is([class~="bg-neutral-100"], [class~="bg-neutral-200"]) {
background-color: var(--coollabs-fill) !important;
}
html[data-theme="custom"]:not(.dark) [class~="border-neutral-200"] {
border-color: var(--coollabs-hairline) !important;
}
html[data-theme="custom"]:not(.dark) :is([class~="border-neutral-300"], [class~="border-neutral-100"]) {
border-color: var(--coollabs-line) !important;
}
html[data-theme="custom"] input:read-only,
@ -1318,27 +1586,6 @@ .error-actions {
margin-top: 1.75rem;
}
.error-contact-link {
display: inline-flex;
align-items: center;
gap: 0.25rem;
margin-left: 0.25rem;
font-size: 0.8125rem;
font-weight: 500;
color: #525252;
text-underline-offset: 3px;
transition: color 0.15s ease;
}
.dark .error-contact-link {
color: var(--color-fg-dim);
}
.error-contact-link:hover {
color: var(--color-accent);
text-decoration: underline;
}
/* Layer card root: elevated shell with a hairline ring. overflow stays
visible so floating panels (listbox, tooltips) can escape the card. */
.application-settings-section {
@ -1349,7 +1596,9 @@ .application-settings-section {
border-radius: 8px;
border: none;
background: var(--coollabs-elevated);
box-shadow: 0 0 0 1px var(--coollabs-hairline);
/* crisp ring + a restrained lift so the card separates from the canvas
(shadcn-style), not a heavy floating shadow */
box-shadow: 0 0 0 1px var(--coollabs-hairline), 0 1px 2px 0 rgb(0 0 0 / 0.05);
/* keep anchored scrolls (scrollIntoView) clear of the fixed topbar + layer-2 nav */
scroll-margin-top: 7rem;
}
@ -1415,7 +1664,14 @@ .application-settings-section > :is(header, .application-settings-section-header
font-weight: 500;
line-height: 1.25rem;
letter-spacing: 0;
color: inherit;
/* Card title is a heading, so give it a readable strength instead of the
muted --coollabs-subtle (5.66:1 light / 6.98:1 dark). ~8.7:1 in both
modes (comfortable AAA) while descriptions/labels stay muted. */
color: oklch(40% 0 0);
}
.dark .application-settings-section > :is(header, .application-settings-section-header) :is(h2, h3) {
color: var(--color-fg-dim);
}
.application-settings-section > .application-settings-section-header p {
@ -1425,12 +1681,16 @@ .application-settings-section > .application-settings-section-header p {
color: var(--coollabs-subtle);
}
/* Layer card body: nested rounded panel with base bg + fill ring */
/* Layer card body: nested rounded panel with base bg. Ring uses the same
--coollabs-hairline as the card shell (line ~1456) so the body edge and the
header edge read as one continuous border instead of two different grays
(fill was lighter than hairline, so the header sides and body sides did not
match). */
.application-settings-section-body {
position: relative;
border-radius: 8px;
background: var(--coollabs-base);
box-shadow: 0 0 0 1px var(--coollabs-fill);
box-shadow: 0 0 0 1px var(--coollabs-hairline);
padding: 1rem;
}
@ -1469,28 +1729,71 @@ .server-settings-workspace > :not(.application-settings-navigation) {
}
@media (min-width: 1280px) {
/* Fixed like the main sidebar (3rem bottom); left tracks its width (--sidebar-w).
Fixed leaves the grid, so the content sibling is pinned to column 2 below. */
.application-settings-navigation {
position: sticky;
top: calc(3rem + 1.75rem);
align-self: start;
max-height: calc(100dvh - 5.5rem);
padding-right: 0.375rem;
position: fixed;
top: 3rem;
left: var(--sidebar-w, 14rem);
width: calc(210px + 2.5rem);
height: calc(100dvh - 3rem);
overflow-x: hidden;
overflow-y: auto;
overscroll-behavior: contain;
scrollbar-width: thin;
scrollbar-color: var(--coollabs-fill) transparent;
scrollbar-gutter: stable;
--rail-fill: var(--coollabs-elevated);
padding: 0.5rem;
background: var(--rail-fill);
border-right: 1px solid var(--coollabs-hairline);
transition: left 200ms ease;
}
/* Resource pages have a second fixed header row below the main header. */
.application-settings-workspace > .application-settings-navigation {
top: 4rem;
max-height: calc(100dvh - 5rem);
.application-settings-navigation ~ * {
grid-column: 2;
}
html:not(.dark) .application-settings-navigation {
--rail-fill: var(--coollabs-base);
}
.application-settings-navigation::-webkit-scrollbar {
width: 6px;
}
.application-settings-navigation::-webkit-scrollbar-track {
background: transparent;
}
.application-settings-navigation::-webkit-scrollbar-thumb {
border-radius: 9999px;
background: var(--coollabs-fill);
}
.application-settings-navigation::-webkit-scrollbar-thumb:hover {
background: var(--coollabs-line);
}
/* Embeds: keep the surface, drop the fixed positioning. */
.application-settings-navigation.is-flush {
position: static;
max-height: none;
inset: auto;
width: auto;
height: auto;
overflow: visible;
transition: none;
}
}
/* Honor reduced-motion for the settings nav disclosure/accordion and the mobile
sidebar sheet: keep open/close functional but drop the height/slide/fade. */
@media (prefers-reduced-motion: reduce) {
.application-settings-navigation,
.application-settings-navigation *,
.mobile-sidebar-sheet * {
transition-duration: 0.01ms !important;
animation-duration: 0.01ms !important;
}
}
@ -1560,7 +1863,7 @@ .application-settings-workspace .select:focus-visible,
.application-settings-form .input:focus-visible,
.application-settings-form .select:focus-visible {
border-color: var(--color-accent);
box-shadow: 0 0 0 1px var(--color-accent) !important;
box-shadow: none !important;
}
.application-settings-workspace textarea.input,
@ -1603,7 +1906,7 @@ .resource-heading-tabs::-webkit-scrollbar {
* Circular chevrons sit over gradient fades when more tabs are off-screen.
*/
.resource-heading-tabs-scroller {
--resource-heading-tabs-fade: #f5f5f5; /* neutral-100, matches pill track */
--resource-heading-tabs-fade: oklch(97.02% 0 0); /* neutral-100, matches pill track */
}
.dark .resource-heading-tabs-scroller {
@ -1662,7 +1965,7 @@ .resource-heading-tabs-control-icon {
margin-inline: 0.25rem;
border-radius: 9999px;
color: #737373;
background: #ffffff;
background: var(--coollabs-base);
box-shadow:
0 1px 2px rgba(0, 0, 0, 0.06),
0 0 0 1px rgba(0, 0, 0, 0.08);
@ -1736,20 +2039,6 @@ .resource-heading-overflow-items.is-measuring {
box-shadow: none !important;
}
.resource-heading-overflow-separator {
width: 1px;
align-self: stretch;
margin: 0.25rem 0.25rem;
background: color-mix(in srgb, var(--coollabs-line) 80%, transparent);
}
.resource-heading-overflow.is-collapsed .resource-heading-overflow-separator {
width: auto;
height: 1px;
align-self: auto;
margin: 0.25rem 0.375rem;
}
.resource-heading-overflow.is-collapsed .resource-heading-overflow-items > .button,
.resource-heading-overflow.is-collapsed .resource-heading-overflow-items > a.button {
width: 100%;
@ -1870,6 +2159,61 @@ .application-heading-actions .app-tab svg {
flex-shrink: 0;
}
.split-action {
display: inline-flex;
}
.split-action-main,
.split-action-caret {
@apply button-highlighted;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.375rem;
height: 2rem;
font-size: 13px;
font-weight: 500;
white-space: nowrap;
cursor: pointer;
}
.split-action-main {
flex: 1 1 auto;
min-width: 0;
padding: 0 0.625rem;
border-radius: 6px 0 0 6px;
}
.split-action-caret {
flex-shrink: 0;
width: 1.75rem;
border-left: 1px solid rgba(255, 255, 255, 0.25);
border-radius: 0 6px 6px 0;
}
.split-action > .split-action-main:only-of-type {
border-radius: 6px;
}
.split-action-main:disabled,
.split-action-caret:disabled {
cursor: not-allowed;
opacity: 0.45;
}
.split-action-main:focus-visible,
.split-action-caret:focus-visible {
outline: none;
position: relative;
z-index: 1;
box-shadow: 0 0 0 3px color-mix(in oklab, var(--color-accent) 40%, transparent);
}
.application-heading-actions .split-action-main,
.application-heading-actions .split-action-caret {
height: 1.75rem;
}
/* Custom listbox (replaces native <select>) */
.listbox-trigger {
display: flex;
@ -1919,7 +2263,7 @@ .dark .listbox-trigger:disabled {
.listbox-trigger:focus-visible {
outline: none;
border-color: var(--color-accent);
box-shadow: 0 0 0 1px var(--color-accent);
box-shadow: none;
}
.listbox-panel {
@ -1944,7 +2288,7 @@ .listbox-panel {
border-radius: 10px;
border: 1px solid var(--coollabs-line);
background: var(--coollabs-recessed);
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.45);
box-shadow: var(--shadow-dropdown);
}
@media (max-width: 767px) {
@ -2040,7 +2384,7 @@ .searchable-listbox-search-input::placeholder {
.searchable-listbox-search-input:focus {
outline: none;
border-color: var(--color-accent);
box-shadow: 0 0 0 1px var(--color-accent);
box-shadow: none;
}
/* Hide native WebKit search clear so custom clear buttons are not doubled */
@ -3009,7 +3353,7 @@ .dark .data-table-cell-dash {
/* Shared logs viewer (deployment + runtime) mobile-first toolbar
Mobile stacks: search meta full-width actions (no side-by-side overlap). */
.logs-viewer {
background: #fff;
background: var(--coollabs-base);
color: #262626;
}
@ -3510,7 +3854,7 @@ .runtime-log-menu {
.runtime-log-viewport {
min-height: 12rem;
border-radius: 0 0 8px 8px;
background: #fafafa;
background: var(--coollabs-elevated);
color: #262626;
}
@ -3525,10 +3869,6 @@ .dark .runtime-log-viewport {
color: var(--color-fg-dim);
}
.env-table-detail {
padding: 0.25rem 1rem 1.25rem;
}
/* Small pill badges for table cells */
.table-badge {
display: inline-flex;
@ -3606,7 +3946,7 @@ .chip-input {
.chip-input:focus-within {
border-color: var(--color-accent);
box-shadow: 0 0 0 1px var(--color-accent);
box-shadow: none;
}
.chip-input input {
@ -3679,93 +4019,6 @@ .chip-remove:hover {
color: inherit;
}
/* Detached pill options (segmented radio groups rendered as separate pills) */
.option-pill-group {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.option-pill-group label {
margin-bottom: 0;
}
.option-pill {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.375rem;
height: 2rem;
padding: 0 0.875rem;
border-radius: 8px;
border: 1px solid var(--coollabs-line);
background: var(--coollabs-elevated);
font-size: 0.875rem;
font-weight: 500;
white-space: nowrap;
color: #000000;
transition: background-color 0.15s, border-color 0.15s;
}
.dark .option-pill {
color: var(--color-fg);
}
.option-pill:hover {
background: var(--coollabs-recessed);
}
.peer:checked + .option-pill {
background: var(--coollabs-fill);
border-color: var(--coollabs-line);
}
.peer:disabled + .option-pill {
cursor: not-allowed;
opacity: 0.5;
}
/* Label column + content rows inside a card body */
.application-purpose-row {
display: grid;
gap: 1rem;
border-top: 1px solid var(--coollabs-fill);
padding-top: 1.25rem;
}
@media (min-width: 768px) {
.application-purpose-row {
grid-template-columns: 13rem minmax(0, 1fr);
gap: 2rem;
}
}
.application-purpose-copy h4 {
font-size: 0.875rem;
font-weight: 600;
color: #000000;
}
.dark .application-purpose-copy h4 {
color: var(--color-fg);
}
.application-purpose-copy p {
margin-top: 0.25rem;
font-size: 0.875rem;
line-height: 1.25rem;
color: #71717a;
}
.dark .application-purpose-copy p {
color: var(--color-fg-dim);
}
.application-details-card .application-purpose-row {
grid-template-columns: minmax(0, 1fr);
gap: 1rem;
}
/* In-card sub-headings (e.g. "Container labels", "Docker Compose") */
.application-settings-form h4,
.application-settings-form .application-settings-section-body h3 {
@ -3922,7 +4175,7 @@ .command-palette {
overflow: hidden;
border-radius: 10px;
background: var(--coollabs-elevated);
box-shadow: 0 0 0 1px var(--coollabs-hairline), var(--shadow-modal);
box-shadow: 0 0 0 1px var(--coollabs-hairline), var(--shadow-dropdown);
}
.command-palette-header {

View file

@ -15,7 +15,7 @@ @utility apexcharts-xaxistooltip {
}
@utility apexcharts-tooltip-custom {
@apply bg-white dark:bg-coolgray-100 border border-neutral-200 dark:border-coolgray-300 rounded-lg shadow-lg p-3 text-sm;
@apply bg-white dark:bg-coolgray-100 border border-neutral-200 dark:border-coolgray-300 rounded-lg shadow-dropdown p-3 text-sm;
min-width: 160px;
}
@ -59,7 +59,7 @@ @utility input-focus {
/* input, select before */
@utility input-select {
@apply block h-9 px-3 py-1.5 w-full text-sm text-black rounded-md border border-neutral-200 bg-white dark:bg-surface dark:text-fg dark:border-white/[0.08] transition-colors disabled:bg-neutral-100 disabled:text-neutral-400 dark:disabled:bg-white/[0.03] dark:disabled:text-fg-faint;
@apply block h-9 px-3 py-1.5 w-full text-sm text-black rounded-md border border-neutral-200 bg-[var(--coollabs-recessed)] dark:text-fg dark:border-white/[0.08] transition-colors disabled:bg-neutral-100 disabled:text-neutral-400 dark:disabled:bg-white/[0.03] dark:disabled:text-fg-faint;
box-shadow: none;
&:where(.dark, .dark *) {
@ -77,18 +77,18 @@ @utility input-select {
/* Readonly */
@utility input {
@apply dark:read-only:text-neutral-500 dark:read-only:bg-coolgray-100/40 placeholder:text-neutral-300 dark:placeholder:text-neutral-700 read-only:text-neutral-500 read-only:bg-neutral-200;
@apply dark:read-only:text-neutral-500 dark:read-only:bg-coolgray-100/40 placeholder:text-neutral-400 dark:placeholder:text-neutral-500 read-only:text-neutral-500 read-only:bg-neutral-200;
@apply input-select;
@apply focus-visible:outline-none;
&:focus-visible {
border-color: var(--color-accent);
box-shadow: 0 0 0 1px var(--color-accent);
box-shadow: none;
}
&:where(.dark, .dark *):focus-visible {
border-color: var(--color-accent);
box-shadow: 0 0 0 1px var(--color-accent);
box-shadow: none;
}
&:read-only {
@ -116,12 +116,12 @@ @utility select {
&:focus-visible {
border-color: var(--color-accent);
box-shadow: 0 0 0 1px var(--color-accent);
box-shadow: none;
}
&:where(.dark, .dark *):focus-visible {
border-color: var(--color-accent);
box-shadow: 0 0 0 1px var(--color-accent);
box-shadow: none;
}
}
@ -162,7 +162,7 @@ @utility app-tab-active {
}
@utility auth-tooltip {
@apply fixed z-[10000] px-2.5 py-1.5 text-xs font-medium rounded-lg pointer-events-none whitespace-nowrap text-white bg-neutral-900 border border-neutral-700 shadow-lg dark:text-fg dark:bg-raised dark:border-white/10;
@apply fixed z-[10000] px-2.5 py-1.5 text-xs font-medium rounded-lg pointer-events-none whitespace-nowrap text-white bg-neutral-900 border border-neutral-700 shadow-dropdown dark:text-fg dark:bg-raised dark:border-white/10;
}
@utility alert-success {
@ -222,7 +222,10 @@ @utility menu {
}
@utility menu-item {
@apply relative flex gap-2.5 items-center h-8 px-2.5 w-full text-[13px] font-medium rounded-md truncate min-w-0 transition-colors text-nav-text hover:bg-neutral-100 hover:text-nav-active dark:hover:bg-white/[0.05];
/* Translucent hover (like the active pill) so it darkens against any surface,
including the light-mode settings rail's gray fill; on white it matches the
old neutral-100. */
@apply relative flex gap-2.5 items-center h-8 px-2.5 w-full text-[13px] font-medium rounded-md truncate min-w-0 transition-colors text-nav-text hover:bg-black/[0.04] hover:text-nav-active dark:hover:bg-white/[0.05];
}
@utility menu-item-icon {
@apply shrink-0 size-[18px] opacity-90;
@ -245,7 +248,7 @@ @utility nav-section {
/* Indented child rows in a collapsible nav group */
@utility menu-subitem {
/* Label owns text ellipsis; keep this row overflow-visible so the focus ring is not clipped. */
@apply relative flex gap-2.5 items-center h-8 pl-3 pr-2.5 w-full text-[13px] font-medium rounded-md min-w-0 transition-colors text-nav-text hover:bg-neutral-100 hover:text-nav-active dark:hover:bg-white/[0.05];
@apply relative flex gap-2.5 items-center h-8 pl-3 pr-2.5 w-full text-[13px] font-medium rounded-md min-w-0 transition-colors text-nav-text hover:bg-black/[0.04] hover:text-nav-active dark:hover:bg-white/[0.05];
}
@utility menu-subitem-active {
@apply rounded-md bg-black/[0.05] text-nav-active hover:bg-black/[0.05] dark:bg-white/[0.06] dark:hover:bg-white/[0.06];
@ -344,7 +347,7 @@ @utility info-helper {
}
@utility info-helper-popup {
@apply rounded-lg border border-neutral-200 bg-white text-neutral-600 shadow-modal whitespace-normal break-words dark:border-white/10 dark:bg-raised dark:text-fg-dim;
@apply rounded-lg border border-neutral-200 bg-white text-neutral-600 shadow-dropdown whitespace-normal break-words dark:border-white/10 dark:bg-raised dark:text-fg-dim;
}
@utility buyme {
@ -367,6 +370,14 @@ @utility toast {
@apply z-[1];
}
/* Floating notice / popover surface: elevated fill, hairline ring, and the
shared dropdown lift. Toasts, change-pending popovers and the deployments
list share this so they match menus/dropdowns instead of the heavier modal. */
@utility surface-popover {
background: var(--coollabs-elevated);
box-shadow: 0 0 0 1px var(--coollabs-line), var(--shadow-dropdown);
}
@utility dz-button {
@apply p-4 py-10 my-4 w-full font-bold bg-white border dark:border-coolgray-400 dark:text-white dark:bg-transparent dark:hover:bg-coolgray-400;
}

View file

@ -124,3 +124,16 @@ window.scrollToSettingsSection = function scrollToSettingsSection(id) {
rafId = window.requestAnimationFrame(tick);
};
// When a settings sub-section link navigates across pages (href="route#section-id"),
// scroll to that section once the destination page has rendered.
function scrollToHashSettingsSection() {
const hash = window.location.hash;
if (!hash || hash.length < 2) {
return;
}
const id = decodeURIComponent(hash.slice(1));
window.requestAnimationFrame(() => window.scrollToSettingsSection?.(id));
}
document.addEventListener('livewire:navigated', scrollToHashSettingsSection);
document.addEventListener('DOMContentLoaded', scrollToHashSettingsSection);

View file

@ -238,52 +238,104 @@
];
@endphp
@php
$activeMenuLabel = collect($groupedMenuItems)->flatMap(fn ($items) => $items)->firstWhere('active', true)['label'] ?? 'Settings';
@endphp
<aside @class([
'application-settings-navigation min-w-0 xl:self-start',
'is-flush' => $flush,
])>
<nav aria-label="Configuration sections"
class="grid grid-cols-2 gap-0.5 border-y border-neutral-200 py-3 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-1 xl:border-y-0 xl:py-0 dark:border-white/[0.06]">
])
x-data="{
menuOpen: false,
desktop: window.matchMedia('(min-width: 1280px)').matches,
init() {
const mq = window.matchMedia('(min-width: 1280px)');
mq.addEventListener('change', (e) => { this.desktop = e.matches; if (e.matches) { this.menuOpen = false; } });
},
}"
x-on:click.outside="menuOpen = false"
x-on:keydown.escape.window="menuOpen = false">
{{-- Mobile disclosure: tap to reveal the full grouped nav; hidden at xl (the pinned rail). --}}
<button type="button" x-show="!desktop" x-cloak x-on:click="menuOpen = !menuOpen"
:aria-expanded="menuOpen" aria-label="Configuration menu"
:class="menuOpen && 'ring-1 ring-black/10 dark:ring-white/15'"
class="flex h-10 w-full items-center justify-between gap-2 rounded-xl border border-neutral-200 bg-white px-3 text-[13px] font-medium text-black transition-transform duration-100 ease-out hover:bg-neutral-50 active:scale-[0.985] dark:border-white/[0.08] dark:bg-white/[0.05] dark:text-fg dark:hover:bg-white/[0.08]">
<span class="flex min-w-0 items-center gap-2">
<x-reicon name="settings" class="size-4 shrink-0 text-nav-muted" />
<span class="truncate">{{ $activeMenuLabel }}</span>
</span>
<svg class="size-3.5 shrink-0 text-nav-muted transition-transform duration-200 ease-[cubic-bezier(0.23,1,0.32,1)]"
:class="menuOpen && 'rotate-180'" viewBox="0 0 12 12" fill="none" aria-hidden="true">
<path d="m3.5 4.75 2.5 2.5 2.5-2.5" stroke="currentColor" stroke-width="1.25"
stroke-linecap="round" stroke-linejoin="round" />
</svg>
</button>
<nav aria-label="Configuration sections" x-show="desktop || menuOpen" x-collapse.duration.200ms x-cloak
class="mt-2 flex flex-col gap-0.5 rounded-xl border border-neutral-200 bg-white p-2 shadow-[var(--shadow-dropdown)] dark:border-white/[0.08] dark:bg-white/[0.03] xl:mt-0 xl:rounded-none xl:border-0 xl:bg-transparent xl:p-0 xl:shadow-none xl:dark:bg-transparent">
@foreach ($groupedMenuItems as $groupLabel => $groupItems)
@unless ($loop->first)
<div class="hidden xl:block my-2 border-t border-neutral-200 dark:border-white/[0.06]" aria-hidden="true"></div>
<div class="my-2 border-t border-neutral-200 dark:border-white/[0.06]" aria-hidden="true"></div>
@endunless
<div class="nav-section hidden xl:block">{{ $groupLabel }}</div>
<div class="nav-section">{{ $groupLabel }}</div>
@foreach ($groupItems as $menuItem)
<a wire:key="application-settings-link-{{ str($menuItem['label'])->slug() }}"
@class([
'menu-item',
'menu-item-active' => $menuItem['active'],
])
@if ($menuItem['navigate'] ?? true) {{ wireNavigate() }} @endif
href="{{ route($menuItem['route'], $applicationRouteParameters) }}">
<x-reicon :name="$menuIcons[$menuItem['label']] ?? 'settings'" class="menu-item-icon" />
<span class="menu-item-label">{{ $menuItem['label'] }}</span>
@if ($menuItem['badge'] ?? false)
<span class="shrink-0">
<livewire:project.application.server-status-badge :application="$application"
:key="'application-server-status-'.$application->uuid" />
</span>
@php $sections = $pageSections[$menuItem['route']] ?? []; @endphp
<div wire:key="application-settings-group-{{ str($menuItem['label'])->slug() }}"
@if (filled($sections)) x-data="{ open: @js($menuItem['active']), activeSection: '' }" @endif
class="relative">
<a wire:key="application-settings-link-{{ str($menuItem['label'])->slug() }}"
@class([
'menu-item',
'menu-item-active' => $menuItem['active'],
'pr-9' => filled($sections),
])
@if ($menuItem['navigate'] ?? true) {{ wireNavigate() }} @endif
href="{{ route($menuItem['route'], $applicationRouteParameters) }}"
x-on:click="menuOpen = false">
<x-reicon :name="$menuIcons[$menuItem['label']] ?? 'settings'" class="menu-item-icon" />
<span class="menu-item-label">{{ $menuItem['label'] }}</span>
@if ($menuItem['badge'] ?? false)
<span class="shrink-0">
<livewire:project.application.server-status-badge :application="$application"
:key="'application-server-status-'.$application->uuid" />
</span>
@endif
</a>
@if (filled($sections))
{{-- Expand/collapse the page's sub-sections. Active page opens by
default; the label still navigates, the chevron only toggles. --}}
<button type="button"
class="absolute right-1 top-1 flex size-6 items-center justify-center rounded-md text-nav-muted transition-colors hover:bg-black/[0.04] hover:text-nav-active dark:hover:bg-white/[0.06]"
x-on:click.stop.prevent="open = !open" :aria-expanded="open"
aria-label="Toggle {{ $menuItem['label'] }} sections">
<svg class="size-3.5 transition-transform duration-200 ease-[cubic-bezier(0.23,1,0.32,1)]"
:class="open && 'rotate-90'" viewBox="0 0 12 12" fill="none" aria-hidden="true">
<path d="m4.5 3 3 3-3 3" stroke="currentColor" stroke-width="1.25"
stroke-linecap="round" stroke-linejoin="round" />
</svg>
</button>
<div>
<div x-show="open" x-collapse.duration.200ms x-cloak
class="nav-children flex flex-col gap-0.5 py-1">
@foreach ($sections as $section)
@if ($menuItem['active'])
<button type="button" class="menu-subitem"
:class="activeSection === '{{ $section['id'] }}' && 'menu-subitem-active'"
x-on:click="menuOpen = false; activeSection = '{{ $section['id'] }}'; history.replaceState(null, '', '#{{ $section['id'] }}'); window.scrollToSettingsSection?.('{{ $section['id'] }}')">
<span class="menu-item-label text-left">{{ $section['label'] }}</span>
</button>
@else
<a class="menu-subitem"
href="{{ route($menuItem['route'], $applicationRouteParameters) }}#{{ $section['id'] }}"
{{ wireNavigate() }} x-on:click="menuOpen = false">
<span class="menu-item-label text-left">{{ $section['label'] }}</span>
</a>
@endif
@endforeach
</div>
</div>
@endif
</a>
@if ($menuItem['active'] && count($pageSections[$menuItem['route']] ?? []) >= 4)
<div class="nav-children hidden flex-col gap-0.5 py-1 xl:flex"
x-data="{
activeSection: '',
scrollToSection(id) {
this.activeSection = id;
window.scrollToSettingsSection?.(id);
},
}">
@foreach ($pageSections[$menuItem['route']] as $section)
<button type="button" class="menu-subitem"
:class="activeSection === '{{ $section['id'] }}' && 'menu-subitem-active'"
@click="scrollToSection('{{ $section['id'] }}')">
<span class="menu-item-label text-left">{{ $section['label'] }}</span>
</button>
@endforeach
</div>
@endif
</div>
@endforeach
@endforeach
</nav>

View file

@ -2,7 +2,7 @@
<div class="mx-auto mb-6 w-full max-w-3xl">
<div
class="grid grid-cols-3 overflow-hidden rounded-[10px] border border-neutral-200 bg-white dark:border-white/[0.08] dark:bg-white/[0.025]">
class="grid grid-cols-3 overflow-hidden rounded-[10px] border border-neutral-200 bg-white dark:border-white/[0.08] dark:bg-white/[0.05]">
@for ($i = 1; $i <= $totalSteps; $i++)
<div @class([
'flex min-h-10 items-center justify-center gap-2 border-r border-neutral-200 px-3 text-[11px] font-medium last:border-r-0 dark:border-white/[0.08]',

View file

@ -9,8 +9,7 @@ class="flex h-8 items-center justify-center gap-1.5 rounded-lg px-2 text-amber-7
</button>
<div x-show="open" x-cloak x-transition.opacity role="dialog"
class="fixed top-14 left-1/2 z-[1100] w-[calc(100vw-2rem)] max-w-sm -translate-x-1/2 rounded-lg p-3 lg:absolute lg:top-full lg:right-0 lg:left-auto lg:mt-2 lg:translate-x-0"
style="background: var(--coollabs-elevated); box-shadow: 0 0 0 1px var(--coollabs-line), var(--shadow-modal);">
class="surface-popover fixed top-14 left-1/2 z-[1100] w-[calc(100vw-2rem)] max-w-sm -translate-x-1/2 rounded-lg p-3 lg:absolute lg:top-full lg:right-0 lg:left-auto lg:mt-2 lg:translate-x-0">
<div class="flex items-start gap-2.5">
<span
class="flex size-7 shrink-0 items-center justify-center rounded-md bg-amber-100 text-amber-700 dark:bg-warning/10 dark:text-warning">

View file

@ -60,15 +60,21 @@
</svg>
</button>
<div x-ref="panel" x-show="dropdownOpen" @click.away="if (! {{ $inline ? 'true' : 'false' }}) close()" x-transition:enter="ease-out duration-200"
x-transition:enter-start="-translate-y-2" x-transition:enter-end="translate-y-0"
<div x-ref="panel" x-show="dropdownOpen" @click.away="if (! {{ $inline ? 'true' : 'false' }}) close()"
x-transition:enter="transition ease-out duration-150"
x-transition:enter-start="opacity-0 -translate-y-1 scale-[0.98]"
x-transition:enter-end="opacity-100 translate-y-0 scale-100"
x-transition:leave="transition ease-in duration-100"
x-transition:leave-start="opacity-100 translate-y-0 scale-100"
x-transition:leave-end="opacity-0 -translate-y-1 scale-[0.98]"
:style="panelStyles" @class([
'origin-top' => true,
'mt-1 w-full' => $inline,
'absolute top-full z-50 mt-1 min-w-max max-w-[calc(100vw-1rem)] md:top-0 md:mt-6' => ! $inline,
]) x-cloak>
<div @class([
'border border-neutral-300 bg-white p-1 dark:border-coolgray-300',
'shadow-sm dark:bg-coolgray-200' => ! $inline,
'shadow-[var(--shadow-dropdown)] dark:bg-coolgray-200' => ! $inline,
'border-0 bg-transparent shadow-none dark:border-0 dark:bg-transparent' => $inline,
$panelClass,
])>

View file

@ -80,6 +80,10 @@
</button>
@if ($authDisabled || filled($tooltip))
<div x-ref="tip" x-show="visible" x-cloak :id="$id('button-tooltip')" role="tooltip"
x-transition:enter="transition-opacity ease-out duration-100"
x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100"
x-transition:leave="transition-opacity ease-in duration-75"
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0"
class="auth-tooltip">
{{ $tooltip ?: 'You do not have permission to perform this action.' }}
</div>

View file

@ -49,7 +49,7 @@ class="flex min-w-0 grow items-center gap-1.5 break-words text-[12px] text-neutr
@endif
<span
class="pointer-events-none absolute inset-0 rounded-[5px] border border-neutral-300 bg-white shadow-[inset_0_1px_1px_rgb(0_0_0/0.04)] transition-colors group-hover:border-neutral-400 peer-checked:border-coollabs peer-checked:bg-coollabs peer-focus-visible:ring-2 peer-focus-visible:ring-coollabs/25 peer-focus-visible:ring-offset-2 peer-disabled:opacity-50 dark:border-white/[0.14] dark:bg-white/[0.045] dark:shadow-none dark:group-hover:border-white/[0.22] dark:peer-checked:border-warning dark:peer-checked:bg-warning dark:peer-focus-visible:ring-warning/30 dark:peer-focus-visible:ring-offset-base"></span>
class="pointer-events-none absolute inset-0 rounded-[5px] border border-neutral-300 bg-white shadow-[inset_0_1px_1px_rgb(0_0_0/0.04)] transition-[color,background-color,border-color,box-shadow] duration-150 ease-out group-hover:border-neutral-400 peer-checked:border-coollabs peer-checked:bg-coollabs peer-focus-visible:ring-2 peer-focus-visible:ring-coollabs/25 peer-focus-visible:ring-offset-2 peer-disabled:opacity-50 dark:border-white/[0.14] dark:bg-white/[0.045] dark:shadow-none dark:group-hover:border-white/[0.22] dark:peer-checked:border-warning dark:peer-checked:bg-warning dark:peer-focus-visible:ring-warning/30 dark:peer-focus-visible:ring-offset-base"></span>
<svg class="pointer-events-none absolute inset-0 m-auto size-3 scale-75 text-white opacity-0 transition-[opacity,transform] peer-checked:scale-100 peer-checked:opacity-100 dark:text-black"
viewBox="0 0 12 12" fill="none" aria-hidden="true">
<path d="m2.25 6.15 2.35 2.3 5.15-5" stroke="currentColor" stroke-width="1.8"

View file

@ -132,7 +132,7 @@ class="flex-1 min-w-[120px] text-sm border-0 outline-none bg-transparent p-0 foc
{{-- Dropdown Options --}}
<div x-show="open && !{{ $disabled ? 'true' : 'false' }}" x-transition
class="absolute z-50 w-full mt-1 bg-white dark:bg-coolgray-100 border border-neutral-300 dark:border-coolgray-400 rounded shadow-lg max-h-60 overflow-auto scrollbar">
class="absolute z-50 w-full mt-1 bg-white dark:bg-coolgray-100 border border-neutral-300 dark:border-coolgray-400 rounded shadow-dropdown max-h-60 overflow-auto scrollbar">
<template x-if="filteredOptions.length === 0">
<div class="px-3 py-2 text-sm text-neutral-500 dark:text-neutral-400">
@ -277,7 +277,7 @@ class="shrink-0 text-neutral-400 px-2 {{ $disabled ? 'cursor-not-allowed' : 'cur
{{-- Dropdown Options --}}
<div x-show="open && !{{ $disabled ? 'true' : 'false' }}" x-transition
class="absolute z-50 w-full mt-1 bg-white dark:bg-coolgray-100 border border-neutral-300 dark:border-coolgray-400 rounded shadow-lg max-h-60 overflow-auto scrollbar">
class="absolute z-50 w-full mt-1 bg-white dark:bg-coolgray-100 border border-neutral-300 dark:border-coolgray-400 rounded shadow-dropdown max-h-60 overflow-auto scrollbar">
<template x-if="filteredOptions.length === 0">
<div class="px-3 py-2 text-sm text-neutral-500 dark:text-neutral-400">

View file

@ -69,8 +69,12 @@
},
}" class="contents">
<div x-ref="tooltip" x-show="visible" x-cloak role="tooltip" x-text="text"
x-transition:enter="transition-opacity ease-out duration-100"
x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100"
x-transition:leave="transition-opacity ease-in duration-75"
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0"
:style="`left: ${x}px; top: ${y}px;`"
:class="[below ? '' : '-translate-y-full', positioned ? 'visible' : 'invisible']"
class="pointer-events-none fixed z-[10000] whitespace-nowrap rounded-lg border border-neutral-700 bg-neutral-900 px-2 py-1 text-xs font-medium text-white shadow-lg dark:border-white/10 dark:bg-raised">
class="pointer-events-none fixed z-[10000] whitespace-nowrap rounded-lg border border-neutral-700 bg-neutral-900 px-2 py-1 text-xs font-medium text-white shadow-dropdown dark:border-white/10 dark:bg-raised">
</div>
</div>

View file

@ -211,14 +211,19 @@
<template x-teleport="body">
<div x-show="modalOpen"
class="fixed inset-0 z-99 flex min-h-full items-center justify-center overflow-y-auto p-4" x-cloak>
<div x-show="modalOpen" class="absolute inset-0 bg-black/50 backdrop-blur-[2px]">
<div x-show="modalOpen" class="absolute inset-0 bg-black/50 backdrop-blur-[2px]"
x-transition:enter="transition-opacity ease-out duration-200"
x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100"
x-transition:leave="transition-opacity ease-in duration-150"
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0">
</div>
<div x-show="modalOpen" x-trap.inert.noscroll="modalOpen" x-transition:enter="ease-out duration-100"
x-transition:enter-start="opacity-0 -translate-y-2 sm:scale-95"
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave="ease-in duration-100"
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave-end="opacity-0 -translate-y-2 sm:scale-95"
<div x-show="modalOpen" x-trap.inert.noscroll="modalOpen"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
class="application-settings-form application-settings-section relative flex max-h-[calc(100dvh-2rem)] w-full flex-col lg:min-w-[36rem] lg:max-w-2xl"
style="box-shadow: 0 0 0 1px var(--coollabs-hairline), var(--shadow-modal)">
<header class="flex-nowrap!">

View file

@ -39,18 +39,18 @@
<div x-show="modalOpen"
x-init="$watch('modalOpen', value => { if(value) { $nextTick(() => { const firstInput = $el.querySelector('input, textarea, select'); firstInput?.focus(); }) } })"
class="fixed inset-0 z-99 overflow-y-auto">
<div x-show="modalOpen" x-transition:enter="ease-out duration-100" x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100" x-transition:leave="ease-in duration-100"
<div x-show="modalOpen" x-transition:enter="transition-opacity ease-out duration-200" x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100" x-transition:leave="transition-opacity ease-in duration-150"
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0"
class="absolute inset-0 w-full h-full bg-black/50 backdrop-blur-[2px]"></div>
<div @if ($closeOutside) @click.self="modalOpen=false" @endif class="relative flex min-h-full items-start justify-center p-2 sm:items-center sm:p-4">
<div x-show="modalOpen" x-trap.inert.noscroll="modalOpen"
x-transition:enter="ease-out duration-100"
x-transition:enter-start="opacity-0 -translate-y-2 sm:scale-95"
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave="ease-in duration-100"
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave-end="opacity-0 -translate-y-2 sm:scale-95"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
@class([
'application-settings-form application-settings-section relative flex max-h-[calc(100dvh-2rem)] w-full flex-col overflow-hidden',
'lg:w-[95vw]! lg:max-w-7xl!' => $isLarge,

View file

@ -240,6 +240,6 @@ class="menu-item w-8 shrink-0 justify-center px-0">
</div>
<div x-show="collapsed && tooltip.show" x-cloak x-transition.opacity.duration.100ms
:style="`left: ${tooltip.x}px; top: ${tooltip.y}px;`"
class="fixed z-[10000] -translate-y-1/2 px-2 py-1 text-xs font-medium rounded-lg bg-neutral-900 dark:bg-raised text-white whitespace-nowrap pointer-events-none shadow-lg border border-neutral-700 dark:border-white/10"
class="fixed z-[10000] -translate-y-1/2 px-2 py-1 text-xs font-medium rounded-lg bg-neutral-900 dark:bg-raised text-white whitespace-nowrap pointer-events-none shadow-dropdown border border-neutral-700 dark:border-white/10"
x-text="tooltip.text"></div>
</nav>

View file

@ -53,8 +53,7 @@
x-transition:leave-end="translate-y-3 opacity-0"
class="fixed right-4 z-999 {{ $position === 'top-right' ? 'top-16' : 'bottom-4' }}">
<template x-if="iconOnly">
<button type="button" @click="restore()" aria-label="Restore warning" class="flex rounded-lg p-2"
style="background: var(--coollabs-elevated); box-shadow: 0 0 0 1px var(--coollabs-line), var(--shadow-modal);">
<button type="button" @click="restore()" aria-label="Restore warning" class="surface-popover flex rounded-lg p-2">
@isset($icon)
<span
class="flex size-7 shrink-0 items-center justify-center rounded-md bg-amber-100 text-amber-700 dark:bg-warning/10 dark:text-warning">
@ -65,12 +64,11 @@ class="flex size-7 shrink-0 items-center justify-center rounded-md bg-amber-100
</template>
<template x-if="!iconOnly">
<div class="relative flex items-start gap-2.5 rounded-lg p-3 pr-10"
<div class="surface-popover relative flex items-start gap-2.5 rounded-lg p-3 pr-10"
:class="compact
? 'w-[calc(100vw-2rem)] max-w-sm cursor-pointer'
: 'w-[calc(100vw-2rem)] max-w-sm'"
@click="restore()"
style="background: var(--coollabs-elevated); box-shadow: 0 0 0 1px var(--coollabs-line), var(--shadow-modal);">
@click="restore()">
@isset($icon)
<div
class="flex size-7 shrink-0 items-center justify-center rounded-md bg-amber-100 text-amber-700 dark:bg-warning/10 dark:text-warning">

View file

@ -11,7 +11,7 @@ class="fixed bottom-0 right-0 w-full h-auto duration-300 ease-out sm:px-5 sm:pb-
{{ $customActions }}
@else
<div
class="flex lg:items-center flex-col justify-between w-full h-full max-w-4xl p-6 mx-auto bg-white border shadow-lg lg:border-t dark:border-coolgray-300 border-neutral-200 dark:bg-coolgray-100 lg:p-8 lg:flex-row sm:rounded-sm">
class="flex lg:items-center flex-col justify-between w-full h-full max-w-4xl p-6 mx-auto bg-white border shadow-modal lg:border-t dark:border-coolgray-300 border-neutral-200 dark:bg-coolgray-100 lg:p-8 lg:flex-row sm:rounded-sm">
<div
class="flex flex-col items-start h-full pb-6 text-xs lg:items-center lg:flex-row lg:pb-0 lg:pr-6 lg:space-x-5 dark:text-neutral-300">
@if (isset($icon))

View file

@ -9,8 +9,7 @@ class="flex h-8 items-center justify-center gap-1.5 rounded-lg px-2 text-amber-7
</button>
<div x-show="open" x-cloak x-transition.opacity role="dialog"
class="fixed top-14 left-1/2 z-[1100] w-[calc(100vw-2rem)] max-w-sm -translate-x-1/2 rounded-lg p-3 lg:absolute lg:top-full lg:right-0 lg:left-auto lg:mt-2 lg:translate-x-0"
style="background: var(--coollabs-elevated); box-shadow: 0 0 0 1px var(--coollabs-line), var(--shadow-modal);">
class="surface-popover fixed top-14 left-1/2 z-[1100] w-[calc(100vw-2rem)] max-w-sm -translate-x-1/2 rounded-lg p-3 lg:absolute lg:top-full lg:right-0 lg:left-auto lg:mt-2 lg:translate-x-0">
<div class="flex items-start gap-2.5">
<span
class="flex size-7 shrink-0 items-center justify-center rounded-md bg-amber-100 text-amber-700 dark:bg-warning/10 dark:text-warning">

View file

@ -91,7 +91,7 @@
x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-75" x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
class="absolute z-20 top-full mt-1 w-56 -ml-2 bg-white dark:bg-coolgray-100 rounded-md shadow-lg py-1 border border-neutral-200 dark:border-coolgray-200 max-h-96 overflow-y-auto scrollbar">
class="absolute z-20 top-full mt-1 w-56 -ml-2 bg-white dark:bg-coolgray-100 rounded-md shadow-[var(--shadow-dropdown)] py-1 border border-neutral-200 dark:border-coolgray-200 max-h-96 overflow-y-auto scrollbar">
@foreach ($projects as $project)
<a href="{{ route('project.show', ['project_uuid' => $project->uuid]) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200 {{ $project->uuid === $currentProjectUuid ? 'dark:text-warning font-semibold' : '' }}"
@ -130,7 +130,7 @@ class="absolute z-20 top-full mt-1 left-0 sm:left-auto max-w-[calc(100vw-1rem)]"
x-init="$nextTick(() => { const rect = $el.getBoundingClientRect(); if (rect.right > window.innerWidth) { $el.style.left = 'auto'; $el.style.right = '0'; } })">
<!-- Environment List -->
<div
class="relative w-48 bg-white dark:bg-coolgray-100 rounded-md shadow-lg py-1 border border-neutral-200 dark:border-coolgray-200 max-h-96 overflow-y-auto scrollbar">
class="relative w-48 bg-white dark:bg-coolgray-100 rounded-md shadow-[var(--shadow-dropdown)] py-1 border border-neutral-200 dark:border-coolgray-200 max-h-96 overflow-y-auto scrollbar">
@foreach ($environments as $environment)
@php
$envDatabases = collect()
@ -205,7 +205,7 @@ class="flex items-center gap-2 px-4 py-2 text-sm hover:bg-neutral-100 dark:hover
:style="'position: absolute; left: 100%; top: ' + (envPositions['{{ $environment->uuid }}'] || 0) + 'px; z-index: 30;'"
class="flex flex-col sm:flex-row items-start pl-1">
<div
class="relative w-56 bg-white dark:bg-coolgray-100 rounded-md shadow-lg py-1 border border-neutral-200 dark:border-coolgray-200 max-h-96 overflow-y-auto scrollbar">
class="relative w-56 bg-white dark:bg-coolgray-100 rounded-md shadow-[var(--shadow-dropdown)] py-1 border border-neutral-200 dark:border-coolgray-200 max-h-96 overflow-y-auto scrollbar">
@foreach ($envResources as $envResource)
@php
$resType = $envResource['type'];

View file

@ -0,0 +1,34 @@
@props([
'title',
'subtitle' => null,
'href' => null,
'actionLabel' => 'View all',
'icon' => 'arrow-right',
])
{{--
Shared section heading: a 14px title, an optional 11px muted subtitle, and an
optional right-aligned action. Pass `href` for the default ghost "View all"
button, or an `<x-slot:actions>` for a custom control.
--}}
<div {{ $attributes->merge(['class' => 'mb-3 min-w-0']) }}>
<div class="flex items-center justify-between gap-4">
<h2 class="min-w-0 truncate text-[14px]! leading-5! font-semibold! text-black dark:text-fg">
{{ $title }}
</h2>
@isset($actions)
<div class="shrink-0">{{ $actions }}</div>
@elseif (filled($href))
<a href="{{ $href }}" {{ wireNavigate() }}
class="group inline-flex h-7 shrink-0 items-center gap-1.5 rounded-md border border-neutral-200 bg-white px-2.5 text-[12px] font-medium text-neutral-600 transition-[color,background-color,transform] duration-100 ease-out hover:bg-neutral-100 hover:text-black active:scale-[0.97] dark:border-white/[0.08] dark:bg-white/[0.06] dark:text-fg-dim dark:hover:bg-white/[0.1] dark:hover:text-fg">
{{ $actionLabel }}
@if ($icon)
<x-reicon :name="$icon" class="size-3 opacity-70 transition-transform duration-150 ease-out group-hover:translate-x-0.5" />
@endif
</a>
@endisset
</div>
@if (filled($subtitle))
<p class="mt-0.5 text-[11px] text-neutral-500 dark:text-fg-faint">{{ $subtitle }}</p>
@endif
</div>

View file

@ -28,7 +28,7 @@ class="button button-highlighted">
<div class="grid grid-cols-1 gap-3 p-3 sm:grid-cols-2 lg:grid-cols-4">
@foreach ($tokens as $token)
<a wire:key="{{ $provider }}-token-{{ $token->id }}"
class="group flex min-h-28 min-w-0 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]"
class="group flex min-h-28 min-w-0 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]"
href="{{ route('server.create.token', ['type' => $routeType ?: $provider, 'token_uuid' => $token->uuid]) }}"
{{ wireNavigate() }}>
<div class="flex items-start gap-3">

View file

@ -16,22 +16,26 @@
<div x-show="slideOverOpen" @if (!$closeWithX) @keydown.window.escape="slideOverOpen=false" @endif
class="relative z-99 ">
<div x-show="slideOverOpen" @if (!$closeWithX) @click="slideOverOpen = false" @endif
x-transition:enter="transition-opacity ease-out duration-300"
x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100"
x-transition:leave="transition-opacity ease-in duration-200"
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0"
class="fixed inset-0 dark:bg-black/60 backdrop-blur-xs"></div>
<div class="fixed inset-0 overflow-hidden">
<div class="absolute inset-0 overflow-hidden ">
<div class="fixed inset-y-0 right-0 flex max-w-full pl-10">
<div x-show="slideOverOpen"
@if (!$closeWithX) @click.away="slideOverOpen = false" @endif
x-transition:enter="transform transition ease-in-out duration-100 sm:duration-300"
x-transition:enter="transform transition ease-[cubic-bezier(0.32,0.72,0,1)] duration-200 sm:duration-300"
x-transition:enter-start="translate-x-full" x-transition:enter-end="translate-x-0"
x-transition:leave="transform transition ease-in-out duration-100 sm:duration-300"
x-transition:leave="transform transition ease-in duration-200"
x-transition:leave-start="translate-x-0" x-transition:leave-end="translate-x-full"
@class([
'max-w-xl w-screen' => !$fullScreen,
'max-w-4xl w-screen' => $fullScreen,
])>
<div
class="flex flex-col h-full py-6 overflow-hidden border-l shadow-lg bg-neutral-50 dark:bg-base dark:border-neutral-800 border-neutral-200">
class="flex flex-col h-full py-6 overflow-hidden border-l shadow-modal bg-neutral-50 dark:bg-base dark:border-neutral-800 border-neutral-200">
<div class="px-4 pb-4 sm:px-5">
<div class="flex items-start justify-between pb-1">
<h2 class="text-2xl leading-6" id="slide-over-title">

View file

@ -1,4 +1,4 @@
@props(['status', 'title' => 'Application status', 'containerName' => 'Container'])
@props(['status', 'title' => 'Application status', 'containerName' => 'Container', 'align' => 'left'])
@php
$rawStatus = str((string) $status)->lower()->trim()->value();
@ -62,8 +62,12 @@ class="cursor-pointer">
</span>
</x-status-badge>
<div x-cloak x-show="open" x-transition.origin.top.left
class="listbox-panel top-8! right-auto! left-0! z-[90]! w-[min(16rem,calc(100vw-1.5rem))]! min-w-0! sm:w-64! sm:min-w-64!" role="menu">
<div x-cloak x-show="open" x-transition.origin.top.{{ $align === 'right' ? 'right' : 'left' }}
@class([
'listbox-panel top-8! z-[90]! w-[min(16rem,calc(100vw-1.5rem))]! min-w-0! sm:w-64! sm:min-w-64!',
'right-auto! left-0!' => $align !== 'right',
'left-auto! right-0!' => $align === 'right',
]) role="menu">
<div class="px-3 py-2 text-[11px] font-medium text-neutral-400 dark:text-fg-faint">{{ $title }}</div>
<div class="listbox-option cursor-default! gap-2.5!">
<span @class([

View file

@ -36,7 +36,13 @@
</div>
<div x-ref="panel" x-show="open" x-cloak :style="panelStyle"
class="listbox-panel fixed! right-auto! bottom-auto! z-[90]! mt-0! {{ $panelClass }}" role="{{ $role }}"
x-transition:enter="transition ease-out duration-150"
x-transition:enter-start="opacity-0 -translate-y-1 scale-[0.98]"
x-transition:enter-end="opacity-100 translate-y-0 scale-100"
x-transition:leave="transition ease-in duration-100"
x-transition:leave-start="opacity-100 translate-y-0 scale-100"
x-transition:leave-end="opacity-0 -translate-y-1 scale-[0.98]"
class="listbox-panel origin-top fixed! right-auto! bottom-auto! z-[90]! mt-0! {{ $panelClass }}" role="{{ $role }}"
@if ($multiselectable) aria-multiselectable="true" @endif>
{{ $slot }}
</div>

View file

@ -14,7 +14,7 @@ class="terminal-theme-trigger flex h-8 items-center gap-2 rounded-md px-2.5 text
</button>
<div x-cloak x-show="themeOpen" x-transition.origin.top.right
class="console-theme-selector absolute top-11 right-0 z-50 max-h-80 w-56 overflow-y-auto rounded-lg border border-neutral-200 bg-white p-1 shadow-[0_18px_50px_rgba(0,0,0,0.18)] dark:border-white/[0.1] dark:bg-[#111113] dark:shadow-[0_18px_50px_rgba(0,0,0,0.55)]">
class="console-theme-selector absolute top-11 right-0 z-50 max-h-80 w-56 overflow-y-auto rounded-lg border border-neutral-200 bg-white p-1 shadow-[var(--shadow-dropdown)] dark:border-white/[0.1] dark:bg-[#111113]">
@foreach ($themes as $theme)
<button type="button"
class="flex h-8 w-full items-center gap-2 rounded-md px-2 text-left text-[11px] text-neutral-600 transition-colors hover:bg-neutral-100 hover:text-neutral-950 dark:text-white/65 dark:hover:bg-white/[0.07] dark:hover:text-white"

View file

@ -0,0 +1,185 @@
@props(['variant' => 'full'])
@php($checkPath = 'm2.5 6.25 2.1 2.1 4.9-5')
@if ($variant === 'menu')
{{-- Compact list for the profile dropdown. The parent controls visibility;
all theme state/logic comes from the shared window.themeControls(). --}}
<div x-data="themeControls()" class="grid gap-0.5">
@foreach ([
['value' => 'light', 'label' => 'Light'],
['value' => 'system', 'label' => 'System'],
['value' => 'dark', 'label' => 'Dark'],
['value' => 'custom', 'label' => 'Custom'],
] as $option)
@if ($option['value'] === 'custom')
<div class="relative" @click.outside="pickerOpen = false">
<button type="button" @click="chooseCustom()" aria-haspopup="dialog" :aria-expanded="pickerOpen"
class="flex h-8 w-full items-center justify-between rounded-md px-2 text-left text-xs text-neutral-600 transition-colors hover:bg-neutral-200 hover:text-neutral-950 dark:text-fg-dim dark:hover:bg-white/[0.06] dark:hover:text-fg">
<span class="flex items-center gap-2">
<span class="size-3.5 rounded-full border border-white/20"
:style="`background: ${themeColor}`"></span>
Custom
</span>
<svg x-show="theme === 'custom'" class="size-3.5 text-coollabs dark:text-warning"
viewBox="0 0 12 12" fill="none" aria-hidden="true">
<path d="{{ $checkPath }}" stroke="currentColor" stroke-width="1.4"
stroke-linecap="round" stroke-linejoin="round" />
</svg>
</button>
<x-theme-controls.picker position="left-full top-0 ml-2" />
</div>
@else
<button type="button" @click="setTheme('{{ $option['value'] }}')"
class="flex h-8 w-full items-center justify-between rounded-md px-2 text-left text-xs text-neutral-600 transition-colors hover:bg-neutral-200 hover:text-neutral-950 dark:text-fg-dim dark:hover:bg-white/[0.06] dark:hover:text-fg">
<span>{{ $option['label'] }}</span>
<svg x-show="theme === '{{ $option['value'] }}'"
class="size-3.5 text-coollabs dark:text-warning" viewBox="0 0 12 12" fill="none"
aria-hidden="true">
<path d="{{ $checkPath }}" stroke="currentColor" stroke-width="1.4"
stroke-linecap="round" stroke-linejoin="round" />
</svg>
</button>
@endif
@endforeach
<div class="my-1 h-px bg-neutral-200 dark:bg-white/[0.07]"></div>
<div class="px-2 pt-1 pb-0.5 text-[10px] font-medium tracking-wide text-neutral-400 uppercase dark:text-fg-faint">
Page width
</div>
@foreach ([
['value' => 'full', 'label' => 'Full width'],
['value' => 'centered', 'label' => 'Centered'],
] as $option)
<button type="button" @click="setWidth('{{ $option['value'] }}')"
class="flex h-8 w-full items-center justify-between rounded-md px-2 text-left text-xs text-neutral-600 transition-colors hover:bg-neutral-200 hover:text-neutral-950 dark:text-fg-dim dark:hover:bg-white/[0.06] dark:hover:text-fg">
<span>{{ $option['label'] }}</span>
<svg x-show="pageWidth === '{{ $option['value'] }}'"
class="size-3.5 text-coollabs dark:text-warning" viewBox="0 0 12 12" fill="none"
aria-hidden="true">
<path d="{{ $checkPath }}" stroke="currentColor" stroke-width="1.4"
stroke-linecap="round" stroke-linejoin="round" />
</svg>
</button>
@endforeach
</div>
@else
{{-- Full card grid for the Appearance settings page. --}}
<div x-data="themeControls()" class="mt-8 flex w-full max-w-none flex-col gap-6 lg:mt-3">
<section class="application-settings-section">
<div class="application-settings-section-header">
<div>
<h2>Color theme</h2>
<p>Choose the color scheme used in this browser.</p>
</div>
</div>
<div class="application-settings-section-body grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
@foreach ([
['value' => 'light', 'label' => 'Light', 'description' => 'Bright surfaces and dark text.', 'preview' => 'bg-white'],
['value' => 'system', 'label' => 'System', 'description' => 'Follow your operating system.', 'preview' => 'bg-gradient-to-r from-white via-neutral-400 to-[#050505]'],
['value' => 'dark', 'label' => 'Dark', 'description' => 'Dark surfaces and soft contrast.', 'preview' => 'bg-[#181818]'],
['value' => 'custom', 'label' => 'Custom', 'description' => 'Tint light or dark surfaces with any color.', 'preview' => ''],
] as $option)
@if ($option['value'] === 'custom')
{{-- Clicking the card opens the custom color picker popover (color + light/dark). --}}
<div class="relative" @click.outside="pickerOpen = false">
<div role="button" tabindex="0" @click="chooseCustom()" @keydown.enter.prevent="chooseCustom()"
aria-haspopup="dialog" :aria-expanded="pickerOpen"
class="group block w-full overflow-hidden rounded-[10px] border border-neutral-200 bg-white text-left transition-[border-color,box-shadow] hover:border-neutral-300 hover:shadow-sm dark:border-white/[0.07] dark:bg-white/[0.05] dark:hover:border-white/[0.12]"
:class="theme === 'custom'
? 'ring-1 ring-coollabs/30 border-coollabs/40 dark:ring-warning/30 dark:border-warning/40'
: ''">
<div class="h-20 border-b border-neutral-200 dark:border-white/[0.07]"
:style="`background: color-mix(in oklab, ${themeColor} 28%, ${customMode === 'light' ? 'oklch(97% 0 0)' : 'oklch(17.35% 0.0020 286.18)'})`">
<div class="flex h-full items-center justify-center">
<div class="h-10 w-20 rounded-md border border-white/15 p-1 shadow-sm">
<div class="h-full w-full rounded-sm" :style="`background: ${themeColor}`"></div>
</div>
</div>
</div>
<div class="p-3">
<div class="flex items-center justify-between gap-2">
<span class="text-sm font-semibold text-black dark:text-fg">{{ $option['label'] }}</span>
<x-reicon name="check-circle" class="size-4 text-coollabs dark:text-warning"
x-show="theme === 'custom'" x-cloak />
</div>
<p class="mt-1 text-xs leading-5 text-neutral-500 dark:text-fg-dim">
{{ $option['description'] }}
</p>
</div>
</div>
<x-theme-controls.picker position="left-0 top-full mt-2" />
</div>
@else
<div role="button" tabindex="0"
@click="setTheme('{{ $option['value'] }}')"
@keydown.enter.prevent="setTheme('{{ $option['value'] }}')"
class="group relative overflow-hidden rounded-[10px] border border-neutral-200 bg-white text-left transition-[border-color,box-shadow] hover:border-neutral-300 hover:shadow-sm dark:border-white/[0.07] dark:bg-white/[0.05] dark:hover:border-white/[0.12]"
:class="theme === '{{ $option['value'] }}'
? 'ring-1 ring-coollabs/30 border-coollabs/40 dark:ring-warning/30 dark:border-warning/40'
: ''">
<div class="h-20 {{ $option['preview'] }} border-b border-neutral-200 dark:border-white/[0.07]">
<div class="flex h-full items-center justify-center">
<div
class="h-8 w-20 rounded-md border border-black/10 bg-neutral-100/80 shadow-sm dark:border-white/10 dark:bg-black/20">
</div>
</div>
</div>
<div class="p-3">
<div class="flex items-center justify-between gap-2">
<span class="text-sm font-semibold text-black dark:text-fg">
{{ $option['label'] }}
</span>
<x-reicon name="check-circle" class="size-4 text-coollabs dark:text-warning"
x-show="theme === '{{ $option['value'] }}'" x-cloak />
</div>
<p class="mt-1 text-xs leading-5 text-neutral-500 dark:text-fg-dim">
{{ $option['description'] }}
</p>
</div>
</div>
@endif
@endforeach
</div>
</section>
<section class="application-settings-section">
<div class="application-settings-section-header">
<div>
<h2>Page width</h2>
<p>Choose how content uses the available browser width.</p>
</div>
</div>
<div class="application-settings-section-body grid gap-3 sm:grid-cols-2">
@foreach ([
['value' => 'full', 'label' => 'Full width', 'description' => 'Use all available space for page content.'],
['value' => 'centered', 'label' => 'Centered', 'description' => 'Keep content centered at a comfortable maximum width.'],
] as $option)
<button type="button" @click="setWidth('{{ $option['value'] }}')"
class="group overflow-hidden rounded-[10px] border border-neutral-200 bg-white text-left transition-[border-color,box-shadow] hover:border-neutral-300 hover:shadow-sm dark:border-white/[0.07] dark:bg-white/[0.05] dark:hover:border-white/[0.12]"
:class="pageWidth === '{{ $option['value'] }}'
? 'ring-1 ring-coollabs/30 border-coollabs/40 dark:ring-warning/30 dark:border-warning/40'
: ''">
<div class="flex h-20 items-center border-b border-neutral-200 bg-neutral-50 px-4 dark:border-white/[0.07] dark:bg-black/15">
<div class="flex h-11 w-full gap-1.5 rounded-md border border-neutral-300 bg-white p-1.5 dark:border-white/15 dark:bg-[#181818]">
<div class="w-3 shrink-0 rounded-sm bg-neutral-200 dark:bg-white/10"></div>
<div @class([
'h-full rounded-sm bg-neutral-200 dark:bg-white/10',
'w-full' => $option['value'] === 'full',
'mx-auto w-2/3' => $option['value'] === 'centered',
])></div>
</div>
</div>
<div class="p-3">
<div class="flex items-center justify-between gap-2">
<span class="text-sm font-semibold text-black dark:text-fg">{{ $option['label'] }}</span>
<x-reicon name="check-circle" class="size-4 text-coollabs dark:text-warning"
x-show="pageWidth === '{{ $option['value'] }}'" x-cloak />
</div>
<p class="mt-1 text-xs leading-5 text-neutral-500 dark:text-fg-dim">{{ $option['description'] }}</p>
</div>
</button>
@endforeach
</div>
</section>
</div>
@endif

View file

@ -0,0 +1,48 @@
@props(['position' => 'left-0 top-full mt-2'])
{{-- Custom color picker popover: color field + light/dark mode in one panel.
Renders inline (no own Alpine scope) so it uses the parent themeControls()
state: pickerOpen, themeColor, customMode, previewThemeColor, saveThemeColor,
setCustomMode. --}}
<div x-show="pickerOpen" x-cloak
x-transition:enter="transition ease-out duration-150"
x-transition:enter-start="opacity-0 -translate-y-1 scale-[0.98]"
x-transition:enter-end="opacity-100 translate-y-0 scale-100"
x-transition:leave="transition ease-in duration-100"
x-transition:leave-start="opacity-100 translate-y-0 scale-100"
x-transition:leave-end="opacity-0 -translate-y-1 scale-[0.98]"
role="dialog" aria-label="Custom color"
class="absolute z-[60] w-60 origin-top {{ $position }} rounded-xl border border-neutral-200 bg-white p-3 text-left shadow-dropdown dark:border-white/[0.1] dark:bg-panel">
<div class="mb-2 text-[11px] font-semibold tracking-wide text-neutral-400 uppercase dark:text-fg-faint">
Custom color
</div>
<label class="flex cursor-pointer items-center gap-2.5 rounded-lg border border-neutral-200 p-2 transition-colors hover:border-neutral-300 dark:border-white/[0.08] dark:hover:border-white/[0.14]">
<span class="relative size-8 shrink-0 overflow-hidden rounded-md border border-black/10 shadow-sm dark:border-white/15">
<span class="absolute inset-0" :style="`background: ${themeColor}`"></span>
<input type="color" :value="themeColor" @input="previewThemeColor($event.target.value)"
@change="saveThemeColor($event.target.value)" aria-label="Custom theme color"
class="absolute inset-0 h-full w-full cursor-pointer opacity-0" />
</span>
<span class="flex min-w-0 flex-col">
<span class="text-xs font-medium text-black dark:text-fg">Pick a color</span>
<span class="text-[11px] tracking-wide text-neutral-500 uppercase dark:text-fg-dim" x-text="themeColor"></span>
</span>
</label>
<div class="mt-2 grid grid-cols-2 gap-1 rounded-lg border border-neutral-200 p-0.5 dark:border-white/[0.08]"
role="group" aria-label="Custom theme mode">
<button type="button" @click="setCustomMode('light')" :aria-pressed="customMode === 'light'"
class="rounded-md py-1.5 text-xs font-medium transition-colors"
:class="customMode === 'light'
? 'bg-neutral-100 text-black shadow-sm dark:bg-white/[0.14] dark:text-fg'
: 'text-neutral-500 hover:text-black dark:text-fg-dim dark:hover:text-fg'">
Light
</button>
<button type="button" @click="setCustomMode('dark')" :aria-pressed="customMode === 'dark'"
class="rounded-md py-1.5 text-xs font-medium transition-colors"
:class="customMode === 'dark'
? 'bg-neutral-100 text-black shadow-sm dark:bg-white/[0.14] dark:text-fg'
: 'text-neutral-500 hover:text-black dark:text-fg-dim dark:hover:text-fg'">
Dark
</button>
</div>
</div>

View file

@ -101,10 +101,9 @@ class="fixed z-9999 flex w-[calc(100%-2rem)] gap-2.5 sm:max-w-[26rem]"
x-transition:enter-end="translate-y-0 opacity-100"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="translate-y-0 opacity-100"
x-transition:leave-end="-translate-y-1 opacity-0"
x-transition:leave-end="translate-y-1 opacity-0"
@mouseenter="pauseToast(toast)" @mouseleave="resumeToast(toast)"
class="relative flex w-full items-start rounded-lg group"
style="background: var(--coollabs-elevated); box-shadow: 0 0 0 1px var(--coollabs-line), var(--shadow-modal);"
class="surface-popover relative flex w-full items-start rounded-lg group"
:class="{ 'p-3.5 pr-20': !toast.html, 'p-0': toast.html }">
<template x-if="!toast.html">
<div class="flex min-w-0 items-start gap-3">

View file

@ -11,10 +11,6 @@
<div @class(['relative', 'min-w-0' => $sidebar]) x-data="{
open: false,
appearanceOpen: false,
theme: localStorage.getItem('theme') === 'purple' ? 'custom' : (localStorage.getItem('theme') || 'dark'),
pageWidth: localStorage.getItem('pageWidth') || 'full',
themeColor: localStorage.getItem('themeColor') || '#6b16ed',
themeColorFrame: null,
avatarUrl: @js($user?->avatar_path ? profile_avatar_url($user) : null),
openPanel() {
this.appearanceOpen = false;
@ -23,51 +19,6 @@
closePanel() {
this.open = false;
},
setTheme(type, closeMenu = true) {
this.theme = type;
localStorage.setItem('theme', type);
if (closeMenu) {
this.closePanel();
}
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const isDark = type === 'dark' || type === 'custom' || (type === 'system' && prefersDark);
document.documentElement.classList.toggle('dark', isDark);
document.documentElement.dataset.theme = type === 'custom' ? 'custom' : (isDark ? 'dark' : 'light');
document.documentElement.style.setProperty('--theme-base-color', localStorage.themeColor || '#6b16ed');
document.documentElement.style.setProperty('--theme-accent-foreground', window.themeAccentForeground(this.themeColor));
document.querySelector('meta[name=theme-color]')?.setAttribute('content', isDark ? '#101010' : '#ffffff');
},
setWidth(width) {
this.pageWidth = width;
localStorage.setItem('pageWidth', width);
window.dispatchEvent(new CustomEvent('page-width-changed', { detail: width }));
},
previewThemeColor(color) {
this.themeColor = color;
if (this.theme !== 'custom') {
this.theme = 'custom';
document.documentElement.classList.add('dark');
document.documentElement.dataset.theme = 'custom';
}
if (this.themeColorFrame) {
return;
}
this.themeColorFrame = requestAnimationFrame(() => {
document.documentElement.style.setProperty('--theme-base-color', this.themeColor);
document.documentElement.style.setProperty('--theme-accent-foreground', window.themeAccentForeground(this.themeColor));
this.themeColorFrame = null;
});
},
saveThemeColor(color) {
this.previewThemeColor(color);
localStorage.setItem('themeColor', color);
localStorage.setItem('theme', 'custom');
},
}" @avatar-updated.window="avatarUrl = $event.detail.url" @keydown.escape.window="closePanel()"
@click.outside="closePanel()">
<button type="button" @click="open ? closePanel() : openPanel()"
@ -93,8 +44,13 @@ class="flex size-5 shrink-0 items-center justify-center rounded-full bg-neutral-
</svg>
</button>
<div x-show="open" x-cloak @class([
'top-user-menu-panel listbox-panel z-[90]! max-h-none! w-52! min-w-0! overflow-visible! animate-in fade-in zoom-in-95 duration-150',
<div x-show="open" x-cloak
x-transition:enter="transition ease-out duration-150"
x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-100"
x-transition:leave-start="opacity-100 scale-100" x-transition:leave-end="opacity-0 scale-95"
@class([
'top-user-menu-panel listbox-panel z-[90]! max-h-none! w-52! min-w-0! overflow-visible!',
'right-0! left-auto!' => ! $sidebar,
'bottom-full! left-0! right-auto! top-auto! mb-1!' => $sidebar,
'origin-bottom-left' => $sidebar,
@ -124,65 +80,26 @@ class="flex size-5 shrink-0 items-center justify-center rounded-full bg-neutral-
stroke-linejoin="round" />
</svg>
</button>
<div x-show="appearanceOpen" x-collapse class="mx-1 grid gap-0.5 pb-1 pl-6">
@foreach ([
['value' => 'light', 'label' => 'Light'],
['value' => 'system', 'label' => 'System'],
['value' => 'dark', 'label' => 'Dark'],
['value' => 'custom', 'label' => 'Custom'],
] as $option)
@if ($option['value'] === 'custom')
<div
class="relative flex h-8 w-full items-center justify-between rounded-md px-2 text-left text-xs text-neutral-600 transition-colors hover:bg-neutral-200 hover:text-neutral-950 dark:text-fg-dim dark:hover:bg-white/[0.06] dark:hover:text-fg">
<span class="flex items-center gap-2">
<span class="size-3.5 rounded-full border border-white/20"
:style="`background: ${themeColor}`"></span>
Custom
</span>
<svg x-show="theme === 'custom'" class="size-3.5 text-coollabs dark:text-warning"
viewBox="0 0 12 12" fill="none" aria-hidden="true">
<path d="m2.5 6.25 2.1 2.1 4.9-5" stroke="currentColor" stroke-width="1.4"
stroke-linecap="round" stroke-linejoin="round" />
</svg>
<input type="color" :value="themeColor" @input="previewThemeColor($event.target.value)"
@change="saveThemeColor($event.target.value)"
aria-label="Custom theme color"
class="absolute inset-0 z-10 h-full w-full cursor-pointer opacity-0" />
</div>
@else
<button type="button" @click="setTheme('{{ $option['value'] }}')"
class="flex h-8 w-full items-center justify-between rounded-md px-2 text-left text-xs text-neutral-600 transition-colors hover:bg-neutral-200 hover:text-neutral-950 dark:text-fg-dim dark:hover:bg-white/[0.06] dark:hover:text-fg">
<span>{{ $option['label'] }}</span>
<svg x-show="theme === '{{ $option['value'] }}'"
class="size-3.5 text-coollabs dark:text-warning" viewBox="0 0 12 12" fill="none"
aria-hidden="true">
<path d="m2.5 6.25 2.1 2.1 4.9-5" stroke="currentColor" stroke-width="1.4"
stroke-linecap="round" stroke-linejoin="round" />
</svg>
</button>
@endif
@endforeach
<div class="my-1 h-px bg-neutral-200 dark:bg-white/[0.07]"></div>
<div class="px-2 pt-1 pb-0.5 text-[10px] font-medium tracking-wide text-neutral-400 uppercase dark:text-fg-faint">
Page width
</div>
@foreach ([
['value' => 'full', 'label' => 'Full width'],
['value' => 'centered', 'label' => 'Centered'],
] as $option)
<button type="button" @click="setWidth('{{ $option['value'] }}')"
class="flex h-8 w-full items-center justify-between rounded-md px-2 text-left text-xs text-neutral-600 transition-colors hover:bg-neutral-200 hover:text-neutral-950 dark:text-fg-dim dark:hover:bg-white/[0.06] dark:hover:text-fg">
<span>{{ $option['label'] }}</span>
<svg x-show="pageWidth === '{{ $option['value'] }}'"
class="size-3.5 text-coollabs dark:text-warning" viewBox="0 0 12 12" fill="none"
aria-hidden="true">
<path d="m2.5 6.25 2.1 2.1 4.9-5" stroke="currentColor" stroke-width="1.4"
stroke-linecap="round" stroke-linejoin="round" />
</svg>
</button>
@endforeach
<div x-show="appearanceOpen" x-collapse.duration.200ms class="mx-1 pb-1 pl-6">
<x-theme-controls variant="menu" />
</div>
<button type="button" class="listbox-option w-full" @click="toggleAutoCollapse()"
:aria-pressed="autoCollapse" title="Collapse the sidebar on pages that have a settings menu">
<span class="flex items-center gap-2">
<svg class="size-4 opacity-80" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<rect x="3" y="4" width="18" height="16" rx="2" stroke="currentColor" stroke-width="1.6" />
<path d="M9 4v16" stroke="currentColor" stroke-width="1.6" />
</svg>
Auto-collapse sidebar
</span>
<svg x-show="autoCollapse" x-cloak class="size-4 shrink-0 text-black dark:text-fg" viewBox="0 0 24 24"
fill="none" aria-hidden="true">
<path d="M5 12l5 5L20 7" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" />
</svg>
</button>
<div class="my-1 h-px bg-neutral-200 dark:bg-white/[0.07]"></div>
<livewire:settings-dropdown trigger="account-menu" />

View file

@ -20,7 +20,7 @@
<div class="w-full">
<div
class="grid grid-cols-4 overflow-hidden rounded-[10px] border border-neutral-200 bg-white dark:border-white/[0.08] dark:bg-white/[0.025]">
class="grid grid-cols-4 overflow-hidden rounded-[10px] border border-neutral-200 bg-white dark:border-white/[0.08] dark:bg-white/[0.05]">
@foreach ($steps as $stepNumber => $label)
<div
class="flex min-h-10 items-center justify-center gap-2 border-r border-neutral-200 px-2 text-[11px] font-medium last:border-r-0 dark:border-white/[0.08] sm:px-3"

View file

@ -9,21 +9,48 @@
@auth
<div x-data="{
open: false,
collapsed: localStorage.getItem('sidebarCollapsed') === 'true',
userCollapsed: localStorage.getItem('sidebarCollapsed') === 'true',
autoCollapse: localStorage.getItem('sidebarAutoCollapse') !== 'false',
hasSecondBar: false,
collapsed: false,
pageWidth: localStorage.getItem('pageWidth') || 'full',
sidebarReady: false,
init() {
this.applyCollapsed(false);
this.$nextTick(() => {
requestAnimationFrame(() => {
this.sidebarReady = true;
});
});
},
targetCollapsed() {
this.hasSecondBar = !!document.querySelector('.application-settings-navigation');
return this.userCollapsed || (this.autoCollapse && this.hasSecondBar);
},
applyCollapsed(animate) {
const target = this.targetCollapsed();
if (target === this.collapsed) return;
if (animate) {
// Let the new page paint at the current width, then animate the
// slide a frame later so the auto-collapse reads as a motion.
this.sidebarReady = true;
requestAnimationFrame(() => requestAnimationFrame(() => { this.collapsed = target; }));
} else {
this.collapsed = target;
}
},
toggleSidebar() {
this.collapsed = !this.collapsed;
localStorage.setItem('sidebarCollapsed', this.collapsed);
this.userCollapsed = this.collapsed;
localStorage.setItem('sidebarCollapsed', this.userCollapsed);
},
toggleAutoCollapse() {
this.autoCollapse = !this.autoCollapse;
localStorage.setItem('sidebarAutoCollapse', this.autoCollapse);
this.applyCollapsed(true);
}
}" @open-global-search.window="open = false" @page-width-changed.window="pageWidth = $event.detail" x-cloak
}" @open-global-search.window="open = false" @page-width-changed.window="pageWidth = $event.detail" x-on:livewire:navigated.window="applyCollapsed(true)"
:style="{ '--sidebar-w': collapsed ? '4rem' : '14rem' }" x-cloak
class="dark:text-inherit text-black">
<livewire:deployments-indicator />
@ -69,29 +96,42 @@ class="flex h-full items-center gap-0.5 min-w-0 flex-1 border-b border-neutral-2
</div>
</header>
{{-- ============ MOBILE SLIDE-OVER SIDEBAR ============ --}}
<div class="relative z-[1000] lg:hidden" :class="open ? 'block' : 'hidden'" role="dialog" aria-modal="true">
<div class="fixed inset-0 bg-black/80" x-on:click="open = false"></div>
<div class="fixed inset-y-0 right-0 flex h-full">
<div
class="relative flex h-full w-full max-w-56 min-w-0 flex-col border-l border-neutral-200 bg-white shadow-xl dark:border-white/[0.12] dark:bg-panel">
<div class="absolute top-0 right-full flex w-16 justify-center pt-5">
<button type="button" class="-m-2.5 p-2.5" x-on:click="open = !open">
<span class="sr-only">Close sidebar</span>
<svg class="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
{{-- ============ MOBILE SLIDE-OVER SIDEBAR (shadcn-style sheet) ============ --}}
<div class="mobile-sidebar-sheet relative z-[1000] lg:hidden"
x-on:keydown.escape.window="open = false"
x-on:livewire:navigated.window="open = false">
{{-- Scrim: fades in/out --}}
<div class="fixed inset-0 bg-black/50" x-show="open" x-cloak x-on:click="open = false"
x-transition:enter="transition-opacity ease-out duration-300"
x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100"
x-transition:leave="transition-opacity ease-in duration-200"
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0"></div>
{{-- Panel: slides in from the right (iOS drawer curve), exits faster --}}
<div class="fixed inset-y-0 right-0 flex" :class="!open && 'pointer-events-none'">
<div x-show="open" x-cloak x-trap.inert.noscroll="open"
role="dialog" aria-modal="true" aria-label="Navigation menu"
x-transition:enter="transform transition ease-[cubic-bezier(0.32,0.72,0,1)] duration-300"
x-transition:enter-start="translate-x-full" x-transition:enter-end="translate-x-0"
x-transition:leave="transform transition ease-in duration-200"
x-transition:leave-start="translate-x-0" x-transition:leave-end="translate-x-full"
class="relative flex h-full w-72 max-w-[85vw] min-w-0 flex-col overflow-hidden rounded-l-2xl border-l border-neutral-200 bg-white shadow-[-8px_0_30px_-6px_rgba(0,0,0,0.18)] dark:border-white/[0.12] dark:bg-panel dark:shadow-[-8px_0_30px_-4px_rgba(0,0,0,0.5)]">
<div data-mobile-sidebar-brand
class="flex h-12 shrink-0 items-center justify-between gap-1.5 border-b border-neutral-200 px-4 dark:border-white/[0.06]">
<div class="flex min-w-0 items-baseline gap-1.5">
<a href="/" {{ wireNavigate() }} title="Coolify"
class="text-[15px] font-semibold tracking-tight text-black transition-opacity hover:opacity-80 dark:text-white">
Coolify
</a>
<x-version class="!text-[10.5px] font-medium text-neutral-400 dark:text-fg-faint !opacity-100 hover:!opacity-100 hover:text-black dark:hover:text-fg" />
</div>
<button type="button" x-on:click="open = false" aria-label="Close menu"
class="-mr-1.5 flex size-8 shrink-0 items-center justify-center rounded-md text-neutral-500 transition-colors hover:bg-neutral-100 hover:text-black active:scale-95 dark:text-fg-dim dark:hover:bg-white/[0.06] dark:hover:text-fg">
<svg class="size-5" fill="none" viewBox="0 0 24 24" stroke-width="1.75"
stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div data-mobile-sidebar-brand
class="flex h-12 shrink-0 items-center gap-1.5 border-b border-neutral-200 px-4 dark:border-white/[0.06]">
<a href="/" {{ wireNavigate() }} title="Coolify"
class="text-[15px] font-semibold tracking-tight text-black transition-opacity hover:opacity-80 dark:text-white">
Coolify
</a>
<x-version class="!text-[10.5px] font-medium text-neutral-400 dark:text-fg-faint !opacity-100 hover:!opacity-100 hover:text-black dark:hover:text-fg" />
</div>
<div class="flex min-h-0 min-w-0 flex-1 flex-col overflow-y-auto pb-2 scrollbar">
<x-navbar />
</div>
@ -128,11 +168,12 @@ class="hidden shrink-0 items-center"></div>
<livewire:upgrade key="mobile-upgrade" />
@endif
<x-top-user-menu />
<button type="button" class="-m-1 p-2 text-neutral-500 dark:text-fg-dim" x-on:click="open = !open">
<button type="button" x-on:click="open = !open"
class="-mr-1 flex size-9 items-center justify-center rounded-md text-neutral-500 transition-transform duration-100 ease-out hover:bg-neutral-100 hover:text-black active:scale-90 dark:text-fg-dim dark:hover:bg-white/[0.06] dark:hover:text-fg">
<span class="sr-only">Open sidebar</span>
<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
<svg class="size-5" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<rect x="3" y="4" width="18" height="16" rx="2" stroke="currentColor" stroke-width="1.6" />
<path d="M9 4v16" stroke="currentColor" stroke-width="1.6" />
</svg>
</button>
</div>
@ -140,7 +181,7 @@ class="hidden shrink-0 items-center"></div>
{{-- ============ MAIN ============ --}}
<main
class="min-h-screen bg-white dark:bg-panel px-5 py-6 sm:px-8 lg:px-10 lg:pt-[calc(3rem+1.75rem)] lg:pb-10"
class="min-h-screen bg-neutral-50 dark:bg-app px-5 py-6 sm:px-8 lg:px-10 lg:pt-[calc(3rem+1.75rem)] lg:pb-10"
:class="[collapsed ? 'lg:ml-16' : 'lg:ml-56', sidebarReady ? 'transition-[margin] duration-200' : '']">
<div class="w-full" :class="pageWidth === 'centered' ? 'mx-auto max-w-[1400px]' : 'max-w-none'">
{{ $slot }}

View file

@ -3,6 +3,22 @@
<script data-navigate-once>
// Immediate theme application - runs before any rendering
(function () {
// The OS color picker only speaks hex. Convert it once so the whole theme
// cascade is authored in OKLCH (sRGB -> linear -> OKLab -> OKLCH).
const srgbToLinear = (c) => c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
window.hexToOklch = (hex) => {
const [r, g, b] = hex.match(/[a-f\d]{2}/gi).map((channel) => srgbToLinear(parseInt(channel, 16) / 255));
const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);
const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
const okL = 0.2104542553 * l + 0.7936177850 * m - 0.0040720468 * s;
const okA = 1.9779984951 * l - 2.4285922050 * m + 0.4505937099 * s;
const okB = 0.0259040371 * l + 0.7827717662 * m - 0.8086757660 * s;
const chroma = Math.sqrt(okA * okA + okB * okB);
let hue = Math.atan2(okB, okA) * 180 / Math.PI;
if (hue < 0) hue += 360;
return `oklch(${(okL * 100).toFixed(2)}% ${chroma.toFixed(4)} ${hue.toFixed(2)})`;
};
window.themeAccentForeground = (color) => {
const channels = color.match(/[a-f\d]{2}/gi).map(channel => parseInt(channel, 16) * 0.85 + 255 * 0.15);
const luminance = channels
@ -10,20 +26,93 @@
.map(channel => channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4)
.reduce((sum, channel, index) => sum + channel * [0.2126, 0.7152, 0.0722][index], 0);
return luminance > 0.179 ? '#000000' : '#ffffff';
return luminance > 0.179 ? 'oklch(0% 0 0)' : 'oklch(100% 0 0)';
};
window.applyStoredTheme = () => {
const theme = localStorage.theme === 'purple' ? 'custom' : (localStorage.theme || 'dark');
const themeColor = localStorage.themeColor || '#6b16ed';
const isDark = theme === 'dark' || theme === 'custom' || (theme === 'system' && matchMedia('(prefers-color-scheme: dark)').matches);
const customMode = localStorage.customMode || 'dark';
const isDark = theme === 'dark'
|| (theme === 'custom' && customMode === 'dark')
|| (theme === 'system' && matchMedia('(prefers-color-scheme: dark)').matches);
localStorage.theme = theme;
document.documentElement.classList.toggle('dark', isDark);
document.documentElement.dataset.theme = theme === 'custom' ? 'custom' : (isDark ? 'dark' : 'light');
document.documentElement.style.setProperty('--theme-base-color', themeColor);
document.documentElement.style.setProperty('--theme-base-color', window.hexToOklch(themeColor));
document.documentElement.style.setProperty('--theme-accent-foreground', window.themeAccentForeground(themeColor));
document.querySelector('meta[name=theme-color]')?.setAttribute('content', isDark ? '#101010' : '#ffffff');
};
// Single source for the theme controls Alpine state, shared by the
// Appearance page and the profile dropdown via x-data="themeControls()".
window.themeControls = () => ({
theme: localStorage.getItem('theme') === 'purple' ? 'custom' : (localStorage.getItem('theme') || 'dark'),
themeColor: localStorage.getItem('themeColor') || '#6b16ed',
customMode: localStorage.getItem('customMode') || 'dark',
pageWidth: localStorage.getItem('pageWidth') || 'full',
themeColorFrame: null,
pickerOpen: false,
init() {
localStorage.setItem('theme', this.theme);
this.applyTheme();
},
chooseCustom() {
this.setTheme('custom');
this.pickerOpen = !this.pickerOpen;
},
setTheme(type) {
this.theme = type;
localStorage.setItem('theme', type);
this.applyTheme();
},
setCustomMode(mode) {
this.customMode = mode;
localStorage.setItem('customMode', mode);
if (this.theme !== 'custom') {
this.setTheme('custom');
return;
}
this.applyTheme();
},
setWidth(width) {
this.pageWidth = width;
localStorage.setItem('pageWidth', width);
window.dispatchEvent(new CustomEvent('page-width-changed', { detail: width }));
},
previewThemeColor(color) {
this.themeColor = color;
if (this.theme !== 'custom') {
this.theme = 'custom';
localStorage.setItem('theme', 'custom');
}
document.documentElement.dataset.theme = 'custom';
document.documentElement.classList.toggle('dark', this.customMode === 'dark');
if (this.themeColorFrame) {
return;
}
this.themeColorFrame = requestAnimationFrame(() => {
document.documentElement.style.setProperty('--theme-base-color', window.hexToOklch(this.themeColor));
document.documentElement.style.setProperty('--theme-accent-foreground', window.themeAccentForeground(this.themeColor));
this.themeColorFrame = null;
});
},
saveThemeColor(color) {
this.previewThemeColor(color);
localStorage.setItem('themeColor', color);
localStorage.setItem('theme', 'custom');
},
applyTheme() {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const isDark = this.theme === 'dark'
|| (this.theme === 'custom' && this.customMode === 'dark')
|| (this.theme === 'system' && prefersDark);
document.documentElement.classList.toggle('dark', isDark);
document.documentElement.dataset.theme = this.theme === 'custom' ? 'custom' : (isDark ? 'dark' : 'light');
document.documentElement.style.setProperty('--theme-base-color', window.hexToOklch(this.themeColor));
document.documentElement.style.setProperty('--theme-accent-foreground', window.themeAccentForeground(this.themeColor));
document.querySelector('meta[name=theme-color]')?.setAttribute('content', isDark ? '#101010' : '#ffffff');
},
});
document.addEventListener('livewire:navigated', window.applyStoredTheme);
window.applyStoredTheme();

View file

@ -4,7 +4,7 @@
styles (e.g. [wire\:loading] { display: none }) must be rendered manually too. --}}
@livewireStyles
@livewireScripts
<main class="h-full bg-gray-50 dark:bg-base">
<main class="h-full bg-neutral-50 dark:bg-base">
{{ $slot }}
</main>
@parent

View file

@ -40,7 +40,7 @@ class="flex size-8 shrink-0 items-center justify-center rounded-lg border border
Continue
</x-forms.button>
<div
class="inline-flex flex-wrap items-center justify-center gap-0.5 rounded-lg border border-neutral-200 bg-neutral-50 p-0.5 dark:border-white/[0.08] dark:bg-white/[0.025]">
class="inline-flex flex-wrap items-center justify-center gap-0.5 rounded-lg border border-neutral-200 bg-neutral-50 p-0.5 dark:border-white/[0.08] dark:bg-white/[0.05]">
<button type="button" wire:click="skipBoarding"
class="inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-[12px] font-medium text-neutral-500 transition-colors hover:bg-white hover:text-coollabs dark:text-fg-dim dark:hover:bg-white/[0.06] dark:hover:text-warning">
<x-reicon name="arrow-right" class="size-3.5 shrink-0" />
@ -101,7 +101,7 @@ class="inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-[12px] font-
<p class="mb-3 text-sm text-neutral-500 dark:text-neutral-400">Use this machine or connect a server you already manage.</p>
<div class="grid w-full grid-cols-1 gap-4 lg:grid-cols-2">
<button
class="group relative cursor-pointer min-h-36 rounded-[10px] border border-neutral-200 bg-white p-4 text-left shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]"
class="group relative cursor-pointer min-h-36 rounded-[10px] border border-neutral-200 bg-white p-4 text-left shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]"
wire:target="setServerType('localhost')" wire:click="setServerType('localhost')">
<span role="button" tabindex="0" aria-label="About this machine"
data-tooltip="The machine running Coolify. Not recommended for production workloads due to resource contention."
@ -125,7 +125,7 @@ class="absolute top-3 right-3 flex size-6 items-center justify-center rounded-fu
<button
class="group relative cursor-pointer min-h-36 rounded-[10px] border border-neutral-200 bg-white p-4 text-left shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]"
class="group relative cursor-pointer min-h-36 rounded-[10px] border border-neutral-200 bg-white p-4 text-left shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]"
wire:target="setServerType('remote')" wire:click="setServerType('remote')">
<span role="button" tabindex="0" aria-label="About remote servers"
data-tooltip="Any SSH-accessible server, including cloud VPS, bare metal, and self-hosted infrastructure."
@ -157,7 +157,7 @@ class="absolute top-3 right-3 flex size-6 items-center justify-center rounded-fu
<x-modal-input title="Connect a Hetzner Server" isFullWidth>
<x-slot:content>
<div
class="group relative cursor-pointer flex h-full min-h-36 flex-col rounded-[10px] border border-neutral-200 bg-white p-4 text-left shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]">
class="group relative cursor-pointer flex h-full min-h-36 flex-col rounded-[10px] border border-neutral-200 bg-white p-4 text-left shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]">
<div class="flex h-full flex-col gap-4 text-left">
<img src="{{ asset('svgs/hetzner.svg') }}" alt="Hetzner"
class="size-10 shrink-0">
@ -175,7 +175,7 @@ class="size-10 shrink-0">
<x-modal-input title="Connect a Vultr Server" isFullWidth>
<x-slot:content>
<div
class="group relative cursor-pointer flex h-full min-h-36 flex-col rounded-[10px] border border-neutral-200 bg-white p-4 text-left shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]">
class="group relative cursor-pointer flex h-full min-h-36 flex-col rounded-[10px] border border-neutral-200 bg-white p-4 text-left shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]">
<div class="flex h-full flex-col gap-4 text-left">
<img src="https://www.vultr.com/media/logo_ondark.svg" alt="Vultr"
class="h-10 w-28 shrink-0 object-contain object-left">
@ -193,7 +193,7 @@ class="h-10 w-28 shrink-0 object-contain object-left">
<x-modal-input title="Connect a DigitalOcean Server" isFullWidth>
<x-slot:content>
<div
class="group relative cursor-pointer flex h-full min-h-36 flex-col rounded-[10px] border border-neutral-200 bg-white p-4 text-left shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]">
class="group relative cursor-pointer flex h-full min-h-36 flex-col rounded-[10px] border border-neutral-200 bg-white p-4 text-left shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]">
<div class="flex h-full flex-col gap-4 text-left">
<x-digital-ocean-icon class="size-10 shrink-0" />
<div class="min-h-0 flex-1">
@ -279,7 +279,7 @@ class="bg-red-200 dark:bg-red-900 px-1 rounded-sm">~/.ssh/authorized_keys</code>
@endphp
<div class="w-full space-y-4">
<div
class="rounded-[10px] border border-neutral-200 bg-neutral-50 p-4 dark:border-white/[0.08] dark:bg-white/[0.025]">
class="rounded-[10px] border border-neutral-200 bg-neutral-50 p-4 dark:border-white/[0.08] dark:bg-white/[0.05]">
<form wire:submit="selectExistingPrivateKey"
class="flex flex-col gap-3 sm:flex-row sm:items-end">
<div class="min-w-0 flex-1">
@ -304,7 +304,7 @@ class="bg-[var(--coollabs-base)] px-2.5 text-[10px] font-semibold uppercase trac
@endif
<div class="grid w-full grid-cols-1 gap-3 lg:grid-cols-2">
<button type="button"
class="group flex h-full min-h-28 items-start gap-3 rounded-[10px] border border-neutral-200 bg-white p-4 text-left shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]"
class="group flex h-full min-h-28 items-start gap-3 rounded-[10px] border border-neutral-200 bg-white p-4 text-left shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]"
wire:target="setPrivateKey('own')" wire:click="setPrivateKey('own')">
<span
class="flex size-9 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">
@ -318,7 +318,7 @@ class="flex size-9 shrink-0 items-center justify-center rounded-lg border border
</span>
</button>
<button type="button"
class="group flex h-full min-h-28 items-start gap-3 rounded-[10px] border border-neutral-200 bg-white p-4 text-left shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]"
class="group flex h-full min-h-28 items-start gap-3 rounded-[10px] border border-neutral-200 bg-white p-4 text-left shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]"
wire:target="setPrivateKey('create')" wire:click="setPrivateKey('create')">
<span
class="flex size-9 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">
@ -597,7 +597,7 @@ class="text-[10px] font-semibold uppercase tracking-[0.08em] text-neutral-400 da
Deploy your first resource
</x-forms.button>
<div
class="inline-flex flex-wrap items-center justify-center gap-0.5 rounded-lg border border-neutral-200 bg-neutral-50 p-0.5 dark:border-white/[0.08] dark:bg-white/[0.025]">
class="inline-flex flex-wrap items-center justify-center gap-0.5 rounded-lg border border-neutral-200 bg-neutral-50 p-0.5 dark:border-white/[0.08] dark:bg-white/[0.05]">
<button type="button" wire:click="skipBoarding"
class="inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-[12px] font-medium text-neutral-500 transition-colors hover:bg-white hover:text-coollabs dark:text-fg-dim dark:hover:bg-white/[0.06] dark:hover:text-warning">
<x-reicon name="arrow-right" class="size-3.5 shrink-0" />
@ -622,7 +622,7 @@ class="inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-[12px] font-
@if ($currentState !== 'welcome' && $currentState !== 'create-resource')
<div class="mx-auto mt-6 flex w-full max-w-3xl flex-col items-center gap-3">
<div
class="inline-flex flex-wrap items-center justify-center gap-0.5 rounded-lg border border-neutral-200 bg-neutral-50 p-0.5 dark:border-white/[0.08] dark:bg-white/[0.025]">
class="inline-flex flex-wrap items-center justify-center gap-0.5 rounded-lg border border-neutral-200 bg-neutral-50 p-0.5 dark:border-white/[0.08] dark:bg-white/[0.05]">
<button type="button" wire:click="skipBoarding"
class="inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-[12px] font-medium text-neutral-500 transition-colors hover:bg-white hover:text-coollabs dark:text-fg-dim dark:hover:bg-white/[0.06] dark:hover:text-warning">
<x-reicon name="arrow-right" class="size-3.5 shrink-0" />

View file

@ -17,21 +17,8 @@
<livewire:dashboard.active-deployments />
<section class="mb-0! min-w-0">
<div class="mb-3 flex items-end justify-between gap-4">
<div>
<h2 class="text-[14px]! leading-5! font-semibold! text-black dark:text-fg">
Projects
</h2>
<p class="mt-0.5 text-[11px] text-neutral-500 dark:text-fg-faint">
Your deployment workspaces
</p>
</div>
<a href="{{ route('project.index') }}" {{ wireNavigate() }}
class="inline-flex shrink-0 items-center gap-1 text-[12px] font-medium text-neutral-500 transition-colors hover:text-black dark:text-fg-dim dark:hover:text-fg">
View all
<x-reicon name="arrow-right" class="size-3" />
</a>
</div>
<x-section-heading title="Projects" subtitle="Your deployment workspaces"
:href="route('project.index')" />
@if ($dashboardProjects->isEmpty())
<x-empty title="No projects yet"
@ -57,7 +44,7 @@ class="inline-flex shrink-0 items-center gap-1 text-[12px] font-medium text-neut
@endphp
<article
class="group relative flex min-h-28 min-w-0 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]">
class="group relative flex min-h-28 min-w-0 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]">
<a href="{{ $project->navigateTo() }}" {{ wireNavigate() }}
class="absolute inset-0 rounded-xl"
aria-label="Open {{ $project->name }}"></a>
@ -125,21 +112,8 @@ class="flex size-6.5 items-center justify-center rounded-md text-neutral-400 tra
</section>
<section class="mb-0! min-w-0">
<div class="mb-3 flex items-end justify-between gap-4">
<div>
<h2 class="text-[14px]! leading-5! font-semibold! text-black dark:text-fg">
Servers
</h2>
<p class="mt-0.5 text-[11px] text-neutral-500 dark:text-fg-faint">
Infrastructure available for deployments
</p>
</div>
<a href="{{ route('server.index') }}" {{ wireNavigate() }}
class="inline-flex shrink-0 items-center gap-1 text-[12px] font-medium text-neutral-500 transition-colors hover:text-black dark:text-fg-dim dark:hover:text-fg">
View all
<x-reicon name="arrow-right" class="size-3" />
</a>
</div>
<x-section-heading title="Servers" subtitle="Infrastructure available for deployments"
:href="route('server.index')" />
@if ($dashboardServers->isEmpty())
@if ($privateKeys->isEmpty())
@ -190,7 +164,7 @@ class="button button-highlighted">
<a href="{{ route('server.show', ['server_uuid' => $server->uuid]) }}"
{{ wireNavigate() }} aria-label="Open {{ $server->name }}"
class="group relative flex min-h-28 min-w-0 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]">
class="group relative flex min-h-28 min-w-0 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]">
@if ($server->isMetricsEnabled())
<livewire:dashboard.server-metrics-chart :server="$server"
:key="'dashboard-server-metrics-'.$server->uuid" />

View file

@ -24,24 +24,15 @@
$hasAnyDeployments = $hasActiveDeployments || $hasRecentDeployments;
@endphp
<div wire:poll.3000ms="refreshDeployments" @class(['mb-0! min-w-0' => $hasAnyDeployments])>
<div wire:poll.3000ms="refreshDeployments" @class(['mb-0! min-w-0' => $hasAnyDeployments, 'hidden' => ! $hasAnyDeployments])>
@if ($hasAnyDeployments)
<section class="mb-0! min-w-0">
<div class="mb-3">
<div>
<h2 class="text-[14px]! leading-5! font-semibold! text-black dark:text-fg">
Deployments
</h2>
<p class="mt-0.5 text-[11px] text-neutral-500 dark:text-fg-faint">
Active and recent deployment activity
</p>
</div>
</div>
<x-section-heading title="Deployments" subtitle="Active and recent deployment activity" />
<div class="flex min-w-0 flex-col gap-4">
@if ($hasActiveDeployments)
<div
class="overflow-hidden rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.025]">
class="overflow-hidden rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.05]">
<div class="border-b border-neutral-200 px-4 py-2.5 dark:border-white/[0.08]">
<h3 class="text-[12px]! leading-4! font-semibold! text-black dark:text-fg">
Active
@ -52,7 +43,7 @@ class="overflow-hidden rounded-xl border border-neutral-200 bg-white shadow-sm d
</div>
<div
class="dashboard-deployment-table-grid hidden items-center gap-4 border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 md:grid dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-faint">
class="dashboard-deployment-table-grid hidden items-center gap-4 border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 md:grid dark:border-white/[0.08] dark:bg-white/[0.05] dark:text-fg-faint">
<span>Application</span>
<span>Environment</span>
<span>Server</span>
@ -110,7 +101,7 @@ class="dashboard-deployment-table-grid group block border-b border-neutral-200 p
@if ($hasRecentDeployments)
<div
class="overflow-hidden rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.025]">
class="overflow-hidden rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.05]">
<div class="border-b border-neutral-200 px-4 py-2.5 dark:border-white/[0.08]">
<h3 class="text-[12px]! leading-4! font-semibold! text-black dark:text-fg">
Recent
@ -121,7 +112,7 @@ class="overflow-hidden rounded-xl border border-neutral-200 bg-white shadow-sm d
</div>
<div
class="dashboard-deployment-table-grid hidden items-center gap-4 border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 md:grid dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-faint">
class="dashboard-deployment-table-grid hidden items-center gap-4 border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 md:grid dark:border-white/[0.08] dark:bg-white/[0.05] dark:text-fg-faint">
<span>Application</span>
<span>Environment</span>
<span>Server</span>

View file

@ -11,8 +11,7 @@
x-transition:enter-start="translate-y-2" x-transition:enter-end="translate-y-0"
x-transition:leave="transition ease-in duration-150" x-transition:leave-start="translate-y-0"
x-transition:leave-end="translate-y-2" x-cloak
class="absolute bottom-full mb-2 w-[min(22rem,calc(100vw-2rem))] overflow-hidden rounded-xl"
style="background: var(--coollabs-elevated); box-shadow: 0 0 0 1px var(--coollabs-line), var(--shadow-modal);">
class="surface-popover absolute bottom-full mb-2 w-[min(22rem,calc(100vw-2rem))] overflow-hidden rounded-xl">
<div class="max-h-96 space-y-1 overflow-y-auto p-2 scrollbar">
@foreach ($this->deployments as $deployment)
@php
@ -71,8 +70,7 @@ class="shrink-0" />
{{-- Collapsed pill --}}
<button type="button" @click="expanded = !expanded"
class="flex items-center gap-2 rounded-xl border border-neutral-200 bg-white px-3.5 py-2 text-sm font-medium text-neutral-800 transition-colors hover:bg-neutral-50 dark:border-coolgray-300 dark:bg-surface dark:text-fg dark:hover:bg-raised"
style="box-shadow: 0 0 0 1px var(--coollabs-hairline), var(--shadow-modal);"
class="flex items-center gap-2 rounded-xl border border-neutral-200 bg-white px-3.5 py-2 text-sm font-medium text-neutral-800 shadow-dropdown transition-colors hover:bg-neutral-50 dark:border-coolgray-300 dark:bg-surface dark:text-fg dark:hover:bg-raised"
:aria-expanded="expanded.toString()" aria-label="Active deployments">
<svg class="loading-indicator size-3.5 shrink-0 animate-spin"
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" aria-hidden="true">

View file

@ -62,7 +62,7 @@ class="button button-highlighted">
<div x-cloak x-show="viewMode === 'grid'" class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
@foreach ($destinations as $destination)
<a x-show="matches(@js([$destination->name, $destination->server->name, $destination->getMorphClass() === 'App\\Models\\SwarmDocker' ? 'Docker Swarm' : 'Standalone Docker']))" class="group flex min-h-28 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]"
<a x-show="matches(@js([$destination->name, $destination->server->name, $destination->getMorphClass() === 'App\\Models\\SwarmDocker' ? 'Docker Swarm' : 'Standalone Docker']))" class="group flex min-h-28 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]"
{{ wireNavigate() }}
href="{{ route('destination.show', ['destination_uuid' => data_get($destination, 'uuid')]) }}">
<div class="flex items-start gap-3">
@ -90,8 +90,8 @@ class="flex size-8 shrink-0 items-center justify-center rounded-lg border border
</a>
@endforeach
</div>
<div x-show="viewMode === 'table'" class="overflow-x-auto rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.025]">
<div class="grid min-w-[620px] grid-cols-[minmax(0,1fr)_minmax(10rem,.7fr)_10rem] border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-faint">
<div x-show="viewMode === 'table'" class="overflow-x-auto rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.05]">
<div class="grid min-w-[620px] grid-cols-[minmax(0,1fr)_minmax(10rem,.7fr)_10rem] border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.05] dark:text-fg-faint">
<div>Destination</div><div>Server</div><div>Type</div>
</div>
@foreach ($destinations as $destination)

View file

@ -31,7 +31,7 @@ class="h-8! w-full rounded-lg! border-neutral-200! bg-white! py-0! pr-3! pl-8! t
<div class="overflow-x-auto">
<div
class="grid min-w-[680px] grid-cols-[minmax(10rem,.8fr)_minmax(10rem,.8fr)_minmax(12rem,1fr)_8rem] border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-faint">
class="grid min-w-[680px] grid-cols-[minmax(10rem,.8fr)_minmax(10rem,.8fr)_minmax(12rem,1fr)_8rem] border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.05] dark:text-fg-faint">
<div>Project</div>
<div>Environment</div>
<div>Resource</div>

View file

@ -751,7 +751,7 @@ class="relative flex min-h-full items-start justify-center p-4 sm:items-center">
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave-end="opacity-0 -translate-y-2 sm:scale-95"
class="{{ $createModalShell }}"
style="box-shadow: 0 0 0 1px var(--coollabs-hairline), var(--shadow-modal)">
style="box-shadow: 0 0 0 1px var(--coollabs-hairline), var(--shadow-dropdown)">
<header class="flex-nowrap!">
<h3 class="min-w-0 flex-1 truncate">New project</h3>
<button type="button" @click="modalOpen=false" class="{{ $createModalClose }}">
@ -794,7 +794,7 @@ class="relative flex min-h-full items-start justify-center p-4 sm:items-center">
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave-end="opacity-0 -translate-y-2 sm:scale-95"
class="{{ $createModalShell }}"
style="box-shadow: 0 0 0 1px var(--coollabs-hairline), var(--shadow-modal)">
style="box-shadow: 0 0 0 1px var(--coollabs-hairline), var(--shadow-dropdown)">
<header class="flex-nowrap!">
<h3 class="min-w-0 flex-1 truncate">New team</h3>
<button type="button" @click="modalOpen=false" class="{{ $createModalClose }}">
@ -837,7 +837,7 @@ class="relative flex min-h-full items-start justify-center p-4 sm:items-center">
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave-end="opacity-0 -translate-y-2 sm:scale-95"
class="{{ $createModalShell }}"
style="box-shadow: 0 0 0 1px var(--coollabs-hairline), var(--shadow-modal)">
style="box-shadow: 0 0 0 1px var(--coollabs-hairline), var(--shadow-dropdown)">
<header class="flex-nowrap!">
<h3 class="min-w-0 flex-1 truncate">New S3 storage</h3>
<button type="button" @click="modalOpen=false" class="{{ $createModalClose }}">
@ -880,7 +880,7 @@ class="relative flex min-h-full items-start justify-center p-4 sm:items-center">
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave-end="opacity-0 -translate-y-2 sm:scale-95"
class="{{ $createModalShell }}"
style="box-shadow: 0 0 0 1px var(--coollabs-hairline), var(--shadow-modal)">
style="box-shadow: 0 0 0 1px var(--coollabs-hairline), var(--shadow-dropdown)">
<header class="flex-nowrap!">
<h3 class="min-w-0 flex-1 truncate">New private key</h3>
<button type="button" @click="modalOpen=false" class="{{ $createModalClose }}">
@ -923,7 +923,7 @@ class="relative flex min-h-full items-start justify-center p-4 sm:items-center">
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave-end="opacity-0 -translate-y-2 sm:scale-95"
class="{{ $createModalShell }}"
style="box-shadow: 0 0 0 1px var(--coollabs-hairline), var(--shadow-modal)">
style="box-shadow: 0 0 0 1px var(--coollabs-hairline), var(--shadow-dropdown)">
<header class="flex-nowrap!">
<h3 class="min-w-0 flex-1 truncate">New GitHub app</h3>
<button type="button" @click="modalOpen=false" class="{{ $createModalClose }}">

View file

@ -1,156 +1,4 @@
<div>
<x-slot:title>Appearance | Coolify</x-slot>
<div x-data="{
theme: localStorage.getItem('theme') === 'purple' ? 'custom' : (localStorage.getItem('theme') || 'dark'),
themeColor: localStorage.getItem('themeColor') || '#6b16ed',
pageWidth: localStorage.getItem('pageWidth') || 'full',
themeColorFrame: null,
init() {
localStorage.setItem('theme', this.theme);
this.applyTheme();
},
setTheme(type) {
this.theme = type;
localStorage.setItem('theme', type);
this.applyTheme();
},
setWidth(width) {
this.pageWidth = width;
localStorage.setItem('pageWidth', width);
window.dispatchEvent(new CustomEvent('page-width-changed', { detail: width }));
},
previewThemeColor(color) {
this.themeColor = color;
if (this.theme !== 'custom') {
this.theme = 'custom';
document.documentElement.classList.add('dark');
document.documentElement.dataset.theme = 'custom';
}
if (this.themeColorFrame) {
return;
}
this.themeColorFrame = requestAnimationFrame(() => {
document.documentElement.style.setProperty('--theme-base-color', this.themeColor);
document.documentElement.style.setProperty('--theme-accent-foreground', window.themeAccentForeground(this.themeColor));
this.themeColorFrame = null;
});
},
saveThemeColor(color) {
this.previewThemeColor(color);
localStorage.setItem('themeColor', color);
localStorage.setItem('theme', 'custom');
},
applyTheme() {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const isDark = this.theme === 'dark' || this.theme === 'custom' || (this.theme === 'system' && prefersDark);
document.documentElement.classList.toggle('dark', isDark);
document.documentElement.dataset.theme = this.theme === 'custom' ? 'custom' : (isDark ? 'dark' : 'light');
document.documentElement.style.setProperty('--theme-base-color', this.themeColor);
document.documentElement.style.setProperty('--theme-accent-foreground', window.themeAccentForeground(this.themeColor));
document.querySelector('meta[name=theme-color]')?.setAttribute('content', isDark ? '#101010' : '#ffffff');
},
}" class="mt-8 flex w-full max-w-none flex-col gap-6 lg:mt-3">
<section class="application-settings-section">
<div class="application-settings-section-header">
<div>
<h2>Color theme</h2>
<p>Choose the color scheme used in this browser.</p>
</div>
</div>
<div class="application-settings-section-body grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
@foreach ([
['value' => 'light', 'label' => 'Light', 'description' => 'Bright surfaces and dark text.', 'preview' => 'bg-white'],
['value' => 'system', 'label' => 'System', 'description' => 'Follow your operating system.', 'preview' => 'bg-gradient-to-r from-white via-neutral-400 to-[#050505]'],
['value' => 'dark', 'label' => 'Dark', 'description' => 'Dark surfaces and soft contrast.', 'preview' => 'bg-[#181818]'],
['value' => 'custom', 'label' => 'Custom', 'description' => 'Choose any color for dark surfaces.', 'preview' => ''],
] as $option)
<div role="button" tabindex="0"
@if ($option['value'] !== 'custom')
@click="setTheme('{{ $option['value'] }}')"
@keydown.enter.prevent="setTheme('{{ $option['value'] }}')"
@endif
class="group relative overflow-hidden rounded-[10px] border border-neutral-200 bg-white text-left transition-[border-color,box-shadow] hover:border-neutral-300 hover:shadow-sm dark:border-white/[0.07] dark:bg-white/[0.025] dark:hover:border-white/[0.12]"
:class="theme === '{{ $option['value'] }}'
? 'ring-1 ring-coollabs/30 border-coollabs/40 dark:ring-warning/30 dark:border-warning/40'
: ''">
<div class="h-20 {{ $option['preview'] }} border-b border-neutral-200 dark:border-white/[0.07]"
@if ($option['value'] === 'custom') :style="`background: color-mix(in oklab, ${themeColor} 28%, #101011)`" @endif>
<div class="flex h-full items-center justify-center">
@if ($option['value'] === 'custom')
<div class="h-10 w-20 rounded-md border border-white/15 p-1 shadow-sm">
<div class="h-full w-full rounded-sm" :style="`background: ${themeColor}`"></div>
</div>
@else
<div
class="h-8 w-20 rounded-md border border-black/10 bg-neutral-100/80 shadow-sm dark:border-white/10 dark:bg-black/20">
</div>
@endif
</div>
</div>
<div class="p-3">
<div class="flex items-center justify-between gap-2">
<span class="text-sm font-semibold text-black dark:text-fg">
{{ $option['label'] }}
</span>
<x-reicon name="check-circle" class="size-4 text-coollabs dark:text-warning"
x-show="theme === '{{ $option['value'] }}'" x-cloak />
</div>
<p class="mt-1 text-xs leading-5 text-neutral-500 dark:text-fg-dim">
{{ $option['description'] }}
</p>
</div>
@if ($option['value'] === 'custom')
<input type="color" :value="themeColor" @input="previewThemeColor($event.target.value)"
@change="saveThemeColor($event.target.value)"
aria-label="Custom theme color"
class="absolute inset-0 z-10 h-full w-full cursor-pointer opacity-0" />
@endif
</div>
@endforeach
</div>
</section>
<section class="application-settings-section">
<div class="application-settings-section-header">
<div>
<h2>Page width</h2>
<p>Choose how content uses the available browser width.</p>
</div>
</div>
<div class="application-settings-section-body grid gap-3 sm:grid-cols-2">
@foreach ([
['value' => 'full', 'label' => 'Full width', 'description' => 'Use all available space for page content.'],
['value' => 'centered', 'label' => 'Centered', 'description' => 'Keep content centered at a comfortable maximum width.'],
] as $option)
<button type="button" @click="setWidth('{{ $option['value'] }}')"
class="group overflow-hidden rounded-[10px] border border-neutral-200 bg-white text-left transition-[border-color,box-shadow] hover:border-neutral-300 hover:shadow-sm dark:border-white/[0.07] dark:bg-white/[0.025] dark:hover:border-white/[0.12]"
:class="pageWidth === '{{ $option['value'] }}'
? 'ring-1 ring-coollabs/30 border-coollabs/40 dark:ring-warning/30 dark:border-warning/40'
: ''">
<div class="flex h-20 items-center border-b border-neutral-200 bg-neutral-50 px-4 dark:border-white/[0.07] dark:bg-black/15">
<div class="flex h-11 w-full gap-1.5 rounded-md border border-neutral-300 bg-white p-1.5 dark:border-white/15 dark:bg-[#181818]">
<div class="w-3 shrink-0 rounded-sm bg-neutral-200 dark:bg-white/10"></div>
<div @class([
'h-full rounded-sm bg-neutral-200 dark:bg-white/10',
'w-full' => $option['value'] === 'full',
'mx-auto w-2/3' => $option['value'] === 'centered',
])></div>
</div>
</div>
<div class="p-3">
<div class="flex items-center justify-between gap-2">
<span class="text-sm font-semibold text-black dark:text-fg">{{ $option['label'] }}</span>
<x-reicon name="check-circle" class="size-4 text-coollabs dark:text-warning"
x-show="pageWidth === '{{ $option['value'] }}'" x-cloak />
</div>
<p class="mt-1 text-xs leading-5 text-neutral-500 dark:text-fg-dim">{{ $option['description'] }}</p>
</div>
</button>
@endforeach
</div>
</section>
</div>
<x-theme-controls variant="full" />
</div>

View file

@ -275,7 +275,7 @@ class="flex items-end gap-2">
@if (session('status') === 'two-factor-authentication-confirmed'
|| session('status') === 'recovery-codes-generated')
<div
class="grid gap-2 rounded-lg border border-neutral-200 bg-neutral-50 p-4 font-mono text-xs text-neutral-700 sm:grid-cols-2 dark:border-white/[0.07] dark:bg-white/[0.025] dark:text-fg-dim">
class="grid gap-2 rounded-lg border border-neutral-200 bg-neutral-50 p-4 font-mono text-xs text-neutral-700 sm:grid-cols-2 dark:border-white/[0.07] dark:bg-white/[0.05] dark:text-fg-dim">
@foreach (request()->user()->recoveryCodes() as $code)
<div>{{ $code }}</div>
@endforeach

View file

@ -5,7 +5,7 @@
</div>
<p
class="rounded-lg border border-neutral-200 bg-neutral-50 px-3 py-2.5 text-[12px] text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-dim">
class="rounded-lg border border-neutral-200 bg-neutral-50 px-3 py-2.5 text-[12px] text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.05] dark:text-fg-dim">
A production environment will be created automatically.
</p>

View file

@ -5,7 +5,7 @@
<livewire:project.shared.configuration-checker :resource="$application" />
<livewire:project.application.heading :application="$application" :wire:key="'application-heading-'.$currentRoute" />
<section class="application-settings-workspace mt-4 w-full max-w-none lg:mt-0">
<section class="application-settings-workspace w-full max-w-none">
<div class="grid min-w-0 gap-8 xl:grid-cols-[210px_minmax(0,1fr)] xl:gap-8">
<x-application.configuration-sidebar :application="$application" :current-route="$currentRoute" />

View file

@ -367,7 +367,7 @@ class="logs-viewer-btn">
x-transition:leave="transition ease-in duration-75"
x-transition:leave-start="transform opacity-100 scale-100"
x-transition:leave-end="transform opacity-0 scale-95"
class="absolute right-0 z-50 mt-2 w-max origin-top-right rounded-lg border border-neutral-200 bg-white p-1 shadow-modal focus:outline-none dark:border-white/[0.1] dark:bg-[#181818]">
class="absolute right-0 z-50 mt-2 w-max origin-top-right rounded-lg border border-neutral-200 bg-white p-1 shadow-dropdown focus:outline-none dark:border-white/[0.1] dark:bg-[#181818]">
<div>
<button x-on:click="downloadLogs(); downloadMenuOpen = false"
class="listbox-option text-neutral-700! hover:bg-neutral-100! dark:text-neutral-200! dark:hover:bg-white/[0.07]!">

View file

@ -48,7 +48,7 @@
$domainCount = countDomains($fqdn);
}
@endphp
<div class="flex items-center gap-3 rounded-lg border border-neutral-200 bg-neutral-50/60 px-4 py-3 dark:border-white/[0.07] dark:bg-white/[0.025]">
<div class="flex items-center gap-3 rounded-lg border border-neutral-200 bg-neutral-50/60 px-4 py-3 dark:border-white/[0.07] dark:bg-white/[0.05]">
<div class="flex min-w-0 flex-1 items-center gap-3">
<div class="flex size-9 shrink-0 items-center justify-center rounded-md bg-neutral-200/70 text-neutral-600 dark:bg-white/[0.07] dark:text-fg-dim">
<x-reicon name="globe" class="size-4" />

View file

@ -26,12 +26,13 @@
@endphp
<div>
<div class="mb-3 w-full xl:hidden">
<div class="flex min-w-0 flex-col items-start gap-2">
<h1 class="min-w-0 max-w-full truncate text-[24px]! leading-7! font-semibold! tracking-tight! text-black dark:text-fg">
{{-- Identity row: name truncates, status + links stay pinned right. --}}
<div class="flex w-full min-w-0 items-center gap-3">
<h1 class="min-w-0 flex-1 truncate text-[22px]! leading-7! font-semibold! tracking-tight! text-black dark:text-fg">
{{ $application->name }}
</h1>
<div class="relative flex w-full min-w-0 items-center gap-2">
<x-status-summary :status="$application->status" />
<div class="flex shrink-0 items-center gap-2">
<x-status-summary :status="$application->status" align="right" />
<x-applications.links :application="$application" compact />
</div>
<div class="flex w-full flex-wrap gap-1">

View file

@ -25,7 +25,7 @@
@if (blank($privateKeyId))
<div
class="mb-4 flex items-center justify-between gap-3 rounded-lg bg-neutral-50 px-3 py-2.5 ring-1 ring-neutral-200 dark:bg-white/[0.025] dark:ring-white/[0.07]">
class="mb-4 flex items-center justify-between gap-3 rounded-lg bg-neutral-50 px-3 py-2.5 ring-1 ring-neutral-200 dark:bg-white/[0.05] dark:ring-white/[0.07]">
<span class="text-[12px] text-neutral-500 dark:text-fg-dim">Connected source</span>
<span class="text-[12px] font-medium text-neutral-900 dark:text-fg">
{{ data_get($application, 'source.name', 'No source connected') }}
@ -48,7 +48,7 @@ class="mb-4 flex items-center justify-between gap-3 rounded-lg bg-neutral-50 px-
<x-application.settings-section title="Deploy key"
description="The SSH key Coolify uses to clone this private repository.">
<div
class="mb-4 flex items-center justify-between gap-3 rounded-lg bg-neutral-50 px-3 py-2.5 ring-1 ring-neutral-200 dark:bg-white/[0.025] dark:ring-white/[0.07]">
class="mb-4 flex items-center justify-between gap-3 rounded-lg bg-neutral-50 px-3 py-2.5 ring-1 ring-neutral-200 dark:bg-white/[0.05] dark:ring-white/[0.07]">
<span class="text-[12px] text-neutral-500 dark:text-fg-dim">Attached private key</span>
<span class="text-[12px] font-medium text-neutral-900 dark:text-fg">{{ $privateKeyName }}</span>
</div>
@ -56,7 +56,7 @@ class="mb-4 flex items-center justify-between gap-3 rounded-lg bg-neutral-50 px-
<div class="grid gap-2 sm:grid-cols-2">
@forelse ($privateKeys as $key)
<button type="button" wire:click="setPrivateKey('{{ $key->id }}')"
class="flex min-h-11 items-center rounded-lg border border-neutral-200 bg-white px-3 text-left text-[12px] font-medium transition-colors hover:bg-neutral-50 dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:bg-white/[0.05]">
class="flex min-h-11 items-center rounded-lg border border-neutral-200 bg-white px-3 text-left text-[12px] font-medium transition-colors hover:bg-neutral-50 dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:bg-white/[0.05]">
{{ $key->name }}
</button>
@empty
@ -129,7 +129,7 @@ class="size-4 shrink-0 text-coollabs dark:text-warning" />
shortConfirmationLabel="Confirmation text" :confirmWithPassword="false">
<x-slot:trigger>
<button type="button"
class="group flex w-full min-h-16 items-center gap-3 rounded-xl border border-neutral-200 bg-white p-3 text-left shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:bg-neutral-50 hover:shadow-md focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14] dark:hover:bg-white/[0.05]">
class="group flex w-full min-h-16 items-center gap-3 rounded-xl border border-neutral-200 bg-white p-3 text-left shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:bg-neutral-50 hover:shadow-md focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14] dark:hover:bg-white/[0.05]">
<div
class="flex size-9 shrink-0 items-center justify-center rounded-lg border border-neutral-200 bg-neutral-50 text-neutral-500 transition-colors group-hover:border-neutral-300 group-hover:text-neutral-700 dark:border-white/[0.08] dark:bg-white/[0.04] dark:text-fg-dim dark:group-hover:border-white/[0.12] dark:group-hover:text-fg">
<x-git-icon class="size-4" :git="$sourceMorph" />

View file

@ -105,7 +105,7 @@
class="flex min-h-20 items-center gap-3 rounded-[10px] border p-3 text-left transition-colors"
:class="restoreType === 'file'
? 'border-coollabs/35 bg-coollabs/[0.06] text-coollabs dark:border-warning/30 dark:bg-warning/[0.08] dark:text-warning'
: 'border-neutral-200 bg-white hover:border-neutral-300 dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]'">
: 'border-neutral-200 bg-white hover:border-neutral-300 dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]'">
<span
class="flex size-9 shrink-0 items-center justify-center rounded-lg bg-neutral-100 dark:bg-white/[0.06]">
<x-reicon name="file" class="size-4" />
@ -122,7 +122,7 @@ class="flex size-9 shrink-0 items-center justify-center rounded-lg bg-neutral-10
class="flex min-h-20 items-center gap-3 rounded-[10px] border p-3 text-left transition-colors"
:class="restoreType === 's3'
? 'border-coollabs/35 bg-coollabs/[0.06] text-coollabs dark:border-warning/30 dark:bg-warning/[0.08] dark:text-warning'
: 'border-neutral-200 bg-white hover:border-neutral-300 dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]'">
: 'border-neutral-200 bg-white hover:border-neutral-300 dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]'">
<span
class="flex size-9 shrink-0 items-center justify-center rounded-lg bg-neutral-100 dark:bg-white/[0.06]">
<x-reicon name="storages" class="size-4" />
@ -139,7 +139,7 @@ class="flex size-9 shrink-0 items-center justify-center rounded-lg bg-neutral-10
{{-- File Restore Section --}}
@can('update', $this->resource)
<div x-cloak x-show="restoreType === 'file'"
class="mt-4 rounded-[10px] border border-neutral-200 bg-neutral-50 p-4 dark:border-white/[0.08] dark:bg-white/[0.025]">
class="mt-4 rounded-[10px] border border-neutral-200 bg-neutral-50 p-4 dark:border-white/[0.08] dark:bg-white/[0.05]">
<form class="flex flex-col gap-3 sm:flex-row sm:items-end">
<div class="min-w-0 flex-1">
<x-forms.input label="File path on the server"
@ -158,7 +158,7 @@ class="mt-4 rounded-[10px] border border-neutral-200 bg-neutral-50 p-4 dark:bord
</div>
<form action="{{ route('upload.backup', ['databaseUuid' => $resourceUuid]) }}"
class="dropzone rounded-lg! border! border-dashed! border-neutral-300! bg-white! dark:border-white/[0.12]! dark:bg-white/[0.025]!"
class="dropzone rounded-lg! border! border-dashed! border-neutral-300! bg-white! dark:border-white/[0.12]! dark:bg-white/[0.05]!"
id="my-dropzone" wire:ignore>
@csrf
</form>
@ -206,7 +206,7 @@ class="mt-4 flex flex-col gap-3 rounded-lg border border-neutral-200 bg-white p-
->all();
@endphp
<div x-cloak x-show="restoreType === 's3'"
class="mt-4 rounded-[10px] border border-neutral-200 bg-neutral-50 p-4 dark:border-white/[0.08] dark:bg-white/[0.025]">
class="mt-4 rounded-[10px] border border-neutral-200 bg-neutral-50 p-4 dark:border-white/[0.08] dark:bg-white/[0.05]">
<div class="grid gap-4 sm:grid-cols-2">
<x-forms.listbox id="s3StorageId" label="S3 storage" :options="$s3StorageOptions"
placeholder="Select storage" live />

View file

@ -56,7 +56,7 @@ class="input h-8! w-full pl-8! text-[13px]!" placeholder="Search backups" />
description="Create a schedule to start protecting this database."
icon-name="storages" />
@else
<div x-cloak x-show="search === '' || hasMatches()" class="data-table overflow-hidden rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.025]">
<div x-cloak x-show="search === '' || hasMatches()" class="data-table overflow-hidden rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.05]">
<div class="data-table-header scheduled-backups-table-grid">
<span>Schedule</span>
<span>Latest run</span>

View file

@ -103,7 +103,7 @@ class="flex size-7.5 items-center justify-center rounded-md transition-colors"
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
<template x-for="project in paginatedProjects" :key="project.uuid">
<article
class="group relative flex min-h-28 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]">
class="group relative flex min-h-28 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]">
<a :href="project.href" {{ wireNavigate() }} class="absolute inset-0 rounded-xl"
:aria-label="`Open ${project.name}`"></a>
<div class="flex items-start gap-3">
@ -151,15 +151,15 @@ class="flex size-7.5 items-center justify-center rounded-md text-neutral-400 tra
</article>
</template>
</div>
<x-client-pagination x-show="filteredProjects.length > 0" class="mt-3 rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.025]"
<x-client-pagination x-show="filteredProjects.length > 0" class="mt-3 rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.05]"
summary="`${rangeStart}-${rangeEnd} of ${filteredProjects.length}`" page-size-model="pageSize"
storage-key="coolify.page-size.projects" :options="[12, 24, 48, 96]" />
</div>
<div x-show="viewMode === 'table'"
class="overflow-hidden rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.025]">
class="overflow-hidden rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.05]">
<div
class="projects-table-grid border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-faint">
class="projects-table-grid border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.05] dark:text-fg-faint">
<div>Project</div>
<div>Environments</div>
<div>Resources</div>
@ -212,7 +212,7 @@ class="flex size-7 items-center justify-center rounded-md text-neutral-400 trans
</div>
<div x-show="filteredProjects.length === 0"
class="flex min-h-52 flex-col items-center justify-center rounded-xl border border-neutral-200 bg-white px-6 text-center dark:border-white/[0.08] dark:bg-white/[0.025]">
class="flex min-h-52 flex-col items-center justify-center rounded-xl border border-neutral-200 bg-white px-6 text-center dark:border-white/[0.08] dark:bg-white/[0.05]">
<x-reicon name="search" class="mb-3 size-6 text-neutral-300 dark:text-fg-faint" />
<p class="text-[13px] font-medium">No matching projects</p>
<p class="mt-1 text-[12px] text-neutral-500 dark:text-fg-dim">

View file

@ -111,7 +111,7 @@ class="application-settings-section-body grid grid-cols-1 justify-start gap-3 te
<article role="button" tabindex="0" :aria-label="'Deploy ' + application.name"
@click="setType(application.id)" @keydown.enter.self.prevent="setType(application.id)"
@keydown.space.self.prevent="setType(application.id)"
class="group flex min-h-48 cursor-pointer flex-col rounded-xl border border-neutral-200 bg-white p-4 transition-colors hover:border-neutral-300 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]">
class="group flex min-h-48 cursor-pointer flex-col rounded-xl border border-neutral-200 bg-white p-4 transition-colors hover:border-neutral-300 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]">
<div class="flex min-w-0 items-start gap-3">
<div
class="flex size-11 shrink-0 items-center justify-center overflow-hidden rounded-xl border border-neutral-200 bg-neutral-50 dark:border-white/[0.08] dark:bg-white/[0.04]">
@ -157,7 +157,7 @@ class="mt-auto flex items-center gap-1.5 border-t border-neutral-200 pt-3 dark:b
<article role="button" tabindex="0" :aria-label="'Deploy ' + application.name"
@click="setType(application.id)" @keydown.enter.self.prevent="setType(application.id)"
@keydown.space.self.prevent="setType(application.id)"
class="group flex min-h-48 cursor-pointer flex-col rounded-xl border border-neutral-200 bg-white p-4 transition-colors hover:border-neutral-300 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]">
class="group flex min-h-48 cursor-pointer flex-col rounded-xl border border-neutral-200 bg-white p-4 transition-colors hover:border-neutral-300 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]">
<div class="flex min-w-0 items-start gap-3">
<div
class="flex size-11 shrink-0 items-center justify-center overflow-hidden rounded-xl border border-neutral-200 bg-neutral-50 dark:border-white/[0.08] dark:bg-white/[0.04]">
@ -206,7 +206,7 @@ class="application-settings-section-body grid grid-cols-1 justify-start gap-3 te
<article role="button" tabindex="0" :aria-label="'Deploy ' + database.name"
@click="setType(database.id)" @keydown.enter.self.prevent="setType(database.id)"
@keydown.space.self.prevent="setType(database.id)"
class="group flex min-h-48 cursor-pointer flex-col rounded-xl border border-neutral-200 bg-white p-4 transition-colors hover:border-neutral-300 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]">
class="group flex min-h-48 cursor-pointer flex-col rounded-xl border border-neutral-200 bg-white p-4 transition-colors hover:border-neutral-300 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]">
<div class="flex min-w-0 items-center gap-3">
<div
class="flex size-11 shrink-0 items-center justify-center overflow-hidden rounded-xl border border-neutral-200 bg-neutral-50 dark:border-white/[0.08] dark:bg-white/[0.04]">
@ -283,7 +283,7 @@ class="text-[11px] text-neutral-500 dark:text-fg-faint">
@click="setType('one-click-service-' + service.id)"
@keydown.enter.self.prevent="setType('one-click-service-' + service.id)"
@keydown.space.self.prevent="setType('one-click-service-' + service.id)"
class="group flex min-h-48 cursor-pointer flex-col rounded-xl border border-neutral-200 bg-white p-4 transition-colors hover:border-neutral-300 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]">
class="group flex min-h-48 cursor-pointer flex-col rounded-xl border border-neutral-200 bg-white p-4 transition-colors hover:border-neutral-300 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]">
<div class="flex min-w-0 items-start gap-3">
<div
class="flex size-11 shrink-0 items-center justify-center overflow-hidden rounded-xl border border-neutral-200 bg-neutral-50 dark:border-white/[0.08] dark:bg-white/[0.04]">

View file

@ -166,9 +166,9 @@ class="flex size-7.5 items-center justify-center rounded-md transition-colors"
</div>
<div x-show="viewMode === 'table'"
class="overflow-hidden rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.025]">
class="overflow-hidden rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.05]">
<div
class="environment-resource-grid border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-faint">
class="environment-resource-grid border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.05] dark:text-fg-faint">
<div>Resource</div>
<div class="resource-type">Type</div>
<div>Status</div>
@ -271,7 +271,7 @@ class="flex min-h-52 flex-col items-center justify-center px-6 text-center">
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
<template x-for="item in paginatedResources" :key="item.uuid">
<article
class="group relative flex min-h-28 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]">
class="group relative flex min-h-28 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]">
<a :href="item.hrefLink"
@click="if (item.version === 'v5') { $event.preventDefault(); window.location.assign(item.hrefLink) }"
{{ wireNavigate() }} class="absolute inset-0 rounded-xl"
@ -320,7 +320,7 @@ class="relative z-10 max-w-full self-start truncate text-[11px] text-neutral-500
</div>
<div x-show="filteredResources.length === 0"
class="flex min-h-52 flex-col items-center justify-center rounded-xl border border-neutral-200 bg-white px-6 text-center dark:border-white/[0.08] dark:bg-white/[0.025]">
class="flex min-h-52 flex-col items-center justify-center rounded-xl border border-neutral-200 bg-white px-6 text-center dark:border-white/[0.08] dark:bg-white/[0.05]">
<x-reicon name="search" class="mb-3 size-6 text-neutral-300 dark:text-fg-faint" />
<p class="text-[13px] font-medium">No matching resources</p>
<p class="mt-1 text-[12px] text-neutral-500 dark:text-fg-dim">
@ -328,7 +328,7 @@ class="flex min-h-52 flex-col items-center justify-center rounded-xl border bord
</p>
</div>
<x-client-pagination x-show="filteredResources.length > 0"
class="mt-3 rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.025]"
class="mt-3 rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.05]"
summary="filteredResources.length === 0 ? '0 resources' : `${rangeStart}-${rangeEnd} of ${filteredResources.length}`"
page-size-model="pageSize" storage-key="coolify.page-size.environment-resources" />
</div>

View file

@ -144,10 +144,10 @@ class="flex size-7.5 items-center justify-center rounded-md transition-colors"
<div :class="viewMode === 'grid'
? 'grid grid-cols-1 gap-3 sm:grid-cols-2'
: 'overflow-x-auto rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.025]'">
: 'overflow-x-auto rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.05]'">
@if ($applications->isNotEmpty() || $databases->isNotEmpty())
<div x-cloak x-show="viewMode === 'table'"
class="grid min-w-[48rem] grid-cols-[minmax(14rem,1fr)_minmax(12rem,1fr)_12rem_5rem] gap-3 border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-faint">
class="grid min-w-[48rem] grid-cols-[minmax(14rem,1fr)_minmax(12rem,1fr)_12rem_5rem] gap-3 border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.05] dark:text-fg-faint">
<div>Resource</div>
<div>Image</div>
<div class="justify-self-start">Status</div>

View file

@ -25,7 +25,7 @@ class="flex min-h-0 flex-col gap-3">
</div>
<div
class="flex flex-col items-stretch gap-2 rounded-lg border border-neutral-200 bg-neutral-50 p-1 sm:flex-row sm:flex-wrap sm:items-center dark:border-white/[0.08] dark:bg-white/[0.025]">
class="flex flex-col items-stretch gap-2 rounded-lg border border-neutral-200 bg-neutral-50 p-1 sm:flex-row sm:flex-wrap sm:items-center dark:border-white/[0.08] dark:bg-white/[0.05]">
<x-forms.checkbox label="Escape special characters in labels"
helper="By default, $ (and other characters) is escaped. A $ in a label is saved as $$. Turn this off to use environment variables inside labels."
id="isContainerLabelEscapeEnabled" instantSave />

View file

@ -36,7 +36,7 @@ class="rounded bg-neutral-100 px-1.5 py-0.5 font-mono text-xs text-neutral-700 d
<a href="{{ route('server.show', ['server_uuid' => data_get($resource, 'destination.server.uuid')]) }}"
{{ wireNavigate() }} class="button">Open server</a>
<x-application.restart-limit-warning :application="$resource" />
<x-status-summary :status="$resource->status" />
<x-status-summary :status="$resource->status" align="right" />
@if ($hasAdditionalDestinations)
<x-forms.button canGate="deploy" :canResource="$resource"
wire:click="redeploy('{{ data_get($resource, 'destination.id') }}','{{ data_get($resource, 'destination.server.id') }}')">

View file

@ -106,11 +106,11 @@
},
}">
<div data-terminal-session-canvas
class="application-console-shell flex h-full min-h-0 flex-col overflow-hidden rounded-lg p-3 sm:p-6"
class="application-console-shell flex h-full min-h-0 flex-col overflow-hidden rounded-lg"
:data-console-theme="consoleTheme"
:style="{ '--terminal-scrollbar': themeAccents[consoleTheme] }">
<header
class="terminal-session-toolbar absolute top-3 right-3 left-3 z-20 flex items-center gap-3 text-white select-none">
class="terminal-session-toolbar flex items-center gap-3 text-white select-none">
@if ($type === 'server')
<div class="terminal-session-target-trigger flex h-8 min-w-0 max-w-sm flex-1 items-center gap-2 rounded-md px-2.5 text-xs font-medium text-white/70"
x-data="{ autoConnected: false }"
@ -150,7 +150,7 @@ class="terminal-session-target-trigger flex h-8 max-w-[34rem] min-w-48 items-cen
</svg>
</button>
<div x-cloak x-show="containerOpen" x-transition.origin.top.left
class="terminal-target-picker terminal-target-list absolute top-11 left-0 z-50 max-h-72 w-80 overflow-y-auto rounded-lg border p-1 shadow-[0_18px_50px_rgba(0,0,0,0.35)]">
class="terminal-target-picker terminal-target-list absolute top-11 left-0 z-50 max-h-72 w-80 overflow-y-auto rounded-lg border p-1 shadow-[var(--shadow-dropdown)]">
<template x-for="option in containerOptions" :key="option.value">
<button type="button"
class="flex h-8 w-full items-center gap-2 rounded-md px-2 text-left text-[11px] text-white/65 transition-colors hover:bg-white/[0.07] hover:text-white"
@ -175,12 +175,12 @@ class="size-3 shrink-0 text-[#fcd452]" viewBox="0 0 12 12"
:theme-accents="$consoleThemeAccents" />
</header>
<div class="terminal-session-panel mt-8 flex min-h-0 flex-1 flex-col overflow-hidden">
<div class="terminal-session-panel flex min-h-0 flex-1 flex-col overflow-hidden p-3 sm:p-6">
<div class="application-console-block min-h-0 flex-1">
@if ($type !== 'server' && $containers->count() > 1)
<div x-cloak x-show="!targetChosen" data-terminal-target-picker="launcher"
class="absolute inset-0 z-20 flex items-start justify-start p-6 sm:p-10">
<div class="terminal-target-picker w-full max-w-md rounded-lg border p-2 shadow-[0_18px_50px_rgba(0,0,0,0.28)]">
<div class="terminal-target-picker w-full max-w-md rounded-lg border p-2 shadow-[var(--shadow-dropdown)]">
<div class="px-2 pt-1 pb-2">
<div class="text-sm font-semibold text-white/80">Start a terminal session</div>
<div class="mt-0.5 text-[11px] text-white/45">

View file

@ -295,7 +295,7 @@
@if ($streamLogs)
<div class="sr-only" wire:poll.2000ms="getLogs(true)" aria-hidden="true"></div>
@endif
<div x-show="expanded" {{ $collapsible ? 'x-collapse' : '' }}
<div x-show="expanded" {{ $collapsible ? 'x-collapse.duration.200ms' : '' }}
:class="fullscreen ? 'fullscreen flex flex-col !overflow-visible' : 'relative w-full mx-auto'"
:style="fullscreen ? 'max-height: none !important; height: 100% !important;' : ''">
<div class="runtime-log-panel"

View file

@ -25,7 +25,7 @@
@if ($executionCount > 0)
<div class="data-table w-full overflow-x-auto">
<div
class="data-table-header volume-backup-executions-grid border-b border-neutral-200 bg-neutral-50 dark:border-white/[0.08] dark:bg-white/[0.025]">
class="data-table-header volume-backup-executions-grid border-b border-neutral-200 bg-neutral-50 dark:border-white/[0.08] dark:bg-white/[0.05]">
<span>Status</span>
<span>Archive</span>
<span>Time</span>
@ -129,7 +129,7 @@ class="icon-button shrink-0 text-red-500 hover:text-red-600 dark:text-red-400 da
@if ($execution->message)
<pre
class="volume-backup-execution-message col-span-6 mt-2 max-h-32 overflow-auto rounded-lg border border-neutral-200 bg-neutral-50 p-2 text-[11px] whitespace-pre-wrap text-neutral-600 dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-dim">{{ $execution->message }}</pre>
class="volume-backup-execution-message col-span-6 mt-2 max-h-32 overflow-auto rounded-lg border border-neutral-200 bg-neutral-50 p-2 text-[11px] whitespace-pre-wrap text-neutral-600 dark:border-white/[0.08] dark:bg-white/[0.05] dark:text-fg-dim">{{ $execution->message }}</pre>
@endif
</div>
@endforeach

View file

@ -16,7 +16,7 @@
</div>
</x-slot:actions>
<div class="mb-4 flex items-center justify-between gap-3 rounded-lg bg-neutral-50 px-3 py-2.5 ring-1 ring-neutral-200 dark:bg-white/[0.025] dark:ring-white/[0.07]">
<div class="mb-4 flex items-center justify-between gap-3 rounded-lg bg-neutral-50 px-3 py-2.5 ring-1 ring-neutral-200 dark:bg-white/[0.05] dark:ring-white/[0.07]">
<span class="text-[12px] text-neutral-500 dark:text-fg-dim">
{{ $backup?->targetType() ?? ($storage instanceof \App\Models\LocalFileVolume ? 'Directory' : 'Volume') }}
</span>

View file

@ -123,7 +123,7 @@ class="flex size-7.5 items-center justify-center rounded-md transition-colors"
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
<template x-for="environment in paginatedEnvironments" :key="environment.uuid">
<article
class="group relative flex min-h-28 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]">
class="group relative flex min-h-28 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]">
<a :href="environment.href" {{ wireNavigate() }} class="absolute inset-0 rounded-xl"
:aria-label="`Open ${environment.name}`"></a>
@ -165,15 +165,15 @@ class="flex size-7.5 items-center justify-center rounded-md text-neutral-400 tra
</article>
</template>
</div>
<x-client-pagination x-show="filteredEnvironments.length > 0" class="mt-3 rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.025]"
<x-client-pagination x-show="filteredEnvironments.length > 0" class="mt-3 rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.05]"
summary="`${rangeStart}-${rangeEnd} of ${filteredEnvironments.length}`" page-size-model="pageSize"
storage-key="coolify.page-size.project-environments" :options="[12, 24, 48, 96]" />
</div>
<div x-show="viewMode === 'table'"
class="overflow-hidden rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.025]">
class="overflow-hidden rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.05]">
<div
class="environments-table-grid border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-faint">
class="environments-table-grid border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.05] dark:text-fg-faint">
<div>Environment</div>
<div class="environment-resource-count">Resources</div>
<div class="environment-description">Description</div>
@ -222,7 +222,7 @@ class="flex size-7 items-center justify-center rounded-md text-neutral-400 trans
</div>
<div x-show="filteredEnvironments.length === 0"
class="flex min-h-52 flex-col items-center justify-center rounded-xl border border-neutral-200 bg-white px-6 text-center dark:border-white/[0.08] dark:bg-white/[0.025]">
class="flex min-h-52 flex-col items-center justify-center rounded-xl border border-neutral-200 bg-white px-6 text-center dark:border-white/[0.08] dark:bg-white/[0.05]">
<x-reicon name="search" class="mb-3 size-6 text-neutral-300 dark:text-fg-faint" />
<p class="text-[13px] font-medium">No matching environments</p>
<p class="mt-1 text-[12px] text-neutral-500 dark:text-fg-dim">

View file

@ -28,7 +28,7 @@ class="button button-highlighted">
icon-name="file-content" size="sm" />
@else
<div>
<div class="grid grid-cols-[minmax(0,1fr)_12rem_1.75rem] items-center gap-3 border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[13px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-faint">
<div class="grid grid-cols-[minmax(0,1fr)_12rem_1.75rem] items-center gap-3 border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[13px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.05] dark:text-fg-faint">
<div class="pl-11">Script</div>
<div>Last updated</div>
<div class="w-7"></div>

View file

@ -24,7 +24,7 @@
@endif
<div
class="rounded-lg border border-neutral-200 bg-neutral-50 p-3 text-[11px] leading-5 text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-dim">
class="rounded-lg border border-neutral-200 bg-neutral-50 p-3 text-[11px] leading-5 text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.05] dark:text-fg-dim">
Create the token in the
<a :href="providerConsoleUrl"
target="_blank" class="font-medium text-coollabs hover:underline dark:text-warning">

View file

@ -20,7 +20,7 @@ class="button button-highlighted">
description="Add a provider token to provision new cloud servers." icon-name="keys" size="sm" />
@else
<div>
<div class="grid grid-cols-[minmax(0,1fr)_8rem_1.75rem] items-center gap-3 border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[13px] font-medium text-neutral-500 sm:grid-cols-[minmax(0,1fr)_8rem_minmax(0,1fr)_1.75rem] dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-faint">
<div class="grid grid-cols-[minmax(0,1fr)_8rem_1.75rem] items-center gap-3 border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[13px] font-medium text-neutral-500 sm:grid-cols-[minmax(0,1fr)_8rem_minmax(0,1fr)_1.75rem] dark:border-white/[0.08] dark:bg-white/[0.05] dark:text-fg-faint">
<div class="pl-11">Token</div>
<div class="text-center">Provider</div>
<div class="hidden sm:block">Description</div>

View file

@ -68,7 +68,7 @@ class="listbox-option justify-start! gap-2.5!" role="menuitem">
icon-name="keys" />
@else
<div>
<div class="grid grid-cols-[minmax(0,1fr)_7rem_1.75rem] items-center gap-3 border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[13px] font-medium text-neutral-500 sm:grid-cols-[minmax(0,1.2fr)_minmax(0,1fr)_7rem_1.75rem] dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-faint">
<div class="grid grid-cols-[minmax(0,1fr)_7rem_1.75rem] items-center gap-3 border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[13px] font-medium text-neutral-500 sm:grid-cols-[minmax(0,1.2fr)_minmax(0,1fr)_7rem_1.75rem] dark:border-white/[0.08] dark:bg-white/[0.05] dark:text-fg-faint">
<div class="pl-11">Private key</div>
<div class="hidden sm:block">Description</div>
<div class="text-center">Status</div>
@ -115,7 +115,7 @@ class="flex size-8 shrink-0 items-center justify-center rounded-lg border border
title="You do not have permission to view this private key">
<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-white text-neutral-400 dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-faint">
class="flex size-8 shrink-0 items-center justify-center rounded-lg border border-neutral-200 bg-white text-neutral-400 dark:border-white/[0.08] dark:bg-white/[0.05] dark:text-fg-faint">
<x-reicon name="keys" class="size-4" />
</div>
<div class="min-w-0 flex-1">

View file

@ -24,7 +24,7 @@
<div class="application-settings-section-body is-flush">
<div class="grid grid-cols-1 gap-3 p-3 sm:grid-cols-2 lg:grid-cols-4">
<a href="{{ route('server.create.type', ['type' => 'manual']) }}"
class="group flex min-h-32 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]"
class="group flex min-h-32 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]"
{{ wireNavigate() }}>
<div class="flex items-start">
<span
@ -52,7 +52,7 @@ class="flex size-8 items-center justify-center rounded-lg border border-neutral-
<div class="application-settings-section-body is-flush">
<div class="grid grid-cols-1 gap-3 p-3 sm:grid-cols-2 lg:grid-cols-3">
<a href="{{ route('server.create.type', ['type' => 'hetzner']) }}"
class="group flex min-h-32 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]"
class="group flex min-h-32 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]"
{{ wireNavigate() }}>
<div class="flex items-start">
<img src="{{ asset('svgs/hetzner.svg') }}" alt="Hetzner" class="size-8">
@ -66,7 +66,7 @@ class="group flex min-h-32 flex-col rounded-xl border border-neutral-200 bg-whit
</a>
<a href="{{ route('server.create.type', ['type' => 'vultr']) }}"
class="group flex min-h-32 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]"
class="group flex min-h-32 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]"
{{ wireNavigate() }}>
<div class="flex items-start">
<img src="https://www.vultr.com/media/logo_ondark.svg" alt="Vultr"
@ -81,7 +81,7 @@ class="h-8 w-20 object-contain object-left">
</a>
<a href="{{ route('server.create.type', ['type' => 'digital-ocean']) }}"
class="group flex min-h-32 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]"
class="group flex min-h-32 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]"
{{ wireNavigate() }}>
<div class="flex items-start">
<x-digital-ocean-icon class="size-8" />

View file

@ -134,7 +134,7 @@ class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
<a x-cloak
x-show="filteredServers.some(server => server.uuid === @js($server->uuid))"
href="{{ $serverRow['href'] }}" {{ wireNavigate() }}
class="group relative flex min-h-28 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]">
class="group relative flex min-h-28 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]">
@if ($server->isMetricsEnabled())
<livewire:dashboard.server-metrics-chart :server="$server"
:key="'server-index-metrics-'.$server->uuid" />
@ -170,9 +170,9 @@ class="flex size-8 shrink-0 items-center justify-center rounded-lg border border
</div>
<div x-show="viewMode === 'table'"
class="overflow-x-auto rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.025]">
class="overflow-x-auto rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.05]">
<div
class="grid min-w-[480px] grid-cols-[minmax(0,1fr)_9.5rem] border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-faint">
class="grid min-w-[480px] grid-cols-[minmax(0,1fr)_9.5rem] border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.05] dark:text-fg-faint">
<div>Server</div>
<div>Status</div>
</div>
@ -205,7 +205,7 @@ class="ml-auto flex size-6 shrink-0 items-center justify-center rounded-md"
</div>
<div x-show="filteredServers.length === 0"
class="flex min-h-52 flex-col items-center justify-center rounded-xl border border-neutral-200 bg-white px-6 text-center dark:border-white/[0.08] dark:bg-white/[0.025]">
class="flex min-h-52 flex-col items-center justify-center rounded-xl border border-neutral-200 bg-white px-6 text-center dark:border-white/[0.08] dark:bg-white/[0.05]">
<x-reicon name="search" class="mb-3 size-6 text-neutral-300 dark:text-fg-faint" />
<p class="text-[13px] font-medium">No matching servers</p>
<p class="mt-1 text-[12px] text-neutral-500 dark:text-fg-dim">Try a different search.</p>

View file

@ -146,7 +146,7 @@ class="flex size-9 shrink-0 items-center justify-center rounded-lg bg-neutral-10
Link provider
</button>
<div x-cloak x-show="open" x-transition.origin.top.right
class="absolute top-9 right-0 z-50 w-56 rounded-lg border border-neutral-200 bg-white p-1 shadow-modal dark:border-white/[0.1] dark:bg-raised">
class="absolute top-9 right-0 z-50 w-56 rounded-lg border border-neutral-200 bg-white p-1 shadow-dropdown dark:border-white/[0.1] dark:bg-raised">
@if (!$server->hetzner_server_id && $availableHetznerTokens->isNotEmpty())
<x-server.provider-link-modal :server="$server" provider="hetzner"
providerLabel="Hetzner" tokenModel="selectedHetznerTokenId"

View file

@ -75,7 +75,7 @@
<div class="flex h-full min-h-0 flex-col gap-4 overflow-y-auto scrollbar">
@if ($ask)
<div
class="rounded-[10px] border border-neutral-200 bg-neutral-50 px-4 py-3 text-[13px] leading-5 text-neutral-600 dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-dim">
class="rounded-[10px] border border-neutral-200 bg-neutral-50 px-4 py-3 text-[13px] leading-5 text-neutral-600 dark:border-white/[0.08] dark:bg-white/[0.05] dark:text-fg-dim">
This will revalidate the server, install or update Docker Engine, Docker Compose, and related
configuration. Docker Engine will restart, so running containers may be briefly unreachable.
</div>

View file

@ -22,17 +22,14 @@
$project->description,
...$project->environments->flatMap(fn ($environment) => [$environment->name, $environment->description])->all(),
]))">
<div class="mb-3">
<h2 class="text-[14px]! leading-5! font-semibold! text-black dark:text-fg">{{ $project->name }}</h2>
<p class="mt-0.5 text-[11px] text-neutral-500 dark:text-fg-faint">{{ $project->description ?: 'Project environments' }}</p>
</div>
<x-section-heading :title="$project->name" :subtitle="$project->description ?: 'Project environments'" />
@if ($project->environments->isEmpty())
<x-empty title="No environments in this project." size="sm" />
@else
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
@foreach ($project->environments as $environment)
<a x-show="matches(@js([$environment->name, $environment->description, $project->name]))"
class="group flex min-h-24 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]"
class="group flex min-h-24 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]"
href="{{ route('shared-variables.environment.show', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid]) }}" {{ wireNavigate() }}>
<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"><x-reicon name="layers" class="size-4" /></div>
@ -46,7 +43,7 @@ class="group flex min-h-24 flex-col rounded-xl border border-neutral-200 bg-whit
@endforeach
</div>
<div x-show="viewMode === 'list'" class="overflow-hidden rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.025]">
<div x-show="viewMode === 'list'" class="overflow-hidden rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.05]">
@foreach ($projects as $project)
@foreach ($project->environments as $environment)
<a x-show="matches(@js([$environment->name, $environment->description, $project->name]))"

View file

@ -7,7 +7,7 @@
<div class="w-full">
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
<a class="group flex min-h-32 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]"
<a class="group flex min-h-32 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]"
href="{{ route('shared-variables.team.index') }}" {{ wireNavigate() }}>
<div
class="flex size-8 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">
@ -21,7 +21,7 @@ class="flex size-8 items-center justify-center rounded-lg border border-neutral-
</div>
</a>
<a class="group flex min-h-32 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]"
<a class="group flex min-h-32 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]"
href="{{ route('shared-variables.project.index') }}" {{ wireNavigate() }}>
<div
class="flex size-8 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">
@ -35,7 +35,7 @@ class="flex size-8 items-center justify-center rounded-lg border border-neutral-
</div>
</a>
<a class="group flex min-h-32 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]"
<a class="group flex min-h-32 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]"
href="{{ route('shared-variables.environment.index') }}" {{ wireNavigate() }}>
<div
class="flex size-8 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">
@ -49,7 +49,7 @@ class="flex size-8 items-center justify-center rounded-lg border border-neutral-
</div>
</a>
<a class="group flex min-h-32 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]"
<a class="group flex min-h-32 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]"
href="{{ route('shared-variables.server.index') }}" {{ wireNavigate() }}>
<div
class="flex size-8 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">

View file

@ -19,7 +19,7 @@
<div x-cloak x-show="viewMode === 'grid'" class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
@foreach ($projects as $project)
<a x-show="matches(@js([$project->name, $project->description]))"
class="group flex min-h-28 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]"
class="group flex min-h-28 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]"
href="{{ route('shared-variables.project.show', ['project_uuid' => $project->uuid]) }}" {{ wireNavigate() }}>
<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">
@ -43,7 +43,7 @@ class="h-full w-full rounded-lg object-cover">
@endforeach
</div>
<div x-show="viewMode === 'list'" class="overflow-hidden rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.025]">
<div x-show="viewMode === 'list'" class="overflow-hidden rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.05]">
@foreach ($projects as $project)
<a x-show="matches(@js([$project->name, $project->description]))"
href="{{ route('shared-variables.project.show', ['project_uuid' => $project->uuid]) }}" {{ wireNavigate() }}

View file

@ -18,7 +18,7 @@
<div x-cloak x-show="viewMode === 'grid'" class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
@foreach ($servers as $server)
<a x-show="matches(@js([$server->name, $server->description, $server->ip]))"
class="group flex min-h-28 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]"
class="group flex min-h-28 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]"
href="{{ route('shared-variables.server.show', ['server_uuid' => $server->uuid]) }}" {{ wireNavigate() }}>
<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"><x-reicon name="servers" class="size-4" /></div>
@ -29,7 +29,7 @@ class="group flex min-h-28 flex-col rounded-xl border border-neutral-200 bg-whit
@endforeach
</div>
<div x-show="viewMode === 'list'" class="overflow-hidden rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.025]">
<div x-show="viewMode === 'list'" class="overflow-hidden rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.05]">
@foreach ($servers as $server)
<a x-show="matches(@js([$server->name, $server->description, $server->ip]))"
href="{{ route('shared-variables.server.show', ['server_uuid' => $server->uuid]) }}" {{ wireNavigate() }}

View file

@ -1,4 +1,4 @@
<div x-show="filteredItems.length === 0" class="flex min-h-52 flex-col items-center justify-center rounded-xl border border-neutral-200 bg-white px-6 text-center dark:border-white/[0.08] dark:bg-white/[0.025]">
<div x-show="filteredItems.length === 0" class="flex min-h-52 flex-col items-center justify-center rounded-xl border border-neutral-200 bg-white px-6 text-center dark:border-white/[0.08] dark:bg-white/[0.05]">
<x-reicon name="search" class="mb-3 size-6 text-neutral-300 dark:text-fg-faint" />
<p class="text-[13px] font-medium">No matching {{ $label }}</p>
<p class="mt-1 text-[12px] text-neutral-500 dark:text-fg-dim">Try a different search.</p>

View file

@ -300,7 +300,7 @@ class="rounded-lg border border-red-300 bg-red-50 p-4 ring-1 ring-inset ring-red
@endif
<div
class="rounded-lg border border-neutral-200 bg-neutral-50 p-3 text-[12px] leading-5 text-neutral-600 dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-dim">
class="rounded-lg border border-neutral-200 bg-neutral-50 p-3 text-[12px] leading-5 text-neutral-600 dark:border-white/[0.08] dark:bg-white/[0.05] dark:text-fg-dim">
<p class="font-medium text-black dark:text-fg">Mandatory permissions</p>
<p class="mt-1">Contents: read · Metadata: read · Email: read</p>
</div>

View file

@ -36,7 +36,7 @@ class="flex w-full items-center justify-between px-3 py-2.5 text-left text-[12px
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</button>
<div x-cloak x-show="open" x-collapse class="border-t border-neutral-200 px-3 py-3 dark:border-white/[0.08]">
<div x-cloak x-show="open" x-collapse.duration.200ms class="border-t border-neutral-200 px-3 py-3 dark:border-white/[0.08]">
<div class="grid gap-4 sm:grid-cols-2">
<x-forms.input id="html_url" label="HTML URL" required
helper="For GitHub Enterprise, enter your instance URL (e.g. https://github.example.com)." />

View file

@ -17,7 +17,7 @@ class="h-8! w-full rounded-lg! border-neutral-200! bg-white! py-0! pr-3! pl-8! t
</div>
<div class="overflow-x-auto">
<div
class="grid min-w-[680px] grid-cols-[minmax(10rem,.8fr)_minmax(10rem,.8fr)_minmax(12rem,1fr)_8rem] border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-faint">
class="grid min-w-[680px] grid-cols-[minmax(10rem,.8fr)_minmax(10rem,.8fr)_minmax(12rem,1fr)_8rem] border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.05] dark:text-fg-faint">
<div>Project</div>
<div>Environment</div>
<div>Resource</div>

View file

@ -124,7 +124,7 @@ class="h-8! w-full rounded-lg! border-neutral-200! bg-white! py-0! pr-3! pl-8! t
</div>
<div class="overflow-x-auto">
<div
class="grid min-w-[680px] grid-cols-[minmax(10rem,.8fr)_minmax(10rem,.8fr)_minmax(12rem,1fr)_8rem] border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-faint">
class="grid min-w-[680px] grid-cols-[minmax(10rem,.8fr)_minmax(10rem,.8fr)_minmax(12rem,1fr)_8rem] border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.05] dark:text-fg-faint">
<div>Project</div>
<div>Environment</div>
<div>Resource</div>
@ -275,7 +275,7 @@ class="flex w-full items-center justify-between rounded-lg border border-neutral
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</button>
<div x-cloak x-show="open" x-collapse
<div x-cloak x-show="open" x-collapse.duration.200ms
class="mt-3 grid gap-4 rounded-lg border border-neutral-200 p-3 lg:grid-cols-2 dark:border-white/[0.08]">
<x-forms.input id="htmlUrl" label="GitLab URL"
helper="Only change this for self-hosted GitLab (e.g. https://gitlab.example.com)." />

View file

@ -36,7 +36,7 @@ class="flex w-full items-center justify-between px-3 py-2.5 text-left text-[12px
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</button>
<div x-cloak x-show="open" x-collapse class="border-t border-neutral-200 px-3 py-3 dark:border-white/[0.08]">
<div x-cloak x-show="open" x-collapse.duration.200ms class="border-t border-neutral-200 px-3 py-3 dark:border-white/[0.08]">
<div class="grid gap-4 sm:grid-cols-2">
<x-forms.input id="html_url" label="GitLab URL" required
helper="For self-hosted GitLab, enter your instance URL (e.g. https://gitlab.example.com)." />

View file

@ -48,7 +48,7 @@ class="button button-highlighted">
<div x-cloak x-show="viewMode === 'grid'" class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
@foreach ($s3 as $storage)
<a x-show="matches(@js([$storage->name, $storage->description ?: 'S3-compatible storage', $storage->is_usable ? 'Connected' : 'Not usable']))" {{ wireNavigate() }} href="/storages/{{ $storage->uuid }}"
class="group flex min-h-28 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]">
class="group flex min-h-28 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]">
<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">
@ -74,8 +74,8 @@ class="flex size-8 shrink-0 items-center justify-center rounded-lg border border
</a>
@endforeach
</div>
<div x-show="viewMode === 'table'" class="overflow-x-auto rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.025]">
<div class="grid min-w-[620px] grid-cols-[minmax(0,1fr)_minmax(10rem,.8fr)_9rem] border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-faint"><div>Storage</div><div>Description</div><div>Status</div></div>
<div x-show="viewMode === 'table'" class="overflow-x-auto rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.05]">
<div class="grid min-w-[620px] grid-cols-[minmax(0,1fr)_minmax(10rem,.8fr)_9rem] border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.05] dark:text-fg-faint"><div>Storage</div><div>Description</div><div>Status</div></div>
@foreach ($s3 as $storage)
<a x-show="matches(@js([$storage->name, $storage->description ?: 'S3-compatible storage', $storage->is_usable ? 'Connected' : 'Not usable']))" {{ wireNavigate() }} href="/storages/{{ $storage->uuid }}" class="grid min-h-14 min-w-[620px] grid-cols-[minmax(0,1fr)_minmax(10rem,.8fr)_9rem] items-center border-b border-neutral-200 px-4 py-2.5 text-[12px] transition-colors last:border-b-0 hover:bg-neutral-50 hover:no-underline dark:border-white/[0.07] dark:hover:bg-white/[0.025]">
<div class="truncate font-semibold text-black dark:text-fg">{{ $storage->name }}</div>

View file

@ -17,7 +17,7 @@ class="h-8! w-full rounded-lg! border-neutral-200! bg-white! py-0! pr-3! pl-8! t
<div class="overflow-x-auto">
<div
class="grid min-w-[780px] grid-cols-[minmax(12rem,1fr)_9rem_7rem_minmax(15rem,1.2fr)] border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-faint">
class="grid min-w-[780px] grid-cols-[minmax(12rem,1fr)_9rem_7rem_minmax(15rem,1.2fr)] border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.05] dark:text-fg-faint">
<div>Backup target</div>
<div>Frequency</div>
<div>Status</div>

View file

@ -23,7 +23,7 @@ class="app-tab h-6! px-2.5!"
<div class="grid gap-6 lg:grid-cols-[minmax(0,.8fr)_minmax(0,1.2fr)]">
<div
class="rounded-[10px] border border-neutral-200 bg-neutral-50 p-4 dark:border-white/[0.08] dark:bg-white/[0.025]">
class="rounded-[10px] border border-neutral-200 bg-neutral-50 p-4 dark:border-white/[0.08] dark:bg-white/[0.05]">
<p class="text-[11px] font-medium text-neutral-500 dark:text-fg-faint">Base price</p>
<div class="mt-2 flex items-end gap-1.5">
<span x-show="selected === 'monthly'" x-cloak

View file

@ -8,7 +8,7 @@
@else
<div class="data-table w-full">
<div
class="grid grid-cols-[minmax(0,1fr)_minmax(8rem,.55fr)_8rem] border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-faint">
class="grid grid-cols-[minmax(0,1fr)_minmax(8rem,.55fr)_8rem] border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.05] dark:text-fg-faint">
<span>Resource</span>
<span>Server</span>
<span>Status</span>

View file

@ -96,7 +96,7 @@ class="flex size-7.5 items-center justify-center rounded-md transition-colors"
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
<template x-for="tag in paginatedTags" :key="tag.id">
<article
class="group relative flex min-h-28 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]">
class="group relative flex min-h-28 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]">
<a :href="tag.href" {{ wireNavigate() }} class="absolute inset-0 rounded-xl"
:aria-label="`Open ${tag.name}`"></a>
@ -121,15 +121,15 @@ class="flex size-8 shrink-0 items-center justify-center rounded-lg border border
</article>
</template>
</div>
<x-client-pagination x-show="filteredTags.length > 0" class="mt-3 rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.025]"
<x-client-pagination x-show="filteredTags.length > 0" class="mt-3 rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.05]"
summary="`${rangeStart}-${rangeEnd} of ${filteredTags.length}`" page-size-model="pageSize"
storage-key="coolify.page-size.tags" :options="[12, 24, 48, 96]" />
</div>
<div x-show="viewMode === 'table'"
class="overflow-hidden rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.025]">
class="overflow-hidden rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.05]">
<div
class="tags-table-grid border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-faint">
class="tags-table-grid border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.05] dark:text-fg-faint">
<div>Tag</div>
<div>Resources</div>
<div>Applications</div>
@ -161,7 +161,7 @@ class="flex size-8 shrink-0 items-center justify-center rounded-lg border border
</div>
<div x-show="filteredTags.length === 0"
class="flex min-h-52 flex-col items-center justify-center rounded-xl border border-neutral-200 bg-white px-6 text-center dark:border-white/[0.08] dark:bg-white/[0.025]">
class="flex min-h-52 flex-col items-center justify-center rounded-xl border border-neutral-200 bg-white px-6 text-center dark:border-white/[0.08] dark:bg-white/[0.05]">
<x-reicon name="search" class="mb-3 size-6 text-neutral-300 dark:text-fg-faint" />
<p class="text-[13px] font-medium">No matching tags</p>
<p class="mt-1 text-[12px] text-neutral-500 dark:text-fg-dim">
@ -250,7 +250,7 @@ function tagsIndex() {
<div class="flex flex-col gap-6">
<div class="grid grid-cols-1 gap-3 sm:grid-cols-3">
<div
class="rounded-xl border border-neutral-200 bg-white p-3 dark:border-white/[0.08] dark:bg-white/[0.025]">
class="rounded-xl border border-neutral-200 bg-white p-3 dark:border-white/[0.08] dark:bg-white/[0.05]">
<p class="text-[11px] font-medium text-neutral-500 dark:text-fg-faint">Resources</p>
<p class="mt-1 text-[20px] font-semibold tracking-tight text-black dark:text-fg">
{{ $resourceCount }}
@ -260,7 +260,7 @@ class="rounded-xl border border-neutral-200 bg-white p-3 dark:border-white/[0.08
</p>
</div>
<div
class="rounded-xl border border-neutral-200 bg-white p-3 dark:border-white/[0.08] dark:bg-white/[0.025]">
class="rounded-xl border border-neutral-200 bg-white p-3 dark:border-white/[0.08] dark:bg-white/[0.05]">
<p class="text-[11px] font-medium text-neutral-500 dark:text-fg-faint">Applications</p>
<p class="mt-1 text-[20px] font-semibold tracking-tight text-black dark:text-fg">
{{ $applications?->count() ?? 0 }}
@ -270,7 +270,7 @@ class="rounded-xl border border-neutral-200 bg-white p-3 dark:border-white/[0.08
</p>
</div>
<div
class="rounded-xl border border-neutral-200 bg-white p-3 dark:border-white/[0.08] dark:bg-white/[0.025]">
class="rounded-xl border border-neutral-200 bg-white p-3 dark:border-white/[0.08] dark:bg-white/[0.05]">
<p class="text-[11px] font-medium text-neutral-500 dark:text-fg-faint">Active deployments</p>
<p class="mt-1 text-[20px] font-semibold tracking-tight text-black dark:text-fg">
{{ collect($deploymentsPerTagPerServer ?? [])->flatten(1)->count() }}
@ -308,7 +308,7 @@ class="rounded-xl border border-neutral-200 bg-white p-3 dark:border-white/[0.08
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
@foreach ($applications ?? [] as $application)
<a {{ wireNavigate() }} href="{{ $application->link() }}"
class="group flex min-h-24 flex-col rounded-lg border border-neutral-200 bg-neutral-50/70 p-3 transition-colors hover:border-neutral-300 hover:no-underline dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]">
class="group flex min-h-24 flex-col rounded-lg border border-neutral-200 bg-neutral-50/70 p-3 transition-colors hover:border-neutral-300 hover:no-underline dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]">
<div class="flex items-start gap-2.5">
<div
class="flex size-7 shrink-0 items-center justify-center rounded-md border border-neutral-200 bg-white text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.035] dark:text-fg-dim">
@ -332,7 +332,7 @@ class="truncate text-[12px]! leading-4! font-semibold! text-black dark:text-fg">
@foreach ($services ?? [] as $service)
<a {{ wireNavigate() }} href="{{ $service->link() }}"
class="group flex min-h-24 flex-col rounded-lg border border-neutral-200 bg-neutral-50/70 p-3 transition-colors hover:border-neutral-300 hover:no-underline dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]">
class="group flex min-h-24 flex-col rounded-lg border border-neutral-200 bg-neutral-50/70 p-3 transition-colors hover:border-neutral-300 hover:no-underline dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]">
<div class="flex items-start gap-2.5">
<div
class="flex size-7 shrink-0 items-center justify-center rounded-md border border-neutral-200 bg-white text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.035] dark:text-fg-dim">
@ -374,7 +374,7 @@ class="truncate text-[12px]! leading-4! font-semibold! text-black dark:text-fg">
</div>
@else
<div
class="grid min-w-[620px] grid-cols-[minmax(0,1fr)_minmax(10rem,.55fr)_7rem] border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-faint">
class="grid min-w-[620px] grid-cols-[minmax(0,1fr)_minmax(10rem,.55fr)_7rem] border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.05] dark:text-fg-faint">
<div>Resource</div>
<div>Server</div>
<div>Status</div>

View file

@ -176,11 +176,11 @@ class="px-3 py-8 text-center text-[13px] text-neutral-500 dark:text-fg-dim">
</div>
@else
<div wire:key="terminal-session-canvas" data-terminal-session-canvas
class="application-console-shell flex h-full min-h-0 flex-col overflow-hidden rounded-lg p-3 sm:p-6"
class="application-console-shell flex h-full min-h-0 flex-col overflow-hidden rounded-lg"
:data-console-theme="consoleTheme"
:style="{ '--terminal-scrollbar': themeAccents[consoleTheme] }">
<header
class="terminal-session-toolbar absolute top-3 right-3 left-3 z-20 flex items-center gap-3 text-white select-none">
class="terminal-session-toolbar flex items-center gap-3 text-white select-none">
<div class="relative flex min-w-0 flex-1 items-center gap-2"
x-on:click.outside="targetOpen = false">
@if ($isLoadingContainers)
@ -215,7 +215,7 @@ class="terminal-session-target-trigger flex h-8 min-w-0 max-w-sm cursor-pointer
</button>
<div x-cloak x-show="targetOpen" x-transition.origin.top.left
class="terminal-target-picker absolute top-11 left-0 z-50 w-80 overflow-hidden rounded-lg border shadow-[0_18px_50px_rgba(0,0,0,0.35)]">
class="terminal-target-picker absolute top-11 left-0 z-50 w-80 overflow-hidden rounded-lg border shadow-[var(--shadow-dropdown)]">
<div class="border-b border-white/[0.08] p-1.5">
<div class="relative">
<x-reicon name="search"
@ -264,7 +264,7 @@ class="px-2 py-5 text-center text-[11px] text-white/35">
:theme-accents="$consoleThemeAccents" />
</header>
<div class="terminal-session-panel mt-8 flex min-h-0 flex-1 flex-col overflow-hidden">
<div class="terminal-session-panel flex min-h-0 flex-1 flex-col overflow-hidden p-3 sm:p-6">
<div class="application-console-block min-h-0 flex-1 overflow-hidden">
@if ($isLoadingContainers)
<div class="flex h-full min-h-0 items-center justify-center">
@ -287,7 +287,7 @@ class="px-2 py-5 text-center text-[11px] text-white/35">
@else
<div x-cloak x-show="!targetChosen" data-terminal-target-picker="launcher"
class="absolute inset-0 z-20 flex items-start justify-start p-6 sm:p-10">
<div class="terminal-target-picker w-full max-w-md overflow-hidden rounded-lg border shadow-[0_18px_50px_rgba(0,0,0,0.28)]">
<div class="terminal-target-picker w-full max-w-md overflow-hidden rounded-lg border shadow-[var(--shadow-dropdown)]">
<div class="border-b border-white/[0.08] p-2">
<div class="px-1 pb-2">
<div class="text-sm font-semibold text-white/80">Start a terminal session</div>

View file

@ -75,7 +75,7 @@ class="listbox-option justify-start! gap-2.5!" role="menuitem">
<div x-cloak x-show="viewMode === 'grid'" class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
@foreach ($sources as $source)
@if ($source->getMorphClass() === 'App\Models\GithubApp')
<a x-show="matches(@js([$source->name, 'GitHub', $source->organization, $source->isConnected() ? 'Connected' : 'Setup incomplete']))" class="group flex min-h-28 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]"
<a x-show="matches(@js([$source->name, 'GitHub', $source->organization, $source->isConnected() ? 'Connected' : 'Setup incomplete']))" class="group flex min-h-28 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]"
{{ wireNavigate() }}
href="{{ route('source.github.show', ['github_app_uuid' => data_get($source, 'uuid')]) }}">
<div class="flex items-start gap-3">
@ -102,7 +102,7 @@ class="flex size-8 shrink-0 items-center justify-center rounded-lg border border
</div>
</a>
@elseif ($source->getMorphClass() === 'App\Models\GitlabApp')
<a x-show="matches(@js([$source->name, 'GitLab', $source->group_name, $source->isConnected() ? 'Connected' : 'Setup incomplete']))" class="group flex min-h-28 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]"
<a x-show="matches(@js([$source->name, 'GitLab', $source->group_name, $source->isConnected() ? 'Connected' : 'Setup incomplete']))" class="group flex min-h-28 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]"
{{ wireNavigate() }}
href="{{ route('source.gitlab.show', ['gitlab_app_uuid' => data_get($source, 'uuid')]) }}">
<div class="flex items-start gap-3">
@ -131,8 +131,8 @@ class="flex size-8 shrink-0 items-center justify-center rounded-lg border border
@endif
@endforeach
</div>
<div x-show="viewMode === 'table'" class="overflow-x-auto rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.025]">
<div class="grid min-w-[620px] grid-cols-[minmax(0,1fr)_minmax(10rem,.7fr)_9rem] border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-faint"><div>Source</div><div>Provider</div><div>Status</div></div>
<div x-show="viewMode === 'table'" class="overflow-x-auto rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.05]">
<div class="grid min-w-[620px] grid-cols-[minmax(0,1fr)_minmax(10rem,.7fr)_9rem] border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.05] dark:text-fg-faint"><div>Source</div><div>Provider</div><div>Status</div></div>
@foreach ($sources as $source)
@php
$isGithub = $source->getMorphClass() === 'App\\Models\\GithubApp';

View file

@ -3,6 +3,8 @@
test('custom color theme is available and applied across theme controls', function () {
$appearance = file_get_contents(resource_path('views/livewire/profile/appearance.blade.php'));
$accountMenu = file_get_contents(resource_path('views/components/top-user-menu.blade.php'));
$component = file_get_contents(resource_path('views/components/theme-controls.blade.php'));
$picker = file_get_contents(resource_path('views/components/theme-controls/picker.blade.php'));
$layout = file_get_contents(resource_path('views/layouts/base.blade.php'));
$deploymentLogs = file_get_contents(resource_path('views/livewire/project/application/deployment/show.blade.php'));
$serverTiming = file_get_contents(resource_path('views/components/server-timing-hud.blade.php'));
@ -11,22 +13,23 @@
$utilities = file_get_contents(resource_path('css/utilities.css'));
expect($appearance)
->toContain('<x-theme-controls variant="full"')
->and($accountMenu)
->toContain('<x-theme-controls variant="menu"')
->and($component)
->toContain("['value' => 'custom', 'label' => 'Custom'")
->toContain('<x-theme-controls.picker')
->and($picker)
->toContain('type="color"')
->toContain('absolute inset-0 z-10 h-full w-full cursor-pointer opacity-0')
->toContain('requestAnimationFrame(() =>')
->toContain('@input="previewThemeColor($event.target.value)"')
->toContain('@change="saveThemeColor($event.target.value)"')
->toContain('aria-label="Custom theme color"')
->toContain('setCustomMode')
->and($layout)
->toContain('window.themeControls')
->toContain('requestAnimationFrame(() =>')
->toContain("localStorage.setItem('themeColor', color)")
->toContain("this.theme === 'custom'")
->and($accountMenu)
->toContain("['value' => 'custom', 'label' => 'Custom']")
->toContain('aria-label="Custom theme color"')
->toContain('requestAnimationFrame(() =>')
->toContain('@input="previewThemeColor($event.target.value)"')
->toContain('@change="saveThemeColor($event.target.value)"')
->toContain("localStorage.setItem('themeColor', color)")
->toContain('absolute inset-0 z-10 h-full w-full cursor-pointer opacity-0')
->and($layout)
->toContain("t === 'custom'")
->toContain("localStorage.themeColor || '#6b16ed'")
@ -35,34 +38,35 @@
->toContain("document.addEventListener('livewire:navigated', window.applyStoredTheme)")
->and($styles)
->toContain('html[data-theme="custom"]')
->toContain('--theme-base-color: #6b16ed;')
->toContain('--theme-bright-color: color-mix(in srgb, var(--theme-base-color) 85%, white);')
->toContain('--theme-base-color: oklch(49.65% 0.2709 289.33);')
->toContain('--theme-bright-color: color-mix(in srgb, var(--theme-base-color) 85%, oklch(100% 0 0));')
->toContain('--color-accent: var(--theme-bright-color);')
->toContain('--color-coollabs: var(--theme-bright-color);')
->toContain('--color-warning: var(--theme-bright-color);')
->toContain('--color-accent-foreground: var(--theme-accent-foreground);')
->toContain('--color-fg-dim: #e4dfea;')
->toContain('--color-fg-faint: #d8d2df;')
->toContain('--color-fg-dim: oklch(91.08% 0.0157 306.4);')
->toContain('--color-fg-faint: oklch(87.24% 0.0188 306.63);')
->toContain('html[data-theme="custom"] .control-selected')
->toContain('--theme-scrollbar-thumb: color-mix(in srgb, var(--theme-bright-color) 70%, var(--theme-accent-foreground));')
->toContain('--theme-border-color: color-mix(in oklab, var(--theme-base-color) 42%, #52525b);')
->toContain('--theme-placeholder-color: color-mix(in srgb, white 20%, var(--theme-base-color));')
->toContain('--theme-border-color: color-mix(in oklab, var(--theme-base-color) 42%, oklch(44.19% 0.0146 285.79));')
->toContain('--theme-fg-on-surface: oklch(from var(--theme-base-color) clamp(0.72, l, 0.86) min(c, 0.13) h);')
->toContain('--theme-placeholder-color: oklch(from var(--theme-base-color) 0.62 min(c, 0.03) h);')
->toContain('html[data-theme="custom"] *')
->toContain('scrollbar-color: var(--theme-scrollbar-thumb) var(--color-panel);')
->toContain('html[data-theme="custom"] *::-webkit-scrollbar-thumb')
->toContain('html[data-theme="custom"] .application-settings-navigation')
->toContain('scrollbar-gutter: stable;')
->toContain('--color-panel: color-mix(in oklab, var(--theme-base-color) 14%, #0c0c0d);')
->toContain('--color-surface: color-mix(in oklab, var(--theme-base-color) 18%, #101011);')
->toContain('--color-panel: color-mix(in oklab, var(--theme-base-color) 14%, oklch(15.48% 0.0021 286.15));')
->toContain('--color-surface: color-mix(in oklab, var(--theme-base-color) 18%, oklch(17.35% 0.0020 286.18));')
->toContain('--coollabs-elevated: var(--color-surface);')
->toContain('--coollabs-recessed: var(--color-raised);')
->toContain('--coollabs-base: color-mix(in oklab, var(--theme-base-color) 22%, #111112);')
->toContain('--coollabs-base: color-mix(in oklab, var(--theme-base-color) 22%, oklch(17.81% 0.0020 286.19));')
->toContain('--color-content-surface: var(--coollabs-base);')
->toContain('--coollabs-fill: color-mix(in oklab, var(--theme-base-color) 32%, #18181a);')
->toContain('--color-nav-text: #e4dfea;')
->toContain('--color-nav-muted: #d8d2df;')
->toContain('--color-log: color-mix(in oklab, var(--theme-base-color) 20%, #080809);')
->toContain('--color-log-toolbar: color-mix(in oklab, var(--theme-base-color) 26%, #101011);')
->toContain('--coollabs-fill: color-mix(in oklab, var(--theme-base-color) 32%, oklch(20.99% 0.0039 286.06));')
->toContain('--color-nav-text: oklch(91.08% 0.0157 306.4);')
->toContain('--color-nav-muted: oklch(87.24% 0.0188 306.63);')
->toContain('--color-log: color-mix(in oklab, var(--theme-base-color) 20%, oklch(13.49% 0.0024 286.07));')
->toContain('--color-log-toolbar: color-mix(in oklab, var(--theme-base-color) 26%, oklch(17.35% 0.0020 286.18));')
->toContain('html[data-theme="custom"] .logs-viewer-toolbar')
->toContain('html[data-theme="custom"] .logs-viewer-timestamp')
->toContain('html[data-theme="custom"] #nprogress .bar')
@ -78,7 +82,7 @@
->toContain('border-color: var(--coollabs-hairline) !important;')
->toContain('html[data-theme="custom"] input::placeholder')
->toContain('color: var(--theme-placeholder-color) !important;')
->toMatch('/html\[data-theme="custom"\] input::placeholder,[^{]+\{[^}]*opacity: 0\.7;/s')
->toMatch('/html\[data-theme="custom"\] input::placeholder,[^{]+\{[^}]*opacity: 1;/s')
->toContain('html[data-theme="custom"] input:read-only')
->and($deploymentLogs)
->toContain('dark:bg-log')

View file

@ -5,7 +5,7 @@
$applicationSection = str($view)->before('@else')->value();
expect($applicationSection)
->toContain('<x-status-summary :status="$resource->status" />')
->toContain('<x-status-summary :status="$resource->status" align="right" />')
->not->toContain('<x-status :resource="$resource"');
});

View file

@ -0,0 +1,111 @@
<?php
/**
* The custom theme derives its whole palette from one user-picked color.
* These tests lock in two guarantees:
* 1. The pipeline is authored entirely in OKLCH (no hex color literals).
* 2. Foreground colors (accent text, placeholders) are clamped so they stay
* readable on the dark surfaces regardless of the picked hue.
*/
function customThemeBlock(): string
{
$css = file_get_contents(resource_path('css/app.css'));
return str($css)
->after('html[data-theme="custom"] {')
->before('}')
->toString();
}
it('derives the custom theme entirely in oklch with no hex color literals', function () {
expect(customThemeBlock())
->not->toMatch('/#[0-9a-fA-F]{3,8}\b/')
->toContain('--theme-base-color:')
->toContain('oklch(');
});
it('adds clamped, readable foreground tokens for the custom theme', function () {
$block = customThemeBlock();
expect($block)
// Accent text/icons on dark surfaces: lightness forced into a readable band.
->toContain('--theme-fg-on-surface:')
->toMatch('/--theme-fg-on-surface:\s*oklch\(from var\(--theme-base-color\)[^;]*clamp\(/')
// Placeholder: light, near-neutral, guaranteed readable.
->toContain('--theme-placeholder-color:')
->toMatch('/--theme-placeholder-color:\s*oklch\(from var\(--theme-base-color\)/');
});
it('routes custom-theme accent text utilities to the readable on-surface token', function () {
$css = file_get_contents(resource_path('css/app.css'));
expect($css)
->toMatch('/\[class~="text-warning"\][^{]*\{\s*color:\s*var\(--theme-fg-on-surface\)/')
->toContain('[class~="text-coollabs"]');
});
it('uses the clamped placeholder token for custom-theme inputs', function () {
$css = file_get_contents(resource_path('css/app.css'));
expect($css)
->toMatch('/html\[data-theme="custom"\] input::placeholder[^}]*var\(--theme-placeholder-color\)/');
});
it('adds a light custom variant that tints surfaces toward white with dark foreground', function () {
$css = file_get_contents(resource_path('css/app.css'));
expect($css)
->toContain('html[data-theme="custom"]:not(.dark)')
// Surfaces hold a fixed high lightness with injected base-hue chroma, so the
// tint is visible (not a washed-out mix into white).
->toMatch('/html\[data-theme="custom"\]:not\(\.dark\)[^}]*--color-app:\s*oklch\(from var\(--theme-base-color\) 9[0-9](\.[0-9]+)?% min\(c,/s')
// Accent text/icons clamp to a dark, readable band on light surfaces.
->toMatch('/html\[data-theme="custom"\]:not\(\.dark\)[^}]*--theme-fg-on-surface:\s*oklch\(from var\(--theme-base-color\) clamp\(0\.3/s');
});
it('lets the theme scripts toggle a custom light or dark mode via customMode', function () {
$base = file_get_contents(resource_path('views/layouts/base.blade.php'));
$component = file_get_contents(resource_path('views/components/theme-controls.blade.php'));
$picker = file_get_contents(resource_path('views/components/theme-controls/picker.blade.php'));
$appearance = file_get_contents(resource_path('views/livewire/profile/appearance.blade.php'));
$accountMenu = file_get_contents(resource_path('views/components/top-user-menu.blade.php'));
expect($base)
// One shared Alpine factory holds the theme logic.
->toContain('window.themeControls')
->toContain('localStorage.customMode')
->toContain('setCustomMode')
// The picker no longer force-adds dark; it follows customMode.
->not->toContain("document.documentElement.classList.add('dark')")
// Light/dark lives inside the custom color picker popover.
->and($picker)
->toContain('customMode')
->toContain('setCustomMode')
->toContain('type="color"')
->and($component)
// Both variants open the shared picker popover for Custom.
->toContain('<x-theme-controls.picker')
->toContain('chooseCustom')
// Both the page and the dropdown reuse the one component.
->and($appearance)
->toContain('<x-theme-controls variant="full"')
->and($accountMenu)
->toContain('<x-theme-controls variant="menu"');
});
it('computes the theme base color and accent foreground in oklch, not hex', function () {
$base = file_get_contents(resource_path('views/layouts/base.blade.php'));
$accentFn = str($base)
->after('window.themeAccentForeground')
->before('window.applyStoredTheme')
->toString();
expect($base)
->toContain('window.hexToOklch')
->and($accentFn)
->toContain('oklch(0% 0 0)')
->toContain('oklch(100% 0 0)')
->not->toContain('#000000')
->not->toContain('#ffffff');
});

View file

@ -1,5 +1,13 @@
<?php
use Illuminate\Support\Facades\Blade;
test('input modal renders without requiring a modal id', function () {
$html = Blade::render('<x-modal-input>Modal content</x-modal-input>');
expect($html)->toContain('Modal content');
});
test('confirmation modal closes before dispatching an event that can open another modal', function () {
$modal = file_get_contents(resource_path('views/components/modal-confirmation.blade.php'));

View file

@ -180,8 +180,8 @@
expect($view)
->toContain('data-terminal-session-canvas')
->toContain('terminal-session-toolbar absolute top-3 right-3 left-3')
->toContain('terminal-session-panel mt-8')
->toContain('terminal-session-toolbar flex items-center gap-3 text-white select-none')
->toContain('terminal-session-panel flex min-h-0 flex-1 flex-col overflow-hidden p-3 sm:p-6')
->toContain('<x-terminal.theme-selector')
->not->toContain('application-console-header flex h-[30px]');
});

View file

@ -45,6 +45,18 @@
->toContain('stableFrames');
});
test('application configuration subitems scroll without navigating the active page', function () {
$sidebar = file_get_contents(resource_path('views/components/application/configuration-sidebar.blade.php'));
expect($sidebar)
->toContain("@if (\$menuItem['active'])")
->toContain('<button type="button" class="menu-subitem"')
->toContain("window.scrollToSettingsSection?.('{{ \$section['id'] }}')")
->toContain('@else')
->toContain("href=\"{{ route(\$menuItem['route'], \$applicationRouteParameters) }}#{{ \$section['id'] }}\"")
->not->toContain("if (document.getElementById('{{ \$section['id'] }}'))");
});
test('postgresql general navigation lists each in-page settings section', function () {
$sidebar = file_get_contents(resource_path('views/components/database/configuration-sidebar.blade.php'));
$general = file_get_contents(resource_path('views/livewire/project/database/postgresql/general.blade.php'));

View file

@ -27,7 +27,10 @@
$layout = file_get_contents(resource_path('views/layouts/app.blade.php'));
expect($layout)
->toContain("collapsed: localStorage.getItem('sidebarCollapsed') === 'true'")
->toContain("userCollapsed: localStorage.getItem('sidebarCollapsed') === 'true'")
->toContain('return this.userCollapsed || (this.autoCollapse && this.hasSecondBar);')
->toContain('this.userCollapsed = this.collapsed;')
->toContain("localStorage.setItem('sidebarCollapsed', this.userCollapsed);")
->toContain('sidebarReady: false')
->toContain(":class=\"[collapsed ? 'lg:w-16' : 'lg:w-56', sidebarReady ? 'transition-[width] duration-200' : '']\"")
->toContain(":class=\"[collapsed ? 'lg:ml-16' : 'lg:ml-56', sidebarReady ? 'transition-[margin] duration-200' : '']\"");

View file

@ -90,7 +90,7 @@
expect($view)
->toContain('data-terminal-session-canvas')
->toContain('p-3 sm:p-6')
->toContain('terminal-session-panel mt-8')
->toContain('terminal-session-panel flex min-h-0 flex-1 flex-col overflow-hidden p-3 sm:p-6')
->and($styles)
->toContain('.terminal-session-panel')
->toContain('border-radius: 0.75rem;')
@ -145,7 +145,7 @@
->and($selector)
->toContain('terminal-theme-trigger flex h-8 items-center gap-2 rounded-md px-2.5 text-xs font-medium')
->and($view)
->toContain('terminal-session-toolbar absolute top-3 right-3 left-3')
->toContain('terminal-session-toolbar flex items-center gap-3 text-white select-none')
->not->toContain('terminal-theme-trigger');
});
@ -177,7 +177,7 @@
$view = file_get_contents(resource_path('views/livewire/terminal/index.blade.php'));
expect($view)
->toContain('terminal-session-toolbar absolute top-3 right-3 left-3')
->toContain('terminal-session-toolbar flex items-center gap-3 text-white select-none')
->toContain('<x-terminal.theme-selector')
->not->toContain('class="absolute top-3 right-3 z-20"');
});
@ -256,7 +256,7 @@
->and($appCss)
->toContain('[data-console-theme="system"]')
->toContain('html:not(.dark) .application-console-shell[data-console-theme="system"]')
->toContain('--console-theme-border: #d4d4d8')
->toContain('--console-theme-border: oklch(87.11% 0.0055 286.29)')
->toContain('background: transparent')
->toContain('html.dark .application-console-shell[data-console-theme="system"]');
});

View file

@ -0,0 +1,57 @@
<?php
function cssSource(string $file): string
{
return file_get_contents(__DIR__.'/../../resources/css/'.$file);
}
function bladeSource(string $path): string
{
return file_get_contents(__DIR__.'/../../resources/views/'.$path);
}
it('gives the shared popover surface the dropdown lift, not the modal lift', function () {
$utilities = cssSource('utilities.css');
expect($utilities)
->toContain('@utility surface-popover')
->toContain('var(--shadow-dropdown)');
// The surface-popover block itself must not fall back to the heavy modal shadow.
preg_match('/@utility surface-popover\s*\{(.*?)\}/s', $utilities, $match);
expect($match[1] ?? '')
->toContain('var(--shadow-dropdown)')
->not->toContain('var(--shadow-modal)');
});
it('renders toasts and change-pending popovers with the shared dropdown-level surface', function (string $path) {
$source = bladeSource($path);
expect($source)
->toContain('surface-popover')
->not->toContain('--shadow-modal');
})->with([
'components/toast.blade.php',
'components/configuration-warning.blade.php',
'components/proxy-configuration-warning.blade.php',
'components/popup-small.blade.php',
'livewire/deployments-indicator.blade.php',
]);
it('uses the dropdown shadow on floating menus instead of the modal shadow', function (string $path) {
expect(bladeSource($path))
->toContain('shadow-dropdown')
->not->toContain('shadow-modal');
})->with([
'livewire/server/show.blade.php',
'livewire/project/application/deployment/show.blade.php',
]);
it('gives the command palette the shared dropdown lift', function () {
$app = cssSource('app.css');
preg_match('/\.command-palette\s*\{(.*?)\}/s', $app, $match);
expect($match[1] ?? '')
->toContain('var(--shadow-dropdown)')
->not->toContain('var(--shadow-modal)');
});