Added support for Streamdeck Pedal and updated UI to better fit the Packed UI style

This commit is contained in:
2026-02-27 22:47:08 +01:00
committed by erik
parent 5a70f775f1
commit 93faae5cc8
1463 changed files with 306917 additions and 0 deletions

View File

@@ -0,0 +1,184 @@
/**
* PirpCombobox - Tippbare Dropdown-Auswahl als Ersatz für <select>
*
* Verwendung:
* new PirpCombobox(containerEl, accountOptions, {
* placeholder: '--Konto--',
* selectedValue: 'kasse_s|soll||',
* onSelect: function(opt) { ... }
* });
*
* accountOptions: { "Gruppenname": [ {value, label, side, rev_id, exp_id}, ... ], ... }
*/
class PirpCombobox {
constructor(container, options, config) {
this.container = container;
this.options = options; // grouped: { group: [{value, label, side, rev_id, exp_id}] }
this.config = config || {};
this.flatOptions = [];
this.highlighted = -1;
// Flatten options for filtering
for (const [group, opts] of Object.entries(this.options)) {
for (const opt of opts) {
this.flatOptions.push({ group, ...opt });
}
}
this.build();
this.bindEvents();
// Set initial selection
if (this.config.selectedValue) {
const parts = this.config.selectedValue.split('|');
const match = this.flatOptions.find(o =>
o.value === parts[0] && o.side === parts[1] &&
String(o.rev_id) === String(parts[2]) &&
String(o.exp_id) === String(parts[3])
);
if (match) {
this.input.value = match.label;
this.selectedOpt = match;
}
}
}
build() {
this.container.classList.add('pirp-combobox');
this.input = document.createElement('input');
this.input.type = 'text';
this.input.placeholder = this.config.placeholder || '--Konto--';
this.input.autocomplete = 'off';
this.dropdown = document.createElement('div');
this.dropdown.className = 'pirp-combobox-dropdown';
this.dropdown.style.display = 'none';
this.container.appendChild(this.input);
this.container.appendChild(this.dropdown);
this.selectedOpt = null;
}
renderDropdown(filter) {
this.dropdown.innerHTML = '';
this.highlighted = -1;
const f = (filter || '').toLowerCase();
let visibleItems = [];
let lastGroup = null;
for (const opt of this.flatOptions) {
if (f && opt.label.toLowerCase().indexOf(f) === -1) continue;
if (opt.group !== lastGroup) {
lastGroup = opt.group;
const groupEl = document.createElement('div');
groupEl.className = 'pirp-combobox-group';
groupEl.textContent = opt.group;
this.dropdown.appendChild(groupEl);
}
const optEl = document.createElement('div');
optEl.className = 'pirp-combobox-option';
optEl.textContent = opt.label;
optEl.dataset.index = visibleItems.length;
visibleItems.push({ el: optEl, opt });
this.dropdown.appendChild(optEl);
}
this.visibleItems = visibleItems;
}
open() {
this.renderDropdown(this.input.value === (this.selectedOpt ? this.selectedOpt.label : '') ? '' : this.input.value);
this.dropdown.style.display = 'block';
}
close() {
this.dropdown.style.display = 'none';
this.highlighted = -1;
}
select(opt) {
this.selectedOpt = opt;
this.input.value = opt.label;
this.close();
if (this.config.onSelect) {
this.config.onSelect(opt);
}
}
highlightIndex(idx) {
if (!this.visibleItems || !this.visibleItems.length) return;
if (this.highlighted >= 0 && this.highlighted < this.visibleItems.length) {
this.visibleItems[this.highlighted].el.classList.remove('highlighted');
}
this.highlighted = Math.max(0, Math.min(idx, this.visibleItems.length - 1));
this.visibleItems[this.highlighted].el.classList.add('highlighted');
this.visibleItems[this.highlighted].el.scrollIntoView({ block: 'nearest' });
}
bindEvents() {
this.input.addEventListener('focus', () => {
this.open();
});
this.input.addEventListener('input', () => {
this.renderDropdown(this.input.value);
this.dropdown.style.display = 'block';
if (this.visibleItems.length > 0) {
this.highlightIndex(0);
}
});
this.input.addEventListener('keydown', (e) => {
if (this.dropdown.style.display === 'none') {
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
this.open();
e.preventDefault();
}
return;
}
if (e.key === 'ArrowDown') {
e.preventDefault();
this.highlightIndex(this.highlighted + 1);
} else if (e.key === 'ArrowUp') {
e.preventDefault();
this.highlightIndex(this.highlighted - 1);
} else if (e.key === 'Enter') {
e.preventDefault();
if (this.highlighted >= 0 && this.visibleItems[this.highlighted]) {
this.select(this.visibleItems[this.highlighted].opt);
}
} else if (e.key === 'Escape') {
e.preventDefault();
if (this.selectedOpt) {
this.input.value = this.selectedOpt.label;
}
this.close();
}
});
this.input.addEventListener('blur', () => {
// Delay to allow click on option to register
setTimeout(() => {
if (this.selectedOpt) {
this.input.value = this.selectedOpt.label;
}
this.close();
}, 150);
});
// Use mousedown to prevent blur race condition
this.dropdown.addEventListener('mousedown', (e) => {
e.preventDefault(); // Prevent blur
const optEl = e.target.closest('.pirp-combobox-option');
if (optEl) {
const idx = parseInt(optEl.dataset.index);
if (this.visibleItems[idx]) {
this.select(this.visibleItems[idx].opt);
}
}
});
}
}

