Built-in Applications
ElyOS ships with 6 built-in applications that provide the system’s core functionality. Every application uses a unified framework that ensures a consistent user experience and easy extensibility.
Application Framework
Section titled “Application Framework”ElyOS applications use a unified framework that provides a consistent user experience, easy extensibility, and efficient state management. The central elements of the framework are AppShell (state management logic) and AppLayout (visual structure), which work together to handle menu navigation, dynamic component loading, and the application lifecycle.
Architecture Overview
Section titled “Architecture Overview”Every built-in application follows the same architecture:
┌───────────────────────────────────────────────────────┐│ AppLayout ││ ┌──────────────┐ ┌──────────────────────────────┐ ││ │ │ │ │ ││ │ AppSideBar │ │ AppContentArea │ ││ │ │ │ (dynamic component) │ ││ │ ┌────────┐ │ │ │ ││ │ │ Menu │ │ │ - Vite glob import │ ││ │ │ Items │ │ │ - Lazy loading │ ││ │ └────────┘ │ │ - Props passing │ ││ │ │ │ │ ││ └──────────────┘ └──────────────────────────────┘ ││ ┌──────────────────────────────┐ ││ │ ActionBar │ ││ │ (optional action bar) │ ││ └──────────────────────────────┘ │└───────────────────────────────────────────────────────┘Data flow:
- AppShell manages state (active menu item, component, props)
- AppSideBarMenu renders the menu and handles clicks
- AppShell.handleMenuItemClick() updates state
- AppContentArea detects the change and loads the new component
- Component renders with props and can set the ActionBar
Key Components
Section titled “Key Components”1. AppShell
Section titled “1. AppShell”AppShell is the central state manager and logic layer for every application. It is responsible for:
- Menu state management (active menu item, expanded parents)
- Component loading coordination
- Navigation handling
- Localization (multilingual menu)
- Parameter handling (URL hash-based navigation)
Usage:
<script lang="ts"> import { createAppShell } from '$lib/apps/appShell.svelte'; import menuData from './menu.json';
const shell = createAppShell({ appName: 'settings', menuData: menuData as RawMenuItem[] });</script>Key features:
menuItems- Localized menu items (reactive)activeMenuItem- Active menu item hrefactiveComponent- Loaded component namecomponentProps- Props passed to the componentexpandedParents- Expanded parent menu itemshandleMenuItemClick(item)- Menu item click handlernavigateTo(component, props, menuHref)- Programmatic navigation
2. AppLayout
Section titled “2. AppLayout”AppLayout is the visual framework that connects all UI elements:
<AppLayout {shell} namespaces={['settings', 'common']} maxWidthClass="max-w-3xl" sidebarWidth={230} searchable={false}/>Parameters:
shell- AppShell instance (required)namespaces- i18n namespaces (automatically adds ‘common’)maxWidthClass- Content max width (Tailwind class)sidebarWidth- Sidebar width in pixels or ‘auto’searchable- Enable search in the menuisPlugin- Plugin mode (components loaded via API)
Features:
- I18nProvider setup
- Sidebar and menu rendering
- Content area management
- ActionBar display (if content is present)
- Automatic ActionBar clearing on component switch
3. AppSideBar and AppSideBarMenu
Section titled “3. AppSideBar and AppSideBarMenu”The left sidebar contains the navigation menu and provides collapsible/expandable functionality.
AppSideBar features:
- Collapsible sidebar (ChevronLeft/Right button)
- State saved to localStorage (
app-sidebar-collapsed-${appName}) - Configurable width (pixels or ‘auto’)
- Smooth animations (CSS transitions)
- Gradient background (light/dark mode support)
AppSideBarMenu features:
- Hierarchical menu structure: Parent-child relationship support
- Collapsible parents: Chevron icon with animation
- Active menu item highlight: Visual feedback
- Search: Optional search field for filtering menu items
- Smooth animations: CSS Grid-based (
grid-template-rows: 0fr → 1fr) - Icon support: UniversalIcon component (Lucide/Phosphor)
- Separator support: Divider lines in the menu
- Hidden item filtering: Via
hidden: truefield
Menu state management:
// Expanded itemslet expandedItems = $state<Set<string>>(new Set());
// Items manually closed by the user// (these are not automatically reopened)let manuallyClosed = $state<Set<string>>(new Set());Automatic expansion:
- On search: All parent items containing results are expanded
- Active menu item: Parent items are automatically expanded
- On initialization: Based on the
initialExpandedParentsprop
Search behavior:
- User types a search term
filterItemsBySearch()recursively filters menu itemsgetExpandedParentsForSearch()determines which parents to expandexpandedItemsis updated, menu animates openmanuallyClosedis cleared (everything visible during search)
Menu item click:
function handleClick(item: MenuItem, event: MouseEvent) { event.preventDefault(); onItemClick?.(item); // AppShell.handleMenuItemClick()}4. AppContentArea
Section titled “4. AppContentArea”The content area dynamically loads and renders components. This is one of the most important parts of the framework, providing lazy loading and efficient component management.
How it works:
-
Vite glob import for all app components:
const appComponentModules = import.meta.glob('/src/apps/*/components/*.svelte');// Result: { '/src/apps/settings/components/ProfileSettings.svelte': () => Promise<Module> } -
On-demand component loading (lazy loading):
const moduleKey = `/src/apps/${appName}/components/${componentName}.svelte`;const moduleLoader = appComponentModules[moduleKey];if (!moduleLoader) {throw new Error(`Component not found: ${componentName}`);}const module = await moduleLoader();loadedComponent = module.default; -
Component rendering with props:
{#if loadedComponent}{@const Component = loadedComponent}<Component {...props} />{/if}
Reactive loading:
$effect(() => { const currentComponent = component;
// Guard: don't reload if already loading or already loaded if (isLoadingComponent) return;
if (currentComponent && currentComponent !== lastLoadedComponent) { untrack(() => loadComponent(currentComponent)); } else if (!currentComponent) { untrack(() => { loadedComponent = null; error = null; lastLoadedComponent = null; }); }});States:
loading- Loading in progress (show spinner)error- An error occurred (show error message)loadedComponent- Loaded component (render)placeholder- No menu item selected (empty state)isLoadingComponent- Guard flag (prevent double loading)lastLoadedComponent- Last loaded component name (cache)
Plugin support:
Plugin components are loaded via API and rendered as Web Components:
async function loadPluginComponent(componentName: string) { // 1. Fetch component code via API const response = await fetch(`/api/plugins/${appName}/components/${componentName}`); const code = await response.text();
// 2. Execute code (create factory function) const script = document.createElement('script'); script.textContent = code; document.head.appendChild(script);
// 3. Call factory and get custom element tag name const factoryName = `${appName.replace(/-/g, '_')}_Component_${componentName}`; const componentFactory = window[factoryName]; const componentInfo = componentFactory();
// 4. Render custom element loadedComponent = { __pluginTagName: componentInfo.tagName, __pluginProps: props };}Error handling:
- Component not found → Show error message
- Loading error → Console log + error message
- Plugin loading error → Factory function missing
Performance optimization:
- Lazy loading: Only the active component is loaded
- Cache: Avoid reloading based on
lastLoadedComponent - Guard flag:
isLoadingComponentprevents double loading untrack(): Avoids infinite loops
5. ActionBar
Section titled “5. ActionBar”The ActionBar is an optional action bar at the bottom of the application where components can place buttons and other controls.
Usage in a component:
<script lang="ts"> import { getActionBar } from '$lib/apps/actionBar.svelte'; import { Button } from '$lib/components/ui/button';
const actionBar = getActionBar();
// Set ActionBar content actionBar.set(myActions);</script>
{#snippet myActions()} <Button onclick={handleSave}>Save</Button> <Button variant="outline" onclick={handleCancel}>Cancel</Button>{/snippet}API:
content- Current snippet (null if none)set(snippet)- Set snippetclear()- Clear content
Automatic clearing:
AppLayout automatically clears the ActionBar content on component switch, so no manual management is needed.
Menu Structure (menu.json)
Section titled “Menu Structure (menu.json)”Every application has a menu.json file that defines the menu items. The menu supports hierarchical structure, localization, and permission-based filtering.
Example menu structure:
[ { "labelKey": "menu.profile", "href": "#profile", "icon": "User", "component": "ProfileSettings" }, { "labelKey": "menu.desktop", "href": "#", "icon": "Monitor", "children": [ { "labelKey": "menu.general", "href": "#desktop", "icon": "Settings", "component": "DesktopSettings" }, { "labelKey": "menu.background", "href": "#background", "icon": "Image", "component": "BackgroundSettings" } ] }, { "separator": true }, { "labelKey": "menu.advanced", "href": "#advanced", "icon": "Settings", "component": "AdvancedSettings", "requiredPermission": "settings.advanced.view", "hideWhen": "notDevMode" }]Menu item fields:
| Field | Type | Required | Description |
|---|---|---|---|
labelKey | string | Yes | Translation key (e.g. “menu.profile” → “settings.menu.profile”) |
href | string | Yes | URL hash (e.g. “#profile”). Can be ”#” for parent items |
icon | string | No | Icon name (Lucide or Phosphor, e.g. “User”, “Settings”) |
component | string | No* | Component name from the components/ folder (e.g. “ProfileSettings”) |
children | array | No | Sub-menu items array (hierarchical menu) |
separator | boolean | No | Divider line (no other fields needed when true) |
requiredPermission | string | No | Required permission (e.g. “settings.advanced.view”) |
hideWhen | string | No | Conditional hiding (e.g. “notDevMode”, “singleLocale”) |
props | object | No | Props passed to the component (e.g. { "mode": "advanced" }) |
hidden | boolean | No | Hide menu item (not shown when true) |
* The component field is required if the menu item has no children and is not a separator.
Menu localization:
The menu is automatically localized using the localizeMenuItems() function:
// Namespace: appName (e.g. 'settings')// Key: labelKey (e.g. 'menu.profile')// Full key: 'settings.menu.profile'
const menuItems = $derived.by(() => { void translationStore.currentLocale; return localizeMenuItems(appName, rawMenuData);});Adding translations:
INSERT INTO translations (namespace, key, locale, value) VALUES ('settings', 'menu.profile', 'hu', 'Profil'), ('settings', 'menu.profile', 'en', 'Profile'), ('settings', 'menu.desktop', 'hu', 'Asztal'), ('settings', 'menu.desktop', 'en', 'Desktop');Hierarchical menu:
Use the children field to create multi-level menus:
{ "labelKey": "menu.desktop", "href": "#", "icon": "Monitor", "children": [ { "labelKey": "menu.general", "href": "#desktop", "component": "DesktopSettings" } ]}Important rules:
- Parent menu item href: If it has
children, thehrefcan be ”#” (no navigation) - Parent menu item component: Optional; if present, clicking the parent also loads it
- Child menu items: Always require a
componentfield - Separator: Only
separator: trueis needed, nothing else - Icon: Optional, but recommended for better UX
Permission-based filtering:
Use the requiredPermission field to restrict menu item visibility:
{ "labelKey": "menu.advanced", "href": "#advanced", "component": "AdvancedSettings", "requiredPermission": "settings.advanced.view"}If the user does not have the required permission, the menu item is automatically hidden.
Conditional hiding:
Use the hideWhen field to conditionally hide menu items:
{ "labelKey": "menu.language", "href": "#language", "component": "LanguageSettings", "hideWhen": "singleLocale"}Supported values:
"singleLocale"- Hides when only one language is in the system"notDevMode"- Hides when the system is not running in developer mode
Component Lifecycle
Section titled “Component Lifecycle”-
Application startup
- AppShell creation
- Menu loading and localization
- Default menu item selection (first one with a component)
-
Menu item click
handleMenuItemClick(item)calledactiveComponentupdatedcomponentPropsset- URL hash updated (section parameter)
-
Component loading
- AppContentArea
$effecttriggered - Vite glob import used
- Async component loading
- Component rendering with props
- AppContentArea
-
ActionBar management
- Component sets the ActionBar (
actionBar.set()) - ActionBar appears at the bottom of the application
- Automatic clearing on component switch
- Component sets the ActionBar (
-
Navigation
- URL hash change (
#profile→#security) - AppShell
$effectdetects the change - Appropriate menu item activated
- Component loading
- URL hash change (
Multilingual Support
Section titled “Multilingual Support”The menu is automatically localized using the localizeMenuItems function:
// Namespace: appName (e.g. 'settings')// Key: labelKey (e.g. 'menu.profile')// Full key: 'settings.menu.profile'
const menuItems = $derived.by(() => { void translationStore.currentLocale; return localizeMenuItems(appName, rawMenuData);});Translations:
INSERT INTO translations (namespace, key, locale, value) VALUES ('settings', 'menu.profile', 'hu', 'Profil'), ('settings', 'menu.profile', 'en', 'Profile');Permission Checking
Section titled “Permission Checking”Menu items are automatically filtered based on the user’s permissions:
// menu.json{ "labelKey": "menu.advanced", "requiredPermission": "settings.advanced.view", // ...}If the user does not have the required permission, the menu item is not shown.
URL Parameters
Section titled “URL Parameters”Applications support the section parameter in the URL hash:
// Open window with section parameterwindowManager.openWindow('settings', 'Settings', settingsApp, { section: 'profile' // → activate #profile menu item});AppShell automatically detects the section parameter and activates the appropriate menu item.
Built-in Applications List
Section titled “Built-in Applications List”1. Settings
Section titled “1. Settings”System and user settings management.
Key features:
- Profile settings
- Appearance (theme, language)
- Security (password, 2FA)
- Theme previews
Menu structure: 3 main categories, 11 components
2. Users
Section titled “2. Users”User, group, role, and permission management.
Key features:
- User CRUD
- Group management
- Role management
- Permission management
- Resource management
Access: Admin users only
3. Chat
Section titled “3. Chat”Real-time messaging system with Socket.IO.
Key features:
- Real-time messaging
- Online/offline status
- Typing indicator
- Unread messages
- Toast notifications
Technology: Socket.IO + REST API fallback
4. Log
Section titled “4. Log”System and error log viewer.
Key features:
- Error logs in tabular view
- Filter by log level
- Filter by source
- Sorting and pagination
Access: log.error.view (admin)
5. Plugin Manager
Section titled “5. Plugin Manager”Plugin installation, management, and removal.
Key features:
- Plugin upload (.elyospkg)
- Plugin validation
- Installed plugins list
- Plugin details
- Plugin removal
Access: plugin.manual.install (admin)
6. Help
Section titled “6. Help”Context-sensitive help system.
Status: Under development
Planned features:
- Context-sensitive help
- Help button in windows
- Search in help content
- Multilingual content
Creating a New Application
Section titled “Creating a New Application”1. Basic Structure
Section titled “1. Basic Structure”apps/my-app/├── index.svelte # Main entry point├── icon.svg # Application icon├── menu.json # Menu definition├── my-app.remote.ts # Server actions├── components/ # UI components│ ├── Component1.svelte│ └── Component2.svelte└── stores/ # State management (optional) └── myAppStore.svelte.ts2. Creating index.svelte
Section titled “2. Creating index.svelte”<script lang="ts"> import type { RawMenuItem } from '$lib/types/menu'; import { AppLayout } from '$lib/components/shared'; import { createAppShell } from '$lib/apps/appShell.svelte'; import menuData from './menu.json';
const shell = createAppShell({ appName: 'my-app', menuData: menuData as RawMenuItem[] });</script>
<AppLayout {shell} namespaces={['my-app']} />3. Creating menu.json
Section titled “3. Creating menu.json”[ { "labelKey": "menu.overview", "href": "#overview", "icon": "Home", "component": "Overview" }, { "labelKey": "menu.settings", "href": "#settings", "icon": "Settings", "component": "Settings" }]4. Creating Components
Section titled “4. Creating Components”<script lang="ts"> import { getActionBar } from '$lib/apps/actionBar.svelte'; import { Button } from '$lib/components/ui/button';
const actionBar = getActionBar(); actionBar.set(actions);
function handleAction() { console.log('Action clicked'); }</script>
{#snippet actions()} <Button onclick={handleAction}>Action</Button>{/snippet}
<div class="title-block"> <h2>Overview</h2> <h3>Application overview page</h3></div>
<p>Content...</p>5. Adding Translations
Section titled “5. Adding Translations”-- packages/database/src/seeds/translations/my-app.tsINSERT INTO translations (namespace, key, locale, value) VALUES ('my-app', 'title', 'hu', 'Saját Alkalmazás'), ('my-app', 'title', 'en', 'My App'), ('my-app', 'menu.overview', 'hu', 'Áttekintés'), ('my-app', 'menu.overview', 'en', 'Overview');6. Registering the Application
Section titled “6. Registering the Application”-- packages/database/src/seeds/platform/apps.ts{ appId: 'my-app', appType: 'builtin', name: { hu: 'Saját Alkalmazás', en: 'My App' }, description: { hu: 'Leírás', en: 'Description' }, version: '1.0.0', icon: 'icon.svg', category: 'productivity', isActive: true}Best Practices
Section titled “Best Practices”- Consistent menu structure: Use meaningful hrefs (#profile, #security)
- Component names: PascalCase (ProfileSettings.svelte)
- ActionBar usage: Only when necessary (save, cancel buttons)
- Translations: Always provide both Hungarian and English versions
- Permissions: Use
requiredPermissionfor sensitive menu items - Icon selection: Use consistent icons (Lucide or Phosphor)
- Props passing: Use the
propsfield in menu.json - Error handling: Handle component loading errors
- Loading state: Display the loading state
- Responsive design: Design for mobile views as well
Troubleshooting
Section titled “Troubleshooting”Component Not Loading
Section titled “Component Not Loading”Problem: “Component not found” error.
Solution:
- Check the component name in menu.json
- Verify the component exists in the
components/folder - Check the browser console for detailed errors
Menu Not Showing
Section titled “Menu Not Showing”Problem: The sidebar is empty.
Solution:
- Check the menu.json syntax
- Check translations (namespace + labelKey)
- Verify permissions
ActionBar Not Showing
Section titled “ActionBar Not Showing”Problem: The ActionBar is not visible.
Solution:
- Check that you called
actionBar.set() - Verify the snippet is correctly defined
- Check that AppLayout renders the ActionBar