AppLayout
AppLayout is the whole admin shell: gradient topbar (search, messages, notifications, language, color mode, user menu), multi-level side menu with active trail, user profile, settings panel, page header and footer. Switch between vertical, mini and horizontal with config.layout.
Preview
<div class="layout-frame"><iframe src="http://localhost:5182/" title="Vue playground" loading="lazy"></iframe><p class="layout-frame__hint">Live Vue playground (run <code>npm run dev:vue</code>). Open the ⚙ settings panel to switch skin, layout, dark mode and language.</p></div>import { AppLayout, Page } from '@thl-ui/react';
import { Link, useLocation } from 'react-router';
<AppLayout menu={menu} currentPath={loc.pathname} user={{ ...user, onSelect: (k) => k === 'logout' && logout() }}
onSearch={(q) => nav(`/search?q=${q}`)}
renderLink={(item, children, className) => <Link to={item.href!} className={className}>{children}</Link>}
topbarRight={<TopbarMessages items={messages} icon="mail" />}
sidebarFooter={<Button variant="success" block>Upgrade</Button>}>
<Page title="Dashboard" breadcrumb={[{ label: 'Home', href: '/' }, { label: 'Dashboard' }]}>…</Page>
</AppLayout><thl-app-layout [menu]="menu" [currentPath]="path()" [user]="user" searchable (search)="search($event)" (userSelect)="onUser($event)">
<ng-template thlMenuLink let-item let-cls="cls" let-content="content">
<a [routerLink]="item.href" [class]="cls"><ng-container [ngTemplateOutlet]="content" /></a>
</ng-template>
<ng-container thlTopbarRight><thl-topbar-messages [items]="messages" icon="mail" /></ng-container>
<div thlSidebarFooter><button thl-button variant="success" block>Upgrade</button></div>
<router-outlet />
</thl-app-layout>Vue
<ThlAppLayout :menu="menu" :current-path="route.path" :user="user" searchable @search="onSearch" @user-select="onUser">
<template #topbar-right><ThlTopbarMessages :items="messages" icon="mail" /></template>
<template #link="{ item, content, class: cls }"><RouterLink :to="item.href" :class="cls"><component :is="() => content" /></RouterLink></template>
<template #sidebar-footer><ThlButton variant="success" block>Upgrade</ThlButton></template>
<ThlPage title="Dashboard" :breadcrumb="[{ label: 'Home', href: '/' }, { label: 'Dashboard' }]">…</ThlPage>
</ThlAppLayout>Menu
import type { MenuItem } from '@thl-ui/core';
export const menu: MenuItem[] = [
{ key: 'dash', label: 'menu.dashboard', icon: 'layout-dashboard', badge: { text: 4, color: 'danger' }, children: [
{ key: 'd1', label: 'menu.dashboard1', href: '/' },
{ key: 'd2', label: 'menu.dashboard2', href: '/dashboard-2' },
] },
{ key: 'h', label: 'menu.apps', heading: true },
{ key: 'inbox', label: 'menu.inbox', icon: 'mail', href: '/inbox' },
];label may be an i18n key (translated automatically) or a literal. The active trail is computed from currentPath.
Horizontal layout
With layout: 'horizontal' the menu moves from the side to a bar under the topbar (like Cubic-horizontal). Sub-menus open as dropdowns on hover or keyboard focus. The menu always stays on one row: items that do not fit are moved into a More dropdown, and come back as the window gets wider. Below 768px the menu becomes the usual off-canvas sidebar. The same menu array works for every layout.
// main.tsx: start in horizontal mode
<ThlProvider config={{ layout: 'horizontal' }}>
<AppLayout menu={menu} currentPath={loc.pathname} renderLink={renderLink}>…</AppLayout>
</ThlProvider>
// switch at runtime (the ⚙ settings panel does the same)
const [, setConfig] = useThlConfig();
setConfig({ layout: 'horizontal' });// main.ts: start in horizontal mode
app.use(ThlUI, { layout: 'horizontal' });
// switch at runtime (the ⚙ settings panel does the same)
const { set } = useThlConfig();
set({ layout: 'horizontal' });// main.ts: start in horizontal mode
bootstrapApplication(AppComponent, { providers: [provideThlUI({ layout: 'horizontal' })] });
// switch at runtime (the ⚙ settings panel does the same)
inject(ThlConfigService).set({ layout: 'horizontal' });Try it in a playground: localhost:5182/?layout=horizontal. The playgrounds also read ?layout=mini, ?theme=red-dark, ?lang=ja and ?mode=dark.
Responsive behaviour
- ≥ 1170px: full sidebar (or mini / horizontal per
layout) - 768 – 1169px: mini sidebar automatically
- < 768px: off-canvas sidebar with backdrop, toggled from the topbar
The breakpoint is configurable with components.Sidebar.breakpoint.
API
AppLayout
<ThlAppLayout> · import { ThlAppLayout } from '@thl-ui/vue'
| Prop | Type | Default |
|---|---|---|
menu* | array | — |
currentPath | string | — |
brand | string | "thl-ui" |
brandHref | string | "/" |
user | object | — |
searchable | boolean | — |
settings | boolean | true |
footer | string | boolean | — |
Events: searchuser-select
Slots: brandtopbar-lefttopbar-rightlinksidebar-footerdefaultfooter
SideMenu
<ThlSideMenu> · import { ThlSideMenu } from '@thl-ui/vue'
| Prop | Type | Default |
|---|---|---|
items* | array | — |
currentPath | string | — |
accordion | boolean | true |
horizontal | boolean | — |
Slots: link
TopbarMessages
<ThlTopbarMessages> · import { ThlTopbarMessages } from '@thl-ui/vue'
| Prop | Type | Default |
|---|---|---|
items* | array | — |
icon | string | "mail" |
title | string | — |
footer | string | — |
footerHref | string | — |
SettingsPanel
<ThlSettingsPanel> · import { ThlSettingsPanel } from '@thl-ui/vue'
| Prop | Type | Default |
|---|---|---|
open | boolean | — |
Events: update:open
Page
<ThlPage> · import { ThlPage } from '@thl-ui/vue'
| Prop | Type | Default |
|---|---|---|
title | string | — |
breadcrumb | array | — |
Slots: actionslinkdefault