From 9b0b448cb137d1e0de596af4e5d7ccf723f0d607 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Sat, 3 Jan 2026 12:40:11 +0000 Subject: [PATCH] feat(appui): add dees-appui-bottombar component with config, programmatic API, demo and docs --- changelog.md | 9 + readme.hints.md | 112 ++++++- ts_web/00_commitinfo_data.ts | 2 +- .../dees-appui-bottombar.demo.ts | 210 ++++++++++++ .../dees-appui-bottombar.ts | 314 ++++++++++++++++++ .../dees-appui-bottombar/index.ts | 1 + .../dees-appui/dees-appui.demo.ts | 38 +++ .../00group-appui/dees-appui/dees-appui.ts | 111 ++++++- ts_web/elements/00group-appui/index.ts | 1 + ts_web/elements/interfaces/appconfig.ts | 105 ++++++ 10 files changed, 899 insertions(+), 4 deletions(-) create mode 100644 ts_web/elements/00group-appui/dees-appui-bottombar/dees-appui-bottombar.demo.ts create mode 100644 ts_web/elements/00group-appui/dees-appui-bottombar/dees-appui-bottombar.ts create mode 100644 ts_web/elements/00group-appui/dees-appui-bottombar/index.ts diff --git a/changelog.md b/changelog.md index 471c355..e1e4a37 100644 --- a/changelog.md +++ b/changelog.md @@ -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 diff --git a/readme.hints.md b/readme.hints.md index 2cf9f46..621fdb7 100644 --- a/readme.hints.md +++ b/readme.hints.md @@ -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 \ No newline at end of file +- **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; + 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 \ No newline at end of file diff --git a/ts_web/00_commitinfo_data.ts b/ts_web/00_commitinfo_data.ts index e6e6771..e482ab8 100644 --- a/ts_web/00_commitinfo_data.ts +++ b/ts_web/00_commitinfo_data.ts @@ -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.' } diff --git a/ts_web/elements/00group-appui/dees-appui-bottombar/dees-appui-bottombar.demo.ts b/ts_web/elements/00group-appui/dees-appui-bottombar/dees-appui-bottombar.demo.ts new file mode 100644 index 0000000..6bf0f86 --- /dev/null +++ b/ts_web/elements/00group-appui/dees-appui-bottombar/dees-appui-bottombar.demo.ts @@ -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` + + +
+
+
Bottom bar with status widgets and actions
+
+ +
+
+ +
+
Controls
+
+ + + + + + + +
+
+
+ +
+ `; +}; diff --git a/ts_web/elements/00group-appui/dees-appui-bottombar/dees-appui-bottombar.ts b/ts_web/elements/00group-appui/dees-appui-bottombar/dees-appui-bottombar.ts new file mode 100644 index 0000000..f4e07e3 --- /dev/null +++ b/ts_web/elements/00group-appui/dees-appui-bottombar/dees-appui-bottombar.ts @@ -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` +
+ + ${leftActions.map(action => this.renderAction(action))} + + + ${leftWidgets.map((widget, index) => html` + ${index > 0 || leftActions.length > 0 ? html`
` : ''} + ${this.renderWidget(widget)} + `)} + +
+ + + ${rightWidgets.map((widget, index) => html` + ${this.renderWidget(widget)} + ${index < rightWidgets.length - 1 || rightActions.length > 0 ? html`
` : ''} + `)} + + + ${rightActions.map(action => this.renderAction(action))} +
+ `; + } + + 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` +
widget.onClick?.()} + @contextmenu=${(e: MouseEvent) => this.handleWidgetContextMenu(e, widget)} + > + ${iconName ? html` + + ` : ''} + ${widget.label ? html`${widget.label}` : ''} +
+ `; + } + + private renderAction(action: IBottomBarAction): TemplateResult { + const iconName = action.iconName.startsWith('lucide:') + ? action.iconName + : `lucide:${action.iconName}`; + + return html` +
!action.disabled && action.onClick?.()} + > + +
+ `; + } + + private async handleWidgetContextMenu(e: MouseEvent, widget: IBottomBarWidget): Promise { + if (!widget.contextMenuItems || widget.contextMenuItems.length === 0) return; + + e.preventDefault(); + + const menuItems: Parameters[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): 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 = []; + } +} diff --git a/ts_web/elements/00group-appui/dees-appui-bottombar/index.ts b/ts_web/elements/00group-appui/dees-appui-bottombar/index.ts new file mode 100644 index 0000000..490d919 --- /dev/null +++ b/ts_web/elements/00group-appui/dees-appui-bottombar/index.ts @@ -0,0 +1 @@ +export * from './dees-appui-bottombar.js'; diff --git a/ts_web/elements/00group-appui/dees-appui/dees-appui.demo.ts b/ts_web/elements/00group-appui/dees-appui/dees-appui.demo.ts index 36d2d87..a4805de 100644 --- a/ts_web/elements/00group-appui/dees-appui/dees-appui.demo.ts +++ b/ts_web/elements/00group-appui/dees-appui/dees-appui.demo.ts @@ -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})`); }, diff --git a/ts_web/elements/00group-appui/dees-appui/dees-appui.ts b/ts_web/elements/00group-appui/dees-appui/dees-appui.ts index 7eeb4b7..47caeca 100644 --- a/ts_web/elements/00group-appui/dees-appui/dees-appui.ts +++ b/ts_web/elements/00group-appui/dees-appui/dees-appui.ts @@ -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'}" > + ${this.bottombarVisible ? html` + + ` : ''} `; } @@ -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); diff --git a/ts_web/elements/00group-appui/index.ts b/ts_web/elements/00group-appui/index.ts index 274dcc6..ea656f4 100644 --- a/ts_web/elements/00group-appui/index.ts +++ b/ts_web/elements/00group-appui/index.ts @@ -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'; diff --git a/ts_web/elements/interfaces/appconfig.ts b/ts_web/elements/interfaces/appconfig.ts index c2973e1..94ee00e 100644 --- a/ts_web/elements/interfaces/appconfig.ts +++ b/ts_web/elements/interfaces/appconfig.ts @@ -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; + 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; + /** 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) => 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) => Promise; 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;