# DeesAppui A comprehensive application shell component providing a complete UI framework with navigation, menus, activity logging, and view management. πŸš€ ## Quick Start ```typescript import { html, DeesElement, customElement } from '@design.estate/dees-element'; import { DeesAppui } from '@design.estate/dees-catalog'; @customElement('my-app') class MyApp extends DeesElement { private appui: DeesAppui; async firstUpdated() { this.appui = this.shadowRoot.querySelector('dees-appui'); // Configure with views and menu this.appui.configure({ branding: { logoIcon: 'lucide:box', logoText: 'My App' }, views: [ { id: 'dashboard', name: 'Dashboard', iconName: 'lucide:home', content: 'my-dashboard' }, { id: 'settings', name: 'Settings', iconName: 'lucide:settings', content: 'my-settings' }, ], mainMenu: { sections: [{ name: 'Main', views: ['dashboard', 'settings'] }] }, defaultView: 'dashboard' }); } render() { return html``; } } ``` ## Architecture Overview The DeesAppui shell consists of several interconnected components: ``` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ AppBar (dees-appui-appbar) β”‚ β”‚ β”œβ”€β”€ Menus (File, Edit, View...) β”‚ β”‚ β”œβ”€β”€ Breadcrumbs β”‚ β”‚ β”œβ”€β”€ User Profile + Dropdown β”‚ β”‚ └── Activity Log Toggle β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ Main Menu β”‚ Content Area β”‚ Activity Log β”‚ β”‚ (collapsed/ β”‚ β”œβ”€β”€ Content Tabs β”‚ (slide panel) β”‚ β”‚ expanded) β”‚ β”‚ (closable, from tables/lists)β”‚ β”‚ β”‚ β”‚ └── View Container β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ └── Active View β”‚ β”‚ β”‚ β”‚ 🏠 Home β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ β”‚ πŸ“ Filesβ”‚ β”‚ Secondary Menu β”‚ β”‚ β”‚ β”‚ β”‚ βš™ Settings β”œβ”€β”€ Collapsible Groups β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ Item 1 β”‚ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”œβ”€β”€ Item 2 (with badge) β”‚ β”‚ β”‚ β”‚ β”‚ └── Item 3 β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` --- ## Configuration API ### `configure(config: IAppConfig)` Configure the entire application shell with a single configuration object. ```typescript interface IAppConfig { branding?: IBrandingConfig; appBar?: IAppBarConfig; views: IViewDefinition[]; mainMenu?: IMainMenuConfig; defaultView?: string; activityLog?: IActivityLogConfig; onViewChange?: (viewId: string, view: IViewDefinition) => void; onSearch?: (query: string) => void; } ``` ### View Definition ```typescript interface IViewDefinition { id: string; // Unique identifier name: string; // Display name iconName?: string; // Icon (e.g., 'lucide:home') content: // View content | string // Tag name ('my-component') | (new () => HTMLElement) // Class constructor | (() => TemplateResult) // Template function | (() => Promise<...>); // Async for lazy loading secondaryMenu?: ISecondaryMenuGroup[]; contentTabs?: ITab[]; route?: string; // URL route (default: id) badge?: string | number; cache?: boolean; // Cache view instance (default: true) } ``` --- ## Programmatic APIs ### App Bar API Control the top application bar. ```typescript // Set menu items (File, Edit, View, etc.) appui.setAppBarMenus([ { name: 'File', submenu: [ { name: 'New', shortcut: 'Cmd+N', action: () => {} }, { name: 'Save', shortcut: 'Cmd+S', action: () => {} }, ] } ]); // Update single menu appui.updateAppBarMenu('File', { submenu: [...newItems] }); // Breadcrumbs appui.setBreadcrumbs('Dashboard > Settings > Profile'); appui.setBreadcrumbs(['Dashboard', 'Settings', 'Profile']); // User profile appui.setUser({ name: 'John Doe', email: 'john@example.com', avatar: '/avatars/john.png', status: 'online' // 'online' | 'offline' | 'busy' | 'away' }); appui.setProfileMenuItems([ { name: 'Profile', iconName: 'lucide:user', action: () => {} }, { divider: true }, { name: 'Sign Out', iconName: 'lucide:log-out', action: () => {} } ]); // Search appui.setSearchVisible(true); appui.onSearch((query) => console.log('Search:', query)); // Window controls (for Electron/Tauri apps) appui.setWindowControlsVisible(false); ``` ### Main Menu API (Left Sidebar) Control the main navigation menu. ```typescript // Set entire menu appui.setMainMenu({ logoIcon: 'lucide:box', logoText: 'My App', groups: [ { name: 'Main', tabs: [ { key: 'dashboard', iconName: 'lucide:home', action: () => {} }, { key: 'inbox', iconName: 'lucide:inbox', badge: 5, action: () => {} }, ] } ], bottomTabs: [ { key: 'settings', iconName: 'lucide:settings', action: () => {} } ] }); // Update specific group appui.updateMainMenuGroup('Main', { tabs: [...newTabs] }); // Add/remove items appui.addMainMenuItem('Main', { key: 'tasks', iconName: 'lucide:check', action: () => {} }); appui.removeMainMenuItem('Main', 'tasks'); // Selection appui.setMainMenuSelection('dashboard'); // Visibility control appui.setMainMenuCollapsed(true); // Collapse to icon-only sidebar appui.setMainMenuVisible(false); // Hide completely // Badges appui.setMainMenuBadge('inbox', 12); appui.clearMainMenuBadge('inbox'); ``` --- ## Secondary Menu API πŸ“‹ The secondary menu is a contextual sidebar that appears next to the main content area. It supports **collapsible groups** with icons and badges, making it perfect for: - **Settings pages** (grouped settings categories) - **File browsers** (folder trees) - **Project navigation** (grouped by category) - **Documentation** (chapters/sections) ### Collapsible Groups Groups can be collapsed/expanded by clicking the group header. The state is visually indicated with an icon rotation. ```typescript // Set secondary menu with collapsible groups appui.setSecondaryMenu({ heading: 'Settings', groups: [ { name: 'Account', iconName: 'lucide:user', // Group icon collapsed: false, // Initial state (default: false) items: [ { key: 'profile', iconName: 'lucide:user', action: () => showProfile() }, { key: 'security', iconName: 'lucide:shield', badge: '!', badgeVariant: 'warning', action: () => showSecurity() }, { key: 'billing', iconName: 'lucide:credit-card', action: () => showBilling() } ] }, { name: 'Preferences', iconName: 'lucide:settings', collapsed: true, // Start collapsed items: [ { key: 'notifications', iconName: 'lucide:bell', action: () => {} }, { key: 'appearance', iconName: 'lucide:palette', action: () => {} }, { key: 'language', iconName: 'lucide:globe', action: () => {} } ] } ] }); ``` ### Secondary Menu Item Properties ```typescript interface ISecondaryMenuItem { key: string; // Unique identifier iconName?: string; // Icon (e.g., 'lucide:user') action: () => void; // Click handler badge?: string | number; // Badge text/count badgeVariant?: 'default' | 'success' | 'warning' | 'error'; } interface ISecondaryMenuGroup { name: string; // Group name (shown in header) iconName?: string; // Group icon collapsed?: boolean; // Initial collapsed state items: ISecondaryMenuItem[]; // Items in this group } ``` ### Updating Secondary Menu ```typescript // Update a specific group appui.updateSecondaryMenuGroup('Account', { items: [...newItems] }); // Add item to a group appui.addSecondaryMenuItem('Account', { key: 'api-keys', iconName: 'lucide:key', action: () => showApiKeys() }); // Selection (highlights the item) appui.setSecondaryMenuSelection('profile'); // Visibility control appui.setSecondaryMenuCollapsed(true); // Collapse panel appui.setSecondaryMenuVisible(false); // Hide completely // Clear appui.clearSecondaryMenu(); ``` ### View-Specific Secondary Menus Each view can define its own secondary menu that appears when the view is activated: ```typescript // In view definition { id: 'settings', name: 'Settings', content: 'my-settings-view', secondaryMenu: [ { name: 'General', items: [ { key: 'account', iconName: 'lucide:user', action: () => {} }, { key: 'security', iconName: 'lucide:shield', action: () => {} } ] } ] } // Or set dynamically in view's onActivate hook onActivate(context: IViewActivationContext) { context.appui.setSecondaryMenu({ heading: 'Project Files', groups: [...] }); } ``` --- ## Content Tabs API πŸ“‘ Content tabs appear above the main view content. They're designed for **opening multiple items** from tables, lists, or other data sourcesβ€”similar to browser tabs or IDE editor tabs. ### Common Use Cases - **Table row details** - Click a row to open it as a tab - **Document editing** - Open multiple documents - **Entity inspection** - View customer, order, product details - **Multi-file editing** - Edit multiple configuration files ### Closable Tabs Tabs can be closable, allowing users to open items, work with them, and close when done: ```typescript // Set initial tabs appui.setContentTabs([ { key: 'overview', iconName: 'lucide:home', action: () => showOverview() }, { key: 'activity', iconName: 'lucide:activity', action: () => showActivity() } ]); // Add a closable tab when user clicks a table row table.addEventListener('row-click', (e) => { const item = e.detail.item; appui.addContentTab({ key: `item-${item.id}`, label: item.name, // Display label iconName: 'lucide:file', closable: true, // Allow closing action: () => showItemDetails(item) }); // Select the new tab appui.selectContentTab(`item-${item.id}`); }); // Handle tab close appui.addEventListener('tab-close', (e) => { const tabKey = e.detail.key; // Cleanup resources if needed console.log(`Tab ${tabKey} closed`); }); ``` ### Tab Management ```typescript // Add/remove tabs appui.addContentTab({ key: 'debug', iconName: 'lucide:bug', closable: true, action: () => {} }); appui.removeContentTab('debug'); // Select tab appui.selectContentTab('preview'); // Get current tab const current = appui.getSelectedContentTab(); // Visibility control appui.setContentTabsVisible(false); // Hide tab bar // Auto-hide when only one tab appui.setContentTabsAutoHide(true, 1); // Hide when ≀ 1 tab ``` ### Opening Items from Tables/Lists A common pattern is opening table rows as closable tabs: ```typescript @customElement('my-customers-view') class MyCustomersView extends DeesElement { private appui: DeesAppui; onActivate(context: IViewActivationContext) { this.appui = context.appui; // Set base tabs this.appui.setContentTabs([ { key: 'list', label: 'All Customers', iconName: 'lucide:users', action: () => this.showList() } ]); } render() { return html` `; } openCustomerTab(e: CustomEvent) { const customer = e.detail.item; const tabKey = `customer-${customer.id}`; // Check if tab already exists const existingTab = this.appui.getSelectedContentTab(); if (existingTab?.key === tabKey) { return; // Already viewing this customer } // Add new closable tab this.appui.addContentTab({ key: tabKey, label: customer.name, iconName: 'lucide:user', closable: true, action: () => this.showCustomerDetails(customer) }); this.appui.selectContentTab(tabKey); } showCustomerDetails(customer: Customer) { // Render customer details this.currentView = html``; } showList() { this.currentView = html``; } } ``` --- ## Activity Log API πŸ“Š The activity log is a slide-out panel on the right side showing user actions and system events. ### Activity Log Toggle The appbar includes a toggle button with a badge showing the entry count: ```typescript // Control visibility appui.setActivityLogVisible(true); // Show panel appui.toggleActivityLog(); // Toggle state const isVisible = appui.getActivityLogVisible(); // The toggle button automatically shows entry count // Add entries and the badge updates automatically ``` ### Adding Entries ```typescript // Add single entry appui.activityLog.add({ type: 'create', // 'login' | 'logout' | 'view' | 'create' | 'update' | 'delete' | 'custom' user: 'John Doe', message: 'created a new invoice', iconName: 'lucide:file-plus', // Optional custom icon data: { invoiceId: '123' } // Optional metadata }); // Add multiple entries (e.g., from backend) appui.activityLog.addMany([...entries]); // Clear all entries appui.activityLog.clear(); // Query entries const entries = appui.activityLog.getEntries(); const filtered = appui.activityLog.filter({ user: 'John', type: 'create' }); const searched = appui.activityLog.search('invoice'); ``` ### Activity Entry Types Each type has a default icon that can be overridden: | Type | Default Icon | Use Case | |------|--------------|----------| | `login` | `lucide:log-in` | User sign-in | | `logout` | `lucide:log-out` | User sign-out | | `view` | `lucide:eye` | Page/item viewed | | `create` | `lucide:plus` | New item created | | `update` | `lucide:pencil` | Item modified | | `delete` | `lucide:trash` | Item deleted | | `custom` | `lucide:activity` | Custom events | --- ## Navigation API Navigate between views programmatically. ```typescript // Navigate to view await appui.navigateToView('settings'); await appui.navigateToView('settings', { section: 'profile' }); // Get current view const current = appui.getCurrentView(); // Subscribe to view changes appui.viewChanged$.subscribe((event) => { console.log(`Navigated to: ${event.viewId}`); }); // Subscribe to lifecycle events appui.viewLifecycle$.subscribe((event) => { if (event.type === 'activated') { console.log(`View ${event.viewId} activated`); } }); ``` --- ## View Lifecycle Hooks Views can implement lifecycle hooks to respond to activation/deactivation. ```typescript import { DeesElement, customElement } from '@design.estate/dees-element'; import type { IViewActivationContext, IViewLifecycle } from '@design.estate/dees-catalog'; @customElement('my-settings-view') class MySettingsView extends DeesElement implements IViewLifecycle { /** * Called when view is activated (displayed) * Receives typed context with appui reference */ async onActivate(context: IViewActivationContext) { const { appui, viewId, params } = context; // Set view-specific secondary menu appui.setSecondaryMenu({ heading: 'Settings', groups: [{ name: 'Options', items: [...] }] }); // Set view-specific tabs appui.setContentTabs([...]); // Load data based on route params if (params?.section) { await this.loadSection(params.section); } } /** * Called when view is deactivated (hidden) */ onDeactivate() { this.cleanup(); } /** * Called before navigation away * Return false or a message string to block navigation */ canDeactivate(): boolean | string { if (this.hasUnsavedChanges) { return 'You have unsaved changes. Leave anyway?'; } return true; } } ``` ### IViewActivationContext ```typescript interface IViewActivationContext { appui: DeesAppui; // Reference to the app shell viewId: string; // The view ID being activated params?: Record; // Route parameters } ``` --- ## Routing Routes are automatically registered from view definitions using `domtools.router`. ```typescript const views = [ { id: 'dashboard', route: 'dashboard', ... }, { id: 'settings', route: 'settings/:section?', ... }, // Parameterized { id: 'user', route: 'users/:id', ... }, ]; // URL: #dashboard β†’ navigates to dashboard view // URL: #settings/profile β†’ navigates to settings with params.section = 'profile' // URL: #users/123 β†’ navigates to user with params.id = '123' ``` ### Hash-based Routing The router uses hash-based routing by default (`#viewId`). URLs are automatically synchronized when navigating via `navigateToView()`. --- ## View Caching Views are cached by default. When navigating away and back, the same DOM element is reused (hidden/shown) rather than destroyed and recreated. ```typescript // Disable caching for a specific view { id: 'reports', name: 'Reports', content: 'my-reports-view', cache: false // Always recreate this view } ``` --- ## Lazy Loading Use async content functions for lazy loading views. ```typescript { id: 'analytics', name: 'Analytics', content: async () => { const module = await import('./views/analytics.js'); return module.AnalyticsView; } } ``` --- ## RxJS Observables The component exposes RxJS Subjects for reactive programming. ```typescript // View lifecycle events appui.viewLifecycle$.subscribe((event) => { // event.type: 'loading' | 'activated' | 'deactivated' | 'loaded' | 'loadError' // event.viewId: string // event.element?: HTMLElement // event.params?: Record // event.error?: unknown }); // View change events appui.viewChanged$.subscribe((event) => { // event.viewId: string // event.view: IViewDefinition // event.previousView?: IViewDefinition // event.params?: Record }); ``` --- ## Complete Example ```typescript import { html, DeesElement, customElement } from '@design.estate/dees-element'; import { DeesAppui, IViewActivationContext } from '@design.estate/dees-catalog'; @customElement('my-app') class MyApp extends DeesElement { private appui: DeesAppui; async firstUpdated() { this.appui = this.shadowRoot.querySelector('dees-appui'); this.appui.configure({ branding: { logoIcon: 'lucide:briefcase', logoText: 'CRM Pro' }, appBar: { menuItems: [ { name: 'File', submenu: [...] }, { name: 'Edit', submenu: [...] } ], showSearch: true, user: { name: 'Jane Smith', status: 'online' } }, views: [ { id: 'dashboard', name: 'Dashboard', iconName: 'lucide:home', content: 'crm-dashboard', route: 'dashboard' }, { id: 'contacts', name: 'Contacts', iconName: 'lucide:users', content: 'crm-contacts', route: 'contacts', badge: 42 }, { id: 'settings', name: 'Settings', iconName: 'lucide:settings', content: 'crm-settings', route: 'settings/:section?' } ], mainMenu: { sections: [ { name: 'Main', views: ['dashboard', 'contacts'] } ], bottomItems: ['settings'] }, defaultView: 'dashboard', onViewChange: (viewId, view) => { console.log(`Navigated to: ${view.name}`); }, onSearch: (query) => { console.log(`Search: ${query}`); } }); // Load activity from backend const activities = await fetch('/api/activities').then(r => r.json()); this.appui.activityLog.addMany(activities); } render() { return html``; } } // View with lifecycle hooks @customElement('crm-settings') class CrmSettings extends DeesElement { private appui: DeesAppui; onActivate(context: IViewActivationContext) { this.appui = context.appui; // Set secondary menu for settings this.appui.setSecondaryMenu({ heading: 'Settings', groups: [ { name: 'Account', iconName: 'lucide:user', items: [ { key: 'profile', iconName: 'lucide:user', action: () => this.showSection('profile') }, { key: 'security', iconName: 'lucide:shield', action: () => this.showSection('security') } ] }, { name: 'Preferences', iconName: 'lucide:settings', collapsed: true, items: [ { key: 'notifications', iconName: 'lucide:bell', action: () => this.showSection('notifications') } ] } ] }); // Navigate to section from URL params if (context.params?.section) { this.showSection(context.params.section); } } showSection(section: string) { this.appui.setSecondaryMenuSelection(section); // ... load section content } } ``` --- ## TypeScript Types All interfaces are exported from `@design.estate/dees-catalog`: - `IAppConfig` - Main configuration - `IViewDefinition` - View definition - `IViewActivationContext` - Context passed to `onActivate` - `IViewLifecycle` - Lifecycle hooks interface - `IViewLifecycleEvent` - Lifecycle event for rxjs Subject - `IViewChangeEvent` - View change event - `IAppUser` - User configuration - `IActivityEntry` - Activity log entry - `IActivityLogAPI` - Activity log methods - `IAppBarMenuItem` - App bar menu item - `IMainMenuConfig` - Main menu configuration - `ISecondaryMenuGroup` - Secondary menu group - `ISecondaryMenuItem` - Secondary menu item - `IMenuItem` - Tab/menu item definition