Внешний вид

Layout

Скринкасты

Основы

Layout в MoonShine представляет собой набор компонентов, формирующих структуру страницы административной панели. Каждый элемент страницы, включая HTML теги, является компонентом MoonShine. Это обеспечивает высокую степень гибкости и возможность кастомизации.

При установке MoonShine, публикуется шаблон по умолчанию app/MoonShine/Layouts/AppLayout.php и регистрируется в конфигурационном файле.

Вы можете:

  • Модифицировать существующий шаблон,
  • Создать новый шаблон,
  • Применять разные шаблоны для различных страниц.

Полный список компонентов ищите в разделе Компоненты.

Как можно заметить, компонентов огромное количество, и для удобства мы объединили их в группы, чтобы вы могли удобно переопределять только те группы, которые требуются.

 namespaces
use MoonShine\Laravel\Layouts\AppLayout;
 
final class MoonShineLayout extends AppLayout
{
// ...
 
protected function getFooterMenu(): array
{
return [
'https://example.com' => 'Custom link',
];
}
 
protected function getFooterCopyright(): string
{
return 'MoonShine';
}
 
public function build(): Layout
{
return parent::build();
}
}
 namespaces
use MoonShine\Laravel\Layouts\AppLayout;
 
final class MoonShineLayout extends AppLayout
{
// ...
 
protected function getFooterMenu(): array
{
return [
'https://example.com' => 'Custom link',
];
}
 
protected function getFooterCopyright(): string
{
return 'MoonShine';
}
 
public function build(): Layout
{
return parent::build();
}
}

В примере выше, с помощью методов getFooterMenu() и getFooterCopyright(), мы переопределили вывод меню в футере и copyright.

Доступные быстрые методы:

Переопределить компонент Head

protected function getHeadComponent(bool $withAssetsFragment = true): Head
{
return Head::make([
// ...
]);
}
protected function getHeadComponent(bool $withAssetsFragment = true): Head
{
return Head::make([
// ...
]);
}

Переопределить компонент Logo

protected function getLogoComponent(): Logo
{
return Logo::make(
$this->getHomeUrl(),
$this->getLogo(),
$this->getLogo(small: true),
);
}
protected function getLogoComponent(): Logo
{
return Logo::make(
$this->getHomeUrl(),
$this->getLogo(),
$this->getLogo(small: true),
);
}

Переопределить компонент Sidebar

protected function getSidebarComponent(): Sidebar
{
return Sidebar::make([
// ...
]);
}
protected function getSidebarComponent(): Sidebar
{
return Sidebar::make([
// ...
]);
}

Переопределить компонент Header

protected function getHeaderComponent(): Header
{
Header::make([
// ...
]);
}
protected function getHeaderComponent(): Header
{
Header::make([
// ...
]);
}

Переопределить или интегрировать компонент TopBar

protected function getTopBarComponent(): Topbar
{
Topbar::make([
// ...
]);
}
protected function getTopBarComponent(): Topbar
{
Topbar::make([
// ...
]);
}

Переопределить компонент Footer

protected function getFooterComponent(): Footer
{
Footer::make([
// ...
]);
}
protected function getFooterComponent(): Footer
{
Footer::make([
// ...
]);
}

Переопределить компонент Profile

protected function getProfileComponent(): Profile
{
return Profile::make();
}
protected function getProfileComponent(): Profile
{
return Profile::make();
}

Переопределить содержимое компонента Content

protected function getContentComponents(): array
{
// ...
}
protected function getContentComponents(): array
{
// ...
}
Content::make(
$this->getContentComponents()
)
Content::make(
$this->getContentComponents()
)

Путь до логотипа

protected function getLogo(bool $small = false): string
{
// ...
}
protected function getLogo(bool $small = false): string
{
// ...
}

URL главной страницы

protected function getHomeUrl(): string
{
// ...
}
protected function getHomeUrl(): string
{
// ...
}

Упрощенное отображение контента

Вы можете убрать обводку и фон у контентной части страницы, установив свойство $contentSimpled = true в вашем Layout:

