Skip to content

menu.json and AppLayout Mode

If a menu.json file is present in the application root, Racona loads the application in AppLayout mode. In this mode, the application appears as a multi-page application with a sidebar navigation — the same layout as built-in applications (e.g. Settings, Users).

Without AppLayout mode (standalone), the application loads as a single Web Component filling the entire window.


[
{
"labelKey": "menu.overview",
"href": "#overview",
"icon": "Home",
"component": "Overview"
},
{
"labelKey": "menu.settings",
"href": "#settings",
"icon": "Settings",
"component": "Settings"
}
]
FieldTypeDescription
labelKeystringi18n key for the menu item label (from the application’s locales/ files)
hrefstringHash-based route (e.g. #overview)
iconstringLucide icon name (PascalCase, e.g. Home, Settings, Table)
componentstringName of the Svelte component to load (filename without extension)
childrenMenuItem[]Optional submenu. The parent then needs no component, and href may be "#".
requiredPermissionstringCore permission (e.g. plugin.manual.install) required to show the item.
requiredCapabilitystringPlugin-specific capability (e.g. project.create) required to show the item — see below.
[
{
"labelKey": "menu.projects",
"href": "#",
"icon": "FolderKanban",
"children": [
{ "labelKey": "menu.projects.list", "href": "#projects", "icon": "List", "component": "ProjectList" },
{ "labelKey": "menu.projects.create", "href": "#projects/new", "icon": "Plus", "component": "ProjectCreate", "requiredCapability": "project.create" }
]
}
]

requiredCapability builds on the plugin’s own permission model: the plugin computes the current user’s capabilities on the client (e.g. via a server function reading its own roles tables) and publishes them to the core:

window.dispatchEvent(
new CustomEvent('plugin-capabilities-changed', {
detail: { pluginId: 'my-app', capabilities: ['project.create', 'employee.view'] }
})
);

The core then hides menu items whose requiredCapability is not in the set. If all children of a parent are hidden, the parent disappears too. Re-dispatch the event whenever the set changes (e.g. on organization switch).

menu.json can be a plain array, or { "layout": { "sidebarWidth": 280, "maxWidthClass": "w-full" }, "items": [...] }. See the manifest reference for the layout field.

The icon field value can be any Lucide icon name in PascalCase format:

"icon": "Home" // home icon
"icon": "Settings" // gear icon
"icon": "Table" // table icon
"icon": "Puzzle" // puzzle piece
"icon": "Users" // users icon
"icon": "BarChart" // bar chart

In AppLayout mode, each menu item has a dedicated Svelte component. These should be placed in the src/components/ folder:

my-app/
├── manifest.json
├── menu.json
├── src/
│ ├── App.svelte # Main entry point (optional in AppLayout mode)
│ └── components/
│ ├── Overview.svelte
│ ├── Settings.svelte
│ └── DataTableDemo.svelte
├── locales/
│ ├── hu.json
│ └── en.json
└── build-all.js

The component field value in menu.json must match the filename (without extension):

{ "component": "Overview" } → src/components/Overview.svelte
{ "component": "Settings" } → src/components/Settings.svelte

Since each component must be built separately, the build-all.js script automates this:

Terminál
bun run build:all

The script:

  1. Builds the main application (BUILD_MODE=main)
  2. Iterates through the src/components/ folder
  3. Builds each .svelte file as a separate entry point
build-all.js
// 1. Main application build
execSync('BUILD_MODE=main vite build', { stdio: 'inherit' });
// 2. Build components
for (const file of svelteFiles) {
execSync(`BUILD_MODE=components COMPONENT_FILE=${file} vite build`, {
stdio: 'inherit'
});
}

The build output:

dist/
├── index.js # Main entry point
├── Overview.js # Overview component
├── Settings.js # Settings component
├── DataTableDemo.js # DataTableDemo component
└── assets/

The labelKey values are loaded from the application’s locales/ files. For example:

locales/hu.json
{
"menu.overview": "Áttekintés",
"menu.settings": "Beállítások",
"menu.datatable": "Adattábla"
}
// locales/en.json
{
"menu.overview": "Overview",
"menu.settings": "Settings",
"menu.datatable": "Data Table"
}

The sdk-demo application is a good reference for AppLayout mode. It has four menu items:

[
{
"labelKey": "menu.overview",
"href": "#overview",
"icon": "Home",
"component": "Overview"
},
{
"labelKey": "menu.demo",
"href": "#demo",
"icon": "Puzzle",
"component": "HelloWorldDemo"
},
{
"labelKey": "menu.datatable",
"href": "#datatable",
"icon": "Table",
"component": "DataTableDemo"
},
{
"labelKey": "menu.settings",
"href": "#settings",
"icon": "Settings",
"component": "Settings"
}
]

Source: racona-core/examples/apps/sdk-demo/


Standalone (no menu.json)AppLayout (with menu.json)
Simple, single-page widgetApplication with multiple views
Dashboard elementSettings, admin interface
Small utility toolComplex data management app

If your application contains multiple logically separate sections, AppLayout mode provides a better user experience and consistent appearance with other built-in applications.