feat: step 10 done (claimed)

This commit is contained in:
2026-04-29 19:09:54 +02:00
parent dccb6a8786
commit 4ae6c55e83
13 changed files with 1662 additions and 10 deletions

View File

@@ -64,6 +64,35 @@
"translationValidation.revalidate": "Erneut validieren",
"translationValidation.fix": "Probleme beheben",
"translationValidation.toast.fixSuccess": "%{dbRows} DB-Zeilen und %{files} Dateien gelöscht, %{flushed} Übersetzungen auf Datenträger geschrieben",
"menuEditor.tabTitle": "Blog-Menü",
"menuEditor.title": "Blog-Menü-Editor",
"menuEditor.description": "Verwalte die zentrale Blog-Navigationsstruktur und speichere sie in meta/menu.opml.",
"menuEditor.save": "Menü speichern",
"menuEditor.saved": "Blog-Menü gespeichert",
"menuEditor.addEntry": "Eintrag hinzufügen",
"menuEditor.addCategoryArchive": "Kategorie-Archiv hinzufügen",
"menuEditor.addCategoryArchiveShort": "K+",
"menuEditor.addSubmenu": "Untermenü hinzufügen",
"menuEditor.moveUp": "Nach oben",
"menuEditor.moveDown": "Nach unten",
"menuEditor.indent": "Einrücken",
"menuEditor.unindent": "Ausrücken",
"menuEditor.delete": "Löschen",
"menuEditor.newEntryPlaceholder": "Seitentitel oder Untermenü-Bezeichnung eingeben",
"menuEditor.newCategoryPlaceholder": "Kategorienamen eingeben",
"menuEditor.createHint": "Wähle unten eine Seite aus oder drücke Enter, um ein Untermenü zu erstellen",
"menuEditor.dragHandle": "Menüeintrag ziehen",
"menuEditor.empty": "Noch keine Menüeinträge. Füge eine Seite oder ein Untermenü hinzu, um zu beginnen.",
"menuEditor.newPage": "Neue Seite",
"menuEditor.newSubmenu": "Neues Untermenü",
"menuEditor.pagePicker.title": "Seite auswählen",
"menuEditor.pagePicker.empty": "Keine passenden Seiten gefunden.",
"menuEditor.categoryPicker.hint": "Wähle eine vorhandene Kategorie oder drücke Enter, um einen neuen Archiv-Eintrag zu erstellen",
"menuEditor.categoryPicker.empty": "Keine passenden Kategorien gefunden.",
"menuEditor.type.page": "Seite",
"menuEditor.type.home": "Startseite",
"menuEditor.type.submenu": "Untermenü",
"menuEditor.type.categoryArchive": "Kategorie-Archiv",
"chat.newChat": "Neuer Chat",
"chat.setupTitle": "KI-Chat-Einrichtung",
"chat.apiKeyRequiredTitle": "API-Schlüssel erforderlich",

View File

@@ -64,6 +64,35 @@
"translationValidation.revalidate": "Revalidate",
"translationValidation.fix": "Fix Issues",
"translationValidation.toast.fixSuccess": "Deleted %{dbRows} DB rows and %{files} files, flushed %{flushed} translations to disk",
"menuEditor.tabTitle": "Blog Menu",
"menuEditor.title": "Blog Menu Editor",
"menuEditor.description": "Manage the central blog navigation outline and save it to meta/menu.opml.",
"menuEditor.save": "Save Menu",
"menuEditor.saved": "Blog menu saved",
"menuEditor.addEntry": "Add Entry",
"menuEditor.addCategoryArchive": "Add Category Archive",
"menuEditor.addCategoryArchiveShort": "C+",
"menuEditor.addSubmenu": "Add Submenu",
"menuEditor.moveUp": "Move Up",
"menuEditor.moveDown": "Move Down",
"menuEditor.indent": "Indent",
"menuEditor.unindent": "Unindent",
"menuEditor.delete": "Delete",
"menuEditor.newEntryPlaceholder": "Type a page title or submenu label",
"menuEditor.newCategoryPlaceholder": "Type a category name",
"menuEditor.createHint": "Select a page below or press Enter to create a submenu",
"menuEditor.dragHandle": "Drag menu item",
"menuEditor.empty": "No menu entries yet. Add a page or submenu to start.",
"menuEditor.newPage": "New Page",
"menuEditor.newSubmenu": "New Submenu",
"menuEditor.pagePicker.title": "Select Page",
"menuEditor.pagePicker.empty": "No matching pages found.",
"menuEditor.categoryPicker.hint": "Select an existing category or press Enter to create a new archive entry",
"menuEditor.categoryPicker.empty": "No matching categories found.",
"menuEditor.type.page": "Page",
"menuEditor.type.home": "Home",
"menuEditor.type.submenu": "Submenu",
"menuEditor.type.categoryArchive": "Category Archive",
"chat.newChat": "New Chat",
"chat.setupTitle": "AI Chat Setup",
"chat.apiKeyRequiredTitle": "API Key Required",

