Skip to main content

Page Layouts

Hub GUI provides three page layout classes. Each layout defines the outer shell of an application page — header, footer, side menu, tab content area, and built-in primitives like skeleton loader, system state alerts, and release notes.

Package: io-oidis-hub-gui

Layout Variants​

LayoutHeaderSide MenuMobile MenuFooterUse Case
BasePageLayout❌❌❌❌Full-screen apps, kiosk mode
StickyHeaderLayout✅ Sticky❌❌✅Header navigation with buttons/tabs
MenuPageLayout✅ Sticky✅ Collapsible✅ Auto✅Admin dashboards, multi-section apps

All three layouts support hash-based tab routing (/#/path), lazy loading, skeleton loader, system state alerts, release notes, backdoor login, and print section.

BasePageLayout​

The abstract base for all layouts. Provides the core tab system, routing, and primitives — but no header, footer, or menu chrome. Extend this directly for full-screen or kiosk-style UIs where you want complete control over the layout.

import { BasePageLayout } from "@io-oidis-hub-gui/Io/Oidis/Hub/Gui/Primitives/BasePageLayout.js";

export class KioskPage extends BasePageLayout {
// Full-screen layout — tabs fill the entire viewport
}

Key properties (available on all layouts):

PropertyTypeDescription
skeletonLoaderSkeletonLoaderLoading shimmer placeholder
tabsContentGuiCommonsTab content container
headerStripGuiCommonsEnvironment warning bar (dev/eap/prod)
systemStateSystemStateNotificationOffline/online/reload alerts
releaseNotesDialogReleaseNotesDialogVersion release notes offcanvas
notificationsDialogNotificationsDialogNotifications offcanvas
backdoorLoginDialogBackdoorLoginDialogAdmin impersonation dialog
panelsContainerGuiCommonsPanels wrapper
printSectionPrintContainerHidden area for Puppeteer print rendering
toastsGuiCommonsToast notification container (fixed bottom-right)

StickyHeaderLayout​

Adds a sticky header bar (z-index: 100, customizable height) and a footer. Tab navigation is typically handled via buttons in the header — there is no side menu.

import { StickyHeaderLayout } from "@io-oidis-hub-gui/Io/Oidis/Hub/Gui/Primitives/StickyHeaderLayout.js";

export class MainPage extends StickyHeaderLayout {
public declare readonly headerPanel : HeaderPanel;
public declare readonly footerPanel : FooterPanel;

constructor() {
super();
this.headerPanel = new HeaderPanel();
this.footerPanel = new FooterPanel();
this.headerHeight = 117; // custom height (default: 70px)
}
}

Additional properties:

PropertyTypeDefaultDescription
headerPanelHeaderPanel—Sticky header bar (search, buttons, user profile)
footerPanelFooterPanel—Footer with version/build info
pageContentGuiCommons—Main content area (responsive column)
scrollBodyGuiCommons—Scrollable body wrapper
headerHeightnumber70Header height in pixels

The controller typically overrides selectTab() to mark active buttons in the header, since there is no menu panel with auto-highlighting.

Adds a collapsible side menu with automatic mobile support. On desktop, the menu is a persistent sidebar. On mobile (< 768px), the menu collapses to a hamburger toggle that auto-closes when an item is clicked.

import { MenuPageLayout } from "@io-oidis-hub-gui/Io/Oidis/Hub/Gui/Primitives/MenuPageLayout.js";

export class AdminPage extends MenuPageLayout {
public declare readonly headerPanel : HeaderPanel;
public declare readonly footerPanel : FooterPanel;
public declare readonly menuPanel : MenuPanel;

constructor() {
super();
this.headerPanel = new HeaderPanel();
this.footerPanel = new FooterPanel();
}
}

Additional properties:

PropertyTypeDescription
headerPanelHeaderPanelSticky header with menu toggle button
footerPanelFooterPanelFooter
menuContentGuiCommonsSide menu container (toggles "Show" class)
menuPanelMenuPanelSide menu with categorized items
pageContentGuiCommonsMain content area
scrollBodyGuiCommonsScrollable wrapper

Methods:

MethodDescription
ShowMenu($status)Toggle menu visibility (adds/removes "Show"/"Close" CSS classes)
withMobileMenu()Returns true if menu toggle is visible (i.e., viewport is mobile-sized)

Menu state is persisted in localStorage via persistence.Variable("MenuShow"). The controller restores the user's preferred state on page load (desktop only — mobile always starts closed).

Tab Configuration​

Tabs are defined in getTabsConfiguration() on the Page class. The return type is formally ITabDescriptor[], but in practice it is written as an object map where keys become tab IDs:

protected getTabsConfiguration() : ITabDescriptor[] {
return <any>{
home: {
content: HomePanel,
controller: HomePanelController,
isDefault: true
},
groups: {
content: GroupsPanel,
controller: GroupsPanelController,
autoReload: true,
scope: MenuItemScope.ADMINS
},
reports: {
content: ReportsPanel,
controller: ReportsPanelController
},
faq: {
name: "Otázky a odpovědi",
scope: MenuItemScope.MOBILE_ONLY,
icon: `<svg>...</svg>`,
content: FAQPanel,
controller: FAQPanelController
},
myProfile: {
scope: MenuItemScope.HIDDEN,
content: MyProfilePanel,
controller: MyProfilePanelController
}
};
}

ITabDescriptor​

PropertyTypeDefaultDescription
contentclass / instance—Panel class or instance
controllerclass / instance—PanelController class
namestring"Tab N"Display name in menu
idstringobject keyUnique tab ID
linkstring"/#/id"Hash route (auto-generated from ID)
altLinksstring[]—Alternative routes matching this tab
iconstring—SVG/HTML icon for menu item
scopeMenuItemScopeFOR_ALLVisibility control
isDefaultbooleanfalseActive tab when no route matches
autoReloadbooleanfalseRe-fetch data on every tab click
reloadOnRestartbooleanfalseRe-fetch data on server reconnect
lazyLoadbooleantrueDefer panel render until first click
enabledbooleantrueTab is enabled/disabled
declaredBystring—Property name in Page class (for multiple pages sharing a panel)

Route Matching — Registration Order Wins​

processRequestPath() walks the tabs in declaration order (the key order of the object returned by getTabsConfiguration()) and stops at the first tab whose link or one of its altLinks matches. A {placeholder} in a link is turned into * before matching, and that wildcard spans slashes — so /#/invoices/{id} matches /invoices/new, /invoices/settings and /invoices/42/edit just as happily as /invoices/42.

The practical rule: declare the specific routes before the {id} route.

// correct: exact routes first, the parameterised catch-all last
invoiceEditor : { link: "/#/invoices/new", /* ... */ },
invoicingSettings: { link: "/#/invoices/settings", /* ... */ },
invoiceEditorEdit: { link: "/#/invoices/{id}/edit", /* ... */ },
invoiceDetail : { link: "/#/invoices/{id}", /* ... */ }

Get the order wrong and the later tabs are simply never reached — the detail panel opens for /invoices/new with an empty id, and the editor and settings pages become unreachable by any route. There is no specificity ranking and no warning; the symptom is a page that silently renders the wrong panel.

Two related details worth knowing:

  • A tab with no controller is selected without any access check, because the layout only consults AccessValidator() when a controller exists.
  • When two tabs match the same path, a refusal from the earlier one is final — see Authentication for why the predicates of a route family must agree.

Controls tab visibility in the menu. Defined in MenuPanel:

ScopeValueDescription
FOR_ALL"all"Visible to all authenticated users
MOBILE_ONLY"mobile"Visible only on mobile (< 768px), placed in separate section
ADMINS"admins"Visible only if user.HasAdminRights or user.IsAdmin
SYS_ADMINS"sysadmins"Visible only if user.IsAdmin or user.IsSysAdmin
HIDDEN"hidden"Not visible in menu, accessible via direct URL only
import { MenuItemScope } from "@io-oidis-hub-gui/Io/Oidis/Hub/Gui/Primitives/MenuPanel.js";

PageTabIds​

Predefined tab ID constants for common framework tabs:

ConstantValueDescription
PageTabIds.FAQ"faq"FAQ panel
PageTabIds.FEEDBACK"feedback"Feedback panel
PageTabIds.RELEASE_NOTES"releaseNotes"Release notes panel
PageTabIds.NOTIFICATIONS"notifications"Notifications panel

Mobile Menu​

The mobile menu is a feature of MenuPageLayout only. It works automatically — no extra configuration is needed beyond setting the correct scope on tabs.

How It Works​

  1. Desktop (≥ 768px): Side menu is a persistent sidebar. Menu state (open/closed) is persisted in localStorage.
  2. Mobile (< 768px): Menu collapses behind a hamburger toggle in the header. Clicking a menu item auto-closes the menu.

MenuPanel organizes items into sections based on their scope:

SectionScopeVisibility
MainFOR_ALLAlways visible
MobileMOBILE_ONLYHidden on desktop, shown on mobile
AdminADMINSHidden if user is not admin
SysAdminSYS_ADMINSHidden if user is not sys admin

Sections with no items are hidden automatically. A horizontal separator (<hr>) divides the mobile section from the rest.

Mobile-Only Tabs​

Use MOBILE_ONLY scope for items that only make sense on mobile (e.g., FAQ shortcut, compact navigation items):

faq: {
name: "FAQ",
scope: MenuItemScope.MOBILE_ONLY,
icon: `<svg>...</svg>`,
content: FAQPanel,
controller: FAQPanelController
}

These tabs are still accessible via direct URL on desktop — they are just hidden from the menu.

Built-in Primitives​

All layouts include these built-in components, managed by BasePageLayoutController.

SkeletonLoader​

Shimmer animation placeholder shown during initial page load and tab loading. The controller manages it via ShowLoader($value) — hides tabsContent and shows the skeleton.

SystemStateNotification​

Monitors the WebSocket connection and shows alerts:

  • Offline (warning): Shown when connection drops
  • Online (success): Brief confirmation when connection restores
  • Reload required (info): Countdown timer (15s default) before auto-refresh when server restarts

The binding (ServerStateBinding) auto-reloads all tabs with reloadOnRestart: true when connection is restored.

ReleaseNotesDialog​

Offcanvas (slide-in from right, 80% width) displaying version changelogs. Bound via ReleaseNotesBinding in the controller.

  • Auto-opens on first visit after a version update (tracks user's WhatsNewSeen profile field)
  • Version selector dropdown to browse historical releases
  • Content organized into: news list, admin-specific changes, known issues

Triggers:

  • StickyHeaderLayout: headerPanel.releaseNotes button
  • MenuPageLayout: headerPanel.releaseNotes + menuPanel.releaseNotes buttons

BackdoorLoginDialog​

Modal dialog for admin users to impersonate other accounts during testing. Provides predefined profile switches (normal user, contributor/SysAdmin, admin) and custom login form.

Triggered by navigating to /#/backdoorLogin.

PrintContainer​

Hidden section for server-side (Puppeteer) print rendering. Contains a reference ruler for debugging and a warning message if users attempt native browser print.


See also: Page-Panel-Controller | Panels and Controllers | Component Internals