protected bool $contentSimpled = true; // default false
protected bool $contentSimpled = true; // default false

Отображение контента по центру

По умолчанию контент страницы занимает всю ширину экрана. Чтобы разместить его в центрированном контейнере фиксированной ширины, установите свойство $contentCentered = true в вашем Layout.

protected bool $contentCentered = true; // default false
protected bool $contentCentered = true; // default false

Slots

С помощью "слотов" вы можете быстро добавить компоненты в Sidebar или Topbar.

protected function sidebarSlot(): array
{
return [
Search::make()->enabled(),
// ...
];
}
 
protected function sidebarTopSlot(): array
{
return [
Notifications::make(),
// ...
];
}
 
protected function topBarSlot(): array
{
return [
// ...
];
}
protected function sidebarSlot(): array
{
return [
Search::make()->enabled(),
// ...
];
}
 
protected function sidebarTopSlot(): array
{
return [
Notifications::make(),
// ...
];
}
 
protected function topBarSlot(): array
{
return [
// ...
];
}

Вы также можете создать собственный шаблон со своим набором удобных методов для дальнейшего удобного взаимодействия.

В стандартном Layout компоненты Sidebar, Topbar и Mobilebar оформлены в темных цветах. Но если добавить в них другие компоненты, они будут меняться в зависимости от выбранной темы, что приведёт к некорректному их отображению. Чтобы избежать такого поведения, можно принудительно перевести их в тёмный режим добавив класс 'dark'.

$this->getSidebarComponent()->class('dark'),
 
$this->getTopBarComponent()->class('dark'),
 
MobileBar::make([
// ...
])->class('dark'),
$this->getSidebarComponent()->class('dark'),
 
$this->getTopBarComponent()->class('dark'),
 
MobileBar::make([
// ...
])->class('dark'),

Управление компонентами

MoonShine предоставляет удобный способ управления отображением основных компонентов шаблона через защищённые свойства класса. Это позволяет быстро включать или отключать элементы интерфейса без переопределения метода build().

По умолчанию боковое меню включено. Чтобы отключить его, установите свойство $sidebar = false в вашем шаблоне:

 namespaces
use MoonShine\Laravel\Layouts\AppLayout;
 
final class MoonShineLayout extends AppLayout
{
protected bool $sidebar = false;
 
// ...
}
 namespaces
use MoonShine\Laravel\Layouts\AppLayout;
 
final class MoonShineLayout extends AppLayout
{
protected bool $sidebar = false;
 
// ...
}

TopBar

Верхняя панель по умолчанию отключена. Чтобы включить её, установите свойство $topBar = true:

 namespaces
use MoonShine\Laravel\Layouts\AppLayout;
 
final class MoonShineLayout extends AppLayout
{
protected bool $topBar = true;
 
// ...
}
 namespaces
use MoonShine\Laravel\Layouts\AppLayout;
 
final class MoonShineLayout extends AppLayout
{
protected bool $topBar = true;
 
// ...
}

Если вы используете и Sidebar и TopBar одновременно, обязательно соблюдайте очередность в методе build() — первым должен идти TopBar.

BottomBar

Нижняя панель по умолчанию отключена. Чтобы включить её, установите свойство $bottomBar = true:

 namespaces
use MoonShine\Laravel\Layouts\AppLayout;
 
final class MoonShineLayout extends AppLayout
{
protected bool $bottomBar = true;
 
// ...
}
 namespaces
use MoonShine\Laravel\Layouts\AppLayout;
 
final class MoonShineLayout extends AppLayout
{
protected bool $bottomBar = true;
 
// ...
}

Компонент BottomBar автоматически отображает верхнее меню внутри себя.

Мобильный режим

Вы можете включить специальный мобильный режим, который оптимизирует интерфейс для мобильных устройств. При включении этого режима автоматически применяются компактные стили и активируется компонент BottomBar:

 namespaces
use MoonShine\Laravel\Layouts\AppLayout;
 
