feat(appui): add dees-appui-bottombar component with config, programmatic API, demo and docs

This commit is contained in:
2026-01-03 12:40:11 +00:00
parent ba4aa912af
commit 9b0b448cb1
10 changed files with 899 additions and 4 deletions

View File

@@ -1,5 +1,14 @@
# Changelog
## 2026-01-03 - 3.30.0 - feat(appui)
add dees-appui-bottombar component with config, programmatic API, demo and docs
- Adds a new dees-appui-bottombar web component (ts_web/elements/00group-appui/dees-appui-bottombar/) implementing widget and action management (add/update/remove/get/clear).
- Introduces bottom bar types and API in ts_web/elements/interfaces/appconfig.ts (IBottomBarWidget, IBottomBarAction, IBottomBarConfig, IBottomBarAPI) and extends the app config/type to include bottomBar and bottomBar APIs.
- Integrates the bottom bar into dees-appui: imports and registers component, renders conditionally, exposes bottomBar proxy API, visibility controls (set/getBottomBarVisible), and wires initial config to populate widgets/actions.
- Updates layout/styles (reduces main grid height to account for 24px fixed bottom bar and adds bottombar-hidden attribute handling) and exports component from the appui index.
- Adds interactive demos (dees-appui-bottombar.demo.ts and integration demo) and documents usage and API in readme.hints.md.
## 2026-01-03 - 3.29.3 - fix(elements/appui)
prevent scroll chaining on app UI components by adding overscroll-behavior: contain

View File