View File

@@ -64,6 +64,35 @@
"translationValidation.revalidate": "Revalidar",
"translationValidation.fix": "Corregir problemas",
"translationValidation.toast.fixSuccess": "%{dbRows} filas de BD y %{files} archivos eliminados, %{flushed} traducciones escritas a disco",
"menuEditor.tabTitle": "Menú del blog",
"menuEditor.title": "Editor del menú del blog",
"menuEditor.description": "Gestiona la estructura central de navegación del blog y guárdala en meta/menu.opml.",
"menuEditor.save": "Guardar menú",
"menuEditor.saved": "Menú del blog guardado",
"menuEditor.addEntry": "Agregar entrada",
"menuEditor.addCategoryArchive": "Agregar archivo de categoría",
"menuEditor.addCategoryArchiveShort": "C+",
"menuEditor.addSubmenu": "Agregar submenú",
"menuEditor.moveUp": "Subir",
"menuEditor.moveDown": "Bajar",
"menuEditor.indent": "Indentar",
"menuEditor.unindent": "Desindentar",
"menuEditor.delete": "Eliminar",
"menuEditor.newEntryPlaceholder": "Escribe un título de página o una etiqueta de submenú",
"menuEditor.newCategoryPlaceholder": "Escribe un nombre de categoría",
"menuEditor.createHint": "Selecciona una página abajo o pulsa Enter para crear un submenú",
"menuEditor.dragHandle": "Arrastrar elemento del menú",
"menuEditor.empty": "Todavía no hay entradas de menú. Agrega una página o un submenú para empezar.",
"menuEditor.newPage": "Nueva página",
"menuEditor.newSubmenu": "Nuevo submenú",
"menuEditor.pagePicker.title": "Seleccionar página",
"menuEditor.pagePicker.empty": "No se encontraron páginas coincidentes.",
"menuEditor.categoryPicker.hint": "Selecciona una categoría existente o pulsa Enter para crear una nueva entrada de archivo",
"menuEditor.categoryPicker.empty": "No se encontraron categorías coincidentes.",
"menuEditor.type.page": "Página",
"menuEditor.type.home": "Inicio",
"menuEditor.type.submenu": "Submenú",
"menuEditor.type.categoryArchive": "Archivo de categoría",
"chat.newChat": "Nuevo chat",
"chat.setupTitle": "Configuración de chat IA",
"chat.apiKeyRequiredTitle": "Clave API requerida",

View File

@@ -64,6 +64,35 @@
"translationValidation.revalidate": "Revalider",
"translationValidation.fix": "Corriger les problèmes",
"translationValidation.toast.fixSuccess": "%{dbRows} lignes DB et %{files} fichiers supprimés, %{flushed} traductions écrites sur disque",
"menuEditor.tabTitle": "Menu du blog",
"menuEditor.title": "Éditeur du menu du blog",
"menuEditor.description": "Gérez la structure centrale de navigation du blog et enregistrez-la dans meta/menu.opml.",
"menuEditor.save": "Enregistrer le menu",
"menuEditor.saved": "Menu du blog enregistré",
"menuEditor.addEntry": "Ajouter une entrée",
"menuEditor.addCategoryArchive": "Ajouter une archive de catégorie",
"menuEditor.addCategoryArchiveShort": "C+",
"menuEditor.addSubmenu": "Ajouter un sous-menu",
"menuEditor.moveUp": "Monter",
"menuEditor.moveDown": "Descendre",
"menuEditor.indent": "Indenter",
"menuEditor.unindent": "Désindenter",
"menuEditor.delete": "Supprimer",
"menuEditor.newEntryPlaceholder": "Saisissez un titre de page ou un libellé de sous-menu",
"menuEditor.newCategoryPlaceholder": "Saisissez un nom de catégorie",
"menuEditor.createHint": "Sélectionnez une page ci-dessous ou appuyez sur Entrée pour créer un sous-menu",
"menuEditor.dragHandle": "Faire glisser lentrée du menu",
"menuEditor.empty": "Aucune entrée de menu pour le moment. Ajoutez une page ou un sous-menu pour commencer.",
"menuEditor.newPage": "Nouvelle page",
"menuEditor.newSubmenu": "Nouveau sous-menu",
"menuEditor.pagePicker.title": "Sélectionner une page",
"menuEditor.pagePicker.empty": "Aucune page correspondante trouvée.",
"menuEditor.categoryPicker.hint": "Sélectionnez une catégorie existante ou appuyez sur Entrée pour créer une nouvelle entrée darchive",
"menuEditor.categoryPicker.empty": "Aucune catégorie correspondante trouvée.",
"menuEditor.type.page": "Page",
"menuEditor.type.home": "Accueil",
"menuEditor.type.submenu": "Sous-menu",
"menuEditor.type.categoryArchive": "Archive de catégorie",
"chat.newChat": "Nouveau chat",
"chat.welcomeTitle": "Bienvenue dans lassistant IA",
"chat.welcomeDescription": "Je peux vous aider à gérer votre blog avec des visualisations riches. Essayez par exemple :",

