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
| Layout | Header | Side Menu | Mobile Menu | Footer | Use 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):
| Property | Type | Description |
|---|---|---|
skeletonLoader | SkeletonLoader | Loading shimmer placeholder |
tabsContent | GuiCommons | Tab content container |
headerStrip | GuiCommons | Environment warning bar (dev/eap/prod) |
systemState | SystemStateNotification | Offline/online/reload alerts |
releaseNotesDialog | ReleaseNotesDialog | Version release notes offcanvas |
notificationsDialog | NotificationsDialog | Notifications offcanvas |
backdoorLoginDialog | BackdoorLoginDialog | Admin impersonation dialog |
panelsContainer | GuiCommons | Panels wrapper |
printSection | PrintContainer | Hidden area for Puppeteer print rendering |
toasts | GuiCommons | Toast 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:
| Property | Type | Default | Description |
|---|---|---|---|
headerPanel | HeaderPanel | — | Sticky header bar (search, buttons, user profile) |
footerPanel | FooterPanel | — | Footer with version/build info |
pageContent | GuiCommons | — | Main content area (responsive column) |
scrollBody | GuiCommons | — | Scrollable body wrapper |
headerHeight | number | 70 | Header height in pixels |
The controller typically overrides selectTab() to mark active buttons in the header, since there is no menu panel with auto-highlighting.
MenuPageLayout
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:
| Property | Type | Description |
|---|---|---|
headerPanel | HeaderPanel | Sticky header with menu toggle button |
footerPanel | FooterPanel | Footer |
menuContent | GuiCommons | Side menu container (toggles "Show" class) |
menuPanel | MenuPanel | Side menu with categorized items |
pageContent | GuiCommons | Main content area |
scrollBody | GuiCommons | Scrollable wrapper |
Methods:
| Method | Description |
|---|---|
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
| Property | Type | Default | Description |
|---|---|---|---|
content | class / instance | — | Panel class or instance |
controller | class / instance | — | PanelController class |
name | string | "Tab N" | Display name in menu |
id | string | object key | Unique tab ID |
link | string | "/#/id" | Hash route (auto-generated from ID) |
altLinks | string[] | — | Alternative routes matching this tab |
icon | string | — | SVG/HTML icon for menu item |
scope | MenuItemScope | FOR_ALL | Visibility control |
isDefault | boolean | false | Active tab when no route matches |
autoReload | boolean | false | Re-fetch data on every tab click |
reloadOnRestart | boolean | false | Re-fetch data on server reconnect |
lazyLoad | boolean | true | Defer panel render until first click |
enabled | boolean | true | Tab is enabled/disabled |
declaredBy | string | — | 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.
MenuItemScope
Controls tab visibility in the menu. Defined in MenuPanel:
| Scope | Value | Description |
|---|---|---|
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:
| Constant | Value | Description |
|---|---|---|
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
- Desktop (≥ 768px): Side menu is a persistent sidebar. Menu state (open/closed) is persisted in
localStorage. - Mobile (< 768px): Menu collapses behind a hamburger toggle in the header. Clicking a menu item auto-closes the menu.
Menu Sections
MenuPanel organizes items into sections based on their scope:
| Section | Scope | Visibility |
|---|---|---|
| Main | FOR_ALL | Always visible |
| Mobile | MOBILE_ONLY | Hidden on desktop, shown on mobile |
| Admin | ADMINS | Hidden if user is not admin |
| SysAdmin | SYS_ADMINS | Hidden 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
WhatsNewSeenprofile field) - Version selector dropdown to browse historical releases
- Content organized into: news list, admin-specific changes, known issues
Triggers:
StickyHeaderLayout:headerPanel.releaseNotesbuttonMenuPageLayout:headerPanel.releaseNotes+menuPanel.releaseNotesbuttons
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