final class MoonShineLayout extends AppLayout
{
protected bool $mobileMode = true;
 
// ...
}
 namespaces
use MoonShine\Laravel\Layouts\AppLayout;
 
final class MoonShineLayout extends AppLayout
{
protected bool $mobileMode = true;
 
// ...
}

При включении мобильного режима:

  • применяются компактные отступы и размеры элементов,
  • скрывается компонент Burger,
  • автоматически включается BottomBar с верхним меню.

Вы можете комбинировать эти свойства для создания различных вариантов шаблонов — например, отключить Sidebar и включить только TopBar для более горизонтального интерфейса.

Создание шаблона

Чтобы создать еще один шаблон, воспользуйтесь командой:

php artisan moonshine:layout
php artisan moonshine:layout

О всех поддерживаемых опциях можно узнать в разделе Команды.

Изменение шаблона страницы

По умолчанию страницы используют шаблон отображения AppLayout. Но вы можете изменить его на собственный шаблон, просто заменив значение свойства $layout.

Подробнее про страницы читайте в разделе Страница.

 namespaces
use App\MoonShine\Layouts\MyLayout;
use MoonShine\Laravel\Pages\Page;
 
class CustomPage extends Page
{
protected ?string $layout = MyLayout::class;
 
// ...
}
 namespaces
use App\MoonShine\Layouts\MyLayout;
use MoonShine\Laravel\Pages\Page;
 
class CustomPage extends Page
{
protected ?string $layout = MyLayout::class;
 
// ...
}

Assets

Каждый шаблон может иметь свой набор стилей и скриптов, определяемых через метод assets().

 namespaces
use MoonShine\Laravel\Layouts\AppLayout;
use MoonShine\AssetManager\Css;
 
final class MyLayout extends AppLayout
{
// ...
 
protected function assets(): array
{
return [
...parent::assets(),
 
Css::make('/vendor/moonshine/assets/minimalistic.css')->defer(),
];
}
 
// ...
}
 namespaces
use MoonShine\Laravel\Layouts\AppLayout;
use MoonShine\AssetManager\Css;
 
final class MyLayout extends AppLayout
{
// ...
 
protected function assets(): array
{
return [
...parent::assets(),
 
Css::make('/vendor/moonshine/assets/minimalistic.css')->defer(),
];
}
 
// ...
}

За более подробной информацией обратитесь в раздел Assets.

Добавление инлайн-стилей

protected function assets(): array
{
return [
...parent::assets(),
InlineCss::make(<<<'Style'
:root {
--spacing: 0.15rem;
}
Style),
];
}
protected function assets(): array
{
return [
...parent::assets(),
InlineCss::make(<<<'Style'
:root {
--spacing: 0.15rem;
}
Style),
];
}

Favicons

Вы можете заменить набор favicons в шаблоне через переопределение метода getFaviconComponent().

 namespaces
use MoonShine\Laravel\Layouts\AppLayout;
 
final class MyLayout extends AppLayout
{
// ...
 
protected function getFaviconComponent(): Favicon
{
return parent::getFaviconComponent()->customAssets([
'apple-touch' => 'favicon_path',
'32' => 'favicon_path',
'16' => 'favicon_path',
'safari-pinned-tab' => 'favicon_path',
]);
}
}
 namespaces
use MoonShine\Laravel\Layouts\AppLayout;
 
final class MyLayout extends AppLayout
{
// ...
 
protected function getFaviconComponent(): Favicon
{
return parent::getFaviconComponent()->customAssets([
'apple-touch' => 'favicon_path',
'32' => 'favicon_path',
'16' => 'favicon_path',
'safari-pinned-tab' => 'favicon_path',
]);
}
}

Для каждого шаблона можно объявить список пунктов меню через метод menu(), которые автоматически будут переданы в компонент Menu.

 namespaces
use MoonShine\Laravel\Layouts\AppLayout;
use MoonShine\MenuManager\MenuItem;
 
final class MyLayout extends AppLayout
{
// ...
 
protected function menu(): array
{
return [
...parent::menu(),
MenuItem::make(ArticleResource::class),
];
}
}
 namespaces