View File

@@ -64,6 +64,35 @@
"translationValidation.revalidate": "Rivalidare",
"translationValidation.fix": "Correggi problemi",
"translationValidation.toast.fixSuccess": "%{dbRows} righe DB e %{files} file eliminati, %{flushed} traduzioni scritte su disco",
"menuEditor.tabTitle": "Menu del blog",
"menuEditor.title": "Editor del menu del blog",
"menuEditor.description": "Gestisci la struttura centrale di navigazione del blog e salvala in meta/menu.opml.",
"menuEditor.save": "Salva menu",
"menuEditor.saved": "Menu del blog salvato",
"menuEditor.addEntry": "Aggiungi voce",
"menuEditor.addCategoryArchive": "Aggiungi archivio categoria",
"menuEditor.addCategoryArchiveShort": "C+",
"menuEditor.addSubmenu": "Aggiungi sottomenu",
"menuEditor.moveUp": "Sposta su",
"menuEditor.moveDown": "Sposta giù",
"menuEditor.indent": "Rientra",
"menuEditor.unindent": "Riduci rientro",
"menuEditor.delete": "Elimina",
"menuEditor.newEntryPlaceholder": "Digita un titolo pagina o un'etichetta del sottomenu",
"menuEditor.newCategoryPlaceholder": "Digita un nome categoria",
"menuEditor.createHint": "Seleziona una pagina qui sotto o premi Invio per creare un sottomenu",
"menuEditor.dragHandle": "Trascina voce di menu",
"menuEditor.empty": "Nessuna voce di menu ancora. Aggiungi una pagina o un sottomenu per iniziare.",
"menuEditor.newPage": "Nuova pagina",
"menuEditor.newSubmenu": "Nuovo sottomenu",
"menuEditor.pagePicker.title": "Seleziona pagina",
"menuEditor.pagePicker.empty": "Nessuna pagina corrispondente trovata.",
"menuEditor.categoryPicker.hint": "Seleziona una categoria esistente o premi Invio per creare una nuova voce di archivio",
"menuEditor.categoryPicker.empty": "Nessuna categoria corrispondente trovata.",
"menuEditor.type.page": "Pagina",
"menuEditor.type.home": "Home",
"menuEditor.type.submenu": "Sottomenu",
"menuEditor.type.categoryArchive": "Archivio categoria",
"chat.newChat": "Nuova chat",
"chat.setupTitle": "Configurazione chat IA",
"chat.apiKeyRequiredTitle": "Chiave API richiesta",

View File

