mirror of
https://github.com/coollabsio/coolify.git
synced 2026-06-14 03:19:51 +00:00
feat(profile): add appearance preferences page
Add a profile appearance section for theme, page width, and zoom preferences. Move changelog access into the sidebar and bump the Coolify version to 4.1.2.
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Profile;
|
||||
|
||||
use Livewire\Component;
|
||||
|
||||
class Appearance extends Component
|
||||
{
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.profile.appearance');
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,8 @@ class SettingsDropdown extends Component
|
||||
{
|
||||
public $showWhatsNewModal = false;
|
||||
|
||||
public string $trigger = 'preferences';
|
||||
|
||||
public function getUnreadCountProperty()
|
||||
{
|
||||
return Auth::user()->getUnreadChangelogCount();
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
return [
|
||||
'coolify' => [
|
||||
'version' => '4.1.1',
|
||||
'version' => '4.1.2',
|
||||
'helper_version' => '1.0.14',
|
||||
'realtime_version' => '1.0.15',
|
||||
'railpack_version' => '0.23.0',
|
||||
|
||||
@@ -129,7 +129,7 @@ services:
|
||||
networks:
|
||||
- coolify
|
||||
minio:
|
||||
image: ghcr.io/coollabsio/maxio:latest
|
||||
image: coollabsio/maxio:latest
|
||||
pull_policy: always
|
||||
container_name: coolify-minio
|
||||
ports:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"coolify": {
|
||||
"v4": {
|
||||
"version": "4.1.1"
|
||||
"version": "4.1.2"
|
||||
},
|
||||
"nightly": {
|
||||
"version": "4.2.0"
|
||||
|
||||
@@ -49,23 +49,32 @@
|
||||
localStorage.setItem('theme', type);
|
||||
this.queryTheme();
|
||||
},
|
||||
cycleTheme() {
|
||||
const themes = ['light', 'system', 'dark'];
|
||||
const currentIndex = themes.indexOf(this.theme || localStorage.getItem('theme') || 'dark');
|
||||
this.setTheme(themes[(currentIndex + 1) % themes.length]);
|
||||
},
|
||||
queryTheme() {
|
||||
const darkModePreference = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
const userSettings = localStorage.getItem('theme') || 'dark';
|
||||
localStorage.setItem('theme', userSettings);
|
||||
let isDark = false;
|
||||
if (userSettings === 'dark') {
|
||||
document.documentElement.classList.add('dark');
|
||||
this.theme = 'dark';
|
||||
isDark = true;
|
||||
} else if (userSettings === 'light') {
|
||||
document.documentElement.classList.remove('dark');
|
||||
this.theme = 'light';
|
||||
} else if (darkModePreference) {
|
||||
this.theme = 'system';
|
||||
document.documentElement.classList.add('dark');
|
||||
isDark = true;
|
||||
} else if (!darkModePreference) {
|
||||
this.theme = 'system';
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
document.querySelector('meta[name=theme-color]')?.setAttribute('content', isDark ? '#101010' : '#ffffff');
|
||||
},
|
||||
checkZoom() {
|
||||
if (this.zoom === null) {
|
||||
@@ -92,9 +101,9 @@
|
||||
}
|
||||
}
|
||||
}">
|
||||
<div class="flex pt-4 pb-4 pl-2 items-start gap-2 motion-safe:transition-all motion-safe:duration-200 motion-safe:ease-out motion-reduce:transition-none"
|
||||
:class="collapsed ? 'lg:flex-col lg:items-center lg:pl-0 lg:gap-3 lg:pt-7' : 'lg:pt-6'">
|
||||
<div class="flex flex-col w-full" :class="collapsed && 'lg:hidden'">
|
||||
<div class="flex pt-4 pb-4 pl-2 pr-3 items-start gap-3 motion-safe:transition-all motion-safe:duration-200 motion-safe:ease-out motion-reduce:transition-none"
|
||||
:class="collapsed ? 'lg:flex-col lg:items-center lg:pl-0 lg:pr-0 lg:gap-3 lg:pt-7' : 'lg:pt-6'">
|
||||
<div class="flex min-w-0 flex-1 flex-col" :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>
|
||||
@@ -107,10 +116,10 @@
|
||||
</a>
|
||||
<x-version class="text-[10px]" />
|
||||
</div>
|
||||
<div :class="collapsed && 'lg:hidden'">
|
||||
<div class="min-w-0 flex-1" :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">
|
||||
class="flex h-8 w-full items-center justify-between 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">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-neutral-500 dark:text-neutral-400"
|
||||
fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
@@ -120,9 +129,6 @@
|
||||
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>
|
||||
<div :class="collapsed && 'lg:hidden'">
|
||||
<livewire:settings-dropdown />
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-2 pt-2 pb-7 overflow-hidden motion-safe:transition-all motion-safe:duration-200 motion-safe:ease-out motion-reduce:transition-none" :class="collapsed && 'lg:px-0 lg:pt-0 lg:pb-4 lg:min-h-8 lg:flex lg:justify-center'">
|
||||
<livewire:switch-team />
|
||||
@@ -366,6 +372,70 @@
|
||||
@endif
|
||||
@endif
|
||||
<div class="flex-1"></div>
|
||||
<li>
|
||||
<livewire:settings-dropdown trigger="changelog-sidebar" />
|
||||
</li>
|
||||
<li>
|
||||
<div class="menu-item" title="Theme" aria-label="Theme switcher" :class="collapsed && 'lg:hidden'">
|
||||
<svg x-show="theme === 'dark'" class="menu-item-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
|
||||
</svg>
|
||||
<svg x-show="theme === 'light'" class="menu-item-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
</svg>
|
||||
<svg x-show="theme === 'system'" class="menu-item-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<span class="menu-item-label">Theme</span>
|
||||
<div class="ml-auto flex items-center gap-0.5 rounded-sm bg-neutral-100 p-0.5 dark:bg-coolgray-200">
|
||||
<button type="button" @click.stop="setTheme('light')" title="Light" aria-label="Use light theme"
|
||||
class="grid size-6 place-items-center rounded-sm text-xs hover:bg-white hover:text-coollabs dark:hover:bg-base dark:hover:text-warning"
|
||||
:class="theme === 'light' && 'bg-white text-coollabs shadow-sm dark:bg-base dark:text-warning'">
|
||||
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" @click.stop="setTheme('system')" title="System default" aria-label="Use system theme"
|
||||
class="grid size-6 place-items-center rounded-sm text-xs hover:bg-white hover:text-coollabs dark:hover:bg-base dark:hover:text-warning"
|
||||
:class="theme === 'system' && 'bg-white text-coollabs shadow-sm dark:bg-base dark:text-warning'">
|
||||
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" @click.stop="setTheme('dark')" title="Dark" aria-label="Use dark theme"
|
||||
class="grid size-6 place-items-center rounded-sm text-xs hover:bg-white hover:text-coollabs dark:hover:bg-base dark:hover:text-warning"
|
||||
:class="theme === 'dark' && 'bg-white text-coollabs shadow-sm dark:bg-base dark:text-warning'">
|
||||
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" @click.stop="cycleTheme()"
|
||||
:title="`Theme: ${theme === 'system' ? 'System default' : theme}. Click to change.`"
|
||||
:aria-label="`Theme: ${theme === 'system' ? 'System default' : theme}. Click to change theme.`"
|
||||
class="menu-item hidden"
|
||||
:class="collapsed && 'lg:flex'">
|
||||
<svg x-show="theme === 'dark'" class="menu-item-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
|
||||
</svg>
|
||||
<svg x-show="theme === 'light'" class="menu-item-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
</svg>
|
||||
<svg x-show="theme === 'system'" class="menu-item-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</button>
|
||||
</li>
|
||||
@if (isInstanceAdmin() && !isCloud())
|
||||
@persist('upgrade')
|
||||
<li>
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<div class="pb-6">
|
||||
<h1>Profile</h1>
|
||||
<div class="subtitle">Your user profile settings.</div>
|
||||
<div class="navbar-main">
|
||||
<nav class="flex items-center gap-6 min-h-10">
|
||||
<a class="{{ request()->routeIs('profile') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('profile') }}">
|
||||
General
|
||||
</a>
|
||||
<a class="{{ request()->routeIs('profile.appearance') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('profile.appearance') }}">
|
||||
Appearance
|
||||
</a>
|
||||
<div class="flex-1"></div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,119 @@
|
||||
<div>
|
||||
<x-slot:title>
|
||||
Appearance | Coolify
|
||||
</x-slot>
|
||||
<x-profile.navbar />
|
||||
|
||||
<div x-data="{
|
||||
theme: localStorage.getItem('theme') || 'dark',
|
||||
pageWidth: localStorage.getItem('pageWidth') || 'full',
|
||||
zoom: localStorage.getItem('zoom') || '100',
|
||||
init() {
|
||||
localStorage.setItem('theme', this.theme);
|
||||
localStorage.setItem('pageWidth', this.pageWidth);
|
||||
localStorage.setItem('zoom', this.zoom);
|
||||
this.applyTheme();
|
||||
},
|
||||
setTheme(type) {
|
||||
this.theme = type;
|
||||
localStorage.setItem('theme', type);
|
||||
this.applyTheme();
|
||||
},
|
||||
applyTheme() {
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
const isDark = this.theme === 'dark' || (this.theme === 'system' && prefersDark);
|
||||
document.documentElement.classList.toggle('dark', isDark);
|
||||
document.querySelector('meta[name=theme-color]')?.setAttribute('content', isDark ? '#101010' : '#ffffff');
|
||||
},
|
||||
setWidth(width) {
|
||||
this.pageWidth = width;
|
||||
localStorage.setItem('pageWidth', width);
|
||||
window.location.reload();
|
||||
},
|
||||
setZoom(value) {
|
||||
this.zoom = value;
|
||||
localStorage.setItem('zoom', value);
|
||||
window.location.reload();
|
||||
},
|
||||
}" class="flex max-w-2xl flex-col">
|
||||
<section class="space-y-1.5">
|
||||
<h2>Appearance</h2>
|
||||
<div>Choose how Coolify looks in this browser.</div>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
<button type="button" @click="setTheme('light')" aria-label="Use light theme"
|
||||
class="flex items-center gap-2 rounded-sm border border-neutral-300 bg-white px-2 py-1 text-left text-sm hover:bg-neutral-100 focus-visible:ring-2 focus-visible:ring-coollabs dark:border-coolgray-300 dark:bg-coolgray-100 dark:hover:bg-coolgray-200 dark:focus-visible:ring-warning"
|
||||
:class="theme === 'light' && 'border-coollabs text-coollabs dark:border-warning dark:text-warning'">
|
||||
<svg class="size-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
</svg>
|
||||
<span>Light</span>
|
||||
</button>
|
||||
<button type="button" @click="setTheme('system')" aria-label="Use system theme"
|
||||
class="flex items-center gap-2 rounded-sm border border-neutral-300 bg-white px-2 py-1 text-left text-sm hover:bg-neutral-100 focus-visible:ring-2 focus-visible:ring-coollabs dark:border-coolgray-300 dark:bg-coolgray-100 dark:hover:bg-coolgray-200 dark:focus-visible:ring-warning"
|
||||
:class="theme === 'system' && 'border-coollabs text-coollabs dark:border-warning dark:text-warning'">
|
||||
<svg class="size-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<span>System</span>
|
||||
</button>
|
||||
<button type="button" @click="setTheme('dark')" aria-label="Use dark theme"
|
||||
class="flex items-center gap-2 rounded-sm border border-neutral-300 bg-white px-2 py-1 text-left text-sm hover:bg-neutral-100 focus-visible:ring-2 focus-visible:ring-coollabs dark:border-coolgray-300 dark:bg-coolgray-100 dark:hover:bg-coolgray-200 dark:focus-visible:ring-warning"
|
||||
:class="theme === 'dark' && 'border-coollabs text-coollabs dark:border-warning dark:text-warning'">
|
||||
<svg class="size-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
|
||||
</svg>
|
||||
<span>Dark</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="space-y-1.5">
|
||||
<h2>Width</h2>
|
||||
<div>Choose the maximum page width for this browser.</div>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
<button type="button" @click="setWidth('center')" aria-label="Use centered width"
|
||||
class="flex items-center gap-2 rounded-sm border border-neutral-300 bg-white px-2 py-1 text-sm hover:bg-neutral-100 focus-visible:ring-2 focus-visible:ring-coollabs dark:border-coolgray-300 dark:bg-coolgray-100 dark:hover:bg-coolgray-200 dark:focus-visible:ring-warning"
|
||||
:class="pageWidth === 'center' && 'border-coollabs text-coollabs dark:border-warning dark:text-warning'">
|
||||
<svg class="size-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h7" />
|
||||
</svg>
|
||||
Center
|
||||
</button>
|
||||
<button type="button" @click="setWidth('full')" aria-label="Use full width"
|
||||
class="flex items-center gap-2 rounded-sm border border-neutral-300 bg-white px-2 py-1 text-sm hover:bg-neutral-100 focus-visible:ring-2 focus-visible:ring-coollabs dark:border-coolgray-300 dark:bg-coolgray-100 dark:hover:bg-coolgray-200 dark:focus-visible:ring-warning"
|
||||
:class="pageWidth === 'full' && 'border-coollabs text-coollabs dark:border-warning dark:text-warning'">
|
||||
<svg class="size-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
Full
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="space-y-1.5">
|
||||
<h2>Zoom</h2>
|
||||
<div>Choose interface density for this browser.</div>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
<button type="button" @click="setZoom('100')" aria-label="Use 100 percent zoom"
|
||||
class="flex items-center gap-2 rounded-sm border border-neutral-300 bg-white px-2 py-1 text-sm hover:bg-neutral-100 focus-visible:ring-2 focus-visible:ring-coollabs dark:border-coolgray-300 dark:bg-coolgray-100 dark:hover:bg-coolgray-200 dark:focus-visible:ring-warning"
|
||||
:class="zoom === '100' && 'border-coollabs text-coollabs dark:border-warning dark:text-warning'">
|
||||
<svg class="size-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
100%
|
||||
</button>
|
||||
<button type="button" @click="setZoom('90')" aria-label="Use 90 percent zoom"
|
||||
class="flex items-center gap-2 rounded-sm border border-neutral-300 bg-white px-2 py-1 text-sm hover:bg-neutral-100 focus-visible:ring-2 focus-visible:ring-coollabs dark:border-coolgray-300 dark:bg-coolgray-100 dark:hover:bg-coolgray-200 dark:focus-visible:ring-warning"
|
||||
:class="zoom === '90' && 'border-coollabs text-coollabs dark:border-warning dark:text-warning'">
|
||||
<svg class="size-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM10 10h4v4h-4v-4z" />
|
||||
</svg>
|
||||
90%
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2,8 +2,7 @@
|
||||
<x-slot:title>
|
||||
Profile | Coolify
|
||||
</x-slot>
|
||||
<h1>Profile</h1>
|
||||
<div class="subtitle -mt-2">Your user profile settings.</div>
|
||||
<x-profile.navbar />
|
||||
<form wire:submit='submit' class="flex flex-col">
|
||||
<div class="flex items-center gap-2">
|
||||
<h2>General</h2>
|
||||
|
||||
@@ -103,138 +103,98 @@
|
||||
return new Date(b.published_at) - new Date(a.published_at);
|
||||
});
|
||||
}
|
||||
}" @click.outside="dropdownOpen = false">
|
||||
<!-- Custom Dropdown without arrow -->
|
||||
<div class="relative">
|
||||
<button @click="dropdownOpen = !dropdownOpen"
|
||||
class="relative p-2 dark:text-neutral-400 hover:dark:text-white transition-colors cursor-pointer"
|
||||
title="Preferences">
|
||||
<!-- Sliders Icon -->
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" title="Preferences">
|
||||
}" @click.outside="dropdownOpen = false" class="{{ $trigger === 'changelog-sidebar' ? 'w-full' : '' }}">
|
||||
@if ($trigger === 'changelog-sidebar')
|
||||
<button wire:click="openWhatsNewModal" type="button" title="What's New" aria-label="What's New"
|
||||
class="relative text-left menu-item">
|
||||
<svg class="menu-item-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4" />
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
|
||||
<!-- Unread Count Badge -->
|
||||
<span class="text-left menu-item-label" :class="collapsed && 'lg:hidden'">What's New</span>
|
||||
@if ($unreadCount > 0)
|
||||
<span
|
||||
class="absolute -top-1 -right-1 bg-error text-white text-xs rounded-full w-4.5 h-4.5 flex items-center justify-center">
|
||||
class="absolute top-0 right-0 bg-error text-white text-[10px] rounded-full min-w-4 h-4 px-1 flex items-center justify-center"
|
||||
aria-label="{{ $unreadCount }} unread changelog {{ Str::plural('entry', $unreadCount) }}">
|
||||
{{ $unreadCount > 9 ? '9+' : $unreadCount }}
|
||||
</span>
|
||||
@endif
|
||||
</button>
|
||||
@else
|
||||
<!-- Custom Dropdown without arrow -->
|
||||
<div class="relative">
|
||||
<button @click="dropdownOpen = !dropdownOpen"
|
||||
class="relative p-2 dark:text-neutral-400 hover:dark:text-white transition-colors cursor-pointer"
|
||||
title="Preferences">
|
||||
<!-- Sliders Icon -->
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" title="Preferences">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4" />
|
||||
</svg>
|
||||
|
||||
<!-- Dropdown Menu -->
|
||||
<div x-show="dropdownOpen" x-transition:enter="ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 -translate-y-2" x-transition:enter-end="opacity-100 translate-y-0"
|
||||
x-transition:leave="ease-in duration-150" x-transition:leave-start="opacity-100 translate-y-0"
|
||||
x-transition:leave-end="opacity-0 -translate-y-2" class="absolute right-0 top-full mt-1 z-50 w-48" x-cloak>
|
||||
<div
|
||||
class="p-1 bg-white border rounded-sm shadow-lg dark:bg-coolgray-200 dark:border-coolgray-300 border-neutral-300">
|
||||
<div class="flex flex-col gap-1">
|
||||
<!-- What's New Section -->
|
||||
@if ($unreadCount > 0)
|
||||
<button wire:click="openWhatsNewModal" @click="dropdownOpen = false"
|
||||
class="px-1 dropdown-item-no-padding flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span>What's New</span>
|
||||
</div>
|
||||
<span
|
||||
class="bg-error text-white text-xs rounded-full w-5 h-5 flex items-center justify-center">
|
||||
{{ $unreadCount > 9 ? '9+' : $unreadCount }}
|
||||
</span>
|
||||
<!-- Unread Count Badge -->
|
||||
@if ($unreadCount > 0)
|
||||
<span
|
||||
class="absolute -top-1 -right-1 bg-error text-white text-xs rounded-full w-4.5 h-4.5 flex items-center justify-center">
|
||||
{{ $unreadCount > 9 ? '9+' : $unreadCount }}
|
||||
</span>
|
||||
@endif
|
||||
</button>
|
||||
|
||||
<!-- Dropdown Menu -->
|
||||
<div x-show="dropdownOpen" x-transition:enter="ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 -translate-y-2" x-transition:enter-end="opacity-100 translate-y-0"
|
||||
x-transition:leave="ease-in duration-150" x-transition:leave-start="opacity-100 translate-y-0"
|
||||
x-transition:leave-end="opacity-0 -translate-y-2" class="absolute right-0 top-full mt-1 z-50 w-48" x-cloak>
|
||||
<div
|
||||
class="p-1 bg-white border rounded-sm shadow-lg dark:bg-coolgray-200 dark:border-coolgray-300 border-neutral-300">
|
||||
<div class="flex flex-col gap-1">
|
||||
<!-- Width Section -->
|
||||
<div
|
||||
class="my-1 font-bold border-b dark:border-coolgray-500 border-neutral-300 dark:text-white text-md">
|
||||
Width</div>
|
||||
<button @click="switchWidth(); dropdownOpen = false"
|
||||
class="px-1 dropdown-item-no-padding flex items-center gap-2" x-show="full === 'full'">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h7" />
|
||||
</svg>
|
||||
<span>Center</span>
|
||||
</button>
|
||||
@else
|
||||
<button wire:click="openWhatsNewModal" @click="dropdownOpen = false"
|
||||
<button @click="switchWidth(); dropdownOpen = false"
|
||||
class="px-1 dropdown-item-no-padding flex items-center gap-2" x-show="full === 'center'">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
<span>Full</span>
|
||||
</button>
|
||||
|
||||
<!-- Zoom Section -->
|
||||
<div
|
||||
class="my-1 font-bold border-b dark:border-coolgray-500 border-neutral-300 dark:text-white text-md">
|
||||
Zoom</div>
|
||||
<button @click="setZoom(100); dropdownOpen = false"
|
||||
class="px-1 dropdown-item-no-padding flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
<span>Changelog</span>
|
||||
<span>100%</span>
|
||||
</button>
|
||||
@endif
|
||||
|
||||
<!-- Divider -->
|
||||
<div class="border-b dark:border-coolgray-500 border-neutral-300"></div>
|
||||
|
||||
<!-- Theme Section -->
|
||||
<div class="font-bold border-b dark:border-coolgray-500 border-neutral-300 dark:text-white pb-1">
|
||||
Appearance</div>
|
||||
<button @click="setTheme('dark'); dropdownOpen = false"
|
||||
class="px-1 dropdown-item-no-padding flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
|
||||
</svg>
|
||||
<span>Dark</span>
|
||||
</button>
|
||||
<button @click="setTheme('light'); dropdownOpen = false"
|
||||
class="px-1 dropdown-item-no-padding flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
</svg>
|
||||
<span>Light</span>
|
||||
</button>
|
||||
<button @click="setTheme('system'); dropdownOpen = false"
|
||||
class="px-1 dropdown-item-no-padding flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<span>System</span>
|
||||
</button>
|
||||
|
||||
<!-- Width Section -->
|
||||
<div
|
||||
class="my-1 font-bold border-b dark:border-coolgray-500 border-neutral-300 dark:text-white text-md">
|
||||
Width</div>
|
||||
<button @click="switchWidth(); dropdownOpen = false"
|
||||
class="px-1 dropdown-item-no-padding flex items-center gap-2" x-show="full === 'full'">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h7" />
|
||||
</svg>
|
||||
<span>Center</span>
|
||||
</button>
|
||||
<button @click="switchWidth(); dropdownOpen = false"
|
||||
class="px-1 dropdown-item-no-padding flex items-center gap-2" x-show="full === 'center'">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
<span>Full</span>
|
||||
</button>
|
||||
|
||||
<!-- Zoom Section -->
|
||||
<div
|
||||
class="my-1 font-bold border-b dark:border-coolgray-500 border-neutral-300 dark:text-white text-md">
|
||||
Zoom</div>
|
||||
<button @click="setZoom(100); dropdownOpen = false"
|
||||
class="px-1 dropdown-item-no-padding flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
<span>100%</span>
|
||||
</button>
|
||||
<button @click="setZoom(90); dropdownOpen = false"
|
||||
class="px-1 dropdown-item-no-padding flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM10 10h4v4h-4v-4z" />
|
||||
</svg>
|
||||
<span>90%</span>
|
||||
</button>
|
||||
<button @click="setZoom(90); dropdownOpen = false"
|
||||
class="px-1 dropdown-item-no-padding flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM10 10h4v4h-4v-4z" />
|
||||
</svg>
|
||||
<span>90%</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- What's New Modal -->
|
||||
@if ($showWhatsNewModal)
|
||||
@@ -262,8 +222,7 @@
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
@if (isDev())
|
||||
<x-forms.button wire:click="manualFetchChangelog"
|
||||
class="bg-coolgray-200 hover:bg-coolgray-300">
|
||||
<x-forms.button wire:click="manualFetchChangelog">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
|
||||
@@ -15,6 +15,7 @@ use App\Livewire\Notifications\Pushover as NotificationPushover;
|
||||
use App\Livewire\Notifications\Slack as NotificationSlack;
|
||||
use App\Livewire\Notifications\Telegram as NotificationTelegram;
|
||||
use App\Livewire\Notifications\Webhook as NotificationWebhook;
|
||||
use App\Livewire\Profile\Appearance as ProfileAppearance;
|
||||
use App\Livewire\Profile\Index as ProfileIndex;
|
||||
use App\Livewire\Project\Application\Configuration as ApplicationConfiguration;
|
||||
use App\Livewire\Project\Application\Deployment\Index as DeploymentIndex;
|
||||
@@ -124,6 +125,7 @@ Route::middleware(['auth', 'verified'])->group(function () {
|
||||
Route::get('/settings/scheduled-jobs', SettingsScheduledJobs::class)->name('settings.scheduled-jobs');
|
||||
|
||||
Route::get('/profile', ProfileIndex::class)->name('profile');
|
||||
Route::get('/profile/appearance', ProfileAppearance::class)->name('profile.appearance');
|
||||
|
||||
Route::prefix('tags')->group(function () {
|
||||
Route::get('/{tagName?}', TagsShow::class)->name('tags.show');
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
it('adds profile navigation with an appearance tab and route', function () {
|
||||
$routes = file_get_contents(base_path('routes/web.php'));
|
||||
$profileNavbar = file_get_contents(resource_path('views/components/profile/navbar.blade.php'));
|
||||
$profileView = file_get_contents(resource_path('views/livewire/profile/index.blade.php'));
|
||||
|
||||
expect($routes)
|
||||
->toContain("Route::get('/profile/appearance', ProfileAppearance::class)->name('profile.appearance')")
|
||||
->and($profileNavbar)
|
||||
->toContain('route(\'profile\')')
|
||||
->toContain('route(\'profile.appearance\')')
|
||||
->toContain('General')
|
||||
->toContain('Appearance')
|
||||
->and($profileView)
|
||||
->toContain('<x-profile.navbar />')
|
||||
->not->toContain('<h1>Profile</h1>\n <div class="subtitle -mt-2">');
|
||||
});
|
||||
|
||||
it('moves appearance preferences to the profile appearance view', function () {
|
||||
$appearanceView = file_get_contents(resource_path('views/livewire/profile/appearance.blade.php'));
|
||||
|
||||
expect($appearanceView)
|
||||
->toContain('<x-profile.navbar />')
|
||||
->toContain("setTheme('light')")
|
||||
->toContain("setTheme('system')")
|
||||
->toContain("setTheme('dark')")
|
||||
->toContain("setWidth('center')")
|
||||
->toContain("setWidth('full')")
|
||||
->toContain("setZoom('100')")
|
||||
->toContain("setZoom('90')")
|
||||
->toContain('aria-label="Use light theme"')
|
||||
->toContain('aria-label="Use system theme"')
|
||||
->toContain('aria-label="Use dark theme"')
|
||||
->toContain('aria-label="Use centered width"')
|
||||
->toContain('aria-label="Use full width"')
|
||||
->toContain('aria-label="Use 100 percent zoom"')
|
||||
->toContain('aria-label="Use 90 percent zoom"')
|
||||
->toContain('max-w-2xl')
|
||||
->toContain('class="space-y-1.5"')
|
||||
->toContain('gap-1.5')
|
||||
->toContain('px-2 py-1 text-sm');
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\SettingsDropdown;
|
||||
|
||||
it('keeps changelog and the theme switcher in the sidebar without the old preferences trigger', function () {
|
||||
$navbarView = file_get_contents(resource_path('views/components/navbar.blade.php'));
|
||||
|
||||
expect($navbarView)
|
||||
->toContain('<livewire:settings-dropdown trigger="changelog-sidebar" />')
|
||||
->not->toContain('<livewire:settings-dropdown />')
|
||||
->toContain('aria-label="Theme switcher"')
|
||||
->toContain('aria-label="Use light theme"')
|
||||
->toContain('aria-label="Use system theme"')
|
||||
->toContain('aria-label="Use dark theme"')
|
||||
->toContain('cycleTheme()')
|
||||
->toContain("const themes = ['light', 'system', 'dark'];")
|
||||
->toContain('pl-2 pr-3 items-start gap-3')
|
||||
->toContain('class="flex min-w-0 flex-1 flex-col"')
|
||||
->toContain('class="min-w-0 flex-1"')
|
||||
->toContain('class="flex h-8 w-full items-center justify-between');
|
||||
});
|
||||
|
||||
it('keeps changelog and appearance options out of the preferences dropdown', function () {
|
||||
$dropdownView = file_get_contents(resource_path('views/livewire/settings-dropdown.blade.php'));
|
||||
|
||||
expect($dropdownView)
|
||||
->toContain("\$trigger === 'changelog-sidebar'")
|
||||
->toContain('title="What\'s New"')
|
||||
->toContain('aria-label="What\'s New"')
|
||||
->toContain('wire:click="openWhatsNewModal"')
|
||||
->toContain('class="relative text-left menu-item"')
|
||||
->toContain('class="text-left menu-item-label"')
|
||||
->toContain("What's New</span>")
|
||||
->not->toContain('<span>Changelog</span>')
|
||||
->not->toContain('Appearance</div>')
|
||||
->not->toContain("@click=\"setTheme('dark'); dropdownOpen = false\"")
|
||||
->not->toContain("@click=\"setTheme('light'); dropdownOpen = false\"")
|
||||
->not->toContain("@click=\"setTheme('system'); dropdownOpen = false\"");
|
||||
});
|
||||
|
||||
it('opens and closes the changelog modal state', function () {
|
||||
$component = new SettingsDropdown;
|
||||
$component->trigger = 'changelog-sidebar';
|
||||
|
||||
expect($component->trigger)->toBe('changelog-sidebar')
|
||||
->and($component->showWhatsNewModal)->toBeFalse();
|
||||
|
||||
$component->openWhatsNewModal();
|
||||
|
||||
expect($component->showWhatsNewModal)->toBeTrue();
|
||||
|
||||
$component->closeWhatsNewModal();
|
||||
|
||||
expect($component->showWhatsNewModal)->toBeFalse();
|
||||
});
|
||||
|
||||
it('uses the default button palette for the changelog fetch action in light mode', function () {
|
||||
$dropdownView = file_get_contents(resource_path('views/livewire/settings-dropdown.blade.php'));
|
||||
|
||||
expect($dropdownView)
|
||||
->toContain('wire:click="manualFetchChangelog"')
|
||||
->not->toContain('bg-coolgray-200 hover:bg-coolgray-300');
|
||||
});
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"coolify": {
|
||||
"v4": {
|
||||
"version": "4.1.1"
|
||||
"version": "4.1.2"
|
||||
},
|
||||
"nightly": {
|
||||
"version": "4.2.0"
|
||||
|
||||
Reference in New Issue
Block a user