feat(ui): WCAG contrast pass, neutral oklch surface system, and mobile nav overhaul (#11659)
This commit is contained in:
commit
caa07574b1
95 changed files with 1574 additions and 859 deletions
48
DESIGN.md
48
DESIGN.md
|
|
@ -34,7 +34,9 @@ ## 1. Visual direction
|
|||
|
||||
- near-neutral layered surfaces instead of large bordered boxes;
|
||||
- 13–14px UI typography and 32px controls;
|
||||
- hairline rings instead of heavy borders;
|
||||
- crisp hairline rings plus a restrained card lift (single 1px ring +
|
||||
`0 1px 2px rgb(0 0 0 / 0.05)`) so cards and tables separate from the canvas,
|
||||
never heavy borders or a strong floating shadow;
|
||||
- full-width data tables for dense collections;
|
||||
- outline Reicon glyphs through `<x-reicon>`;
|
||||
- the Coolify purple brand accent in light mode;
|
||||
|
|
@ -89,14 +91,14 @@ ## 3. Tokens and color behavior
|
|||
|
||||
| Token | Light | Dark | Use |
|
||||
|---|---|---|---|
|
||||
| `--coollabs-canvas` | near white | 10% neutral | page canvas |
|
||||
| `--coollabs-canvas` | 97% off-white | 10% neutral | page canvas (kept below card fills so cards lift) |
|
||||
| `--coollabs-elevated` | 98% neutral | 15% neutral | shells and card headers |
|
||||
| `--coollabs-base` | white | 17% neutral | nested card bodies |
|
||||
| `--coollabs-recessed` | 96% neutral | 20% neutral | inputs and listboxes |
|
||||
| `--coollabs-fill` | 92.2% neutral | 26.9% neutral | dividers and passive fills |
|
||||
| `--coollabs-line` | translucent dark | 32% neutral | control borders |
|
||||
| `--coollabs-hairline` | 93.5% neutral | 26.9% neutral | shell rings |
|
||||
| `--coollabs-subtle` | 55.6% neutral | 70.8% neutral | labels and muted titles |
|
||||
| `--coollabs-hairline` | 85.5% neutral | 32% neutral | shell rings (crisp enough to read as a card edge, ~1.5:1) |
|
||||
| `--coollabs-subtle` | 50% neutral | 70.8% neutral | labels and muted titles (light darkened for WCAG AA 4.5:1) |
|
||||
|
||||
Accent behavior is intentionally theme-aware:
|
||||
|
||||
|
|
@ -116,6 +118,38 @@ ## 3. Tokens and color behavior
|
|||
The filled top-level action/tab treatment uses the same palette at a restrained
|
||||
opacity rather than a fully saturated fill.
|
||||
|
||||
### Shell layering
|
||||
|
||||
The app shell is three distinct surface layers, not one flat color. Chrome
|
||||
lifts, content is the base, cards lift off the content:
|
||||
|
||||
- **Content canvas** is the base layer: `bg-app` in dark (deepest,
|
||||
`--color-app` `#0a0a0b`), `bg-gray-50` in light. The `<main>` content area
|
||||
and page body use it.
|
||||
- **Sidebar and topbar chrome** use `bg-panel` in dark (`--color-panel`
|
||||
`#141418`, a clear step lighter than the content canvas) and `bg-white` in
|
||||
light, so the chrome reads as a separate panel from the content.
|
||||
|
||||
Dark surface tokens are hex, not oklch. oklch lightness compresses toward pure
|
||||
black below ~15% (oklch(10%) renders as sRGB 3, oklch(15%) as sRGB 11), so oklch
|
||||
values there give no visible step between layers. The dark ladder is
|
||||
`--color-app` 10, `--coollabs-elevated` 22, `--coollabs-base` 28,
|
||||
`--coollabs-recessed` 34 (sRGB), which reads as distinct surfaces.
|
||||
|
||||
Temperature: every panel is **pure neutral gray** (r=g=b), one consistent
|
||||
temperature across the sidebar, tables, cards, inputs, dividers, borders, and
|
||||
text, in both modes. Do not give one surface a cool (blue) or warm cast while
|
||||
the others stay neutral. The light page canvas uses `bg-neutral-50` (not
|
||||
`bg-gray-50`, which is faintly cool) so it matches the neutral cards and chrome.
|
||||
The only intentional color is the purple/yellow brand accent.
|
||||
- **Cards, tables, and collection tiles** lift off the content canvas with
|
||||
`dark:bg-white/[0.05]` plus the crisp `--coollabs-hairline` ring; in light
|
||||
they are `bg-white` with the ring and the restrained card lift.
|
||||
|
||||
Do not paint the content area with the same `bg-panel` as the sidebar, and do
|
||||
not drop card fills below `dark:bg-white/[0.05]`; both make surfaces read as one
|
||||
color. Row-hover states keep the lighter `dark:hover:bg-white/[0.025]`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Page shells and navigation
|
||||
|
|
@ -419,6 +453,12 @@ ### Dropdowns
|
|||
the option content left-aligned and size the panel to its content or trigger;
|
||||
do not create an unnecessarily wide menu.
|
||||
|
||||
Every dropdown, menu, listbox panel, and the command palette uses the shared
|
||||
`--shadow-dropdown` token (`0 4px 12px rgb(0 0 0 / 0.12), 0 2px 4px
|
||||
rgb(0 0 0 / 0.08)`) for a restrained, consistent lift. Do not hand-roll a
|
||||
heavier `shadow-lg` / `0 18px 50px` / `0.45`-alpha drop shadow on a menu.
|
||||
Reserve the stronger `--shadow-modal` for actual modals, dialogs, and toasts.
|
||||
|
||||
Toolbar filter and sort buttons keep static labels (`Filter`, `Sort`). The
|
||||
selected option is indicated inside the menu, not repeated on the trigger.
|
||||
|
||||
|
|
|
|||
|
|
@ -20,51 +20,104 @@ @theme {
|
|||
--font-geist-sans: 'Geist Sans', Inter, sans-serif;
|
||||
--font-logs: 'Geist Mono', 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||
|
||||
--color-base: #101010;
|
||||
--color-warning: #fcd452;
|
||||
--color-warning-50: #fefce8;
|
||||
--color-warning-100: #fef9c3;
|
||||
--color-warning-200: #fef08a;
|
||||
--color-warning-300: #fde047;
|
||||
--color-warning-400: #fcd452;
|
||||
--color-warning-500: #facc15;
|
||||
--color-warning-600: #ca8a04;
|
||||
--color-warning-700: #a16207;
|
||||
--color-warning-800: #854d0e;
|
||||
--color-warning-900: #713f12;
|
||||
--color-success: #22C55E;
|
||||
--color-error: #dc2626;
|
||||
--color-coollabs-50: #f5f0ff;
|
||||
--color-coollabs: #6b16ed;
|
||||
--color-coollabs-100: #7317ff;
|
||||
--color-coollabs-200: #5a12c7;
|
||||
--color-coollabs-300: #4a0fa3;
|
||||
--color-coolgray-100: #181818;
|
||||
--color-coolgray-200: #202020;
|
||||
--color-coolgray-300: #242424;
|
||||
--color-coolgray-400: #282828;
|
||||
--color-coolgray-500: #323232;
|
||||
--color-base: oklch(17.3% 0 0);
|
||||
--color-warning: oklch(88.13% 0.1507 91.7);
|
||||
--color-warning-50: oklch(98.73% 0.0262 102.21);
|
||||
--color-warning-100: oklch(97.29% 0.0693 103.19);
|
||||
--color-warning-200: oklch(94.51% 0.1243 101.54);
|
||||
--color-warning-300: oklch(90.52% 0.1657 98.11);
|
||||
--color-warning-400: oklch(88.13% 0.1507 91.7);
|
||||
--color-warning-500: oklch(86.06% 0.1731 91.94);
|
||||
--color-warning-600: oklch(68.06% 0.1423 75.83);
|
||||
--color-warning-700: oklch(55.38% 0.1207 66.44);
|
||||
--color-warning-800: oklch(47.62% 0.1034 61.91);
|
||||
--color-warning-900: oklch(42.1% 0.0897 57.71);
|
||||
--color-success: oklch(72.27% 0.192 149.58);
|
||||
--color-error: oklch(57.71% 0.2152 27.33);
|
||||
--color-coollabs-50: oklch(96.33% 0.0206 301.15);
|
||||
--color-coollabs: oklch(49.65% 0.2709 289.33);
|
||||
--color-coollabs-100: oklch(52.34% 0.287 289.16);
|
||||
--color-coollabs-200: oklch(43.76% 0.2369 289.82);
|
||||
--color-coollabs-300: oklch(38.04% 0.2031 290.47);
|
||||
--color-coolgray-100: oklch(20.9% 0 0);
|
||||
--color-coolgray-200: oklch(24.35% 0 0);
|
||||
--color-coolgray-300: oklch(26.03% 0 0);
|
||||
--color-coolgray-400: oklch(27.68% 0 0);
|
||||
--color-coolgray-500: oklch(31.71% 0 0);
|
||||
|
||||
/* Graphite design language (ported from ref/frontend). Layered neutral
|
||||
surfaces + translucent hairlines. See DESIGN.md. */
|
||||
--color-app: #0c0c0d;
|
||||
/* Canvas: neutral near-black (oklch), not pure black */
|
||||
--color-panel: oklch(10% 0 0);
|
||||
--color-surface: #161618;
|
||||
--color-raised: #1c1c1e;
|
||||
--color-selected: #26262a;
|
||||
--color-fg: #f2f2f2;
|
||||
--color-fg-dim: #b4b4b8;
|
||||
--color-fg-faint: #6e6e74;
|
||||
--color-accent: #6b16ed;
|
||||
--color-accent-foreground: #ffffff;
|
||||
--color-hairline: rgba(255, 255, 255, 0.08);
|
||||
--color-nav-text: #525252;
|
||||
--color-nav-muted: #666666;
|
||||
--color-nav-active: #171717;
|
||||
--color-log: #0d0d0d;
|
||||
/* Content canvas: the darkest shell layer (sRGB ~10). Hex, not oklch:
|
||||
oklch lightness compresses to near-black below ~15%, so oklch(7.5%)
|
||||
renders as sRGB 1 with no visible step. */
|
||||
--color-app: oklch(14.48% 0 0);
|
||||
/* Sidebar + topbar chrome (sRGB ~20): a clear step lighter than the content
|
||||
canvas so the shell chrome separates from the content area. */
|
||||
--color-panel: oklch(19.13% 0 0);
|
||||
/* Pure neutral (r=g=b). Were cool-tinted (#161618/#1c1c1e/#26262a, b>r),
|
||||
which clashed with the neutral panel ladder. */
|
||||
--color-surface: oklch(20.02% 0 0);
|
||||
--color-raised: oklch(22.64% 0 0);
|
||||
--color-selected: oklch(26.86% 0 0);
|
||||
--color-fg: oklch(96.12% 0 0);
|
||||
--color-fg-dim: oklch(76.99% 0 0);
|
||||
/* Tertiary text. Raised from #6e6e74 (3.78:1 on base, failed WCAG AA)
|
||||
to #7e7e84 so 13-14px text clears 4.5:1 on the dark surface ladder. */
|
||||
--color-fg-faint: oklch(59.31% 0 0);
|
||||
--color-accent: oklch(49.65% 0.2709 289.33);
|
||||
--color-accent-foreground: oklch(100.0% 0 0);
|
||||
--color-hairline: oklch(100.0% 0 0 / 0.08);
|
||||
--color-nav-text: oklch(43.86% 0 0);
|
||||
--color-nav-muted: oklch(51.03% 0 0);
|
||||
--color-nav-active: oklch(20.46% 0 0);
|
||||
--color-log: oklch(15.91% 0 0);
|
||||
|
||||
--shadow-modal: 0 24px 64px rgba(0, 0, 0, 0.55), 0 4px 16px rgba(0, 0, 0, 0.4);
|
||||
/* One restrained lift shared by every dropdown/menu/listbox panel and the
|
||||
command palette. Lighter and consistent, replacing the old scattered
|
||||
0.45 / 0.35 / shadow-lg / shadow-sm mix. Modals keep --shadow-modal. */
|
||||
--shadow-dropdown: 0 4px 12px rgb(0 0 0 / 0.12), 0 2px 4px rgb(0 0 0 / 0.08);
|
||||
|
||||
/* Shared motion curves (stronger than the built-in CSS easings). Use these
|
||||
for consistency: --ease-out for UI enter/press, --ease-drawer for
|
||||
drawers/sheets, --ease-in-out for on-screen movement. */
|
||||
--ease-out: cubic-bezier(0.23, 1, 0.32, 1);
|
||||
--ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
|
||||
--ease-drawer: cubic-bezier(0.32, 0.72, 0, 1);
|
||||
}
|
||||
|
||||
/* Shared press feedback: every button-like control dips slightly on press and
|
||||
eases back, so pressable elements feel responsive and consistent app-wide. */
|
||||
.button,
|
||||
.icon-button,
|
||||
.app-tab,
|
||||
.split-action-main,
|
||||
.split-action-caret {
|
||||
transition-property: color, background-color, border-color, box-shadow, transform;
|
||||
transition-timing-function: var(--ease-out);
|
||||
transition-duration: 120ms;
|
||||
}
|
||||
|
||||
.button:not(:disabled):active,
|
||||
.icon-button:not(:disabled):active,
|
||||
.app-tab:active,
|
||||
.split-action-main:not(:disabled):active,
|
||||
.split-action-caret:not(:disabled):active {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
|
||||
/* Focus border/ring on any form control should ease, not snap. The accent
|
||||
focus indicator is a border-color and/or box-shadow (ring) change, which the
|
||||
utilities' `transition-colors` did not fully animate. Target the elements
|
||||
themselves (covers bespoke search fields too) plus the div-based controls. */
|
||||
input,
|
||||
textarea,
|
||||
select,
|
||||
.listbox-trigger,
|
||||
.chip-input {
|
||||
transition-property: color, background-color, border-color, box-shadow;
|
||||
transition-timing-function: var(--ease-out);
|
||||
transition-duration: 120ms;
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
@ -258,7 +311,7 @@ @layer components {
|
|||
*/
|
||||
html,
|
||||
body {
|
||||
@apply w-full min-h-full bg-gray-50 dark:bg-app dark:text-fg-dim;
|
||||
@apply w-full min-h-full bg-neutral-50 dark:bg-app dark:text-fg-dim;
|
||||
}
|
||||
|
||||
body {
|
||||
|
|
@ -505,25 +558,24 @@ .terminal-fullscreen-shell::before {
|
|||
|
||||
.application-console-shell[data-console-theme="system"],
|
||||
.terminal-fullscreen-shell[data-console-theme="system"] {
|
||||
--console-theme-background: #fff;
|
||||
--console-theme-border: #d4d4d8;
|
||||
--console-theme-background: oklch(100.0% 0 0);
|
||||
--console-theme-border: oklch(87.11% 0.0055 286.29);
|
||||
--console-theme-opacity: 1;
|
||||
}
|
||||
|
||||
html:not(.dark) .application-console-shell[data-console-theme="system"] .application-console-header {
|
||||
background: transparent;
|
||||
color: #52525b;
|
||||
border-color: rgb(0 0 0 / 0.1);
|
||||
backdrop-filter: none;
|
||||
}
|
||||
|
||||
html:not(.dark) .application-console-shell[data-console-theme="system"] .application-console-header [class*="text-white"] {
|
||||
/* Session toolbar (selected-target trigger + label) sits over the white
|
||||
system console in light mode. Without this its text-white/* stayed white on
|
||||
white and the selected server was invisible (issue #11532). */
|
||||
html:not(.dark) .application-console-shell[data-console-theme="system"] .terminal-session-toolbar[class*="text-white"],
|
||||
html:not(.dark) .application-console-shell[data-console-theme="system"] .terminal-session-toolbar [class*="text-white"],
|
||||
html:not(.dark) .terminal-fullscreen-shell[data-console-theme="system"] .terminal-session-toolbar[class*="text-white"],
|
||||
html:not(.dark) .terminal-fullscreen-shell[data-console-theme="system"] .terminal-session-toolbar [class*="text-white"] {
|
||||
color: #52525b !important;
|
||||
}
|
||||
|
||||
html.dark .application-console-shell[data-console-theme="system"],
|
||||
html.dark .terminal-fullscreen-shell[data-console-theme="system"] {
|
||||
--console-theme-background: #121214;
|
||||
--console-theme-background: oklch(18.31% 0.004 285.99);
|
||||
--console-theme-border: rgb(255 255 255 / 0.08);
|
||||
}
|
||||
|
||||
|
|
@ -533,6 +585,19 @@ html[data-theme="custom"] .terminal-fullscreen-shell[data-console-theme="system"
|
|||
--console-theme-border: var(--coollabs-line);
|
||||
}
|
||||
|
||||
/* Docked System console only: the fill behind the terminal panel is the
|
||||
elevated (header) surface, so the panel's rounded-corner notch reads as the
|
||||
header colour — the card's figure/ground. The panel paints its own base
|
||||
surface over this fill, so only the notch shows it. The fullscreen shell has
|
||||
no panel, so it is intentionally excluded and keeps its plain terminal fill. */
|
||||
.application-console-shell[data-console-theme="system"] {
|
||||
--console-theme-background: var(--coollabs-elevated);
|
||||
}
|
||||
|
||||
html.dark .application-console-shell[data-console-theme="system"] {
|
||||
--console-theme-background: var(--coollabs-elevated);
|
||||
}
|
||||
|
||||
.console-theme-selector {
|
||||
scrollbar-color: rgb(161 161 170) transparent;
|
||||
scrollbar-width: thin;
|
||||
|
|
@ -656,6 +721,10 @@ .terminal-page-console {
|
|||
flex: 1 1 0%;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
/* Breathing room so the console's box-shadow ring + lift are not clipped by
|
||||
this overflow-hidden, viewport-height container (the card gets this from
|
||||
the scrollable page around it). */
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.terminal-page-console > .application-console-shell {
|
||||
|
|
@ -712,11 +781,71 @@ body.terminal-is-fullscreen .terminal-fullscreen-shell [data-terminal-mobile-too
|
|||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
.application-console-header {
|
||||
/* Session header band: an attached card-style header on top of the console,
|
||||
mirroring the page card header (elevated surface + hairline divider). The
|
||||
System theme uses the neutral card surface in both modes; colorful themes
|
||||
fall back to a translucent dark band so the white controls stay legible. */
|
||||
.terminal-session-toolbar {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
background: rgb(0 0 0 / 0.24);
|
||||
backdrop-filter: blur(20px);
|
||||
flex-shrink: 0;
|
||||
min-height: 3rem;
|
||||
padding: 0.5rem 0.75rem 0.5rem 1rem;
|
||||
/* No border-radius: the shell is overflow-hidden + rounded and clips the
|
||||
header's top corners, so rounding here only risks a sub-pixel seam. */
|
||||
border-bottom: 1px solid var(--terminal-header-border, rgb(255 255 255 / 0.1));
|
||||
background: var(--terminal-header-bg, rgb(0 0 0 / 0.28));
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
/* System header has no bottom border: the body panel's ring is the divider, and
|
||||
its rounded top corners tuck under the header — the same merge the
|
||||
target-picker card uses (elevated header meeting a base body panel). */
|
||||
.application-console-shell[data-console-theme="system"] .terminal-session-toolbar {
|
||||
--terminal-header-bg: var(--coollabs-elevated);
|
||||
--terminal-header-border: transparent;
|
||||
/* Shell no longer clips (overflow: visible), so the header rounds its own
|
||||
top corners — matching the card header. */
|
||||
border-radius: 8px 8px 0 0;
|
||||
/* Drop the z-index:2 stacking context: it painted the header OVER the body
|
||||
panel, hiding the panel's ring (the divider). The header only had z-2 to
|
||||
beat ::before, but its dropdowns sit in their own .relative wrappers, so
|
||||
z:auto is safe and lets the body panel's ring paint over the header —
|
||||
exactly how the card body overlaps the card header. */
|
||||
z-index: auto;
|
||||
-webkit-backdrop-filter: none;
|
||||
backdrop-filter: none;
|
||||
}
|
||||
|
||||
/* System console body: the terminal panel, a full rounded box-shadow ring like
|
||||
the card body. Its base surface is the terminal fill; the elevated surface
|
||||
behind it (::before) shows only through the rounded-corner notch, reading as
|
||||
the header colour — the card's figure/ground. */
|
||||
.application-console-shell[data-console-theme="system"] .terminal-session-panel {
|
||||
background: var(--coollabs-base);
|
||||
/* The body panel is a full rounded ring (like the card body). position:
|
||||
relative puts it (and its ring) above ::before AND, because it comes
|
||||
after the now-z:auto header in the DOM, above the header — so the ring's
|
||||
top edge is the divider and its rounded top corners connect down into the
|
||||
side ring, which coincides with the shell's ring (one hairline, no
|
||||
doubling). A box-shadow ring rounds and connects cleanly; a border-top
|
||||
cannot (square corners, or a disconnected transparent edge). */
|
||||
position: relative;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0 0 1px var(--coollabs-hairline);
|
||||
}
|
||||
|
||||
/* Connected System console frame = the target-picker card recipe: a hairline
|
||||
box-shadow ring + a restrained lift. overflow stays visible so the ring is
|
||||
not clipped (the .terminal-page-console padding gives it room inside the
|
||||
viewport-locked, overflow-hidden page). Colorful themes keep the themed,
|
||||
overflow-hidden shell. */
|
||||
.application-console-shell[data-console-theme="system"] {
|
||||
overflow: visible;
|
||||
box-shadow:
|
||||
0 0 0 1px var(--coollabs-hairline),
|
||||
0 1px 2px 0 rgb(0 0 0 / 0.05);
|
||||
}
|
||||
|
||||
.terminal-loading-label {
|
||||
|
|
@ -824,7 +953,7 @@ .terminal-session-panel {
|
|||
}
|
||||
|
||||
html:not(.dark) .application-console-shell[data-console-theme="system"] .terminal-session-panel {
|
||||
background: transparent;
|
||||
background: var(--coollabs-base);
|
||||
}
|
||||
|
||||
.terminal-target-list {
|
||||
|
|
@ -851,7 +980,7 @@ html:not(.dark) .application-console-shell[data-console-theme="system"] .termina
|
|||
color: #52525b;
|
||||
background: #fff;
|
||||
border-color: #d4d4d8;
|
||||
box-shadow: 0 18px 50px rgb(0 0 0 / 0.18);
|
||||
box-shadow: var(--shadow-dropdown);
|
||||
}
|
||||
|
||||
html:not(.dark) .application-console-shell[data-console-theme="system"] .terminal-target-picker [class*="text-white"] {
|
||||
|
|
@ -924,75 +1053,166 @@ .terminal-fullscreen-btn:hover {
|
|||
* utility classes placed in the card markup.
|
||||
*/
|
||||
:root {
|
||||
--coollabs-canvas: oklch(98.75% 0 0);
|
||||
/* Page canvas is a faint off-white so white/elevated cards lift off it
|
||||
(shadcn pattern), instead of card and page reading as one flat color. */
|
||||
--coollabs-canvas: oklch(97% 0 0);
|
||||
--coollabs-elevated: oklch(98% 0 0);
|
||||
--coollabs-recessed: oklch(96% 0 0);
|
||||
--coollabs-base: #ffffff;
|
||||
--coollabs-base: oklch(100.0% 0 0);
|
||||
--coollabs-fill: oklch(92.2% 0 0);
|
||||
--coollabs-line: oklch(14.5% 0 0 / 0.1);
|
||||
--coollabs-hairline: oklch(93.5% 0 0);
|
||||
--coollabs-subtle: oklch(55.6% 0 0);
|
||||
/* Card/table edge ring. Crisped from 93.5% (1.17:1 on canvas, invisible)
|
||||
to 85.5% (1.51:1) so surfaces read as bordered cards, not flat fills. */
|
||||
--coollabs-hairline: oklch(85.5% 0 0);
|
||||
/* Muted labels/titles. Darkened from 55.6% (failed WCAG AA on elevated
|
||||
4.46, recessed 4.24, fill 3.76) to 50% so muted text clears 4.5:1 on
|
||||
every light surface above white. Dark stays 70.8% (already passes). */
|
||||
--coollabs-subtle: oklch(50% 0 0);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--color-accent: #fcd452;
|
||||
--coollabs-canvas: oklch(10% 0 0);
|
||||
/* elevated (card shells/headers) and recessed (input fills) sit a touch
|
||||
above the canvas so they read lighter than near-black */
|
||||
--coollabs-elevated: oklch(15% 0 0);
|
||||
--coollabs-recessed: oklch(20% 0 0);
|
||||
--coollabs-base: oklch(17% 0 0);
|
||||
--coollabs-fill: oklch(26.9% 0 0);
|
||||
--color-accent: oklch(88.13% 0.1507 91.7);
|
||||
/* Dark surface ladder in hex (sRGB), not oklch: oklch lightness compresses
|
||||
to near-black below ~15%, so oklch(15%) rendered as sRGB 11 and cards
|
||||
could not lift off the content canvas (sRGB 10). These give clear,
|
||||
visible steps: content 10 -> elevated 22 -> base 28 -> recessed 34.
|
||||
Pure neutral (r=g=b), matching the neutral borders/text and the light
|
||||
ladder, so every panel shares one temperature (no blue cast). */
|
||||
--coollabs-canvas: oklch(14.48% 0 0);
|
||||
--coollabs-elevated: oklch(20.02% 0 0);
|
||||
--coollabs-recessed: oklch(25.2% 0 0);
|
||||
--coollabs-base: oklch(22.64% 0 0);
|
||||
--coollabs-fill: oklch(29.31% 0 0);
|
||||
--coollabs-line: oklch(32% 0 0);
|
||||
--coollabs-hairline: oklch(26.9% 0 0);
|
||||
/* Crisped from 26.9% (1.36:1 on canvas) to 32% (1.63:1) so dark cards
|
||||
and tables show a visible edge instead of blending into the canvas. */
|
||||
--coollabs-hairline: oklch(32% 0 0);
|
||||
--coollabs-subtle: oklch(70.8% 0 0);
|
||||
--color-nav-text: #a8a8b0;
|
||||
--color-nav-muted: #7a7a84;
|
||||
--color-nav-active: #f2f2f2;
|
||||
/* Neutral (was #a8a8b0 / #7a7a84, both cool-tinted). Sidebar text now shares
|
||||
the same neutral temperature as the content text, so the sidebar no longer
|
||||
reads cooler than the panels/cards. */
|
||||
--color-nav-text: oklch(73.8% 0 0);
|
||||
/* Raised from 58.97% (4.47:1 on panel, just under AA) to 61% (4.86:1) so the
|
||||
11px sidebar section labels clear 4.5:1. */
|
||||
--color-nav-muted: oklch(61% 0 0);
|
||||
--color-nav-active: oklch(96.12% 0 0);
|
||||
/* Tertiary text. Base token (59.31%) only clears 4.5:1 on app/panel; it
|
||||
failed on surface/raised/selected (4.46/4.20/3.73). Raised to 64% so the
|
||||
313 dark:text-fg-faint usages clear AA across the whole surface ladder,
|
||||
while staying clearly dimmer than fg-dim (76.99%). */
|
||||
--color-fg-faint: oklch(65% 0 0);
|
||||
/* Error text. The base error red (57.71%) failed on every dark surface
|
||||
(4.10/3.75/3.53). Brightened for dark mode so text-error clears AA on the
|
||||
card ladder while staying unmistakably red. */
|
||||
--color-error: oklch(66% 0.2 27.33);
|
||||
}
|
||||
|
||||
/* Light-mode semantic text.
|
||||
warning / success / error double as the bright brand accent and as solid
|
||||
fills, which only appear under `dark:` — so in light mode these tokens are
|
||||
used almost entirely as text, translucent tints (bg-warning/10) and borders.
|
||||
The bright values (yellow 88%, green 72%) and the base red (57.71%) fail AA
|
||||
as text on white and the light surface ladder (warning 1.43, success 2.28,
|
||||
error 4.43 on canvas). Scoped to `:not(.dark)` so dark keeps the brand
|
||||
colors and the custom theme keeps its own overrides. Every value clears
|
||||
4.5:1 on white, canvas, elevated, recessed and fill, and is in sRGB gamut. */
|
||||
html:not(.dark) {
|
||||
--color-warning: oklch(50% 0.105 80);
|
||||
--color-success: oklch(48% 0.135 149.58);
|
||||
--color-error: oklch(52% 0.2 27.33);
|
||||
}
|
||||
|
||||
/* Theme surfaces are derived from one brand color. Changing
|
||||
--theme-base-color is enough to generate a complete dark surface ladder. */
|
||||
html[data-theme="custom"] {
|
||||
--theme-base-color: #6b16ed;
|
||||
--theme-bright-color: color-mix(in srgb, var(--theme-base-color) 85%, white);
|
||||
--theme-base-color: oklch(49.65% 0.2709 289.33);
|
||||
--theme-bright-color: color-mix(in srgb, var(--theme-base-color) 85%, oklch(100% 0 0));
|
||||
--theme-scrollbar-thumb: color-mix(in srgb, var(--theme-bright-color) 70%, var(--theme-accent-foreground));
|
||||
--theme-border-color: color-mix(in oklab, var(--theme-base-color) 42%, #52525b);
|
||||
--theme-placeholder-color: color-mix(in srgb, white 20%, var(--theme-base-color));
|
||||
--theme-border-color: color-mix(in oklab, var(--theme-base-color) 42%, oklch(44.19% 0.0146 285.79));
|
||||
/* Accent text/icons sit directly on dark surfaces. Force the lightness into a
|
||||
readable band (and tame chroma) so any picked hue keeps enough contrast. */
|
||||
--theme-fg-on-surface: oklch(from var(--theme-base-color) clamp(0.72, l, 0.86) min(c, 0.13) h);
|
||||
/* Placeholders read as a light, near-neutral hint of the hue - never the raw
|
||||
(possibly dark) base color, which is invisible on the dark input. */
|
||||
--theme-placeholder-color: oklch(from var(--theme-base-color) 0.62 min(c, 0.03) h);
|
||||
--color-accent: var(--theme-bright-color);
|
||||
--color-coollabs: var(--theme-bright-color);
|
||||
--color-coollabs-100: color-mix(in oklab, var(--theme-bright-color) 88%, white);
|
||||
--color-coollabs-200: color-mix(in oklab, var(--theme-bright-color) 88%, black);
|
||||
--color-coollabs-300: color-mix(in oklab, var(--theme-bright-color) 72%, black);
|
||||
--color-coollabs-100: color-mix(in oklab, var(--theme-bright-color) 88%, oklch(100% 0 0));
|
||||
--color-coollabs-200: color-mix(in oklab, var(--theme-bright-color) 88%, oklch(0% 0 0));
|
||||
--color-coollabs-300: color-mix(in oklab, var(--theme-bright-color) 72%, oklch(0% 0 0));
|
||||
/* Legacy dark-theme accent utilities use warning as the brand color. */
|
||||
--color-warning: var(--theme-bright-color);
|
||||
--color-accent-foreground: var(--theme-accent-foreground);
|
||||
--color-app: color-mix(in oklab, var(--theme-base-color) 12%, #09090a);
|
||||
--color-panel: color-mix(in oklab, var(--theme-base-color) 14%, #0c0c0d);
|
||||
--color-surface: color-mix(in oklab, var(--theme-base-color) 18%, #101011);
|
||||
--color-raised: color-mix(in oklab, var(--theme-base-color) 24%, #141416);
|
||||
--color-selected: color-mix(in oklab, var(--theme-base-color) 32%, #18181a);
|
||||
--color-app: color-mix(in oklab, var(--theme-base-color) 12%, oklch(14.03% 0.0022 286.11));
|
||||
--color-panel: color-mix(in oklab, var(--theme-base-color) 14%, oklch(15.48% 0.0021 286.15));
|
||||
--color-surface: color-mix(in oklab, var(--theme-base-color) 18%, oklch(17.35% 0.0020 286.18));
|
||||
--color-raised: color-mix(in oklab, var(--theme-base-color) 24%, oklch(19.21% 0.0040 286.02));
|
||||
--color-selected: color-mix(in oklab, var(--theme-base-color) 32%, oklch(20.99% 0.0039 286.06));
|
||||
--color-coolgray-100: var(--color-surface);
|
||||
--color-coolgray-200: var(--color-raised);
|
||||
--color-coolgray-300: var(--color-selected);
|
||||
--color-coolgray-400: color-mix(in oklab, var(--theme-base-color) 28%, #1b1b1e);
|
||||
--color-coolgray-500: color-mix(in oklab, var(--theme-base-color) 32%, #202024);
|
||||
--color-coolgray-400: color-mix(in oklab, var(--theme-base-color) 28%, oklch(22.34% 0.0058 285.92));
|
||||
--color-coolgray-500: color-mix(in oklab, var(--theme-base-color) 32%, oklch(24.52% 0.0075 285.83));
|
||||
--coollabs-canvas: var(--color-app);
|
||||
--coollabs-elevated: var(--color-surface);
|
||||
--coollabs-recessed: var(--color-raised);
|
||||
--coollabs-base: color-mix(in oklab, var(--theme-base-color) 22%, #111112);
|
||||
--coollabs-base: color-mix(in oklab, var(--theme-base-color) 22%, oklch(17.81% 0.0020 286.19));
|
||||
--color-content-surface: var(--coollabs-base);
|
||||
--coollabs-fill: color-mix(in oklab, var(--theme-base-color) 32%, #18181a);
|
||||
--coollabs-fill: color-mix(in oklab, var(--theme-base-color) 32%, oklch(20.99% 0.0039 286.06));
|
||||
--coollabs-line: var(--theme-border-color);
|
||||
--coollabs-hairline: color-mix(in srgb, var(--theme-border-color) 55%, var(--color-panel));
|
||||
--color-nav-text: #e4dfea;
|
||||
--color-nav-muted: #d8d2df;
|
||||
--color-nav-active: #ffffff;
|
||||
--color-log: color-mix(in oklab, var(--theme-base-color) 20%, #080809);
|
||||
--color-log-toolbar: color-mix(in oklab, var(--theme-base-color) 26%, #101011);
|
||||
--color-fg-dim: #e4dfea;
|
||||
--color-fg-faint: #d8d2df;
|
||||
--coollabs-subtle: #d8d2df;
|
||||
--color-nav-text: oklch(91.08% 0.0157 306.4);
|
||||
--color-nav-muted: oklch(87.24% 0.0188 306.63);
|
||||
--color-nav-active: oklch(100.0% 0 0);
|
||||
--color-log: color-mix(in oklab, var(--theme-base-color) 20%, oklch(13.49% 0.0024 286.07));
|
||||
--color-log-toolbar: color-mix(in oklab, var(--theme-base-color) 26%, oklch(17.35% 0.0020 286.18));
|
||||
--color-fg-dim: oklch(91.08% 0.0157 306.4);
|
||||
--color-fg-faint: oklch(87.24% 0.0188 306.63);
|
||||
--coollabs-subtle: oklch(87.24% 0.0188 306.63);
|
||||
}
|
||||
|
||||
/* Light custom theme: the same single brand color, but surfaces tint toward
|
||||
white and every foreground token flips to a dark, readable band. This selector
|
||||
outranks the plain html:not(.dark) light palette, so it wins where they meet.
|
||||
Accent fills (--theme-bright-color, --color-warning/coollabs) are inherited from
|
||||
the block above so the JS-computed --theme-accent-foreground stays consistent. */
|
||||
html[data-theme="custom"]:not(.dark) {
|
||||
--theme-scrollbar-thumb: color-mix(in srgb, var(--theme-base-color) 45%, oklch(60% 0 0));
|
||||
/* Accent text/icons on light surfaces: force lightness into a dark band. */
|
||||
--theme-fg-on-surface: oklch(from var(--theme-base-color) clamp(0.30, l, 0.48) min(c, 0.16) h);
|
||||
/* Placeholder: medium-dark, near-neutral hint of the hue. */
|
||||
--theme-placeholder-color: oklch(from var(--theme-base-color) 0.50 min(c, 0.03) h);
|
||||
/* Surfaces hold a fixed high lightness (stays light, keeps dark text readable)
|
||||
while injecting the base hue's chroma, so the tint is actually visible.
|
||||
color-mix into white coupled lightness to the base and washed the tint out
|
||||
(a 4% mix of any color into near-white is imperceptible). */
|
||||
--color-app: oklch(from var(--theme-base-color) 96.5% min(c, 0.035) h);
|
||||
--color-panel: oklch(from var(--theme-base-color) 98.5% min(c, 0.025) h);
|
||||
--color-surface: oklch(from var(--theme-base-color) 98% min(c, 0.03) h);
|
||||
--color-raised: oklch(from var(--theme-base-color) 95.5% min(c, 0.04) h);
|
||||
--color-selected: oklch(from var(--theme-base-color) 92% min(c, 0.05) h);
|
||||
--color-coolgray-100: var(--color-surface);
|
||||
--color-coolgray-200: var(--color-raised);
|
||||
--color-coolgray-300: var(--color-selected);
|
||||
--color-coolgray-400: oklch(from var(--theme-base-color) 90% min(c, 0.05) h);
|
||||
--color-coolgray-500: oklch(from var(--theme-base-color) 87% min(c, 0.055) h);
|
||||
--coollabs-canvas: var(--color-app);
|
||||
--coollabs-elevated: oklch(from var(--theme-base-color) 98.5% min(c, 0.025) h);
|
||||
--coollabs-recessed: var(--color-raised);
|
||||
--coollabs-base: oklch(from var(--theme-base-color) 99.3% min(c, 0.018) h);
|
||||
--color-content-surface: var(--coollabs-base);
|
||||
--coollabs-fill: oklch(from var(--theme-base-color) 91% min(c, 0.05) h);
|
||||
--coollabs-line: oklch(from var(--theme-base-color) 82% min(c, 0.055) h);
|
||||
--coollabs-hairline: oklch(from var(--theme-base-color) 88% min(c, 0.04) h);
|
||||
--color-nav-text: oklch(from var(--theme-base-color) 0.40 min(c, 0.04) h);
|
||||
--color-nav-muted: oklch(from var(--theme-base-color) 0.50 min(c, 0.03) h);
|
||||
--color-nav-active: oklch(from var(--theme-base-color) 0.22 min(c, 0.05) h);
|
||||
--color-log: oklch(from var(--theme-base-color) 96.5% min(c, 0.035) h);
|
||||
--color-log-toolbar: oklch(from var(--theme-base-color) 93% min(c, 0.045) h);
|
||||
--color-fg: oklch(from var(--theme-base-color) 0.22 min(c, 0.04) h);
|
||||
--color-fg-dim: oklch(from var(--theme-base-color) 0.40 min(c, 0.04) h);
|
||||
--color-fg-faint: oklch(from var(--theme-base-color) 0.50 min(c, 0.03) h);
|
||||
--coollabs-subtle: oklch(from var(--theme-base-color) 0.50 min(c, 0.03) h);
|
||||
}
|
||||
|
||||
html[data-theme="custom"] .control-selected,
|
||||
|
|
@ -1051,7 +1271,55 @@ .dark [class~="dark:border-white/[0.06]"] {
|
|||
html[data-theme="custom"] input::placeholder,
|
||||
html[data-theme="custom"] textarea::placeholder {
|
||||
color: var(--theme-placeholder-color) !important;
|
||||
opacity: 0.7;
|
||||
/* The token is already a readable light tone; no extra opacity dampening,
|
||||
which would drop it back below the contrast floor. */
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Accent text/icons (links, badges, "update available" pill, check marks) use the
|
||||
brand color as foreground on dark surfaces. Route them to the clamped, always
|
||||
readable token; fills (bg-*) keep the brighter accent + its computed foreground. */
|
||||
html[data-theme="custom"] :is(
|
||||
[class~="text-coollabs"],
|
||||
[class~="text-warning"],
|
||||
[class~="text-accent"],
|
||||
[class~="dark:text-coollabs"],
|
||||
[class~="dark:text-warning"]
|
||||
) {
|
||||
color: var(--theme-fg-on-surface) !important;
|
||||
}
|
||||
|
||||
html[data-theme="custom"] [class~="hover:text-coollabs"]:hover,
|
||||
html[data-theme="custom"] [class~="hover:text-warning"]:hover,
|
||||
html[data-theme="custom"] [class~="dark:hover:text-warning"]:hover,
|
||||
html[data-theme="custom"] [class~="dark:hover:text-coollabs"]:hover {
|
||||
color: var(--theme-fg-on-surface) !important;
|
||||
}
|
||||
|
||||
/* Light custom theme reaches raw neutral utilities too. Dark mode tints
|
||||
everything through the pervasive dark:bg-*/dark:border-* utilities (which
|
||||
resolve to --color-* vars the block overrides); light mode has no equivalent,
|
||||
so components fall back to hardcoded neutrals. Remap the dominant light
|
||||
surface/border utilities to the tinted custom vars so the color reaches the
|
||||
whole UI, not just design-system classes. */
|
||||
html[data-theme="custom"]:not(.dark) :is([class~="bg-white"], [class~="bg-white/95"], [class~="bg-white/80"]) {
|
||||
background-color: var(--coollabs-elevated) !important;
|
||||
}
|
||||
|
||||
html[data-theme="custom"]:not(.dark) [class~="bg-neutral-50"] {
|
||||
background-color: var(--coollabs-canvas) !important;
|
||||
}
|
||||
|
||||
html[data-theme="custom"]:not(.dark) :is([class~="bg-neutral-100"], [class~="bg-neutral-200"]) {
|
||||
background-color: var(--coollabs-fill) !important;
|
||||
}
|
||||
|
||||
html[data-theme="custom"]:not(.dark) [class~="border-neutral-200"] {
|
||||
border-color: var(--coollabs-hairline) !important;
|
||||
}
|
||||
|
||||
html[data-theme="custom"]:not(.dark) :is([class~="border-neutral-300"], [class~="border-neutral-100"]) {
|
||||
border-color: var(--coollabs-line) !important;
|
||||
}
|
||||
|
||||
html[data-theme="custom"] input:read-only,
|
||||
|
|
@ -1318,27 +1586,6 @@ .error-actions {
|
|||
margin-top: 1.75rem;
|
||||
}
|
||||
|
||||
.error-contact-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
margin-left: 0.25rem;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
color: #525252;
|
||||
text-underline-offset: 3px;
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
|
||||
.dark .error-contact-link {
|
||||
color: var(--color-fg-dim);
|
||||
}
|
||||
|
||||
.error-contact-link:hover {
|
||||
color: var(--color-accent);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Layer card root: elevated shell with a hairline ring. overflow stays
|
||||
visible so floating panels (listbox, tooltips) can escape the card. */
|
||||
.application-settings-section {
|
||||
|
|
@ -1349,7 +1596,9 @@ .application-settings-section {
|
|||
border-radius: 8px;
|
||||
border: none;
|
||||
background: var(--coollabs-elevated);
|
||||
box-shadow: 0 0 0 1px var(--coollabs-hairline);
|
||||
/* crisp ring + a restrained lift so the card separates from the canvas
|
||||
(shadcn-style), not a heavy floating shadow */
|
||||
box-shadow: 0 0 0 1px var(--coollabs-hairline), 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
||||
/* keep anchored scrolls (scrollIntoView) clear of the fixed topbar + layer-2 nav */
|
||||
scroll-margin-top: 7rem;
|
||||
}
|
||||
|
|
@ -1415,7 +1664,14 @@ .application-settings-section > :is(header, .application-settings-section-header
|
|||
font-weight: 500;
|
||||
line-height: 1.25rem;
|
||||
letter-spacing: 0;
|
||||
color: inherit;
|
||||
/* Card title is a heading, so give it a readable strength instead of the
|
||||
muted --coollabs-subtle (5.66:1 light / 6.98:1 dark). ~8.7:1 in both
|
||||
modes (comfortable AAA) while descriptions/labels stay muted. */
|
||||
color: oklch(40% 0 0);
|
||||
}
|
||||
|
||||
.dark .application-settings-section > :is(header, .application-settings-section-header) :is(h2, h3) {
|
||||
color: var(--color-fg-dim);
|
||||
}
|
||||
|
||||
.application-settings-section > .application-settings-section-header p {
|
||||
|
|
@ -1425,12 +1681,16 @@ .application-settings-section > .application-settings-section-header p {
|
|||
color: var(--coollabs-subtle);
|
||||
}
|
||||
|
||||
/* Layer card body: nested rounded panel with base bg + fill ring */
|
||||
/* Layer card body: nested rounded panel with base bg. Ring uses the same
|
||||
--coollabs-hairline as the card shell (line ~1456) so the body edge and the
|
||||
header edge read as one continuous border instead of two different grays
|
||||
(fill was lighter than hairline, so the header sides and body sides did not
|
||||
match). */
|
||||
.application-settings-section-body {
|
||||
position: relative;
|
||||
border-radius: 8px;
|
||||
background: var(--coollabs-base);
|
||||
box-shadow: 0 0 0 1px var(--coollabs-fill);
|
||||
box-shadow: 0 0 0 1px var(--coollabs-hairline);
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
|
|
@ -1469,28 +1729,71 @@ .server-settings-workspace > :not(.application-settings-navigation) {
|
|||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
/* Fixed like the main sidebar (3rem → bottom); left tracks its width (--sidebar-w).
|
||||
Fixed leaves the grid, so the content sibling is pinned to column 2 below. */
|
||||
.application-settings-navigation {
|
||||
position: sticky;
|
||||
top: calc(3rem + 1.75rem);
|
||||
align-self: start;
|
||||
max-height: calc(100dvh - 5.5rem);
|
||||
padding-right: 0.375rem;
|
||||
position: fixed;
|
||||
top: 3rem;
|
||||
left: var(--sidebar-w, 14rem);
|
||||
width: calc(210px + 2.5rem);
|
||||
height: calc(100dvh - 3rem);
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--coollabs-fill) transparent;
|
||||
scrollbar-gutter: stable;
|
||||
--rail-fill: var(--coollabs-elevated);
|
||||
padding: 0.5rem;
|
||||
background: var(--rail-fill);
|
||||
border-right: 1px solid var(--coollabs-hairline);
|
||||
transition: left 200ms ease;
|
||||
}
|
||||
|
||||
/* Resource pages have a second fixed header row below the main header. */
|
||||
.application-settings-workspace > .application-settings-navigation {
|
||||
top: 4rem;
|
||||
max-height: calc(100dvh - 5rem);
|
||||
.application-settings-navigation ~ * {
|
||||
grid-column: 2;
|
||||
}
|
||||
|
||||
html:not(.dark) .application-settings-navigation {
|
||||
--rail-fill: var(--coollabs-base);
|
||||
}
|
||||
|
||||
.application-settings-navigation::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.application-settings-navigation::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.application-settings-navigation::-webkit-scrollbar-thumb {
|
||||
border-radius: 9999px;
|
||||
background: var(--coollabs-fill);
|
||||
}
|
||||
|
||||
.application-settings-navigation::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--coollabs-line);
|
||||
}
|
||||
|
||||
/* Embeds: keep the surface, drop the fixed positioning. */
|
||||
.application-settings-navigation.is-flush {
|
||||
position: static;
|
||||
max-height: none;
|
||||
inset: auto;
|
||||
width: auto;
|
||||
height: auto;
|
||||
overflow: visible;
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Honor reduced-motion for the settings nav disclosure/accordion and the mobile
|
||||
sidebar sheet: keep open/close functional but drop the height/slide/fade. */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.application-settings-navigation,
|
||||
.application-settings-navigation *,
|
||||
.mobile-sidebar-sheet * {
|
||||
transition-duration: 0.01ms !important;
|
||||
animation-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1560,7 +1863,7 @@ .application-settings-workspace .select:focus-visible,
|
|||
.application-settings-form .input:focus-visible,
|
||||
.application-settings-form .select:focus-visible {
|
||||
border-color: var(--color-accent);
|
||||
box-shadow: 0 0 0 1px var(--color-accent) !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.application-settings-workspace textarea.input,
|
||||
|
|
@ -1603,7 +1906,7 @@ .resource-heading-tabs::-webkit-scrollbar {
|
|||
* Circular chevrons sit over gradient fades when more tabs are off-screen.
|
||||
*/
|
||||
.resource-heading-tabs-scroller {
|
||||
--resource-heading-tabs-fade: #f5f5f5; /* neutral-100, matches pill track */
|
||||
--resource-heading-tabs-fade: oklch(97.02% 0 0); /* neutral-100, matches pill track */
|
||||
}
|
||||
|
||||
.dark .resource-heading-tabs-scroller {
|
||||
|
|
@ -1662,7 +1965,7 @@ .resource-heading-tabs-control-icon {
|
|||
margin-inline: 0.25rem;
|
||||
border-radius: 9999px;
|
||||
color: #737373;
|
||||
background: #ffffff;
|
||||
background: var(--coollabs-base);
|
||||
box-shadow:
|
||||
0 1px 2px rgba(0, 0, 0, 0.06),
|
||||
0 0 0 1px rgba(0, 0, 0, 0.08);
|
||||
|
|
@ -1736,20 +2039,6 @@ .resource-heading-overflow-items.is-measuring {
|
|||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.resource-heading-overflow-separator {
|
||||
width: 1px;
|
||||
align-self: stretch;
|
||||
margin: 0.25rem 0.25rem;
|
||||
background: color-mix(in srgb, var(--coollabs-line) 80%, transparent);
|
||||
}
|
||||
|
||||
.resource-heading-overflow.is-collapsed .resource-heading-overflow-separator {
|
||||
width: auto;
|
||||
height: 1px;
|
||||
align-self: auto;
|
||||
margin: 0.25rem 0.375rem;
|
||||
}
|
||||
|
||||
.resource-heading-overflow.is-collapsed .resource-heading-overflow-items > .button,
|
||||
.resource-heading-overflow.is-collapsed .resource-heading-overflow-items > a.button {
|
||||
width: 100%;
|
||||
|
|
@ -1870,6 +2159,61 @@ .application-heading-actions .app-tab svg {
|
|||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.split-action {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.split-action-main,
|
||||
.split-action-caret {
|
||||
@apply button-highlighted;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.375rem;
|
||||
height: 2rem;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.split-action-main {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
padding: 0 0.625rem;
|
||||
border-radius: 6px 0 0 6px;
|
||||
}
|
||||
|
||||
.split-action-caret {
|
||||
flex-shrink: 0;
|
||||
width: 1.75rem;
|
||||
border-left: 1px solid rgba(255, 255, 255, 0.25);
|
||||
border-radius: 0 6px 6px 0;
|
||||
}
|
||||
|
||||
.split-action > .split-action-main:only-of-type {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.split-action-main:disabled,
|
||||
.split-action-caret:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
.split-action-main:focus-visible,
|
||||
.split-action-caret:focus-visible {
|
||||
outline: none;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
box-shadow: 0 0 0 3px color-mix(in oklab, var(--color-accent) 40%, transparent);
|
||||
}
|
||||
|
||||
.application-heading-actions .split-action-main,
|
||||
.application-heading-actions .split-action-caret {
|
||||
height: 1.75rem;
|
||||
}
|
||||
|
||||
/* Custom listbox (replaces native <select>) */
|
||||
.listbox-trigger {
|
||||
display: flex;
|
||||
|
|
@ -1919,7 +2263,7 @@ .dark .listbox-trigger:disabled {
|
|||
.listbox-trigger:focus-visible {
|
||||
outline: none;
|
||||
border-color: var(--color-accent);
|
||||
box-shadow: 0 0 0 1px var(--color-accent);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.listbox-panel {
|
||||
|
|
@ -1944,7 +2288,7 @@ .listbox-panel {
|
|||
border-radius: 10px;
|
||||
border: 1px solid var(--coollabs-line);
|
||||
background: var(--coollabs-recessed);
|
||||
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.45);
|
||||
box-shadow: var(--shadow-dropdown);
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
|
|
@ -2040,7 +2384,7 @@ .searchable-listbox-search-input::placeholder {
|
|||
.searchable-listbox-search-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-accent);
|
||||
box-shadow: 0 0 0 1px var(--color-accent);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* Hide native WebKit search clear so custom clear buttons are not doubled */
|
||||
|
|
@ -3009,7 +3353,7 @@ .dark .data-table-cell-dash {
|
|||
/* Shared logs viewer (deployment + runtime) — mobile-first toolbar
|
||||
Mobile stacks: search → meta → full-width actions (no side-by-side overlap). */
|
||||
.logs-viewer {
|
||||
background: #fff;
|
||||
background: var(--coollabs-base);
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
|
|
@ -3510,7 +3854,7 @@ .runtime-log-menu {
|
|||
.runtime-log-viewport {
|
||||
min-height: 12rem;
|
||||
border-radius: 0 0 8px 8px;
|
||||
background: #fafafa;
|
||||
background: var(--coollabs-elevated);
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
|
|
@ -3525,10 +3869,6 @@ .dark .runtime-log-viewport {
|
|||
color: var(--color-fg-dim);
|
||||
}
|
||||
|
||||
.env-table-detail {
|
||||
padding: 0.25rem 1rem 1.25rem;
|
||||
}
|
||||
|
||||
/* Small pill badges for table cells */
|
||||
.table-badge {
|
||||
display: inline-flex;
|
||||
|
|
@ -3606,7 +3946,7 @@ .chip-input {
|
|||
|
||||
.chip-input:focus-within {
|
||||
border-color: var(--color-accent);
|
||||
box-shadow: 0 0 0 1px var(--color-accent);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.chip-input input {
|
||||
|
|
@ -3679,93 +4019,6 @@ .chip-remove:hover {
|
|||
color: inherit;
|
||||
}
|
||||
|
||||
/* Detached pill options (segmented radio groups rendered as separate pills) */
|
||||
.option-pill-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.option-pill-group label {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.option-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.375rem;
|
||||
height: 2rem;
|
||||
padding: 0 0.875rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--coollabs-line);
|
||||
background: var(--coollabs-elevated);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
color: #000000;
|
||||
transition: background-color 0.15s, border-color 0.15s;
|
||||
}
|
||||
|
||||
.dark .option-pill {
|
||||
color: var(--color-fg);
|
||||
}
|
||||
|
||||
.option-pill:hover {
|
||||
background: var(--coollabs-recessed);
|
||||
}
|
||||
|
||||
.peer:checked + .option-pill {
|
||||
background: var(--coollabs-fill);
|
||||
border-color: var(--coollabs-line);
|
||||
}
|
||||
|
||||
.peer:disabled + .option-pill {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Label column + content rows inside a card body */
|
||||
.application-purpose-row {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
border-top: 1px solid var(--coollabs-fill);
|
||||
padding-top: 1.25rem;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.application-purpose-row {
|
||||
grid-template-columns: 13rem minmax(0, 1fr);
|
||||
gap: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.application-purpose-copy h4 {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.dark .application-purpose-copy h4 {
|
||||
color: var(--color-fg);
|
||||
}
|
||||
|
||||
.application-purpose-copy p {
|
||||
margin-top: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
color: #71717a;
|
||||
}
|
||||
|
||||
.dark .application-purpose-copy p {
|
||||
color: var(--color-fg-dim);
|
||||
}
|
||||
|
||||
.application-details-card .application-purpose-row {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
/* In-card sub-headings (e.g. "Container labels", "Docker Compose") */
|
||||
.application-settings-form h4,
|
||||
.application-settings-form .application-settings-section-body h3 {
|
||||
|
|
@ -3922,7 +4175,7 @@ .command-palette {
|
|||
overflow: hidden;
|
||||
border-radius: 10px;
|
||||
background: var(--coollabs-elevated);
|
||||
box-shadow: 0 0 0 1px var(--coollabs-hairline), var(--shadow-modal);
|
||||
box-shadow: 0 0 0 1px var(--coollabs-hairline), var(--shadow-dropdown);
|
||||
}
|
||||
|
||||
.command-palette-header {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ @utility apexcharts-xaxistooltip {
|
|||
}
|
||||
|
||||
@utility apexcharts-tooltip-custom {
|
||||
@apply bg-white dark:bg-coolgray-100 border border-neutral-200 dark:border-coolgray-300 rounded-lg shadow-lg p-3 text-sm;
|
||||
@apply bg-white dark:bg-coolgray-100 border border-neutral-200 dark:border-coolgray-300 rounded-lg shadow-dropdown p-3 text-sm;
|
||||
min-width: 160px;
|
||||
}
|
||||
|
||||
|
|
@ -59,7 +59,7 @@ @utility input-focus {
|
|||
|
||||
/* input, select before */
|
||||
@utility input-select {
|
||||
@apply block h-9 px-3 py-1.5 w-full text-sm text-black rounded-md border border-neutral-200 bg-white dark:bg-surface dark:text-fg dark:border-white/[0.08] transition-colors disabled:bg-neutral-100 disabled:text-neutral-400 dark:disabled:bg-white/[0.03] dark:disabled:text-fg-faint;
|
||||
@apply block h-9 px-3 py-1.5 w-full text-sm text-black rounded-md border border-neutral-200 bg-[var(--coollabs-recessed)] dark:text-fg dark:border-white/[0.08] transition-colors disabled:bg-neutral-100 disabled:text-neutral-400 dark:disabled:bg-white/[0.03] dark:disabled:text-fg-faint;
|
||||
box-shadow: none;
|
||||
|
||||
&:where(.dark, .dark *) {
|
||||
|
|
@ -77,18 +77,18 @@ @utility input-select {
|
|||
|
||||
/* Readonly */
|
||||
@utility input {
|
||||
@apply dark:read-only:text-neutral-500 dark:read-only:bg-coolgray-100/40 placeholder:text-neutral-300 dark:placeholder:text-neutral-700 read-only:text-neutral-500 read-only:bg-neutral-200;
|
||||
@apply dark:read-only:text-neutral-500 dark:read-only:bg-coolgray-100/40 placeholder:text-neutral-400 dark:placeholder:text-neutral-500 read-only:text-neutral-500 read-only:bg-neutral-200;
|
||||
@apply input-select;
|
||||
@apply focus-visible:outline-none;
|
||||
|
||||
&:focus-visible {
|
||||
border-color: var(--color-accent);
|
||||
box-shadow: 0 0 0 1px var(--color-accent);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&:where(.dark, .dark *):focus-visible {
|
||||
border-color: var(--color-accent);
|
||||
box-shadow: 0 0 0 1px var(--color-accent);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&:read-only {
|
||||
|
|
@ -116,12 +116,12 @@ @utility select {
|
|||
|
||||
&:focus-visible {
|
||||
border-color: var(--color-accent);
|
||||
box-shadow: 0 0 0 1px var(--color-accent);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&:where(.dark, .dark *):focus-visible {
|
||||
border-color: var(--color-accent);
|
||||
box-shadow: 0 0 0 1px var(--color-accent);
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -162,7 +162,7 @@ @utility app-tab-active {
|
|||
}
|
||||
|
||||
@utility auth-tooltip {
|
||||
@apply fixed z-[10000] px-2.5 py-1.5 text-xs font-medium rounded-lg pointer-events-none whitespace-nowrap text-white bg-neutral-900 border border-neutral-700 shadow-lg dark:text-fg dark:bg-raised dark:border-white/10;
|
||||
@apply fixed z-[10000] px-2.5 py-1.5 text-xs font-medium rounded-lg pointer-events-none whitespace-nowrap text-white bg-neutral-900 border border-neutral-700 shadow-dropdown dark:text-fg dark:bg-raised dark:border-white/10;
|
||||
}
|
||||
|
||||
@utility alert-success {
|
||||
|
|
@ -222,7 +222,10 @@ @utility menu {
|
|||
}
|
||||
|
||||
@utility menu-item {
|
||||
@apply relative flex gap-2.5 items-center h-8 px-2.5 w-full text-[13px] font-medium rounded-md truncate min-w-0 transition-colors text-nav-text hover:bg-neutral-100 hover:text-nav-active dark:hover:bg-white/[0.05];
|
||||
/* Translucent hover (like the active pill) so it darkens against any surface,
|
||||
including the light-mode settings rail's gray fill; on white it matches the
|
||||
old neutral-100. */
|
||||
@apply relative flex gap-2.5 items-center h-8 px-2.5 w-full text-[13px] font-medium rounded-md truncate min-w-0 transition-colors text-nav-text hover:bg-black/[0.04] hover:text-nav-active dark:hover:bg-white/[0.05];
|
||||
}
|
||||
@utility menu-item-icon {
|
||||
@apply shrink-0 size-[18px] opacity-90;
|
||||
|
|
@ -245,7 +248,7 @@ @utility nav-section {
|
|||
/* Indented child rows in a collapsible nav group */
|
||||
@utility menu-subitem {
|
||||
/* Label owns text ellipsis; keep this row overflow-visible so the focus ring is not clipped. */
|
||||
@apply relative flex gap-2.5 items-center h-8 pl-3 pr-2.5 w-full text-[13px] font-medium rounded-md min-w-0 transition-colors text-nav-text hover:bg-neutral-100 hover:text-nav-active dark:hover:bg-white/[0.05];
|
||||
@apply relative flex gap-2.5 items-center h-8 pl-3 pr-2.5 w-full text-[13px] font-medium rounded-md min-w-0 transition-colors text-nav-text hover:bg-black/[0.04] hover:text-nav-active dark:hover:bg-white/[0.05];
|
||||
}
|
||||
@utility menu-subitem-active {
|
||||
@apply rounded-md bg-black/[0.05] text-nav-active hover:bg-black/[0.05] dark:bg-white/[0.06] dark:hover:bg-white/[0.06];
|
||||
|
|
@ -344,7 +347,7 @@ @utility info-helper {
|
|||
}
|
||||
|
||||
@utility info-helper-popup {
|
||||
@apply rounded-lg border border-neutral-200 bg-white text-neutral-600 shadow-modal whitespace-normal break-words dark:border-white/10 dark:bg-raised dark:text-fg-dim;
|
||||
@apply rounded-lg border border-neutral-200 bg-white text-neutral-600 shadow-dropdown whitespace-normal break-words dark:border-white/10 dark:bg-raised dark:text-fg-dim;
|
||||
}
|
||||
|
||||
@utility buyme {
|
||||
|
|
@ -367,6 +370,14 @@ @utility toast {
|
|||
@apply z-[1];
|
||||
}
|
||||
|
||||
/* Floating notice / popover surface: elevated fill, hairline ring, and the
|
||||
shared dropdown lift. Toasts, change-pending popovers and the deployments
|
||||
list share this so they match menus/dropdowns instead of the heavier modal. */
|
||||
@utility surface-popover {
|
||||
background: var(--coollabs-elevated);
|
||||
box-shadow: 0 0 0 1px var(--coollabs-line), var(--shadow-dropdown);
|
||||
}
|
||||
|
||||
@utility dz-button {
|
||||
@apply p-4 py-10 my-4 w-full font-bold bg-white border dark:border-coolgray-400 dark:text-white dark:bg-transparent dark:hover:bg-coolgray-400;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -124,3 +124,16 @@ window.scrollToSettingsSection = function scrollToSettingsSection(id) {
|
|||
|
||||
rafId = window.requestAnimationFrame(tick);
|
||||
};
|
||||
|
||||
// When a settings sub-section link navigates across pages (href="route#section-id"),
|
||||
// scroll to that section once the destination page has rendered.
|
||||
function scrollToHashSettingsSection() {
|
||||
const hash = window.location.hash;
|
||||
if (!hash || hash.length < 2) {
|
||||
return;
|
||||
}
|
||||
const id = decodeURIComponent(hash.slice(1));
|
||||
window.requestAnimationFrame(() => window.scrollToSettingsSection?.(id));
|
||||
}
|
||||
document.addEventListener('livewire:navigated', scrollToHashSettingsSection);
|
||||
document.addEventListener('DOMContentLoaded', scrollToHashSettingsSection);
|
||||
|
|
|
|||
|
|
@ -238,52 +238,104 @@
|
|||
];
|
||||
@endphp
|
||||
|
||||
@php
|
||||
$activeMenuLabel = collect($groupedMenuItems)->flatMap(fn ($items) => $items)->firstWhere('active', true)['label'] ?? 'Settings';
|
||||
@endphp
|
||||
<aside @class([
|
||||
'application-settings-navigation min-w-0 xl:self-start',
|
||||
'is-flush' => $flush,
|
||||
])>
|
||||
<nav aria-label="Configuration sections"
|
||||
class="grid grid-cols-2 gap-0.5 border-y border-neutral-200 py-3 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-1 xl:border-y-0 xl:py-0 dark:border-white/[0.06]">
|
||||
])
|
||||
x-data="{
|
||||
menuOpen: false,
|
||||
desktop: window.matchMedia('(min-width: 1280px)').matches,
|
||||
init() {
|
||||
const mq = window.matchMedia('(min-width: 1280px)');
|
||||
mq.addEventListener('change', (e) => { this.desktop = e.matches; if (e.matches) { this.menuOpen = false; } });
|
||||
},
|
||||
}"
|
||||
x-on:click.outside="menuOpen = false"
|
||||
x-on:keydown.escape.window="menuOpen = false">
|
||||
{{-- Mobile disclosure: tap to reveal the full grouped nav; hidden at xl (the pinned rail). --}}
|
||||
<button type="button" x-show="!desktop" x-cloak x-on:click="menuOpen = !menuOpen"
|
||||
:aria-expanded="menuOpen" aria-label="Configuration menu"
|
||||
:class="menuOpen && 'ring-1 ring-black/10 dark:ring-white/15'"
|
||||
class="flex h-10 w-full items-center justify-between gap-2 rounded-xl border border-neutral-200 bg-white px-3 text-[13px] font-medium text-black transition-transform duration-100 ease-out hover:bg-neutral-50 active:scale-[0.985] dark:border-white/[0.08] dark:bg-white/[0.05] dark:text-fg dark:hover:bg-white/[0.08]">
|
||||
<span class="flex min-w-0 items-center gap-2">
|
||||
<x-reicon name="settings" class="size-4 shrink-0 text-nav-muted" />
|
||||
<span class="truncate">{{ $activeMenuLabel }}</span>
|
||||
</span>
|
||||
<svg class="size-3.5 shrink-0 text-nav-muted transition-transform duration-200 ease-[cubic-bezier(0.23,1,0.32,1)]"
|
||||
:class="menuOpen && 'rotate-180'" viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
||||
<path d="m3.5 4.75 2.5 2.5 2.5-2.5" stroke="currentColor" stroke-width="1.25"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<nav aria-label="Configuration sections" x-show="desktop || menuOpen" x-collapse.duration.200ms x-cloak
|
||||
class="mt-2 flex flex-col gap-0.5 rounded-xl border border-neutral-200 bg-white p-2 shadow-[var(--shadow-dropdown)] dark:border-white/[0.08] dark:bg-white/[0.03] xl:mt-0 xl:rounded-none xl:border-0 xl:bg-transparent xl:p-0 xl:shadow-none xl:dark:bg-transparent">
|
||||
@foreach ($groupedMenuItems as $groupLabel => $groupItems)
|
||||
@unless ($loop->first)
|
||||
<div class="hidden xl:block my-2 border-t border-neutral-200 dark:border-white/[0.06]" aria-hidden="true"></div>
|
||||
<div class="my-2 border-t border-neutral-200 dark:border-white/[0.06]" aria-hidden="true"></div>
|
||||
@endunless
|
||||
<div class="nav-section hidden xl:block">{{ $groupLabel }}</div>
|
||||
<div class="nav-section">{{ $groupLabel }}</div>
|
||||
@foreach ($groupItems as $menuItem)
|
||||
<a wire:key="application-settings-link-{{ str($menuItem['label'])->slug() }}"
|
||||
@class([
|
||||
'menu-item',
|
||||
'menu-item-active' => $menuItem['active'],
|
||||
])
|
||||
@if ($menuItem['navigate'] ?? true) {{ wireNavigate() }} @endif
|
||||
href="{{ route($menuItem['route'], $applicationRouteParameters) }}">
|
||||
<x-reicon :name="$menuIcons[$menuItem['label']] ?? 'settings'" class="menu-item-icon" />
|
||||
<span class="menu-item-label">{{ $menuItem['label'] }}</span>
|
||||
@if ($menuItem['badge'] ?? false)
|
||||
<span class="shrink-0">
|
||||
<livewire:project.application.server-status-badge :application="$application"
|
||||
:key="'application-server-status-'.$application->uuid" />
|
||||
</span>
|
||||
@php $sections = $pageSections[$menuItem['route']] ?? []; @endphp
|
||||
<div wire:key="application-settings-group-{{ str($menuItem['label'])->slug() }}"
|
||||
@if (filled($sections)) x-data="{ open: @js($menuItem['active']), activeSection: '' }" @endif
|
||||
class="relative">
|
||||
<a wire:key="application-settings-link-{{ str($menuItem['label'])->slug() }}"
|
||||
@class([
|
||||
'menu-item',
|
||||
'menu-item-active' => $menuItem['active'],
|
||||
'pr-9' => filled($sections),
|
||||
])
|
||||
@if ($menuItem['navigate'] ?? true) {{ wireNavigate() }} @endif
|
||||
href="{{ route($menuItem['route'], $applicationRouteParameters) }}"
|
||||
x-on:click="menuOpen = false">
|
||||
<x-reicon :name="$menuIcons[$menuItem['label']] ?? 'settings'" class="menu-item-icon" />
|
||||
<span class="menu-item-label">{{ $menuItem['label'] }}</span>
|
||||
@if ($menuItem['badge'] ?? false)
|
||||
<span class="shrink-0">
|
||||
<livewire:project.application.server-status-badge :application="$application"
|
||||
:key="'application-server-status-'.$application->uuid" />
|
||||
</span>
|
||||
@endif
|
||||
</a>
|
||||
@if (filled($sections))
|
||||
{{-- Expand/collapse the page's sub-sections. Active page opens by
|
||||
default; the label still navigates, the chevron only toggles. --}}
|
||||
<button type="button"
|
||||
class="absolute right-1 top-1 flex size-6 items-center justify-center rounded-md text-nav-muted transition-colors hover:bg-black/[0.04] hover:text-nav-active dark:hover:bg-white/[0.06]"
|
||||
x-on:click.stop.prevent="open = !open" :aria-expanded="open"
|
||||
aria-label="Toggle {{ $menuItem['label'] }} sections">
|
||||
<svg class="size-3.5 transition-transform duration-200 ease-[cubic-bezier(0.23,1,0.32,1)]"
|
||||
:class="open && 'rotate-90'" viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
||||
<path d="m4.5 3 3 3-3 3" stroke="currentColor" stroke-width="1.25"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
<div>
|
||||
<div x-show="open" x-collapse.duration.200ms x-cloak
|
||||
class="nav-children flex flex-col gap-0.5 py-1">
|
||||
@foreach ($sections as $section)
|
||||
@if ($menuItem['active'])
|
||||
<button type="button" class="menu-subitem"
|
||||
:class="activeSection === '{{ $section['id'] }}' && 'menu-subitem-active'"
|
||||
x-on:click="menuOpen = false; activeSection = '{{ $section['id'] }}'; history.replaceState(null, '', '#{{ $section['id'] }}'); window.scrollToSettingsSection?.('{{ $section['id'] }}')">
|
||||
<span class="menu-item-label text-left">{{ $section['label'] }}</span>
|
||||
</button>
|
||||
@else
|
||||
<a class="menu-subitem"
|
||||
href="{{ route($menuItem['route'], $applicationRouteParameters) }}#{{ $section['id'] }}"
|
||||
{{ wireNavigate() }} x-on:click="menuOpen = false">
|
||||
<span class="menu-item-label text-left">{{ $section['label'] }}</span>
|
||||
</a>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</a>
|
||||
@if ($menuItem['active'] && count($pageSections[$menuItem['route']] ?? []) >= 4)
|
||||
<div class="nav-children hidden flex-col gap-0.5 py-1 xl:flex"
|
||||
x-data="{
|
||||
activeSection: '',
|
||||
scrollToSection(id) {
|
||||
this.activeSection = id;
|
||||
window.scrollToSettingsSection?.(id);
|
||||
},
|
||||
}">
|
||||
@foreach ($pageSections[$menuItem['route']] as $section)
|
||||
<button type="button" class="menu-subitem"
|
||||
:class="activeSection === '{{ $section['id'] }}' && 'menu-subitem-active'"
|
||||
@click="scrollToSection('{{ $section['id'] }}')">
|
||||
<span class="menu-item-label text-left">{{ $section['label'] }}</span>
|
||||
</button>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
@endforeach
|
||||
</nav>
|
||||
|
|
|
|||
|
|
@ -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 @@ class="flex h-8 items-center justify-center gap-1.5 rounded-lg px-2 text-amber-7
|
|||
</button>
|
||||
|
||||
<div x-show="open" x-cloak x-transition.opacity role="dialog"
|
||||
class="fixed top-14 left-1/2 z-[1100] w-[calc(100vw-2rem)] max-w-sm -translate-x-1/2 rounded-lg p-3 lg:absolute lg:top-full lg:right-0 lg:left-auto lg:mt-2 lg:translate-x-0"
|
||||
style="background: var(--coollabs-elevated); box-shadow: 0 0 0 1px var(--coollabs-line), var(--shadow-modal);">
|
||||
class="surface-popover fixed top-14 left-1/2 z-[1100] w-[calc(100vw-2rem)] max-w-sm -translate-x-1/2 rounded-lg p-3 lg:absolute lg:top-full lg:right-0 lg:left-auto lg:mt-2 lg:translate-x-0">
|
||||
<div class="flex items-start gap-2.5">
|
||||
<span
|
||||
class="flex size-7 shrink-0 items-center justify-center rounded-md bg-amber-100 text-amber-700 dark:bg-warning/10 dark:text-warning">
|
||||
|
|
|
|||
|
|
@ -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 @@ class="flex min-w-0 grow items-center gap-1.5 break-words text-[12px] text-neutr
|
|||
@endif
|
||||
|
||||
<span
|
||||
class="pointer-events-none absolute inset-0 rounded-[5px] border border-neutral-300 bg-white shadow-[inset_0_1px_1px_rgb(0_0_0/0.04)] transition-colors group-hover:border-neutral-400 peer-checked:border-coollabs peer-checked:bg-coollabs peer-focus-visible:ring-2 peer-focus-visible:ring-coollabs/25 peer-focus-visible:ring-offset-2 peer-disabled:opacity-50 dark:border-white/[0.14] dark:bg-white/[0.045] dark:shadow-none dark:group-hover:border-white/[0.22] dark:peer-checked:border-warning dark:peer-checked:bg-warning dark:peer-focus-visible:ring-warning/30 dark:peer-focus-visible:ring-offset-base"></span>
|
||||
class="pointer-events-none absolute inset-0 rounded-[5px] border border-neutral-300 bg-white shadow-[inset_0_1px_1px_rgb(0_0_0/0.04)] transition-[color,background-color,border-color,box-shadow] duration-150 ease-out group-hover:border-neutral-400 peer-checked:border-coollabs peer-checked:bg-coollabs peer-focus-visible:ring-2 peer-focus-visible:ring-coollabs/25 peer-focus-visible:ring-offset-2 peer-disabled:opacity-50 dark:border-white/[0.14] dark:bg-white/[0.045] dark:shadow-none dark:group-hover:border-white/[0.22] dark:peer-checked:border-warning dark:peer-checked:bg-warning dark:peer-focus-visible:ring-warning/30 dark:peer-focus-visible:ring-offset-base"></span>
|
||||
<svg class="pointer-events-none absolute inset-0 m-auto size-3 scale-75 text-white opacity-0 transition-[opacity,transform] peer-checked:scale-100 peer-checked:opacity-100 dark:text-black"
|
||||
viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
||||
<path d="m2.25 6.15 2.35 2.3 5.15-5" stroke="currentColor" stroke-width="1.8"
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ class="flex-1 min-w-[120px] text-sm border-0 outline-none bg-transparent p-0 foc
|
|||
|
||||
{{-- Dropdown Options --}}
|
||||
<div x-show="open && !{{ $disabled ? 'true' : 'false' }}" x-transition
|
||||
class="absolute z-50 w-full mt-1 bg-white dark:bg-coolgray-100 border border-neutral-300 dark:border-coolgray-400 rounded shadow-lg max-h-60 overflow-auto scrollbar">
|
||||
class="absolute z-50 w-full mt-1 bg-white dark:bg-coolgray-100 border border-neutral-300 dark:border-coolgray-400 rounded shadow-dropdown max-h-60 overflow-auto scrollbar">
|
||||
|
||||
<template x-if="filteredOptions.length === 0">
|
||||
<div class="px-3 py-2 text-sm text-neutral-500 dark:text-neutral-400">
|
||||
|
|
@ -277,7 +277,7 @@ class="shrink-0 text-neutral-400 px-2 {{ $disabled ? 'cursor-not-allowed' : 'cur
|
|||
|
||||
{{-- Dropdown Options --}}
|
||||
<div x-show="open && !{{ $disabled ? 'true' : 'false' }}" x-transition
|
||||
class="absolute z-50 w-full mt-1 bg-white dark:bg-coolgray-100 border border-neutral-300 dark:border-coolgray-400 rounded shadow-lg max-h-60 overflow-auto scrollbar">
|
||||
class="absolute z-50 w-full mt-1 bg-white dark:bg-coolgray-100 border border-neutral-300 dark:border-coolgray-400 rounded shadow-dropdown max-h-60 overflow-auto scrollbar">
|
||||
|
||||
<template x-if="filteredOptions.length === 0">
|
||||
<div class="px-3 py-2 text-sm text-neutral-500 dark:text-neutral-400">
|
||||
|
|
|
|||
|
|
@ -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!">
|
||||
|
|
|
|||
|
|
@ -39,18 +39,18 @@
|
|||
<div x-show="modalOpen"
|
||||
x-init="$watch('modalOpen', value => { if(value) { $nextTick(() => { const firstInput = $el.querySelector('input, textarea, select'); firstInput?.focus(); }) } })"
|
||||
class="fixed inset-0 z-99 overflow-y-auto">
|
||||
<div x-show="modalOpen" x-transition:enter="ease-out duration-100" x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100" x-transition:leave="ease-in duration-100"
|
||||
<div x-show="modalOpen" x-transition:enter="transition-opacity ease-out duration-200" x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100" x-transition:leave="transition-opacity ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0"
|
||||
class="absolute inset-0 w-full h-full bg-black/50 backdrop-blur-[2px]"></div>
|
||||
<div @if ($closeOutside) @click.self="modalOpen=false" @endif class="relative flex min-h-full items-start justify-center p-2 sm:items-center sm:p-4">
|
||||
<div x-show="modalOpen" x-trap.inert.noscroll="modalOpen"
|
||||
x-transition:enter="ease-out duration-100"
|
||||
x-transition:enter-start="opacity-0 -translate-y-2 sm:scale-95"
|
||||
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
|
||||
x-transition:leave="ease-in duration-100"
|
||||
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
|
||||
x-transition:leave-end="opacity-0 -translate-y-2 sm:scale-95"
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 scale-95"
|
||||
x-transition:enter-end="opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100 scale-100"
|
||||
x-transition:leave-end="opacity-0 scale-95"
|
||||
@class([
|
||||
'application-settings-form application-settings-section relative flex max-h-[calc(100dvh-2rem)] w-full flex-col overflow-hidden',
|
||||
'lg:w-[95vw]! lg:max-w-7xl!' => $isLarge,
|
||||
|
|
|
|||
|
|
@ -240,6 +240,6 @@ class="menu-item w-8 shrink-0 justify-center px-0">
|
|||
</div>
|
||||
<div x-show="collapsed && tooltip.show" x-cloak x-transition.opacity.duration.100ms
|
||||
:style="`left: ${tooltip.x}px; top: ${tooltip.y}px;`"
|
||||
class="fixed z-[10000] -translate-y-1/2 px-2 py-1 text-xs font-medium rounded-lg bg-neutral-900 dark:bg-raised text-white whitespace-nowrap pointer-events-none shadow-lg border border-neutral-700 dark:border-white/10"
|
||||
class="fixed z-[10000] -translate-y-1/2 px-2 py-1 text-xs font-medium rounded-lg bg-neutral-900 dark:bg-raised text-white whitespace-nowrap pointer-events-none shadow-dropdown border border-neutral-700 dark:border-white/10"
|
||||
x-text="tooltip.text"></div>
|
||||
</nav>
|
||||
|
|
|
|||
|
|
@ -53,8 +53,7 @@
|
|||
x-transition:leave-end="translate-y-3 opacity-0"
|
||||
class="fixed right-4 z-999 {{ $position === 'top-right' ? 'top-16' : 'bottom-4' }}">
|
||||
<template x-if="iconOnly">
|
||||
<button type="button" @click="restore()" aria-label="Restore warning" class="flex rounded-lg p-2"
|
||||
style="background: var(--coollabs-elevated); box-shadow: 0 0 0 1px var(--coollabs-line), var(--shadow-modal);">
|
||||
<button type="button" @click="restore()" aria-label="Restore warning" class="surface-popover flex rounded-lg p-2">
|
||||
@isset($icon)
|
||||
<span
|
||||
class="flex size-7 shrink-0 items-center justify-center rounded-md bg-amber-100 text-amber-700 dark:bg-warning/10 dark:text-warning">
|
||||
|
|
@ -65,12 +64,11 @@ class="flex size-7 shrink-0 items-center justify-center rounded-md bg-amber-100
|
|||
</template>
|
||||
|
||||
<template x-if="!iconOnly">
|
||||
<div class="relative flex items-start gap-2.5 rounded-lg p-3 pr-10"
|
||||
<div class="surface-popover relative flex items-start gap-2.5 rounded-lg p-3 pr-10"
|
||||
:class="compact
|
||||
? 'w-[calc(100vw-2rem)] max-w-sm cursor-pointer'
|
||||
: 'w-[calc(100vw-2rem)] max-w-sm'"
|
||||
@click="restore()"
|
||||
style="background: var(--coollabs-elevated); box-shadow: 0 0 0 1px var(--coollabs-line), var(--shadow-modal);">
|
||||
@click="restore()">
|
||||
@isset($icon)
|
||||
<div
|
||||
class="flex size-7 shrink-0 items-center justify-center rounded-md bg-amber-100 text-amber-700 dark:bg-warning/10 dark:text-warning">
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ class="fixed bottom-0 right-0 w-full h-auto duration-300 ease-out sm:px-5 sm:pb-
|
|||
{{ $customActions }}
|
||||
@else
|
||||
<div
|
||||
class="flex lg:items-center flex-col justify-between w-full h-full max-w-4xl p-6 mx-auto bg-white border shadow-lg lg:border-t dark:border-coolgray-300 border-neutral-200 dark:bg-coolgray-100 lg:p-8 lg:flex-row sm:rounded-sm">
|
||||
class="flex lg:items-center flex-col justify-between w-full h-full max-w-4xl p-6 mx-auto bg-white border shadow-modal lg:border-t dark:border-coolgray-300 border-neutral-200 dark:bg-coolgray-100 lg:p-8 lg:flex-row sm:rounded-sm">
|
||||
<div
|
||||
class="flex flex-col items-start h-full pb-6 text-xs lg:items-center lg:flex-row lg:pb-0 lg:pr-6 lg:space-x-5 dark:text-neutral-300">
|
||||
@if (isset($icon))
|
||||
|
|
|
|||
|
|
@ -9,8 +9,7 @@ class="flex h-8 items-center justify-center gap-1.5 rounded-lg px-2 text-amber-7
|
|||
</button>
|
||||
|
||||
<div x-show="open" x-cloak x-transition.opacity role="dialog"
|
||||
class="fixed top-14 left-1/2 z-[1100] w-[calc(100vw-2rem)] max-w-sm -translate-x-1/2 rounded-lg p-3 lg:absolute lg:top-full lg:right-0 lg:left-auto lg:mt-2 lg:translate-x-0"
|
||||
style="background: var(--coollabs-elevated); box-shadow: 0 0 0 1px var(--coollabs-line), var(--shadow-modal);">
|
||||
class="surface-popover fixed top-14 left-1/2 z-[1100] w-[calc(100vw-2rem)] max-w-sm -translate-x-1/2 rounded-lg p-3 lg:absolute lg:top-full lg:right-0 lg:left-auto lg:mt-2 lg:translate-x-0">
|
||||
<div class="flex items-start gap-2.5">
|
||||
<span
|
||||
class="flex size-7 shrink-0 items-center justify-center rounded-md bg-amber-100 text-amber-700 dark:bg-warning/10 dark:text-warning">
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@
|
|||
x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-75" x-transition:leave-start="opacity-100 scale-100"
|
||||
x-transition:leave-end="opacity-0 scale-95"
|
||||
class="absolute z-20 top-full mt-1 w-56 -ml-2 bg-white dark:bg-coolgray-100 rounded-md shadow-lg py-1 border border-neutral-200 dark:border-coolgray-200 max-h-96 overflow-y-auto scrollbar">
|
||||
class="absolute z-20 top-full mt-1 w-56 -ml-2 bg-white dark:bg-coolgray-100 rounded-md shadow-[var(--shadow-dropdown)] py-1 border border-neutral-200 dark:border-coolgray-200 max-h-96 overflow-y-auto scrollbar">
|
||||
@foreach ($projects as $project)
|
||||
<a href="{{ route('project.show', ['project_uuid' => $project->uuid]) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200 {{ $project->uuid === $currentProjectUuid ? 'dark:text-warning font-semibold' : '' }}"
|
||||
|
|
@ -130,7 +130,7 @@ class="absolute z-20 top-full mt-1 left-0 sm:left-auto max-w-[calc(100vw-1rem)]"
|
|||
x-init="$nextTick(() => { const rect = $el.getBoundingClientRect(); if (rect.right > window.innerWidth) { $el.style.left = 'auto'; $el.style.right = '0'; } })">
|
||||
<!-- Environment List -->
|
||||
<div
|
||||
class="relative w-48 bg-white dark:bg-coolgray-100 rounded-md shadow-lg py-1 border border-neutral-200 dark:border-coolgray-200 max-h-96 overflow-y-auto scrollbar">
|
||||
class="relative w-48 bg-white dark:bg-coolgray-100 rounded-md shadow-[var(--shadow-dropdown)] py-1 border border-neutral-200 dark:border-coolgray-200 max-h-96 overflow-y-auto scrollbar">
|
||||
@foreach ($environments as $environment)
|
||||
@php
|
||||
$envDatabases = collect()
|
||||
|
|
@ -205,7 +205,7 @@ class="flex items-center gap-2 px-4 py-2 text-sm hover:bg-neutral-100 dark:hover
|
|||
:style="'position: absolute; left: 100%; top: ' + (envPositions['{{ $environment->uuid }}'] || 0) + 'px; z-index: 30;'"
|
||||
class="flex flex-col sm:flex-row items-start pl-1">
|
||||
<div
|
||||
class="relative w-56 bg-white dark:bg-coolgray-100 rounded-md shadow-lg py-1 border border-neutral-200 dark:border-coolgray-200 max-h-96 overflow-y-auto scrollbar">
|
||||
class="relative w-56 bg-white dark:bg-coolgray-100 rounded-md shadow-[var(--shadow-dropdown)] py-1 border border-neutral-200 dark:border-coolgray-200 max-h-96 overflow-y-auto scrollbar">
|
||||
@foreach ($envResources as $envResource)
|
||||
@php
|
||||
$resType = $envResource['type'];
|
||||
|
|
|
|||
34
resources/views/components/section-heading.blade.php
Normal file
34
resources/views/components/section-heading.blade.php
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
@props([
|
||||
'title',
|
||||
'subtitle' => null,
|
||||
'href' => null,
|
||||
'actionLabel' => 'View all',
|
||||
'icon' => 'arrow-right',
|
||||
])
|
||||
|
||||
{{--
|
||||
Shared section heading: a 14px title, an optional 11px muted subtitle, and an
|
||||
optional right-aligned action. Pass `href` for the default ghost "View all"
|
||||
button, or an `<x-slot:actions>` for a custom control.
|
||||
--}}
|
||||
<div {{ $attributes->merge(['class' => 'mb-3 min-w-0']) }}>
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<h2 class="min-w-0 truncate text-[14px]! leading-5! font-semibold! text-black dark:text-fg">
|
||||
{{ $title }}
|
||||
</h2>
|
||||
@isset($actions)
|
||||
<div class="shrink-0">{{ $actions }}</div>
|
||||
@elseif (filled($href))
|
||||
<a href="{{ $href }}" {{ wireNavigate() }}
|
||||
class="group inline-flex h-7 shrink-0 items-center gap-1.5 rounded-md border border-neutral-200 bg-white px-2.5 text-[12px] font-medium text-neutral-600 transition-[color,background-color,transform] duration-100 ease-out hover:bg-neutral-100 hover:text-black active:scale-[0.97] dark:border-white/[0.08] dark:bg-white/[0.06] dark:text-fg-dim dark:hover:bg-white/[0.1] dark:hover:text-fg">
|
||||
{{ $actionLabel }}
|
||||
@if ($icon)
|
||||
<x-reicon :name="$icon" class="size-3 opacity-70 transition-transform duration-150 ease-out group-hover:translate-x-0.5" />
|
||||
@endif
|
||||
</a>
|
||||
@endisset
|
||||
</div>
|
||||
@if (filled($subtitle))
|
||||
<p class="mt-0.5 text-[11px] text-neutral-500 dark:text-fg-faint">{{ $subtitle }}</p>
|
||||
@endif
|
||||
</div>
|
||||
|
|
@ -28,7 +28,7 @@ class="button button-highlighted">
|
|||
<div class="grid grid-cols-1 gap-3 p-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
@foreach ($tokens as $token)
|
||||
<a wire:key="{{ $provider }}-token-{{ $token->id }}"
|
||||
class="group flex min-h-28 min-w-0 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]"
|
||||
class="group flex min-h-28 min-w-0 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]"
|
||||
href="{{ route('server.create.token', ['type' => $routeType ?: $provider, 'token_uuid' => $token->uuid]) }}"
|
||||
{{ wireNavigate() }}>
|
||||
<div class="flex items-start gap-3">
|
||||
|
|
|
|||
|
|
@ -16,22 +16,26 @@
|
|||
<div x-show="slideOverOpen" @if (!$closeWithX) @keydown.window.escape="slideOverOpen=false" @endif
|
||||
class="relative z-99 ">
|
||||
<div x-show="slideOverOpen" @if (!$closeWithX) @click="slideOverOpen = false" @endif
|
||||
x-transition:enter="transition-opacity ease-out duration-300"
|
||||
x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition-opacity ease-in duration-200"
|
||||
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0"
|
||||
class="fixed inset-0 dark:bg-black/60 backdrop-blur-xs"></div>
|
||||
<div class="fixed inset-0 overflow-hidden">
|
||||
<div class="absolute inset-0 overflow-hidden ">
|
||||
<div class="fixed inset-y-0 right-0 flex max-w-full pl-10">
|
||||
<div x-show="slideOverOpen"
|
||||
@if (!$closeWithX) @click.away="slideOverOpen = false" @endif
|
||||
x-transition:enter="transform transition ease-in-out duration-100 sm:duration-300"
|
||||
x-transition:enter="transform transition ease-[cubic-bezier(0.32,0.72,0,1)] duration-200 sm:duration-300"
|
||||
x-transition:enter-start="translate-x-full" x-transition:enter-end="translate-x-0"
|
||||
x-transition:leave="transform transition ease-in-out duration-100 sm:duration-300"
|
||||
x-transition:leave="transform transition ease-in duration-200"
|
||||
x-transition:leave-start="translate-x-0" x-transition:leave-end="translate-x-full"
|
||||
@class([
|
||||
'max-w-xl w-screen' => !$fullScreen,
|
||||
'max-w-4xl w-screen' => $fullScreen,
|
||||
])>
|
||||
<div
|
||||
class="flex flex-col h-full py-6 overflow-hidden border-l shadow-lg bg-neutral-50 dark:bg-base dark:border-neutral-800 border-neutral-200">
|
||||
class="flex flex-col h-full py-6 overflow-hidden border-l shadow-modal bg-neutral-50 dark:bg-base dark:border-neutral-800 border-neutral-200">
|
||||
<div class="px-4 pb-4 sm:px-5">
|
||||
<div class="flex items-start justify-between pb-1">
|
||||
<h2 class="text-2xl leading-6" id="slide-over-title">
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
@props(['status', 'title' => 'Application status', 'containerName' => 'Container'])
|
||||
@props(['status', 'title' => 'Application status', 'containerName' => 'Container', 'align' => 'left'])
|
||||
|
||||
@php
|
||||
$rawStatus = str((string) $status)->lower()->trim()->value();
|
||||
|
|
@ -62,8 +62,12 @@ class="cursor-pointer">
|
|||
</span>
|
||||
</x-status-badge>
|
||||
|
||||
<div x-cloak x-show="open" x-transition.origin.top.left
|
||||
class="listbox-panel top-8! right-auto! left-0! z-[90]! w-[min(16rem,calc(100vw-1.5rem))]! min-w-0! sm:w-64! sm:min-w-64!" role="menu">
|
||||
<div x-cloak x-show="open" x-transition.origin.top.{{ $align === 'right' ? 'right' : 'left' }}
|
||||
@class([
|
||||
'listbox-panel top-8! z-[90]! w-[min(16rem,calc(100vw-1.5rem))]! min-w-0! sm:w-64! sm:min-w-64!',
|
||||
'right-auto! left-0!' => $align !== 'right',
|
||||
'left-auto! right-0!' => $align === 'right',
|
||||
]) role="menu">
|
||||
<div class="px-3 py-2 text-[11px] font-medium text-neutral-400 dark:text-fg-faint">{{ $title }}</div>
|
||||
<div class="listbox-option cursor-default! gap-2.5!">
|
||||
<span @class([
|
||||
|
|
|
|||
|
|
@ -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 @@ class="terminal-theme-trigger flex h-8 items-center gap-2 rounded-md px-2.5 text
|
|||
</button>
|
||||
|
||||
<div x-cloak x-show="themeOpen" x-transition.origin.top.right
|
||||
class="console-theme-selector absolute top-11 right-0 z-50 max-h-80 w-56 overflow-y-auto rounded-lg border border-neutral-200 bg-white p-1 shadow-[0_18px_50px_rgba(0,0,0,0.18)] dark:border-white/[0.1] dark:bg-[#111113] dark:shadow-[0_18px_50px_rgba(0,0,0,0.55)]">
|
||||
class="console-theme-selector absolute top-11 right-0 z-50 max-h-80 w-56 overflow-y-auto rounded-lg border border-neutral-200 bg-white p-1 shadow-[var(--shadow-dropdown)] dark:border-white/[0.1] dark:bg-[#111113]">
|
||||
@foreach ($themes as $theme)
|
||||
<button type="button"
|
||||
class="flex h-8 w-full items-center gap-2 rounded-md px-2 text-left text-[11px] text-neutral-600 transition-colors hover:bg-neutral-100 hover:text-neutral-950 dark:text-white/65 dark:hover:bg-white/[0.07] dark:hover:text-white"
|
||||
|
|
|
|||
185
resources/views/components/theme-controls.blade.php
Normal file
185
resources/views/components/theme-controls.blade.php
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
@props(['variant' => 'full'])
|
||||
|
||||
@php($checkPath = 'm2.5 6.25 2.1 2.1 4.9-5')
|
||||
|
||||
@if ($variant === 'menu')
|
||||
{{-- Compact list for the profile dropdown. The parent controls visibility;
|
||||
all theme state/logic comes from the shared window.themeControls(). --}}
|
||||
<div x-data="themeControls()" class="grid gap-0.5">
|
||||
@foreach ([
|
||||
['value' => 'light', 'label' => 'Light'],
|
||||
['value' => 'system', 'label' => 'System'],
|
||||
['value' => 'dark', 'label' => 'Dark'],
|
||||
['value' => 'custom', 'label' => 'Custom'],
|
||||
] as $option)
|
||||
@if ($option['value'] === 'custom')
|
||||
<div class="relative" @click.outside="pickerOpen = false">
|
||||
<button type="button" @click="chooseCustom()" aria-haspopup="dialog" :aria-expanded="pickerOpen"
|
||||
class="flex h-8 w-full items-center justify-between rounded-md px-2 text-left text-xs text-neutral-600 transition-colors hover:bg-neutral-200 hover:text-neutral-950 dark:text-fg-dim dark:hover:bg-white/[0.06] dark:hover:text-fg">
|
||||
<span class="flex items-center gap-2">
|
||||
<span class="size-3.5 rounded-full border border-white/20"
|
||||
:style="`background: ${themeColor}`"></span>
|
||||
Custom
|
||||
</span>
|
||||
<svg x-show="theme === 'custom'" class="size-3.5 text-coollabs dark:text-warning"
|
||||
viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
||||
<path d="{{ $checkPath }}" stroke="currentColor" stroke-width="1.4"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
<x-theme-controls.picker position="left-full top-0 ml-2" />
|
||||
</div>
|
||||
@else
|
||||
<button type="button" @click="setTheme('{{ $option['value'] }}')"
|
||||
class="flex h-8 w-full items-center justify-between rounded-md px-2 text-left text-xs text-neutral-600 transition-colors hover:bg-neutral-200 hover:text-neutral-950 dark:text-fg-dim dark:hover:bg-white/[0.06] dark:hover:text-fg">
|
||||
<span>{{ $option['label'] }}</span>
|
||||
<svg x-show="theme === '{{ $option['value'] }}'"
|
||||
class="size-3.5 text-coollabs dark:text-warning" viewBox="0 0 12 12" fill="none"
|
||||
aria-hidden="true">
|
||||
<path d="{{ $checkPath }}" stroke="currentColor" stroke-width="1.4"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
@endif
|
||||
@endforeach
|
||||
<div class="my-1 h-px bg-neutral-200 dark:bg-white/[0.07]"></div>
|
||||
<div class="px-2 pt-1 pb-0.5 text-[10px] font-medium tracking-wide text-neutral-400 uppercase dark:text-fg-faint">
|
||||
Page width
|
||||
</div>
|
||||
@foreach ([
|
||||
['value' => 'full', 'label' => 'Full width'],
|
||||
['value' => 'centered', 'label' => 'Centered'],
|
||||
] as $option)
|
||||
<button type="button" @click="setWidth('{{ $option['value'] }}')"
|
||||
class="flex h-8 w-full items-center justify-between rounded-md px-2 text-left text-xs text-neutral-600 transition-colors hover:bg-neutral-200 hover:text-neutral-950 dark:text-fg-dim dark:hover:bg-white/[0.06] dark:hover:text-fg">
|
||||
<span>{{ $option['label'] }}</span>
|
||||
<svg x-show="pageWidth === '{{ $option['value'] }}'"
|
||||
class="size-3.5 text-coollabs dark:text-warning" viewBox="0 0 12 12" fill="none"
|
||||
aria-hidden="true">
|
||||
<path d="{{ $checkPath }}" stroke="currentColor" stroke-width="1.4"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
{{-- Full card grid for the Appearance settings page. --}}
|
||||
<div x-data="themeControls()" class="mt-8 flex w-full max-w-none flex-col gap-6 lg:mt-3">
|
||||
<section class="application-settings-section">
|
||||
<div class="application-settings-section-header">
|
||||
<div>
|
||||
<h2>Color theme</h2>
|
||||
<p>Choose the color scheme used in this browser.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="application-settings-section-body grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
@foreach ([
|
||||
['value' => 'light', 'label' => 'Light', 'description' => 'Bright surfaces and dark text.', 'preview' => 'bg-white'],
|
||||
['value' => 'system', 'label' => 'System', 'description' => 'Follow your operating system.', 'preview' => 'bg-gradient-to-r from-white via-neutral-400 to-[#050505]'],
|
||||
['value' => 'dark', 'label' => 'Dark', 'description' => 'Dark surfaces and soft contrast.', 'preview' => 'bg-[#181818]'],
|
||||
['value' => 'custom', 'label' => 'Custom', 'description' => 'Tint light or dark surfaces with any color.', 'preview' => ''],
|
||||
] as $option)
|
||||
@if ($option['value'] === 'custom')
|
||||
{{-- Clicking the card opens the custom color picker popover (color + light/dark). --}}
|
||||
<div class="relative" @click.outside="pickerOpen = false">
|
||||
<div role="button" tabindex="0" @click="chooseCustom()" @keydown.enter.prevent="chooseCustom()"
|
||||
aria-haspopup="dialog" :aria-expanded="pickerOpen"
|
||||
class="group block w-full overflow-hidden rounded-[10px] border border-neutral-200 bg-white text-left transition-[border-color,box-shadow] hover:border-neutral-300 hover:shadow-sm dark:border-white/[0.07] dark:bg-white/[0.05] dark:hover:border-white/[0.12]"
|
||||
:class="theme === 'custom'
|
||||
? 'ring-1 ring-coollabs/30 border-coollabs/40 dark:ring-warning/30 dark:border-warning/40'
|
||||
: ''">
|
||||
<div class="h-20 border-b border-neutral-200 dark:border-white/[0.07]"
|
||||
:style="`background: color-mix(in oklab, ${themeColor} 28%, ${customMode === 'light' ? 'oklch(97% 0 0)' : 'oklch(17.35% 0.0020 286.18)'})`">
|
||||
<div class="flex h-full items-center justify-center">
|
||||
<div class="h-10 w-20 rounded-md border border-white/15 p-1 shadow-sm">
|
||||
<div class="h-full w-full rounded-sm" :style="`background: ${themeColor}`"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-3">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="text-sm font-semibold text-black dark:text-fg">{{ $option['label'] }}</span>
|
||||
<x-reicon name="check-circle" class="size-4 text-coollabs dark:text-warning"
|
||||
x-show="theme === 'custom'" x-cloak />
|
||||
</div>
|
||||
<p class="mt-1 text-xs leading-5 text-neutral-500 dark:text-fg-dim">
|
||||
{{ $option['description'] }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<x-theme-controls.picker position="left-0 top-full mt-2" />
|
||||
</div>
|
||||
@else
|
||||
<div role="button" tabindex="0"
|
||||
@click="setTheme('{{ $option['value'] }}')"
|
||||
@keydown.enter.prevent="setTheme('{{ $option['value'] }}')"
|
||||
class="group relative overflow-hidden rounded-[10px] border border-neutral-200 bg-white text-left transition-[border-color,box-shadow] hover:border-neutral-300 hover:shadow-sm dark:border-white/[0.07] dark:bg-white/[0.05] dark:hover:border-white/[0.12]"
|
||||
:class="theme === '{{ $option['value'] }}'
|
||||
? 'ring-1 ring-coollabs/30 border-coollabs/40 dark:ring-warning/30 dark:border-warning/40'
|
||||
: ''">
|
||||
<div class="h-20 {{ $option['preview'] }} border-b border-neutral-200 dark:border-white/[0.07]">
|
||||
<div class="flex h-full items-center justify-center">
|
||||
<div
|
||||
class="h-8 w-20 rounded-md border border-black/10 bg-neutral-100/80 shadow-sm dark:border-white/10 dark:bg-black/20">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-3">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="text-sm font-semibold text-black dark:text-fg">
|
||||
{{ $option['label'] }}
|
||||
</span>
|
||||
<x-reicon name="check-circle" class="size-4 text-coollabs dark:text-warning"
|
||||
x-show="theme === '{{ $option['value'] }}'" x-cloak />
|
||||
</div>
|
||||
<p class="mt-1 text-xs leading-5 text-neutral-500 dark:text-fg-dim">
|
||||
{{ $option['description'] }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="application-settings-section">
|
||||
<div class="application-settings-section-header">
|
||||
<div>
|
||||
<h2>Page width</h2>
|
||||
<p>Choose how content uses the available browser width.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="application-settings-section-body grid gap-3 sm:grid-cols-2">
|
||||
@foreach ([
|
||||
['value' => 'full', 'label' => 'Full width', 'description' => 'Use all available space for page content.'],
|
||||
['value' => 'centered', 'label' => 'Centered', 'description' => 'Keep content centered at a comfortable maximum width.'],
|
||||
] as $option)
|
||||
<button type="button" @click="setWidth('{{ $option['value'] }}')"
|
||||
class="group overflow-hidden rounded-[10px] border border-neutral-200 bg-white text-left transition-[border-color,box-shadow] hover:border-neutral-300 hover:shadow-sm dark:border-white/[0.07] dark:bg-white/[0.05] dark:hover:border-white/[0.12]"
|
||||
:class="pageWidth === '{{ $option['value'] }}'
|
||||
? 'ring-1 ring-coollabs/30 border-coollabs/40 dark:ring-warning/30 dark:border-warning/40'
|
||||
: ''">
|
||||
<div class="flex h-20 items-center border-b border-neutral-200 bg-neutral-50 px-4 dark:border-white/[0.07] dark:bg-black/15">
|
||||
<div class="flex h-11 w-full gap-1.5 rounded-md border border-neutral-300 bg-white p-1.5 dark:border-white/15 dark:bg-[#181818]">
|
||||
<div class="w-3 shrink-0 rounded-sm bg-neutral-200 dark:bg-white/10"></div>
|
||||
<div @class([
|
||||
'h-full rounded-sm bg-neutral-200 dark:bg-white/10',
|
||||
'w-full' => $option['value'] === 'full',
|
||||
'mx-auto w-2/3' => $option['value'] === 'centered',
|
||||
])></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-3">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="text-sm font-semibold text-black dark:text-fg">{{ $option['label'] }}</span>
|
||||
<x-reicon name="check-circle" class="size-4 text-coollabs dark:text-warning"
|
||||
x-show="pageWidth === '{{ $option['value'] }}'" x-cloak />
|
||||
</div>
|
||||
<p class="mt-1 text-xs leading-5 text-neutral-500 dark:text-fg-dim">{{ $option['description'] }}</p>
|
||||
</div>
|
||||
</button>
|
||||
@endforeach
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@endif
|
||||
48
resources/views/components/theme-controls/picker.blade.php
Normal file
48
resources/views/components/theme-controls/picker.blade.php
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
@props(['position' => 'left-0 top-full mt-2'])
|
||||
|
||||
{{-- Custom color picker popover: color field + light/dark mode in one panel.
|
||||
Renders inline (no own Alpine scope) so it uses the parent themeControls()
|
||||
state: pickerOpen, themeColor, customMode, previewThemeColor, saveThemeColor,
|
||||
setCustomMode. --}}
|
||||
<div x-show="pickerOpen" x-cloak
|
||||
x-transition:enter="transition ease-out duration-150"
|
||||
x-transition:enter-start="opacity-0 -translate-y-1 scale-[0.98]"
|
||||
x-transition:enter-end="opacity-100 translate-y-0 scale-100"
|
||||
x-transition:leave="transition ease-in duration-100"
|
||||
x-transition:leave-start="opacity-100 translate-y-0 scale-100"
|
||||
x-transition:leave-end="opacity-0 -translate-y-1 scale-[0.98]"
|
||||
role="dialog" aria-label="Custom color"
|
||||
class="absolute z-[60] w-60 origin-top {{ $position }} rounded-xl border border-neutral-200 bg-white p-3 text-left shadow-dropdown dark:border-white/[0.1] dark:bg-panel">
|
||||
<div class="mb-2 text-[11px] font-semibold tracking-wide text-neutral-400 uppercase dark:text-fg-faint">
|
||||
Custom color
|
||||
</div>
|
||||
<label class="flex cursor-pointer items-center gap-2.5 rounded-lg border border-neutral-200 p-2 transition-colors hover:border-neutral-300 dark:border-white/[0.08] dark:hover:border-white/[0.14]">
|
||||
<span class="relative size-8 shrink-0 overflow-hidden rounded-md border border-black/10 shadow-sm dark:border-white/15">
|
||||
<span class="absolute inset-0" :style="`background: ${themeColor}`"></span>
|
||||
<input type="color" :value="themeColor" @input="previewThemeColor($event.target.value)"
|
||||
@change="saveThemeColor($event.target.value)" aria-label="Custom theme color"
|
||||
class="absolute inset-0 h-full w-full cursor-pointer opacity-0" />
|
||||
</span>
|
||||
<span class="flex min-w-0 flex-col">
|
||||
<span class="text-xs font-medium text-black dark:text-fg">Pick a color</span>
|
||||
<span class="text-[11px] tracking-wide text-neutral-500 uppercase dark:text-fg-dim" x-text="themeColor"></span>
|
||||
</span>
|
||||
</label>
|
||||
<div class="mt-2 grid grid-cols-2 gap-1 rounded-lg border border-neutral-200 p-0.5 dark:border-white/[0.08]"
|
||||
role="group" aria-label="Custom theme mode">
|
||||
<button type="button" @click="setCustomMode('light')" :aria-pressed="customMode === 'light'"
|
||||
class="rounded-md py-1.5 text-xs font-medium transition-colors"
|
||||
:class="customMode === 'light'
|
||||
? 'bg-neutral-100 text-black shadow-sm dark:bg-white/[0.14] dark:text-fg'
|
||||
: 'text-neutral-500 hover:text-black dark:text-fg-dim dark:hover:text-fg'">
|
||||
Light
|
||||
</button>
|
||||
<button type="button" @click="setCustomMode('dark')" :aria-pressed="customMode === 'dark'"
|
||||
class="rounded-md py-1.5 text-xs font-medium transition-colors"
|
||||
:class="customMode === 'dark'
|
||||
? 'bg-neutral-100 text-black shadow-sm dark:bg-white/[0.14] dark:text-fg'
|
||||
: 'text-neutral-500 hover:text-black dark:text-fg-dim dark:hover:text-fg'">
|
||||
Dark
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -101,10 +101,9 @@ class="fixed z-9999 flex w-[calc(100%-2rem)] gap-2.5 sm:max-w-[26rem]"
|
|||
x-transition:enter-end="translate-y-0 opacity-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="translate-y-0 opacity-100"
|
||||
x-transition:leave-end="-translate-y-1 opacity-0"
|
||||
x-transition:leave-end="translate-y-1 opacity-0"
|
||||
@mouseenter="pauseToast(toast)" @mouseleave="resumeToast(toast)"
|
||||
class="relative flex w-full items-start rounded-lg group"
|
||||
style="background: var(--coollabs-elevated); box-shadow: 0 0 0 1px var(--coollabs-line), var(--shadow-modal);"
|
||||
class="surface-popover relative flex w-full items-start rounded-lg group"
|
||||
:class="{ 'p-3.5 pr-20': !toast.html, 'p-0': toast.html }">
|
||||
<template x-if="!toast.html">
|
||||
<div class="flex min-w-0 items-start gap-3">
|
||||
|
|
|
|||
|
|
@ -11,10 +11,6 @@
|
|||
<div @class(['relative', 'min-w-0' => $sidebar]) x-data="{
|
||||
open: false,
|
||||
appearanceOpen: false,
|
||||
theme: localStorage.getItem('theme') === 'purple' ? 'custom' : (localStorage.getItem('theme') || 'dark'),
|
||||
pageWidth: localStorage.getItem('pageWidth') || 'full',
|
||||
themeColor: localStorage.getItem('themeColor') || '#6b16ed',
|
||||
themeColorFrame: null,
|
||||
avatarUrl: @js($user?->avatar_path ? profile_avatar_url($user) : null),
|
||||
openPanel() {
|
||||
this.appearanceOpen = false;
|
||||
|
|
@ -23,51 +19,6 @@
|
|||
closePanel() {
|
||||
this.open = false;
|
||||
},
|
||||
setTheme(type, closeMenu = true) {
|
||||
this.theme = type;
|
||||
localStorage.setItem('theme', type);
|
||||
|
||||
if (closeMenu) {
|
||||
this.closePanel();
|
||||
}
|
||||
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
const isDark = type === 'dark' || type === 'custom' || (type === 'system' && prefersDark);
|
||||
document.documentElement.classList.toggle('dark', isDark);
|
||||
document.documentElement.dataset.theme = type === 'custom' ? 'custom' : (isDark ? 'dark' : 'light');
|
||||
document.documentElement.style.setProperty('--theme-base-color', localStorage.themeColor || '#6b16ed');
|
||||
document.documentElement.style.setProperty('--theme-accent-foreground', window.themeAccentForeground(this.themeColor));
|
||||
document.querySelector('meta[name=theme-color]')?.setAttribute('content', isDark ? '#101010' : '#ffffff');
|
||||
},
|
||||
setWidth(width) {
|
||||
this.pageWidth = width;
|
||||
localStorage.setItem('pageWidth', width);
|
||||
window.dispatchEvent(new CustomEvent('page-width-changed', { detail: width }));
|
||||
},
|
||||
previewThemeColor(color) {
|
||||
this.themeColor = color;
|
||||
|
||||
if (this.theme !== 'custom') {
|
||||
this.theme = 'custom';
|
||||
document.documentElement.classList.add('dark');
|
||||
document.documentElement.dataset.theme = 'custom';
|
||||
}
|
||||
|
||||
if (this.themeColorFrame) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.themeColorFrame = requestAnimationFrame(() => {
|
||||
document.documentElement.style.setProperty('--theme-base-color', this.themeColor);
|
||||
document.documentElement.style.setProperty('--theme-accent-foreground', window.themeAccentForeground(this.themeColor));
|
||||
this.themeColorFrame = null;
|
||||
});
|
||||
},
|
||||
saveThemeColor(color) {
|
||||
this.previewThemeColor(color);
|
||||
localStorage.setItem('themeColor', color);
|
||||
localStorage.setItem('theme', 'custom');
|
||||
},
|
||||
}" @avatar-updated.window="avatarUrl = $event.detail.url" @keydown.escape.window="closePanel()"
|
||||
@click.outside="closePanel()">
|
||||
<button type="button" @click="open ? closePanel() : openPanel()"
|
||||
|
|
@ -93,8 +44,13 @@ class="flex size-5 shrink-0 items-center justify-center rounded-full bg-neutral-
|
|||
</svg>
|
||||
</button>
|
||||
|
||||
<div x-show="open" x-cloak @class([
|
||||
'top-user-menu-panel listbox-panel z-[90]! max-h-none! w-52! min-w-0! overflow-visible! animate-in fade-in zoom-in-95 duration-150',
|
||||
<div x-show="open" x-cloak
|
||||
x-transition:enter="transition ease-out duration-150"
|
||||
x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-100"
|
||||
x-transition:leave-start="opacity-100 scale-100" x-transition:leave-end="opacity-0 scale-95"
|
||||
@class([
|
||||
'top-user-menu-panel listbox-panel z-[90]! max-h-none! w-52! min-w-0! overflow-visible!',
|
||||
'right-0! left-auto!' => ! $sidebar,
|
||||
'bottom-full! left-0! right-auto! top-auto! mb-1!' => $sidebar,
|
||||
'origin-bottom-left' => $sidebar,
|
||||
|
|
@ -124,65 +80,26 @@ class="flex size-5 shrink-0 items-center justify-center rounded-full bg-neutral-
|
|||
stroke-linejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
<div x-show="appearanceOpen" x-collapse class="mx-1 grid gap-0.5 pb-1 pl-6">
|
||||
@foreach ([
|
||||
['value' => 'light', 'label' => 'Light'],
|
||||
['value' => 'system', 'label' => 'System'],
|
||||
['value' => 'dark', 'label' => 'Dark'],
|
||||
['value' => 'custom', 'label' => 'Custom'],
|
||||
] as $option)
|
||||
@if ($option['value'] === 'custom')
|
||||
<div
|
||||
class="relative flex h-8 w-full items-center justify-between rounded-md px-2 text-left text-xs text-neutral-600 transition-colors hover:bg-neutral-200 hover:text-neutral-950 dark:text-fg-dim dark:hover:bg-white/[0.06] dark:hover:text-fg">
|
||||
<span class="flex items-center gap-2">
|
||||
<span class="size-3.5 rounded-full border border-white/20"
|
||||
:style="`background: ${themeColor}`"></span>
|
||||
Custom
|
||||
</span>
|
||||
<svg x-show="theme === 'custom'" class="size-3.5 text-coollabs dark:text-warning"
|
||||
viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
||||
<path d="m2.5 6.25 2.1 2.1 4.9-5" stroke="currentColor" stroke-width="1.4"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
<input type="color" :value="themeColor" @input="previewThemeColor($event.target.value)"
|
||||
@change="saveThemeColor($event.target.value)"
|
||||
aria-label="Custom theme color"
|
||||
class="absolute inset-0 z-10 h-full w-full cursor-pointer opacity-0" />
|
||||
</div>
|
||||
@else
|
||||
<button type="button" @click="setTheme('{{ $option['value'] }}')"
|
||||
class="flex h-8 w-full items-center justify-between rounded-md px-2 text-left text-xs text-neutral-600 transition-colors hover:bg-neutral-200 hover:text-neutral-950 dark:text-fg-dim dark:hover:bg-white/[0.06] dark:hover:text-fg">
|
||||
<span>{{ $option['label'] }}</span>
|
||||
<svg x-show="theme === '{{ $option['value'] }}'"
|
||||
class="size-3.5 text-coollabs dark:text-warning" viewBox="0 0 12 12" fill="none"
|
||||
aria-hidden="true">
|
||||
<path d="m2.5 6.25 2.1 2.1 4.9-5" stroke="currentColor" stroke-width="1.4"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
@endif
|
||||
@endforeach
|
||||
<div class="my-1 h-px bg-neutral-200 dark:bg-white/[0.07]"></div>
|
||||
<div class="px-2 pt-1 pb-0.5 text-[10px] font-medium tracking-wide text-neutral-400 uppercase dark:text-fg-faint">
|
||||
Page width
|
||||
</div>
|
||||
@foreach ([
|
||||
['value' => 'full', 'label' => 'Full width'],
|
||||
['value' => 'centered', 'label' => 'Centered'],
|
||||
] as $option)
|
||||
<button type="button" @click="setWidth('{{ $option['value'] }}')"
|
||||
class="flex h-8 w-full items-center justify-between rounded-md px-2 text-left text-xs text-neutral-600 transition-colors hover:bg-neutral-200 hover:text-neutral-950 dark:text-fg-dim dark:hover:bg-white/[0.06] dark:hover:text-fg">
|
||||
<span>{{ $option['label'] }}</span>
|
||||
<svg x-show="pageWidth === '{{ $option['value'] }}'"
|
||||
class="size-3.5 text-coollabs dark:text-warning" viewBox="0 0 12 12" fill="none"
|
||||
aria-hidden="true">
|
||||
<path d="m2.5 6.25 2.1 2.1 4.9-5" stroke="currentColor" stroke-width="1.4"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
@endforeach
|
||||
<div x-show="appearanceOpen" x-collapse.duration.200ms class="mx-1 pb-1 pl-6">
|
||||
<x-theme-controls variant="menu" />
|
||||
</div>
|
||||
|
||||
<button type="button" class="listbox-option w-full" @click="toggleAutoCollapse()"
|
||||
:aria-pressed="autoCollapse" title="Collapse the sidebar on pages that have a settings menu">
|
||||
<span class="flex items-center gap-2">
|
||||
<svg class="size-4 opacity-80" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<rect x="3" y="4" width="18" height="16" rx="2" stroke="currentColor" stroke-width="1.6" />
|
||||
<path d="M9 4v16" stroke="currentColor" stroke-width="1.6" />
|
||||
</svg>
|
||||
Auto-collapse sidebar
|
||||
</span>
|
||||
<svg x-show="autoCollapse" x-cloak class="size-4 shrink-0 text-black dark:text-fg" viewBox="0 0 24 24"
|
||||
fill="none" aria-hidden="true">
|
||||
<path d="M5 12l5 5L20 7" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div class="my-1 h-px bg-neutral-200 dark:bg-white/[0.07]"></div>
|
||||
|
||||
<livewire:settings-dropdown trigger="account-menu" />
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -9,21 +9,48 @@
|
|||
@auth
|
||||
<div x-data="{
|
||||
open: false,
|
||||
collapsed: localStorage.getItem('sidebarCollapsed') === 'true',
|
||||
userCollapsed: localStorage.getItem('sidebarCollapsed') === 'true',
|
||||
autoCollapse: localStorage.getItem('sidebarAutoCollapse') !== 'false',
|
||||
hasSecondBar: false,
|
||||
collapsed: false,
|
||||
pageWidth: localStorage.getItem('pageWidth') || 'full',
|
||||
sidebarReady: false,
|
||||
init() {
|
||||
this.applyCollapsed(false);
|
||||
this.$nextTick(() => {
|
||||
requestAnimationFrame(() => {
|
||||
this.sidebarReady = true;
|
||||
});
|
||||
});
|
||||
},
|
||||
targetCollapsed() {
|
||||
this.hasSecondBar = !!document.querySelector('.application-settings-navigation');
|
||||
return this.userCollapsed || (this.autoCollapse && this.hasSecondBar);
|
||||
},
|
||||
applyCollapsed(animate) {
|
||||
const target = this.targetCollapsed();
|
||||
if (target === this.collapsed) return;
|
||||
if (animate) {
|
||||
// Let the new page paint at the current width, then animate the
|
||||
// slide a frame later so the auto-collapse reads as a motion.
|
||||
this.sidebarReady = true;
|
||||
requestAnimationFrame(() => requestAnimationFrame(() => { this.collapsed = target; }));
|
||||
} else {
|
||||
this.collapsed = target;
|
||||
}
|
||||
},
|
||||
toggleSidebar() {
|
||||
this.collapsed = !this.collapsed;
|
||||
localStorage.setItem('sidebarCollapsed', this.collapsed);
|
||||
this.userCollapsed = this.collapsed;
|
||||
localStorage.setItem('sidebarCollapsed', this.userCollapsed);
|
||||
},
|
||||
toggleAutoCollapse() {
|
||||
this.autoCollapse = !this.autoCollapse;
|
||||
localStorage.setItem('sidebarAutoCollapse', this.autoCollapse);
|
||||
this.applyCollapsed(true);
|
||||
}
|
||||
}" @open-global-search.window="open = false" @page-width-changed.window="pageWidth = $event.detail" x-cloak
|
||||
}" @open-global-search.window="open = false" @page-width-changed.window="pageWidth = $event.detail" x-on:livewire:navigated.window="applyCollapsed(true)"
|
||||
:style="{ '--sidebar-w': collapsed ? '4rem' : '14rem' }" x-cloak
|
||||
class="dark:text-inherit text-black">
|
||||
<livewire:deployments-indicator />
|
||||
|
||||
|
|
@ -69,29 +96,42 @@ class="flex h-full items-center gap-0.5 min-w-0 flex-1 border-b border-neutral-2
|
|||
</div>
|
||||
</header>
|
||||
|
||||
{{-- ============ MOBILE SLIDE-OVER SIDEBAR ============ --}}
|
||||
<div class="relative z-[1000] lg:hidden" :class="open ? 'block' : 'hidden'" role="dialog" aria-modal="true">
|
||||
<div class="fixed inset-0 bg-black/80" x-on:click="open = false"></div>
|
||||
<div class="fixed inset-y-0 right-0 flex h-full">
|
||||
<div
|
||||
class="relative flex h-full w-full max-w-56 min-w-0 flex-col border-l border-neutral-200 bg-white shadow-xl dark:border-white/[0.12] dark:bg-panel">
|
||||
<div class="absolute top-0 right-full flex w-16 justify-center pt-5">
|
||||
<button type="button" class="-m-2.5 p-2.5" x-on:click="open = !open">
|
||||
<span class="sr-only">Close sidebar</span>
|
||||
<svg class="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
{{-- ============ MOBILE SLIDE-OVER SIDEBAR (shadcn-style sheet) ============ --}}
|
||||
<div class="mobile-sidebar-sheet relative z-[1000] lg:hidden"
|
||||
x-on:keydown.escape.window="open = false"
|
||||
x-on:livewire:navigated.window="open = false">
|
||||
{{-- Scrim: fades in/out --}}
|
||||
<div class="fixed inset-0 bg-black/50" x-show="open" x-cloak x-on:click="open = false"
|
||||
x-transition:enter="transition-opacity ease-out duration-300"
|
||||
x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition-opacity ease-in duration-200"
|
||||
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0"></div>
|
||||
{{-- Panel: slides in from the right (iOS drawer curve), exits faster --}}
|
||||
<div class="fixed inset-y-0 right-0 flex" :class="!open && 'pointer-events-none'">
|
||||
<div x-show="open" x-cloak x-trap.inert.noscroll="open"
|
||||
role="dialog" aria-modal="true" aria-label="Navigation menu"
|
||||
x-transition:enter="transform transition ease-[cubic-bezier(0.32,0.72,0,1)] duration-300"
|
||||
x-transition:enter-start="translate-x-full" x-transition:enter-end="translate-x-0"
|
||||
x-transition:leave="transform transition ease-in duration-200"
|
||||
x-transition:leave-start="translate-x-0" x-transition:leave-end="translate-x-full"
|
||||
class="relative flex h-full w-72 max-w-[85vw] min-w-0 flex-col overflow-hidden rounded-l-2xl border-l border-neutral-200 bg-white shadow-[-8px_0_30px_-6px_rgba(0,0,0,0.18)] dark:border-white/[0.12] dark:bg-panel dark:shadow-[-8px_0_30px_-4px_rgba(0,0,0,0.5)]">
|
||||
<div data-mobile-sidebar-brand
|
||||
class="flex h-12 shrink-0 items-center justify-between gap-1.5 border-b border-neutral-200 px-4 dark:border-white/[0.06]">
|
||||
<div class="flex min-w-0 items-baseline gap-1.5">
|
||||
<a href="/" {{ wireNavigate() }} title="Coolify"
|
||||
class="text-[15px] font-semibold tracking-tight text-black transition-opacity hover:opacity-80 dark:text-white">
|
||||
Coolify
|
||||
</a>
|
||||
<x-version class="!text-[10.5px] font-medium text-neutral-400 dark:text-fg-faint !opacity-100 hover:!opacity-100 hover:text-black dark:hover:text-fg" />
|
||||
</div>
|
||||
<button type="button" x-on:click="open = false" aria-label="Close menu"
|
||||
class="-mr-1.5 flex size-8 shrink-0 items-center justify-center rounded-md text-neutral-500 transition-colors hover:bg-neutral-100 hover:text-black active:scale-95 dark:text-fg-dim dark:hover:bg-white/[0.06] dark:hover:text-fg">
|
||||
<svg class="size-5" fill="none" viewBox="0 0 24 24" stroke-width="1.75"
|
||||
stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div data-mobile-sidebar-brand
|
||||
class="flex h-12 shrink-0 items-center gap-1.5 border-b border-neutral-200 px-4 dark:border-white/[0.06]">
|
||||
<a href="/" {{ wireNavigate() }} title="Coolify"
|
||||
class="text-[15px] font-semibold tracking-tight text-black transition-opacity hover:opacity-80 dark:text-white">
|
||||
Coolify
|
||||
</a>
|
||||
<x-version class="!text-[10.5px] font-medium text-neutral-400 dark:text-fg-faint !opacity-100 hover:!opacity-100 hover:text-black dark:hover:text-fg" />
|
||||
</div>
|
||||
<div class="flex min-h-0 min-w-0 flex-1 flex-col overflow-y-auto pb-2 scrollbar">
|
||||
<x-navbar />
|
||||
</div>
|
||||
|
|
@ -128,11 +168,12 @@ class="hidden shrink-0 items-center"></div>
|
|||
<livewire:upgrade key="mobile-upgrade" />
|
||||
@endif
|
||||
<x-top-user-menu />
|
||||
<button type="button" class="-m-1 p-2 text-neutral-500 dark:text-fg-dim" x-on:click="open = !open">
|
||||
<button type="button" x-on:click="open = !open"
|
||||
class="-mr-1 flex size-9 items-center justify-center rounded-md text-neutral-500 transition-transform duration-100 ease-out hover:bg-neutral-100 hover:text-black active:scale-90 dark:text-fg-dim dark:hover:bg-white/[0.06] dark:hover:text-fg">
|
||||
<span class="sr-only">Open sidebar</span>
|
||||
<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
||||
<svg class="size-5" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<rect x="3" y="4" width="18" height="16" rx="2" stroke="currentColor" stroke-width="1.6" />
|
||||
<path d="M9 4v16" stroke="currentColor" stroke-width="1.6" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -140,7 +181,7 @@ class="hidden shrink-0 items-center"></div>
|
|||
|
||||
{{-- ============ MAIN ============ --}}
|
||||
<main
|
||||
class="min-h-screen bg-white dark:bg-panel px-5 py-6 sm:px-8 lg:px-10 lg:pt-[calc(3rem+1.75rem)] lg:pb-10"
|
||||
class="min-h-screen bg-neutral-50 dark:bg-app px-5 py-6 sm:px-8 lg:px-10 lg:pt-[calc(3rem+1.75rem)] lg:pb-10"
|
||||
:class="[collapsed ? 'lg:ml-16' : 'lg:ml-56', sidebarReady ? 'transition-[margin] duration-200' : '']">
|
||||
<div class="w-full" :class="pageWidth === 'centered' ? 'mx-auto max-w-[1400px]' : 'max-w-none'">
|
||||
{{ $slot }}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 @@ class="flex size-8 shrink-0 items-center justify-center rounded-lg border border
|
|||
Continue
|
||||
</x-forms.button>
|
||||
<div
|
||||
class="inline-flex flex-wrap items-center justify-center gap-0.5 rounded-lg border border-neutral-200 bg-neutral-50 p-0.5 dark:border-white/[0.08] dark:bg-white/[0.025]">
|
||||
class="inline-flex flex-wrap items-center justify-center gap-0.5 rounded-lg border border-neutral-200 bg-neutral-50 p-0.5 dark:border-white/[0.08] dark:bg-white/[0.05]">
|
||||
<button type="button" wire:click="skipBoarding"
|
||||
class="inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-[12px] font-medium text-neutral-500 transition-colors hover:bg-white hover:text-coollabs dark:text-fg-dim dark:hover:bg-white/[0.06] dark:hover:text-warning">
|
||||
<x-reicon name="arrow-right" class="size-3.5 shrink-0" />
|
||||
|
|
@ -101,7 +101,7 @@ class="inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-[12px] font-
|
|||
<p class="mb-3 text-sm text-neutral-500 dark:text-neutral-400">Use this machine or connect a server you already manage.</p>
|
||||
<div class="grid w-full grid-cols-1 gap-4 lg:grid-cols-2">
|
||||
<button
|
||||
class="group relative cursor-pointer min-h-36 rounded-[10px] border border-neutral-200 bg-white p-4 text-left shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]"
|
||||
class="group relative cursor-pointer min-h-36 rounded-[10px] border border-neutral-200 bg-white p-4 text-left shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]"
|
||||
wire:target="setServerType('localhost')" wire:click="setServerType('localhost')">
|
||||
<span role="button" tabindex="0" aria-label="About this machine"
|
||||
data-tooltip="The machine running Coolify. Not recommended for production workloads due to resource contention."
|
||||
|
|
@ -125,7 +125,7 @@ class="absolute top-3 right-3 flex size-6 items-center justify-center rounded-fu
|
|||
|
||||
|
||||
<button
|
||||
class="group relative cursor-pointer min-h-36 rounded-[10px] border border-neutral-200 bg-white p-4 text-left shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]"
|
||||
class="group relative cursor-pointer min-h-36 rounded-[10px] border border-neutral-200 bg-white p-4 text-left shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]"
|
||||
wire:target="setServerType('remote')" wire:click="setServerType('remote')">
|
||||
<span role="button" tabindex="0" aria-label="About remote servers"
|
||||
data-tooltip="Any SSH-accessible server, including cloud VPS, bare metal, and self-hosted infrastructure."
|
||||
|
|
@ -157,7 +157,7 @@ class="absolute top-3 right-3 flex size-6 items-center justify-center rounded-fu
|
|||
<x-modal-input title="Connect a Hetzner Server" isFullWidth>
|
||||
<x-slot:content>
|
||||
<div
|
||||
class="group relative cursor-pointer flex h-full min-h-36 flex-col rounded-[10px] border border-neutral-200 bg-white p-4 text-left shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]">
|
||||
class="group relative cursor-pointer flex h-full min-h-36 flex-col rounded-[10px] border border-neutral-200 bg-white p-4 text-left shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]">
|
||||
<div class="flex h-full flex-col gap-4 text-left">
|
||||
<img src="{{ asset('svgs/hetzner.svg') }}" alt="Hetzner"
|
||||
class="size-10 shrink-0">
|
||||
|
|
@ -175,7 +175,7 @@ class="size-10 shrink-0">
|
|||
<x-modal-input title="Connect a Vultr Server" isFullWidth>
|
||||
<x-slot:content>
|
||||
<div
|
||||
class="group relative cursor-pointer flex h-full min-h-36 flex-col rounded-[10px] border border-neutral-200 bg-white p-4 text-left shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]">
|
||||
class="group relative cursor-pointer flex h-full min-h-36 flex-col rounded-[10px] border border-neutral-200 bg-white p-4 text-left shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]">
|
||||
<div class="flex h-full flex-col gap-4 text-left">
|
||||
<img src="https://www.vultr.com/media/logo_ondark.svg" alt="Vultr"
|
||||
class="h-10 w-28 shrink-0 object-contain object-left">
|
||||
|
|
@ -193,7 +193,7 @@ class="h-10 w-28 shrink-0 object-contain object-left">
|
|||
<x-modal-input title="Connect a DigitalOcean Server" isFullWidth>
|
||||
<x-slot:content>
|
||||
<div
|
||||
class="group relative cursor-pointer flex h-full min-h-36 flex-col rounded-[10px] border border-neutral-200 bg-white p-4 text-left shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]">
|
||||
class="group relative cursor-pointer flex h-full min-h-36 flex-col rounded-[10px] border border-neutral-200 bg-white p-4 text-left shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]">
|
||||
<div class="flex h-full flex-col gap-4 text-left">
|
||||
<x-digital-ocean-icon class="size-10 shrink-0" />
|
||||
<div class="min-h-0 flex-1">
|
||||
|
|
@ -279,7 +279,7 @@ class="bg-red-200 dark:bg-red-900 px-1 rounded-sm">~/.ssh/authorized_keys</code>
|
|||
@endphp
|
||||
<div class="w-full space-y-4">
|
||||
<div
|
||||
class="rounded-[10px] border border-neutral-200 bg-neutral-50 p-4 dark:border-white/[0.08] dark:bg-white/[0.025]">
|
||||
class="rounded-[10px] border border-neutral-200 bg-neutral-50 p-4 dark:border-white/[0.08] dark:bg-white/[0.05]">
|
||||
<form wire:submit="selectExistingPrivateKey"
|
||||
class="flex flex-col gap-3 sm:flex-row sm:items-end">
|
||||
<div class="min-w-0 flex-1">
|
||||
|
|
@ -304,7 +304,7 @@ class="bg-[var(--coollabs-base)] px-2.5 text-[10px] font-semibold uppercase trac
|
|||
@endif
|
||||
<div class="grid w-full grid-cols-1 gap-3 lg:grid-cols-2">
|
||||
<button type="button"
|
||||
class="group flex h-full min-h-28 items-start gap-3 rounded-[10px] border border-neutral-200 bg-white p-4 text-left shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]"
|
||||
class="group flex h-full min-h-28 items-start gap-3 rounded-[10px] border border-neutral-200 bg-white p-4 text-left shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]"
|
||||
wire:target="setPrivateKey('own')" wire:click="setPrivateKey('own')">
|
||||
<span
|
||||
class="flex size-9 shrink-0 items-center justify-center rounded-lg border border-neutral-200 bg-neutral-50 text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.035] dark:text-fg-dim">
|
||||
|
|
@ -318,7 +318,7 @@ class="flex size-9 shrink-0 items-center justify-center rounded-lg border border
|
|||
</span>
|
||||
</button>
|
||||
<button type="button"
|
||||
class="group flex h-full min-h-28 items-start gap-3 rounded-[10px] border border-neutral-200 bg-white p-4 text-left shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]"
|
||||
class="group flex h-full min-h-28 items-start gap-3 rounded-[10px] border border-neutral-200 bg-white p-4 text-left shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]"
|
||||
wire:target="setPrivateKey('create')" wire:click="setPrivateKey('create')">
|
||||
<span
|
||||
class="flex size-9 shrink-0 items-center justify-center rounded-lg border border-neutral-200 bg-neutral-50 text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.035] dark:text-fg-dim">
|
||||
|
|
@ -597,7 +597,7 @@ class="text-[10px] font-semibold uppercase tracking-[0.08em] text-neutral-400 da
|
|||
Deploy your first resource
|
||||
</x-forms.button>
|
||||
<div
|
||||
class="inline-flex flex-wrap items-center justify-center gap-0.5 rounded-lg border border-neutral-200 bg-neutral-50 p-0.5 dark:border-white/[0.08] dark:bg-white/[0.025]">
|
||||
class="inline-flex flex-wrap items-center justify-center gap-0.5 rounded-lg border border-neutral-200 bg-neutral-50 p-0.5 dark:border-white/[0.08] dark:bg-white/[0.05]">
|
||||
<button type="button" wire:click="skipBoarding"
|
||||
class="inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-[12px] font-medium text-neutral-500 transition-colors hover:bg-white hover:text-coollabs dark:text-fg-dim dark:hover:bg-white/[0.06] dark:hover:text-warning">
|
||||
<x-reicon name="arrow-right" class="size-3.5 shrink-0" />
|
||||
|
|
@ -622,7 +622,7 @@ class="inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-[12px] font-
|
|||
@if ($currentState !== 'welcome' && $currentState !== 'create-resource')
|
||||
<div class="mx-auto mt-6 flex w-full max-w-3xl flex-col items-center gap-3">
|
||||
<div
|
||||
class="inline-flex flex-wrap items-center justify-center gap-0.5 rounded-lg border border-neutral-200 bg-neutral-50 p-0.5 dark:border-white/[0.08] dark:bg-white/[0.025]">
|
||||
class="inline-flex flex-wrap items-center justify-center gap-0.5 rounded-lg border border-neutral-200 bg-neutral-50 p-0.5 dark:border-white/[0.08] dark:bg-white/[0.05]">
|
||||
<button type="button" wire:click="skipBoarding"
|
||||
class="inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-[12px] font-medium text-neutral-500 transition-colors hover:bg-white hover:text-coollabs dark:text-fg-dim dark:hover:bg-white/[0.06] dark:hover:text-warning">
|
||||
<x-reicon name="arrow-right" class="size-3.5 shrink-0" />
|
||||
|
|
|
|||
|
|
@ -17,21 +17,8 @@
|
|||
<livewire:dashboard.active-deployments />
|
||||
|
||||
<section class="mb-0! min-w-0">
|
||||
<div class="mb-3 flex items-end justify-between gap-4">
|
||||
<div>
|
||||
<h2 class="text-[14px]! leading-5! font-semibold! text-black dark:text-fg">
|
||||
Projects
|
||||
</h2>
|
||||
<p class="mt-0.5 text-[11px] text-neutral-500 dark:text-fg-faint">
|
||||
Your deployment workspaces
|
||||
</p>
|
||||
</div>
|
||||
<a href="{{ route('project.index') }}" {{ wireNavigate() }}
|
||||
class="inline-flex shrink-0 items-center gap-1 text-[12px] font-medium text-neutral-500 transition-colors hover:text-black dark:text-fg-dim dark:hover:text-fg">
|
||||
View all
|
||||
<x-reicon name="arrow-right" class="size-3" />
|
||||
</a>
|
||||
</div>
|
||||
<x-section-heading title="Projects" subtitle="Your deployment workspaces"
|
||||
:href="route('project.index')" />
|
||||
|
||||
@if ($dashboardProjects->isEmpty())
|
||||
<x-empty title="No projects yet"
|
||||
|
|
@ -57,7 +44,7 @@ class="inline-flex shrink-0 items-center gap-1 text-[12px] font-medium text-neut
|
|||
@endphp
|
||||
|
||||
<article
|
||||
class="group relative flex min-h-28 min-w-0 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]">
|
||||
class="group relative flex min-h-28 min-w-0 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]">
|
||||
<a href="{{ $project->navigateTo() }}" {{ wireNavigate() }}
|
||||
class="absolute inset-0 rounded-xl"
|
||||
aria-label="Open {{ $project->name }}"></a>
|
||||
|
|
@ -125,21 +112,8 @@ class="flex size-6.5 items-center justify-center rounded-md text-neutral-400 tra
|
|||
</section>
|
||||
|
||||
<section class="mb-0! min-w-0">
|
||||
<div class="mb-3 flex items-end justify-between gap-4">
|
||||
<div>
|
||||
<h2 class="text-[14px]! leading-5! font-semibold! text-black dark:text-fg">
|
||||
Servers
|
||||
</h2>
|
||||
<p class="mt-0.5 text-[11px] text-neutral-500 dark:text-fg-faint">
|
||||
Infrastructure available for deployments
|
||||
</p>
|
||||
</div>
|
||||
<a href="{{ route('server.index') }}" {{ wireNavigate() }}
|
||||
class="inline-flex shrink-0 items-center gap-1 text-[12px] font-medium text-neutral-500 transition-colors hover:text-black dark:text-fg-dim dark:hover:text-fg">
|
||||
View all
|
||||
<x-reicon name="arrow-right" class="size-3" />
|
||||
</a>
|
||||
</div>
|
||||
<x-section-heading title="Servers" subtitle="Infrastructure available for deployments"
|
||||
:href="route('server.index')" />
|
||||
|
||||
@if ($dashboardServers->isEmpty())
|
||||
@if ($privateKeys->isEmpty())
|
||||
|
|
@ -190,7 +164,7 @@ class="button button-highlighted">
|
|||
|
||||
<a href="{{ route('server.show', ['server_uuid' => $server->uuid]) }}"
|
||||
{{ wireNavigate() }} aria-label="Open {{ $server->name }}"
|
||||
class="group relative flex min-h-28 min-w-0 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]">
|
||||
class="group relative flex min-h-28 min-w-0 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]">
|
||||
@if ($server->isMetricsEnabled())
|
||||
<livewire:dashboard.server-metrics-chart :server="$server"
|
||||
:key="'dashboard-server-metrics-'.$server->uuid" />
|
||||
|
|
|
|||
|
|
@ -24,24 +24,15 @@
|
|||
$hasAnyDeployments = $hasActiveDeployments || $hasRecentDeployments;
|
||||
@endphp
|
||||
|
||||
<div wire:poll.3000ms="refreshDeployments" @class(['mb-0! min-w-0' => $hasAnyDeployments])>
|
||||
<div wire:poll.3000ms="refreshDeployments" @class(['mb-0! min-w-0' => $hasAnyDeployments, 'hidden' => ! $hasAnyDeployments])>
|
||||
@if ($hasAnyDeployments)
|
||||
<section class="mb-0! min-w-0">
|
||||
<div class="mb-3">
|
||||
<div>
|
||||
<h2 class="text-[14px]! leading-5! font-semibold! text-black dark:text-fg">
|
||||
Deployments
|
||||
</h2>
|
||||
<p class="mt-0.5 text-[11px] text-neutral-500 dark:text-fg-faint">
|
||||
Active and recent deployment activity
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<x-section-heading title="Deployments" subtitle="Active and recent deployment activity" />
|
||||
|
||||
<div class="flex min-w-0 flex-col gap-4">
|
||||
@if ($hasActiveDeployments)
|
||||
<div
|
||||
class="overflow-hidden rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.025]">
|
||||
class="overflow-hidden rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.05]">
|
||||
<div class="border-b border-neutral-200 px-4 py-2.5 dark:border-white/[0.08]">
|
||||
<h3 class="text-[12px]! leading-4! font-semibold! text-black dark:text-fg">
|
||||
Active
|
||||
|
|
@ -52,7 +43,7 @@ class="overflow-hidden rounded-xl border border-neutral-200 bg-white shadow-sm d
|
|||
</div>
|
||||
|
||||
<div
|
||||
class="dashboard-deployment-table-grid hidden items-center gap-4 border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 md:grid dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-faint">
|
||||
class="dashboard-deployment-table-grid hidden items-center gap-4 border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 md:grid dark:border-white/[0.08] dark:bg-white/[0.05] dark:text-fg-faint">
|
||||
<span>Application</span>
|
||||
<span>Environment</span>
|
||||
<span>Server</span>
|
||||
|
|
@ -110,7 +101,7 @@ class="dashboard-deployment-table-grid group block border-b border-neutral-200 p
|
|||
|
||||
@if ($hasRecentDeployments)
|
||||
<div
|
||||
class="overflow-hidden rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.025]">
|
||||
class="overflow-hidden rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.05]">
|
||||
<div class="border-b border-neutral-200 px-4 py-2.5 dark:border-white/[0.08]">
|
||||
<h3 class="text-[12px]! leading-4! font-semibold! text-black dark:text-fg">
|
||||
Recent
|
||||
|
|
@ -121,7 +112,7 @@ class="overflow-hidden rounded-xl border border-neutral-200 bg-white shadow-sm d
|
|||
</div>
|
||||
|
||||
<div
|
||||
class="dashboard-deployment-table-grid hidden items-center gap-4 border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 md:grid dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-faint">
|
||||
class="dashboard-deployment-table-grid hidden items-center gap-4 border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 md:grid dark:border-white/[0.08] dark:bg-white/[0.05] dark:text-fg-faint">
|
||||
<span>Application</span>
|
||||
<span>Environment</span>
|
||||
<span>Server</span>
|
||||
|
|
|
|||
|
|
@ -11,8 +11,7 @@
|
|||
x-transition:enter-start="translate-y-2" x-transition:enter-end="translate-y-0"
|
||||
x-transition:leave="transition ease-in duration-150" x-transition:leave-start="translate-y-0"
|
||||
x-transition:leave-end="translate-y-2" x-cloak
|
||||
class="absolute bottom-full mb-2 w-[min(22rem,calc(100vw-2rem))] overflow-hidden rounded-xl"
|
||||
style="background: var(--coollabs-elevated); box-shadow: 0 0 0 1px var(--coollabs-line), var(--shadow-modal);">
|
||||
class="surface-popover absolute bottom-full mb-2 w-[min(22rem,calc(100vw-2rem))] overflow-hidden rounded-xl">
|
||||
<div class="max-h-96 space-y-1 overflow-y-auto p-2 scrollbar">
|
||||
@foreach ($this->deployments as $deployment)
|
||||
@php
|
||||
|
|
@ -71,8 +70,7 @@ class="shrink-0" />
|
|||
|
||||
{{-- Collapsed pill --}}
|
||||
<button type="button" @click="expanded = !expanded"
|
||||
class="flex items-center gap-2 rounded-xl border border-neutral-200 bg-white px-3.5 py-2 text-sm font-medium text-neutral-800 transition-colors hover:bg-neutral-50 dark:border-coolgray-300 dark:bg-surface dark:text-fg dark:hover:bg-raised"
|
||||
style="box-shadow: 0 0 0 1px var(--coollabs-hairline), var(--shadow-modal);"
|
||||
class="flex items-center gap-2 rounded-xl border border-neutral-200 bg-white px-3.5 py-2 text-sm font-medium text-neutral-800 shadow-dropdown transition-colors hover:bg-neutral-50 dark:border-coolgray-300 dark:bg-surface dark:text-fg dark:hover:bg-raised"
|
||||
:aria-expanded="expanded.toString()" aria-label="Active deployments">
|
||||
<svg class="loading-indicator size-3.5 shrink-0 animate-spin"
|
||||
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" aria-hidden="true">
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ class="button button-highlighted">
|
|||
|
||||
<div x-cloak x-show="viewMode === 'grid'" class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
@foreach ($destinations as $destination)
|
||||
<a x-show="matches(@js([$destination->name, $destination->server->name, $destination->getMorphClass() === 'App\\Models\\SwarmDocker' ? 'Docker Swarm' : 'Standalone Docker']))" class="group flex min-h-28 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]"
|
||||
<a x-show="matches(@js([$destination->name, $destination->server->name, $destination->getMorphClass() === 'App\\Models\\SwarmDocker' ? 'Docker Swarm' : 'Standalone Docker']))" class="group flex min-h-28 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]"
|
||||
{{ wireNavigate() }}
|
||||
href="{{ route('destination.show', ['destination_uuid' => data_get($destination, 'uuid')]) }}">
|
||||
<div class="flex items-start gap-3">
|
||||
|
|
@ -90,8 +90,8 @@ class="flex size-8 shrink-0 items-center justify-center rounded-lg border border
|
|||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
<div x-show="viewMode === 'table'" class="overflow-x-auto rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.025]">
|
||||
<div class="grid min-w-[620px] grid-cols-[minmax(0,1fr)_minmax(10rem,.7fr)_10rem] border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-faint">
|
||||
<div x-show="viewMode === 'table'" class="overflow-x-auto rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.05]">
|
||||
<div class="grid min-w-[620px] grid-cols-[minmax(0,1fr)_minmax(10rem,.7fr)_10rem] border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.05] dark:text-fg-faint">
|
||||
<div>Destination</div><div>Server</div><div>Type</div>
|
||||
</div>
|
||||
@foreach ($destinations as $destination)
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ class="h-8! w-full rounded-lg! border-neutral-200! bg-white! py-0! pr-3! pl-8! t
|
|||
|
||||
<div class="overflow-x-auto">
|
||||
<div
|
||||
class="grid min-w-[680px] grid-cols-[minmax(10rem,.8fr)_minmax(10rem,.8fr)_minmax(12rem,1fr)_8rem] border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-faint">
|
||||
class="grid min-w-[680px] grid-cols-[minmax(10rem,.8fr)_minmax(10rem,.8fr)_minmax(12rem,1fr)_8rem] border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.05] dark:text-fg-faint">
|
||||
<div>Project</div>
|
||||
<div>Environment</div>
|
||||
<div>Resource</div>
|
||||
|
|
|
|||
|
|
@ -751,7 +751,7 @@ class="relative flex min-h-full items-start justify-center p-4 sm:items-center">
|
|||
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
|
||||
x-transition:leave-end="opacity-0 -translate-y-2 sm:scale-95"
|
||||
class="{{ $createModalShell }}"
|
||||
style="box-shadow: 0 0 0 1px var(--coollabs-hairline), var(--shadow-modal)">
|
||||
style="box-shadow: 0 0 0 1px var(--coollabs-hairline), var(--shadow-dropdown)">
|
||||
<header class="flex-nowrap!">
|
||||
<h3 class="min-w-0 flex-1 truncate">New project</h3>
|
||||
<button type="button" @click="modalOpen=false" class="{{ $createModalClose }}">
|
||||
|
|
@ -794,7 +794,7 @@ class="relative flex min-h-full items-start justify-center p-4 sm:items-center">
|
|||
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
|
||||
x-transition:leave-end="opacity-0 -translate-y-2 sm:scale-95"
|
||||
class="{{ $createModalShell }}"
|
||||
style="box-shadow: 0 0 0 1px var(--coollabs-hairline), var(--shadow-modal)">
|
||||
style="box-shadow: 0 0 0 1px var(--coollabs-hairline), var(--shadow-dropdown)">
|
||||
<header class="flex-nowrap!">
|
||||
<h3 class="min-w-0 flex-1 truncate">New team</h3>
|
||||
<button type="button" @click="modalOpen=false" class="{{ $createModalClose }}">
|
||||
|
|
@ -837,7 +837,7 @@ class="relative flex min-h-full items-start justify-center p-4 sm:items-center">
|
|||
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
|
||||
x-transition:leave-end="opacity-0 -translate-y-2 sm:scale-95"
|
||||
class="{{ $createModalShell }}"
|
||||
style="box-shadow: 0 0 0 1px var(--coollabs-hairline), var(--shadow-modal)">
|
||||
style="box-shadow: 0 0 0 1px var(--coollabs-hairline), var(--shadow-dropdown)">
|
||||
<header class="flex-nowrap!">
|
||||
<h3 class="min-w-0 flex-1 truncate">New S3 storage</h3>
|
||||
<button type="button" @click="modalOpen=false" class="{{ $createModalClose }}">
|
||||
|
|
@ -880,7 +880,7 @@ class="relative flex min-h-full items-start justify-center p-4 sm:items-center">
|
|||
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
|
||||
x-transition:leave-end="opacity-0 -translate-y-2 sm:scale-95"
|
||||
class="{{ $createModalShell }}"
|
||||
style="box-shadow: 0 0 0 1px var(--coollabs-hairline), var(--shadow-modal)">
|
||||
style="box-shadow: 0 0 0 1px var(--coollabs-hairline), var(--shadow-dropdown)">
|
||||
<header class="flex-nowrap!">
|
||||
<h3 class="min-w-0 flex-1 truncate">New private key</h3>
|
||||
<button type="button" @click="modalOpen=false" class="{{ $createModalClose }}">
|
||||
|
|
@ -923,7 +923,7 @@ class="relative flex min-h-full items-start justify-center p-4 sm:items-center">
|
|||
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
|
||||
x-transition:leave-end="opacity-0 -translate-y-2 sm:scale-95"
|
||||
class="{{ $createModalShell }}"
|
||||
style="box-shadow: 0 0 0 1px var(--coollabs-hairline), var(--shadow-modal)">
|
||||
style="box-shadow: 0 0 0 1px var(--coollabs-hairline), var(--shadow-dropdown)">
|
||||
<header class="flex-nowrap!">
|
||||
<h3 class="min-w-0 flex-1 truncate">New GitHub app</h3>
|
||||
<button type="button" @click="modalOpen=false" class="{{ $createModalClose }}">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -275,7 +275,7 @@ class="flex items-end gap-2">
|
|||
@if (session('status') === 'two-factor-authentication-confirmed'
|
||||
|| session('status') === 'recovery-codes-generated')
|
||||
<div
|
||||
class="grid gap-2 rounded-lg border border-neutral-200 bg-neutral-50 p-4 font-mono text-xs text-neutral-700 sm:grid-cols-2 dark:border-white/[0.07] dark:bg-white/[0.025] dark:text-fg-dim">
|
||||
class="grid gap-2 rounded-lg border border-neutral-200 bg-neutral-50 p-4 font-mono text-xs text-neutral-700 sm:grid-cols-2 dark:border-white/[0.07] dark:bg-white/[0.05] dark:text-fg-dim">
|
||||
@foreach (request()->user()->recoveryCodes() as $code)
|
||||
<div>{{ $code }}</div>
|
||||
@endforeach
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
||||
|
|
|
|||
|
|
@ -367,7 +367,7 @@ class="logs-viewer-btn">
|
|||
x-transition:leave="transition ease-in duration-75"
|
||||
x-transition:leave-start="transform opacity-100 scale-100"
|
||||
x-transition:leave-end="transform opacity-0 scale-95"
|
||||
class="absolute right-0 z-50 mt-2 w-max origin-top-right rounded-lg border border-neutral-200 bg-white p-1 shadow-modal focus:outline-none dark:border-white/[0.1] dark:bg-[#181818]">
|
||||
class="absolute right-0 z-50 mt-2 w-max origin-top-right rounded-lg border border-neutral-200 bg-white p-1 shadow-dropdown focus:outline-none dark:border-white/[0.1] dark:bg-[#181818]">
|
||||
<div>
|
||||
<button x-on:click="downloadLogs(); downloadMenuOpen = false"
|
||||
class="listbox-option text-neutral-700! hover:bg-neutral-100! dark:text-neutral-200! dark:hover:bg-white/[0.07]!">
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@
|
|||
$domainCount = countDomains($fqdn);
|
||||
}
|
||||
@endphp
|
||||
<div class="flex items-center gap-3 rounded-lg border border-neutral-200 bg-neutral-50/60 px-4 py-3 dark:border-white/[0.07] dark:bg-white/[0.025]">
|
||||
<div class="flex items-center gap-3 rounded-lg border border-neutral-200 bg-neutral-50/60 px-4 py-3 dark:border-white/[0.07] dark:bg-white/[0.05]">
|
||||
<div class="flex min-w-0 flex-1 items-center gap-3">
|
||||
<div class="flex size-9 shrink-0 items-center justify-center rounded-md bg-neutral-200/70 text-neutral-600 dark:bg-white/[0.07] dark:text-fg-dim">
|
||||
<x-reicon name="globe" class="size-4" />
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
|
||||
@if (blank($privateKeyId))
|
||||
<div
|
||||
class="mb-4 flex items-center justify-between gap-3 rounded-lg bg-neutral-50 px-3 py-2.5 ring-1 ring-neutral-200 dark:bg-white/[0.025] dark:ring-white/[0.07]">
|
||||
class="mb-4 flex items-center justify-between gap-3 rounded-lg bg-neutral-50 px-3 py-2.5 ring-1 ring-neutral-200 dark:bg-white/[0.05] dark:ring-white/[0.07]">
|
||||
<span class="text-[12px] text-neutral-500 dark:text-fg-dim">Connected source</span>
|
||||
<span class="text-[12px] font-medium text-neutral-900 dark:text-fg">
|
||||
{{ data_get($application, 'source.name', 'No source connected') }}
|
||||
|
|
@ -48,7 +48,7 @@ class="mb-4 flex items-center justify-between gap-3 rounded-lg bg-neutral-50 px-
|
|||
<x-application.settings-section title="Deploy key"
|
||||
description="The SSH key Coolify uses to clone this private repository.">
|
||||
<div
|
||||
class="mb-4 flex items-center justify-between gap-3 rounded-lg bg-neutral-50 px-3 py-2.5 ring-1 ring-neutral-200 dark:bg-white/[0.025] dark:ring-white/[0.07]">
|
||||
class="mb-4 flex items-center justify-between gap-3 rounded-lg bg-neutral-50 px-3 py-2.5 ring-1 ring-neutral-200 dark:bg-white/[0.05] dark:ring-white/[0.07]">
|
||||
<span class="text-[12px] text-neutral-500 dark:text-fg-dim">Attached private key</span>
|
||||
<span class="text-[12px] font-medium text-neutral-900 dark:text-fg">{{ $privateKeyName }}</span>
|
||||
</div>
|
||||
|
|
@ -56,7 +56,7 @@ class="mb-4 flex items-center justify-between gap-3 rounded-lg bg-neutral-50 px-
|
|||
<div class="grid gap-2 sm:grid-cols-2">
|
||||
@forelse ($privateKeys as $key)
|
||||
<button type="button" wire:click="setPrivateKey('{{ $key->id }}')"
|
||||
class="flex min-h-11 items-center rounded-lg border border-neutral-200 bg-white px-3 text-left text-[12px] font-medium transition-colors hover:bg-neutral-50 dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:bg-white/[0.05]">
|
||||
class="flex min-h-11 items-center rounded-lg border border-neutral-200 bg-white px-3 text-left text-[12px] font-medium transition-colors hover:bg-neutral-50 dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:bg-white/[0.05]">
|
||||
{{ $key->name }}
|
||||
</button>
|
||||
@empty
|
||||
|
|
@ -129,7 +129,7 @@ class="size-4 shrink-0 text-coollabs dark:text-warning" />
|
|||
shortConfirmationLabel="Confirmation text" :confirmWithPassword="false">
|
||||
<x-slot:trigger>
|
||||
<button type="button"
|
||||
class="group flex w-full min-h-16 items-center gap-3 rounded-xl border border-neutral-200 bg-white p-3 text-left shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:bg-neutral-50 hover:shadow-md focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14] dark:hover:bg-white/[0.05]">
|
||||
class="group flex w-full min-h-16 items-center gap-3 rounded-xl border border-neutral-200 bg-white p-3 text-left shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:bg-neutral-50 hover:shadow-md focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14] dark:hover:bg-white/[0.05]">
|
||||
<div
|
||||
class="flex size-9 shrink-0 items-center justify-center rounded-lg border border-neutral-200 bg-neutral-50 text-neutral-500 transition-colors group-hover:border-neutral-300 group-hover:text-neutral-700 dark:border-white/[0.08] dark:bg-white/[0.04] dark:text-fg-dim dark:group-hover:border-white/[0.12] dark:group-hover:text-fg">
|
||||
<x-git-icon class="size-4" :git="$sourceMorph" />
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@
|
|||
class="flex min-h-20 items-center gap-3 rounded-[10px] border p-3 text-left transition-colors"
|
||||
:class="restoreType === 'file'
|
||||
? 'border-coollabs/35 bg-coollabs/[0.06] text-coollabs dark:border-warning/30 dark:bg-warning/[0.08] dark:text-warning'
|
||||
: 'border-neutral-200 bg-white hover:border-neutral-300 dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]'">
|
||||
: 'border-neutral-200 bg-white hover:border-neutral-300 dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]'">
|
||||
<span
|
||||
class="flex size-9 shrink-0 items-center justify-center rounded-lg bg-neutral-100 dark:bg-white/[0.06]">
|
||||
<x-reicon name="file" class="size-4" />
|
||||
|
|
@ -122,7 +122,7 @@ class="flex size-9 shrink-0 items-center justify-center rounded-lg bg-neutral-10
|
|||
class="flex min-h-20 items-center gap-3 rounded-[10px] border p-3 text-left transition-colors"
|
||||
:class="restoreType === 's3'
|
||||
? 'border-coollabs/35 bg-coollabs/[0.06] text-coollabs dark:border-warning/30 dark:bg-warning/[0.08] dark:text-warning'
|
||||
: 'border-neutral-200 bg-white hover:border-neutral-300 dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]'">
|
||||
: 'border-neutral-200 bg-white hover:border-neutral-300 dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]'">
|
||||
<span
|
||||
class="flex size-9 shrink-0 items-center justify-center rounded-lg bg-neutral-100 dark:bg-white/[0.06]">
|
||||
<x-reicon name="storages" class="size-4" />
|
||||
|
|
@ -139,7 +139,7 @@ class="flex size-9 shrink-0 items-center justify-center rounded-lg bg-neutral-10
|
|||
{{-- File Restore Section --}}
|
||||
@can('update', $this->resource)
|
||||
<div x-cloak x-show="restoreType === 'file'"
|
||||
class="mt-4 rounded-[10px] border border-neutral-200 bg-neutral-50 p-4 dark:border-white/[0.08] dark:bg-white/[0.025]">
|
||||
class="mt-4 rounded-[10px] border border-neutral-200 bg-neutral-50 p-4 dark:border-white/[0.08] dark:bg-white/[0.05]">
|
||||
<form class="flex flex-col gap-3 sm:flex-row sm:items-end">
|
||||
<div class="min-w-0 flex-1">
|
||||
<x-forms.input label="File path on the server"
|
||||
|
|
@ -158,7 +158,7 @@ class="mt-4 rounded-[10px] border border-neutral-200 bg-neutral-50 p-4 dark:bord
|
|||
</div>
|
||||
|
||||
<form action="{{ route('upload.backup', ['databaseUuid' => $resourceUuid]) }}"
|
||||
class="dropzone rounded-lg! border! border-dashed! border-neutral-300! bg-white! dark:border-white/[0.12]! dark:bg-white/[0.025]!"
|
||||
class="dropzone rounded-lg! border! border-dashed! border-neutral-300! bg-white! dark:border-white/[0.12]! dark:bg-white/[0.05]!"
|
||||
id="my-dropzone" wire:ignore>
|
||||
@csrf
|
||||
</form>
|
||||
|
|
@ -206,7 +206,7 @@ class="mt-4 flex flex-col gap-3 rounded-lg border border-neutral-200 bg-white p-
|
|||
->all();
|
||||
@endphp
|
||||
<div x-cloak x-show="restoreType === 's3'"
|
||||
class="mt-4 rounded-[10px] border border-neutral-200 bg-neutral-50 p-4 dark:border-white/[0.08] dark:bg-white/[0.025]">
|
||||
class="mt-4 rounded-[10px] border border-neutral-200 bg-neutral-50 p-4 dark:border-white/[0.08] dark:bg-white/[0.05]">
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<x-forms.listbox id="s3StorageId" label="S3 storage" :options="$s3StorageOptions"
|
||||
placeholder="Select storage" live />
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ class="input h-8! w-full pl-8! text-[13px]!" placeholder="Search backups" />
|
|||
description="Create a schedule to start protecting this database."
|
||||
icon-name="storages" />
|
||||
@else
|
||||
<div x-cloak x-show="search === '' || hasMatches()" class="data-table overflow-hidden rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.025]">
|
||||
<div x-cloak x-show="search === '' || hasMatches()" class="data-table overflow-hidden rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.05]">
|
||||
<div class="data-table-header scheduled-backups-table-grid">
|
||||
<span>Schedule</span>
|
||||
<span>Latest run</span>
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ class="flex size-7.5 items-center justify-center rounded-md transition-colors"
|
|||
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<template x-for="project in paginatedProjects" :key="project.uuid">
|
||||
<article
|
||||
class="group relative flex min-h-28 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]">
|
||||
class="group relative flex min-h-28 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]">
|
||||
<a :href="project.href" {{ wireNavigate() }} class="absolute inset-0 rounded-xl"
|
||||
:aria-label="`Open ${project.name}`"></a>
|
||||
<div class="flex items-start gap-3">
|
||||
|
|
@ -151,15 +151,15 @@ class="flex size-7.5 items-center justify-center rounded-md text-neutral-400 tra
|
|||
</article>
|
||||
</template>
|
||||
</div>
|
||||
<x-client-pagination x-show="filteredProjects.length > 0" class="mt-3 rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.025]"
|
||||
<x-client-pagination x-show="filteredProjects.length > 0" class="mt-3 rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.05]"
|
||||
summary="`${rangeStart}-${rangeEnd} of ${filteredProjects.length}`" page-size-model="pageSize"
|
||||
storage-key="coolify.page-size.projects" :options="[12, 24, 48, 96]" />
|
||||
</div>
|
||||
|
||||
<div x-show="viewMode === 'table'"
|
||||
class="overflow-hidden rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.025]">
|
||||
class="overflow-hidden rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.05]">
|
||||
<div
|
||||
class="projects-table-grid border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-faint">
|
||||
class="projects-table-grid border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.05] dark:text-fg-faint">
|
||||
<div>Project</div>
|
||||
<div>Environments</div>
|
||||
<div>Resources</div>
|
||||
|
|
@ -212,7 +212,7 @@ class="flex size-7 items-center justify-center rounded-md text-neutral-400 trans
|
|||
</div>
|
||||
|
||||
<div x-show="filteredProjects.length === 0"
|
||||
class="flex min-h-52 flex-col items-center justify-center rounded-xl border border-neutral-200 bg-white px-6 text-center dark:border-white/[0.08] dark:bg-white/[0.025]">
|
||||
class="flex min-h-52 flex-col items-center justify-center rounded-xl border border-neutral-200 bg-white px-6 text-center dark:border-white/[0.08] dark:bg-white/[0.05]">
|
||||
<x-reicon name="search" class="mb-3 size-6 text-neutral-300 dark:text-fg-faint" />
|
||||
<p class="text-[13px] font-medium">No matching projects</p>
|
||||
<p class="mt-1 text-[12px] text-neutral-500 dark:text-fg-dim">
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ class="application-settings-section-body grid grid-cols-1 justify-start gap-3 te
|
|||
<article role="button" tabindex="0" :aria-label="'Deploy ' + application.name"
|
||||
@click="setType(application.id)" @keydown.enter.self.prevent="setType(application.id)"
|
||||
@keydown.space.self.prevent="setType(application.id)"
|
||||
class="group flex min-h-48 cursor-pointer flex-col rounded-xl border border-neutral-200 bg-white p-4 transition-colors hover:border-neutral-300 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]">
|
||||
class="group flex min-h-48 cursor-pointer flex-col rounded-xl border border-neutral-200 bg-white p-4 transition-colors hover:border-neutral-300 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]">
|
||||
<div class="flex min-w-0 items-start gap-3">
|
||||
<div
|
||||
class="flex size-11 shrink-0 items-center justify-center overflow-hidden rounded-xl border border-neutral-200 bg-neutral-50 dark:border-white/[0.08] dark:bg-white/[0.04]">
|
||||
|
|
@ -157,7 +157,7 @@ class="mt-auto flex items-center gap-1.5 border-t border-neutral-200 pt-3 dark:b
|
|||
<article role="button" tabindex="0" :aria-label="'Deploy ' + application.name"
|
||||
@click="setType(application.id)" @keydown.enter.self.prevent="setType(application.id)"
|
||||
@keydown.space.self.prevent="setType(application.id)"
|
||||
class="group flex min-h-48 cursor-pointer flex-col rounded-xl border border-neutral-200 bg-white p-4 transition-colors hover:border-neutral-300 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]">
|
||||
class="group flex min-h-48 cursor-pointer flex-col rounded-xl border border-neutral-200 bg-white p-4 transition-colors hover:border-neutral-300 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]">
|
||||
<div class="flex min-w-0 items-start gap-3">
|
||||
<div
|
||||
class="flex size-11 shrink-0 items-center justify-center overflow-hidden rounded-xl border border-neutral-200 bg-neutral-50 dark:border-white/[0.08] dark:bg-white/[0.04]">
|
||||
|
|
@ -206,7 +206,7 @@ class="application-settings-section-body grid grid-cols-1 justify-start gap-3 te
|
|||
<article role="button" tabindex="0" :aria-label="'Deploy ' + database.name"
|
||||
@click="setType(database.id)" @keydown.enter.self.prevent="setType(database.id)"
|
||||
@keydown.space.self.prevent="setType(database.id)"
|
||||
class="group flex min-h-48 cursor-pointer flex-col rounded-xl border border-neutral-200 bg-white p-4 transition-colors hover:border-neutral-300 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]">
|
||||
class="group flex min-h-48 cursor-pointer flex-col rounded-xl border border-neutral-200 bg-white p-4 transition-colors hover:border-neutral-300 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]">
|
||||
<div class="flex min-w-0 items-center gap-3">
|
||||
<div
|
||||
class="flex size-11 shrink-0 items-center justify-center overflow-hidden rounded-xl border border-neutral-200 bg-neutral-50 dark:border-white/[0.08] dark:bg-white/[0.04]">
|
||||
|
|
@ -283,7 +283,7 @@ class="text-[11px] text-neutral-500 dark:text-fg-faint">
|
|||
@click="setType('one-click-service-' + service.id)"
|
||||
@keydown.enter.self.prevent="setType('one-click-service-' + service.id)"
|
||||
@keydown.space.self.prevent="setType('one-click-service-' + service.id)"
|
||||
class="group flex min-h-48 cursor-pointer flex-col rounded-xl border border-neutral-200 bg-white p-4 transition-colors hover:border-neutral-300 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]">
|
||||
class="group flex min-h-48 cursor-pointer flex-col rounded-xl border border-neutral-200 bg-white p-4 transition-colors hover:border-neutral-300 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]">
|
||||
<div class="flex min-w-0 items-start gap-3">
|
||||
<div
|
||||
class="flex size-11 shrink-0 items-center justify-center overflow-hidden rounded-xl border border-neutral-200 bg-neutral-50 dark:border-white/[0.08] dark:bg-white/[0.04]">
|
||||
|
|
|
|||
|
|
@ -166,9 +166,9 @@ class="flex size-7.5 items-center justify-center rounded-md transition-colors"
|
|||
</div>
|
||||
|
||||
<div x-show="viewMode === 'table'"
|
||||
class="overflow-hidden rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.025]">
|
||||
class="overflow-hidden rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.05]">
|
||||
<div
|
||||
class="environment-resource-grid border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-faint">
|
||||
class="environment-resource-grid border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.05] dark:text-fg-faint">
|
||||
<div>Resource</div>
|
||||
<div class="resource-type">Type</div>
|
||||
<div>Status</div>
|
||||
|
|
@ -271,7 +271,7 @@ class="flex min-h-52 flex-col items-center justify-center px-6 text-center">
|
|||
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<template x-for="item in paginatedResources" :key="item.uuid">
|
||||
<article
|
||||
class="group relative flex min-h-28 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]">
|
||||
class="group relative flex min-h-28 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]">
|
||||
<a :href="item.hrefLink"
|
||||
@click="if (item.version === 'v5') { $event.preventDefault(); window.location.assign(item.hrefLink) }"
|
||||
{{ wireNavigate() }} class="absolute inset-0 rounded-xl"
|
||||
|
|
@ -320,7 +320,7 @@ class="relative z-10 max-w-full self-start truncate text-[11px] text-neutral-500
|
|||
</div>
|
||||
|
||||
<div x-show="filteredResources.length === 0"
|
||||
class="flex min-h-52 flex-col items-center justify-center rounded-xl border border-neutral-200 bg-white px-6 text-center dark:border-white/[0.08] dark:bg-white/[0.025]">
|
||||
class="flex min-h-52 flex-col items-center justify-center rounded-xl border border-neutral-200 bg-white px-6 text-center dark:border-white/[0.08] dark:bg-white/[0.05]">
|
||||
<x-reicon name="search" class="mb-3 size-6 text-neutral-300 dark:text-fg-faint" />
|
||||
<p class="text-[13px] font-medium">No matching resources</p>
|
||||
<p class="mt-1 text-[12px] text-neutral-500 dark:text-fg-dim">
|
||||
|
|
@ -328,7 +328,7 @@ class="flex min-h-52 flex-col items-center justify-center rounded-xl border bord
|
|||
</p>
|
||||
</div>
|
||||
<x-client-pagination x-show="filteredResources.length > 0"
|
||||
class="mt-3 rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.025]"
|
||||
class="mt-3 rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.05]"
|
||||
summary="filteredResources.length === 0 ? '0 resources' : `${rangeStart}-${rangeEnd} of ${filteredResources.length}`"
|
||||
page-size-model="pageSize" storage-key="coolify.page-size.environment-resources" />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -144,10 +144,10 @@ class="flex size-7.5 items-center justify-center rounded-md transition-colors"
|
|||
|
||||
<div :class="viewMode === 'grid'
|
||||
? 'grid grid-cols-1 gap-3 sm:grid-cols-2'
|
||||
: 'overflow-x-auto rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.025]'">
|
||||
: 'overflow-x-auto rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.05]'">
|
||||
@if ($applications->isNotEmpty() || $databases->isNotEmpty())
|
||||
<div x-cloak x-show="viewMode === 'table'"
|
||||
class="grid min-w-[48rem] grid-cols-[minmax(14rem,1fr)_minmax(12rem,1fr)_12rem_5rem] gap-3 border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-faint">
|
||||
class="grid min-w-[48rem] grid-cols-[minmax(14rem,1fr)_minmax(12rem,1fr)_12rem_5rem] gap-3 border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.05] dark:text-fg-faint">
|
||||
<div>Resource</div>
|
||||
<div>Image</div>
|
||||
<div class="justify-self-start">Status</div>
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ class="flex min-h-0 flex-col gap-3">
|
|||
</div>
|
||||
|
||||
<div
|
||||
class="flex flex-col items-stretch gap-2 rounded-lg border border-neutral-200 bg-neutral-50 p-1 sm:flex-row sm:flex-wrap sm:items-center dark:border-white/[0.08] dark:bg-white/[0.025]">
|
||||
class="flex flex-col items-stretch gap-2 rounded-lg border border-neutral-200 bg-neutral-50 p-1 sm:flex-row sm:flex-wrap sm:items-center dark:border-white/[0.08] dark:bg-white/[0.05]">
|
||||
<x-forms.checkbox label="Escape special characters in labels"
|
||||
helper="By default, $ (and other characters) is escaped. A $ in a label is saved as $$. Turn this off to use environment variables inside labels."
|
||||
id="isContainerLabelEscapeEnabled" instantSave />
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ class="rounded bg-neutral-100 px-1.5 py-0.5 font-mono text-xs text-neutral-700 d
|
|||
<a href="{{ route('server.show', ['server_uuid' => data_get($resource, 'destination.server.uuid')]) }}"
|
||||
{{ wireNavigate() }} class="button">Open server</a>
|
||||
<x-application.restart-limit-warning :application="$resource" />
|
||||
<x-status-summary :status="$resource->status" />
|
||||
<x-status-summary :status="$resource->status" align="right" />
|
||||
@if ($hasAdditionalDestinations)
|
||||
<x-forms.button canGate="deploy" :canResource="$resource"
|
||||
wire:click="redeploy('{{ data_get($resource, 'destination.id') }}','{{ data_get($resource, 'destination.server.id') }}')">
|
||||
|
|
|
|||
|
|
@ -106,11 +106,11 @@
|
|||
},
|
||||
}">
|
||||
<div data-terminal-session-canvas
|
||||
class="application-console-shell flex h-full min-h-0 flex-col overflow-hidden rounded-lg p-3 sm:p-6"
|
||||
class="application-console-shell flex h-full min-h-0 flex-col overflow-hidden rounded-lg"
|
||||
:data-console-theme="consoleTheme"
|
||||
:style="{ '--terminal-scrollbar': themeAccents[consoleTheme] }">
|
||||
<header
|
||||
class="terminal-session-toolbar absolute top-3 right-3 left-3 z-20 flex items-center gap-3 text-white select-none">
|
||||
class="terminal-session-toolbar flex items-center gap-3 text-white select-none">
|
||||
@if ($type === 'server')
|
||||
<div class="terminal-session-target-trigger flex h-8 min-w-0 max-w-sm flex-1 items-center gap-2 rounded-md px-2.5 text-xs font-medium text-white/70"
|
||||
x-data="{ autoConnected: false }"
|
||||
|
|
@ -150,7 +150,7 @@ class="terminal-session-target-trigger flex h-8 max-w-[34rem] min-w-48 items-cen
|
|||
</svg>
|
||||
</button>
|
||||
<div x-cloak x-show="containerOpen" x-transition.origin.top.left
|
||||
class="terminal-target-picker terminal-target-list absolute top-11 left-0 z-50 max-h-72 w-80 overflow-y-auto rounded-lg border p-1 shadow-[0_18px_50px_rgba(0,0,0,0.35)]">
|
||||
class="terminal-target-picker terminal-target-list absolute top-11 left-0 z-50 max-h-72 w-80 overflow-y-auto rounded-lg border p-1 shadow-[var(--shadow-dropdown)]">
|
||||
<template x-for="option in containerOptions" :key="option.value">
|
||||
<button type="button"
|
||||
class="flex h-8 w-full items-center gap-2 rounded-md px-2 text-left text-[11px] text-white/65 transition-colors hover:bg-white/[0.07] hover:text-white"
|
||||
|
|
@ -175,12 +175,12 @@ class="size-3 shrink-0 text-[#fcd452]" viewBox="0 0 12 12"
|
|||
:theme-accents="$consoleThemeAccents" />
|
||||
</header>
|
||||
|
||||
<div class="terminal-session-panel mt-8 flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||
<div class="terminal-session-panel flex min-h-0 flex-1 flex-col overflow-hidden p-3 sm:p-6">
|
||||
<div class="application-console-block min-h-0 flex-1">
|
||||
@if ($type !== 'server' && $containers->count() > 1)
|
||||
<div x-cloak x-show="!targetChosen" data-terminal-target-picker="launcher"
|
||||
class="absolute inset-0 z-20 flex items-start justify-start p-6 sm:p-10">
|
||||
<div class="terminal-target-picker w-full max-w-md rounded-lg border p-2 shadow-[0_18px_50px_rgba(0,0,0,0.28)]">
|
||||
<div class="terminal-target-picker w-full max-w-md rounded-lg border p-2 shadow-[var(--shadow-dropdown)]">
|
||||
<div class="px-2 pt-1 pb-2">
|
||||
<div class="text-sm font-semibold text-white/80">Start a terminal session</div>
|
||||
<div class="mt-0.5 text-[11px] text-white/45">
|
||||
|
|
|
|||
|
|
@ -295,7 +295,7 @@
|
|||
@if ($streamLogs)
|
||||
<div class="sr-only" wire:poll.2000ms="getLogs(true)" aria-hidden="true"></div>
|
||||
@endif
|
||||
<div x-show="expanded" {{ $collapsible ? 'x-collapse' : '' }}
|
||||
<div x-show="expanded" {{ $collapsible ? 'x-collapse.duration.200ms' : '' }}
|
||||
:class="fullscreen ? 'fullscreen flex flex-col !overflow-visible' : 'relative w-full mx-auto'"
|
||||
:style="fullscreen ? 'max-height: none !important; height: 100% !important;' : ''">
|
||||
<div class="runtime-log-panel"
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
@if ($executionCount > 0)
|
||||
<div class="data-table w-full overflow-x-auto">
|
||||
<div
|
||||
class="data-table-header volume-backup-executions-grid border-b border-neutral-200 bg-neutral-50 dark:border-white/[0.08] dark:bg-white/[0.025]">
|
||||
class="data-table-header volume-backup-executions-grid border-b border-neutral-200 bg-neutral-50 dark:border-white/[0.08] dark:bg-white/[0.05]">
|
||||
<span>Status</span>
|
||||
<span>Archive</span>
|
||||
<span>Time</span>
|
||||
|
|
@ -129,7 +129,7 @@ class="icon-button shrink-0 text-red-500 hover:text-red-600 dark:text-red-400 da
|
|||
|
||||
@if ($execution->message)
|
||||
<pre
|
||||
class="volume-backup-execution-message col-span-6 mt-2 max-h-32 overflow-auto rounded-lg border border-neutral-200 bg-neutral-50 p-2 text-[11px] whitespace-pre-wrap text-neutral-600 dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-dim">{{ $execution->message }}</pre>
|
||||
class="volume-backup-execution-message col-span-6 mt-2 max-h-32 overflow-auto rounded-lg border border-neutral-200 bg-neutral-50 p-2 text-[11px] whitespace-pre-wrap text-neutral-600 dark:border-white/[0.08] dark:bg-white/[0.05] dark:text-fg-dim">{{ $execution->message }}</pre>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@ class="flex size-7.5 items-center justify-center rounded-md transition-colors"
|
|||
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<template x-for="environment in paginatedEnvironments" :key="environment.uuid">
|
||||
<article
|
||||
class="group relative flex min-h-28 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]">
|
||||
class="group relative flex min-h-28 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]">
|
||||
<a :href="environment.href" {{ wireNavigate() }} class="absolute inset-0 rounded-xl"
|
||||
:aria-label="`Open ${environment.name}`"></a>
|
||||
|
||||
|
|
@ -165,15 +165,15 @@ class="flex size-7.5 items-center justify-center rounded-md text-neutral-400 tra
|
|||
</article>
|
||||
</template>
|
||||
</div>
|
||||
<x-client-pagination x-show="filteredEnvironments.length > 0" class="mt-3 rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.025]"
|
||||
<x-client-pagination x-show="filteredEnvironments.length > 0" class="mt-3 rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.05]"
|
||||
summary="`${rangeStart}-${rangeEnd} of ${filteredEnvironments.length}`" page-size-model="pageSize"
|
||||
storage-key="coolify.page-size.project-environments" :options="[12, 24, 48, 96]" />
|
||||
</div>
|
||||
|
||||
<div x-show="viewMode === 'table'"
|
||||
class="overflow-hidden rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.025]">
|
||||
class="overflow-hidden rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.05]">
|
||||
<div
|
||||
class="environments-table-grid border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-faint">
|
||||
class="environments-table-grid border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.05] dark:text-fg-faint">
|
||||
<div>Environment</div>
|
||||
<div class="environment-resource-count">Resources</div>
|
||||
<div class="environment-description">Description</div>
|
||||
|
|
@ -222,7 +222,7 @@ class="flex size-7 items-center justify-center rounded-md text-neutral-400 trans
|
|||
</div>
|
||||
|
||||
<div x-show="filteredEnvironments.length === 0"
|
||||
class="flex min-h-52 flex-col items-center justify-center rounded-xl border border-neutral-200 bg-white px-6 text-center dark:border-white/[0.08] dark:bg-white/[0.025]">
|
||||
class="flex min-h-52 flex-col items-center justify-center rounded-xl border border-neutral-200 bg-white px-6 text-center dark:border-white/[0.08] dark:bg-white/[0.05]">
|
||||
<x-reicon name="search" class="mb-3 size-6 text-neutral-300 dark:text-fg-faint" />
|
||||
<p class="text-[13px] font-medium">No matching environments</p>
|
||||
<p class="mt-1 text-[12px] text-neutral-500 dark:text-fg-dim">
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ class="button button-highlighted">
|
|||
icon-name="file-content" size="sm" />
|
||||
@else
|
||||
<div>
|
||||
<div class="grid grid-cols-[minmax(0,1fr)_12rem_1.75rem] items-center gap-3 border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[13px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-faint">
|
||||
<div class="grid grid-cols-[minmax(0,1fr)_12rem_1.75rem] items-center gap-3 border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[13px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.05] dark:text-fg-faint">
|
||||
<div class="pl-11">Script</div>
|
||||
<div>Last updated</div>
|
||||
<div class="w-7"></div>
|
||||
|
|
|
|||
|
|
@ -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 @@ class="button button-highlighted">
|
|||
description="Add a provider token to provision new cloud servers." icon-name="keys" size="sm" />
|
||||
@else
|
||||
<div>
|
||||
<div class="grid grid-cols-[minmax(0,1fr)_8rem_1.75rem] items-center gap-3 border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[13px] font-medium text-neutral-500 sm:grid-cols-[minmax(0,1fr)_8rem_minmax(0,1fr)_1.75rem] dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-faint">
|
||||
<div class="grid grid-cols-[minmax(0,1fr)_8rem_1.75rem] items-center gap-3 border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[13px] font-medium text-neutral-500 sm:grid-cols-[minmax(0,1fr)_8rem_minmax(0,1fr)_1.75rem] dark:border-white/[0.08] dark:bg-white/[0.05] dark:text-fg-faint">
|
||||
<div class="pl-11">Token</div>
|
||||
<div class="text-center">Provider</div>
|
||||
<div class="hidden sm:block">Description</div>
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ class="listbox-option justify-start! gap-2.5!" role="menuitem">
|
|||
icon-name="keys" />
|
||||
@else
|
||||
<div>
|
||||
<div class="grid grid-cols-[minmax(0,1fr)_7rem_1.75rem] items-center gap-3 border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[13px] font-medium text-neutral-500 sm:grid-cols-[minmax(0,1.2fr)_minmax(0,1fr)_7rem_1.75rem] dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-faint">
|
||||
<div class="grid grid-cols-[minmax(0,1fr)_7rem_1.75rem] items-center gap-3 border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[13px] font-medium text-neutral-500 sm:grid-cols-[minmax(0,1.2fr)_minmax(0,1fr)_7rem_1.75rem] dark:border-white/[0.08] dark:bg-white/[0.05] dark:text-fg-faint">
|
||||
<div class="pl-11">Private key</div>
|
||||
<div class="hidden sm:block">Description</div>
|
||||
<div class="text-center">Status</div>
|
||||
|
|
@ -115,7 +115,7 @@ class="flex size-8 shrink-0 items-center justify-center rounded-lg border border
|
|||
title="You do not have permission to view this private key">
|
||||
<div class="flex items-start gap-3">
|
||||
<div
|
||||
class="flex size-8 shrink-0 items-center justify-center rounded-lg border border-neutral-200 bg-white text-neutral-400 dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-faint">
|
||||
class="flex size-8 shrink-0 items-center justify-center rounded-lg border border-neutral-200 bg-white text-neutral-400 dark:border-white/[0.08] dark:bg-white/[0.05] dark:text-fg-faint">
|
||||
<x-reicon name="keys" class="size-4" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@
|
|||
<div class="application-settings-section-body is-flush">
|
||||
<div class="grid grid-cols-1 gap-3 p-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<a href="{{ route('server.create.type', ['type' => 'manual']) }}"
|
||||
class="group flex min-h-32 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]"
|
||||
class="group flex min-h-32 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]"
|
||||
{{ wireNavigate() }}>
|
||||
<div class="flex items-start">
|
||||
<span
|
||||
|
|
@ -52,7 +52,7 @@ class="flex size-8 items-center justify-center rounded-lg border border-neutral-
|
|||
<div class="application-settings-section-body is-flush">
|
||||
<div class="grid grid-cols-1 gap-3 p-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<a href="{{ route('server.create.type', ['type' => 'hetzner']) }}"
|
||||
class="group flex min-h-32 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]"
|
||||
class="group flex min-h-32 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]"
|
||||
{{ wireNavigate() }}>
|
||||
<div class="flex items-start">
|
||||
<img src="{{ asset('svgs/hetzner.svg') }}" alt="Hetzner" class="size-8">
|
||||
|
|
@ -66,7 +66,7 @@ class="group flex min-h-32 flex-col rounded-xl border border-neutral-200 bg-whit
|
|||
</a>
|
||||
|
||||
<a href="{{ route('server.create.type', ['type' => 'vultr']) }}"
|
||||
class="group flex min-h-32 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]"
|
||||
class="group flex min-h-32 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]"
|
||||
{{ wireNavigate() }}>
|
||||
<div class="flex items-start">
|
||||
<img src="https://www.vultr.com/media/logo_ondark.svg" alt="Vultr"
|
||||
|
|
@ -81,7 +81,7 @@ class="h-8 w-20 object-contain object-left">
|
|||
</a>
|
||||
|
||||
<a href="{{ route('server.create.type', ['type' => 'digital-ocean']) }}"
|
||||
class="group flex min-h-32 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]"
|
||||
class="group flex min-h-32 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]"
|
||||
{{ wireNavigate() }}>
|
||||
<div class="flex items-start">
|
||||
<x-digital-ocean-icon class="size-8" />
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
|||
<a x-cloak
|
||||
x-show="filteredServers.some(server => server.uuid === @js($server->uuid))"
|
||||
href="{{ $serverRow['href'] }}" {{ wireNavigate() }}
|
||||
class="group relative flex min-h-28 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]">
|
||||
class="group relative flex min-h-28 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]">
|
||||
@if ($server->isMetricsEnabled())
|
||||
<livewire:dashboard.server-metrics-chart :server="$server"
|
||||
:key="'server-index-metrics-'.$server->uuid" />
|
||||
|
|
@ -170,9 +170,9 @@ class="flex size-8 shrink-0 items-center justify-center rounded-lg border border
|
|||
</div>
|
||||
|
||||
<div x-show="viewMode === 'table'"
|
||||
class="overflow-x-auto rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.025]">
|
||||
class="overflow-x-auto rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.05]">
|
||||
<div
|
||||
class="grid min-w-[480px] grid-cols-[minmax(0,1fr)_9.5rem] border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-faint">
|
||||
class="grid min-w-[480px] grid-cols-[minmax(0,1fr)_9.5rem] border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.05] dark:text-fg-faint">
|
||||
<div>Server</div>
|
||||
<div>Status</div>
|
||||
</div>
|
||||
|
|
@ -205,7 +205,7 @@ class="ml-auto flex size-6 shrink-0 items-center justify-center rounded-md"
|
|||
</div>
|
||||
|
||||
<div x-show="filteredServers.length === 0"
|
||||
class="flex min-h-52 flex-col items-center justify-center rounded-xl border border-neutral-200 bg-white px-6 text-center dark:border-white/[0.08] dark:bg-white/[0.025]">
|
||||
class="flex min-h-52 flex-col items-center justify-center rounded-xl border border-neutral-200 bg-white px-6 text-center dark:border-white/[0.08] dark:bg-white/[0.05]">
|
||||
<x-reicon name="search" class="mb-3 size-6 text-neutral-300 dark:text-fg-faint" />
|
||||
<p class="text-[13px] font-medium">No matching servers</p>
|
||||
<p class="mt-1 text-[12px] text-neutral-500 dark:text-fg-dim">Try a different search.</p>
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@ class="flex size-9 shrink-0 items-center justify-center rounded-lg bg-neutral-10
|
|||
Link provider
|
||||
</button>
|
||||
<div x-cloak x-show="open" x-transition.origin.top.right
|
||||
class="absolute top-9 right-0 z-50 w-56 rounded-lg border border-neutral-200 bg-white p-1 shadow-modal dark:border-white/[0.1] dark:bg-raised">
|
||||
class="absolute top-9 right-0 z-50 w-56 rounded-lg border border-neutral-200 bg-white p-1 shadow-dropdown dark:border-white/[0.1] dark:bg-raised">
|
||||
@if (!$server->hetzner_server_id && $availableHetznerTokens->isNotEmpty())
|
||||
<x-server.provider-link-modal :server="$server" provider="hetzner"
|
||||
providerLabel="Hetzner" tokenModel="selectedHetznerTokenId"
|
||||
|
|
|
|||
|
|
@ -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 @@ class="group flex min-h-24 flex-col rounded-xl border border-neutral-200 bg-whit
|
|||
@endforeach
|
||||
</div>
|
||||
|
||||
<div x-show="viewMode === 'list'" class="overflow-hidden rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.025]">
|
||||
<div x-show="viewMode === 'list'" class="overflow-hidden rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.05]">
|
||||
@foreach ($projects as $project)
|
||||
@foreach ($project->environments as $environment)
|
||||
<a x-show="matches(@js([$environment->name, $environment->description, $project->name]))"
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
<div class="w-full">
|
||||
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<a class="group flex min-h-32 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]"
|
||||
<a class="group flex min-h-32 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]"
|
||||
href="{{ route('shared-variables.team.index') }}" {{ wireNavigate() }}>
|
||||
<div
|
||||
class="flex size-8 items-center justify-center rounded-lg border border-neutral-200 bg-neutral-50 text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.04] dark:text-fg-dim">
|
||||
|
|
@ -21,7 +21,7 @@ class="flex size-8 items-center justify-center rounded-lg border border-neutral-
|
|||
</div>
|
||||
</a>
|
||||
|
||||
<a class="group flex min-h-32 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]"
|
||||
<a class="group flex min-h-32 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]"
|
||||
href="{{ route('shared-variables.project.index') }}" {{ wireNavigate() }}>
|
||||
<div
|
||||
class="flex size-8 items-center justify-center rounded-lg border border-neutral-200 bg-neutral-50 text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.04] dark:text-fg-dim">
|
||||
|
|
@ -35,7 +35,7 @@ class="flex size-8 items-center justify-center rounded-lg border border-neutral-
|
|||
</div>
|
||||
</a>
|
||||
|
||||
<a class="group flex min-h-32 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]"
|
||||
<a class="group flex min-h-32 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]"
|
||||
href="{{ route('shared-variables.environment.index') }}" {{ wireNavigate() }}>
|
||||
<div
|
||||
class="flex size-8 items-center justify-center rounded-lg border border-neutral-200 bg-neutral-50 text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.04] dark:text-fg-dim">
|
||||
|
|
@ -49,7 +49,7 @@ class="flex size-8 items-center justify-center rounded-lg border border-neutral-
|
|||
</div>
|
||||
</a>
|
||||
|
||||
<a class="group flex min-h-32 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]"
|
||||
<a class="group flex min-h-32 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]"
|
||||
href="{{ route('shared-variables.server.index') }}" {{ wireNavigate() }}>
|
||||
<div
|
||||
class="flex size-8 items-center justify-center rounded-lg border border-neutral-200 bg-neutral-50 text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.04] dark:text-fg-dim">
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
<div x-cloak x-show="viewMode === 'grid'" class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
@foreach ($projects as $project)
|
||||
<a x-show="matches(@js([$project->name, $project->description]))"
|
||||
class="group flex min-h-28 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]"
|
||||
class="group flex min-h-28 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]"
|
||||
href="{{ route('shared-variables.project.show', ['project_uuid' => $project->uuid]) }}" {{ wireNavigate() }}>
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="flex size-8 shrink-0 items-center justify-center rounded-lg border border-neutral-200 bg-neutral-50 text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.04] dark:text-fg-dim">
|
||||
|
|
@ -43,7 +43,7 @@ class="h-full w-full rounded-lg object-cover">
|
|||
@endforeach
|
||||
</div>
|
||||
|
||||
<div x-show="viewMode === 'list'" class="overflow-hidden rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.025]">
|
||||
<div x-show="viewMode === 'list'" class="overflow-hidden rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.05]">
|
||||
@foreach ($projects as $project)
|
||||
<a x-show="matches(@js([$project->name, $project->description]))"
|
||||
href="{{ route('shared-variables.project.show', ['project_uuid' => $project->uuid]) }}" {{ wireNavigate() }}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
<div x-cloak x-show="viewMode === 'grid'" class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
@foreach ($servers as $server)
|
||||
<a x-show="matches(@js([$server->name, $server->description, $server->ip]))"
|
||||
class="group flex min-h-28 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]"
|
||||
class="group flex min-h-28 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]"
|
||||
href="{{ route('shared-variables.server.show', ['server_uuid' => $server->uuid]) }}" {{ wireNavigate() }}>
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="flex size-8 shrink-0 items-center justify-center rounded-lg border border-neutral-200 bg-neutral-50 text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.04] dark:text-fg-dim"><x-reicon name="servers" class="size-4" /></div>
|
||||
|
|
@ -29,7 +29,7 @@ class="group flex min-h-28 flex-col rounded-xl border border-neutral-200 bg-whit
|
|||
@endforeach
|
||||
</div>
|
||||
|
||||
<div x-show="viewMode === 'list'" class="overflow-hidden rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.025]">
|
||||
<div x-show="viewMode === 'list'" class="overflow-hidden rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.05]">
|
||||
@foreach ($servers as $server)
|
||||
<a x-show="matches(@js([$server->name, $server->description, $server->ip]))"
|
||||
href="{{ route('shared-variables.server.show', ['server_uuid' => $server->uuid]) }}" {{ wireNavigate() }}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -300,7 +300,7 @@ class="rounded-lg border border-red-300 bg-red-50 p-4 ring-1 ring-inset ring-red
|
|||
@endif
|
||||
|
||||
<div
|
||||
class="rounded-lg border border-neutral-200 bg-neutral-50 p-3 text-[12px] leading-5 text-neutral-600 dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-dim">
|
||||
class="rounded-lg border border-neutral-200 bg-neutral-50 p-3 text-[12px] leading-5 text-neutral-600 dark:border-white/[0.08] dark:bg-white/[0.05] dark:text-fg-dim">
|
||||
<p class="font-medium text-black dark:text-fg">Mandatory permissions</p>
|
||||
<p class="mt-1">Contents: read · Metadata: read · Email: read</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ class="flex w-full items-center justify-between px-3 py-2.5 text-left text-[12px
|
|||
<polyline points="6 9 12 15 18 9"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
<div x-cloak x-show="open" x-collapse class="border-t border-neutral-200 px-3 py-3 dark:border-white/[0.08]">
|
||||
<div x-cloak x-show="open" x-collapse.duration.200ms class="border-t border-neutral-200 px-3 py-3 dark:border-white/[0.08]">
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<x-forms.input id="html_url" label="HTML URL" required
|
||||
helper="For GitHub Enterprise, enter your instance URL (e.g. https://github.example.com)." />
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ class="h-8! w-full rounded-lg! border-neutral-200! bg-white! py-0! pr-3! pl-8! t
|
|||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<div
|
||||
class="grid min-w-[680px] grid-cols-[minmax(10rem,.8fr)_minmax(10rem,.8fr)_minmax(12rem,1fr)_8rem] border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-faint">
|
||||
class="grid min-w-[680px] grid-cols-[minmax(10rem,.8fr)_minmax(10rem,.8fr)_minmax(12rem,1fr)_8rem] border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.05] dark:text-fg-faint">
|
||||
<div>Project</div>
|
||||
<div>Environment</div>
|
||||
<div>Resource</div>
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ class="h-8! w-full rounded-lg! border-neutral-200! bg-white! py-0! pr-3! pl-8! t
|
|||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<div
|
||||
class="grid min-w-[680px] grid-cols-[minmax(10rem,.8fr)_minmax(10rem,.8fr)_minmax(12rem,1fr)_8rem] border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-faint">
|
||||
class="grid min-w-[680px] grid-cols-[minmax(10rem,.8fr)_minmax(10rem,.8fr)_minmax(12rem,1fr)_8rem] border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.05] dark:text-fg-faint">
|
||||
<div>Project</div>
|
||||
<div>Environment</div>
|
||||
<div>Resource</div>
|
||||
|
|
@ -275,7 +275,7 @@ class="flex w-full items-center justify-between rounded-lg border border-neutral
|
|||
<polyline points="6 9 12 15 18 9"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
<div x-cloak x-show="open" x-collapse
|
||||
<div x-cloak x-show="open" x-collapse.duration.200ms
|
||||
class="mt-3 grid gap-4 rounded-lg border border-neutral-200 p-3 lg:grid-cols-2 dark:border-white/[0.08]">
|
||||
<x-forms.input id="htmlUrl" label="GitLab URL"
|
||||
helper="Only change this for self-hosted GitLab (e.g. https://gitlab.example.com)." />
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ class="flex w-full items-center justify-between px-3 py-2.5 text-left text-[12px
|
|||
<polyline points="6 9 12 15 18 9"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
<div x-cloak x-show="open" x-collapse class="border-t border-neutral-200 px-3 py-3 dark:border-white/[0.08]">
|
||||
<div x-cloak x-show="open" x-collapse.duration.200ms class="border-t border-neutral-200 px-3 py-3 dark:border-white/[0.08]">
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<x-forms.input id="html_url" label="GitLab URL" required
|
||||
helper="For self-hosted GitLab, enter your instance URL (e.g. https://gitlab.example.com)." />
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ class="button button-highlighted">
|
|||
<div x-cloak x-show="viewMode === 'grid'" class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
@foreach ($s3 as $storage)
|
||||
<a x-show="matches(@js([$storage->name, $storage->description ?: 'S3-compatible storage', $storage->is_usable ? 'Connected' : 'Not usable']))" {{ wireNavigate() }} href="/storages/{{ $storage->uuid }}"
|
||||
class="group flex min-h-28 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]">
|
||||
class="group flex min-h-28 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]">
|
||||
<div class="flex items-start gap-3">
|
||||
<div
|
||||
class="flex size-8 shrink-0 items-center justify-center rounded-lg border border-neutral-200 bg-neutral-50 text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.04] dark:text-fg-dim">
|
||||
|
|
@ -74,8 +74,8 @@ class="flex size-8 shrink-0 items-center justify-center rounded-lg border border
|
|||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
<div x-show="viewMode === 'table'" class="overflow-x-auto rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.025]">
|
||||
<div class="grid min-w-[620px] grid-cols-[minmax(0,1fr)_minmax(10rem,.8fr)_9rem] border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-faint"><div>Storage</div><div>Description</div><div>Status</div></div>
|
||||
<div x-show="viewMode === 'table'" class="overflow-x-auto rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.05]">
|
||||
<div class="grid min-w-[620px] grid-cols-[minmax(0,1fr)_minmax(10rem,.8fr)_9rem] border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.05] dark:text-fg-faint"><div>Storage</div><div>Description</div><div>Status</div></div>
|
||||
@foreach ($s3 as $storage)
|
||||
<a x-show="matches(@js([$storage->name, $storage->description ?: 'S3-compatible storage', $storage->is_usable ? 'Connected' : 'Not usable']))" {{ wireNavigate() }} href="/storages/{{ $storage->uuid }}" class="grid min-h-14 min-w-[620px] grid-cols-[minmax(0,1fr)_minmax(10rem,.8fr)_9rem] items-center border-b border-neutral-200 px-4 py-2.5 text-[12px] transition-colors last:border-b-0 hover:bg-neutral-50 hover:no-underline dark:border-white/[0.07] dark:hover:bg-white/[0.025]">
|
||||
<div class="truncate font-semibold text-black dark:text-fg">{{ $storage->name }}</div>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ class="h-8! w-full rounded-lg! border-neutral-200! bg-white! py-0! pr-3! pl-8! t
|
|||
|
||||
<div class="overflow-x-auto">
|
||||
<div
|
||||
class="grid min-w-[780px] grid-cols-[minmax(12rem,1fr)_9rem_7rem_minmax(15rem,1.2fr)] border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-faint">
|
||||
class="grid min-w-[780px] grid-cols-[minmax(12rem,1fr)_9rem_7rem_minmax(15rem,1.2fr)] border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.05] dark:text-fg-faint">
|
||||
<div>Backup target</div>
|
||||
<div>Frequency</div>
|
||||
<div>Status</div>
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ class="app-tab h-6! px-2.5!"
|
|||
|
||||
<div class="grid gap-6 lg:grid-cols-[minmax(0,.8fr)_minmax(0,1.2fr)]">
|
||||
<div
|
||||
class="rounded-[10px] border border-neutral-200 bg-neutral-50 p-4 dark:border-white/[0.08] dark:bg-white/[0.025]">
|
||||
class="rounded-[10px] border border-neutral-200 bg-neutral-50 p-4 dark:border-white/[0.08] dark:bg-white/[0.05]">
|
||||
<p class="text-[11px] font-medium text-neutral-500 dark:text-fg-faint">Base price</p>
|
||||
<div class="mt-2 flex items-end gap-1.5">
|
||||
<span x-show="selected === 'monthly'" x-cloak
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ class="flex size-7.5 items-center justify-center rounded-md transition-colors"
|
|||
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<template x-for="tag in paginatedTags" :key="tag.id">
|
||||
<article
|
||||
class="group relative flex min-h-28 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]">
|
||||
class="group relative flex min-h-28 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]">
|
||||
<a :href="tag.href" {{ wireNavigate() }} class="absolute inset-0 rounded-xl"
|
||||
:aria-label="`Open ${tag.name}`"></a>
|
||||
|
||||
|
|
@ -121,15 +121,15 @@ class="flex size-8 shrink-0 items-center justify-center rounded-lg border border
|
|||
</article>
|
||||
</template>
|
||||
</div>
|
||||
<x-client-pagination x-show="filteredTags.length > 0" class="mt-3 rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.025]"
|
||||
<x-client-pagination x-show="filteredTags.length > 0" class="mt-3 rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.05]"
|
||||
summary="`${rangeStart}-${rangeEnd} of ${filteredTags.length}`" page-size-model="pageSize"
|
||||
storage-key="coolify.page-size.tags" :options="[12, 24, 48, 96]" />
|
||||
</div>
|
||||
|
||||
<div x-show="viewMode === 'table'"
|
||||
class="overflow-hidden rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.025]">
|
||||
class="overflow-hidden rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.05]">
|
||||
<div
|
||||
class="tags-table-grid border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-faint">
|
||||
class="tags-table-grid border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.05] dark:text-fg-faint">
|
||||
<div>Tag</div>
|
||||
<div>Resources</div>
|
||||
<div>Applications</div>
|
||||
|
|
@ -161,7 +161,7 @@ class="flex size-8 shrink-0 items-center justify-center rounded-lg border border
|
|||
</div>
|
||||
|
||||
<div x-show="filteredTags.length === 0"
|
||||
class="flex min-h-52 flex-col items-center justify-center rounded-xl border border-neutral-200 bg-white px-6 text-center dark:border-white/[0.08] dark:bg-white/[0.025]">
|
||||
class="flex min-h-52 flex-col items-center justify-center rounded-xl border border-neutral-200 bg-white px-6 text-center dark:border-white/[0.08] dark:bg-white/[0.05]">
|
||||
<x-reicon name="search" class="mb-3 size-6 text-neutral-300 dark:text-fg-faint" />
|
||||
<p class="text-[13px] font-medium">No matching tags</p>
|
||||
<p class="mt-1 text-[12px] text-neutral-500 dark:text-fg-dim">
|
||||
|
|
@ -250,7 +250,7 @@ function tagsIndex() {
|
|||
<div class="flex flex-col gap-6">
|
||||
<div class="grid grid-cols-1 gap-3 sm:grid-cols-3">
|
||||
<div
|
||||
class="rounded-xl border border-neutral-200 bg-white p-3 dark:border-white/[0.08] dark:bg-white/[0.025]">
|
||||
class="rounded-xl border border-neutral-200 bg-white p-3 dark:border-white/[0.08] dark:bg-white/[0.05]">
|
||||
<p class="text-[11px] font-medium text-neutral-500 dark:text-fg-faint">Resources</p>
|
||||
<p class="mt-1 text-[20px] font-semibold tracking-tight text-black dark:text-fg">
|
||||
{{ $resourceCount }}
|
||||
|
|
@ -260,7 +260,7 @@ class="rounded-xl border border-neutral-200 bg-white p-3 dark:border-white/[0.08
|
|||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="rounded-xl border border-neutral-200 bg-white p-3 dark:border-white/[0.08] dark:bg-white/[0.025]">
|
||||
class="rounded-xl border border-neutral-200 bg-white p-3 dark:border-white/[0.08] dark:bg-white/[0.05]">
|
||||
<p class="text-[11px] font-medium text-neutral-500 dark:text-fg-faint">Applications</p>
|
||||
<p class="mt-1 text-[20px] font-semibold tracking-tight text-black dark:text-fg">
|
||||
{{ $applications?->count() ?? 0 }}
|
||||
|
|
@ -270,7 +270,7 @@ class="rounded-xl border border-neutral-200 bg-white p-3 dark:border-white/[0.08
|
|||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="rounded-xl border border-neutral-200 bg-white p-3 dark:border-white/[0.08] dark:bg-white/[0.025]">
|
||||
class="rounded-xl border border-neutral-200 bg-white p-3 dark:border-white/[0.08] dark:bg-white/[0.05]">
|
||||
<p class="text-[11px] font-medium text-neutral-500 dark:text-fg-faint">Active deployments</p>
|
||||
<p class="mt-1 text-[20px] font-semibold tracking-tight text-black dark:text-fg">
|
||||
{{ collect($deploymentsPerTagPerServer ?? [])->flatten(1)->count() }}
|
||||
|
|
@ -308,7 +308,7 @@ class="rounded-xl border border-neutral-200 bg-white p-3 dark:border-white/[0.08
|
|||
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
@foreach ($applications ?? [] as $application)
|
||||
<a {{ wireNavigate() }} href="{{ $application->link() }}"
|
||||
class="group flex min-h-24 flex-col rounded-lg border border-neutral-200 bg-neutral-50/70 p-3 transition-colors hover:border-neutral-300 hover:no-underline dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]">
|
||||
class="group flex min-h-24 flex-col rounded-lg border border-neutral-200 bg-neutral-50/70 p-3 transition-colors hover:border-neutral-300 hover:no-underline dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]">
|
||||
<div class="flex items-start gap-2.5">
|
||||
<div
|
||||
class="flex size-7 shrink-0 items-center justify-center rounded-md border border-neutral-200 bg-white text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.035] dark:text-fg-dim">
|
||||
|
|
@ -332,7 +332,7 @@ class="truncate text-[12px]! leading-4! font-semibold! text-black dark:text-fg">
|
|||
|
||||
@foreach ($services ?? [] as $service)
|
||||
<a {{ wireNavigate() }} href="{{ $service->link() }}"
|
||||
class="group flex min-h-24 flex-col rounded-lg border border-neutral-200 bg-neutral-50/70 p-3 transition-colors hover:border-neutral-300 hover:no-underline dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]">
|
||||
class="group flex min-h-24 flex-col rounded-lg border border-neutral-200 bg-neutral-50/70 p-3 transition-colors hover:border-neutral-300 hover:no-underline dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]">
|
||||
<div class="flex items-start gap-2.5">
|
||||
<div
|
||||
class="flex size-7 shrink-0 items-center justify-center rounded-md border border-neutral-200 bg-white text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.035] dark:text-fg-dim">
|
||||
|
|
@ -374,7 +374,7 @@ class="truncate text-[12px]! leading-4! font-semibold! text-black dark:text-fg">
|
|||
</div>
|
||||
@else
|
||||
<div
|
||||
class="grid min-w-[620px] grid-cols-[minmax(0,1fr)_minmax(10rem,.55fr)_7rem] border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-faint">
|
||||
class="grid min-w-[620px] grid-cols-[minmax(0,1fr)_minmax(10rem,.55fr)_7rem] border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.05] dark:text-fg-faint">
|
||||
<div>Resource</div>
|
||||
<div>Server</div>
|
||||
<div>Status</div>
|
||||
|
|
|
|||
|
|
@ -176,11 +176,11 @@ class="px-3 py-8 text-center text-[13px] text-neutral-500 dark:text-fg-dim">
|
|||
</div>
|
||||
@else
|
||||
<div wire:key="terminal-session-canvas" data-terminal-session-canvas
|
||||
class="application-console-shell flex h-full min-h-0 flex-col overflow-hidden rounded-lg p-3 sm:p-6"
|
||||
class="application-console-shell flex h-full min-h-0 flex-col overflow-hidden rounded-lg"
|
||||
:data-console-theme="consoleTheme"
|
||||
:style="{ '--terminal-scrollbar': themeAccents[consoleTheme] }">
|
||||
<header
|
||||
class="terminal-session-toolbar absolute top-3 right-3 left-3 z-20 flex items-center gap-3 text-white select-none">
|
||||
class="terminal-session-toolbar flex items-center gap-3 text-white select-none">
|
||||
<div class="relative flex min-w-0 flex-1 items-center gap-2"
|
||||
x-on:click.outside="targetOpen = false">
|
||||
@if ($isLoadingContainers)
|
||||
|
|
@ -215,7 +215,7 @@ class="terminal-session-target-trigger flex h-8 min-w-0 max-w-sm cursor-pointer
|
|||
</button>
|
||||
|
||||
<div x-cloak x-show="targetOpen" x-transition.origin.top.left
|
||||
class="terminal-target-picker absolute top-11 left-0 z-50 w-80 overflow-hidden rounded-lg border shadow-[0_18px_50px_rgba(0,0,0,0.35)]">
|
||||
class="terminal-target-picker absolute top-11 left-0 z-50 w-80 overflow-hidden rounded-lg border shadow-[var(--shadow-dropdown)]">
|
||||
<div class="border-b border-white/[0.08] p-1.5">
|
||||
<div class="relative">
|
||||
<x-reicon name="search"
|
||||
|
|
@ -264,7 +264,7 @@ class="px-2 py-5 text-center text-[11px] text-white/35">
|
|||
:theme-accents="$consoleThemeAccents" />
|
||||
</header>
|
||||
|
||||
<div class="terminal-session-panel mt-8 flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||
<div class="terminal-session-panel flex min-h-0 flex-1 flex-col overflow-hidden p-3 sm:p-6">
|
||||
<div class="application-console-block min-h-0 flex-1 overflow-hidden">
|
||||
@if ($isLoadingContainers)
|
||||
<div class="flex h-full min-h-0 items-center justify-center">
|
||||
|
|
@ -287,7 +287,7 @@ class="px-2 py-5 text-center text-[11px] text-white/35">
|
|||
@else
|
||||
<div x-cloak x-show="!targetChosen" data-terminal-target-picker="launcher"
|
||||
class="absolute inset-0 z-20 flex items-start justify-start p-6 sm:p-10">
|
||||
<div class="terminal-target-picker w-full max-w-md overflow-hidden rounded-lg border shadow-[0_18px_50px_rgba(0,0,0,0.28)]">
|
||||
<div class="terminal-target-picker w-full max-w-md overflow-hidden rounded-lg border shadow-[var(--shadow-dropdown)]">
|
||||
<div class="border-b border-white/[0.08] p-2">
|
||||
<div class="px-1 pb-2">
|
||||
<div class="text-sm font-semibold text-white/80">Start a terminal session</div>
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ class="listbox-option justify-start! gap-2.5!" role="menuitem">
|
|||
<div x-cloak x-show="viewMode === 'grid'" class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
@foreach ($sources as $source)
|
||||
@if ($source->getMorphClass() === 'App\Models\GithubApp')
|
||||
<a x-show="matches(@js([$source->name, 'GitHub', $source->organization, $source->isConnected() ? 'Connected' : 'Setup incomplete']))" class="group flex min-h-28 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]"
|
||||
<a x-show="matches(@js([$source->name, 'GitHub', $source->organization, $source->isConnected() ? 'Connected' : 'Setup incomplete']))" class="group flex min-h-28 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]"
|
||||
{{ wireNavigate() }}
|
||||
href="{{ route('source.github.show', ['github_app_uuid' => data_get($source, 'uuid')]) }}">
|
||||
<div class="flex items-start gap-3">
|
||||
|
|
@ -102,7 +102,7 @@ class="flex size-8 shrink-0 items-center justify-center rounded-lg border border
|
|||
</div>
|
||||
</a>
|
||||
@elseif ($source->getMorphClass() === 'App\Models\GitlabApp')
|
||||
<a x-show="matches(@js([$source->name, 'GitLab', $source->group_name, $source->isConnected() ? 'Connected' : 'Setup incomplete']))" class="group flex min-h-28 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]"
|
||||
<a x-show="matches(@js([$source->name, 'GitLab', $source->group_name, $source->isConnected() ? 'Connected' : 'Setup incomplete']))" class="group flex min-h-28 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.05] dark:hover:border-white/[0.14]"
|
||||
{{ wireNavigate() }}
|
||||
href="{{ route('source.gitlab.show', ['gitlab_app_uuid' => data_get($source, 'uuid')]) }}">
|
||||
<div class="flex items-start gap-3">
|
||||
|
|
@ -131,8 +131,8 @@ class="flex size-8 shrink-0 items-center justify-center rounded-lg border border
|
|||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
<div x-show="viewMode === 'table'" class="overflow-x-auto rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.025]">
|
||||
<div class="grid min-w-[620px] grid-cols-[minmax(0,1fr)_minmax(10rem,.7fr)_9rem] border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-faint"><div>Source</div><div>Provider</div><div>Status</div></div>
|
||||
<div x-show="viewMode === 'table'" class="overflow-x-auto rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.05]">
|
||||
<div class="grid min-w-[620px] grid-cols-[minmax(0,1fr)_minmax(10rem,.7fr)_9rem] border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.05] dark:text-fg-faint"><div>Source</div><div>Provider</div><div>Status</div></div>
|
||||
@foreach ($sources as $source)
|
||||
@php
|
||||
$isGithub = $source->getMorphClass() === 'App\\Models\\GithubApp';
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
test('custom color theme is available and applied across theme controls', function () {
|
||||
$appearance = file_get_contents(resource_path('views/livewire/profile/appearance.blade.php'));
|
||||
$accountMenu = file_get_contents(resource_path('views/components/top-user-menu.blade.php'));
|
||||
$component = file_get_contents(resource_path('views/components/theme-controls.blade.php'));
|
||||
$picker = file_get_contents(resource_path('views/components/theme-controls/picker.blade.php'));
|
||||
$layout = file_get_contents(resource_path('views/layouts/base.blade.php'));
|
||||
$deploymentLogs = file_get_contents(resource_path('views/livewire/project/application/deployment/show.blade.php'));
|
||||
$serverTiming = file_get_contents(resource_path('views/components/server-timing-hud.blade.php'));
|
||||
|
|
@ -11,22 +13,23 @@
|
|||
$utilities = file_get_contents(resource_path('css/utilities.css'));
|
||||
|
||||
expect($appearance)
|
||||
->toContain('<x-theme-controls variant="full"')
|
||||
->and($accountMenu)
|
||||
->toContain('<x-theme-controls variant="menu"')
|
||||
->and($component)
|
||||
->toContain("['value' => 'custom', 'label' => 'Custom'")
|
||||
->toContain('<x-theme-controls.picker')
|
||||
->and($picker)
|
||||
->toContain('type="color"')
|
||||
->toContain('absolute inset-0 z-10 h-full w-full cursor-pointer opacity-0')
|
||||
->toContain('requestAnimationFrame(() =>')
|
||||
->toContain('@input="previewThemeColor($event.target.value)"')
|
||||
->toContain('@change="saveThemeColor($event.target.value)"')
|
||||
->toContain('aria-label="Custom theme color"')
|
||||
->toContain('setCustomMode')
|
||||
->and($layout)
|
||||
->toContain('window.themeControls')
|
||||
->toContain('requestAnimationFrame(() =>')
|
||||
->toContain("localStorage.setItem('themeColor', color)")
|
||||
->toContain("this.theme === 'custom'")
|
||||
->and($accountMenu)
|
||||
->toContain("['value' => 'custom', 'label' => 'Custom']")
|
||||
->toContain('aria-label="Custom theme color"')
|
||||
->toContain('requestAnimationFrame(() =>')
|
||||
->toContain('@input="previewThemeColor($event.target.value)"')
|
||||
->toContain('@change="saveThemeColor($event.target.value)"')
|
||||
->toContain("localStorage.setItem('themeColor', color)")
|
||||
->toContain('absolute inset-0 z-10 h-full w-full cursor-pointer opacity-0')
|
||||
->and($layout)
|
||||
->toContain("t === 'custom'")
|
||||
->toContain("localStorage.themeColor || '#6b16ed'")
|
||||
|
|
@ -35,34 +38,35 @@
|
|||
->toContain("document.addEventListener('livewire:navigated', window.applyStoredTheme)")
|
||||
->and($styles)
|
||||
->toContain('html[data-theme="custom"]')
|
||||
->toContain('--theme-base-color: #6b16ed;')
|
||||
->toContain('--theme-bright-color: color-mix(in srgb, var(--theme-base-color) 85%, white);')
|
||||
->toContain('--theme-base-color: oklch(49.65% 0.2709 289.33);')
|
||||
->toContain('--theme-bright-color: color-mix(in srgb, var(--theme-base-color) 85%, oklch(100% 0 0));')
|
||||
->toContain('--color-accent: var(--theme-bright-color);')
|
||||
->toContain('--color-coollabs: var(--theme-bright-color);')
|
||||
->toContain('--color-warning: var(--theme-bright-color);')
|
||||
->toContain('--color-accent-foreground: var(--theme-accent-foreground);')
|
||||
->toContain('--color-fg-dim: #e4dfea;')
|
||||
->toContain('--color-fg-faint: #d8d2df;')
|
||||
->toContain('--color-fg-dim: oklch(91.08% 0.0157 306.4);')
|
||||
->toContain('--color-fg-faint: oklch(87.24% 0.0188 306.63);')
|
||||
->toContain('html[data-theme="custom"] .control-selected')
|
||||
->toContain('--theme-scrollbar-thumb: color-mix(in srgb, var(--theme-bright-color) 70%, var(--theme-accent-foreground));')
|
||||
->toContain('--theme-border-color: color-mix(in oklab, var(--theme-base-color) 42%, #52525b);')
|
||||
->toContain('--theme-placeholder-color: color-mix(in srgb, white 20%, var(--theme-base-color));')
|
||||
->toContain('--theme-border-color: color-mix(in oklab, var(--theme-base-color) 42%, oklch(44.19% 0.0146 285.79));')
|
||||
->toContain('--theme-fg-on-surface: oklch(from var(--theme-base-color) clamp(0.72, l, 0.86) min(c, 0.13) h);')
|
||||
->toContain('--theme-placeholder-color: oklch(from var(--theme-base-color) 0.62 min(c, 0.03) h);')
|
||||
->toContain('html[data-theme="custom"] *')
|
||||
->toContain('scrollbar-color: var(--theme-scrollbar-thumb) var(--color-panel);')
|
||||
->toContain('html[data-theme="custom"] *::-webkit-scrollbar-thumb')
|
||||
->toContain('html[data-theme="custom"] .application-settings-navigation')
|
||||
->toContain('scrollbar-gutter: stable;')
|
||||
->toContain('--color-panel: color-mix(in oklab, var(--theme-base-color) 14%, #0c0c0d);')
|
||||
->toContain('--color-surface: color-mix(in oklab, var(--theme-base-color) 18%, #101011);')
|
||||
->toContain('--color-panel: color-mix(in oklab, var(--theme-base-color) 14%, oklch(15.48% 0.0021 286.15));')
|
||||
->toContain('--color-surface: color-mix(in oklab, var(--theme-base-color) 18%, oklch(17.35% 0.0020 286.18));')
|
||||
->toContain('--coollabs-elevated: var(--color-surface);')
|
||||
->toContain('--coollabs-recessed: var(--color-raised);')
|
||||
->toContain('--coollabs-base: color-mix(in oklab, var(--theme-base-color) 22%, #111112);')
|
||||
->toContain('--coollabs-base: color-mix(in oklab, var(--theme-base-color) 22%, oklch(17.81% 0.0020 286.19));')
|
||||
->toContain('--color-content-surface: var(--coollabs-base);')
|
||||
->toContain('--coollabs-fill: color-mix(in oklab, var(--theme-base-color) 32%, #18181a);')
|
||||
->toContain('--color-nav-text: #e4dfea;')
|
||||
->toContain('--color-nav-muted: #d8d2df;')
|
||||
->toContain('--color-log: color-mix(in oklab, var(--theme-base-color) 20%, #080809);')
|
||||
->toContain('--color-log-toolbar: color-mix(in oklab, var(--theme-base-color) 26%, #101011);')
|
||||
->toContain('--coollabs-fill: color-mix(in oklab, var(--theme-base-color) 32%, oklch(20.99% 0.0039 286.06));')
|
||||
->toContain('--color-nav-text: oklch(91.08% 0.0157 306.4);')
|
||||
->toContain('--color-nav-muted: oklch(87.24% 0.0188 306.63);')
|
||||
->toContain('--color-log: color-mix(in oklab, var(--theme-base-color) 20%, oklch(13.49% 0.0024 286.07));')
|
||||
->toContain('--color-log-toolbar: color-mix(in oklab, var(--theme-base-color) 26%, oklch(17.35% 0.0020 286.18));')
|
||||
->toContain('html[data-theme="custom"] .logs-viewer-toolbar')
|
||||
->toContain('html[data-theme="custom"] .logs-viewer-timestamp')
|
||||
->toContain('html[data-theme="custom"] #nprogress .bar')
|
||||
|
|
@ -78,7 +82,7 @@
|
|||
->toContain('border-color: var(--coollabs-hairline) !important;')
|
||||
->toContain('html[data-theme="custom"] input::placeholder')
|
||||
->toContain('color: var(--theme-placeholder-color) !important;')
|
||||
->toMatch('/html\[data-theme="custom"\] input::placeholder,[^{]+\{[^}]*opacity: 0\.7;/s')
|
||||
->toMatch('/html\[data-theme="custom"\] input::placeholder,[^{]+\{[^}]*opacity: 1;/s')
|
||||
->toContain('html[data-theme="custom"] input:read-only')
|
||||
->and($deploymentLogs)
|
||||
->toContain('dark:bg-log')
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
$applicationSection = str($view)->before('@else')->value();
|
||||
|
||||
expect($applicationSection)
|
||||
->toContain('<x-status-summary :status="$resource->status" />')
|
||||
->toContain('<x-status-summary :status="$resource->status" align="right" />')
|
||||
->not->toContain('<x-status :resource="$resource"');
|
||||
});
|
||||
|
||||
|
|
|
|||
111
tests/Feature/CustomThemeContrastTest.php
Normal file
111
tests/Feature/CustomThemeContrastTest.php
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* The custom theme derives its whole palette from one user-picked color.
|
||||
* These tests lock in two guarantees:
|
||||
* 1. The pipeline is authored entirely in OKLCH (no hex color literals).
|
||||
* 2. Foreground colors (accent text, placeholders) are clamped so they stay
|
||||
* readable on the dark surfaces regardless of the picked hue.
|
||||
*/
|
||||
function customThemeBlock(): string
|
||||
{
|
||||
$css = file_get_contents(resource_path('css/app.css'));
|
||||
|
||||
return str($css)
|
||||
->after('html[data-theme="custom"] {')
|
||||
->before('}')
|
||||
->toString();
|
||||
}
|
||||
|
||||
it('derives the custom theme entirely in oklch with no hex color literals', function () {
|
||||
expect(customThemeBlock())
|
||||
->not->toMatch('/#[0-9a-fA-F]{3,8}\b/')
|
||||
->toContain('--theme-base-color:')
|
||||
->toContain('oklch(');
|
||||
});
|
||||
|
||||
it('adds clamped, readable foreground tokens for the custom theme', function () {
|
||||
$block = customThemeBlock();
|
||||
|
||||
expect($block)
|
||||
// Accent text/icons on dark surfaces: lightness forced into a readable band.
|
||||
->toContain('--theme-fg-on-surface:')
|
||||
->toMatch('/--theme-fg-on-surface:\s*oklch\(from var\(--theme-base-color\)[^;]*clamp\(/')
|
||||
// Placeholder: light, near-neutral, guaranteed readable.
|
||||
->toContain('--theme-placeholder-color:')
|
||||
->toMatch('/--theme-placeholder-color:\s*oklch\(from var\(--theme-base-color\)/');
|
||||
});
|
||||
|
||||
it('routes custom-theme accent text utilities to the readable on-surface token', function () {
|
||||
$css = file_get_contents(resource_path('css/app.css'));
|
||||
|
||||
expect($css)
|
||||
->toMatch('/\[class~="text-warning"\][^{]*\{\s*color:\s*var\(--theme-fg-on-surface\)/')
|
||||
->toContain('[class~="text-coollabs"]');
|
||||
});
|
||||
|
||||
it('uses the clamped placeholder token for custom-theme inputs', function () {
|
||||
$css = file_get_contents(resource_path('css/app.css'));
|
||||
|
||||
expect($css)
|
||||
->toMatch('/html\[data-theme="custom"\] input::placeholder[^}]*var\(--theme-placeholder-color\)/');
|
||||
});
|
||||
|
||||
it('adds a light custom variant that tints surfaces toward white with dark foreground', function () {
|
||||
$css = file_get_contents(resource_path('css/app.css'));
|
||||
|
||||
expect($css)
|
||||
->toContain('html[data-theme="custom"]:not(.dark)')
|
||||
// Surfaces hold a fixed high lightness with injected base-hue chroma, so the
|
||||
// tint is visible (not a washed-out mix into white).
|
||||
->toMatch('/html\[data-theme="custom"\]:not\(\.dark\)[^}]*--color-app:\s*oklch\(from var\(--theme-base-color\) 9[0-9](\.[0-9]+)?% min\(c,/s')
|
||||
// Accent text/icons clamp to a dark, readable band on light surfaces.
|
||||
->toMatch('/html\[data-theme="custom"\]:not\(\.dark\)[^}]*--theme-fg-on-surface:\s*oklch\(from var\(--theme-base-color\) clamp\(0\.3/s');
|
||||
});
|
||||
|
||||
it('lets the theme scripts toggle a custom light or dark mode via customMode', function () {
|
||||
$base = file_get_contents(resource_path('views/layouts/base.blade.php'));
|
||||
$component = file_get_contents(resource_path('views/components/theme-controls.blade.php'));
|
||||
$picker = file_get_contents(resource_path('views/components/theme-controls/picker.blade.php'));
|
||||
$appearance = file_get_contents(resource_path('views/livewire/profile/appearance.blade.php'));
|
||||
$accountMenu = file_get_contents(resource_path('views/components/top-user-menu.blade.php'));
|
||||
|
||||
expect($base)
|
||||
// One shared Alpine factory holds the theme logic.
|
||||
->toContain('window.themeControls')
|
||||
->toContain('localStorage.customMode')
|
||||
->toContain('setCustomMode')
|
||||
// The picker no longer force-adds dark; it follows customMode.
|
||||
->not->toContain("document.documentElement.classList.add('dark')")
|
||||
// Light/dark lives inside the custom color picker popover.
|
||||
->and($picker)
|
||||
->toContain('customMode')
|
||||
->toContain('setCustomMode')
|
||||
->toContain('type="color"')
|
||||
->and($component)
|
||||
// Both variants open the shared picker popover for Custom.
|
||||
->toContain('<x-theme-controls.picker')
|
||||
->toContain('chooseCustom')
|
||||
// Both the page and the dropdown reuse the one component.
|
||||
->and($appearance)
|
||||
->toContain('<x-theme-controls variant="full"')
|
||||
->and($accountMenu)
|
||||
->toContain('<x-theme-controls variant="menu"');
|
||||
});
|
||||
|
||||
it('computes the theme base color and accent foreground in oklch, not hex', function () {
|
||||
$base = file_get_contents(resource_path('views/layouts/base.blade.php'));
|
||||
|
||||
$accentFn = str($base)
|
||||
->after('window.themeAccentForeground')
|
||||
->before('window.applyStoredTheme')
|
||||
->toString();
|
||||
|
||||
expect($base)
|
||||
->toContain('window.hexToOklch')
|
||||
->and($accentFn)
|
||||
->toContain('oklch(0% 0 0)')
|
||||
->toContain('oklch(100% 0 0)')
|
||||
->not->toContain('#000000')
|
||||
->not->toContain('#ffffff');
|
||||
});
|
||||
|
|
@ -1,5 +1,13 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
|
||||
test('input modal renders without requiring a modal id', function () {
|
||||
$html = Blade::render('<x-modal-input>Modal content</x-modal-input>');
|
||||
|
||||
expect($html)->toContain('Modal content');
|
||||
});
|
||||
|
||||
test('confirmation modal closes before dispatching an event that can open another modal', function () {
|
||||
$modal = file_get_contents(resource_path('views/components/modal-confirmation.blade.php'));
|
||||
|
||||
|
|
|
|||
|
|
@ -180,8 +180,8 @@
|
|||
|
||||
expect($view)
|
||||
->toContain('data-terminal-session-canvas')
|
||||
->toContain('terminal-session-toolbar absolute top-3 right-3 left-3')
|
||||
->toContain('terminal-session-panel mt-8')
|
||||
->toContain('terminal-session-toolbar flex items-center gap-3 text-white select-none')
|
||||
->toContain('terminal-session-panel flex min-h-0 flex-1 flex-col overflow-hidden p-3 sm:p-6')
|
||||
->toContain('<x-terminal.theme-selector')
|
||||
->not->toContain('application-console-header flex h-[30px]');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -45,6 +45,18 @@
|
|||
->toContain('stableFrames');
|
||||
});
|
||||
|
||||
test('application configuration subitems scroll without navigating the active page', function () {
|
||||
$sidebar = file_get_contents(resource_path('views/components/application/configuration-sidebar.blade.php'));
|
||||
|
||||
expect($sidebar)
|
||||
->toContain("@if (\$menuItem['active'])")
|
||||
->toContain('<button type="button" class="menu-subitem"')
|
||||
->toContain("window.scrollToSettingsSection?.('{{ \$section['id'] }}')")
|
||||
->toContain('@else')
|
||||
->toContain("href=\"{{ route(\$menuItem['route'], \$applicationRouteParameters) }}#{{ \$section['id'] }}\"")
|
||||
->not->toContain("if (document.getElementById('{{ \$section['id'] }}'))");
|
||||
});
|
||||
|
||||
test('postgresql general navigation lists each in-page settings section', function () {
|
||||
$sidebar = file_get_contents(resource_path('views/components/database/configuration-sidebar.blade.php'));
|
||||
$general = file_get_contents(resource_path('views/livewire/project/database/postgresql/general.blade.php'));
|
||||
|
|
|
|||
|
|
@ -27,7 +27,10 @@
|
|||
$layout = file_get_contents(resource_path('views/layouts/app.blade.php'));
|
||||
|
||||
expect($layout)
|
||||
->toContain("collapsed: localStorage.getItem('sidebarCollapsed') === 'true'")
|
||||
->toContain("userCollapsed: localStorage.getItem('sidebarCollapsed') === 'true'")
|
||||
->toContain('return this.userCollapsed || (this.autoCollapse && this.hasSecondBar);')
|
||||
->toContain('this.userCollapsed = this.collapsed;')
|
||||
->toContain("localStorage.setItem('sidebarCollapsed', this.userCollapsed);")
|
||||
->toContain('sidebarReady: false')
|
||||
->toContain(":class=\"[collapsed ? 'lg:w-16' : 'lg:w-56', sidebarReady ? 'transition-[width] duration-200' : '']\"")
|
||||
->toContain(":class=\"[collapsed ? 'lg:ml-16' : 'lg:ml-56', sidebarReady ? 'transition-[margin] duration-200' : '']\"");
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@
|
|||
expect($view)
|
||||
->toContain('data-terminal-session-canvas')
|
||||
->toContain('p-3 sm:p-6')
|
||||
->toContain('terminal-session-panel mt-8')
|
||||
->toContain('terminal-session-panel flex min-h-0 flex-1 flex-col overflow-hidden p-3 sm:p-6')
|
||||
->and($styles)
|
||||
->toContain('.terminal-session-panel')
|
||||
->toContain('border-radius: 0.75rem;')
|
||||
|
|
@ -145,7 +145,7 @@
|
|||
->and($selector)
|
||||
->toContain('terminal-theme-trigger flex h-8 items-center gap-2 rounded-md px-2.5 text-xs font-medium')
|
||||
->and($view)
|
||||
->toContain('terminal-session-toolbar absolute top-3 right-3 left-3')
|
||||
->toContain('terminal-session-toolbar flex items-center gap-3 text-white select-none')
|
||||
->not->toContain('terminal-theme-trigger');
|
||||
});
|
||||
|
||||
|
|
@ -177,7 +177,7 @@
|
|||
$view = file_get_contents(resource_path('views/livewire/terminal/index.blade.php'));
|
||||
|
||||
expect($view)
|
||||
->toContain('terminal-session-toolbar absolute top-3 right-3 left-3')
|
||||
->toContain('terminal-session-toolbar flex items-center gap-3 text-white select-none')
|
||||
->toContain('<x-terminal.theme-selector')
|
||||
->not->toContain('class="absolute top-3 right-3 z-20"');
|
||||
});
|
||||
|
|
@ -256,7 +256,7 @@
|
|||
->and($appCss)
|
||||
->toContain('[data-console-theme="system"]')
|
||||
->toContain('html:not(.dark) .application-console-shell[data-console-theme="system"]')
|
||||
->toContain('--console-theme-border: #d4d4d8')
|
||||
->toContain('--console-theme-border: oklch(87.11% 0.0055 286.29)')
|
||||
->toContain('background: transparent')
|
||||
->toContain('html.dark .application-console-shell[data-console-theme="system"]');
|
||||
});
|
||||
|
|
|
|||
57
tests/Unit/ShadowConsistencyTest.php
Normal file
57
tests/Unit/ShadowConsistencyTest.php
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
function cssSource(string $file): string
|
||||
{
|
||||
return file_get_contents(__DIR__.'/../../resources/css/'.$file);
|
||||
}
|
||||
|
||||
function bladeSource(string $path): string
|
||||
{
|
||||
return file_get_contents(__DIR__.'/../../resources/views/'.$path);
|
||||
}
|
||||
|
||||
it('gives the shared popover surface the dropdown lift, not the modal lift', function () {
|
||||
$utilities = cssSource('utilities.css');
|
||||
|
||||
expect($utilities)
|
||||
->toContain('@utility surface-popover')
|
||||
->toContain('var(--shadow-dropdown)');
|
||||
|
||||
// The surface-popover block itself must not fall back to the heavy modal shadow.
|
||||
preg_match('/@utility surface-popover\s*\{(.*?)\}/s', $utilities, $match);
|
||||
expect($match[1] ?? '')
|
||||
->toContain('var(--shadow-dropdown)')
|
||||
->not->toContain('var(--shadow-modal)');
|
||||
});
|
||||
|
||||
it('renders toasts and change-pending popovers with the shared dropdown-level surface', function (string $path) {
|
||||
$source = bladeSource($path);
|
||||
|
||||
expect($source)
|
||||
->toContain('surface-popover')
|
||||
->not->toContain('--shadow-modal');
|
||||
})->with([
|
||||
'components/toast.blade.php',
|
||||
'components/configuration-warning.blade.php',
|
||||
'components/proxy-configuration-warning.blade.php',
|
||||
'components/popup-small.blade.php',
|
||||
'livewire/deployments-indicator.blade.php',
|
||||
]);
|
||||
|
||||
it('uses the dropdown shadow on floating menus instead of the modal shadow', function (string $path) {
|
||||
expect(bladeSource($path))
|
||||
->toContain('shadow-dropdown')
|
||||
->not->toContain('shadow-modal');
|
||||
})->with([
|
||||
'livewire/server/show.blade.php',
|
||||
'livewire/project/application/deployment/show.blade.php',
|
||||
]);
|
||||
|
||||
it('gives the command palette the shared dropdown lift', function () {
|
||||
$app = cssSource('app.css');
|
||||
|
||||
preg_match('/\.command-palette\s*\{(.*?)\}/s', $app, $match);
|
||||
expect($match[1] ?? '')
|
||||
->toContain('var(--shadow-dropdown)')
|
||||
->not->toContain('var(--shadow-modal)');
|
||||
});
|
||||
Loading…
Reference in a new issue