@@ -2550,6 +2550,295 @@ button svg * {
font: inherit;
}
.menu-editor-view {
padding: 1rem;
height: 100%;
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
gap: 0.75rem;
overflow: hidden;
background: var(--vscode-editor-background);
}
.menu-editor-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
}
.menu-editor-header h2 {
margin: 0;
}
.menu-editor-header p {
margin: 0.25rem 0 0;
color: var(--vscode-descriptionForeground);
}
.menu-editor-main {
display: flex;
flex-direction: column;
min-height: 0;
flex: 1;
overflow: hidden;
}
.menu-editor-tree-wrap {
display: flex;
flex-direction: column;
flex: 1;
border: 1px solid var(--vscode-panel-border);
border-radius: 6px;
background: var(--vscode-editor-background);
padding: 0.5rem;
min-height: 0;
}
.menu-editor-toolbar {
display: flex;
align-items: center;
gap: 0.2rem;
margin-bottom: 0.5rem;
padding-bottom: 0.4rem;
border-bottom: 1px solid var(--vscode-panel-border);
}
.menu-editor-tool {
width: 1.8rem;
height: 1.8rem;
display: inline-flex;
align-items: center;
justify-content: center;
border: 1px solid transparent;
border-radius: 4px;
background: transparent;
color: var(--vscode-foreground);
cursor: pointer;
padding: 0;
}
.menu-editor-tool:hover:not(:disabled) {
background: var(--vscode-toolbar-hoverBackground);
border-color: var(--vscode-panel-border);
}
.menu-editor-tool:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.menu-editor-tree-shell {
flex: 1;
min-height: 0;
overflow: auto;
}
.menu-editor-tree-level {
list-style: none;
margin: 0;
padding: 0;
}
.menu-editor-tree-item {
margin: 0;
padding: 0;
}
.menu-editor-row {
--menu-editor-indent: calc(var(--menu-editor-depth) * 1rem);
display: flex;
align-items: flex-start;
gap: 0.5rem;
padding: 0.3rem 0.45rem 0.3rem calc(0.4rem + var(--menu-editor-indent));
border-radius: 4px;
cursor: pointer;
position: relative;
}
.menu-editor-row.is-selected {
background: var(--vscode-list-activeSelectionBackground);
color: var(--vscode-list-activeSelectionForeground);
}
.menu-editor-row.is-dragging {
opacity: 0.45;
}
.menu-editor-row.is-drop-before::before,
.menu-editor-row.is-drop-after::after {
content: "";
position: absolute;
left: calc(0.4rem + var(--menu-editor-indent));
right: 0.45rem;
height: 2px;
background: var(--vscode-focusBorder);
}
.menu-editor-row.is-drop-before::before {
top: 0;
}
.menu-editor-row.is-drop-after::after {
bottom: 0;
}
.menu-editor-row.is-drop-inside {
box-shadow: inset 0 0 0 1px var(--vscode-focusBorder);
background: var(--vscode-list-hoverBackground);
}
.menu-editor-row-handle {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1rem;
min-width: 1rem;
color: var(--vscode-descriptionForeground);
cursor: grab;
user-select: none;
}
.menu-editor-row-handle:active {
cursor: grabbing;
}
.menu-editor-row-kind {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1rem;
min-width: 1rem;
opacity: 0.9;
}
.menu-editor-row-title {
flex: 1;
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.menu-editor-row-title.is-editing {
white-space: normal;
overflow: visible;
text-overflow: clip;
}
.menu-editor-entry-form {
display: block;
}
.menu-editor-inline-input {
width: 100%;
border: 1px solid var(--vscode-focusBorder);
border-radius: 4px;
background: var(--vscode-input-background);
color: var(--vscode-input-foreground);
padding: 0.25rem 0.45rem;
min-height: 1.8rem;
}
.menu-editor-inline-search {
margin-top: 0.5rem;
border-top: 1px solid var(--vscode-panel-border);
padding-top: 0.5rem;
display: flex;
flex-direction: column;
gap: 0.4rem;
max-height: 18rem;
overflow: hidden;
}
.menu-editor-inline-search-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
}
.menu-editor-inline-search-head strong {
display: block;
font-size: 0.8rem;
}
.menu-editor-inline-search-head span {
color: var(--vscode-descriptionForeground);
font-size: 0.75rem;
}
.menu-editor-inline-actions {
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.menu-editor-inline-action {
border: 1px solid var(--vscode-button-border, transparent);
border-radius: 4px;
background: var(--vscode-button-secondaryBackground);
color: var(--vscode-button-secondaryForeground);
padding: 0.2rem 0.5rem;
cursor: pointer;
}
.menu-editor-inline-action:hover {
background: var(--vscode-button-secondaryHoverBackground);
}
.menu-editor-picker-list {
display: flex;
flex-direction: column;
gap: 0.35rem;
max-height: 16rem;
overflow-y: auto;
}
.menu-editor-picker-item {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
border: 1px solid var(--vscode-panel-border);
border-radius: 4px;
background: var(--vscode-input-background);
color: var(--vscode-input-foreground);
padding: 0.45rem 0.55rem;
text-align: left;
cursor: pointer;
}
.menu-editor-picker-item:hover {
border-color: var(--vscode-focusBorder);
background: var(--vscode-list-hoverBackground);
}
.menu-editor-picker-item small,
.menu-editor-picker-state {
color: var(--vscode-descriptionForeground);
}
.menu-editor-empty {
color: var(--vscode-descriptionForeground);
padding: 0.5rem 0.25rem;
}
@media (max-width: 720px) {
.menu-editor-inline-search-head {
flex-direction: column;
align-items: flex-start;
}
.menu-editor-inline-actions {
width: 100%;
justify-content: flex-start;
flex-wrap: wrap;
}
}
[data-testid="media-editor"] {
flex: 1;
display: flex;

View File

@@ -790,6 +790,141 @@ document.addEventListener("DOMContentLoaded", () => {
}
},
MenuEditorTree: {
mounted() {
this.dragItemId = null;
this.dragSourceEl = null;
this.dropTargetEl = null;
this.dropPosition = null;
this.clearDropTarget = () => {
if (this.dropTargetEl) {
this.dropTargetEl.classList.remove("is-drop-before", "is-drop-after", "is-drop-inside");
}
this.dropTargetEl = null;
this.dropPosition = null;
};
this.setDropTarget = (row, position) => {
if (this.dropTargetEl === row && this.dropPosition === position) {
return;
}
this.clearDropTarget();
this.dropTargetEl = row;
this.dropPosition = position;
row.classList.add(`is-drop-${position}`);
};
this.handleDragStart = (event) => {
const handle = event.target.closest("[data-menu-drag-handle='true']");
const row = event.target.closest("[data-menu-item-id]");
if (!handle || !row || !this.el.contains(row)) {
return;
}
this.dragItemId = row.dataset.menuItemId || null;
this.dragSourceEl = row;
row.classList.add("is-dragging");
if (event.dataTransfer) {
event.dataTransfer.effectAllowed = "move";
event.dataTransfer.setData("text/plain", this.dragItemId || "");
}
};
this.handleDragOver = (event) => {
const row = event.target.closest("[data-menu-item-id]");
if (!this.dragItemId || !row || !this.el.contains(row)) {
this.clearDropTarget();
return;
}
const targetItemId = row.dataset.menuItemId || "";
if (!targetItemId || targetItemId === this.dragItemId) {
this.clearDropTarget();
return;
}
event.preventDefault();
const rect = row.getBoundingClientRect();
const offsetY = event.clientY - rect.top;
const allowInside = row.dataset.menuCanDropInside === "true";
const insideBandTop = rect.height * 0.3;
const insideBandBottom = rect.height * 0.7;
const position =
allowInside && offsetY >= insideBandTop && offsetY <= insideBandBottom
? "inside"
: offsetY < rect.height / 2
? "before"
: "after";
this.setDropTarget(row, position);
if (event.dataTransfer) {
event.dataTransfer.dropEffect = "move";
}
};
this.handleDrop = (event) => {
const row = event.target.closest("[data-menu-item-id]");
if (!this.dragItemId || !row || !this.el.contains(row) || !this.dropPosition) {
this.clearDropTarget();
return;
}
event.preventDefault();
this.pushEvent("menu_editor_drop_item", {
drag_item_id: this.dragItemId,
target_item_id: row.dataset.menuItemId,
position: this.dropPosition
});
this.clearDropTarget();
};
this.handleDragLeave = (event) => {
const related = event.relatedTarget;
if (this.dropTargetEl && (!related || !this.dropTargetEl.contains(related))) {
this.clearDropTarget();
}
};
this.handleDragEnd = () => {
if (this.dragSourceEl) {
this.dragSourceEl.classList.remove("is-dragging");
}
this.dragItemId = null;
this.dragSourceEl = null;
this.clearDropTarget();
};
this.el.addEventListener("dragstart", this.handleDragStart);
this.el.addEventListener("dragover", this.handleDragOver);
this.el.addEventListener("drop", this.handleDrop);
this.el.addEventListener("dragleave", this.handleDragLeave);
this.el.addEventListener("dragend", this.handleDragEnd);
},
destroyed() {
this.el.removeEventListener("dragstart", this.handleDragStart);
this.el.removeEventListener("dragover", this.handleDragOver);
this.el.removeEventListener("drop", this.handleDrop);
this.el.removeEventListener("dragleave", this.handleDragLeave);
this.el.removeEventListener("dragend", this.handleDragEnd);
}
},
MonacoEditor: {
mounted() {
this.textarea = document.getElementById(this.el.dataset.monacoInputId) || this.el.querySelector("textarea");