move into sonardesk

This commit is contained in:
Mark Gregory
2026-10-12 16:23:27 +01:00
parent 27af94d8ca
commit dd1f144908
15 changed files with 66 additions and 36 deletions
@@ -0,0 +1,8 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void { Schema::table('download_products',function(Blueprint $table){$table->longText('release_notes')->nullable();$table->text('minimum_system')->nullable();$table->integer('sort_order')->default(0);}); }
public function down(): void {Schema::table('download_products',function(Blueprint $table){$table->dropColumn(['release_notes','minimum_system','sort_order']);});}
};
@@ -6,8 +6,16 @@ use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\Rule;
use Modules\Licensing\Models\{SonarDeskRelease,DownloadProduct};
final class DistributionAdminController extends Controller {
public function index(){return view('licensing::distribution.admin', ['releases'=>SonarDeskRelease::orderByDesc('created_at')->paginate(20),'products'=>DownloadProduct::orderBy('name')->get()]);}
public function release(Request $request){$data=$request->validate(['version'=>'required|string|max:40','build_number'=>'required|integer|min:1','channel'=>['required',Rule::in(['stable','beta'])],'minimum_macos'=>'nullable|string|max:30','release_notes'=>'nullable|string','sparkle_signature'=>'nullable|string|max:2048','is_published'=>'nullable|boolean','is_featured'=>'nullable|boolean','file'=>'required|file|max:512000|mimes:dmg,zip,pkg']);$file=$request->file('file');$path=$file->store('sonardesk-distribution/releases','local');$release=SonarDeskRelease::create(['version'=>$data['version'],'build_number'=>$data['build_number'],'channel'=>$data['channel'],'minimum_macos'=>$data['minimum_macos']??null,'release_notes'=>$data['release_notes']??null,'sparkle_signature'=>$data['sparkle_signature']??null,'file_path'=>$path,'original_filename'=>$file->getClientOriginalName(),'file_size'=>$file->getSize(),'sha256'=>hash_file('sha256',$file->getRealPath()),'is_published'=>(bool)($data['is_published']??false),'is_featured'=>(bool)($data['is_featured']??false),'published_at'=>!empty($data['is_published'])?now():null]);return redirect()->route('administration.sonardesk.distribution')->with('status','Release '.$release->version.' created');}
public function product(Request $request){$data=$request->validate(['name'=>'required|string|max:150','description'=>'nullable|string','version'=>'nullable|string|max:60','platform'=>'nullable|string|max:60','is_published'=>'nullable|boolean','file'=>'required|file|max:512000']);$file=$request->file('file');$path=$file->store('sonardesk-distribution/products','local');DownloadProduct::create(['name'=>$data['name'],'description'=>$data['description']??null,'version'=>$data['version']??null,'platform'=>$data['platform']??null,'file_path'=>$path,'original_filename'=>$file->getClientOriginalName(),'file_size'=>$file->getSize(),'is_published'=>(bool)($data['is_published']??false)]);return redirect()->route('administration.sonardesk.distribution')->with('status','Download created');}
public function index(){return view('licensing::distribution.admin',['releases'=>SonarDeskRelease::orderByDesc('created_at')->paginate(20),'products'=>DownloadProduct::orderBy('name')->orderByDesc('created_at')->get()]);}
private function upload($file,string $folder):array{$path=$file->store('sonardesk-distribution/'.$folder,'local');return ['file_path'=>$path,'original_filename'=>$file->getClientOriginalName(),'file_size'=>$file->getSize()];}
private function remove(?string $path):void {if($path && str_starts_with($path,'sonardesk-distribution/') && !str_contains($path,'..'))Storage::disk('local')->delete($path);}
private function productRules(bool $new):array{return ['name'=>'required|string|max:150','description'=>'nullable|string','version'=>'nullable|string|max:60','platform'=>'nullable|string|max:60','release_notes'=>'nullable|string','minimum_system'=>'nullable|string|max:2000','sort_order'=>'nullable|integer|between:-10000,10000','is_published'=>'nullable|boolean','file'=>[$new?'required':'nullable','file','max:512000']];}
private function releaseRules(bool $new):array{return ['version'=>'required|string|max:40','build_number'=>'required|integer|min:1','channel'=>['required',Rule::in(['stable','beta'])],'minimum_macos'=>'nullable|string|max:30','release_notes'=>'nullable|string','sparkle_signature'=>'nullable|string|max:2048','is_published'=>'nullable|boolean','is_featured'=>'nullable|boolean','file'=>[$new?'required':'nullable','file','max:512000','mimes:dmg,zip,pkg']];}
public function product(Request $request){$data=$request->validate($this->productRules(true));$file=$request->file('file');$attributes=$this->upload($file,'products');unset($data['file']);DownloadProduct::create(array_merge($data,$attributes,['is_published'=>$request->boolean('is_published'),'sort_order'=>(int)($data['sort_order']??0)]));return back()->with('status','Download added');}
public function updateProduct(Request $request,DownloadProduct $product){$data=$request->validate($this->productRules(false));unset($data['file']);$data['is_published']=$request->boolean('is_published');$data['sort_order']=(int)($data['sort_order']??0);if($request->hasFile('file')){$old=$product->file_path;$data=array_merge($data,$this->upload($request->file('file'),'products'));$product->update($data);$this->remove($old);}else{$product->update($data);}return back()->with('status','Download updated');}
public function deleteProduct(DownloadProduct $product){$path=$product->file_path;$product->delete();$this->remove($path);return back()->with('status','Download removed');}
public function release(Request $request){$data=$request->validate($this->releaseRules(true));$file=$request->file('file');unset($data['file']);$release=SonarDeskRelease::create(array_merge($data,$this->upload($file,'releases'),['sha256'=>hash_file('sha256',$file->getRealPath()),'is_published'=>$request->boolean('is_published'),'is_featured'=>$request->boolean('is_featured'),'published_at'=>$request->boolean('is_published')?now():null]));return back()->with('status','Release '.$release->version.' created');}
public function updateRelease(Request $request,SonarDeskRelease $release){$data=$request->validate($this->releaseRules(false));unset($data['file']);$data['is_published']=$request->boolean('is_published');$data['is_featured']=$request->boolean('is_featured');$data['published_at']=$data['is_published']?($release->published_at??now()):null;if($request->hasFile('file')){$file=$request->file('file');$old=$release->file_path;$data=array_merge($data,$this->upload($file,'releases'),['sha256'=>hash_file('sha256',$file->getRealPath())]);$release->update($data);$this->remove($old);}else{$release->update($data);}return back()->with('status','Release updated');}
public function deleteRelease(SonarDeskRelease $release){$path=$release->file_path;$release->delete();$this->remove($path);return back()->with('status','Release removed');}
public function publish(Request $request,SonarDeskRelease $release){$data=$request->validate(['is_published'=>'required|boolean']);$release->update(['is_published'=>(bool)$data['is_published'],'published_at'=>$data['is_published']?($release->published_at??now()):null]);return back()->with('status','Release status updated');}
}
@@ -5,7 +5,7 @@ use Illuminate\Support\Facades\Storage;
use Modules\Licensing\Models\{SonarDeskRelease,DownloadProduct};
use Symfony\Component\HttpFoundation\BinaryFileResponse;
final class DistributionController extends Controller {
public function index(){ $latest=SonarDeskRelease::published()->where('channel','stable')->orderByDesc('published_at')->first(); $releases=SonarDeskRelease::published()->orderByDesc('published_at')->paginate(12);$products=DownloadProduct::where('is_published',true)->whereNotNull('file_path')->orderBy('name')->get();return view('licensing::downloads.index',compact('latest','releases','products')); }
public function index(){ $latest=SonarDeskRelease::published()->where('channel','stable')->orderByDesc('published_at')->first(); $releases=SonarDeskRelease::published()->orderByDesc('published_at')->paginate(12);$products=DownloadProduct::where('is_published',true)->whereNotNull('file_path')->orderBy('name')->orderBy('sort_order')->orderByDesc('created_at')->get();return view('licensing::downloads.index',compact('latest','releases','products')); }
public function release(SonarDeskRelease $release){abort_unless($release->is_published && $release->published_at && $release->file_path,404);return view('licensing::downloads.release',compact('release'));}
public function product(DownloadProduct $product){abort_unless($product->is_published && $product->file_path,404);return view('licensing::downloads.product',compact('product'));}
public function latest():BinaryFileResponse{$r=SonarDeskRelease::published()->where('channel','stable')->orderByDesc('published_at')->firstOrFail();return $this->send($r);}
+2 -2
View File
@@ -3,6 +3,6 @@ namespace Modules\Licensing\Models;
use Illuminate\Database\Eloquent\Model;
final class DownloadProduct extends Model {
protected $table='download_products';
protected $fillable=['name','description','version','platform','file_path','original_filename','file_size','is_published'];
protected function casts():array{return ['is_published'=>'boolean','file_size'=>'integer'];}
protected $fillable=['name','description','version','platform','file_path','original_filename','file_size','is_published','release_notes','minimum_system','sort_order'];
protected function casts():array{return ['is_published'=>'boolean','file_size'=>'integer','sort_order'=>'integer'];}
}
@@ -3,13 +3,16 @@
<div class="space-y-6">
<div class="flex flex-wrap items-center justify-between gap-4 border-b border-zinc-200 pb-5 dark:border-zinc-700">
<div>
<p class="text-xs font-semibold uppercase tracking-widest text-amber-600 dark:text-amber-400">ABSDev / Products</p>
<p class="text-xs font-semibold uppercase tracking-widest text-orange-600 dark:text-orange-400">ABSDev / Products</p>
<h1 class="mt-1 text-3xl font-bold tracking-tight text-zinc-900 dark:text-white">SonarDesk</h1>
<p class="mt-1 text-sm text-zinc-500 dark:text-zinc-400">Licensing administration · Read only</p>
<p class="mt-1 text-sm text-zinc-500 dark:text-zinc-400">Manage licensing, releases and product downloads</p>
</div>
<div class="flex flex-wrap gap-2 text-sm">
<a class="rounded-lg border border-zinc-300 px-4 py-2 hover:bg-zinc-100 dark:border-zinc-600 dark:hover:bg-zinc-800" href="{{ route('administration.sonardesk.index') }}">Overview</a>
<a class="rounded-lg border border-zinc-300 px-4 py-2 hover:bg-zinc-100 dark:border-zinc-600 dark:hover:bg-zinc-800" href="{{ route('administration.sonardesk.licenses') }}">Licences</a>
@if (\Illuminate\Support\Facades\Route::has('administration.sonardesk.distribution'))
<a class="rounded-lg border border-orange-500 bg-orange-500 px-4 py-2 font-semibold text-white hover:bg-orange-600" href="{{ route('administration.sonardesk.distribution') }}">Downloads &amp; releases</a>
@endif
</div>
</div>
<div class="sonardesk-admin space-y-6">@yield('content')</div>
@@ -17,11 +20,11 @@
<style>
.sonardesk-admin h1{font-size:1.65rem;font-weight:700;margin-bottom:1.25rem}
.sonardesk-admin h2{font-size:1.125rem;font-weight:650;margin:1.5rem 0 .85rem}
.sonardesk-admin a{color:#b7791f;text-decoration:underline;text-underline-offset:3px}
.sonardesk-admin a{color:#f97316;text-decoration:underline;text-underline-offset:3px}
.sonardesk-admin .cards,.sonardesk-admin .detail{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,240px),1fr));gap:1rem;margin-bottom:1.5rem}
.sonardesk-admin .card{border:1px solid #d4d4d8;border-radius:.85rem;padding:1.4rem;background:rgba(127,127,127,.035)}
.dark .sonardesk-admin .card{border-color:#52525b}
.sonardesk-admin .metric{font-size:2rem;font-weight:750;color:#b7791f}
.sonardesk-admin .metric{font-size:2rem;font-weight:750;color:#f97316}
.sonardesk-admin .muted,.sonardesk-admin dt{color:#71717a}
.sonardesk-admin dd{margin:.25rem 0 .8rem;overflow-wrap:anywhere}
.sonardesk-admin .table-wrap{width:100%;overflow-x:auto;border:1px solid #d4d4d8;border-radius:.85rem}
@@ -1,9 +1,12 @@
@extends('licensing::admin.layout')
@section('title','Downloads & releases')
@section('content')
<h1>Downloads &amp; releases</h1>@if(session('status'))<p>{{ session('status') }}</p>@endif
<h2>Publish SonarDesk release</h2><form method="POST" enctype="multipart/form-data" action="{{ route('administration.sonardesk.distribution.releases.store') }}">@csrf
<p><input name="version" placeholder="Version (e.g. 1.2.0)" required> <input name="build_number" type="number" min="1" placeholder="Build" required> <select name="channel"><option value="stable">Stable</option><option value="beta">Beta</option></select></p>
<p><input name="minimum_macos" placeholder="Minimum macOS version"><input type="file" name="file" accept=".dmg,.zip,.pkg" required></p><p><textarea name="release_notes" placeholder="Full release notes" rows="5" cols="70"></textarea></p><p><input name="sparkle_signature" placeholder="Sparkle EdDSA signature (optional)" size="65"></p><label><input type="checkbox" name="is_published" value="1"> Publish</label> <button type="submit">Create release</button></form>
<h2>Releases</h2><table><thead><tr><th>Version</th><th>Channel</th><th>Status</th><th>Downloads</th><th>Action</th></tr></thead><tbody>@foreach($releases as $r)<tr><td>{{ $r->version }} ({{ $r->build_number }})</td><td>{{ $r->channel }}</td><td>{{ $r->is_published ? 'Published' : 'Draft' }}</td><td>{{ $r->download_count }}</td><td><form method="POST" action="{{ route('administration.sonardesk.distribution.releases.publish',$r) }}">@csrf @method('PATCH')<input type="hidden" name="is_published" value="{{ $r->is_published ? 0 : 1 }}"><button>{{ $r->is_published ? 'Unpublish' : 'Publish' }}</button></form></td></tr>@endforeach</tbody></table>{{ $releases->links() }}
<h2>Add other download (e.g. SonarDisk)</h2><form method="POST" enctype="multipart/form-data" action="{{ route('administration.sonardesk.distribution.products.store') }}">@csrf<p><input name="name" placeholder="Product name" required><input name="version" placeholder="Version"><input name="platform" placeholder="Platform"></p><p><textarea name="description" placeholder="Description"></textarea></p><p><input name="file" type="file" required></p><label><input type="checkbox" name="is_published" value="1"> Publish</label><button>Add download</button></form><h2>Other downloads</h2><ul>@foreach($products as $p)<li>{{ $p->name }} {{ $p->version }} — {{ $p->download_count }} downloads ({{ $p->is_published ? 'published' : 'draft' }})</li>@endforeach</ul>
@endsection
<div class="sd-admin"><div class="sd-admin-head"><div><p class="sd-eyebrow">PRODUCT DISTRIBUTION</p><h1>Downloads &amp; releases</h1><p>Manage SonarDesk releases, SonarDisk and alternative installers from one place.</p></div><a href="{{ route('sonardesk.downloads') }}" target="_blank" rel="noopener" class="sd-link">View public downloads ↗</a></div>
@if(session('status'))<div class="sd-notice">{{ session('status') }}</div>@endif
@if($errors->any())<div class="sd-errors"><strong>Unable to save:</strong><ul>@foreach($errors->all() as $error)<li>{{ $error }}</li>@endforeach</ul></div>@endif
<div class="sd-two"><section class="sd-panel"><h2>Add SonarDesk release</h2><form method="POST" enctype="multipart/form-data" action="{{ route('administration.sonardesk.distribution.releases.store') }}">@csrf<div class="sd-grid"><label>Version<input name="version" required placeholder="1.2.0" value="{{ old('version') }}"></label><label>Build<input name="build_number" type="number" min="1" required value="{{ old('build_number') }}"></label><label>Channel<select name="channel"><option value="stable">Stable</option><option value="beta">Beta</option></select></label><label>Minimum macOS<input name="minimum_macos" placeholder="14.0"></label></div><label>Full release notes<textarea name="release_notes" rows="4"></textarea></label><label>Sparkle signature (optional)<input name="sparkle_signature"></label><label>Installer (.dmg, .zip, .pkg; up to 500 MB)<input type="file" name="file" required accept=".dmg,.zip,.pkg"></label><div class="sd-actions"><label class="sd-check"><input type="checkbox" name="is_published" value="1"> Publish now</label><button>Create release</button></div></form></section>
<section class="sd-panel"><h2>Add product / alternative download</h2><form method="POST" enctype="multipart/form-data" action="{{ route('administration.sonardesk.distribution.products.store') }}">@csrf<div class="sd-grid"><label>Product name<input name="name" required placeholder="SonarDisk"></label><label>Version<input name="version" placeholder="1.0"></label><label>Platform<input name="platform" placeholder="macOS · Apple Silicon"></label><label>Display order<input name="sort_order" type="number" value="0"></label></div><label>Short description<textarea name="description" rows="2"></textarea></label><label>Full release notes<textarea name="release_notes" rows="3"></textarea></label><label>System requirements<input name="minimum_system" placeholder="macOS 14 or later"></label><label>Download file (up to 500 MB)<input name="file" type="file" required></label><div class="sd-actions"><label class="sd-check"><input type="checkbox" name="is_published" value="1"> Publish now</label><button>Add download</button></div></form></section></div>
<section class="sd-panel"><h2>Manage products and alternative downloads <small>({{ $products->count() }})</small></h2><p class="sd-help">Add multiple files under the same product name to group them on the public page. Expand an entry to edit, replace its file or unpublish it.</p>@forelse($products as $p)<details class="sd-entry"><summary><strong>{{ $p->name }}</strong><span>{{ $p->version ?: 'Unversioned' }} · {{ $p->platform ?: 'Any platform' }}</span><span class="sd-status {{ $p->is_published ? 'live' : '' }}">{{ $p->is_published ? 'Published' : 'Draft' }}</span><span>{{ number_format($p->download_count) }} downloads</span></summary><form method="POST" enctype="multipart/form-data" action="{{ route('administration.sonardesk.distribution.products.update',$p) }}">@csrf @method('PATCH')<div class="sd-grid"><label>Name<input name="name" value="{{ $p->name }}" required></label><label>Version<input name="version" value="{{ $p->version }}"></label><label>Platform<input name="platform" value="{{ $p->platform }}"></label><label>Order<input type="number" name="sort_order" value="{{ $p->sort_order ?? 0 }}"></label></div><label>Description<textarea name="description" rows="2">{{ $p->description }}</textarea></label><label>Release notes<textarea name="release_notes" rows="4">{{ $p->release_notes }}</textarea></label><label>System requirements<input name="minimum_system" value="{{ $p->minimum_system }}"></label><label>Replace file (optional) — currently {{ $p->original_filename }}<input type="file" name="file"></label><div class="sd-actions"><label class="sd-check"><input type="checkbox" name="is_published" value="1" @checked($p->is_published)> Published</label><button>Save changes</button><a href="{{ route('sonardesk.products.show',$p) }}" target="_blank" rel="noopener">Public details ↗</a></div></form><form method="POST" action="{{ route('administration.sonardesk.distribution.products.delete',$p) }}" onsubmit="return confirm('Permanently delete this download and its file?')">@csrf @method('DELETE')<button class="sd-danger">Delete download</button></form></details>@empty<p>No additional downloads yet. Add SonarDisk or an alternative installer above.</p>@endforelse</section>
<section class="sd-panel"><h2>Manage SonarDesk releases</h2>@forelse($releases as $r)<details class="sd-entry"><summary><strong>SonarDesk {{ $r->version }}</strong><span>Build {{ $r->build_number }} · {{ ucfirst($r->channel) }}</span><span class="sd-status {{ $r->is_published ? 'live' : '' }}">{{ $r->is_published ? 'Published' : 'Draft' }}</span><span>{{ number_format($r->download_count) }} downloads</span></summary><form method="POST" enctype="multipart/form-data" action="{{ route('administration.sonardesk.distribution.releases.update',$r) }}">@csrf @method('PATCH')<div class="sd-grid"><label>Version<input name="version" required value="{{ $r->version }}"></label><label>Build<input type="number" min="1" name="build_number" required value="{{ $r->build_number }}"></label><label>Channel<select name="channel"><option value="stable" @selected($r->channel==='stable')>Stable</option><option value="beta" @selected($r->channel==='beta')>Beta</option></select></label><label>Minimum macOS<input name="minimum_macos" value="{{ $r->minimum_macos }}"></label></div><label>Release notes<textarea name="release_notes" rows="5">{{ $r->release_notes }}</textarea></label><label>Sparkle signature<input name="sparkle_signature" value="{{ $r->sparkle_signature }}"></label><label>Replace installer (optional) — {{ $r->original_filename }}<input type="file" name="file" accept=".dmg,.zip,.pkg"></label><div class="sd-actions"><label class="sd-check"><input type="checkbox" name="is_published" value="1" @checked($r->is_published)> Published</label><label class="sd-check"><input type="checkbox" name="is_featured" value="1" @checked($r->is_featured)> Featured</label><button>Save release</button></div></form><form method="POST" action="{{ route('administration.sonardesk.distribution.releases.delete',$r) }}" onsubmit="return confirm('Permanently delete this release and its installer?')">@csrf @method('DELETE')<button class="sd-danger">Delete release</button></form></details>@empty<p>No SonarDesk releases yet.</p>@endforelse<div class="sd-pager">{{ $releases->links() }}</div></section></div>
<style>.sd-admin{max-width:1480px;margin:auto}.sd-admin-head{display:flex;justify-content:space-between;gap:20px;align-items:center;margin-bottom:25px}.sd-admin-head h1{font-size:2rem;margin:4px 0}.sd-admin-head p{color:#71717a}.sd-eyebrow{font-size:11px;letter-spacing:.18em;color:#f97316!important;font-weight:800}.sd-two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:20px}.sd-panel{border:1px solid #a1a1aa55;border-radius:16px;padding:25px;margin-bottom:20px;background:rgba(127,127,127,.035)}.sd-panel h2{font-size:1.25rem;margin:0 0 20px}.sd-panel label{display:block;font-size:13px;font-weight:650;margin-bottom:14px}.sd-panel input:not([type=checkbox]),.sd-panel select,.sd-panel textarea{display:block;width:100%;margin-top:6px;padding:10px 12px;border:1px solid #a1a1aa88;border-radius:8px;background:transparent;color:inherit}.sd-panel select option{color:#111}.sd-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:0 14px}.sd-actions{display:flex;gap:15px;align-items:center;flex-wrap:wrap}.sd-panel .sd-check{display:flex;align-items:center;gap:8px;margin:0}.sd-panel button:not(.sd-danger){background:#f97316;color:#fff;border:0;padding:10px 18px;border-radius:8px;font-weight:750;cursor:pointer}.sd-panel .sd-danger{background:transparent;border:1px solid #dc2626;color:#dc2626;padding:8px 12px;border-radius:8px;margin-top:12px;cursor:pointer}.sd-entry{border-top:1px solid #a1a1aa55;padding:15px 0}.sd-entry summary{cursor:pointer;display:flex;gap:20px;align-items:center;flex-wrap:wrap}.sd-entry summary strong{min-width:170px}.sd-entry summary span{font-size:13px;color:#71717a}.sd-entry[open] summary{margin-bottom:20px}.sd-status{padding:4px 9px;border-radius:30px;background:#a1a1aa33}.sd-status.live{background:#16a34a22!important;color:#16a34a!important}.sd-notice,.sd-errors{padding:14px;border-radius:8px;margin-bottom:18px;background:#16a34a22}.sd-errors{background:#dc262622}.sd-help{color:#71717a;margin-bottom:18px}.sd-link{white-space:nowrap}.sd-pager{margin-top:20px}@media(max-width:900px){.sd-two{grid-template-columns:1fr}}@media(max-width:540px){.sd-grid{grid-template-columns:1fr}.sd-admin-head{display:block}}</style>
@endsection
@@ -1,7 +1,4 @@
@extends('sonardesk.layout')
@section('title','Downloads & Release Notes')
@section('content')<section class="sd-pagehead"><div class="sd-container"><span class="sd-eyebrow">Software distribution</span><h1>Downloads &amp; release notes</h1><p>All published applications in one place. Download current installers, view product details and read complete release notes.</p></div></section><section class="sd-section"><div class="sd-container"><span class="sd-eyebrow">Current downloads</span><h2>Available software</h2><div class="sd-grid">
@if($latest)<article class="sd-card"><span class="sd-eyebrow">SonarDesk · macOS</span><h3>SonarDesk</h3><p class="sd-meta">Version {{ $latest->version }} @if($latest->published_at) · {{ $latest->published_at->format('j M Y') }} @endif</p><p>{{ \Illuminate\Support\Str::limit(trim(preg_replace('/\s+/', ' ', strip_tags(\Illuminate\Support\Str::markdown($latest->release_notes ?: 'Native SonarQube project monitoring for macOS.', ['html_input'=>'strip','allow_unsafe_links'=>false])))), 155) }}</p><div class="sd-card-actions"><a class="sd-btn sd-btn-dark" href="{{ route('sonardesk.latest') }}">Download ↓</a><a class="sd-textlink" href="{{ route('sonardesk.releases.show',$latest) }}">Details &amp; notes →</a></div></article>@endif
@foreach($products as $product)<article class="sd-card"><span class="sd-eyebrow">{{ $product->platform ?: 'Software' }}</span><h3>{{ $product->name }}</h3><p class="sd-meta">{{ $product->version ? 'Version '.$product->version : 'Current release' }}</p><p>{{ \Illuminate\Support\Str::limit(trim(preg_replace('/\s+/', ' ', strip_tags(\Illuminate\Support\Str::markdown($product->description ?: 'Published software download.', ['html_input'=>'strip','allow_unsafe_links'=>false])))), 155) }}</p><div class="sd-card-actions"><a class="sd-btn sd-btn-dark" href="{{ route('sonardesk.products.download',$product) }}">Download ↓</a><a class="sd-textlink" href="{{ route('sonardesk.products.show',$product) }}">Product details →</a></div></article>@endforeach
</div>@if(!$latest && $products->isEmpty())<p class="sd-note">No published installers are available yet. Downloads appear automatically when a product or release is published in the distribution administration.</p>@endif</div></section>
<section class="sd-section sd-section-alt"><div class="sd-container"><span class="sd-eyebrow">Release archive</span><h2>SonarDesk release history</h2><p class="sd-intro">Read a summary here or open the full release page for notes, requirements and the installer.</p><div class="sd-list">@forelse($releases as $release)<article class="sd-row"><div><strong>SonarDesk {{ $release->version }}</strong><p>Build {{ $release->build_number }} · {{ ucfirst($release->channel) }} @if($release->published_at) · {{ $release->published_at->format('j M Y') }} @endif</p><p>{{ \Illuminate\Support\Str::limit(trim(preg_replace('/\s+/', ' ', strip_tags(\Illuminate\Support\Str::markdown($release->release_notes ?: 'Release information available on the details page.', ['html_input'=>'strip','allow_unsafe_links'=>false])))), 160) }}</p></div><div class="sd-row-actions"><a href="{{ route('sonardesk.releases.show',$release) }}">Release details →</a><a href="{{ route('sonardesk.releases.download',$release) }}">Download ↓</a></div></article>@empty<p>No published releases yet.</p>@endforelse</div><div class="sd-pagination">{{ $releases->links() }}</div></div></section>@endsection
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Product downloads · ABSDEV</title><link rel="stylesheet" href="{{ asset('css/sonardesk-absdev.css') }}"><link rel="stylesheet" href="{{ asset('css/sonardesk-downloads.css') }}"></head><body class="sd-site"><header class="sd-top"><a class="sd-brand" href="{{ url('/') }}">ABS<span>DEV</span></a><nav><a href="{{ url('/sonardesk') }}">SonarDesk</a><a href="{{ url('/sonardesk/features') }}">Features</a><a href="{{ url('/sonardesk/pricing') }}">Pricing</a><a class="selected" href="{{ route('sonardesk.downloads') }}">Downloads</a></nav></header><main class="sd-main"><section class="sd-intro"><span class="sd-kicker">ABSDEV SOFTWARE</span><h1>Downloads &amp; releases</h1><p>Get the latest versions of SonarDesk, SonarDisk and additional tools. Choose the installer that matches your system.</p></section>
@if($latest)<section class="sd-feature"><div><span class="sd-kicker">LATEST STABLE · SONARDESK</span><h2>SonarDesk <span>{{ $latest->version }}</span></h2><p>{{ \Illuminate\Support\Str::limit(strip_tags($latest->release_notes ?: 'Native SonarQube monitoring for macOS.'),180) }}</p><div class="sd-buttons"><a class="sd-btn" href="{{ route('sonardesk.releases.download',$latest) }}">Download for macOS ↓</a><a class="sd-btn ghost" href="{{ route('sonardesk.releases.show',$latest) }}">Release details →</a></div></div><div class="sd-feature-art"><span>Sonar<span class="sd-gold">Desk</span></span><small>Native code quality dashboard</small></div></section>@endif
@if($products->isNotEmpty())<section class="sd-section"><div class="sd-section-title"><span class="sd-kicker">SOFTWARE LIBRARY</span><h2>Products &amp; alternative downloads</h2><p>Installers are grouped by product, with each available platform and version listed together.</p></div>@foreach($products->groupBy('name') as $name => $files)<article class="sd-product"><div class="sd-product-head"><div><h3>{{ $name }}</h3><p>{{ $files->first()->description ?: 'Available downloads and supporting files.' }}</p></div><span class="sd-count">{{ $files->count() }} {{ $files->count() === 1 ? 'download' : 'downloads' }}</span></div><div class="sd-files">@foreach($files as $file)<div class="sd-file"><div><strong>{{ $file->version ? 'Version '.$file->version : ($file->original_filename ?: 'Download') }}</strong><small>{{ $file->platform ?: 'All platforms' }} @if($file->file_size) · {{ number_format($file->file_size / 1048576,1) }} MB @endif</small></div><div class="sd-file-actions"><a href="{{ route('sonardesk.products.show',$file) }}">Details</a><a class="sd-btn small" href="{{ route('sonardesk.products.download',$file) }}">Download ↓</a></div></div>@endforeach</div></article>@endforeach</section>@endif
<section class="sd-section"><div class="sd-section-title"><span class="sd-kicker">RELEASE HISTORY</span><h2>SonarDesk versions</h2><p>Full release notes and system requirements are available on each release page.</p></div><div class="sd-history">@forelse($releases as $release)<div class="sd-file"><div><strong>SonarDesk {{ $release->version }}</strong><small>Build {{ $release->build_number }} · {{ ucfirst($release->channel) }}</small><p>{{ \Illuminate\Support\Str::limit(strip_tags($release->release_notes ?: 'Release information'),120) }}</p></div><div class="sd-file-actions"><a href="{{ route('sonardesk.releases.show',$release) }}">Release notes</a><a class="sd-btn small" href="{{ route('sonardesk.releases.download',$release) }}">Download ↓</a></div></div>@empty<p>No SonarDesk releases published yet.</p>@endforelse</div><div class="sd-pagination">{{ $releases->links() }}</div></section></main><footer class="sd-footer"><span>© {{ date('Y') }} ABSDEV</span><a href="{{ url('/') }}">ABSDEV homepage</a></footer></body></html>
@@ -1,3 +1 @@
@extends('sonardesk.layout')
@section('title',$product->name.' — Download')
@section('content')<section class="sd-pagehead"><div class="sd-container"><a class="sd-back" href="{{ route('sonardesk.downloads') }}">← All downloads</a><p class="sd-eyebrow" style="margin-top:24px">{{ $product->platform ?: 'Software' }} · {{ $product->version ? 'Version '.$product->version : 'Current release' }}</p><h1>{{ $product->name }}</h1><p>Product information and installer download.</p><div class="sd-actions"><a class="sd-btn sd-btn-primary" href="{{ route('sonardesk.products.download',$product) }}">Download {{ $product->name }} ↓</a></div></div></section><section class="sd-section"><div class="sd-container sd-detail"><article class="sd-card"><h2>About {{ $product->name }}</h2><div class="sd-prose">{!! \Illuminate\Support\Str::markdown($product->description ?: 'Published software download.', ['html_input'=>'strip','allow_unsafe_links'=>false]) !!}</div><hr class="sd-divider"><h2>Download information</h2><p><strong>Platform:</strong> {{ $product->platform ?: 'See product documentation' }}</p>@if($product->version)<p><strong>Version:</strong> {{ $product->version }}</p>@endif @if($product->file_size)<p><strong>Size:</strong> {{ number_format($product->file_size / 1048576, 2) }} MB</p>@endif<a class="sd-btn sd-btn-dark" href="{{ route('sonardesk.products.download',$product) }}">Download installer ↓</a></article></div></section>@endsection
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>{{ $product->name }} · ABSDEV</title><link rel="stylesheet" href="{{ asset('css/sonardesk-downloads.css') }}"></head><body class="sd-site"><header class="sd-top"><a class="sd-brand" href="{{ url('/') }}">ABS<span>DEV</span></a><nav><a href="{{ url('/sonardesk') }}">SonarDesk</a><a href="{{ route('sonardesk.downloads') }}">All downloads</a></nav></header><main class="sd-main sd-detail"><a class="sd-back" href="{{ route('sonardesk.downloads') }}">← All downloads</a><span class="sd-kicker">PRODUCT DOWNLOAD</span><h1>{{ $product->name }}</h1><p class="sd-lead">{{ $product->description }}</p><div class="sd-meta"><span>Version {{ $product->version ?: 'Unspecified' }}</span><span>{{ $product->platform ?: 'All platforms' }}</span></div><a class="sd-btn" href="{{ route('sonardesk.products.download',$product) }}">Download {{ $product->original_filename }} ↓</a>@if($product->release_notes)<section class="sd-detail-section"><h2>Release notes</h2><div class="sd-pre">{{ $product->release_notes }}</div></section>@endif@if($product->minimum_system)<section class="sd-detail-section"><h2>System requirements</h2><div class="sd-pre">{{ $product->minimum_system }}</div></section>@endif</main><footer class="sd-footer">© {{ date('Y') }} ABSDEV</footer></body></html>
@@ -1,3 +1 @@
@extends('sonardesk.layout')
@section('title','SonarDesk '.$release->version.' — Release Notes')
@section('content')<section class="sd-pagehead"><div class="sd-container"><a class="sd-back" href="{{ route('sonardesk.downloads') }}">← All downloads</a><p class="sd-eyebrow" style="margin-top:24px">SonarDesk · {{ ucfirst($release->channel) }}</p><h1>SonarDesk {{ $release->version }}</h1><p>Build {{ $release->build_number }} @if($release->published_at) · Released {{ $release->published_at->format('j F Y') }} @endif</p><div class="sd-actions"><a class="sd-btn sd-btn-primary" href="{{ route('sonardesk.releases.download',$release) }}">Download SonarDesk ↓</a></div></div></section><section class="sd-section"><div class="sd-container sd-detail"><article class="sd-card"><h2>Release notes</h2><div class="sd-prose">{!! \Illuminate\Support\Str::markdown($release->release_notes ?: 'No release notes were provided for this version.', ['html_input'=>'strip','allow_unsafe_links'=>false]) !!}</div><hr class="sd-divider"><h2>System requirements</h2><p><strong>Platform:</strong> macOS</p><p><strong>Minimum version:</strong> {{ $release->minimum_macos ?: 'See application documentation' }}</p><hr class="sd-divider"><h2>Installation</h2><ol><li>Download the signed application installer.</li><li>Open the downloaded disk image and move SonarDesk into Applications.</li><li>Launch SonarDesk and configure your SonarQube connection.</li></ol>@if($release->file_size)<p class="sd-meta">Installer size: {{ number_format($release->file_size / 1048576, 2) }} MB</p>@endif @if($release->sha256)<details><summary>SHA-256 checksum</summary><p class="sd-checksum"><code>{{ $release->sha256 }}</code></p></details>@endif<div class="sd-actions"><a class="sd-btn sd-btn-dark" href="{{ route('sonardesk.releases.download',$release) }}">Download version {{ $release->version }} ↓</a></div></article></div></section>@endsection
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>SonarDesk {{ $release->version }} · ABSDEV</title><link rel="stylesheet" href="{{ asset('css/sonardesk-downloads.css') }}"></head><body class="sd-site"><header class="sd-top"><a class="sd-brand" href="{{ url('/') }}">ABS<span>DEV</span></a><nav><a href="{{ url('/sonardesk') }}">SonarDesk</a><a href="{{ route('sonardesk.downloads') }}">All downloads</a></nav></header><main class="sd-main sd-detail"><a class="sd-back" href="{{ route('sonardesk.downloads') }}">← All downloads</a><span class="sd-kicker">SONARDESK · {{ strtoupper($release->channel) }}</span><h1>SonarDesk {{ $release->version }}</h1><p class="sd-lead">Build {{ $release->build_number }}</p><a class="sd-btn" href="{{ route('sonardesk.releases.download',$release) }}">Download {{ $release->original_filename }} ↓</a><section class="sd-detail-section"><h2>Release notes</h2><div class="sd-pre">{{ $release->release_notes ?: 'No release notes provided.' }}</div></section><section class="sd-detail-section"><h2>System requirements</h2><p>macOS {{ $release->minimum_macos ?: 'See application documentation' }} or later.</p></section>@if($release->sha256)<section class="sd-detail-section"><h2>File verification</h2><p>SHA-256</p><code class="sd-hash">{{ $release->sha256 }}</code></section>@endif</main><footer class="sd-footer">© {{ date('Y') }} ABSDEV</footer></body></html>
@@ -15,5 +15,9 @@ Route::middleware(['web','auth','verified','can:manage-sonardesk-licensing'])->p
Route::get('/distribution',[AdminDownloads::class,'index'])->name('distribution');
Route::post('/distribution/releases',[AdminDownloads::class,'release'])->name('distribution.releases.store');
Route::post('/distribution/products',[AdminDownloads::class,'product'])->name('distribution.products.store');
Route::patch('/distribution/products/{product}',[AdminDownloads::class,'updateProduct'])->name('distribution.products.update');
Route::delete('/distribution/products/{product}',[AdminDownloads::class,'deleteProduct'])->name('distribution.products.delete');
Route::patch('/distribution/releases/{release}',[AdminDownloads::class,'updateRelease'])->name('distribution.releases.update');
Route::delete('/distribution/releases/{release}',[AdminDownloads::class,'deleteRelease'])->name('distribution.releases.delete');
Route::patch('/distribution/releases/{release}/publish',[AdminDownloads::class,'publish'])->name('distribution.releases.publish');
});
File diff suppressed because one or more lines are too long
+3
View File
@@ -0,0 +1,3 @@
*{box-sizing:border-box}.sd-site{margin:0;background:#0f0e0d;color:#f4f7fd;font:16px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}.sd-site a{color:inherit;text-decoration:none}.sd-top,.sd-footer{display:flex;justify-content:space-between;align-items:center;gap:20px;padding:22px max(5vw,24px);border-bottom:1px solid #ffffff1e}.sd-top nav{display:flex;gap:26px;flex-wrap:wrap;font-size:14px;color:#d7cbc2}.sd-top nav a:hover,.sd-top nav .selected{color:#f97316}.sd-brand{font-size:24px;font-weight:850;letter-spacing:-1px}.sd-brand span,.sd-gold{color:#f97316}.sd-main{width:min(1420px,calc(100% - 64px));margin:auto;padding:50px 0 95px}.sd-intro{max-width:900px;margin-bottom:46px}.sd-kicker{color:#f97316;font-size:11px;font-weight:800;letter-spacing:.19em}.sd-site h1{font-size:clamp(42px,5.6vw,72px);line-height:1.07;letter-spacing:-.045em;margin:13px 0 19px}.sd-intro p,.sd-lead{font-size:19px;color:#b9c8dc;max-width:740px}.sd-feature{display:grid;grid-template-columns:1.3fr 1fr;gap:40px;align-items:center;background:linear-gradient(125deg,#162f4d,#0e2239);border:1px solid #ffffff24;border-radius:18px;padding:clamp(28px,4vw,54px);margin-bottom:80px}.sd-feature h2{font-size:clamp(32px,4vw,52px);margin:12px 0}.sd-feature h2 span{color:#f97316}.sd-feature p,.sd-product p,.sd-section-title p{color:#b7c6d9}.sd-feature-art{min-height:230px;display:flex;flex-direction:column;justify-content:center;align-items:center;background:radial-gradient(circle at 50% 50%,#294c73,#11243d 70%);border:1px solid #ffffff20;border-radius:14px}.sd-feature-art>span{font-size:clamp(34px,4vw,58px);font-weight:850;letter-spacing:-.05em}.sd-feature-art small{color:#aabbd0}.sd-buttons{display:flex;flex-wrap:wrap;gap:12px;margin-top:25px}.sd-btn{display:inline-flex;align-items:center;justify-content:center;background:#f97316;color:#13233a!important;padding:12px 20px;border-radius:9px;font-weight:800;font-size:14px;white-space:nowrap}.sd-btn:hover{background:#f5cb77}.sd-btn.ghost{background:transparent;color:#f5f7fb!important;border:1px solid #ffffff60}.sd-btn.small{padding:9px 15px;font-size:13px}.sd-section{margin-top:65px}.sd-section-title{margin-bottom:28px}.sd-section-title h2{font-size:clamp(27px,3vw,39px);letter-spacing:-.03em;margin:8px 0}.sd-product{border:1px solid #ffffff25;border-radius:15px;background:#ffffff08;margin:15px 0;overflow:hidden}.sd-product-head{padding:25px 30px;display:flex;justify-content:space-between;align-items:center;gap:18px}.sd-product-head h3{font-size:25px;margin:0}.sd-product-head p{margin:6px 0 0}.sd-count{color:#f97316;font-size:13px;white-space:nowrap}.sd-file{padding:19px 30px;border-top:1px solid #ffffff1c;display:flex;align-items:center;justify-content:space-between;gap:20px}.sd-file strong{display:block;font-size:16px}.sd-file small{display:block;color:#a9bad1;margin-top:3px}.sd-file p{color:#a9bad1;font-size:13px;margin:6px 0 0}.sd-file-actions{display:flex;align-items:center;gap:20px;white-space:nowrap;font-size:14px}.sd-file-actions>a:not(.sd-btn){color:#f97316}.sd-history{border:1px solid #ffffff25;border-radius:15px;overflow:hidden}.sd-history .sd-file:first-child{border-top:0}.sd-pagination{margin-top:25px}.sd-detail{max-width:1000px;min-height:65vh}.sd-back{display:block;color:#f97316!important;margin-bottom:55px}.sd-meta{display:flex;gap:12px;flex-wrap:wrap;margin:25px 0}.sd-meta span{padding:6px 12px;background:#ffffff15;border-radius:30px}.sd-detail-section{margin-top:55px;padding-top:25px;border-top:1px solid #ffffff24}.sd-detail-section h2{font-size:26px}.sd-pre{white-space:pre-wrap;overflow-wrap:anywhere;color:#cad7e8}.sd-hash{overflow-wrap:anywhere;color:#aebfd5}.sd-footer{border-top:1px solid #ffffff1e;border-bottom:0;color:#a7b9cf;font-size:13px}@media(max-width:760px){.sd-main{width:calc(100% - 36px);padding-top:36px}.sd-feature{grid-template-columns:1fr}.sd-feature-art{min-height:150px}.sd-file{padding:17px;align-items:flex-start;flex-direction:column}.sd-product-head{padding:20px;align-items:flex-start;flex-direction:column}.sd-top{align-items:flex-start;flex-direction:column}.sd-top nav{gap:16px}}
/* SaaSland orange product downloads */
.sd-site{background:#0f0e0d;color:#f7f3ef}.sd-top,.sd-footer{background:#0f0e0d}.sd-top nav a:hover,.sd-top nav .selected,.sd-brand span,.sd-gold{color:#f97316}
@@ -56,6 +56,9 @@
@if (\Illuminate\Support\Facades\Route::has('administration.sonardesk.paddle-reconciliation'))
<flux:sidebar.item icon="arrow-path" :href="route('administration.sonardesk.paddle-reconciliation')" :current="request()->routeIs('administration.sonardesk.paddle-reconciliation')">{{ __('Paddle Reconciliation') }}</flux:sidebar.item>
@endif
@if (\Illuminate\Support\Facades\Route::has('administration.sonardesk.distribution'))
<flux:sidebar.item icon="arrow-down-tray" :href="route('administration.sonardesk.distribution')" :current="request()->routeIs('administration.sonardesk.distribution*')">{{ __('Downloads & Releases') }}</flux:sidebar.item>
@endif
@if (\Illuminate\Support\Facades\Route::has('administration.sonardesk.subscriptions'))
<flux:sidebar.item icon="credit-card" :href="route('administration.sonardesk.subscriptions')" :current="request()->routeIs('administration.sonardesk.subscriptions')">{{ __('Subscriptions') }}</flux:sidebar.item>
@endif
@@ -23,10 +23,11 @@
: 'Streamline operations, boost productivity, and improve customer satisfaction with one connected platform built for modern teams.',
])
@endif
<section style="padding:78px 0;background:linear-gradient(120deg,#0b1a30,#17375a);color:#f4f7fb">
<section style="padding:78px 0;background:linear-gradient(120deg,#0f0e0d,#302219);color:#faf7f3">
<div class="wrap" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:36px;align-items:center">
<div><span style="color:#e5b95b;font-size:12px;font-weight:800;letter-spacing:.17em">ABSDEV SOFTWARE</span><h2 style="font-size:clamp(36px,4vw,55px);line-height:1.12;margin:15px 0">Meet <span style="color:#e5b95b">SonarDesk</span></h2><p style="color:#c6d4e7;font-size:18px;max-width:650px">Native SonarQube monitoring for macOS. Quality gates, vulnerabilities, bugs and issue details — without the browser clutter.</p><div style="display:flex;gap:12px;flex-wrap:wrap;margin-top:26px"><a href="{{ url('/sonardesk') }}" style="padding:12px 20px;border-radius:8px;background:#e5b95b;color:#102540;font-weight:750">Explore SonarDesk →</a><a href="{{ route('sonardesk.downloads') }}" style="padding:12px 20px;border-radius:8px;border:1px solid #ffffff77;color:white;font-weight:750">Downloads ↓</a></div></div>
<div style="border:1px solid #ffffff28;border-radius:18px;background:#ffffff0c;padding:30px"><p style="color:#e5b95b;font-weight:750">SonarDesk for macOS</p><h3 style="font-size:25px;margin:10px 0">Know your code health.</h3><p style="color:#c6d4e7">One focused workspace for project quality, issues and server connections.</p><a href="{{ url('/sonardesk/features') }}" style="color:#e5b95b;font-weight:700">Explore features →</a></div>
<div><span style="color:#f97316;font-size:12px;font-weight:800;letter-spacing:.17em">ABSDEV SOFTWARE</span><h2 style="font-size:clamp(36px,4vw,55px);line-height:1.12;margin:15px 0">Meet <span style="color:#f97316">SonarDesk</span></h2><p style="color:#d7cbc2;font-size:18px;max-width:650px">Native SonarQube monitoring for macOS. Quality gates, vulnerabilities, bugs and issue details — without the browser clutter.</p><div style="display:flex;gap:12px;flex-wrap:wrap;margin-top:26px"><a href="{{ url('/sonardesk') }}" style="padding:12px 20px;border-radius:8px;background:#f97316;color:#191714;font-weight:750">Explore SonarDesk →</a><a href="{{ route('sonardesk.downloads') }}" style="padding:12px 20px;border-radius:8px;border:1px solid #ffffff77;color:white;font-weight:750">Downloads ↓</a></div></div>
<div style="border:1px solid #ffffff28;border-radius:18px;background:#ffffff0c;padding:30px"><p style="color:#f97316;font-weight:750">SonarDesk for macOS</p><h3 style="font-size:25px;margin:10px 0">Know your code health.</h3><p style="color:#d7cbc2">One focused workspace for project quality, issues and server connections.</p><a href="{{ url('/sonardesk/features') }}" style="color:#f97316;font-weight:700">Explore features →</a></div>
</div>
</section>
@endsection
@@ -23,10 +23,10 @@
<style>
.saasland-theme .abs-products-menu{position:relative;display:flex;align-items:center;height:100%}
.saasland-theme .abs-products-menu>a{white-space:nowrap}
.saasland-theme .abs-products-dropdown{display:none;position:absolute;top:100%;right:0;min-width:235px;padding:10px;background:#142c48;border:1px solid #ffffff33;border-radius:12px;box-shadow:0 16px 36px #0005;z-index:100}
.saasland-theme .abs-products-dropdown{display:none;position:absolute;top:100%;right:0;min-width:235px;padding:10px;background:#241c16;border:1px solid #ffffff33;border-radius:12px;box-shadow:0 16px 36px #0005;z-index:100}
.saasland-theme .abs-products-menu:hover .abs-products-dropdown,.saasland-theme .abs-products-menu:focus-within .abs-products-dropdown{display:grid}
.saasland-theme .abs-products-dropdown a{padding:10px 12px;color:#f0f4fa;border-radius:6px;font-size:14px}
.saasland-theme .abs-products-dropdown a:hover{background:#ffffff16;color:#e5b95b}
.saasland-theme .abs-products-dropdown a:hover{background:#ffffff16;color:#f97316}
</style>
<div class="nav-actions">