use MoonShine\Laravel\Layouts\AppLayout;
use MoonShine\MenuManager\MenuItem;
 
final class MyLayout extends AppLayout
{
// ...
 
protected function menu(): array
{
return [
...parent::menu(),
MenuItem::make(ArticleResource::class),
];
}
}

За более подробной информацией обратитесь в раздел Меню.

Вы также можете не пользоваться методом menu(), а передать список вручную в компонент Menu.

Верхнее меню

По умолчанию MoonShine имеет компонент верхнего меню, который можно использовать вместо Sidebar или совместно с ним.

Чтобы заменить Sidebar на TopBar, переопределите метод build() и заменит в нём вызов метода getSidebarComponent() на getTopBarComponent().

Если вы хотите оставить и Sidebar и TopBar одновременно, то обязательно соблюдайте очередность, первым должен идти TopBar.

Темы оформления

В Moonshine "из коробки" доступна поддержка двух тем оформления — светлой и тёмной. По умолчанию используется тема, заданная в системе, либо светлая, если определить не удалось.

Тёмная тема

Если вы хотите, чтобы тёмная тема всегда была включена, переопределите метод isAlwaysDark() и верните true. Переключатель тем при этом отображаться не будет.

protected function isAlwaysDark(): bool
{
return true;
}
protected function isAlwaysDark(): bool
{
return true;
}

Вкл/выкл тем оформления

Чтобы убрать переключатель тем и оставить только светлую тему, переопределите метод hasThemes() и верните false.

protected function hasThemes(): bool
{
return false;
}
protected function hasThemes(): bool
{
return false;
}

Цвета

Каждый шаблон может иметь собственную цветовую схему. Самый простой способ задать её — указать реализацию PaletteContract в свойстве $palette:

 namespaces
use App\MoonShine\Palettes\CorporatePalette;
use MoonShine\Laravel\Layouts\AppLayout;
 
final class MyLayout extends AppLayout
{
protected ?string $palette = CorporatePalette::class;
}
 namespaces
use App\MoonShine\Palettes\CorporatePalette;
use MoonShine\Laravel\Layouts\AppLayout;
 
final class MyLayout extends AppLayout
{
protected ?string $palette = CorporatePalette::class;
}

За более подробной информацией обратитесь в раздел Палитры.

Если оставить $palette равным null, будет использоваться значение из config('moonshine.palette').

Если требуется полное управление, переопределите метод colors():

 namespaces
use MoonShine\Laravel\Layouts\AppLayout;
use MoonShine\Contracts\ColorManager\ColorManagerContract;
 
final class MyLayout extends AppLayout
{
protected function colors(ColorManagerContract $colorManager): void
{
$colorManager
->primary('oklch(65% 0.18 264)')
->secondary('oklch(70% 0.14 230)')
->bulkAssign([
'theme' => [
'body' => '0 0 0',
50 => '0.99 0 0',
100 => '0.98 0 0',
900 => '0.90 0 0',
],
])
->successBg('oklch(63.9% 0.218 142.495)')
->warningBg('oklch(80.88% 0.170358 75.3501)')
->errorBg('oklch(58.9% 0.214 26.855)')
->infoBg('oklch(60.1% 0.219 257.63)');
 
$colorManager
->set('body', '0.2 0.0168 274.32', dark: true)
->theme([
'body' => '1 0 0',
'stroke' => '1 0 0 / 10%',
'default' => '0.24 0.0168 274.32',
900 => '0.39 0.025 274.32',
], dark: true)
->successBg('0.639 0.218 142.495', dark: true)
->warningBg('0.898 0.177 96.726', dark: true)
->errorBg('0.589 0.214 26.855', dark: true)
->infoBg('0.601 0.219 257.63', dark: true);
}
}
 namespaces
use MoonShine\Laravel\Layouts\AppLayout;
use MoonShine\Contracts\ColorManager\ColorManagerContract;
 
