added theme support
This commit is contained in:
@@ -28,7 +28,13 @@ final class ThemeManagementController extends Controller
|
||||
{
|
||||
$item = $loader->find($theme);
|
||||
|
||||
return $views->makeForTheme($item->key(), $item->entryView(), [
|
||||
abort_unless(
|
||||
in_array($item->type(), ['frontend', 'backend'], true),
|
||||
422,
|
||||
'Only frontend and backend themes can be previewed.'
|
||||
);
|
||||
|
||||
return view('stylist::preview', [
|
||||
'themePreview' => true,
|
||||
'previewTheme' => $item,
|
||||
]);
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Stylist\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller;
|
||||
use Modules\Stylist\Services\SiteSettingsRepository;
|
||||
use Modules\Stylist\Services\ThemeAccessibilityValidator;
|
||||
use Modules\Stylist\Services\ThemeOptimizer;
|
||||
use Modules\Stylist\Services\ThemePackageManager;
|
||||
use Modules\Stylist\Services\ThemeSettings;
|
||||
use Modules\Stylist\Theme\Loader;
|
||||
use Modules\Stylist\View\ThemeViewResolver;
|
||||
|
||||
final class ThemeManagementController extends Controller
|
||||
{
|
||||
public function activate(string $theme, Loader $loader, SiteSettingsRepository $settings)
|
||||
{
|
||||
$item = $loader->find($theme);
|
||||
abort_unless(in_array($item->type(), ['frontend', 'backend'], true), 422, 'Only frontend and backend themes can be activated.');
|
||||
|
||||
$settings->put($item->type().'_theme', $item->key(), 'site', 'string', true);
|
||||
|
||||
return back()->with('status', $item->name().' is now the active '.$item->type().' theme.');
|
||||
}
|
||||
|
||||
public function preview(string $theme, Loader $loader, ThemeViewResolver $views)
|
||||
{
|
||||
$item = $loader->find($theme);
|
||||
|
||||
return $views->makeForTheme($item->key(), $item->entryView(), [
|
||||
'themePreview' => true,
|
||||
'previewTheme' => $item,
|
||||
]);
|
||||
}
|
||||
|
||||
public function settings(Request $request, string $theme, Loader $loader, ThemeSettings $settings)
|
||||
{
|
||||
$item = $loader->find($theme);
|
||||
$settings->save(
|
||||
$item,
|
||||
(array) $request->input('settings', []),
|
||||
(string) $request->input('variant', 'light'),
|
||||
$request->string('appearance')->toString() ?: null,
|
||||
$request->string('accent')->toString() ?: null,
|
||||
);
|
||||
|
||||
return back()->with('status', 'Theme settings saved.');
|
||||
}
|
||||
|
||||
public function install(Request $request, ThemePackageManager $packages)
|
||||
{
|
||||
$request->validate(['theme' => ['required', 'file', 'mimes:zip', 'max:51200']]);
|
||||
$theme = $packages->install($request->file('theme'));
|
||||
|
||||
return back()->with('status', $theme->name().' installed.');
|
||||
}
|
||||
|
||||
public function uninstall(string $theme, Loader $loader, ThemePackageManager $packages)
|
||||
{
|
||||
$item = $loader->find($theme);
|
||||
abort_if(in_array($item->key(), ['sonardesk', 'sonardesk-admin'], true), 403, 'Bundled themes cannot be removed.');
|
||||
$settings = app(SiteSettingsRepository::class);
|
||||
abort_if($settings->get($item->type().'_theme', null, 'site') === $item->key(), 409, 'Deactivate this theme before uninstalling.');
|
||||
$packages->uninstall($item);
|
||||
|
||||
return back()->with('status', $item->name().' uninstalled.');
|
||||
}
|
||||
|
||||
public function export(string $theme, Loader $loader, ThemePackageManager $packages)
|
||||
{
|
||||
$item = $loader->find($theme);
|
||||
$path = $packages->export($item);
|
||||
|
||||
return response()->download($path)->deleteFileAfterSend();
|
||||
}
|
||||
|
||||
public function optimize(string $theme, Loader $loader, ThemeOptimizer $optimizer)
|
||||
{
|
||||
$item = $loader->find($theme);
|
||||
$result = $optimizer->optimize($item);
|
||||
|
||||
return back()->with('status', "Optimised {$result['css']} CSS, {$result['js']} JS and {$result['images']} images.");
|
||||
}
|
||||
|
||||
public function validateTheme(string $theme, Loader $loader, ThemeAccessibilityValidator $validator)
|
||||
{
|
||||
return response()->json(['issues' => $validator->validate($loader->find($theme))]);
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,11 @@ final class StylistServiceProvider extends ServiceProvider
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
\Illuminate\Support\Facades\View::addNamespace(
|
||||
'sonardesk-base',
|
||||
resource_path('views')
|
||||
);
|
||||
|
||||
$this->registerViews();
|
||||
$this->registerThemeViews();
|
||||
$this->registerTranslations();
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Stylist\Providers;
|
||||
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\Facades\View;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Modules\Stylist\Console\CreateThemeCommand;
|
||||
use Modules\Stylist\Console\FindDuplicateViewsCommand;
|
||||
use Modules\Stylist\Console\InspectThemesCommand;
|
||||
use Modules\Stylist\Console\PublishAssetsCommand;
|
||||
use Modules\Stylist\Console\ThemeDoctorCommand;
|
||||
use Modules\Stylist\Console\ValidateThemeCommand;
|
||||
use Modules\Stylist\Html\ThemeHtmlBuilder;
|
||||
use Modules\Stylist\Html\ThemeSeoBuilder;
|
||||
use Modules\Stylist\Support\FrontendNavigation;
|
||||
use Modules\Stylist\Support\ModuleInfo;
|
||||
use Modules\Stylist\Services\FrontendContent;
|
||||
use Modules\Stylist\Services\ThemeSettings;
|
||||
use Modules\Stylist\Theme\Loader;
|
||||
use Modules\Stylist\Theme\Stylist;
|
||||
|
||||
use Modules\Stylist\Services\SiteSettingsRepository;
|
||||
|
||||
final class StylistServiceProvider extends ServiceProvider
|
||||
{
|
||||
protected string $moduleName = 'Stylist';
|
||||
|
||||
protected string $moduleNameLower = 'stylist';
|
||||
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->register(RouteServiceProvider::class);
|
||||
|
||||
$this->mergeConfigFrom(module_path(ModuleInfo::name(), 'Config/config.php'), ModuleInfo::nameLower());
|
||||
|
||||
$this->app->singleton('stylist', fn ($app) => $app->make(Stylist::class));
|
||||
$this->app->singleton('stylist.html', fn ($app) => $app->make(ThemeHtmlBuilder::class));
|
||||
$this->app->singleton('stylist.seo', fn ($app) => $app->make(ThemeSeoBuilder::class));
|
||||
}
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
$this->registerViews();
|
||||
$this->registerThemeViews();
|
||||
$this->registerTranslations();
|
||||
$this->registerPublishing();
|
||||
$this->registerHelpers();
|
||||
$this->registerBlade();
|
||||
|
||||
if ($this->app->runningInConsole()) {
|
||||
$this->commands([
|
||||
PublishAssetsCommand::class,
|
||||
ValidateThemeCommand::class,
|
||||
InspectThemesCommand::class,
|
||||
FindDuplicateViewsCommand::class,
|
||||
ThemeDoctorCommand::class,
|
||||
CreateThemeCommand::class,
|
||||
]);
|
||||
}
|
||||
|
||||
$this->loadMigrationsFrom(module_path(ModuleInfo::name(), 'Database/Migrations'));
|
||||
|
||||
View::composer('*', function ($view): void {
|
||||
if (! $this->app->bound(Stylist::class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$view->with('activeTheme', $this->app->make(Stylist::class)->current());
|
||||
});
|
||||
|
||||
|
||||
View::composer('stylist-theme-saasland::*', function ($view): void {
|
||||
$theme = $this->app->make(Loader::class)->find('saasland');
|
||||
$settings = $this->app->make(ThemeSettings::class)->get($theme);
|
||||
$siteSettings = $this->app->make(SiteSettingsRepository::class)->site();
|
||||
$navigation = $this->app->make(FrontendNavigation::class);
|
||||
$homepage = $this->app->make(FrontendContent::class)->homepage($settings);
|
||||
|
||||
$view->with([
|
||||
'siteSettings' => $siteSettings,
|
||||
'siteTitle' => $siteSettings['site_name'],
|
||||
'siteDescription' => $siteSettings['site_description'],
|
||||
'logoUrl' => $siteSettings['logo_url'] ?: null,
|
||||
'facebookUrl' => $siteSettings['facebook_url'] ?: null,
|
||||
'xUrl' => $siteSettings['x_url'] ?: null,
|
||||
'linkedinUrl' => $siteSettings['linkedin_url'] ?: null,
|
||||
'primaryNavigation' => $navigation->primary(),
|
||||
'footerNavigation' => $navigation->footer(),
|
||||
'themeSettings' => $settings,
|
||||
...$homepage,
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
private function registerViews(): void
|
||||
{
|
||||
$sourcePath = module_path(ModuleInfo::name(), 'Resources/views');
|
||||
$viewPath = resource_path('views/modules/'.ModuleInfo::nameLower());
|
||||
|
||||
$this->publishes([$sourcePath => $viewPath], ['views', 'stylist-module-views']);
|
||||
$this->loadViewsFrom([$viewPath, $sourcePath], ModuleInfo::nameLower());
|
||||
}
|
||||
|
||||
private function registerThemeViews(): void
|
||||
{
|
||||
foreach ($this->themeRoots() as $root) {
|
||||
if (! is_dir($root)) {
|
||||
logger()->warning('Stylist theme root does not exist', [
|
||||
'root' => $root,
|
||||
]);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$themeDirectories = glob($root.'/*', GLOB_ONLYDIR) ?: [];
|
||||
|
||||
foreach ($themeDirectories as $themeDirectory) {
|
||||
$themeViewsPath = $themeDirectory.'/views';
|
||||
|
||||
if (! is_dir($themeViewsPath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$themeKey = strtolower(basename($themeDirectory));
|
||||
|
||||
// Themes always live outside the module, normally in resources/themes.
|
||||
// Register the canonical namespace and the legacy namespace as an alias
|
||||
// to the same application-owned directory. The alias keeps existing
|
||||
// routes and Blade templates working without reintroducing module themes.
|
||||
$namespaces = [
|
||||
"stylist-theme-{$themeKey}",
|
||||
"stylist-module-theme-{$themeKey}",
|
||||
];
|
||||
|
||||
$componentsPath = $themeViewsPath.'/components';
|
||||
|
||||
// The common theme is the application-wide owner of layouts,
|
||||
// authentication, settings and shared components. Livewire 4
|
||||
// resolves its default full-page layout through layouts::app,
|
||||
// so point that compatibility namespace at the common theme.
|
||||
if ($themeKey === 'common') {
|
||||
$layoutsPath = $componentsPath.'/layouts';
|
||||
|
||||
if (is_dir($layoutsPath)) {
|
||||
View::addNamespace('layouts', $layoutsPath);
|
||||
Blade::anonymousComponentPath($layoutsPath, 'layouts');
|
||||
}
|
||||
|
||||
View::addNamespace('common', $themeViewsPath);
|
||||
|
||||
if (is_dir($componentsPath)) {
|
||||
Blade::anonymousComponentPath($componentsPath, 'common');
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($namespaces as $namespace) {
|
||||
View::addNamespace($namespace, $themeViewsPath);
|
||||
|
||||
if (is_dir($componentsPath)) {
|
||||
Blade::anonymousComponentPath($componentsPath, $namespace);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function normalisePath(string $path): string
|
||||
{
|
||||
return rtrim(
|
||||
str_replace('\\', '/', realpath($path) ?: $path),
|
||||
'/'
|
||||
);
|
||||
}
|
||||
|
||||
/** @return array<int, string> */
|
||||
private function themeRoots(): array
|
||||
{
|
||||
return array_values(array_unique(array_filter([
|
||||
(string) config('stylist.themes_path'),
|
||||
])));
|
||||
}
|
||||
|
||||
private function registerTranslations(): void
|
||||
{
|
||||
$langPath = resource_path('lang/modules/'.ModuleInfo::nameLower());
|
||||
$sourcePath = module_path(ModuleInfo::name(), 'Resources/lang');
|
||||
|
||||
$this->loadTranslationsFrom(is_dir($langPath) ? $langPath : $sourcePath, ModuleInfo::nameLower());
|
||||
}
|
||||
|
||||
private function registerPublishing(): void
|
||||
{
|
||||
$this->publishes([
|
||||
module_path(ModuleInfo::name(), 'Config/config.php') => config_path('stylist.php'),
|
||||
], ['config', 'stylist-config']);
|
||||
|
||||
}
|
||||
|
||||
private function registerHelpers(): void
|
||||
{
|
||||
foreach (glob(module_path(ModuleInfo::name(), 'Helpers/*.php')) ?: [] as $filename) {
|
||||
require_once $filename;
|
||||
}
|
||||
}
|
||||
|
||||
private function registerBlade(): void
|
||||
{
|
||||
Blade::directive('themeVariables', fn () => '<?php echo stylist_html()->variables(); ?>');
|
||||
Blade::directive('themeStylesheet', fn ($expression) => '<?php echo stylist_html()->stylesheet('.($expression ?: 'null').'); ?>');
|
||||
Blade::directive('themeScript', fn ($expression) => '<?php echo stylist_html()->script('.($expression ?: 'null').'); ?>');
|
||||
Blade::directive('themeSwitcher', fn () => '<?php echo stylist_html()->switcher(); ?>');
|
||||
Blade::directive('themeSeo', fn ($expression) => '<?php echo app(\Modules\Stylist\Html\ThemeSeoBuilder::class)->render('.($expression ?: '[]').'); ?>');
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
@extends('layouts.app', ['title' => 'Stylist — SonarDesk Administration'])
|
||||
@extends('sonardesk-base::layouts.app', ['title' => 'Stylist — SonarDesk Administration'])
|
||||
|
||||
@section('content')
|
||||
|
||||
|
||||
@@ -0,0 +1,473 @@
|
||||
@extends('layouts.app', ['title' => 'Stylist — SonarDesk Administration'])
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="stylist-admin">
|
||||
|
||||
<section class="portal-header stylist-header">
|
||||
<div>
|
||||
<span class="eyebrow">Administration / Appearance</span>
|
||||
<h1>Stylist</h1>
|
||||
<p class="lead compact">
|
||||
Manage frontend and administration themes independently.
|
||||
Install, configure, preview and export complete themes.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<a href="{{ route('admin.index') }}" class="btn btn-secondary">
|
||||
Back to administration
|
||||
</a>
|
||||
</section>
|
||||
|
||||
@if(session('status'))
|
||||
<div class="alert">{{ session('status') }}</div>
|
||||
@endif
|
||||
|
||||
@if($errors->any())
|
||||
<div class="error">{{ $errors->first() }}</div>
|
||||
@endif
|
||||
|
||||
<section class="stylist-summary">
|
||||
<div class="stylist-summary-card">
|
||||
<span>Installed themes</span>
|
||||
<strong>{{ count($themes) }}</strong>
|
||||
<small>Available designs</small>
|
||||
</div>
|
||||
|
||||
<div class="stylist-summary-card">
|
||||
<span>Frontend theme</span>
|
||||
<strong>{{ $activeFrontendTheme }}</strong>
|
||||
<small>Public website</small>
|
||||
</div>
|
||||
|
||||
<div class="stylist-summary-card">
|
||||
<span>Backend theme</span>
|
||||
<strong>{{ $activeBackendTheme }}</strong>
|
||||
<small>Administration interface</small>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="stylist-install-panel">
|
||||
<div class="stylist-section-heading">
|
||||
<div>
|
||||
<span class="eyebrow">Theme library</span>
|
||||
<h2>Install a theme</h2>
|
||||
<p>Upload a self-contained theme package in ZIP format.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="POST"
|
||||
action="{{ route('stylist.install') }}"
|
||||
enctype="multipart/form-data"
|
||||
class="stylist-install-form">
|
||||
|
||||
@csrf
|
||||
|
||||
<label class="stylist-upload-field">
|
||||
<span>Theme package</span>
|
||||
<input type="file"
|
||||
name="theme"
|
||||
accept=".zip"
|
||||
required>
|
||||
<small>Maximum upload size: 50 MB</small>
|
||||
</label>
|
||||
|
||||
<button type="submit" class="btn btn-primary">
|
||||
Install theme
|
||||
</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="stylist-library">
|
||||
|
||||
<div class="stylist-section-heading">
|
||||
<div>
|
||||
<span class="eyebrow">Appearance</span>
|
||||
<h2>Installed themes</h2>
|
||||
<p>Frontend and backend themes can be activated independently.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stylist-theme-grid">
|
||||
|
||||
@foreach($themes as $item)
|
||||
|
||||
@php
|
||||
$saved = $settingsService->get($item);
|
||||
$variant = $settingsService->variant($item);
|
||||
$appearance = $settingsService->appearance($item);
|
||||
$accent = $settingsService->accent($item);
|
||||
$issues = $accessibility->validate($item);
|
||||
|
||||
$isActive =
|
||||
($item->type() === 'frontend' && $item->key() === $activeFrontendTheme)
|
||||
|| ($item->type() === 'backend' && $item->key() === $activeBackendTheme);
|
||||
|
||||
$isBundled = in_array(
|
||||
$item->key(),
|
||||
['sonardesk', 'sonardesk-admin'],
|
||||
true
|
||||
);
|
||||
|
||||
$themeRoot = realpath(config('stylist.themes_path'));
|
||||
$themePath = realpath($item->path());
|
||||
|
||||
$isInstalled = $themeRoot
|
||||
&& $themePath
|
||||
&& str_starts_with(
|
||||
$themePath,
|
||||
rtrim($themeRoot, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR
|
||||
);
|
||||
@endphp
|
||||
|
||||
<article class="stylist-theme-card {{ $isActive ? 'stylist-theme-active' : '' }}">
|
||||
|
||||
<div class="stylist-preview">
|
||||
|
||||
@if($item->preview() && $item->has($item->preview()))
|
||||
|
||||
<img
|
||||
src="{{ $urls->forTheme($item, $item->preview()) }}"
|
||||
alt="{{ $item->name() }} preview">
|
||||
|
||||
@else
|
||||
|
||||
<div class="stylist-preview-placeholder">
|
||||
<span class="stylist-preview-symbol">◈</span>
|
||||
<strong>{{ $item->name() }}</strong>
|
||||
<small>No preview image available</small>
|
||||
</div>
|
||||
|
||||
@endif
|
||||
|
||||
</div>
|
||||
|
||||
<div class="stylist-theme-body">
|
||||
|
||||
<div class="stylist-theme-heading">
|
||||
|
||||
<div>
|
||||
<span class="stylist-theme-type">
|
||||
{{ ucfirst($item->type()) }} theme
|
||||
</span>
|
||||
|
||||
<h3>{{ $item->name() }}</h3>
|
||||
|
||||
<p class="stylist-version">
|
||||
Version {{ $item->version() }}
|
||||
@if($item->author())
|
||||
· {{ $item->author() }}
|
||||
@endif
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@if($isActive)
|
||||
<span class="status-chip success">
|
||||
Active
|
||||
</span>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
|
||||
@if($item->description())
|
||||
<p class="stylist-description">
|
||||
{{ $item->description() }}
|
||||
</p>
|
||||
@endif
|
||||
|
||||
@if($item->parent())
|
||||
<p class="stylist-parent">
|
||||
Based on {{ $item->parent() }}
|
||||
</p>
|
||||
@endif
|
||||
|
||||
<div class="stylist-actions">
|
||||
|
||||
@if(in_array($item->type(), ['frontend', 'backend'], true))
|
||||
|
||||
@unless($isActive)
|
||||
|
||||
<form method="POST"
|
||||
action="{{ route('stylist.activate', $item->key()) }}">
|
||||
@csrf
|
||||
|
||||
<button type="submit" class="btn btn-primary">
|
||||
Activate {{ ucfirst($item->type()) }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@endunless
|
||||
|
||||
<a class="btn btn-secondary"
|
||||
href="{{ route('stylist.preview', $item->key()) }}"
|
||||
target="_blank"
|
||||
rel="noopener">
|
||||
Preview
|
||||
</a>
|
||||
|
||||
@endif
|
||||
|
||||
<a class="btn btn-secondary"
|
||||
href="{{ route('stylist.export', $item->key()) }}">
|
||||
Export ZIP
|
||||
</a>
|
||||
|
||||
<form method="POST"
|
||||
action="{{ route('stylist.optimize', $item->key()) }}">
|
||||
@csrf
|
||||
|
||||
<button type="submit" class="btn btn-secondary">
|
||||
Optimise assets
|
||||
</button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
<details class="stylist-settings">
|
||||
|
||||
<summary>
|
||||
<span>Theme configuration</span>
|
||||
<span class="stylist-chevron">⌄</span>
|
||||
</summary>
|
||||
|
||||
<form method="POST"
|
||||
action="{{ route('stylist.settings', $item->key()) }}"
|
||||
class="stylist-settings-form">
|
||||
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
@if($item->variants())
|
||||
|
||||
<label>
|
||||
Variant
|
||||
|
||||
<select name="variant">
|
||||
@foreach(array_keys($item->variants()) as $name)
|
||||
<option value="{{ $name }}"
|
||||
@selected($variant === $name)>
|
||||
{{ ucfirst($name) }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</label>
|
||||
|
||||
@endif
|
||||
|
||||
@if($item->supports('dark_mode') || $item->appearances())
|
||||
|
||||
<label>
|
||||
Appearance
|
||||
|
||||
<select name="appearance">
|
||||
@foreach(array_keys($item->appearances()) ?: ['light', 'dark', 'auto'] as $name)
|
||||
<option value="{{ $name }}"
|
||||
@selected($appearance === $name)>
|
||||
{{ ucfirst($name) }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</label>
|
||||
|
||||
@endif
|
||||
|
||||
@if($item->accents())
|
||||
|
||||
<fieldset class="stylist-accent-options">
|
||||
<legend>Accent colour</legend>
|
||||
|
||||
@foreach($item->accents() as $name => $definition)
|
||||
|
||||
<label class="stylist-accent-option">
|
||||
|
||||
<input type="radio"
|
||||
name="accent"
|
||||
value="{{ $name }}"
|
||||
@checked($accent === $name)>
|
||||
|
||||
<span class="stylist-colour-swatch"
|
||||
style="background: {{ data_get($definition, 'swatch', data_get($definition, 'variables.primary', '#64748b')) }}">
|
||||
</span>
|
||||
|
||||
{{ data_get($definition, 'label', ucfirst($name)) }}
|
||||
|
||||
</label>
|
||||
|
||||
@endforeach
|
||||
|
||||
</fieldset>
|
||||
|
||||
@endif
|
||||
|
||||
@foreach($item->settingsSchema() as $key => $definition)
|
||||
|
||||
@php
|
||||
$definition = is_array($definition)
|
||||
? $definition
|
||||
: ['default' => $definition];
|
||||
|
||||
$fieldType = $definition['type'] ?? 'text';
|
||||
$fieldLabel = $definition['label'] ?? str($key)->headline();
|
||||
$fieldValue = $saved[$key] ?? ($definition['default'] ?? '');
|
||||
@endphp
|
||||
|
||||
<div class="stylist-setting-field">
|
||||
|
||||
<label for="stylist-{{ $item->key() }}-{{ $key }}">
|
||||
{{ $fieldLabel }}
|
||||
</label>
|
||||
|
||||
@switch($fieldType)
|
||||
|
||||
@case('color')
|
||||
|
||||
<input
|
||||
id="stylist-{{ $item->key() }}-{{ $key }}"
|
||||
type="color"
|
||||
name="settings[{{ $key }}]"
|
||||
value="{{ $fieldValue }}">
|
||||
@break
|
||||
|
||||
@case('textarea')
|
||||
|
||||
<textarea
|
||||
id="stylist-{{ $item->key() }}-{{ $key }}"
|
||||
name="settings[{{ $key }}]"
|
||||
rows="4">{{ $fieldValue }}</textarea>
|
||||
@break
|
||||
|
||||
@case('checkbox')
|
||||
|
||||
<input type="hidden"
|
||||
name="settings[{{ $key }}]"
|
||||
value="0">
|
||||
|
||||
<label class="stylist-checkbox">
|
||||
<input
|
||||
id="stylist-{{ $item->key() }}-{{ $key }}"
|
||||
type="checkbox"
|
||||
name="settings[{{ $key }}]"
|
||||
value="1"
|
||||
@checked((bool)$fieldValue)>
|
||||
Enabled
|
||||
</label>
|
||||
@break
|
||||
|
||||
@default
|
||||
|
||||
<input
|
||||
id="stylist-{{ $item->key() }}-{{ $key }}"
|
||||
type="{{ $fieldType === 'url' ? 'url' : 'text' }}"
|
||||
name="settings[{{ $key }}]"
|
||||
value="{{ $fieldValue }}">
|
||||
|
||||
@endswitch
|
||||
|
||||
</div>
|
||||
|
||||
@endforeach
|
||||
|
||||
<button type="submit" class="btn btn-primary">
|
||||
Save configuration
|
||||
</button>
|
||||
|
||||
</form>
|
||||
|
||||
</details>
|
||||
|
||||
<div class="stylist-theme-footer">
|
||||
|
||||
<span class="{{ collect($issues)->contains('severity', 'error') ? 'bad' : 'ok' }}">
|
||||
Accessibility:
|
||||
{{ count($issues) ? count($issues).' issue(s)' : 'passed static checks' }}
|
||||
</span>
|
||||
|
||||
@if($isInstalled && !$isBundled && !$isActive)
|
||||
|
||||
<form method="POST"
|
||||
action="{{ route('stylist.uninstall', $item->key()) }}"
|
||||
onsubmit="return confirm('Permanently uninstall this theme?')">
|
||||
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
|
||||
<button type="submit" class="stylist-remove">
|
||||
Uninstall
|
||||
</button>
|
||||
|
||||
</form>
|
||||
|
||||
@endif
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</article>
|
||||
|
||||
@endforeach
|
||||
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.stylist-admin{max-width:1220px;margin:auto;padding-bottom:60px}
|
||||
.stylist-header{margin-bottom:30px}
|
||||
.stylist-header .lead{max-width:740px}
|
||||
.stylist-summary{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:16px;margin-bottom:28px}
|
||||
.stylist-summary-card{background:#fff;border:1px solid #e4e7ec;border-radius:15px;padding:22px;box-shadow:0 2px 6px rgba(15,23,42,.04)}
|
||||
.stylist-summary-card span,.stylist-summary-card small{display:block;color:#667085;font-size:12px}
|
||||
.stylist-summary-card strong{display:block;margin:8px 0;font-size:24px;color:#101828;overflow-wrap:anywhere}
|
||||
.stylist-install-panel{background:#fff;border:1px solid #e4e7ec;border-radius:16px;padding:26px;margin-bottom:35px;box-shadow:0 2px 8px rgba(15,23,42,.04)}
|
||||
.stylist-section-heading{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:22px}
|
||||
.stylist-section-heading h2{font-size:23px;margin:5px 0}
|
||||
.stylist-section-heading p{margin:0;color:#667085}
|
||||
.stylist-install-form{display:flex;gap:20px;align-items:end}
|
||||
.stylist-upload-field{display:block;flex:1;font-size:13px;font-weight:750}
|
||||
.stylist-upload-field span,.stylist-upload-field small{display:block}
|
||||
.stylist-upload-field small{color:#667085;font-weight:400}
|
||||
.stylist-upload-field input{margin:8px 0}
|
||||
.stylist-install-form button{margin-bottom:25px}
|
||||
.stylist-theme-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:22px}
|
||||
.stylist-theme-card{background:#fff;border:1px solid #e4e7ec;border-radius:18px;overflow:hidden;box-shadow:0 5px 22px rgba(15,23,42,.05);min-width:0}
|
||||
.stylist-theme-active{border-color:#93c5fd;box-shadow:0 0 0 3px #dbeafe}
|
||||
.stylist-preview{height:210px;background:#f1f5f9;border-bottom:1px solid #e4e7ec;overflow:hidden}
|
||||
.stylist-preview img{width:100%;height:100%;object-fit:cover}
|
||||
.stylist-preview-placeholder{height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;background:linear-gradient(135deg,#101828,#1d3557);color:#fff;gap:5px}
|
||||
.stylist-preview-symbol{font-size:40px;color:#93c5fd}
|
||||
.stylist-preview-placeholder strong{font-size:18px}
|
||||
.stylist-preview-placeholder small{font-size:12px;color:#94a3b8}
|
||||
.stylist-theme-body{padding:25px}
|
||||
.stylist-theme-heading{display:flex;justify-content:space-between;align-items:flex-start;gap:12px}
|
||||
.stylist-theme-type{text-transform:uppercase;letter-spacing:.1em;color:#2563eb;font-size:10px;font-weight:850}
|
||||
.stylist-theme-heading h3{font-size:22px;letter-spacing:-.025em;margin:5px 0}
|
||||
.stylist-version{color:#667085;font-size:12px;margin:0}
|
||||
.stylist-description{color:#475467;font-size:14px;line-height:1.6;margin:18px 0}
|
||||
.stylist-parent{color:#7c3aed;font-size:12px}
|
||||
.stylist-actions{display:flex;gap:9px;flex-wrap:wrap;margin:22px 0}
|
||||
.stylist-actions form{margin:0}
|
||||
.stylist-actions .btn{font-size:12px;padding:9px 12px}
|
||||
.stylist-settings{border:1px solid #e4e7ec;border-radius:12px;margin-top:20px;overflow:hidden}
|
||||
.stylist-settings summary{display:flex;justify-content:space-between;align-items:center;padding:15px 17px;cursor:pointer;font-weight:750;list-style:none}
|
||||
.stylist-settings summary::-webkit-details-marker{display:none}
|
||||
.stylist-settings[open] summary{border-bottom:1px solid #e4e7ec}
|
||||
.stylist-chevron{font-size:20px;color:#667085}
|
||||
.stylist-settings-form{padding:20px;display:grid;gap:14px}
|
||||
.stylist-settings-form label{font-size:13px}
|
||||
.stylist-settings-form input:not([type=radio]):not([type=checkbox]),.stylist-settings-form select,.stylist-settings-form textarea{width:100%;margin:6px 0 0}
|
||||
.stylist-settings-form input[type=checkbox],.stylist-settings-form input[type=radio]{width:auto;margin:0}
|
||||
.stylist-accent-options{border:1px solid #e4e7ec;border-radius:10px;padding:14px;display:flex;flex-wrap:wrap;gap:12px}
|
||||
.stylist-accent-options legend{font-size:13px;font-weight:750}
|
||||
.stylist-accent-option{display:inline-flex;align-items:center;gap:7px;cursor:pointer}
|
||||
.stylist-colour-swatch{width:17px;height:17px;border-radius:50%;border:1px solid rgba(0,0,0,.12)}
|
||||
.stylist-checkbox{display:flex;align-items:center;gap:9px}
|
||||
.stylist-theme-footer{display:flex;justify-content:space-between;align-items:center;gap:12px;border-top:1px solid #eef2f6;padding-top:17px;margin-top:20px;font-size:12px}
|
||||
.stylist-remove{background:transparent!important;color:#b42318!important;padding:5px 0!important;font-size:12px!important;box-shadow:none!important}
|
||||
.stylist-remove:hover{text-decoration:underline}
|
||||
@media(max-width:850px){.stylist-theme-grid{grid-template-columns:1fr}.stylist-summary{grid-template-columns:1fr}.stylist-install-form{flex-direction:column;align-items:stretch}.stylist-install-form button{margin:0}}
|
||||
</style>
|
||||
|
||||
@endsection
|
||||
@@ -0,0 +1,181 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{ $previewTheme->name() }} — Theme Preview</title>
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--background: #f3f6fa;
|
||||
--surface: #fff;
|
||||
--text: #17243b;
|
||||
--muted: #64748b;
|
||||
--accent: #2563eb;
|
||||
--border: #dce4ee;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--background);
|
||||
color: var(--text);
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
}
|
||||
a { color: inherit; text-decoration: none; }
|
||||
.toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
padding: 14px 28px;
|
||||
background: #142238;
|
||||
color: white;
|
||||
}
|
||||
.toolbar strong { font-size: 14px; }
|
||||
.toolbar small { display: block; color: #aebbd0; margin-top: 4px; }
|
||||
.toolbar a {
|
||||
background: white;
|
||||
color: #142238;
|
||||
border-radius: 7px;
|
||||
padding: 10px 16px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.site-nav {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 22px 6%;
|
||||
background: white;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.site-nav strong { font-size: 21px; }
|
||||
.site-nav span { color: var(--muted); font-size: 13px; }
|
||||
.container { max-width: 1200px; margin: 0 auto; padding: 55px 25px; }
|
||||
.eyebrow {
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: var(--accent);
|
||||
}
|
||||
h1 { font-size: clamp(32px, 5vw, 54px); margin: 15px 0; }
|
||||
.lead { color: var(--muted); font-size: 18px; max-width: 680px; }
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
|
||||
gap: 20px;
|
||||
margin-top: 35px;
|
||||
}
|
||||
.card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 28px;
|
||||
box-shadow: 0 8px 30px rgba(15,23,42,.035);
|
||||
}
|
||||
.card h3 { margin-top: 0; }
|
||||
.card p { color: var(--muted); line-height: 1.6; }
|
||||
.button {
|
||||
display: inline-block;
|
||||
padding: 13px 20px;
|
||||
border-radius: 8px;
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
margin-top: 15px;
|
||||
}
|
||||
.footer {
|
||||
text-align: center;
|
||||
padding: 35px;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
@media (max-width: 650px) {
|
||||
.toolbar, .site-nav { flex-wrap: wrap; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header class="toolbar">
|
||||
<div>
|
||||
<strong>Stylist — Theme Preview</strong>
|
||||
<small>
|
||||
{{ $previewTheme->name() }}
|
||||
· Version {{ $previewTheme->version() }}
|
||||
· {{ ucfirst($previewTheme->type()) }}
|
||||
</small>
|
||||
</div>
|
||||
<a href="{{ route('stylist.index') }}">← Back to Stylist</a>
|
||||
</header>
|
||||
|
||||
<nav class="site-nav">
|
||||
<strong>SonarDesk</strong>
|
||||
<span>
|
||||
{{ $previewTheme->type() === 'backend'
|
||||
? 'Administration interface'
|
||||
: 'Public website' }}
|
||||
</span>
|
||||
</nav>
|
||||
|
||||
<main class="container">
|
||||
@if($previewTheme->type() === 'backend')
|
||||
<span class="eyebrow">Administration / Dashboard</span>
|
||||
<h1>Administration overview</h1>
|
||||
<p class="lead">
|
||||
Preview the administration interface using representative data.
|
||||
</p>
|
||||
|
||||
<div class="grid">
|
||||
<div class="card"><h3>128</h3><p>Licences</p></div>
|
||||
<div class="card"><h3>116</h3><p>Active licences</p></div>
|
||||
<div class="card"><h3>184</h3><p>Installations</p></div>
|
||||
<div class="card"><h3>92</h3><p>Portal users</p></div>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<h3>Release management</h3>
|
||||
<p>Manage published software releases.</p>
|
||||
<span class="button">Manage releases</span>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Theme management</h3>
|
||||
<p>Configure the appearance of SonarDesk.</p>
|
||||
<span class="button">Manage themes</span>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<span class="eyebrow">SonarDesk for macOS</span>
|
||||
<h1>Code quality at a glance.</h1>
|
||||
<p class="lead">
|
||||
A native macOS dashboard for monitoring SonarQube projects,
|
||||
quality gates, vulnerabilities and technical debt.
|
||||
</p>
|
||||
|
||||
<span class="button">Explore SonarDesk</span>
|
||||
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<h3>Quality gates</h3>
|
||||
<p>Monitor project quality and identify failing checks.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Issues</h3>
|
||||
<p>Review bugs, vulnerabilities and code smells.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Reports</h3>
|
||||
<p>Export project reports for further analysis.</p>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</main>
|
||||
|
||||
<footer class="footer">
|
||||
Theme preview only — no changes have been applied.
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,6 +1,6 @@
|
||||
@extends('layouts.app')
|
||||
@section('content')
|
||||
<section class="portal-header"><div><span class="eyebrow">Admin</span><h1>Licensing administration</h1><p class="lead compact">Manage licences, activations and customer support.</p></div></section>
|
||||
<div class="grid"><div class="card"><strong>{{ $licenseCount }}</strong><p>Licences</p></div><div class="card"><strong>{{ $activeCount }}</strong><p>Not revoked</p></div><div class="card"><strong>{{ $activationCount }}</strong><p>Active installations</p></div><div class="card"><strong>{{ $userCount }}</strong><p>Portal users</p></div></div>
|
||||
<div style="display:flex;gap:10px;flex-wrap:wrap;margin-top:18px"><a class="btn btn-primary" href="{{ route('admin.releases.index') }}">Manage releases</a><a class="btn" href="{{ route('admin.licenses') }}">Manage licences</a><a class="btn" href="{{ route('admin.users.index') }}">Manage users</a><a class="btn btn-secondary" href="{{ route('admin.cms.index') }}">Manage content</a><a class="btn btn-secondary" href="{{ route('admin.seo.edit') }}">SEO & search</a><a class="btn btn-secondary" href="{{ route('admin.support.index') }}">Support inbox</a></div>
|
||||
<div class="grid"><div class="card"><strong>{{ $licenseCount ?? '—' }}</strong><p>Licences</p></div><div class="card"><strong>{{ $activeCount ?? '—' }}</strong><p>Not revoked</p></div><div class="card"><strong>{{ $activationCount ?? '—' }}</strong><p>Active installations</p></div><div class="card"><strong>{{ $userCount ?? '—' }}</strong><p>Portal users</p></div></div>
|
||||
<div style="display:flex;gap:10px;flex-wrap:wrap;margin-top:18px"><a class="btn btn-primary" href="{{ route('admin.releases.index') }}">Manage releases</a><a class="btn" href="{{ route('admin.licenses') }}">Manage licences</a><a class="btn" href="{{ route('admin.users.index') }}">Manage users</a><a class="btn btn-secondary" href="{{ route('admin.cms.index') }}">Manage content</a><a class="btn btn-secondary" href="{{ route('stylist.index') }}">Manage themes</a><a class="btn btn-secondary" href="{{ route('admin.seo.edit') }}">SEO & search</a><a class="btn btn-secondary" href="{{ route('admin.support.index') }}">Support inbox</a></div>
|
||||
@endsection
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
@extends('layouts.app')
|
||||
@section('content')
|
||||
<section class="portal-header"><div><span class="eyebrow">Admin</span><h1>Licensing administration</h1><p class="lead compact">Manage licences, activations and customer support.</p></div></section>
|
||||
<div class="grid"><div class="card"><strong>{{ $licenseCount }}</strong><p>Licences</p></div><div class="card"><strong>{{ $activeCount }}</strong><p>Not revoked</p></div><div class="card"><strong>{{ $activationCount }}</strong><p>Active installations</p></div><div class="card"><strong>{{ $userCount }}</strong><p>Portal users</p></div></div>
|
||||
<div style="display:flex;gap:10px;flex-wrap:wrap;margin-top:18px"><a class="btn btn-primary" href="{{ route('admin.releases.index') }}">Manage releases</a><a class="btn" href="{{ route('admin.licenses') }}">Manage licences</a><a class="btn" href="{{ route('admin.users.index') }}">Manage users</a><a class="btn btn-secondary" href="{{ route('admin.cms.index') }}">Manage content</a><a class="btn btn-secondary" href="{{ route('admin.seo.edit') }}">SEO & search</a><a class="btn btn-secondary" href="{{ route('admin.support.index') }}">Support inbox</a></div>
|
||||
@endsection
|
||||
@@ -0,0 +1,6 @@
|
||||
@extends('layouts.app')
|
||||
@section('content')
|
||||
<section class="portal-header"><div><span class="eyebrow">Admin</span><h1>Licensing administration</h1><p class="lead compact">Manage licences, activations and customer support.</p></div></section>
|
||||
<div class="grid"><div class="card"><strong>{{ $licenseCount }}</strong><p>Licences</p></div><div class="card"><strong>{{ $activeCount }}</strong><p>Not revoked</p></div><div class="card"><strong>{{ $activationCount }}</strong><p>Active installations</p></div><div class="card"><strong>{{ $userCount }}</strong><p>Portal users</p></div></div>
|
||||
<div style="display:flex;gap:10px;flex-wrap:wrap;margin-top:18px"><a class="btn btn-primary" href="{{ route('admin.releases.index') }}">Manage releases</a><a class="btn" href="{{ route('admin.licenses') }}">Manage licences</a><a class="btn" href="{{ route('admin.users.index') }}">Manage users</a><a class="btn btn-secondary" href="{{ route('admin.cms.index') }}">Manage content</a><a class="btn btn-secondary" href="{{ route('stylist.index') }}">Manage themes</a><a class="btn btn-secondary" href="{{ route('admin.seo.edit') }}">SEO & search</a><a class="btn btn-secondary" href="{{ route('admin.support.index') }}">Support inbox</a></div>
|
||||
@endsection
|
||||
@@ -44,7 +44,7 @@
|
||||
<div class="center-action"><a class="btn btn-primary" href="{{ route('buy') }}">See pricing and start trial</a></div>
|
||||
</section>
|
||||
|
||||
<section class="home-download-showcase"><div class="home-download-copy"><span class="eyebrow">Desktop distribution</span><h2>Ready when you are.</h2><p>Download the latest signed SonarDesk release for macOS, review what changed, verify the package checksum and browse previous versions from one dedicated release centre.</p><div class="support-actions">@if($latestRelease)<a class="btn btn-primary" href="{{ route('download.latest') }}">Download SonarDesk {{ $latestRelease->version }}</a>@else<a class="btn btn-primary" href="{{ route('release') }}">Visit download centre</a>@endif<a class="btn btn-secondary" href="{{ route('release') }}">Release notes</a></div></div><div class="home-download-meta">@if($latestRelease)<span>Latest stable</span><strong>Version {{ $latestRelease->version }}</strong><small>Build {{ $latestRelease->build_number }} · {{ $latestRelease->formatted_size }} · macOS {{ $latestRelease->minimum_macos ?: 'supported versions' }}</small><div><b>SHA-256</b><code>{{ Str::limit($latestRelease->sha256, 34) }}</code></div>@else<span>SonarDesk releases</span><strong>Distribution centre</strong><small>Published builds will appear here automatically.</small>@endif</div></section>
|
||||
<section class="home-download-showcase"><div class="home-download-copy"><span class="eyebrow">Desktop distribution</span><h2>Ready when you are.</h2><p>Download the latest signed SonarDesk release for macOS, review what changed, verify the package checksum and browse previous versions from one dedicated release centre.</p><div class="support-actions">@if($latestRelease ?? null)<a class="btn btn-primary" href="{{ route('download.latest') }}">Download SonarDesk {{ $latestRelease->version }}</a>@else<a class="btn btn-primary" href="{{ route('release') }}">Visit download centre</a>@endif<a class="btn btn-secondary" href="{{ route('release') }}">Release notes</a></div></div><div class="home-download-meta">@if($latestRelease ?? null)<span>Latest stable</span><strong>Version {{ $latestRelease->version }}</strong><small>Build {{ $latestRelease->build_number }} · {{ $latestRelease->formatted_size }} · macOS {{ $latestRelease->minimum_macos ?: 'supported versions' }}</small><div><b>SHA-256</b><code>{{ Str::limit($latestRelease->sha256, 34) }}</code></div>@else<span>SonarDesk releases</span><strong>Distribution centre</strong><small>Published builds will appear here automatically.</small>@endif</div></section>
|
||||
|
||||
<section class="cta-panel"><div><span class="eyebrow">SonarDesk for macOS</span><h2>Make SonarQube easier to keep an eye on.</h2><p>Try SonarDesk for seven days and connect it to your existing SonarQube environment.</p></div><a class="btn btn-primary" href="{{ route('buy') }}">Start free trial</a></section>
|
||||
@endsection
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
@extends('layouts.app', [
|
||||
'title' => 'SonarDesk — Native SonarQube monitoring for macOS',
|
||||
'description' => 'SonarDesk is a native macOS companion for SonarQube: monitor quality gates, issues, measures and project health from one focused desktop workspace.'
|
||||
])
|
||||
@section('content')
|
||||
<section class="hero-card marketing-hero">
|
||||
<span class="eyebrow">SonarQube, without the browser tab sprawl</span>
|
||||
<h1>Your code quality.<br>At home on the Mac.</h1>
|
||||
<p class="lead">SonarDesk is a focused native macOS companion for developers and teams using SonarQube. See project health, quality gates, bugs, vulnerabilities, code smells and measures in one fast desktop workspace.</p>
|
||||
<div class="support-actions marketing-actions">
|
||||
<a class="btn btn-primary" href="{{ route('buy') }}">Start 7-day free trial</a>
|
||||
<a class="btn btn-secondary" href="{{ route('features') }}">Explore features</a>
|
||||
</div>
|
||||
<p class="microcopy">Direct download for macOS · Secure licence activation · Cancel anytime</p>
|
||||
</section>
|
||||
|
||||
<section class="trust-strip" aria-label="SonarDesk highlights">
|
||||
<span>Native macOS app</span><span>Multiple SonarQube servers</span><span>Keychain token storage</span><span>Issue detail workflow</span>
|
||||
</section>
|
||||
|
||||
<section class="section-block">
|
||||
<div class="section-heading"><span class="eyebrow">Built for daily engineering work</span><h2>See the signal. Open the issue. Get back to code.</h2><p>SonarDesk keeps the information you check most often close at hand, without replacing SonarQube or changing your existing workflow.</p></div>
|
||||
<div class="feature-grid">
|
||||
<article class="feature-card"><div class="feature-icon">01</div><h3>Project health at a glance</h3><p>Review configured projects, quality gate state and important measures from a clean Mac dashboard.</p></article>
|
||||
<article class="feature-card"><div class="feature-icon">02</div><h3>Issue details that are useful</h3><p>Open bugs, vulnerabilities and code smells to see the context you need before switching back to the codebase.</p></article>
|
||||
<article class="feature-card"><div class="feature-icon">03</div><h3>Focused filtering</h3><p>Work through issue lists with practical filters and server-side pagination rather than fighting a giant browser view.</p></article>
|
||||
<article class="feature-card"><div class="feature-icon">04</div><h3>Multiple servers</h3><p>Keep the SonarQube environments you work with available from one desktop application.</p></article>
|
||||
<article class="feature-card"><div class="feature-icon">05</div><h3>macOS security</h3><p>SonarQube access tokens are stored in the macOS Keychain. SonarDesk respects the permissions of the token you provide.</p></article>
|
||||
<article class="feature-card"><div class="feature-icon">06</div><h3>Native workflow</h3><p>A purpose-built macOS interface with familiar navigation, desktop behaviour and a focused experience for engineering teams.</p></article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section-block split-panel">
|
||||
<div><span class="eyebrow">Designed to complement SonarQube</span><h2>Keep SonarQube as the source of truth.</h2><p class="lead compact">SonarDesk is a desktop companion, not a replacement. It connects to your SonarQube server using your credentials and presents the project and issue information you need for everyday monitoring.</p></div>
|
||||
<div class="check-panel"><div>Quality gates and measures</div><div>Bugs, vulnerabilities and code smells</div><div>Project and issue navigation</div><div>Secure token storage in Keychain</div><div>Customer portal for licences and activations</div></div>
|
||||
</section>
|
||||
|
||||
<section class="section-block pricing-preview">
|
||||
<div class="section-heading"><span class="eyebrow">Straightforward pricing</span><h2>Start small. Upgrade when you need more Macs.</h2><p>Every plan includes the full SonarDesk application. Professional increases the activation allowance for people working across multiple Macs.</p></div>
|
||||
<div class="pricing-preview-grid">
|
||||
<article class="price-card"><span class="eyebrow">Personal</span><h3>For one Mac</h3><div class="marketing-price">£24.99 <small>/ year</small></div><p>or £2.99 monthly</p><ul class="features"><li>Full SonarDesk feature set</li><li>1 active Mac</li><li>7-day free trial</li><li>Licence key delivered by email</li><li>Customer licence portal</li></ul></article>
|
||||
<article class="price-card featured"><span class="pill">Most popular</span><span class="eyebrow">Professional</span><h3>For a multi-Mac workflow</h3><div class="marketing-price">£44.99 <small>/ year</small></div><p>or £4.99 monthly</p><ul class="features"><li>Full SonarDesk feature set</li><li>Up to 3 active Macs</li><li>7-day free trial</li><li>Licence key delivered by email</li><li>Customer licence portal</li></ul></article>
|
||||
</div>
|
||||
<div class="center-action"><a class="btn btn-primary" href="{{ route('buy') }}">See pricing and start trial</a></div>
|
||||
</section>
|
||||
|
||||
<section class="home-download-showcase"><div class="home-download-copy"><span class="eyebrow">Desktop distribution</span><h2>Ready when you are.</h2><p>Download the latest signed SonarDesk release for macOS, review what changed, verify the package checksum and browse previous versions from one dedicated release centre.</p><div class="support-actions">@if($latestRelease)<a class="btn btn-primary" href="{{ route('download.latest') }}">Download SonarDesk {{ $latestRelease->version }}</a>@else<a class="btn btn-primary" href="{{ route('release') }}">Visit download centre</a>@endif<a class="btn btn-secondary" href="{{ route('release') }}">Release notes</a></div></div><div class="home-download-meta">@if($latestRelease)<span>Latest stable</span><strong>Version {{ $latestRelease->version }}</strong><small>Build {{ $latestRelease->build_number }} · {{ $latestRelease->formatted_size }} · macOS {{ $latestRelease->minimum_macos ?: 'supported versions' }}</small><div><b>SHA-256</b><code>{{ Str::limit($latestRelease->sha256, 34) }}</code></div>@else<span>SonarDesk releases</span><strong>Distribution centre</strong><small>Published builds will appear here automatically.</small>@endif</div></section>
|
||||
|
||||
<section class="cta-panel"><div><span class="eyebrow">SonarDesk for macOS</span><h2>Make SonarQube easier to keep an eye on.</h2><p>Try SonarDesk for seven days and connect it to your existing SonarQube environment.</p></div><a class="btn btn-primary" href="{{ route('buy') }}">Start free trial</a></section>
|
||||
@endsection
|
||||
@@ -1,6 +1,6 @@
|
||||
@extends('layouts.app')
|
||||
@section('content')
|
||||
<section class="portal-header"><div><span class="eyebrow">Admin</span><h1>Licensing administration</h1><p class="lead compact">Manage licences, activations and customer support.</p></div></section>
|
||||
<div class="grid"><div class="card"><strong>{{ $licenseCount }}</strong><p>Licences</p></div><div class="card"><strong>{{ $activeCount }}</strong><p>Not revoked</p></div><div class="card"><strong>{{ $activationCount }}</strong><p>Active installations</p></div><div class="card"><strong>{{ $userCount }}</strong><p>Portal users</p></div></div>
|
||||
<div class="grid"><div class="card"><strong>{{ $licenseCount ?? '—' }}</strong><p>Licences</p></div><div class="card"><strong>{{ $activeCount ?? '—' }}</strong><p>Not revoked</p></div><div class="card"><strong>{{ $activationCount ?? '—' }}</strong><p>Active installations</p></div><div class="card"><strong>{{ $userCount ?? '—' }}</strong><p>Portal users</p></div></div>
|
||||
<div style="display:flex;gap:10px;flex-wrap:wrap;margin-top:18px"><a class="btn btn-primary" href="{{ route('admin.releases.index') }}">Manage releases</a><a class="btn" href="{{ route('admin.licenses') }}">Manage licences</a><a class="btn" href="{{ route('admin.users.index') }}">Manage users</a><a class="btn btn-secondary" href="{{ route('admin.cms.index') }}">Manage content</a><a class="btn btn-secondary" href="{{ route('stylist.index') }}">Manage themes</a><a class="btn btn-secondary" href="{{ route('admin.seo.edit') }}">SEO & search</a><a class="btn btn-secondary" href="{{ route('admin.support.index') }}">Support inbox</a></div>
|
||||
@endsection
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
@extends('layouts.app')
|
||||
@section('content')
|
||||
<section class="portal-header"><div><span class="eyebrow">Admin</span><h1>Licensing administration</h1><p class="lead compact">Manage licences, activations and customer support.</p></div></section>
|
||||
<div class="grid"><div class="card"><strong>{{ $licenseCount }}</strong><p>Licences</p></div><div class="card"><strong>{{ $activeCount }}</strong><p>Not revoked</p></div><div class="card"><strong>{{ $activationCount }}</strong><p>Active installations</p></div><div class="card"><strong>{{ $userCount }}</strong><p>Portal users</p></div></div>
|
||||
<div style="display:flex;gap:10px;flex-wrap:wrap;margin-top:18px"><a class="btn btn-primary" href="{{ route('admin.releases.index') }}">Manage releases</a><a class="btn" href="{{ route('admin.licenses') }}">Manage licences</a><a class="btn" href="{{ route('admin.users.index') }}">Manage users</a><a class="btn btn-secondary" href="{{ route('admin.cms.index') }}">Manage content</a><a class="btn btn-secondary" href="{{ route('stylist.index') }}">Manage themes</a><a class="btn btn-secondary" href="{{ route('admin.seo.edit') }}">SEO & search</a><a class="btn btn-secondary" href="{{ route('admin.support.index') }}">Support inbox</a></div>
|
||||
@endsection
|
||||
Reference in New Issue
Block a user