Update.
This commit is contained in:
parent
da30590080
commit
1bc7bbe81e
235
src/snek/static/toolbar-menu.js
Normal file
235
src/snek/static/toolbar-menu.js
Normal 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 = '☰';
|
||||||
|
|
||||||
|
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);
|
||||||
Loading…
Reference in New Issue
Block a user