final class MyLayout extends AppLayout
{
protected function colors(ColorManagerContract $colorManager): void
{
$colorManager
->primary('oklch(65% 0.18 264)')
->secondary('oklch(70% 0.14 230)')
->bulkAssign([
'theme' => [
'body' => '0 0 0',
50 => '0.99 0 0',
100 => '0.98 0 0',
900 => '0.90 0 0',
],
])
->successBg('oklch(63.9% 0.218 142.495)')
->warningBg('oklch(80.88% 0.170358 75.3501)')
->errorBg('oklch(58.9% 0.214 26.855)')
->infoBg('oklch(60.1% 0.219 257.63)');
 
$colorManager
->set('body', '0.2 0.0168 274.32', dark: true)
->theme([
'body' => '1 0 0',
'stroke' => '1 0 0 / 10%',
'default' => '0.24 0.0168 274.32',
900 => '0.39 0.025 274.32',
], dark: true)
->successBg('0.639 0.218 142.495', dark: true)
->warningBg('0.898 0.177 96.726', dark: true)
->errorBg('0.589 0.214 26.855', dark: true)
->infoBg('0.601 0.219 257.63', dark: true);
}
}

За более подробной информацией обратитесь в раздел Цветовая схема.

Fragments

По умолчанию в базовом шаблоне отдельные его части находятся внутри компонентов Fragment. За счёт этого вы можете обновлять их без перезагрузки страницы с помощью событий JSEvents.

Доступные фрагменты:

  • sidebar-top - Верхняя часть бокового меню (логотип и переключатель темы оформления),
  • sidebar-content - Контентная часть бокового меню,
  • topbar-logo - Логотип в верхнем меню,
  • topbar-menu - Пункты верхнего меню,
  • topbar-actions - Блок действий в верхнем меню,
  • assets - Набор стилей и скриптов.

Пример обновления всех фрагментов шаблона из метода контроллера:

public function saveElement(CrudRequestContract $request): JsonResponse
{
//...
return JsonResponse::make()->events([
AlpineJs::event(JsEvent::FRAGMENT_UPDATED, 'sidebar-top'),
AlpineJs::event(JsEvent::FRAGMENT_UPDATED, 'sidebar-content'),
AlpineJs::event(JsEvent::FRAGMENT_UPDATED, 'topbar-logo'),
AlpineJs::event(JsEvent::FRAGMENT_UPDATED, 'topbar-menu'),
AlpineJs::event(JsEvent::FRAGMENT_UPDATED, 'topbar-actions'),
AlpineJs::event(JsEvent::FRAGMENT_UPDATED, 'assets'),
]);
}
public function saveElement(CrudRequestContract $request): JsonResponse
{
//...
return JsonResponse::make()->events([
AlpineJs::event(JsEvent::FRAGMENT_UPDATED, 'sidebar-top'),
AlpineJs::event(JsEvent::FRAGMENT_UPDATED, 'sidebar-content'),
AlpineJs::event(JsEvent::FRAGMENT_UPDATED, 'topbar-logo'),
AlpineJs::event(JsEvent::FRAGMENT_UPDATED, 'topbar-menu'),
AlpineJs::event(JsEvent::FRAGMENT_UPDATED, 'topbar-actions'),
AlpineJs::event(JsEvent::FRAGMENT_UPDATED, 'assets'),
]);
}

Blade

MoonShine позволяет создавать шаблоны напрямую через Blade.

Пример базового шаблона:

<x-moonshine::layout>
<x-moonshine::layout.html :with-alpine-js="true" :with-themes="true">
<x-moonshine::layout.head>
<x-moonshine::layout.meta name="csrf-token" :content="csrf_token()"/>
<x-moonshine::layout.favicon />
<x-moonshine::layout.assets>
@vite([
'resources/css/main.css',
'resources/js/app.js',
], 'vendor/moonshine')
</x-moonshine::layout.assets>
</x-moonshine::layout.head>
<x-moonshine::layout.body>
<x-moonshine::layout.wrapper>
<x-moonshine::layout.sidebar :collapsed="true">
<x-moonshine::layout.div class="menu-header">
<x-moonshine::layout.div class="menu-logo">
<x-moonshine::layout.logo href="/" logo="/tableau.png" logo-small="/tableau.png" :minimized="true"/>
</x-moonshine::layout.div>
 
