Files
dees-catalog/ts_web/elements/00group-appui/dees-appui-base
..

DeesAppuiBase

A comprehensive application shell component providing a complete UI framework with navigation, menus, activity logging, and view management.

Quick Start

import { html, DeesElement, customElement } from '@design.estate/dees-element';
import { DeesAppuiBase } from '@design.estate/dees-catalog';

@customElement('my-app')
class MyApp extends DeesElement {
  private appui: DeesAppuiBase;

  async firstUpdated() {
    this.appui = this.shadowRoot.querySelector('dees-appui-base');

    // 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`<dees-appui-base></dees-appui-base>`;
  }
}

Configuration API

configure(config: IAppConfig)

Configure the entire application shell with a single configuration object.

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

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.

// 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.

// 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');
appui.setMainMenuCollapsed(true);

// Badges
appui.setMainMenuBadge('inbox', 12);
appui.clearMainMenuBadge('inbox');

Secondary Menu API

Views can control the secondary (contextual) menu.

// Set menu
appui.setSecondaryMenu({
  heading: 'Settings',
  groups: [
    {
      name: 'Account',
      items: [
        { key: 'profile', iconName: 'lucide:user', action: () => {} },
        { key: 'security', iconName: 'lucide:shield', action: () => {} },
      ]
    }
  ]
});

// Update group
appui.updateSecondaryMenuGroup('Account', { items: newItems });

// Add item
appui.addSecondaryMenuItem('Account', {
  key: 'notifications',
  iconName: 'lucide:bell',
  action: () => {}
});

// Selection
appui.setSecondaryMenuSelection('profile');

// Clear
appui.clearSecondaryMenu();

Content Tabs API

Control tabs in the main content area.

// Set tabs
appui.setContentTabs([
  { key: 'code', iconName: 'lucide:code', action: () => {} },
  { key: 'preview', iconName: 'lucide:eye', action: () => {} }
]);

// Add/remove
appui.addContentTab({ key: 'debug', iconName: 'lucide:bug', action: () => {} });
appui.removeContentTab('debug');

// Select
appui.selectContentTab('preview');

// Get current
const current = appui.getSelectedContentTab();

Activity Log API

Add activity entries to the right-side activity log.

// 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
appui.activityLog.addMany([...entries]);

// Clear
appui.activityLog.clear();

// Query
const entries = appui.activityLog.getEntries();
const filtered = appui.activityLog.filter({ user: 'John', type: 'create' });
const searched = appui.activityLog.search('invoice');

Navigation API

Navigate between views programmatically.

// 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.

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

interface IViewActivationContext {
  appui: DeesAppuiBase;           // Reference to the app shell
  viewId: string;                  // The view ID being activated
  params?: Record<string, string>; // Route parameters
}

Routing

Routes are automatically registered from view definitions using domtools.router.

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.

// 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.

{
  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.

// View lifecycle events
appui.viewLifecycle$.subscribe((event) => {
  // event.type: 'loading' | 'activated' | 'deactivated' | 'loaded' | 'loadError'
  // event.viewId: string
  // event.element?: HTMLElement
  // event.params?: Record<string, string>
  // event.error?: unknown
});

// View change events
appui.viewChanged$.subscribe((event) => {
  // event.viewId: string
  // event.view: IViewDefinition
  // event.previousView?: IViewDefinition
  // event.params?: Record<string, string>
});

Complete Example

import { html, DeesElement, customElement } from '@design.estate/dees-element';
import { DeesAppuiBase, IViewActivationContext } from '@design.estate/dees-catalog';

@customElement('my-app')
class MyApp extends DeesElement {
  private appui: DeesAppuiBase;

  async firstUpdated() {
    this.appui = this.shadowRoot.querySelector('dees-appui-base');

    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`<dees-appui-base></dees-appui-base>`;
  }
}

// View with lifecycle hooks
@customElement('crm-settings')
class CrmSettings extends DeesElement {
  private appui: DeesAppuiBase;

  onActivate(context: IViewActivationContext) {
    this.appui = context.appui;

    // Set secondary menu for settings
    this.appui.setSecondaryMenu({
      heading: 'Settings',
      groups: [
        {
          name: 'Account',
          items: [
            { key: 'profile', iconName: 'lucide:user', action: () => this.showSection('profile') },
            { key: 'security', iconName: 'lucide:shield', action: () => this.showSection('security') }
          ]
        },
        {
          name: 'Preferences',
          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
  • ITab - Tab definition