316 lines
8.3 KiB
TypeScript
316 lines
8.3 KiB
TypeScript
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;
|
|
public static demoGroup = 'App UI';
|
|
|
|
// 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 = [];
|
|
}
|
|
}
|