561 lines
14 KiB
Markdown
561 lines
14 KiB
Markdown
|
|
# DeesAppuiBase
|
||
|
|
|
||
|
|
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 { 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.
|
||
|
|
|
||
|
|
```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');
|
||
|
|
appui.setMainMenuCollapsed(true);
|
||
|
|
|
||
|
|
// Badges
|
||
|
|
appui.setMainMenuBadge('inbox', 12);
|
||
|
|
appui.clearMainMenuBadge('inbox');
|
||
|
|
```
|
||
|
|
|
||
|
|
### Secondary Menu API
|
||
|
|
|
||
|
|
Views can control the secondary (contextual) menu.
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// 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.
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// 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.
|
||
|
|
|
||
|
|
```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
|
||
|
|
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.
|
||
|
|
|
||
|
|
```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: 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`.
|
||
|
|
|
||
|
|
```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<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
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
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
|