Spracherkennung für: .ts vermutete Sprache: Unknown {[0] [0] [0]} [Methode: Schwerpunktbildung, einfache Gewichte, sechs Dimensionen]
import { html, nothing } from "lit";
import { ref } from "lit/directives/ref.js";
import { t } from "../../i18n/index.ts";
import { SLASH_COMMANDS } from "../chat/slash-commands.ts";
import { icons, type IconName } from "../icons.ts";
import { normalizeLowercaseStringOrEmpty } from "../string-coerce.ts";
type PaletteItem = {
id: string;
label: string;
icon: IconName;
category: "search" | "navigation" | "skills";
action: string;
description?: string;
};
function buildSlashPaletteItems(): PaletteItem[] {
return SLASH_COMMANDS.map((command) => ({
id: `slash:${command.name}`,
label: `/${command.name}`,
icon: command.icon ?? "terminal",
category: "search",
action: `/${command.name}`,
description: command.description,
}));
}
function getPaletteBaseItems(): PaletteItem[] {
return [
{
id: "nav-overview",
label: "Overview",
icon: "barChart",
category: "navigation",
action: "nav:overview",
},
{
id: "nav-sessions",
label: "Sessions",
icon: "fileText",
category: "navigation",
action: "nav:sessions",
},
{
id: "nav-cron",
label: "Scheduled",
icon: "scrollText",
category: "navigation",
action: "nav:cron",
},
{
id: "nav-skills",
label: "Skills",
icon: "zap",
category: "navigation",
action: "nav:skills",
},
{
id: "nav-config",
label: "Settings",
icon: "settings",
category: "navigation",
action: "nav:config",
},
{
id: "nav-agents",
label: "Agents",
icon: "folder",
category: "navigation",
action: "nav:agents",
},
{
id: "skill-shell",
label: "Shell Command",
icon: "monitor",
category: "skills",
action: "/skill shell",
description: "Run shell",
},
{
id: "skill-debug",
label: "Debug Mode",
icon: "bug",
category: "skills",
action: "/verbose full",
description: "Toggle debug",
},
];
}
function getPaletteItemsInternal(): PaletteItem[] {
return [...buildSlashPaletteItems(), ...getPaletteBaseItems()];
}
export function getPaletteItems(): readonly PaletteItem[] {
return getPaletteItemsInternal();
}
export type CommandPaletteProps = {
open: boolean;
query: string;
activeIndex: number;
onToggle: () => void;
onQueryChange: (query: string) => void;
onActiveIndexChange: (index: number) => void;
onNavigate: (tab: string) => void;
onSlashCommand: (command: string) => void;
};
function filteredItems(query: string): PaletteItem[] {
const items = getPaletteItemsInternal();
if (!query) {
return items;
}
const q = normalizeLowercaseStringOrEmpty(query);
return items.filter(
(item) =>
normalizeLowercaseStringOrEmpty(item.label).includes(q) ||
normalizeLowercaseStringOrEmpty(item.description).includes(q),
);
}
function groupItems(items: PaletteItem[]): Array<[string, PaletteItem[]]> {
const map = new Map<string, PaletteItem[]>();
for (const item of items) {
const group = map.get(item.category) ?? [];
group.push(item);
map.set(item.category, group);
}
return [...map.entries()];
}
let previouslyFocused: Element | null = null;
function saveFocus() {
previouslyFocused = document.activeElement;
}
function restoreFocus() {
if (previouslyFocused && previouslyFocused instanceof HTMLElement) {
requestAnimationFrame(() => previouslyFocused && (previouslyFocused as HTMLElement).focus());
}
previouslyFocused = null;
}
function selectItem(item: PaletteItem, props: CommandPaletteProps) {
if (item.action.startsWith("nav:")) {
props.onNavigate(item.action.slice(4));
} else {
props.onSlashCommand(item.action);
}
props.onToggle();
restoreFocus();
}
function scrollActiveIntoView() {
requestAnimationFrame(() => {
const el = document.querySelector(".cmd-palette__item--active");
el?.scrollIntoView({ block: "nearest" });
});
}
function handleKeydown(e: KeyboardEvent, props: CommandPaletteProps) {
const items = filteredItems(props.query);
if (items.length === 0 && (e.key === "ArrowDown" || e.key === "ArrowUp" || e.key === "Enter")) {
return;
}
switch (e.key) {
case "ArrowDown":
e.preventDefault();
props.onActiveIndexChange((props.activeIndex + 1) % items.length);
scrollActiveIntoView();
break;
case "ArrowUp":
e.preventDefault();
props.onActiveIndexChange((props.activeIndex - 1 + items.length) % items.length);
scrollActiveIntoView();
break;
case "Enter":
e.preventDefault();
if (items[props.activeIndex]) {
selectItem(items[props.activeIndex], props);
}
break;
case "Escape":
e.preventDefault();
props.onToggle();
restoreFocus();
break;
}
}
const CATEGORY_LABELS: Record<string, string> = {
search: "Search",
navigation: "Navigation",
skills: "Skills",
};
function focusInput(el: Element | undefined) {
if (el) {
saveFocus();
requestAnimationFrame(() => (el as HTMLInputElement).focus());
}
}
export function renderCommandPalette(props: CommandPaletteProps) {
if (!props.open) {
return nothing;
}
const items = filteredItems(props.query);
const grouped = groupItems(items);
return html`
<div
class="cmd-palette-overlay"
@click=${() => {
props.onToggle();
restoreFocus();
}}
>
<div
class="cmd-palette"
@click=${(e: Event) => e.stopPropagation()}
@keydown=${(e: KeyboardEvent) => handleKeydown(e, props)}
>
<input
${ref(focusInput)}
class="cmd-palette__input"
placeholder="${t("overview.palette.placeholder")}"
.value=${props.query}
@input=${(e: Event) => {
props.onQueryChange((e.target as HTMLInputElement).value);
props.onActiveIndexChange(0);
}}
/>
<div class="cmd-palette__results">
${grouped.length === 0
? html`<div class="cmd-palette__empty">
<span class="nav-item__icon" style="opacity:0.3;width:20px;height:20px"
>${icons.search}</span
>
<span>${t("overview.palette.noResults")}</span>
</div>`
: grouped.map(
([category, groupedItems]) => html`
<div class="cmd-palette__group-label">
${CATEGORY_LABELS[category] ?? category}
</div>
${groupedItems.map((item) => {
const globalIndex = items.indexOf(item);
const isActive = globalIndex === props.activeIndex;
return html`
<div
class="cmd-palette__item ${isActive ? "cmd-palette__item--active" : ""}"
@click=${(e: Event) => {
e.stopPropagation();
selectItem(item, props);
}}
@mouseenter=${() => props.onActiveIndexChange(globalIndex)}
>
<span class="nav-item__icon">${icons[item.icon]}</span>
<span>${item.label}</span>
${item.description
? html`<span class="cmd-palette__item-desc muted"
>${item.description}</span
>`
: nothing}
</div>
`;
})}
`,
)}
</div>
<div class="cmd-palette__footer">
<span><kbd>↑↓</kbd> navigate</span>
<span><kbd>↵</kbd> select</span>
<span><kbd>esc</kbd> close</span>
</div>
</div>
</div>
`;
}
¤ Dauer der Verarbeitung: 0.23 Sekunden
(vorverarbeitet am 2026-04-27)
¤
*© Formatika GbR, Deutschland
|
|