feat(appui): add dees-appui-bottombar component with config, programmatic API, demo and docs
This commit is contained in:
@@ -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>
|
||||
`;
|
||||
};
|
||||
@@ -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 = [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './dees-appui-bottombar.js';
|
||||
Reference in New Issue
Block a user