View File

@@ -0,0 +1,202 @@
// PIRP Command Palette (Ctrl+K / Cmd+K)
(function() {
'use strict';
var overlay = null;
var input = null;
var resultsList = null;
var debounceTimer = null;
var activeIndex = -1;
var currentResults = [];
var typeLabels = {
invoice: 'Rechnungen',
customer: 'Kunden',
expense: 'Ausgaben',
journal: 'Journal'
};
var typeIcons = {
invoice: '\u25B8',
customer: '\u25CF',
expense: '\u25A0',
journal: '\u25C6'
};
function create() {
overlay = document.createElement('div');
overlay.className = 'cmd-palette-overlay hidden';
overlay.addEventListener('click', function(e) {
if (e.target === overlay) close();
});
var modal = document.createElement('div');
modal.className = 'cmd-palette';
input = document.createElement('input');
input.type = 'text';
input.placeholder = 'Suche nach Rechnungen, Kunden, Ausgaben, Journal...';
input.addEventListener('input', function() {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(doSearch, 200);
});
input.addEventListener('keydown', handleKeydown);
resultsList = document.createElement('div');
resultsList.className = 'cmd-palette-results';
modal.appendChild(input);
modal.appendChild(resultsList);
overlay.appendChild(modal);
document.body.appendChild(overlay);
}
function open() {
if (!overlay) create();
overlay.classList.remove('hidden');
input.value = '';
resultsList.innerHTML = '';
activeIndex = -1;
currentResults = [];
setTimeout(function() { input.focus(); }, 10);
}
function close() {
if (overlay) overlay.classList.add('hidden');
}
function isOpen() {
return overlay && !overlay.classList.contains('hidden');
}
function doSearch() {
var q = input.value.trim();
if (q.length < 2) {
resultsList.innerHTML = '';
activeIndex = -1;
currentResults = [];
return;
}
fetch('search_api.php?q=' + encodeURIComponent(q))
.then(function(r) { return r.json(); })
.then(function(data) {
if (!data.ok) return;
currentResults = data.results;
activeIndex = -1;
renderResults(data.results);
})
.catch(function() {});
}
function renderResults(results) {
resultsList.innerHTML = '';
if (!results.length) {
var empty = document.createElement('div');
empty.className = 'cmd-palette-empty';
empty.textContent = 'Keine Ergebnisse';
resultsList.appendChild(empty);
return;
}
// Group by type
var groups = {};
var order = [];
results.forEach(function(r) {
if (!groups[r.type]) {
groups[r.type] = [];
order.push(r.type);
}
groups[r.type].push(r);
});
var idx = 0;
order.forEach(function(type) {
var header = document.createElement('div');
header.className = 'cmd-palette-group';
header.textContent = typeLabels[type] || type;
resultsList.appendChild(header);
groups[type].forEach(function(r) {
var item = document.createElement('a');
item.className = 'cmd-palette-item';
item.href = r.url;
item.setAttribute('data-idx', idx);
var icon = document.createElement('span');
icon.className = 'cmd-palette-icon';
icon.textContent = typeIcons[r.type] || '\u25B8';
item.appendChild(icon);
var text = document.createElement('span');
text.className = 'cmd-palette-text';
var title = document.createElement('span');
title.className = 'cmd-palette-title';
title.textContent = r.title;
text.appendChild(title);
if (r.subtitle) {
var sub = document.createElement('span');
sub.className = 'cmd-palette-subtitle';
sub.textContent = r.subtitle;
text.appendChild(sub);
}
item.appendChild(text);
item.addEventListener('mouseenter', function() {
setActive(parseInt(item.getAttribute('data-idx')));
});
resultsList.appendChild(item);
idx++;
});
});
}
function setActive(idx) {
var items = resultsList.querySelectorAll('.cmd-palette-item');
items.forEach(function(el) { el.classList.remove('active'); });
activeIndex = idx;
if (idx >= 0 && idx < items.length) {
items[idx].classList.add('active');
items[idx].scrollIntoView({ block: 'nearest' });
}
}
function handleKeydown(e) {
var items = resultsList.querySelectorAll('.cmd-palette-item');
var count = items.length;
if (e.key === 'ArrowDown') {
e.preventDefault();
setActive(activeIndex < count - 1 ? activeIndex + 1 : 0);
} else if (e.key === 'ArrowUp') {
e.preventDefault();
setActive(activeIndex > 0 ? activeIndex - 1 : count - 1);
} else if (e.key === 'Enter') {
e.preventDefault();
if (activeIndex >= 0 && activeIndex < count) {
window.location = items[activeIndex].href;
}
} else if (e.key === 'Escape') {
e.preventDefault();
close();
}
}
// Global keyboard listener
document.addEventListener('keydown', function(e) {
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
e.preventDefault();
if (isOpen()) {
close();
} else {
open();
}
}
if (e.key === 'Escape' && isOpen()) {
e.preventDefault();
close();
}
});
})();

1391
pirp/public/assets/style.css Normal file

File diff suppressed because it is too large Load Diff