This commit is contained in:
retoor 2026-01-03 13:42:23 +01:00
parent da30590080
commit 1bc7bbe81e

View File

@ -0,0 +1,235 @@
// retoor <retoor@molodetz.nl>
class ToolbarMenu extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this._isOpen = false;
this._sttButton = null;
this._ttsButton = null;
this._uploadButton = null;
this._observer = null;
this._boundClickOutside = this._handleClickOutside.bind(this);
const style = document.createElement('style');
style.textContent = `
:host {
display: inline-block;
position: relative;
}
.toolbar-container {
position: relative;
}
.menu-toggle {
display: flex;
align-items: center;
justify-content: center;
padding: 10px 20px;
margin-left: 10px;
background-color: #000000;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 18px;
position: relative;
overflow: visible;
transition: background-color 0.2s ease, transform 0.1s ease;
}
.menu-toggle:hover {
background-color: #222222;
}
.menu-toggle:active {
transform: scale(0.95);
}
:host([open]) .menu-toggle {
background-color: #222222;
}
.status-indicator {
position: absolute;
top: 4px;
right: 4px;
width: 8px;
height: 8px;
border-radius: 50%;
display: none;
}
.status-indicator.listening {
display: block;
background-color: #d32f2f;
animation: pulse-indicator 1.5s ease-in-out infinite;
}
.status-indicator.tts-enabled {
display: block;
background-color: #2e7d32;
}
@keyframes pulse-indicator {
0%, 100% {
box-shadow: 0 0 0 0 rgba(211, 47, 47, 0.6);
}
50% {
box-shadow: 0 0 0 4px rgba(211, 47, 47, 0);
}
}
.menu-panel {
position: absolute;
bottom: 100%;
right: 0;
margin-bottom: 8px;
background-color: #111111;
border: 1px solid #333333;
border-radius: 8px;
padding: 8px;
display: none;
flex-direction: column;
gap: 8px;
min-width: 60px;
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.4);
z-index: 1000;
}
:host([open]) .menu-panel {
display: flex;
}
.menu-item {
display: flex;
justify-content: center;
}
`;
const container = document.createElement('div');
container.className = 'toolbar-container';
this._menuPanel = document.createElement('div');
this._menuPanel.className = 'menu-panel';
this._toggleButton = document.createElement('button');
this._toggleButton.className = 'menu-toggle';
this._toggleButton.setAttribute('aria-label', 'Toggle toolbar menu');
this._toggleButton.innerHTML = '&#9776;';
this._statusIndicator = document.createElement('span');
this._statusIndicator.className = 'status-indicator';
this._toggleButton.appendChild(this._statusIndicator);
this._toggleButton.addEventListener('click', (e) => {
e.stopPropagation();
this._toggle();
});
container.appendChild(this._menuPanel);
container.appendChild(this._toggleButton);
this.shadowRoot.append(style, container);
}
_toggle() {
if (this._isOpen) {
this._close();
} else {
this._open();
}
}
_open() {
this._isOpen = true;
this.setAttribute('open', '');
setTimeout(() => {
document.addEventListener('click', this._boundClickOutside);
}, 0);
}
_close() {
this._isOpen = false;
this.removeAttribute('open');
document.removeEventListener('click', this._boundClickOutside);
}
_handleClickOutside(event) {
const path = event.composedPath();
if (!path.includes(this)) {
this._close();
}
}
_observeButtonStates() {
if (this._observer) {
this._observer.disconnect();
}
this._observer = new MutationObserver(() => {
this._updateStatusIndicator();
});
if (this._sttButton) {
this._observer.observe(this._sttButton, {
attributes: true,
attributeFilter: ['listening']
});
}
if (this._ttsButton) {
this._observer.observe(this._ttsButton, {
attributes: true,
attributeFilter: ['enabled']
});
}
this._updateStatusIndicator();
}
_updateStatusIndicator() {
this._statusIndicator.classList.remove('listening', 'tts-enabled');
if (this._sttButton?.hasAttribute('listening')) {
this._statusIndicator.classList.add('listening');
} else if (this._ttsButton?.hasAttribute('enabled')) {
this._statusIndicator.classList.add('tts-enabled');
}
}
addButton(button, type) {
const wrapper = document.createElement('div');
wrapper.className = 'menu-item';
if (button.style) {
button.style.marginLeft = '0';
}
wrapper.appendChild(button);
this._menuPanel.appendChild(wrapper);
if (type === 'stt') {
this._sttButton = button;
} else if (type === 'tts') {
this._ttsButton = button;
} else if (type === 'upload') {
this._uploadButton = button;
}
this._observeButtonStates();
}
connectedCallback() {
this._observeButtonStates();
}
disconnectedCallback() {
document.removeEventListener('click', this._boundClickOutside);
if (this._observer) {
this._observer.disconnect();
}
}
}
customElements.define('toolbar-menu', ToolbarMenu);