From a723f1ea0f8fcdf9fd7b1bfc74a9b4d212cbf445 Mon Sep 17 00:00:00 2001 From: Marek Date: Thu, 30 Apr 2026 21:39:58 +0200 Subject: [PATCH] update --- doc/{bereiche => allgemein}/allgemein.md | 0 doc/{bereiche => allgemein}/area.md | 0 doc/{bereiche => allgemein}/core.md | 0 doc/{bereiche => allgemein}/details.md | 0 doc/{bereiche => allgemein}/feature.md | 0 doc/{bereiche => allgemein}/overview.md | 0 doc/{bereiche => allgemein}/project.md | 0 doc/{bereiche => allgemein}/story.md | 0 doc/{bereiche => allgemein}/target.md | 0 doc/{bereiche => test}/projects.md | 0 src/views/OverviewView.ts | 155 ++++++++++++++++------- src/views/ProjectsView.ts | 41 ++++-- styles.css | 31 +++++ 13 files changed, 169 insertions(+), 58 deletions(-) rename doc/{bereiche => allgemein}/allgemein.md (100%) rename doc/{bereiche => allgemein}/area.md (100%) rename doc/{bereiche => allgemein}/core.md (100%) rename doc/{bereiche => allgemein}/details.md (100%) rename doc/{bereiche => allgemein}/feature.md (100%) rename doc/{bereiche => allgemein}/overview.md (100%) rename doc/{bereiche => allgemein}/project.md (100%) rename doc/{bereiche => allgemein}/story.md (100%) rename doc/{bereiche => allgemein}/target.md (100%) rename doc/{bereiche => test}/projects.md (100%) diff --git a/doc/bereiche/allgemein.md b/doc/allgemein/allgemein.md similarity index 100% rename from doc/bereiche/allgemein.md rename to doc/allgemein/allgemein.md diff --git a/doc/bereiche/area.md b/doc/allgemein/area.md similarity index 100% rename from doc/bereiche/area.md rename to doc/allgemein/area.md diff --git a/doc/bereiche/core.md b/doc/allgemein/core.md similarity index 100% rename from doc/bereiche/core.md rename to doc/allgemein/core.md diff --git a/doc/bereiche/details.md b/doc/allgemein/details.md similarity index 100% rename from doc/bereiche/details.md rename to doc/allgemein/details.md diff --git a/doc/bereiche/feature.md b/doc/allgemein/feature.md similarity index 100% rename from doc/bereiche/feature.md rename to doc/allgemein/feature.md diff --git a/doc/bereiche/overview.md b/doc/allgemein/overview.md similarity index 100% rename from doc/bereiche/overview.md rename to doc/allgemein/overview.md diff --git a/doc/bereiche/project.md b/doc/allgemein/project.md similarity index 100% rename from doc/bereiche/project.md rename to doc/allgemein/project.md diff --git a/doc/bereiche/story.md b/doc/allgemein/story.md similarity index 100% rename from doc/bereiche/story.md rename to doc/allgemein/story.md diff --git a/doc/bereiche/target.md b/doc/allgemein/target.md similarity index 100% rename from doc/bereiche/target.md rename to doc/allgemein/target.md diff --git a/doc/bereiche/projects.md b/doc/test/projects.md similarity index 100% rename from doc/bereiche/projects.md rename to doc/test/projects.md diff --git a/src/views/OverviewView.ts b/src/views/OverviewView.ts index 6b4a6ff..0deb24a 100644 --- a/src/views/OverviewView.ts +++ b/src/views/OverviewView.ts @@ -1,4 +1,4 @@ -import { ItemView, WorkspaceLeaf, ViewStateResult, normalizePath } from "obsidian"; +import { ItemView, Notice, WorkspaceLeaf, ViewStateResult, normalizePath } from "obsidian"; import { VIEW_TYPE_OVERVIEW, VIEW_TYPE_DETAILS, @@ -17,7 +17,7 @@ import { createArea, createFeature, } from "../fs"; -import { card, menu, breadcrumb, emptyState, openMarkdown } from "../ui"; +import { menu, breadcrumb, emptyState, openMarkdown } from "../ui"; import { NameModal } from "../modals/NameModal"; import { ConfirmModal } from "../modals/ConfirmModal"; @@ -26,6 +26,7 @@ export interface OverviewState extends Record { } const NAME_RX = /^[^\\/:*?"<>|]+$/; +const FEATURE_DND_MIME = "application/x-pk-feature"; function validateName(name: string, taken: string[], current?: string): string | null { if (!name) return "Name darf nicht leer sein."; @@ -108,22 +109,24 @@ export class OverviewView extends ItemView { ]); const stack = root.createDiv({ cls: "pk-stack" }); - this.renderInfoCard(stack, "Core", core, corePath); - this.renderInfoCard(stack, "Target", target, targetPath); + this.renderInfoCard(stack, core, corePath); + this.renderInfoCard(stack, target, targetPath); this.renderStoryCard(stack, story, storyPath); this.renderAreas(root); } - private renderInfoCard(parent: HTMLElement, title: string, content: string, path: string): void { - card(parent, { - title, - body: content || "(leer)", - onClick: () => - openMarkdown(this.app, path, { - type: VIEW_TYPE_OVERVIEW, - state: { project: this.project }, - }), + private renderInfoCard(parent: HTMLElement, content: string, path: string): void { + const btn = parent.createDiv({ + cls: "pk-btn-card", + attr: { role: "button", tabindex: "0" }, }); + btn.setText(content || "(leer)"); + btn.addEventListener("click", () => + openMarkdown(this.app, path, { + type: VIEW_TYPE_OVERVIEW, + state: { project: this.project }, + }), + ); } private renderStoryCard(parent: HTMLElement, content: string, path: string): void { @@ -170,44 +173,102 @@ export class OverviewView extends ItemView { areaPath(this.project, area.name), [], ); - card(stack, { - title: area.name, - cls: "pk-area-card", - onClick: () => this.openDetails(area.name), - onContextMenu: (ev) => - menu(ev, [ - { title: "Neues Feature", icon: "plus", onClick: () => this.openCreateFeature(area.name) }, - { title: "Area umbenennen", icon: "pencil", onClick: () => this.openRenameArea(area.name, takenAreas) }, - { title: "Area löschen", icon: "trash", onClick: () => this.openDeleteArea(area.name) }, - ]), - body: (body) => { - if (features.length === 0) { - body.createDiv({ cls: "pk-empty", text: "Keine Features." }); - return; - } - const list = body.createDiv({ cls: "pk-features" }); - for (const f of features) { - const chip = list.createDiv({ cls: "pk-feature-chip", text: f.basename }); - chip.addEventListener("click", (ev) => { - ev.stopPropagation(); - openMarkdown(this.app, f.path, { - type: VIEW_TYPE_OVERVIEW, - state: { project: this.project }, - }); - }); - chip.addEventListener("contextmenu", (ev) => { - ev.preventDefault(); - ev.stopPropagation(); - menu(ev, [ - { title: "Feature löschen", icon: "trash", onClick: () => this.openDeleteFeature(area.name, f.basename) }, - ]); - }); - } - }, + const areaCard = stack.createDiv({ + cls: "pk-btn-card pk-area-card", + attr: { role: "button", tabindex: "0" }, }); + areaCard.createEl("strong", { text: area.name }); + areaCard.addEventListener("click", () => this.openDetails(area.name)); + areaCard.addEventListener("contextmenu", (ev) => { + ev.preventDefault(); + ev.stopPropagation(); + menu(ev, [ + { title: "Neues Feature", icon: "plus", onClick: () => this.openCreateFeature(area.name) }, + { title: "Area umbenennen", icon: "pencil", onClick: () => this.openRenameArea(area.name, takenAreas) }, + { title: "Area löschen", icon: "trash", onClick: () => this.openDeleteArea(area.name) }, + ]); + }); + areaCard.addEventListener("dragover", (ev) => { + if (!ev.dataTransfer?.types.includes(FEATURE_DND_MIME)) return; + ev.preventDefault(); + ev.dataTransfer.dropEffect = "move"; + areaCard.addClass("pk-drop-target"); + }); + areaCard.addEventListener("dragleave", (ev) => { + const next = ev.relatedTarget as Node | null; + if (next && areaCard.contains(next)) return; + areaCard.removeClass("pk-drop-target"); + }); + areaCard.addEventListener("drop", async (ev) => { + areaCard.removeClass("pk-drop-target"); + const raw = ev.dataTransfer?.getData(FEATURE_DND_MIME); + if (!raw) return; + ev.preventDefault(); + ev.stopPropagation(); + await this.handleFeatureDrop(raw, area.name); + }); + if (features.length === 0) { + areaCard.createDiv({ cls: "pk-empty", text: "Keine Features." }); + } else { + const list = areaCard.createDiv({ cls: "pk-features" }); + for (const f of features) { + const chip = list.createDiv({ cls: "pk-feature-chip", text: f.basename }); + chip.draggable = true; + chip.addEventListener("dragstart", (ev) => { + if (!ev.dataTransfer) return; + ev.dataTransfer.setData( + FEATURE_DND_MIME, + JSON.stringify({ sourceArea: area.name, feature: f.basename }), + ); + ev.dataTransfer.effectAllowed = "move"; + chip.addClass("pk-feature-chip-dragging"); + }); + chip.addEventListener("dragend", () => { + chip.removeClass("pk-feature-chip-dragging"); + }); + chip.addEventListener("click", (ev) => { + ev.stopPropagation(); + openMarkdown(this.app, f.path, { + type: VIEW_TYPE_OVERVIEW, + state: { project: this.project }, + }); + }); + chip.addEventListener("contextmenu", (ev) => { + ev.preventDefault(); + ev.stopPropagation(); + menu(ev, [ + { title: "Feature löschen", icon: "trash", onClick: () => this.openDeleteFeature(area.name, f.basename) }, + ]); + }); + } + } } } + private async handleFeatureDrop(raw: string, targetArea: string): Promise { + let data: { sourceArea: string; feature: string }; + try { + data = JSON.parse(raw); + } catch { + return; + } + if (!data?.sourceArea || !data?.feature) return; + if (data.sourceArea === targetArea) return; + const newPath = featurePath(this.project, targetArea, data.feature); + if (this.app.vault.getAbstractFileByPath(newPath)) { + new Notice(`„${data.feature}" existiert in „${targetArea}" bereits.`); + return; + } + const oldPath = featurePath(this.project, data.sourceArea, data.feature); + try { + await rename(this.app, oldPath, newPath); + } catch (e) { + new Notice(`Verschieben fehlgeschlagen: ${(e as Error).message}`); + return; + } + await this.render(); + } + private openCreateArea(): void { const taken = listFolders(this.app, projectPath(this.project)).map((a) => a.name); new NameModal(this.app, { diff --git a/src/views/ProjectsView.ts b/src/views/ProjectsView.ts index c93d662..db48f59 100644 --- a/src/views/ProjectsView.ts +++ b/src/views/ProjectsView.ts @@ -1,4 +1,4 @@ -import { ItemView, Notice, WorkspaceLeaf } from "obsidian"; +import { ItemView, Notice, WorkspaceLeaf, normalizePath } from "obsidian"; import { VIEW_TYPE_PROJECTS, VIEW_TYPE_OVERVIEW, RIBBON_ICON } from "../const"; import { projectsPath, @@ -8,8 +8,9 @@ import { createProject, rename, deleteRecursive, + readFile, } from "../fs"; -import { card, menu, breadcrumb, emptyState } from "../ui"; +import { menu, breadcrumb, emptyState } from "../ui"; import { NameModal } from "../modals/NameModal"; import { ConfirmModal } from "../modals/ConfirmModal"; @@ -45,6 +46,11 @@ export class ProjectsView extends ItemView { this.registerEvent(this.app.vault.on("create", () => this.render())); this.registerEvent(this.app.vault.on("delete", () => this.render())); this.registerEvent(this.app.vault.on("rename", () => this.render())); + this.registerEvent(this.app.vault.on("modify", (f) => { + if (f.path.endsWith("/core.md") && f.path.startsWith(projectsPath() + "/")) { + this.render(); + } + })); this.registerDomEvent(this.containerEl, "contextmenu", (ev) => { if (ev.defaultPrevented) return; ev.preventDefault(); @@ -70,16 +76,29 @@ export class ProjectsView extends ItemView { } const taken = projects.map((p) => p.name); + const cores = await Promise.all( + projects.map((p) => + readFile(this.app, normalizePath(`${projectPath(p.name)}/core.md`)), + ), + ); + const grid = root.createDiv({ cls: "pk-grid" }); - for (const proj of projects) { - card(grid, { - title: proj.name, - onClick: () => this.openOverview(proj.name), - onContextMenu: (ev) => - menu(ev, [ - { title: "Umbenennen", icon: "pencil", onClick: () => this.openRename(proj.name, taken) }, - { title: "Löschen", icon: "trash", onClick: () => this.openDelete(proj.name) }, - ]), + for (let i = 0; i < projects.length; i++) { + const proj = projects[i]; + const core = cores[i].trim(); + const btn = grid.createDiv({ + cls: "pk-btn-card", + attr: { role: "button", tabindex: "0" }, + }); + btn.createEl("strong", { text: proj.name }); + if (core) btn.createDiv({ text: core }); + btn.addEventListener("click", () => this.openOverview(proj.name)); + btn.addEventListener("contextmenu", (ev) => { + ev.preventDefault(); + menu(ev, [ + { title: "Umbenennen", icon: "pencil", onClick: () => this.openRename(proj.name, taken) }, + { title: "Löschen", icon: "trash", onClick: () => this.openDelete(proj.name) }, + ]); }); } } diff --git a/styles.css b/styles.css index f828b8d..aec5391 100644 --- a/styles.css +++ b/styles.css @@ -136,6 +136,37 @@ font-weight: 400; } +.pk-btn-card { + box-sizing: border-box; + padding: 10px; + background: var(--background-secondary); + border: 1px solid var(--background-modifier-border); + border-radius: 6px; + cursor: pointer; + overflow-wrap: anywhere; + display: flex; + flex-direction: column; + gap: 6px; +} + +.pk-btn-card:hover { + background: var(--background-modifier-hover); +} + +.pk-btn-card strong { + font-weight: 600; +} + + +.pk-drop-target { + outline: 2px dashed var(--text-accent); + outline-offset: -2px; +} + +.pk-feature-chip-dragging { + opacity: 0.4; +} + .pk-areas-section { display: flex; flex-direction: column;