@@ -800,4 +800,114 @@ html`
- **External Router Support**: Integrate with Angular Router or other frameworks
- **State Persistence**: Save/restore collapsed menus, selections, and current view
- **View-specific Menus**: Each view can define its own secondary menu and tabs
- **Full Backward Compatibility**: Existing code continues to work
- **Full Backward Compatibility**: Existing code continues to work
## AppUI Bottom Bar (2026-01-03)
Added a new `dees-appui-bottombar` component similar to `dees-workspace-bottombar`, providing a 24px fixed-height status bar at the bottom of the app shell.
### Features:
- **Generic status widgets**: Configurable widgets with icon, label, status colors, loading spinner
- **App-specific actions**: Quick action buttons with icons and tooltips
- **Always visible**: Fixed 24px height at the bottom of the app
- **Status colors**: idle, active (blue), success (green), warning (yellow), error (red)
- **Context menus**: Widgets can have right-click context menus
### New Interfaces (in `interfaces/appconfig.ts`):
```typescript
interface IBottomBarWidget {
id: string;
iconName?: string;
label?: string;
status?: 'idle' | 'active' | 'success' | 'warning' | 'error';
tooltip?: string;
loading?: boolean;
onClick?: () => void;
contextMenuItems?: IBottomBarContextMenuItem[];
position?: 'left' | 'right';
order?: number;
}
interface IBottomBarAction {
id: string;
iconName: string;
tooltip?: string;
onClick: () => void | Promise<void>;
disabled?: boolean;
position?: 'left' | 'right';
}
interface IBottomBarConfig {
visible?: boolean;
widgets?: IBottomBarWidget[];
actions?: IBottomBarAction[];
}
```
### Usage via configure():
```typescript
const config: IAppConfig = {
// ... other config
bottomBar: {
visible: true,
widgets: [
{
id: 'status',
iconName: 'lucide:activity',
label: 'System Online',
status: 'success',
tooltip: 'All systems operational',
onClick: () => console.log('Status clicked'),
},
{
id: 'notifications',
iconName: 'lucide:bell',
label: '3 notifications',
status: 'warning',
position: 'left',
},
{
id: 'version',
iconName: 'lucide:gitBranch',
label: 'v1.2.3',
position: 'right',
},
],
actions: [
{
id: 'terminal',
iconName: 'lucide:terminal',
tooltip: 'Open Terminal',
position: 'right',
onClick: () => console.log('Terminal clicked'),
},
],
},
};
```
### Programmatic API:
```typescript
// Add/update/remove widgets
appui.bottomBar.addWidget({ id: 'status', ... });
appui.bottomBar.updateWidget('status', { status: 'error', label: 'Error!' });
appui.bottomBar.removeWidget('status');
appui.bottomBar.clearWidgets();
// Add/remove actions
appui.bottomBar.addAction({ id: 'refresh', iconName: 'lucide:refreshCw', ... });
appui.bottomBar.removeAction('refresh');
appui.bottomBar.clearActions();
// Visibility control
appui.setBottomBarVisible(false);
appui.getBottomBarVisible();
```
### Files:
- `ts_web/elements/00group-appui/dees-appui-bottombar/dees-appui-bottombar.ts` - Main component
- `ts_web/elements/00group-appui/dees-appui-bottombar/dees-appui-bottombar.demo.ts` - Demo
- `ts_web/elements/interfaces/appconfig.ts` - New interfaces added

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@design.estate/dees-catalog',
version: '3.29.3',
version: '3.30.0',
description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.'
}

View File

@@ -0,0 +1,210 @@
import { html } from '@design.estate/dees-element';
import type { DeesAppuiBottombar } from './dees-appui-bottombar.js';
import '@design.estate/dees-wcctools/demotools';
export const demoFunc = () => {
return html`
<dees-demowrapper>
<style>
.demo-container {
display: flex;
flex-direction: column;
gap: 24px;
padding: 24px;
background: #1a1a1a;
}
.demo-section {
display: flex;
flex-direction: column;
gap: 8px;
}
.demo-label {
font-size: 12px;
color: #737373;
font-family: 'Geist Sans', sans-serif;
}
.demo-bottombar-wrapper {
border: 1px solid hsl(0 0% 20%);
border-radius: 4px;
overflow: hidden;
}
</style>
<div class="demo-container">
<div class="demo-section">
<div class="demo-label">Bottom bar with status widgets and actions</div>
<div class="demo-bottombar-wrapper">
<dees-appui-bottombar
id="demo-bottombar"
></dees-appui-bottombar>
</div>
</div>
<div class="demo-section">
<div class="demo-label">Controls</div>
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
<button onclick="addSuccessWidget()">Add Success Widget</button>
<button onclick="addWarningWidget()">Add Warning Widget</button>
<button onclick="addErrorWidget()">Add Error Widget</button>
<button onclick="addLoadingWidget()">Add Loading Widget</button>
<button onclick="addRightWidget()">Add Right Widget</button>
<button onclick="addAction()">Add Action</button>
<button onclick="clearAll()">Clear All</button>
</div>
</div>
</div>
<script type="module">
const bottombar = document.getElementById('demo-bottombar');
// Wait for component to initialize
await bottombar.updateComplete;
// Add initial widgets
bottombar.addWidget({
id: 'status',
iconName: 'lucide:activity',
label: 'System Online',
status: 'success',
tooltip: 'All systems operational',
onClick: () => console.log('Status clicked'),
contextMenuItems: [
{ name: 'View Details', iconName: 'lucide:info', action: () => alert('System details') },
{ divider: true },
{ name: 'Refresh Status', iconName: 'lucide:refreshCw', action: () => alert('Refreshing...') },
],
});
bottombar.addWidget({
id: 'notifications',
iconName: 'lucide:bell',
label: '3 notifications',
status: 'warning',
tooltip: 'You have unread notifications',
onClick: () => console.log('Notifications clicked'),
});
bottombar.addWidget({
id: 'version',
iconName: 'lucide:gitBranch',
label: 'v1.2.3',
tooltip: 'Current version',
position: 'right',
onClick: () => console.log('Version clicked'),
});
// Add initial actions
bottombar.addAction({
id: 'settings',
iconName: 'lucide:settings',
tooltip: 'Settings',
position: 'right',
onClick: () => alert('Settings clicked'),
});
bottombar.addAction({
id: 'help',
iconName: 'lucide:helpCircle',
tooltip: 'Help',
position: 'right',
onClick: () => alert('Help clicked'),
});
// Demo control functions
let widgetCounter = 0;
let actionCounter = 0;
window.addSuccessWidget = () => {
widgetCounter++;
bottombar.addWidget({
id: 'success-' + widgetCounter,
iconName: 'lucide:checkCircle',
label: 'Success ' + widgetCounter,
status: 'success',
tooltip: 'Success widget',
onClick: () => bottombar.removeWidget('success-' + widgetCounter),
});
};
window.addWarningWidget = () => {
widgetCounter++;
bottombar.addWidget({
id: 'warning-' + widgetCounter,
iconName: 'lucide:alertTriangle',
label: 'Warning ' + widgetCounter,
status: 'warning',
tooltip: 'Warning widget',
onClick: () => bottombar.removeWidget('warning-' + widgetCounter),
});
};
window.addErrorWidget = () => {
widgetCounter++;
bottombar.addWidget({
id: 'error-' + widgetCounter,
iconName: 'lucide:xCircle',
label: 'Error ' + widgetCounter,
status: 'error',
tooltip: 'Error widget',
onClick: () => bottombar.removeWidget('error-' + widgetCounter),
});
};
window.addLoadingWidget = () => {
widgetCounter++;
const id = 'loading-' + widgetCounter;
bottombar.addWidget({
id: id,
iconName: 'lucide:loader2',
label: 'Loading...',
status: 'active',
loading: true,
tooltip: 'Loading in progress',
});
// Simulate completion after 3 seconds
setTimeout(() => {
bottombar.updateWidget(id, {
iconName: 'lucide:check',
label: 'Done!',
status: 'success',
loading: false,
});
}, 3000);
};
window.addRightWidget = () => {
widgetCounter++;
bottombar.addWidget({
id: 'right-' + widgetCounter,
iconName: 'lucide:info',
label: 'Right ' + widgetCounter,
position: 'right',
onClick: () => bottombar.removeWidget('right-' + widgetCounter),
});
};
window.addAction = () => {
actionCounter++;
bottombar.addAction({
id: 'action-' + actionCounter,
iconName: 'lucide:zap',
tooltip: 'Action ' + actionCounter,
onClick: () => {
alert('Action ' + actionCounter + ' clicked');
bottombar.removeAction('action-' + actionCounter);
},
});
};
window.clearAll = () => {
bottombar.clearWidgets();
bottombar.clearActions();
widgetCounter = 0;
actionCounter = 0;
};
</script>
</dees-demowrapper>
`;
};

View File

@@ -0,0 +1,314 @@
import {
DeesElement,
type TemplateResult,
customElement,
html,
css,
cssManager,
state,
} from '@design.estate/dees-element';
import { themeDefaultStyles } from '../../00theme.js';
import '../../dees-icon/dees-icon.js';
import { DeesContextmenu } from '../../dees-contextmenu/dees-contextmenu.js';
import type {
IBottomBarWidget,
IBottomBarAction,
IBottomBarAPI,
} from '../../interfaces/appconfig.js';
import { demoFunc } from './dees-appui-bottombar.demo.js';
declare global {
interface HTMLElementTagNameMap {
'dees-appui-bottombar': DeesAppuiBottombar;
}
}
@customElement('dees-appui-bottombar')
export class DeesAppuiBottombar extends DeesElement implements IBottomBarAPI {
public static demo = demoFunc;
// INSTANCE PROPERTIES
@state()
accessor widgets: IBottomBarWidget[] = [];
@state()
accessor actions: IBottomBarAction[] = [];
public static styles = [
themeDefaultStyles,
cssManager.defaultStyles,
css`
:host {
display: block;
height: 24px;
flex-shrink: 0;
user-select: none;
}
.bottom-bar {
height: 24px;
display: flex;
align-items: center;
padding: 0 8px;
gap: 4px;
background: ${cssManager.bdTheme('hsl(0 0% 94%)', 'hsl(0 0% 6%)')};
border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 85%)', 'hsl(0 0% 15%)')};
font-size: 11px;
color: ${cssManager.bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 60%)')};
}
.widget {
display: flex;
align-items: center;
gap: 4px;
padding: 2px 6px;
border-radius: 3px;
cursor: pointer;
transition: background 0.15s ease, color 0.15s ease;
white-space: nowrap;
}
.widget:hover {
background: ${cssManager.bdTheme('hsl(0 0% 88%)', 'hsl(0 0% 12%)')};
color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 80%)')};
}
.widget dees-icon {
flex-shrink: 0;
}
.widget-separator {
width: 1px;
height: 14px;
background: ${cssManager.bdTheme('hsl(0 0% 80%)', 'hsl(0 0% 20%)')};
margin: 0 4px;
}
/* Status colors matching dees-workspace-bottombar */
.widget.active {
color: ${cssManager.bdTheme('hsl(210 100% 45%)', 'hsl(210 100% 60%)')};
}
.widget.success {
color: ${cssManager.bdTheme('hsl(142 70% 35%)', 'hsl(142 70% 50%)')};
}
.widget.warning {
color: ${cssManager.bdTheme('hsl(38 92% 45%)', 'hsl(38 92% 55%)')};
}
.widget.error {
color: ${cssManager.bdTheme('hsl(0 70% 50%)', 'hsl(0 70% 60%)')};
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.spinning {
animation: spin 1s linear infinite;
}
.spacer {
flex: 1;
}
.action-button {
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
border-radius: 3px;
cursor: pointer;
transition: background 0.15s ease;
color: ${cssManager.bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 60%)')};
}
.action-button:hover {
background: ${cssManager.bdTheme('hsl(0 0% 88%)', 'hsl(0 0% 12%)')};
color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 80%)')};
}
.action-button.disabled {
opacity: 0.5;
cursor: not-allowed;
}
.action-button.disabled:hover {
background: transparent;
color: ${cssManager.bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 60%)')};
}
`,
];
public render(): TemplateResult {
const leftWidgets = this.widgets
.filter(w => w.position !== 'right')
.sort((a, b) => (a.order || 0) - (b.order || 0));
const rightWidgets = this.widgets
.filter(w => w.position === 'right')
.sort((a, b) => (a.order || 0) - (b.order || 0));
const leftActions = this.actions.filter(a => a.position === 'left');
const rightActions = this.actions.filter(a => a.position !== 'left');
return html`
<div class="bottom-bar">
<!-- Left actions -->
${leftActions.map(action => this.renderAction(action))}
<!-- Left widgets -->
${leftWidgets.map((widget, index) => html`
${index > 0 || leftActions.length > 0 ? html`<div class="widget-separator"></div>` : ''}
${this.renderWidget(widget)}
`)}
<div class="spacer"></div>
<!-- Right widgets -->
${rightWidgets.map((widget, index) => html`
${this.renderWidget(widget)}
${index < rightWidgets.length - 1 || rightActions.length > 0 ? html`<div class="widget-separator"></div>` : ''}
`)}
<!-- Right actions -->
${rightActions.map(action => this.renderAction(action))}
</div>
`;
}
private renderWidget(widget: IBottomBarWidget): TemplateResult {
const statusClass = widget.status && widget.status !== 'idle' ? widget.status : '';
const iconName = widget.iconName
? (widget.iconName.startsWith('lucide:') ? widget.iconName : `lucide:${widget.iconName}`)
: '';
return html`
<div
class="widget ${statusClass}"
title="${widget.tooltip || ''}"
@click=${() => widget.onClick?.()}
@contextmenu=${(e: MouseEvent) => this.handleWidgetContextMenu(e, widget)}
>
${iconName ? html`
<dees-icon
.icon=${iconName}
iconSize="12"
class="${widget.loading ? 'spinning' : ''}"
></dees-icon>
` : ''}
${widget.label ? html`<span>${widget.label}</span>` : ''}
</div>
`;
}
private renderAction(action: IBottomBarAction): TemplateResult {
const iconName = action.iconName.startsWith('lucide:')
? action.iconName
: `lucide:${action.iconName}`;
return html`
<div
class="action-button ${action.disabled ? 'disabled' : ''}"
title="${action.tooltip || ''}"
@click=${() => !action.disabled && action.onClick?.()}
>
<dees-icon
.icon=${iconName}
iconSize="12"
></dees-icon>
</div>
`;
}
private async handleWidgetContextMenu(e: MouseEvent, widget: IBottomBarWidget): Promise<void> {
if (!widget.contextMenuItems || widget.contextMenuItems.length === 0) return;
e.preventDefault();
const menuItems: Parameters<typeof DeesContextmenu.openContextMenuWithOptions>[1] = [];
for (const item of widget.contextMenuItems) {
if (item.divider) {
menuItems.push({ divider: true });
} else {
menuItems.push({
name: item.name,
iconName: item.iconName,
action: async () => { await item.action(); },
disabled: item.disabled,
});
}
}
await DeesContextmenu.openContextMenuWithOptions(e, menuItems);
}
// ==========================================
// API METHODS (implements IBottomBarAPI)
// ==========================================
/**
* Add a widget to the bottom bar
*/
public addWidget(widget: IBottomBarWidget): void {
// Remove existing widget with same ID if present
this.widgets = this.widgets.filter(w => w.id !== widget.id);
this.widgets = [...this.widgets, widget];
}
/**
* Update an existing widget by ID
*/
public updateWidget(id: string, update: Partial<IBottomBarWidget>): void {
this.widgets = this.widgets.map(w =>
w.id === id ? { ...w, ...update } : w
);
}
/**
* Remove a widget by ID
*/
public removeWidget(id: string): void {
this.widgets = this.widgets.filter(w => w.id !== id);
}
/**
* Get a widget by ID
*/
public getWidget(id: string): IBottomBarWidget | undefined {
return this.widgets.find(w => w.id === id);
}
/**
* Clear all widgets
*/
public clearWidgets(): void {
this.widgets = [];
}
/**
* Add an action button
*/
public addAction(action: IBottomBarAction): void {
this.actions = this.actions.filter(a => a.id !== action.id);
this.actions = [...this.actions, action];
}
/**
* Remove an action by ID
*/
public removeAction(id: string): void {
this.actions = this.actions.filter(a => a.id !== id);
}
/**
* Clear all actions
*/
public clearActions(): void {
this.actions = [];
}
}

View File

@@ -0,0 +1 @@
export * from './dees-appui-bottombar.js';

View File

@@ -663,6 +663,44 @@ export const demoFunc = () => {
defaultView: 'dashboard',
bottomBar: {
visible: true,
widgets: [
{
id: 'status',
iconName: 'lucide:activity',
label: 'System Online',
status: 'success',
tooltip: 'All systems operational',
onClick: () => console.log('Status clicked'),
},
{
id: 'notifications',
iconName: 'lucide:bell',
label: '3 notifications',
status: 'warning',
tooltip: 'You have unread notifications',
onClick: () => console.log('Notifications clicked'),
},
{
id: 'version',
iconName: 'lucide:gitBranch',
label: 'v1.2.3',
position: 'right',
tooltip: 'Current version',
},
],
actions: [
{
id: 'terminal',
iconName: 'lucide:terminal',
tooltip: 'Open Terminal',
position: 'right',
onClick: () => console.log('Terminal clicked'),
},
],
},
onViewChange: (viewId, view) => {
console.log(`View changed to: ${viewId} (${view.name})`);
},

View File

@@ -15,6 +15,7 @@ import type { DeesAppuiMainmenu } from '../dees-appui-mainmenu/dees-appui-mainme
import type { DeesAppuiSecondarymenu } from '../dees-appui-secondarymenu/dees-appui-secondarymenu.js';
import type { DeesAppuiMaincontent } from '../dees-appui-maincontent/dees-appui-maincontent.js';
import type { DeesAppuiActivitylog } from '../dees-appui-activitylog/dees-appui-activitylog.js';
import type { DeesAppuiBottombar } from '../dees-appui-bottombar/dees-appui-bottombar.js';
import { demoFunc } from './dees-appui.demo.js';
import { themeDefaultStyles } from '../../00theme.js';
@@ -23,6 +24,7 @@ import { ViewRegistry } from './view.registry.js';
// Import child components
import '../dees-appui-appbar/index.js';
import '../dees-appui-bottombar/dees-appui-bottombar.js';
import '../dees-appui-mainmenu/dees-appui-mainmenu.js';
import '../dees-appui-secondarymenu/dees-appui-secondarymenu.js';
import '../dees-appui-maincontent/dees-appui-maincontent.js';
@@ -156,6 +158,12 @@ export class DeesAppui extends DeesElement {
@state()
accessor activitylogElement: DeesAppuiActivitylog | undefined = undefined;
@state()
accessor bottombarElement: DeesAppuiBottombar | undefined = undefined;
@state()
accessor bottombarVisible: boolean = true;
// Current view state
@state()
accessor currentView: interfaces.IViewDefinition | undefined = undefined;
@@ -179,15 +187,27 @@ export class DeesAppui extends DeesElement {
.maingrid {
position: absolute;
top: 40px;
height: calc(100% - 40px);
height: calc(100% - 40px - 24px);
width: 100%;
display: grid;
/* grid-template-columns set dynamically in template */
grid-template-rows: 1fr;
transition: grid-template-columns 0.3s ease;
transition: grid-template-columns 0.3s ease, height 0.3s ease;
overflow: hidden;
}
:host([bottombar-hidden]) .maingrid {
height: calc(100% - 40px);
}
dees-appui-bottombar {
position: absolute;
bottom: 0;
left: 0;
right: 0;
z-index: 4;
}
/* Z-index layering for proper stacking */
.maingrid > dees-appui-mainmenu {
position: relative;
@@ -295,6 +315,9 @@ export class DeesAppui extends DeesElement {
class="${this.activityLogVisible ? 'visible' : 'hidden'}"
></dees-appui-activitylog>
</div>
${this.bottombarVisible ? html`
<dees-appui-bottombar></dees-appui-bottombar>
` : ''}
`;
}
@@ -305,6 +328,7 @@ export class DeesAppui extends DeesElement {
this.secondarymenu = this.shadowRoot!.querySelector('dees-appui-secondarymenu') as DeesAppuiSecondarymenu;
this.maincontent = this.shadowRoot!.querySelector('dees-appui-maincontent') as DeesAppuiMaincontent;
this.activitylogElement = this.shadowRoot!.querySelector('dees-appui-activitylog') as DeesAppuiActivitylog;
this.bottombarElement = this.shadowRoot!.querySelector('dees-appui-bottombar') as DeesAppuiBottombar;
// Subscribe to activity log entry changes for badge count
if (this.activitylogElement) {
@@ -730,6 +754,72 @@ export class DeesAppui extends DeesElement {
return this.activityLogVisible;
}
// ==========================================
// PROGRAMMATIC API: BOTTOM BAR
// ==========================================
/**
* Get the bottom bar API for widget/action management
*/
public get bottomBar(): interfaces.IBottomBarAPI {
if (!this.bottombarElement) {
// Return a deferred API that will work after firstUpdated
return {
addWidget: (widget) => {
this.updateComplete.then(() => this.bottombarElement?.addWidget(widget));
},
updateWidget: (id, update) => {
this.updateComplete.then(() => this.bottombarElement?.updateWidget(id, update));
},
removeWidget: (id) => {
this.updateComplete.then(() => this.bottombarElement?.removeWidget(id));
},
getWidget: (id) => this.bottombarElement?.getWidget(id),
clearWidgets: () => {
this.updateComplete.then(() => this.bottombarElement?.clearWidgets());
},
addAction: (action) => {
this.updateComplete.then(() => this.bottombarElement?.addAction(action));
},
removeAction: (id) => {
this.updateComplete.then(() => this.bottombarElement?.removeAction(id));
},
clearActions: () => {
this.updateComplete.then(() => this.bottombarElement?.clearActions());
},
};
}
return {
addWidget: (widget) => this.bottombarElement!.addWidget(widget),
updateWidget: (id, update) => this.bottombarElement!.updateWidget(id, update),
removeWidget: (id) => this.bottombarElement!.removeWidget(id),
getWidget: (id) => this.bottombarElement!.getWidget(id),
clearWidgets: () => this.bottombarElement!.clearWidgets(),
addAction: (action) => this.bottombarElement!.addAction(action),
removeAction: (id) => this.bottombarElement!.removeAction(id),
clearActions: () => this.bottombarElement!.clearActions(),
};
}
/**
* Set bottom bar visibility
*/
public setBottomBarVisible(visible: boolean): void {
this.bottombarVisible = visible;
if (!visible) {
this.setAttribute('bottombar-hidden', '');
} else {
this.removeAttribute('bottombar-hidden');
}
}
/**
* Get bottom bar visibility state
*/
public getBottomBarVisible(): boolean {
return this.bottombarVisible;
}
// ==========================================
// PROGRAMMATIC API: NAVIGATION
// ==========================================
@@ -842,6 +932,23 @@ export class DeesAppui extends DeesElement {
}
}
// Apply bottom bar config
if (config.bottomBar) {
this.setBottomBarVisible(config.bottomBar.visible ?? true);
if (config.bottomBar.widgets) {
config.bottomBar.widgets.forEach(widget => {
this.bottomBar.addWidget(widget);
});
}
if (config.bottomBar.actions) {
config.bottomBar.actions.forEach(action => {
this.bottomBar.addAction(action);
});
}
}
// Setup domtools.router integration
this.setupRouterIntegration(config);

View File

@@ -1,6 +1,7 @@
// App UI Components
export * from './dees-appui-activitylog/index.js';
export * from './dees-appui-appbar/index.js';
export * from './dees-appui-bottombar/index.js';
export * from './dees-appui/index.js';
export * from './dees-appui-maincontent/index.js';
export * from './dees-appui-mainmenu/index.js';

View File

@@ -4,6 +4,104 @@ import type { IMenuItem } from './tab.js';
import type { IMenuGroup } from './menugroup.js';
import type { ISecondaryMenuGroup, ISecondaryMenuItem } from './secondarymenu.js';
// ==========================================
// BOTTOM BAR INTERFACES
// ==========================================
/**
* Bottom bar widget status for styling
*/
export type TBottomBarWidgetStatus = 'idle' | 'active' | 'success' | 'warning' | 'error';
/**
* Generic status widget for the bottom bar
*/
export interface IBottomBarWidget {
/** Unique identifier for the widget */
id: string;
/** Icon to display (lucide icon name) */
iconName?: string;
/** Text label to display */
label?: string;
/** Status affects styling (colors) */
status?: TBottomBarWidgetStatus;
/** Tooltip text */
tooltip?: string;
/** Whether the widget shows a loading spinner */
loading?: boolean;
/** Click handler for the widget */
onClick?: () => void;
/** Optional context menu items on right-click */
contextMenuItems?: IBottomBarContextMenuItem[];
/** Position: 'left' (default) or 'right' */
position?: 'left' | 'right';
/** Order within position group (lower = earlier) */
order?: number;
}
/**
* Context menu item for bottom bar widgets
*/
export interface IBottomBarContextMenuItem {
name: string;
iconName?: string;
action: () => void | Promise<void>;
disabled?: boolean;
divider?: boolean;
}
/**
* Bottom bar action (quick action button)
*/
export interface IBottomBarAction {
/** Unique identifier */
id: string;
/** Icon to display */
iconName: string;
/** Tooltip */
tooltip?: string;
/** Click handler */
onClick: () => void | Promise<void>;
/** Whether action is disabled */
disabled?: boolean;
/** Position: 'left' or 'right' (default) */
position?: 'left' | 'right';
}
/**
* Bottom bar configuration
*/
export interface IBottomBarConfig {
/** Whether bottom bar is visible */
visible?: boolean;
/** Initial widgets */
widgets?: IBottomBarWidget[];
/** Initial actions */
actions?: IBottomBarAction[];
}
/**
* Bottom bar programmatic API
*/
export interface IBottomBarAPI {
/** Add a widget */
addWidget: (widget: IBottomBarWidget) => void;
/** Update an existing widget by ID */
updateWidget: (id: string, update: Partial<IBottomBarWidget>) => void;
/** Remove a widget by ID */
removeWidget: (id: string) => void;
/** Get a widget by ID */
getWidget: (id: string) => IBottomBarWidget | undefined;
/** Clear all widgets */
clearWidgets: () => void;
/** Add an action button */
addAction: (action: IBottomBarAction) => void;
/** Remove an action by ID */
removeAction: (id: string) => void;
/** Clear all actions */
clearActions: () => void;
}
// Forward declaration for circular reference
export type TDeesAppui = HTMLElement & {
setAppBarMenus: (menus: IAppBarMenuItem[]) => void;
@@ -42,6 +140,10 @@ export type TDeesAppui = HTMLElement & {
getActivityLogVisible: () => boolean;
navigateToView: (viewId: string, params?: Record<string, string>) => Promise<boolean>;
getCurrentView: () => IViewDefinition | undefined;
// Bottom bar
bottomBar: IBottomBarAPI;
setBottomBarVisible: (visible: boolean) => void;
getBottomBarVisible: () => boolean;
};
/**
@@ -233,6 +335,9 @@ export interface IAppConfig {
/** Activity log configuration */
activityLog?: IActivityLogConfig;
/** Bottom bar configuration */
bottomBar?: IBottomBarConfig;
/** Event callbacks */
onViewChange?: (viewId: string, view: IViewDefinition) => void;
onSearch?: (query: string) => void;