Files
XC_VM/docs/en/development/navbar-rendering.md
T
Divarion_D c982bb2f1e docs: audit dev docs for source drift, split oversized pages, add core-wiring
Verify every dev-doc claim against src/ and fix factual drift: wrong method
signatures/return types, wrong enum casing (BootContext cases are PascalCase),
stale paths (M3u parsers are Composer deps under vendor/, MobileDetect is
mobiledetect/mobiledetectlib v4.9.0 \Detection\MobileDetect, NotFoundException
lives in XcVm\Core\Container\Psr), a fictional `stream:check` command/class,
reversed migration-failure semantics ([FAIL] = not recorded, retried),
inverted isStreamRunning/isStreamAlive descriptions, findProcessPIDs ANY-not-ALL,
acquireCronLock has no shutdown callback, and nonexistent make targets.

Split oversized pages and fix nav + cross-links:
- modules.md -> module-authoring / module-lifecycle / module-extension-points
- cli-tools.md -> cli-tools + database-migrations
- streaming-subsystem.md -> + streaming-diagnostics
- geoip-and-device-detection.md -> geoip-isp-and-geo-routing + device-detection-and-stb-locking

Add development/core-wiring.md: how the core assembles itself at boot
(container population, ServiceContainer reference, bootAll orchestration,
CLI command auto-discovery, end-to-end Admin/CLI boot walkthroughs).

Only docs/en + mkdocs.yml touched; docs/ru is regenerated before release.
2026-08-26 22:42:43 +03:00

4.7 KiB

Navbar rendering in module panel

Technical documentation for building and rendering the navbar in the admin panel.

Purpose

The navbar is built declaratively from a NavbarItem tree, not from hardcoded HTML menus.

Tree sources:

  1. Core nodes from CoreNavbarProvider::register().
  2. Module nodes from ModuleInterface::registerNavbar().

Lifecycle

  1. ModuleLoader::bootAll() calls CoreNavbarProvider::register().
  2. Then registerNavbar() is called for each loaded module.
  3. In Public/Views/admin/header.php, the tree is rendered from NavbarRegistry.

Rendering in header

Rendering is done by helper functions:

  1. _xc_nav_visible() - visibility filtering for a node.
  2. _xc_nav_label() - text label resolution.
  3. _xc_nav_children() - recursive rendering of child items.

Top-level nodes come from NavbarRegistry::getTopLevel(), child nodes come from NavbarRegistry::getChildren($key).

Visibility rules

Checks are performed in _xc_nav_visible():

  1. desktopOnly: hides node on mobile.
  2. settingDisabled: hides node when a setting flag is enabled.
  3. permissions: OR-check via Authorization::check('adv', $permission).
  4. A group with url='#' is shown only if at least one child is visible.
  5. divider is always passed through and rendered as separator.

Rendering specifics

  1. divider renders as separator without a link.
  2. submenuClass('megamenu') enables two-column rendering for long lists.
  3. noMobileSubmenu disables child submenu expansion on mobile.

How a module adds a menu item

A module adds items only through registerNavbar():

public function registerNavbar(NavbarRegistry $registry): void {
    NavbarRegistry::add((new NavbarItem('management.service_setup.my_module'))
        ->parent('management.service_setup')
        ->url('my_module')
        ->label('my_module')
        ->permissions(['my_module'])
        ->order(60));

    NavbarRegistry::add((new NavbarItem('management.logs.my_module_log'))
        ->parent('management.logs')
        ->url('my_module_logs')
        ->label('', 'My Module Logs')
        ->permissions(['my_module'])
        ->order(170));
}

NavbarItem builder API

NavbarItem is a fluent value object (src/Core/Module/NavbarItem.php) — chain setters off new NavbarItem($key):

Method Purpose
new NavbarItem($key) create a node; $key is its unique section.group.item id
->parent($parentKey) attach under an existing node (omit for a top-level node)
->url($url) target path; '#' makes it a non-navigating group header
->label($key, $fallback = '') translation key, or ('', 'Literal') for fixed text
->icon($icon) icon CSS class for the item
->permissions([...]) OR-list of permission keys; node hidden unless the viewer has one
->order($n) sort position within the parent
->desktopOnly() hide on mobile
->noMobileSubmenu() don't expand this node's submenu on mobile
->submenuClass('megamenu') two-column rendering for long child lists
->settingDisabled($settingKey) hide the node when that panel setting flag is truthy
->makeDivider() render this node as a separator (no link)

Group node and divider

public function registerNavbar(NavbarRegistry $registry): void {
    // A group header (url('#')) — shown only if at least one child is visible
    NavbarRegistry::add((new NavbarItem('management.my_group'))
        ->parent('management')
        ->url('#')
        ->label('my_group')
        ->order(50));

    // A divider inside that group
    NavbarRegistry::add((new NavbarItem('management.my_group.sep1'))
        ->parent('management.my_group')
        ->makeDivider()
        ->order(55));
}

settingDisabled('some_setting') hides the node whenever that setting is truthy (gate a feature behind a toggle). Visibility is also viewer-scoped: the permissions OR-check runs against the current user via Authorization::check('adv', …), so an admin and a reseller can see different subsets of the same tree.

Practical rules for modules

  1. Use unique key values in section.group.item format.
  2. Set parent to an existing core tree node or your own already-added node.
  3. Position items using order inside one parent.
  4. Use label('translation_key') for translatable text.
  5. Use label('', 'Literal Text') for fixed literal text.
  6. If the module has no menu items, keep registerNavbar() empty.
File Role
src/Core/Module/NavbarRegistry.php Collects navbar items from providers
src/Core/Module/NavbarItem.php Navbar item value object
src/Core/Module/CoreNavbarProvider.php Built-in core menu items
src/Public/Views/admin/header.php Renders the navbar tree