Merge remote-tracking branch 'origin/next' into feat/railpack

This commit is contained in:
Andras Bacsai 2026-05-11 10:58:13 +02:00
commit 9f380b8495
16 changed files with 188 additions and 809 deletions

View file

@ -1,3 +1,7 @@
## Design Reference
For UI/UX design specifications, principles, and visual standards, consult `DESIGN.md` in the [coollabsio/architecture](https://github.com/coollabsio/architecture) repo.
<laravel-boost-guidelines>
=== foundation rules ===

View file

@ -6,6 +6,10 @@ ## Project Overview
Coolify is an open-source, self-hostable PaaS (alternative to Heroku/Netlify/Vercel). It manages servers, applications, databases, and services via SSH. Built with Laravel 12 (using Laravel 10 file structure), Livewire 3, and Tailwind CSS v4.
## Design Reference
For UI/UX design specifications, principles, and visual standards, consult `DESIGN.md` in the [coollabsio/architecture](https://github.com/coollabsio/architecture) repo.
## Development Environment
Docker Compose-based dev setup with services: coolify (app), postgres, redis, soketi (WebSockets), vite, testing-host, mailpit, minio.

752
DESIGN.md
View file

@ -1,752 +0,0 @@
---
version: alpha
name: Coolify
description: Self-hosted PaaS. Dark-first utilitarian UI. Purple (light) / yellow (dark) accent swap. Sharp 2px radii. Inset box-shadow inputs with 4px dirty-bar indicator.
colors:
# Brand
coollabs: "#6b16ed"
coollabs-50: "#f5f0ff"
coollabs-100: "#7317ff"
coollabs-200: "#5a12c7"
coollabs-300: "#4a0fa3"
# Dark-mode accent (warning scale)
warning: "#fcd452"
warning-50: "#fefce8"
warning-100: "#fef9c3"
warning-200: "#fef08a"
warning-300: "#fde047"
warning-400: "#fcd452"
warning-500: "#facc15"
warning-600: "#ca8a04"
warning-700: "#a16207"
warning-800: "#854d0e"
warning-900: "#713f12"
# Dark surfaces
base: "#101010"
coolgray-100: "#181818"
coolgray-200: "#202020"
coolgray-300: "#242424"
coolgray-400: "#282828"
coolgray-500: "#323232"
# Light surfaces (Tailwind neutrals)
surface: "#ffffff"
background: "#f9fafb"
border: "#e5e5e5"
text: "#000000"
text-muted: "#737373"
text-placeholder: "#d4d4d4"
# Semantic
success: "#22C55E"
error: "#dc2626"
primary: "{colors.coollabs}"
typography:
h1:
fontFamily: "'Geist Sans', Inter, sans-serif"
fontSize: 1.875rem
fontWeight: 700
lineHeight: 1.2
h2:
fontFamily: "'Geist Sans', Inter, sans-serif"
fontSize: 1.25rem
fontWeight: 700
h3:
fontFamily: "'Geist Sans', Inter, sans-serif"
fontSize: 1.125rem
fontWeight: 700
h4:
fontFamily: "'Geist Sans', Inter, sans-serif"
fontSize: 1rem
fontWeight: 700
body-md:
fontFamily: "'Geist Sans', Inter, sans-serif"
fontSize: 0.875rem
fontWeight: 400
lineHeight: 1.25rem
label-md:
fontFamily: "'Geist Sans', Inter, sans-serif"
fontSize: 0.875rem
fontWeight: 500
label-sm:
fontFamily: "'Geist Sans', Inter, sans-serif"
fontSize: 0.75rem
fontWeight: 700
lineHeight: 1rem
mono:
fontFamily: "'Geist Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace"
fontSize: 0.875rem
fontWeight: 400
rounded:
sm: 0.125rem # default — inputs, buttons, cards, modals
md: 0.25rem # coolbox
lg: 0.5rem # callouts
full: 9999px # badges, pills
spacing:
xs: 0.25rem
sm: 0.5rem
md: 1rem
lg: 1.5rem
xl: 2rem
section: 3rem
sidebar-width: 14rem
button-height: 2rem
card-min-height: 4rem
input-py: 0.375rem
components:
button:
backgroundColor: "{colors.surface}"
textColor: "{colors.text}"
rounded: "{rounded.sm}"
height: "{spacing.button-height}"
padding: 0 0.5rem
button-dark:
backgroundColor: "{colors.coolgray-100}"
textColor: "#ffffff"
button-hover:
backgroundColor: "#f5f5f5"
button-hover-dark:
backgroundColor: "{colors.coolgray-200}"
button-highlighted:
backgroundColor: "{colors.coollabs-50}"
textColor: "{colors.coollabs-200}"
button-highlighted-hover:
backgroundColor: "{colors.coollabs}"
textColor: "#ffffff"
button-error:
backgroundColor: "#fef2f2"
textColor: "#991b1b"
button-error-hover:
backgroundColor: "#fca5a5"
textColor: "#ffffff"
input:
backgroundColor: "{colors.surface}"
textColor: "{colors.text}"
rounded: "{rounded.sm}"
padding: 0.375rem 0.5rem
input-dark:
backgroundColor: "{colors.coolgray-100}"
textColor: "#ffffff"
textarea:
typography: "{typography.mono}"
backgroundColor: "{colors.surface}"
rounded: "{rounded.sm}"
box:
backgroundColor: "{colors.surface}"
textColor: "{colors.text}"
rounded: "{rounded.sm}"
padding: "{spacing.sm}"
height: "{spacing.card-min-height}"
box-dark:
backgroundColor: "{colors.coolgray-100}"
textColor: "#ffffff"
box-hover:
backgroundColor: "#f5f5f5"
box-hover-dark:
backgroundColor: "{colors.coollabs-100}"
textColor: "#ffffff"
coolbox:
backgroundColor: "{colors.surface}"
rounded: "{rounded.md}"
padding: "{spacing.sm}"
height: "{spacing.card-min-height}"
badge-success:
backgroundColor: "{colors.success}"
size: 0.75rem
rounded: "{rounded.full}"
badge-warning:
backgroundColor: "{colors.warning}"
size: 0.75rem
rounded: "{rounded.full}"
badge-error:
backgroundColor: "{colors.error}"
size: 0.75rem
rounded: "{rounded.full}"
deprecated-badge:
backgroundColor: "rgba(252, 212, 82, 0.15)"
textColor: "{colors.warning}"
rounded: "{rounded.full}"
padding: 0.125rem 0.5rem
callout-warning:
backgroundColor: "{colors.warning-50}"
textColor: "{colors.warning-800}"
rounded: "{rounded.lg}"
padding: "{spacing.md}"
callout-danger:
backgroundColor: "#fef2f2"
textColor: "#991b1b"
rounded: "{rounded.lg}"
padding: "{spacing.md}"
callout-info:
backgroundColor: "#eff6ff"
textColor: "#1e40af"
rounded: "{rounded.lg}"
padding: "{spacing.md}"
callout-success:
backgroundColor: "#f0fdf4"
textColor: "#166534"
rounded: "{rounded.lg}"
padding: "{spacing.md}"
dropdown:
backgroundColor: "{colors.surface}"
rounded: "{rounded.sm}"
padding: "{spacing.xs}"
dropdown-dark:
backgroundColor: "{colors.coolgray-200}"
dropdown-item-hover:
backgroundColor: "#f5f5f5"
dropdown-item-hover-dark:
backgroundColor: "{colors.coollabs}"
textColor: "#ffffff"
menu-item-active:
backgroundColor: "#e5e5e5"
textColor: "{colors.text}"
rounded: "{rounded.sm}"
menu-item-active-dark:
backgroundColor: "{colors.coolgray-200}"
textColor: "{colors.warning}"
tag:
backgroundColor: "#f5f5f5"
textColor: "{colors.text-muted}"
padding: 0.25rem 0.5rem
kbd:
rounded: "{rounded.sm}"
padding: 0 0.5rem
toast:
backgroundColor: "{colors.surface}"
rounded: "{rounded.sm}"
padding: "{spacing.md}"
width: 20rem
modal-input:
backgroundColor: "{colors.surface}"
rounded: "{rounded.sm}"
modal-input-dark:
backgroundColor: "{colors.base}"
modal-confirmation:
backgroundColor: "#f5f5f5"
rounded: "{rounded.sm}"
modal-confirmation-dark:
backgroundColor: "{colors.base}"
---
# Coolify Design System
## Overview
Coolify is a self-hosted PaaS (Heroku/Netlify/Vercel alternative) built with Laravel 12, Livewire 3, and Tailwind CSS v4. UI is **dark-first, dense, utilitarian** — operators want information density over whitespace.
Brand personality: precise, engineered, no-nonsense. No flourish. No gradients outside a single branded upsell. Flat surfaces differentiated by tonal depth, not shadow.
Two signature traits define the system:
1. **Purple/Yellow accent swap.** Light mode uses `coollabs` purple `#6b16ed`. Dark mode swaps to `warning` yellow `#fcd452` for focus rings, active nav items, helper icons, loading spinners, highlighted text, helper links. Never use purple as an accent in dark mode.
2. **Inset box-shadow inputs with a 4px "dirty bar".** Inputs and selects have no border — they use `box-shadow: inset 4px 0 0 transparent, inset 0 0 0 2px <border>`. When the field is focused or has unsaved changes (`wire:dirty`), the left 4px becomes the accent color — a live visual indicator of modified state. This is the single most distinctive UI detail in Coolify.
Sharp geometry everywhere: 2px corner radius by default (`rounded-sm`). 8px only on callouts. Shadows used sparingly — one `shadow-sm` on boxes, one drop-shadow on toasts. The rest is flat tonal layers.
## Colors
Source of truth: `resources/css/app.css` `@theme` block (Tailwind v4).
### Palettes
- **Primary / Coollabs (`#6b16ed`)** — brand purple. Light-mode accent. Used for focus rings, active states, highlighted buttons, spinners, scrollbar thumb. Scale: `coollabs-50 #f5f0ff` (backgrounds), `coollabs #6b16ed` (base), `coollabs-100 #7317ff` (dark-mode button hover), `coollabs-200 #5a12c7` (light-mode text), `coollabs-300 #4a0fa3` (deepest).
- **Warning (`#fcd452`)** — dark-mode accent + callout palette. Full yellow scale `warning-50` through `warning-900`. Swaps in for coollabs under `.dark`.
- **Coolgray (dark surface ladder)** — five shades building dark-mode depth: `base #101010` (page) → `coolgray-100 #181818` (components) → `-200 #202020` (elevated / active nav) → `-300 #242424` (input borders, button borders) → `-400 #282828` (tooltips) → `-500 #323232` (subtle overlays).
- **Semantic**`success #22C55E` for running/healthy, `error #dc2626` for stopped/danger.
- **Light surfaces**`gray-50 #f9fafb` (page), `white` (components), `neutral-200 #e5e5e5` (borders), `neutral-500 #737373` (muted text), `neutral-300 #d4d4d4` (placeholders).
### Dark-mode heading rule (critical)
Body default text in dark mode is `neutral-400 #a3a3a3`. Headings and card titles MUST explicitly force `text-white` — otherwise they render near-invisible on `coolgray-100 #181818`. This is enforced globally: `h1h4` all have `dark:text-white` in `app.css`.
### Default border override
Tailwind v4 defaults `border-color` to `currentcolor`. Coolify overrides it in `@layer base`:
```css
*, ::after, ::before, ::backdrop, ::file-selector-button {
border-color: var(--color-coolgray-200, currentcolor);
}
```
So any `border` utility without an explicit color gets `coolgray-200 #202020` in dark mode.
## Typography
Fonts loaded in `resources/css/fonts.css` (all `woff2`, `font-display: swap`):
- **Geist Sans** — primary UI font. Variable weight `100 900`. Inter as fallback (static weights 100900).
- **Geist Mono** — monospace for code, logs, textareas. Variable weight `100 900`.
Applied via `@theme`:
```css
--font-sans: 'Geist Sans', Inter, sans-serif;
--font-mono: 'Geist Mono', 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
--font-logs: 'Geist Mono', 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
```
### Heading hierarchy (Tailwind utilities)
| Element | Utility |
|---|---|
| `h1` | `text-3xl font-bold dark:text-white` |
| `h2` | `text-xl font-bold dark:text-white` |
| `h3` | `text-lg font-bold dark:text-white` |
| `h4` | `text-base font-bold dark:text-white` |
### Body
| Context | Utility |
|---|---|
| Body default | `text-sm font-sans antialiased` |
| Label | `text-sm font-medium` |
| Badge / status text | `text-xs font-bold` |
| Box description | `text-xs font-bold text-neutral-500` |
| Caption / kbd | `text-xs` |
## Layout
Fixed left sidebar layout on desktop. Mobile collapses to a sticky top bar with hamburger menu overlay.
### Structure
- **Sidebar** — fixed, `w-56` (14rem / 224px), `hidden lg:flex`. Inner `flex flex-col overflow-y-auto gap-y-5 scrollbar`. Nav `bg-white dark:bg-base border-r`.
- **Main content**`lg:pl-56` offset. Inner padding `p-4 sm:px-6 lg:px-8 lg:py-6`.
- **Mobile top bar**`sticky top-0 z-40 lg:hidden` with `bg-white/95 dark:bg-base/95 backdrop-blur-sm`.
### Spacing scale
| Token | Value | Use |
|---|---|---|
| `p-2` | 0.5rem | Component internal padding |
| `p-4` | 1rem | Callout padding |
| `py-1.5` | 0.375rem | Input vertical padding |
| `h-8` | 2rem | Button height |
| `px-2` | 0.5rem | Button horizontal padding |
| `gap-2` | 0.5rem | Button gap |
| `px-2 py-1` | 0.25rem / 0.5rem | Menu item padding |
| `gap-3` | 0.75rem | Menu item gap |
| `mb-12` | 3rem | Section margin |
| `min-h-[4rem]` | 4rem | Card min-height |
No grid system — flex layouts everywhere.
## Elevation & Depth
**Flat + tonal.** Hierarchy comes from background color, not shadows.
### Dark tonal ladder
```
#101010 (base) page background
#181818 (coolgray-100) cards, inputs, components
#202020 (coolgray-200) elevated surfaces, borders, nav active
#242424 (coolgray-300) input borders, button borders
#282828 (coolgray-400) tooltips, hover states
#323232 (coolgray-500) subtle overlays
```
### Light tonal ladder
```
#f9fafb (gray-50) page background
#ffffff (white) cards, inputs, components
#e5e5e5 (neutral-200) borders
#f5f5f5 (neutral-100) hover backgrounds
#d4d4d4 (neutral-300) deeper hover, nav active
```
### Shadows (used sparingly)
- Boxes: `shadow-sm` (`0 1px 2px 0 rgba(0,0,0,0.05)`)
- Toasts: `shadow-[0_5px_15px_-3px_rgb(0_0_0_/_0.08)]`
- Slide-over: `shadow-lg`
- Modal-input: `drop-shadow-sm`
### Input inset box-shadow system (distinctive)
Inputs and selects use `box-shadow` instead of `border` — this enables the 4px left dirty-bar indicator:
```css
/* default */ box-shadow: inset 4px 0 0 transparent, inset 0 0 0 2px #e5e5e5;
/* default dark */ inset 4px 0 0 transparent, inset 0 0 0 2px #242424;
/* focus light */ inset 4px 0 0 #6b16ed, inset 0 0 0 2px #e5e5e5;
/* focus dark */ inset 4px 0 0 #fcd452, inset 0 0 0 2px #242424;
/* dirty (same as focus) — set via wire:dirty.class */
/* disabled / readonly */ box-shadow: none;
```
Variant `input-sticky` uses `1px` outer shadow instead of `2px`.
### Focus ring (buttons, links, checkboxes, non-input)
`focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-coollabs dark:focus-visible:ring-warning focus-visible:ring-offset-2 dark:focus-visible:ring-offset-base`
## Shapes
- **Default**`rounded-sm` (2px). Everything: inputs, buttons, cards, modals, toasts, dropdowns.
- **Coolbox**`rounded` (4px). Alternate card style with ring-hover.
- **Callouts**`rounded-lg` (8px). Only exception to the sharp rule.
- **Badges / deprecated badge / pills / avatars**`rounded-full`.
Never mix radii within the same view.
## Components
All component classes live in `resources/css/utilities.css` as `@utility` blocks, consumed by Blade components under `resources/views/components/`.
### Forms
#### Button
Utility `.button` (`resources/css/utilities.css`):
```
flex gap-2 justify-center items-center px-2 h-8 text-sm text-black normal-case rounded-sm border-2 outline-0 cursor-pointer font-medium bg-white border-neutral-200 hover:bg-neutral-100 dark:bg-coolgray-100 dark:text-white dark:hover:text-white dark:hover:bg-coolgray-200 dark:border-coolgray-300 hover:text-black disabled:cursor-not-allowed min-w-fit dark:disabled:text-neutral-600 disabled:border-transparent disabled:hover:bg-transparent disabled:bg-transparent disabled:text-neutral-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-coollabs dark:focus-visible:ring-warning focus-visible:ring-offset-2 dark:focus-visible:ring-offset-base
```
Attribute variants (in `app.css`):
- `button[isHighlighted]``text-coollabs-200 dark:text-white bg-coollabs-50 dark:bg-coollabs/20 border-coollabs dark:border-coollabs-100 hover:bg-coollabs hover:text-white dark:hover:bg-coollabs-100 dark:hover:text-white`
- `button[isError]``text-red-800 dark:text-red-300 bg-red-50 dark:bg-red-900/30 border-red-300 dark:border-red-800 hover:bg-red-300 hover:text-white dark:hover:bg-red-800 dark:hover:text-white`
Loading: `<x-loading-on-button>` — inline `w-4 h-4 dark:text-warning animate-spin` SVG.
#### Input
Utility chain `.input-select``.input`:
```
block py-1.5 w-full text-sm text-black rounded-sm border-0 dark:bg-coolgray-100 dark:text-white disabled:bg-neutral-200 disabled:text-neutral-500 dark:disabled:bg-coolgray-100/40 placeholder:text-neutral-300 dark:placeholder:text-neutral-700 read-only:text-neutral-500 read-only:bg-neutral-200 dark:read-only:text-neutral-500 dark:read-only:bg-coolgray-100/40 focus-visible:outline-none
```
Plus the inset box-shadow system (see Elevation). Password variant: `.input[type="password"]` gets `pr-[2.4rem]` for the eye icon.
**Dirty indicator.** Livewire sets the focus-colored shadow via `wire:dirty.class`:
```blade
wire:dirty.class="[box-shadow:inset_4px_0_0_#6b16ed,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fcd452,inset_0_0_0_2px_#242424]"
```
Variant `.input-sticky` — same shape, `1px` outer shadow (thinner border).
#### Select
Extends `.input-select` + custom SVG dropdown arrow:
```css
background-image: url("data:image/svg+xml,...stroke='%23000000'...");
padding-right: 2.5rem;
```
Dark mode swaps the SVG stroke to `%23ffffff`.
#### Checkbox
Input class:
```
dark:border-neutral-700 text-coolgray-400 dark:bg-coolgray-100 rounded-sm cursor-pointer dark:disabled:bg-base dark:disabled:cursor-not-allowed focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-coollabs dark:focus-visible:ring-warning focus-visible:ring-offset-2 dark:focus-visible:ring-offset-base
```
Container:
```
form-control flex max-w-full flex-row items-center gap-4 py-1 pr-2 dark:hover:bg-coolgray-100 cursor-pointer
```
#### Textarea
Uses the same `input` utility + `font-mono` + dirty-bar via `wire:dirty.class` (identical to input). Optional `@keydown.tab=handleKeydown` inserts 2 spaces on Tab.
#### Copy-Button
`resources/views/components/forms/copy-button.blade.php` — readonly `.input` with an absolute-positioned copy icon right-side. Copied state shows a green check (`text-green-500`) for 1 second. Only renders in secure contexts (`window.isSecureContext`).
### Containers
#### Box
Utility `.box`:
```
relative flex lg:flex-row flex-col p-2 transition-colors cursor-pointer min-h-[4rem] dark:bg-coolgray-100 shadow-sm bg-white border text-black dark:text-white hover:text-black border-neutral-200 dark:border-coolgray-300 hover:bg-neutral-100 dark:hover:bg-coollabs-100 dark:hover:text-white hover:no-underline rounded-sm
```
**Critical child text rule.** On dark hover, background becomes purple `#7317ff` — description text `#737373` disappears. Utilities `.box-title` and `.box-description` include `dark:group-hover:text-white group-hover:text-black` to flip text contrast.
Variants: `.box-boarding`, `.box-without-bg`, `.box-without-bg-without-border`.
#### Coolbox
Utility `.coolbox`:
```
relative flex transition-all duration-150 dark:bg-coolgray-100 bg-white p-2 rounded border border-neutral-200 dark:border-coolgray-400 hover:ring-2 dark:hover:ring-warning hover:ring-coollabs cursor-pointer min-h-[4rem]
```
Distinguished by `rounded` (4px, not 2px) and **ring-hover** instead of background change.
### Status & Badges
#### Badge base
```
inline-block w-3 h-3 text-xs font-bold rounded-full leading-none border border-neutral-200 dark:border-black
```
Fill utilities: `.badge-success` (`bg-success`), `.badge-warning` (`bg-warning`), `.badge-error` (`bg-error`). Dashboard variant `.badge-dashboard` is `absolute top-1 right-1 w-2.5 h-2.5`.
#### Status indicator pattern
Badge + label side-by-side. Components in `resources/views/components/status/`:
| Component | Badge | Text color | Loading? |
|---|---|---|---|
| `status/running` | `badge-success` | `text-success` (`#22C55E`) | Swaps to `badge-warning` while checking proxy |
| `status/degraded` | `badge-warning` | `dark:text-warning` (`#fcd452`) | `<x-loading>` + `wire:loading.delay.longer` |
| `status/restarting` | `badge-warning` | `dark:text-warning` | `<x-loading>` |
| `status/stopped` | `badge-error` | `text-error` (`#dc2626`) | `<x-loading>` |
Layout: `<div class="flex items-center">` → badge → `<div class="pl-2 pr-1 text-xs font-bold {color}">{label}</div>` → optional `({health})` in same color.
#### Deprecated Badge
`resources/views/components/deprecated-badge.blade.php`:
```
px-2 py-0.5 text-xs font-medium leading-normal rounded-full bg-warning/15 text-warning border border-warning/30
```
#### Tag
Utility `.tag`:
```
px-2 py-1 cursor-pointer box-description dark:bg-coolgray-100 dark:hover:bg-coolgray-300 bg-neutral-100 hover:bg-neutral-200
```
### Overlays
#### Callout
Four types (`warning`, `danger`, `info`, `success`). Base: `relative p-4 border rounded-lg`.
| Type | Background | Border | Title text | Body text |
|---|---|---|---|---|
| warning | `bg-warning-50 dark:bg-warning-900/30` | `border-warning-300 dark:border-warning-800` | `text-warning-800 dark:text-warning-300` | `text-warning-700 dark:text-warning-200` |
| danger | `bg-red-50 dark:bg-red-900/30` | `border-red-300 dark:border-red-800` | `text-red-800 dark:text-red-300` | `text-red-700 dark:text-red-200` |
| info | `bg-blue-50 dark:bg-blue-900/30` | `border-blue-300 dark:border-blue-800` | `text-blue-800 dark:text-blue-300` | `text-blue-700 dark:text-blue-200` |
| success | `bg-green-50 dark:bg-green-900/30` | `border-green-300 dark:border-green-800` | `text-green-800 dark:text-green-300` | `text-green-700 dark:text-green-200` |
Icon colors (600 light / 400 dark) match type.
#### Modal (input variant)
`resources/views/components/modal.blade.php`:
```
relative w-full lg:w-auto lg:min-w-2xl lg:max-w-4xl border rounded-sm drop-shadow-sm bg-white border-neutral-200 dark:bg-base dark:border-coolgray-300 flex flex-col
```
Backdrop: `bg-black/20 backdrop-blur-xs`. Close button: `w-8 h-8 rounded-full hover:bg-neutral-100 dark:hover:bg-coolgray-300` top-right, 24px `stroke-width=1.5` X icon.
#### Modal Confirmation
`resources/views/components/modal-confirmation.blade.php` — destructive-action 2-or-3-step wizard (checkboxes → confirm text → password):
```
relative w-full border rounded-none sm:rounded-sm min-w-full lg:min-w-[36rem] max-w-full sm:max-w-[48rem] h-screen sm:h-auto max-h-screen sm:max-h-[calc(100vh-2rem)] bg-neutral-100 border-neutral-400 dark:bg-base dark:border-coolgray-300 flex flex-col
```
Uses `<x-callout type="danger">` for warning. Password step hidden for OAuth users.
#### Confirm Modal
`resources/views/components/confirm-modal.blade.php` — Livewire-bound simpler confirm dialog.
#### Popup / Popup-Small
Fixed bottom-right notification card with title / description / action button. `bg-white dark:bg-coolgray-100 border dark:border-coolgray-300 shadow-lg sm:rounded-sm`. Popup is responsive max-w-4xl, Popup-Small is `max-w-[46rem]`.
#### Slide-Over
`resources/views/components/slide-over.blade.php`:
Outer: `fixed inset-y-0 right-0 flex max-w-full pl-10`
Panel: `max-w-xl w-screen 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`
#### Toast
`resources/views/components/toast.blade.php` — Alpine-powered stacked toast system.
- Container: `fixed ... sm:max-w-xs z-9999`, positioned via `position` param (`top-right` / `top-left` / `top-center` / `bottom-right` / `bottom-left` / `bottom-center`).
- Toast shell: `relative flex flex-col items-start shadow-[0_5px_15px_-3px_rgb(0_0_0_/_0.08)] w-full dark:bg-coolgray-100 bg-white dark:border dark:border-coolgray-200 rounded-sm sm:max-w-xs`.
- Stacks up to 4 (oldest gets scale 82% then burns).
- Auto-dismiss after 4 s. Hover on container pauses dismissal and expands stack.
- HTML payload sanitized via `window.sanitizeHTML` (XSS guard).
- Per-toast copy-to-clipboard + close buttons.
Icon colors:
| Type | Class |
|---|---|
| success | `text-green-500` |
| info | `text-blue-500` |
| warning | `text-orange-400` |
| danger | `text-red-500` |
| default | `text-gray-800` |
#### Helper / Tooltip
`resources/views/components/helper.blade.php`. Icon utility `.info-helper`:
```
cursor-pointer text-coollabs dark:text-warning
```
Popup utility `.info-helper-popup`:
```
hidden absolute z-40 text-xs rounded-sm text-neutral-700 group-hover:block dark:border-coolgray-500 border-neutral-900 dark:bg-coolgray-400 bg-neutral-200 dark:text-neutral-300 max-w-sm whitespace-normal break-words
```
Shown on parent `.group:hover`. Supports rich HTML (links colored `text-coollabs dark:text-warning underline`).
### Navigation
#### Sidebar / Navbar
Component: `resources/views/components/navbar.blade.php`.
Root nav: `flex flex-col flex-1 px-2 bg-white border-r dark:border-coolgray-200 border-neutral-300 dark:bg-base`
Menu list: `flex flex-col flex-1 gap-y-7` → inner `flex flex-col h-full space-y-1.5`.
Utility `.menu-item`:
```
flex gap-3 items-center px-2 py-1 w-full text-sm dark:hover:bg-coolgray-100 dark:hover:text-white hover:bg-neutral-300 rounded-sm truncate min-w-0
```
Utility `.menu-item-active`:
```
text-black rounded-sm dark:bg-coolgray-200 dark:text-warning bg-neutral-200 overflow-hidden
```
Icon `.menu-item-icon`: `flex-shrink-0 w-6 h-6 dark:hover:text-white`. Sub-items use `gap-2` + `w-4 h-4` icons.
#### Breadcrumbs
`resources/views/components/resources/breadcrumbs.blade.php` — project → environment → resource trail. Desktop: `<ol class="hidden flex-wrap items-center gap-y-1 md:flex">`. Each link `text-xs lg:text-sm hover:text-warning`. Chevron buttons `text-warning`. Dropdowns `absolute ... bg-white dark:bg-coolgray-100 rounded-md shadow-lg border`. Active item `dark:text-warning font-semibold`.
#### External-Link
Mini icon — `inline-flex w-3 h-3 dark:text-neutral-400 text-black` with arrow-out-of-box SVG. Appended to external anchors.
#### Internal-Link
Arrow SVG — `inline-flex w-4 h-4 text-black dark:text-white`. Used in CTA links ("go to deployment" etc).
#### Banner
`resources/views/components/banner.blade.php` — dismissible top bar:
```
relative z-999 w-full py-2 mx-auto duration-100 ease-out shadow-xs bg-coolgray-100 sm:py-0 sm:h-14
```
Close button: `w-6 h-6 rounded-full hover:bg-coolgray-500 text-neutral-200`. Reveals via Alpine `x-transition` after 100ms delay.
### Feedback
#### Loading Spinner
`resources/views/components/loading.blade.php` — inline flex with optional text + spinning SVG:
```
w-4 h-4 mx-1 ml-3 text-coollabs dark:text-warning animate-spin
```
SVG has two paths at `opacity-25` (track) + `opacity-75` (arc).
Utility `.loading`: `w-4 dark:text-warning text-coollabs`.
#### Loading-On-Button
`resources/views/components/loading-on-button.blade.php` — same SVG but **no light-mode color** (`w-4 h-4 mx-1 ml-3 dark:text-warning animate-spin`), meant to inherit button text color.
#### Page-Loading
Full-page loader overlay (variant of `loading` component, fills viewport).
### Text
#### Highlighted text
`resources/views/components/highlighted.blade.php` / utility `.text-helper`:
```
inline-block font-bold text-coollabs dark:text-warning
```
Also used for required-field asterisks via `<x-highlighted text="*" />`.
#### Kbd
Utility `.kbd-custom`:
```
px-2 text-xs rounded-sm border border-dashed border-neutral-700 dark:text-warning
```
### Chrome
#### Scrollbar
Utility `.scrollbar` (uses `tailwind-scrollbar` plugin):
```
scrollbar-thumb-coollabs-100 scrollbar-track-neutral-200 dark:scrollbar-track-coolgray-200 scrollbar-thin
```
Applied globally to `<body>` in `app.css`.
#### Table
Styled via base element rules in `app.css` (not a reusable component):
```css
table { @apply min-w-full divide-y dark:divide-coolgray-200 divide-neutral-300; }
thead { @apply uppercase; }
tbody { @apply divide-y dark:divide-coolgray-200 divide-neutral-300; }
tr { @apply text-black dark:text-neutral-400 dark:hover:bg-coolgray-300 hover:bg-neutral-100; }
tr th { @apply px-3 py-3.5 text-left text-black dark:text-white; }
tr th:first-child { @apply py-3.5 pr-3 pl-4 sm:pl-6; }
tr td { @apply px-3 py-4 whitespace-nowrap; }
tr td:first-child { @apply pr-3 pl-4 font-bold sm:pl-6; }
```
#### Dropdown
`resources/views/components/dropdown.blade.php`. Container:
```
border border-neutral-300 bg-white p-1 shadow-sm dark:border-coolgray-300 dark:bg-coolgray-200
```
Utility `.dropdown-item`:
```
flex relative gap-2 justify-start items-center py-1 pr-4 pl-2 w-full text-xs transition-colors cursor-pointer select-none dark:text-white hover:bg-neutral-100 dark:hover:bg-coollabs outline-none data-disabled:pointer-events-none data-disabled:opacity-50 focus-visible:bg-neutral-100 dark:focus-visible:bg-coollabs
```
Touch variant adds `min-h-10 px-3 py-2 text-sm`.
## Do's and Don'ts
- **Do** force `dark:text-white` on h1h4 and card titles. Default body text `#a3a3a3` is unreadable on `coolgray-100`.
- **Do** swap the accent: `coollabs` in light, `warning` in dark. For focus rings, active nav, helpers, spinners, highlighted text, scrollbar thumb, helper links.
- **Do** use the inset box-shadow system on inputs, selects, and textareas — not a border. It enables the 4px left dirty-bar.
- **Do** wire the dirty indicator via `wire:dirty.class` so Livewire flips the bar color on modified state.
- **Do** flip `.box-title` and `.box-description` to the contrast color on hover. On dark hover the card goes purple `#7317ff`; `text-neutral-500` description becomes invisible.
- **Do** maintain WCAG AA contrast (4.5:1 for normal text).
- **Do** sanitize HTML passed into toasts via `window.sanitizeHTML`.
- **Do** use `<x-loading>` for in-button spinners and as `wire:loading.delay.longer` indicators in status components.
- **Don't** use purple `coollabs` as the dark-mode accent. Always use yellow `warning` in dark.
- **Don't** mix corner radii — 2px everywhere except callouts (8px) and pills (full).
- **Don't** use shadows for elevation in dark mode. Use tonal layers from the coolgray ladder.
- **Don't** set `border` utilities without expecting `coolgray-200` in dark (default override in base layer).
- **Don't** add gradients. The one exception is the `.bg-coollabs-gradient` upsell strip.
- **Don't** use more than two font weights on a single screen (typically 400 body + 700 bold).
---
Source files:
- Theme tokens: `resources/css/app.css` (`@theme` block)
- Fonts: `resources/css/fonts.css`
- Component utilities: `resources/css/utilities.css`
- Blade components: `resources/views/components/**/*.blade.php`

View file

@ -1400,23 +1400,23 @@ function generateEnvValue(string $command, Service|Application|null $service = n
break;
// This is base64,
case 'REALBASE64_64':
$generatedValue = base64_encode(Str::random(64));
$generatedValue = base64_encode(random_bytes(64));
break;
case 'REALBASE64_128':
$generatedValue = base64_encode(Str::random(128));
$generatedValue = base64_encode(random_bytes(128));
break;
case 'REALBASE64':
case 'REALBASE64_32':
$generatedValue = base64_encode(Str::random(32));
$generatedValue = base64_encode(random_bytes(32));
break;
case 'HEX_32':
$generatedValue = bin2hex(Str::random(32));
$generatedValue = bin2hex(random_bytes(16));
break;
case 'HEX_64':
$generatedValue = bin2hex(Str::random(64));
$generatedValue = bin2hex(random_bytes(32));
break;
case 'HEX_128':
$generatedValue = bin2hex(Str::random(128));
$generatedValue = bin2hex(random_bytes(64));
break;
case 'USER':
$generatedValue = Str::random(16);

View file

@ -1,7 +0,0 @@
{
"scripts": {
"setup": "./scripts/conductor-setup.sh",
"run": "docker rm -f coolify coolify-minio-init coolify-realtime coolify-minio coolify-testing-host coolify-redis coolify-db coolify-mail coolify-vite; spin up; spin down"
},
"runScriptMode": "nonconcurrent"
}

View file

@ -343,3 +343,12 @@ @utility log-debug {
@utility log-info {
@apply bg-blue-500/10 dark:bg-blue-500/15;
}
@media (min-width: 1024px) {
.sidebar-collapsed .menu-item {
justify-content: center;
padding-left: 0;
padding-right: 0;
gap: 0;
}
}

View file

@ -229,7 +229,7 @@ class="flex absolute inset-y-0 right-0 z-10 items-center pr-2 cursor-pointer dar
@readonly($readonly)
@if ($modelBinding !== 'null')
wire:model="{{ $modelBinding }}"
wire:dirty.class="dark:border-l-warning border-l-coollabs border-l-4"
wire:dirty.class="[box-shadow:inset_4px_0_0_#6b16ed,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fcd452,inset_0_0_0_2px_#242424]"
@endif
wire:loading.attr="disabled"
@disabled($disabled)

View file

@ -1,5 +1,20 @@
<nav class="flex flex-col flex-1 px-2 bg-white border-r dark:border-coolgray-200 border-neutral-300 dark:bg-base"
<nav class="flex flex-col flex-1 bg-white border-r dark:border-coolgray-200 border-neutral-300 dark:bg-base"
:class="collapsed ? 'lg:px-1 px-2 sidebar-collapsed' : 'px-2'"
@mouseover="
if (!collapsed) return;
const el = $event.target.closest('.menu-item');
if (!el) { tooltip.show = false; return; }
const text = el.getAttribute('title') || el.getAttribute('aria-label') || '';
if (!text) return;
const rect = el.getBoundingClientRect();
tooltip.text = text;
tooltip.x = rect.right + 8;
tooltip.y = rect.top + rect.height / 2;
tooltip.show = true;
"
@mouseleave="tooltip.show = false"
x-data="{
tooltip: { text: '', x: 0, y: 0, show: false },
switchWidth() {
if (this.full === 'full') {
localStorage.setItem('pageWidth', 'center');
@ -77,12 +92,22 @@
}
}
}">
<div class="flex lg:pt-6 pt-4 pb-4 pl-2">
<div class="flex flex-col w-full">
<div class="flex pt-4 pb-4 pl-2 items-start gap-2"
:class="collapsed ? 'lg:flex-col lg:items-center lg:pl-0 lg:gap-3 lg:pt-8' : 'lg:pt-6'">
<div class="flex flex-col w-full" :class="collapsed && 'lg:hidden'">
<a href="/" {{ wireNavigate() }} class="text-2xl font-bold tracking-tight dark:text-white hover:opacity-80 transition-opacity">Coolify</a>
<x-version />
</div>
<div>
<div class="hidden flex-col items-center w-full gap-1"
:class="collapsed && 'lg:flex'">
<a href="/" {{ wireNavigate() }}
class="hover:opacity-80 transition-opacity"
title="Coolify">
<img src="/coolify-logo.svg" alt="Coolify" class="w-6 h-6" />
</a>
<x-version class="text-[10px]" />
</div>
<div :class="collapsed && 'lg:hidden'">
<!-- Search button that triggers global search modal -->
<button @click="$dispatch('open-global-search')" type="button" title="Search (Press / or ⌘K)"
class="flex items-center gap-1.5 px-2.5 py-1.5 bg-neutral-100 dark:bg-coolgray-100 border border-neutral-300 dark:border-coolgray-200 rounded-md hover:bg-neutral-200 dark:hover:bg-coolgray-200 transition-colors">
@ -95,9 +120,11 @@ class="flex items-center gap-1.5 px-2.5 py-1.5 bg-neutral-100 dark:bg-coolgray-1
class="px-1 py-0.5 text-xs font-semibold text-neutral-500 dark:text-neutral-400 bg-neutral-200 dark:bg-coolgray-200 rounded">/</kbd>
</button>
</div>
<livewire:settings-dropdown />
<div :class="collapsed && 'lg:hidden'">
<livewire:settings-dropdown />
</div>
</div>
<div class="px-2 pt-2 pb-7">
<div class="px-2 pt-2 pb-7" :class="collapsed && 'lg:px-0 lg:pt-0 lg:pb-4 lg:flex lg:justify-center'">
<livewire:switch-team />
</div>
<ul role="list" class="flex flex-col flex-1 gap-y-7">
@ -112,7 +139,7 @@ class="{{ request()->is('/') ? 'menu-item-active menu-item' : 'menu-item' }}">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
</svg>
<span class="menu-item-label">Dashboard</span>
<span class="menu-item-label" :class="collapsed && 'lg:hidden'">Dashboard</span>
</a>
</li>
<li>
@ -127,7 +154,7 @@ class="{{ request()->is('project/*') || request()->is('projects') ? 'menu-item m
<path d="M4 12l8 4l8 -4" />
<path d="M4 16l8 4l8 -4" />
</svg>
<span class="menu-item-label">Projects</span>
<span class="menu-item-label" :class="collapsed && 'lg:hidden'">Projects</span>
</a>
</li>
<li>
@ -145,7 +172,7 @@ class="{{ request()->is('server/*') || request()->is('servers') ? 'menu-item men
<path d="M7 16v.01" />
<path d="M20 15l-2 3h3l-2 3" />
</svg>
<span class="menu-item-label">Servers</span>
<span class="menu-item-label" :class="collapsed && 'lg:hidden'">Servers</span>
</a>
</li>
@ -157,7 +184,7 @@ class="{{ request()->is('source*') ? 'menu-item-active menu-item' : 'menu-item'
<path fill="currentColor"
d="m6.793 1.207l.353.354l-.353-.354ZM1.207 6.793l-.353-.354l.353.354Zm0 1.414l.354-.353l-.354.353Zm5.586 5.586l-.354.353l.354-.353Zm1.414 0l-.353-.354l.353.354Zm5.586-5.586l.353.354l-.353-.354Zm0-1.414l-.354.353l.354-.353ZM8.207 1.207l.354-.353l-.354.353ZM6.44.854L.854 6.439l.707.707l5.585-5.585L6.44.854ZM.854 8.56l5.585 5.585l.707-.707l-5.585-5.585l-.707.707Zm7.707 5.585l5.585-5.585l-.707-.707l-5.585 5.585l.707.707Zm5.585-7.707L8.561.854l-.707.707l5.585 5.585l.707-.707Zm0 2.122a1.5 1.5 0 0 0 0-2.122l-.707.707a.5.5 0 0 1 0 .708l.707.707ZM6.44 14.146a1.5 1.5 0 0 0 2.122 0l-.707-.707a.5.5 0 0 1-.708 0l-.707.707ZM.854 6.44a1.5 1.5 0 0 0 0 2.122l.707-.707a.5.5 0 0 1 0-.708L.854 6.44Zm6.292-4.878a.5.5 0 0 1 .708 0L8.56.854a1.5 1.5 0 0 0-2.122 0l.707.707Zm-2 1.293l1 1l.708-.708l-1-1l-.708.708ZM7.5 5a.5.5 0 0 1-.5-.5H6A1.5 1.5 0 0 0 7.5 6V5Zm.5-.5a.5.5 0 0 1-.5.5v1A1.5 1.5 0 0 0 9 4.5H8ZM7.5 4a.5.5 0 0 1 .5.5h1A1.5 1.5 0 0 0 7.5 3v1Zm0-1A1.5 1.5 0 0 0 6 4.5h1a.5.5 0 0 1 .5-.5V3Zm.646 2.854l1.5 1.5l.707-.708l-1.5-1.5l-.707.708ZM10.5 8a.5.5 0 0 1-.5-.5H9A1.5 1.5 0 0 0 10.5 9V8Zm.5-.5a.5.5 0 0 1-.5.5v1A1.5 1.5 0 0 0 12 7.5h-1Zm-.5-.5a.5.5 0 0 1 .5.5h1A1.5 1.5 0 0 0 10.5 6v1Zm0-1A1.5 1.5 0 0 0 9 7.5h1a.5.5 0 0 1 .5-.5V6ZM7 5.5v4h1v-4H7Zm.5 5.5a.5.5 0 0 1-.5-.5H6A1.5 1.5 0 0 0 7.5 12v-1Zm.5-.5a.5.5 0 0 1-.5.5v1A1.5 1.5 0 0 0 9 10.5H8Zm-.5-.5a.5.5 0 0 1 .5.5h1A1.5 1.5 0 0 0 7.5 9v1Zm0-1A1.5 1.5 0 0 0 6 10.5h1a.5.5 0 0 1 .5-.5V9Z" />
</svg>
<span class="menu-item-label">Sources</span>
<span class="menu-item-label" :class="collapsed && 'lg:hidden'">Sources</span>
</a>
</li>
<li>
@ -170,7 +197,7 @@ class="{{ request()->is('destination*') ? 'menu-item-active menu-item' : 'menu-i
stroke-linejoin="round" stroke-width="2"
d="M9 4L3 8v12l6-3l6 3l6-4V4l-6 3l-6-3zm-2 8.001V12m4 .001V12m3-2l2 2m2 2l-2-2m0 0l2-2m-2 2l-2 2" />
</svg>
<span class="menu-item-label">Destinations</span>
<span class="menu-item-label" :class="collapsed && 'lg:hidden'">Destinations</span>
</a>
</li>
<li>
@ -185,7 +212,7 @@ class="{{ request()->is('storages*') ? 'menu-item-active menu-item' : 'menu-item
<path d="M4 12v6a8 3 0 0 0 16 0v-6" />
</g>
</svg>
<span class="menu-item-label">S3 Storages</span>
<span class="menu-item-label" :class="collapsed && 'lg:hidden'">S3 Storages</span>
</a>
</li>
<li>
@ -200,7 +227,7 @@ class="{{ request()->is('shared-variables*') ? 'menu-item-active menu-item' : 'm
<path d="M8 16c1.5 0 3-2 4-3.5S14.5 9 16 9" />
</g>
</svg>
<span class="menu-item-label">Shared Variables</span>
<span class="menu-item-label" :class="collapsed && 'lg:hidden'">Shared Variables</span>
</a>
</li>
<li>
@ -212,7 +239,7 @@ class="{{ request()->is('notifications*') ? 'menu-item-active menu-item' : 'menu
stroke-linejoin="round" stroke-width="2"
d="M10 5a2 2 0 1 1 4 0a7 7 0 0 1 4 6v3a4 4 0 0 0 2 3H4a4 4 0 0 0 2-3v-3a7 7 0 0 1 4-6M9 17v1a3 3 0 0 0 6 0v-1" />
</svg>
<span class="menu-item-label">Notifications</span>
<span class="menu-item-label" :class="collapsed && 'lg:hidden'">Notifications</span>
</a>
</li>
<li>
@ -224,7 +251,7 @@ class="{{ request()->is('security*') ? 'menu-item-active menu-item' : 'menu-item
stroke-linejoin="round" stroke-width="2"
d="m16.555 3.843l3.602 3.602a2.877 2.877 0 0 1 0 4.069l-2.643 2.643a2.877 2.877 0 0 1-4.069 0l-.301-.301l-6.558 6.558a2 2 0 0 1-1.239.578L5.172 21H4a1 1 0 0 1-.993-.883L3 20v-1.172a2 2 0 0 1 .467-1.284l.119-.13L4 17h2v-2h2v-2l2.144-2.144l-.301-.301a2.877 2.877 0 0 1 0-4.069l2.643-2.643a2.877 2.877 0 0 1 4.069 0zM15 9h.01" />
</svg>
<span class="menu-item-label">Keys & Tokens</span>
<span class="menu-item-label" :class="collapsed && 'lg:hidden'">Keys & Tokens</span>
</a>
</li>
<li>
@ -239,7 +266,7 @@ class="{{ request()->is('tags*') ? 'menu-item-active menu-item' : 'menu-item' }}
<path d="m18 19l1.592-1.592a4.82 4.82 0 0 0 0-6.816L15 6m-8 4h-.01" />
</g>
</svg>
<span class="menu-item-label">Tags</span>
<span class="menu-item-label" :class="collapsed && 'lg:hidden'">Tags</span>
</a>
</li>
@can('canAccessTerminal')
@ -254,7 +281,7 @@ class="{{ request()->is('terminal*') ? 'menu-item-active menu-item' : 'menu-item
<path d="M5 7l5 5l-5 5" />
<path d="M12 19l7 0" />
</svg>
<span class="menu-item-label">Terminal</span>
<span class="menu-item-label" :class="collapsed && 'lg:hidden'">Terminal</span>
</a>
</li>
@endcan
@ -270,7 +297,7 @@ class="{{ request()->is('profile*') ? 'menu-item-active menu-item' : 'menu-item'
<path d="M12 10m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0" />
<path d="M6.168 18.849a4 4 0 0 1 3.832 -2.849h4a4 4 0 0 1 3.834 2.855" />
</svg>
<span class="menu-item-label">Profile</span>
<span class="menu-item-label" :class="collapsed && 'lg:hidden'">Profile</span>
</a>
</li>
<li>
@ -288,7 +315,7 @@ class="{{ request()->is('team*') ? 'menu-item-active menu-item' : 'menu-item' }}
<path d="M5 5a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
<path d="M3 13v-1a2 2 0 0 1 2 -2h2" />
</svg>
<span class="menu-item-label">Teams</span>
<span class="menu-item-label" :class="collapsed && 'lg:hidden'">Teams</span>
</a>
</li>
@if (isCloud() && auth()->user()->isAdmin())
@ -301,7 +328,7 @@ class="{{ request()->is('subscription*') ? 'menu-item-active menu-item' : 'menu-
stroke-linejoin="round" stroke-width="2"
d="M3 8a3 3 0 0 1 3-3h12a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3zm0 2h18M7 15h.01M11 15h2" />
</svg>
<span class="menu-item-label">Subscription</span>
<span class="menu-item-label" :class="collapsed && 'lg:hidden'">Subscription</span>
</a>
</li>
@endif
@ -319,7 +346,7 @@ class="{{ request()->is('settings*') ? 'menu-item-active menu-item' : 'menu-item
d="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065z" />
<path d="M9 12a3 3 0 1 0 6 0a3 3 0 0 0 -6 0" />
</svg>
<span class="menu-item-label">Settings</span>
<span class="menu-item-label" :class="collapsed && 'lg:hidden'">Settings</span>
</a>
</li>
@endif
@ -333,7 +360,7 @@ class="{{ request()->is('settings*') ? 'menu-item-active menu-item' : 'menu-item
<path fill="currentColor"
d="M177.62 159.6a52 52 0 0 1-34 34a12.2 12.2 0 0 1-3.6.55a12 12 0 0 1-3.6-23.45a28 28 0 0 0 18.32-18.32a12 12 0 0 1 22.9 7.2ZM220 144a92 92 0 0 1-184 0c0-28.81 11.27-58.18 33.48-87.28a12 12 0 0 1 17.9-1.33l19.69 19.11L127 19.89a12 12 0 0 1 18.94-5.12C168.2 33.25 220 82.85 220 144m-24 0c0-41.71-30.61-78.39-52.52-99.29l-20.21 55.4a12 12 0 0 1-19.63 4.5L80.71 82.36C67 103.38 60 124.06 60 144a68 68 0 0 0 136 0" />
</svg>
<span class="menu-item-label">Admin</span>
<span class="menu-item-label" :class="collapsed && 'lg:hidden'">Admin</span>
</a>
</li>
@endif
@ -341,7 +368,7 @@ class="{{ request()->is('settings*') ? 'menu-item-active menu-item' : 'menu-item
<div class="flex-1"></div>
@if (isInstanceAdmin() && !isCloud())
@persist('upgrade')
<li>
<li :class="collapsed && 'lg:hidden'">
<livewire:upgrade />
</li>
@endpersist
@ -368,7 +395,7 @@ class="{{ request()->is('onboarding*') ? 'menu-item-active menu-item' : 'menu-it
d="M12 6L8.707 9.293a1 1 0 0 0 0 1.414l.543.543c.69.69 1.81.69 2.5 0l1-1a3.182 3.182 0 0 1 4.5 0l2.25 2.25m-7 3l2 2M15 13l2 2" />
</g>
</svg>
<span class="menu-item-label">Sponsor us</span>
<span class="menu-item-label" :class="collapsed && 'lg:hidden'">Sponsor us</span>
</a>
</li>
@endif
@ -384,7 +411,7 @@ class="{{ request()->is('onboarding*') ? 'menu-item-active menu-item' : 'menu-it
<path fill="currentColor"
d="M140 180a12 12 0 1 1-12-12a12 12 0 0 1 12 12M128 72c-22.06 0-40 16.15-40 36v4a8 8 0 0 0 16 0v-4c0-11 10.77-20 24-20s24 9 24 20s-10.77 20-24 20a8 8 0 0 0-8 8v8a8 8 0 0 0 16 0v-.72c18.24-3.35 32-17.9 32-35.28c0-19.85-17.94-36-40-36m104 56A104 104 0 1 1 128 24a104.11 104.11 0 0 1 104 104m-16 0a88 88 0 1 0-88 88a88.1 88.1 0 0 0 88-88" />
</svg>
<span class="menu-item-label">Feedback</span>
<span class="menu-item-label" :class="collapsed && 'lg:hidden'">Feedback</span>
</div>
</x-slot:content>
<livewire:help />
@ -394,15 +421,21 @@ class="{{ request()->is('onboarding*') ? 'menu-item-active menu-item' : 'menu-it
<form action="/logout" method="POST">
@csrf
<button title="Logout" type="submit" class="gap-2 mb-6 menu-item">
<svg class="menu-item-icon mr-1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<svg class="menu-item-icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor"
d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2a9.985 9.985 0 0 1 8 4h-2.71a8 8 0 1 0 .001 12h2.71A9.985 9.985 0 0 1 12 22m7-6v-3h-8v-2h8V8l5 4z" />
</svg>
<span>Logout</span>
<span :class="collapsed && 'lg:hidden'">Logout</span>
</button>
</form>
</li>
</ul>
</li>
</ul>
<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-[100] -translate-y-1/2 px-2 py-1 text-xs font-medium rounded-md bg-neutral-900 dark:bg-coolgray-300 text-white whitespace-nowrap pointer-events-none shadow-lg border border-neutral-700 dark:border-coolgray-200"
x-text="tooltip.text"></div>
</nav>

View file

@ -10,12 +10,19 @@
<livewire:deployments-indicator />
<div x-data="{
open: false,
collapsed: false,
pageWidth: 'full',
init() {
this.pageWidth = localStorage.getItem('pageWidth');
if (!this.pageWidth) {
this.pageWidth = 'full';
localStorage.setItem('pageWidth', 'full');
}
this.collapsed = localStorage.getItem('sidebarCollapsed') === 'true';
},
toggleSidebar() {
this.collapsed = !this.collapsed;
localStorage.setItem('sidebarCollapsed', this.collapsed);
}
}" x-cloak class="mx-auto dark:text-inherit text-black"
:class="pageWidth === 'full' ? '' : 'max-w-7xl'">
@ -40,10 +47,20 @@
</div>
</div>
<div class="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-56 lg:flex-col min-w-0">
<div class="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:flex-col min-w-0 transition-[width] duration-200"
:class="collapsed ? 'lg:w-16' : 'lg:w-56'">
<div class="flex flex-col overflow-y-auto grow gap-y-5 scrollbar min-w-0">
<x-navbar />
</div>
<button type="button" @click="toggleSidebar()"
:title="collapsed ? 'Expand sidebar' : 'Collapse sidebar'"
class="absolute top-8 -right-3 z-50 hidden lg:flex items-center justify-center w-6 h-6 rounded-full border bg-white dark:bg-coolgray-100 dark:border-coolgray-200 border-neutral-300 hover:bg-neutral-100 dark:hover:bg-coolgray-200 transition-colors shadow-sm">
<svg xmlns="http://www.w3.org/2000/svg" class="w-3.5 h-3.5 text-neutral-600 dark:text-neutral-300 transition-transform"
:class="collapsed ? '' : 'rotate-180'"
fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" />
</svg>
</button>
</div>
<div
@ -62,7 +79,7 @@ class="text-xl font-bold tracking-wide dark:text-white hover:opacity-80 transiti
</button>
</div>
<main class="lg:pl-56">
<main class="transition-[padding] duration-200" :class="collapsed ? 'lg:pl-16' : 'lg:pl-56'">
<div class="p-4 sm:px-6 lg:px-8 lg:py-6">
{{ $slot }}
</div>

View file

@ -1,6 +1,49 @@
<x-forms.select wire:model.live="selectedTeamId">
<option value="default" disabled selected>Switch team</option>
@foreach (auth()->user()->teams as $team)
<option value="{{ $team->id }}">{{ $team->name }}</option>
@endforeach
</x-forms.select>
@php
$currentTeam = auth()->user()->currentTeam();
$teamInitial = strtoupper(mb_substr($currentTeam->name, 0, 1));
@endphp
<div>
<div :class="collapsed && 'lg:hidden'">
<x-forms.select wire:model.live="selectedTeamId">
<option value="default" disabled selected>Switch team</option>
@foreach (auth()->user()->teams as $team)
<option value="{{ $team->id }}">{{ $team->name }}</option>
@endforeach
</x-forms.select>
</div>
<div class="hidden"
:class="collapsed && 'lg:block'"
x-data="{
teamOpen: false,
teamX: 0,
teamY: 0,
openTeamMenu(ev) {
const rect = ev.currentTarget.getBoundingClientRect();
this.teamX = rect.right + 8;
this.teamY = rect.top;
this.teamOpen = !this.teamOpen;
}
}">
<button @click="openTeamMenu($event)" type="button"
title="Team: {{ $currentTeam->name }}"
class="flex items-center justify-center w-8 h-8 text-sm font-semibold rounded-md bg-coollabs hover:opacity-80 transition-opacity text-white cursor-pointer">
{{ $teamInitial }}
</button>
<div x-show="teamOpen"
@click.outside="teamOpen = false"
x-transition.opacity.duration.100ms
x-cloak
:style="`left: ${teamX}px; top: ${teamY}px;`"
class="fixed z-[100] min-w-48 max-h-72 overflow-y-auto bg-white dark:bg-coolgray-100 border border-neutral-300 dark:border-coolgray-200 rounded-md shadow-lg py-1">
<div class="px-3 py-1.5 text-xs font-semibold text-neutral-500 dark:text-neutral-400 border-b border-neutral-200 dark:border-coolgray-200">Switch team</div>
@foreach (auth()->user()->teams as $team)
<button type="button"
wire:click="switch_to({{ $team->id }})"
@click="teamOpen = false"
class="w-full px-3 py-1.5 text-left text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200 dark:text-white {{ $team->id === $currentTeam->id ? 'font-semibold text-coollabs dark:text-warning' : '' }}">
{{ $team->name }}
</button>
@endforeach
</div>
</div>
</div>

View file

@ -13,10 +13,10 @@ services:
environment:
- SERVICE_URL_PDS_3000
- 'PDS_HOSTNAME=${SERVICE_FQDN_PDS}'
- 'PDS_JWT_SECRET=${SERVICE_HEX_32_JWTSECRET}'
- 'PDS_JWT_SECRET=${SERVICE_HEX_64_JWTSECRET}'
- 'PDS_ADMIN_PASSWORD=${SERVICE_PASSWORD_ADMIN}'
- 'PDS_ADMIN_EMAIL=${PDS_ADMIN_EMAIL}'
- 'PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=${SERVICE_HEX_32_ROTATIONKEY}'
- 'PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=${SERVICE_HEX_64_ROTATIONKEY}'
- 'PDS_DATA_DIRECTORY=${PDS_DATA_DIRECTORY:-/pds}'
- 'PDS_BLOBSTORE_DISK_LOCATION=${PDS_DATA_DIRECTORY:-/pds}/blocks'
- 'PDS_BLOB_UPLOAD_LIMIT=${PDS_BLOB_UPLOAD_LIMIT:-104857600}'

View file

@ -13,7 +13,7 @@ services:
environment:
- SERVICE_URL_BACKEND_3210
- INSTANCE_NAME=${INSTANCE_NAME:-self-hosted-convex}
- INSTANCE_SECRET=${SERVICE_HEX_32_SECRET}
- INSTANCE_SECRET=${SERVICE_HEX_64_SECRET}
- CONVEX_RELEASE_VERSION_DEV=${CONVEX_RELEASE_VERSION_DEV:-}
- ACTIONS_USER_TIMEOUT_SECS=${ACTIONS_USER_TIMEOUT_SECS:-}
# URL of the Convex API as accessed by the client/frontend.

View file

@ -18,7 +18,7 @@ services:
environment:
- SERVICE_URL_OUTLINE_3000
- NODE_ENV=production
- SECRET_KEY=${SERVICE_HEX_32_OUTLINE}
- SECRET_KEY=${SERVICE_HEX_64_OUTLINE}
- UTILS_SECRET=${SERVICE_PASSWORD_64_OUTLINE}
- DATABASE_URL=postgres://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_64_POSTGRES}@postgres:5432/${POSTGRES_DATABASE:-outline}
- REDIS_URL=redis://:${SERVICE_PASSWORD_64_REDIS}@redis:6379

View file

@ -10,8 +10,7 @@ services:
image: ghcr.io/homarr-labs/homarr:v1.40.0
environment:
- SERVICE_URL_HOMARR_7575
- SERVICE_HEX_32_HOMARR
- 'SECRET_ENCRYPTION_KEY=${SERVICE_HEX_32_HOMARR}'
- 'SECRET_ENCRYPTION_KEY=${SERVICE_HEX_64_HOMARR}'
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./homarr/appdata:/appdata

View file

@ -10,8 +10,8 @@ services:
image: logiclabshq/open-archiver:latest
environment:
- SERVICE_URL_OPENARCHIVER_3000
- ENCRYPTION_KEY=${SERVICE_HEX_32_ENCRYPTIONKEY}
- STORAGE_ENCRYPTION_KEY=${SERVICE_HEX_32_STORAGEENCRYPTIONKEY}
- ENCRYPTION_KEY=${SERVICE_HEX_64_ENCRYPTIONKEY}
- STORAGE_ENCRYPTION_KEY=${SERVICE_HEX_64_STORAGEENCRYPTIONKEY}
- PORT_BACKEND=${PORT_BACKEND:-4000}
- PORT_FRONTEND=${PORT_FRONTEND:-3000}
- NODE_ENV=${NODE_ENV:-production}

View file

@ -0,0 +1,29 @@
<?php
test('hex magic variables generate valid hex strings with expected lengths', function (string $command, int $expectedLength) {
$value = generateEnvValue($command);
expect($value)
->toBeString()
->toMatch('/^[0-9a-f]+$/');
expect(strlen($value))->toBe($expectedLength);
})->with([
'HEX_32' => ['HEX_32', 32],
'HEX_64' => ['HEX_64', 64],
'HEX_128' => ['HEX_128', 128],
]);
test('real base64 magic variables generate valid base64 strings from expected byte lengths', function (string $command, int $expectedBytes) {
$value = generateEnvValue($command);
$decodedValue = base64_decode($value, true);
expect($value)->toBeString();
expect($decodedValue)->not->toBeFalse();
expect(strlen($decodedValue))->toBe($expectedBytes);
})->with([
'REALBASE64' => ['REALBASE64', 32],
'REALBASE64_32' => ['REALBASE64_32', 32],
'REALBASE64_64' => ['REALBASE64_64', 64],
'REALBASE64_128' => ['REALBASE64_128', 128],
]);