Merge remote-tracking branch 'origin/next' into coolify-analytics-traefik-caddy

This commit is contained in:
Andras Bacsai
2026-09-08 14:28:24 +02:00
132 changed files with 2054 additions and 988 deletions
+504 -246
View File
@@ -20,51 +20,152 @@
--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);
}
/* Standard buttons have a compact physical edge. Hover lifts the face by 1px;
pressing nudges it down while the depth disappears. */
.button:not(:disabled) {
--button-depth-color: rgb(0 0 0 / 0.22);
--button-depth: 0 2px 0 var(--button-depth-color);
--button-depth-hover: 0 3px 0 var(--button-depth-color);
box-shadow: var(--button-depth);
}
.button.button-highlighted:not(:disabled),
.button[isHighlighted]:not(:disabled) {
--button-depth-color: color-mix(in oklab, var(--color-coollabs) 52%, black);
}
.dark .button:not(.button-highlighted):not(.button-error):not([isHighlighted]):not(:disabled) {
--button-depth-color: rgb(255 255 255 / 0.08);
}
.button.button-error:not(:disabled) {
--button-depth-color: var(--color-red-300);
}
.dark .button.button-error:not(:disabled) {
--button-depth-color: var(--color-red-800);
}
/* Keep the shared focus indicator when the depth shadow owns box-shadow. */
.button:not(:disabled):focus-visible {
box-shadow: var(--button-depth), 0 0 0 1px var(--color-accent);
}
.button:not(:disabled):hover {
transform: translateY(-1px);
box-shadow: var(--button-depth-hover);
}
.button:not(:disabled):hover:focus-visible {
box-shadow: var(--button-depth-hover), 0 0 0 1px var(--color-accent);
}
/* Other button-like controls retain the lighter scale feedback. */
.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 {
transition-duration: 120ms, 120ms, 120ms, 80ms, 80ms;
}
.icon-button:not(:disabled):active,
.app-tab:active,
.split-action-main:not(:disabled):active,
.split-action-caret:not(:disabled):active {
transform: scale(0.97);
}
.button:not(:disabled):active {
transform: translateY(2px);
box-shadow: none;
}
/* 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;
}
/*
@@ -302,7 +403,7 @@
*/
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 {
@@ -328,8 +429,16 @@ option {
@apply dark:text-white dark:bg-coolgray-100;
}
button[isError]:not(:disabled) {
@apply text-red-800 dark:text-red-300 bg-red-50 dark:bg-red-900/30 border-red-300 dark:border-red-800 hover:bg-red-300 hover:text-white dark:hover:bg-red-800 dark:hover:text-white;
.button.button-error {
@apply text-red-800 dark:text-red-300 bg-red-50 dark:bg-red-900/30 border-red-300 dark:border-red-800;
}
.button.button-error:disabled {
@apply text-red-800 dark:text-red-300 bg-red-50 dark:bg-red-900/30 border-red-300 dark:border-red-800;
}
.button-error:not(:disabled) {
@apply hover:bg-red-100 hover:text-red-900 dark:hover:bg-red-700 dark:hover:text-white;
}
button[isHighlighted]:not(:disabled) {
@@ -549,25 +658,24 @@ html[data-theme="custom"] #nprogress .spinner-icon {
.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);
}
@@ -577,6 +685,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;
@@ -700,6 +821,10 @@ html.dark .console-theme-selector::-webkit-scrollbar-thumb {
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 {
@@ -756,11 +881,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 {
@@ -868,7 +1053,7 @@ html:not(.dark) .application-console-shell[data-console-theme="system"] .termina
}
html:not(.dark) .application-console-shell[data-console-theme="system"] .terminal-session-panel {
background: transparent;
background: var(--coollabs-base);
}
.terminal-target-list {
@@ -895,7 +1080,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"] {
@@ -968,75 +1153,166 @@ html:not(.dark) .application-console-shell[data-console-theme="system"] .termina
* 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,
@@ -1095,7 +1371,55 @@ html[data-theme="custom"] [class~="dark:bg-white/[0.025]"] {
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,
@@ -1362,27 +1686,6 @@ html[data-theme="custom"] textarea:disabled {
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 {
@@ -1393,7 +1696,9 @@ html[data-theme="custom"] textarea:disabled {
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;
}
@@ -1459,7 +1764,14 @@ html[data-theme="custom"] textarea:disabled {
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 {
@@ -1469,12 +1781,16 @@ html[data-theme="custom"] textarea:disabled {
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;
}
@@ -1513,28 +1829,71 @@ html[data-theme="custom"] textarea:disabled {
}
@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;
}
}
@@ -1604,7 +1963,7 @@ html[data-theme="custom"] textarea:disabled {
.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,
@@ -1647,7 +2006,7 @@ html[data-theme="custom"] textarea:disabled {
* 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 {
@@ -1706,7 +2065,7 @@ html[data-theme="custom"] textarea:disabled {
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);
@@ -1780,20 +2139,6 @@ html[data-theme="custom"] textarea:disabled {
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%;
@@ -2018,7 +2363,7 @@ html[data-theme="custom"] textarea: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 {
@@ -2043,7 +2388,7 @@ html[data-theme="custom"] textarea:disabled {
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) {
@@ -2139,7 +2484,7 @@ html[data-theme="custom"] textarea:disabled {
.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 */
@@ -3108,7 +3453,7 @@ input[type="search"]::-webkit-search-results-decoration {
/* 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;
}
@@ -3609,7 +3954,7 @@ html[data-theme="custom"] .logs-viewer-timestamp {
.runtime-log-viewport {
min-height: 12rem;
border-radius: 0 0 8px 8px;
background: #fafafa;
background: var(--coollabs-elevated);
color: #262626;
}
@@ -3927,7 +4272,7 @@ html[data-theme="custom"] .runtime-log-columns {
.chip-input:focus-within {
border-color: var(--color-accent);
box-shadow: 0 0 0 1px var(--color-accent);
box-shadow: none;
}
.chip-input input {
@@ -4000,93 +4345,6 @@ html[data-theme="custom"] .runtime-log-columns {
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 {
@@ -4243,7 +4501,7 @@ html[data-theme="custom"] .runtime-log-columns {
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 {
+23 -12
View File
@@ -15,7 +15,7 @@
}
@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 @@
/* 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 @@
/* 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,17 +116,17 @@
&: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;
}
}
@utility button {
@apply inline-flex shrink-0 gap-1.5 justify-center items-center whitespace-nowrap px-2.5 h-8 min-h-8 text-[13px] text-black normal-case rounded-md border outline-0 cursor-pointer font-medium transition-colors bg-white border-neutral-200 hover:bg-neutral-100 dark:bg-white/[0.06] dark:text-fg dark:hover:text-fg dark:hover:bg-white/[0.1] dark:border-white/[0.08] hover:text-black disabled:cursor-not-allowed min-w-fit dark:disabled:text-fg-faint disabled:border-neutral-200 dark:disabled:border-white/[0.06] disabled:hover:bg-transparent disabled:bg-transparent disabled:text-neutral-300 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent;
@apply inline-flex shrink-0 gap-1.5 justify-center items-center whitespace-nowrap px-2.5 h-8 min-h-8 text-[13px] text-black capitalize rounded-md border outline-0 cursor-pointer font-medium transition-colors bg-white border-neutral-200 hover:bg-neutral-100 dark:bg-white/[0.06] dark:text-fg dark:hover:text-fg dark:hover:bg-white/[0.1] dark:border-white/[0.08] hover:text-black disabled:cursor-not-allowed min-w-fit dark:disabled:text-fg-faint disabled:border-neutral-200 dark:disabled:border-white/[0.06] disabled:hover:bg-transparent disabled:bg-transparent disabled:text-neutral-300 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent;
}
@utility button-highlighted {
@@ -162,7 +162,7 @@
}
@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-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 @@
/* 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-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 @@
@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;
}
+13
View File
@@ -127,3 +127,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);
@@ -2,6 +2,14 @@
<x-auth.shell title="Coolify" description="Verify your identity to finish signing in.">
<div class="flex flex-col gap-4" x-data="{
showRecovery: false,
submitting: false,
handleSubmit(event) {
if (this.submitting) {
event.preventDefault();
}
this.submitting = true;
},
submitAuthenticatorCode(event) {
event.target.value = event.target.value.replace(/\D/g, '').slice(0, 6);
@@ -30,7 +38,8 @@
<p x-show="showRecovery" x-cloak>Enter one of the recovery codes you saved when setting up two-factor authentication.</p>
</div>
<form x-ref="challengeForm" action="/two-factor-challenge" method="POST" class="flex flex-col gap-4">
<form x-ref="challengeForm" action="/two-factor-challenge" method="POST" class="flex flex-col gap-4"
@submit="handleSubmit($event)">
@csrf
<div x-show="!showRecovery" class="flex flex-col gap-3">
@@ -54,7 +63,7 @@
</button>
</div>
<x-forms.button class="w-full justify-center" type="submit" isHighlighted>
<x-forms.button class="w-full justify-center" type="submit" x-bind:disabled="submitting" isHighlighted>
Verify and continue
</x-forms.button>
</form>
@@ -244,52 +244,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>
@@ -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]',
@@ -9,8 +9,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">
@@ -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,
])>
@@ -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>
@@ -49,7 +49,7 @@
@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"
@@ -132,7 +132,7 @@
{{-- 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 @@
{{-- 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">
@@ -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>
@@ -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!">
@@ -13,6 +13,7 @@
'wireOpen' => null,
'contentClicks' => true,
'isLarge' => false,
'fixedHeight' => false,
])
<div x-data="{ modalOpen: @if ($wireOpen) $wire.entangle(@js($wireOpen)) @else false @endif }"
@@ -38,23 +39,24 @@
<template x-teleport="body">
<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"
class="fixed inset-0 z-99 overflow-hidden">
<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,
'lg:w-auto lg:min-w-2xl lg:max-w-4xl' => ! $isLarge,
'sm:h-[40rem]' => $fixedHeight,
])
style="box-shadow: 0 0 0 1px var(--coollabs-hairline), var(--shadow-modal)">
<header class="flex-wrap! sm:flex-nowrap!">
+1 -1
View File
@@ -248,6 +248,6 @@
</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>
@@ -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 @@
</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">
+1 -1
View File
@@ -11,7 +11,7 @@
{{ $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))
@@ -9,8 +9,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">
@@ -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 @@
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 @@
: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'];
@@ -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>
@@ -28,7 +28,7 @@
<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">
@@ -16,22 +16,26 @@ x-init="$watch('slideOverOpen', value => {
<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">
@@ -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 @@
</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([
@@ -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>
@@ -14,7 +14,7 @@
</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"
@@ -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
@@ -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>
+2 -3
View File
@@ -101,10 +101,9 @@
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">
@@ -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 @@
</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 @@
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" />
@@ -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"
+67 -26
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 @@
</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 @@
<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 @@
{{-- ============ 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 }}
+93 -4
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();
@@ -95,7 +184,7 @@
</head>
@section('body')
<body class="overflow-y-scroll dark:text-inherit text-black">
<body class="dark:text-inherit text-black">
<x-toast />
<x-icon-tooltip />
<script data-navigate-once>
+1 -1
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
@@ -40,7 +40,7 @@
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 @@
<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 @@
<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 @@
<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 @@
<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 @@
<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 @@
@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 @@
@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 @@
</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 @@
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 @@
@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" />
+6 -32
View File
@@ -19,21 +19,8 @@
<livewire:dashboard.traffic-analytics />
<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"
@@ -59,7 +46,7 @@
@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>
@@ -127,21 +114,8 @@
</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())
@@ -192,7 +166,7 @@
<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" />
@@ -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 @@
</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 @@
@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 @@
</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>
@@ -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 @@
{{-- 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">
@@ -62,7 +62,7 @@
<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 @@
</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)
@@ -31,7 +31,7 @@
<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>
@@ -47,7 +47,7 @@
step2ButtonText="Permanently Delete" canGate="delete"
:canResource="$destination" />
@else
<x-forms.button disabled tooltip="The default Coolify destination cannot be deleted.">
<x-forms.button isError disabled tooltip="The default Coolify destination cannot be deleted.">
Delete destination
</x-forms.button>
@endif
@@ -751,7 +751,7 @@
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 @@
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 @@
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 @@
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 @@
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 }}">
@@ -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>
@@ -226,8 +226,9 @@
<h2>Two-factor authentication</h2>
<p>Add a time-based one-time password to protect your account.</p>
</div>
@if (! request()->user()->two_factor_confirmed_at
&& session('status') !== 'two-factor-authentication-enabled')
@if (request()->user()->two_factor_confirmed_at)
<x-status-badge status="Enabled" type="success" />
@elseif (session('status') !== 'two-factor-authentication-enabled')
<form action="/user/two-factor-authentication" method="POST">
@csrf
<x-forms.button type="submit">Configure 2FA</x-forms.button>
@@ -284,7 +285,7 @@
@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
@@ -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>
@@ -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" />
@@ -6,7 +6,7 @@
<livewire:project.application.heading :application="$application" wire:key="application-heading-deployment-show" />
<section class="application-settings-workspace mt-4 flex min-h-0 w-full max-w-none flex-1 lg:mt-0">
<div class="grid min-h-0 min-w-0 flex-1 gap-8 xl:grid-cols-[210px_minmax(0,1fr)] xl:gap-8">
<x-application.configuration-sidebar :application="$application" :flush="true"
<x-application.configuration-sidebar :application="$application"
current-route="project.application.deployment.show" />
<div class="flex min-h-0 min-w-0 flex-col gap-4 xl:h-full xl:pt-px">
<livewire:project.application.deployment.index :embedded="true"
@@ -367,7 +367,7 @@
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]!">
@@ -51,7 +51,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" />
@@ -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">
@@ -5,13 +5,22 @@
@endphp
<x-application.settings-section id="preview-settings-section" title="Preview settings"
helper="Automatic pull request deployments and who can trigger them.">
<div class="grid w-full gap-4 sm:grid-cols-2">
<x-forms.listbox id="isPreviewDeploymentsEnabled" label="Preview deployments" onChange="savePreviewSettings"
helper="Automatically deploy Preview Deployments for all opened PRs.<br><br>Closing a PR deletes its Preview Deployment."
:options="[
['value' => false, 'label' => 'Disabled'],
['value' => true, 'label' => 'Deploy opened pull requests'],
]" :disabled="! $canUpdate" />
<x-slot:actions>
@can('update', $application)
@if ($isPreviewDeploymentsEnabled)
<x-forms.button wire:click="togglePreviewDeployments" wire:target="togglePreviewDeployments">
Disable preview deployments
</x-forms.button>
@else
<x-forms.button wire:click="togglePreviewDeployments" wire:target="togglePreviewDeployments"
isHighlighted>
Enable preview deployments
</x-forms.button>
@endif
@endcan
</x-slot:actions>
<div class="w-full">
<x-forms.listbox id="isPrDeploymentsPublicEnabled" label="PR deployment access" onChange="savePreviewSettings"
helper="When public, anyone can trigger PR deployments. Otherwise fork PRs are blocked and only repository owners, members, and collaborators can trigger them."
:options="[
@@ -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 @@
<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 @@
<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 @@
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" />
@@ -1,31 +1,21 @@
@if ($availableS3Storages->isEmpty())
<section class="application-settings-section">
<div class="application-settings-section-header">
<div>
<h2>S3 storage</h2>
<p>Send backup archives to a validated object storage destination.</p>
</div>
</div>
<div class="application-settings-section-body is-flush">
<x-empty title="No validated S3 storage"
description="Add and validate an S3 storage destination before enabling remote backups."
icon-name="storages">
<x-slot:contents>
<a class="button" {{ wireNavigate() }} href="{{ route('storage.index') }}">Open S3 storage</a>
</x-slot:contents>
</x-empty>
</div>
</section>
<x-application.settings-section title="S3 storage"
description="Send backup archives to a validated object storage destination." flush>
<x-empty title="No validated S3 storage"
description="Add and validate an S3 storage destination before enabling remote backups."
icon-name="storages">
<x-slot:contents>
<a class="button" {{ wireNavigate() }} href="{{ route('storage.index') }}">Open S3 storage</a>
</x-slot:contents>
</x-empty>
</x-application.settings-section>
@else
<form wire:submit="submit">
<x-unsaved-bar action="submit" />
<section class="application-settings-section">
<div class="application-settings-section-header">
<div>
<h2>S3 storage</h2>
<p>Choose where remote backups are stored and whether local copies are retained.</p>
</div>
<x-application.settings-section title="S3 storage"
description="Choose where remote backups are stored and whether local copies are retained.">
<x-slot:actions>
@if (! $saveS3)
<x-forms.button type="button" wire:click="toggleS3" wire:loading.attr="disabled"
wire:target="toggleS3" isHighlighted>Enable S3</x-forms.button>
@@ -33,8 +23,8 @@
<x-forms.button type="button" wire:click="toggleS3" wire:loading.attr="disabled"
wire:target="toggleS3">Disable S3</x-forms.button>
@endif
</div>
<div class="application-settings-section-body grid gap-4 sm:grid-cols-2">
</x-slot:actions>
<div class="grid gap-4 sm:grid-cols-2">
<x-forms.listbox id="s3StorageId" label="S3 storage" portal :required="$saveS3"
:options="$availableS3Storages->map(fn ($s3) => [
'value' => $s3->id,
@@ -46,6 +36,6 @@
['value' => true, 'label' => 'Delete after S3 upload'],
]" />
</div>
</section>
</x-application.settings-section>
</form>
@endif
@@ -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 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 @@
{{-- 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 @@
</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 @@
->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 />
@@ -56,7 +56,7 @@
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>
@@ -103,7 +103,7 @@
<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 @@
</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 @@
</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">
@@ -111,7 +111,7 @@
<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 @@
<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 @@
<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 @@
@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]">
@@ -166,9 +166,9 @@
</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 @@
<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 @@
</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 @@
</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>
@@ -144,10 +144,10 @@
<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>
@@ -158,17 +158,7 @@
icon-name="globe" />
</div>
@else
<div wire:key="service-domains-list"
class="application-settings-section-body is-flush mt-1 w-full scroll-mt-28 overflow-visible">
<div class="data-table-header service-domains-overview-grid">
<span>Domain</span>
<span>Protocol redirect</span>
<span>Domain redirect</span>
<span>Internal port</span>
<span>Search indexing</span>
<span>DNS status</span>
<span class="text-right">Actions</span>
</div>
<div wire:key="service-domains-list" class="flex flex-col gap-3">
@foreach ($domainGroups as $appId => $rows)
@php
$app = collect($serviceApps)->firstWhere('id', (int) $appId);
@@ -179,8 +169,8 @@
@endphp
<section id="service-domain-group-{{ $appId }}" wire:key="service-domain-group-{{ $appId }}"
x-show="matchesDomainSearch(@js($heading.' '.$rows->pluck('url')->implode(' ')))"
class="border-b border-neutral-200 last:border-b-0 dark:border-white/10">
<div class="flex w-full flex-wrap items-center gap-3 border-b border-neutral-200 bg-neutral-50 px-4 py-3 dark:border-white/10 dark:bg-white/[0.04]">
class="application-settings-section-body is-flush overflow-visible">
<div class="flex w-full flex-wrap items-center gap-3 rounded-t-lg border-b border-neutral-200 bg-neutral-50 px-4 py-3 dark:border-white/10 dark:bg-white/[0.04]">
<span class="min-w-0 flex-1 truncate text-sm font-medium text-black dark:text-white">{{ $heading }}</span>
@if ($hasHttpsDomains)
<div class="flex w-full items-center gap-2 sm:w-auto service-domains-https">
@@ -204,7 +194,7 @@
'domainRows' => $domainRows,
'service' => $service,
'showServiceColumn' => false,
'showHeader' => false,
'showHeader' => true,
])
</div>
</section>
@@ -25,7 +25,7 @@
</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 />
@@ -60,7 +60,7 @@
@php
$selectedSchedule = $selectedDatabaseBackup ?: $selectedVolumeBackup;
@endphp
<x-modal-input :title="'Edit backup schedule'" wireOpen="scheduleModalOpen" :wireIgnore="false" isLarge
<x-modal-input :title="'Edit backup schedule'" wireOpen="scheduleModalOpen" :wireIgnore="false" isLarge fixedHeight
canGate="update" :canResource="$service">
<x-slot:content><span></span></x-slot:content>
@@ -40,7 +40,7 @@
confirmationLabel="Enter the resource name to confirm permanent deletion"
shortConfirmationLabel="Resource name" />
@else
<x-forms.button disabled tooltip="You do not have permission to delete this resource.">
<x-forms.button isError disabled tooltip="You do not have permission to delete this resource.">
Delete {{ $resourceLabel }}
</x-forms.button>
@endif
@@ -36,7 +36,7 @@
<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') }}')">
@@ -89,7 +89,8 @@
@endif
{{-- Open modal immediately (Alpine); decrypt value in a follow-up Livewire request. --}}
<x-modal-input title="Edit environment variable" :closeOutside="false" :wireIgnore="false"
wireOpen="editorOpen">
wireOpen="editorOpen"
@environment-variable-updated.window="if ($event.detail.envId === @js($env->id)) modalOpen = false">
<x-slot:content>
<button type="button" wire:click="loadValues" data-env-settings-trigger class="icon-button shrink-0"
title="Edit environment variable" aria-label="Edit environment variable">
@@ -224,26 +225,32 @@
@if ($canUpdate || auth()->user()?->can('delete', $this->env))
<div
class="flex flex-wrap justify-end gap-2 border-t border-neutral-200 pt-4 dark:border-white/[0.07]">
@if ($canUpdate && !$isLocked && !$isMagicVariable)
<x-forms.button type="button" wire:click="lock">Lock</x-forms.button>
@endif
@can('delete', $this->env)
@if (!$isMagicVariable)
<x-modal-confirmation title="Confirm Environment Variable Deletion?" isErrorButton
buttonTitle="Delete" submitAction="delete"
:actions="['The selected environment variable will be permanently deleted.']"
confirmationText="{{ $key }}"
confirmationLabel="Please confirm the execution of the actions by entering the Environment Variable Name below"
shortConfirmationLabel="Environment Variable Name" :confirmWithPassword="false"
step2ButtonText="Permanently Delete" />
class="flex flex-wrap items-center justify-between gap-2 border-t border-neutral-200 pt-4 dark:border-white/[0.07]">
<div data-environment-variable-delete-action>
@can('delete', $this->env)
@if (!$isMagicVariable)
<x-modal-confirmation title="Confirm Environment Variable Deletion?" isErrorButton
buttonTitle="Delete" submitAction="delete"
:actions="['The selected environment variable will be permanently deleted.']"
confirmationText="{{ $key }}"
confirmationLabel="Please confirm the execution of the actions by entering the Environment Variable Name below"
shortConfirmationLabel="Environment Variable Name" :confirmWithPassword="false"
step2ButtonText="Permanently Delete" />
@endif
@endcan
</div>
<div class="ml-auto flex flex-wrap gap-2" data-environment-variable-update-actions>
@if ($canUpdate && !$isLocked && !$isMagicVariable)
<x-forms.button type="button" wire:click="lock">Lock</x-forms.button>
@endif
@endcan
@if ($canUpdate)
<x-forms.button type="submit" :disabled="$isDisabled" @click="modalOpen = false">
Update variable
</x-forms.button>
@endif
@if ($canUpdate && !$isDisabled)
<fieldset disabled wire:dirty.attr.remove="disabled">
<x-forms.button type="submit" isHighlighted wire:target="submit">
Update variable
</x-forms.button>
</fieldset>
@endif
</div>
</div>
@endif
</form>
@@ -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 @@
</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 @@
: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">
@@ -312,7 +312,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"
@@ -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 @@
@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
@@ -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>
@@ -1,31 +1,21 @@
@if ($availableS3Storages->isEmpty())
<section class="application-settings-section">
<div class="application-settings-section-header">
<div>
<h2>S3 storage</h2>
<p>Send volume backup archives to a validated object storage destination.</p>
</div>
</div>
<div class="application-settings-section-body is-flush">
<x-empty title="No validated S3 storage"
description="Add and validate an S3 storage destination before enabling remote backups."
icon-name="storages">
<x-slot:contents>
<a class="button" {{ wireNavigate() }} href="{{ route('storage.index') }}">Open S3 storage</a>
</x-slot:contents>
</x-empty>
</div>
</section>
<x-application.settings-section title="S3 storage"
description="Send volume backup archives to a validated object storage destination." flush>
<x-empty title="No validated S3 storage"
description="Add and validate an S3 storage destination before enabling remote backups."
icon-name="storages">
<x-slot:contents>
<a class="button" {{ wireNavigate() }} href="{{ route('storage.index') }}">Open S3 storage</a>
</x-slot:contents>
</x-empty>
</x-application.settings-section>
@else
<form wire:submit="save">
<x-unsaved-bar action="save" />
<section class="application-settings-section">
<div class="application-settings-section-header">
<div>
<h2>S3 storage</h2>
<p>Choose where remote copies are stored and whether local archives are retained.</p>
</div>
<x-application.settings-section title="S3 storage"
description="Choose where remote copies are stored and whether local archives are retained.">
<x-slot:actions>
@if (! $saveToS3)
<x-forms.button type="button" wire:click="toggleS3" wire:loading.attr="disabled"
wire:target="toggleS3" isHighlighted canGate="update" :canResource="$resource">
@@ -37,8 +27,8 @@
Disable S3
</x-forms.button>
@endif
</div>
<div class="application-settings-section-body grid gap-4 sm:grid-cols-2">
</x-slot:actions>
<div class="grid gap-4 sm:grid-cols-2">
<x-forms.listbox canGate="update" :canResource="$resource" id="s3StorageId" label="S3 storage" :required="$saveToS3"
:disabled="! auth()->user()?->can('update', $resource)"
:options="$availableS3Storages->map(fn ($s3Storage) => [
@@ -51,6 +41,6 @@
['value' => true, 'label' => 'Delete after S3 upload'],
]" />
</div>
</section>
</x-application.settings-section>
</form>
@endif
@@ -33,8 +33,7 @@
{{ $tag->name }}
</span>
@can('update', $resource)
<x-forms.button wire:click="deleteTag('{{ $tag->id }}')"
class="h-7! text-neutral-500 dark:text-fg-dim">
<x-forms.button isError wire:click="deleteTag('{{ $tag->id }}')" class="h-7!">
Remove
</x-forms.button>
@endcan
@@ -123,7 +123,7 @@
<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 @@
</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 @@
</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">
@@ -28,7 +28,7 @@
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>
@@ -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">
@@ -20,7 +20,7 @@
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>
@@ -68,7 +68,7 @@
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 @@
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">
@@ -279,10 +279,6 @@
@elseif ($server->isSentinelEnabled())
<x-application.settings-section id="server-metrics-overview-section" title="Metrics"
helper="Inspect recent CPU and memory usage reported by Sentinel.">
<x-slot:actions>
<x-status-badge status="Disabled" type="neutral" />
</x-slot:actions>
<x-empty size="sm" title="Metrics are disabled"
description="Enable metrics to begin collecting CPU and memory history for this server."
icon-name="dashboard">
@@ -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 @@
<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 @@
</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 @@
</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" />
@@ -134,7 +134,7 @@
<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 @@
</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 @@
</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>
@@ -146,7 +146,7 @@
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"
@@ -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>
@@ -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 @@
@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]))"
@@ -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 @@
</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 @@
</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 @@
</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">
@@ -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 @@
@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() }}
@@ -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 @@
@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() }}
@@ -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>
@@ -200,7 +200,7 @@
shortConfirmationLabel="GitHub App Name" :confirmWithPassword="false"
step2ButtonText="Permanently Delete" />
@else
<x-forms.button disabled tooltip="You do not have permission to delete this GitHub App.">
<x-forms.button isError disabled tooltip="You do not have permission to delete this GitHub App.">
Delete
</x-forms.button>
@endcan
@@ -300,7 +300,7 @@
@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>
@@ -36,7 +36,7 @@
<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)." />
@@ -17,7 +17,7 @@
</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>
@@ -124,7 +124,7 @@
</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 @@
<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)." />
@@ -36,7 +36,7 @@
<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)." />
@@ -48,7 +48,7 @@
<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 @@
</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>
@@ -17,7 +17,7 @@
<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>
@@ -102,7 +102,7 @@
shortConfirmationLabel="Storage Name" :confirmWithPassword="false"
step2ButtonText="Permanently Delete" />
@else
<x-forms.button disabled tooltip="You do not have permission to delete this storage.">
<x-forms.button isError disabled tooltip="You do not have permission to delete this storage.">
Delete
</x-forms.button>
@endcan
@@ -23,7 +23,7 @@
<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
@@ -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>
+11 -11
View File
@@ -96,7 +96,7 @@
<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 @@
</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 @@
</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 @@
<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 @@
</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 @@
</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 @@
<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 @@
@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 @@
</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>
@@ -88,7 +88,7 @@
step2ButtonText="Permanently Delete" canGate="delete"
:canResource="$team" />
@else
<x-forms.button disabled tooltip="Resolve the requirements shown before deleting this team.">
<x-forms.button isError disabled tooltip="Resolve the requirements shown before deleting this team.">
Delete team
</x-forms.button>
@endif
@@ -176,11 +176,11 @@
</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 @@
</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 @@
: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 @@
@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>
+4 -4
View File
@@ -75,7 +75,7 @@
<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 @@
</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 @@
@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';