<x-moonshine::layout.div class="menu-actions">
<x-moonshine::layout.theme-switcher/>
</x-moonshine::layout.div>
 
<x-moonshine::layout.div class="menu-burger">
<x-moonshine::layout.burger/>
</x-moonshine::layout.div>
</x-moonshine::layout.div>
 
<x-moonshine::layout.div class="menu menu--vertical">
<x-moonshine::layout.menu :elements="[['label' => 'Dashboard', 'url' => '/'], ['label' => 'Section', 'url' => '/section']]"/>
</x-moonshine::layout.div>
</x-moonshine::layout.sidebar>
 
<x-moonshine::layout.div class="layout-main">
<x-moonshine::layout.div class="layout-page">
<x-moonshine::layout.header>
<x-moonshine::layout.div class="menu-burger">
<x-moonshine::layout.burger/>
</x-moonshine::layout.div>
<x-moonshine::breadcrumbs :items="['#' => 'Home']"/>
<x-moonshine::layout.search placeholder="Search" />
<x-moonshine::layout.locales :locales="collect()"/>
</x-moonshine::layout.header>
<x-moonshine::layout.content>
<article class="article">
Your content
</article>
</x-moonshine::layout.content>
</x-moonshine::layout.div>
</x-moonshine::layout.div>
 
</x-moonshine::layout.wrapper>
</x-moonshine::layout.body>
</x-moonshine::layout.html>
</x-moonshine::layout>
<x-moonshine::layout>
<x-moonshine::layout.html :with-alpine-js="true" :with-themes="true">
<x-moonshine::layout.head>
<x-moonshine::layout.meta name="csrf-token" :content="csrf_token()"/>
<x-moonshine::layout.favicon />
<x-moonshine::layout.assets>
@vite([
'resources/css/main.css',
'resources/js/app.js',
], 'vendor/moonshine')
</x-moonshine::layout.assets>
</x-moonshine::layout.head>
<x-moonshine::layout.body>
<x-moonshine::layout.wrapper>
<x-moonshine::layout.sidebar :collapsed="true">
<x-moonshine::layout.div class="menu-header">
<x-moonshine::layout.div class="menu-logo">
<x-moonshine::layout.logo href="/" logo="/tableau.png" logo-small="/tableau.png" :minimized="true"/>
</x-moonshine::layout.div>
 
<x-moonshine::layout.div class="menu-actions">
<x-moonshine::layout.theme-switcher/>
</x-moonshine::layout.div>
 
<x-moonshine::layout.div class="menu-burger">
<x-moonshine::layout.burger/>
</x-moonshine::layout.div>
</x-moonshine::layout.div>
 
<x-moonshine::layout.div class="menu menu--vertical">
<x-moonshine::layout.menu :elements="[['label' => 'Dashboard', 'url' => '/'], ['label' => 'Section', 'url' => '/section']]"/>
</x-moonshine::layout.div>
</x-moonshine::layout.sidebar>
 
<x-moonshine::layout.div class="layout-main">
<x-moonshine::layout.div class="layout-page">
<x-moonshine::layout.header>
<x-moonshine::layout.div class="menu-burger">
<x-moonshine::layout.burger/>
</x-moonshine::layout.div>
<x-moonshine::breadcrumbs :items="['#' => 'Home']"/>
<x-moonshine::layout.search placeholder="Search" />
<x-moonshine::layout.locales :locales="collect()"/>
</x-moonshine::layout.header>
<x-moonshine::layout.content>
<article class="article">
Your content
</article>
</x-moonshine::layout.content>
</x-moonshine::layout.div>
</x-moonshine::layout.div>
 
</x-moonshine::layout.wrapper>
</x-moonshine::layout.body>
</x-moonshine::layout.html>
</x-moonshine::layout>