Compare commits

..

2 Commits

Author SHA1 Message Date
Marek
6ea56fb5e0 update 2026-05-01 03:33:43 +02:00
Marek
a723f1ea0f update 2026-04-30 21:39:58 +02:00
23 changed files with 375 additions and 124 deletions

1
doc/_core.md Normal file
View File

@@ -0,0 +1 @@
Obsidian Plugin - Projektkontext managen

24
doc/_project.md Normal file
View File

@@ -0,0 +1,24 @@
**Core**
- Obsidian Plugin - Projektkontext managen
**Target**
- Für große Projekte geht der Kontext schnell verloren.
- Sie werden unübersichtlich und schwer zu managen.
- Das Plugin hilft Projekte kontextbezogen zu strukturieren.
- So lassen sich große Projekte wieder managen.
**Story**
- Großes Projekt soll geplannt werden
- Core definieren (Worum geht es?)
- Target beschreiben (Welches Problem löst es?)
- Story schreiben (Wie wird es verwendet?)
- Areas und Features definieren (Welche Funktionen hat es?)
- Features implementieren
- Projekt v1
**Areas**
- **Core**: Kompakter Titel zum Projekt
- **Target**: Problem -> Folge -> Lösung -> Ergebnis zum Projekt
- **Story**: Kernverwendung vom Projekt
- **Area**: Sammlung von Features
- **Feature**: Abgeschlossene Funktion im Bereich
- **Projects**: Übersicht der Projekte
- **Overview**: Übersicht zu einem Projekt
- **Detail**: Übersicht zu einem Area

7
doc/_story.md Normal file
View File

@@ -0,0 +1,7 @@
- Großes Projekt soll geplannt werden
- Core definieren (Worum geht es?)
- Target beschreiben (Welches Problem löst es?)
- Story schreiben (Wie wird es verwendet?)
- Areas und Features definieren (Welche Funktionen hat es?)
- Features implementieren
- Projekt v1

2
doc/_target.md Normal file
View File

@@ -0,0 +1,2 @@
Für große Projekte geht der Kontext schnell verloren, sie werden unübersichtlich und schwer zu managen.
Das Plugin hilft Projekte kontextbezogen zu strukturieren, so lassen sich große Projekte wieder managen.

View File

@@ -1,3 +1,3 @@
- container für features - container für features
- Auflistung als Columns - Auflistung als Columns
- wird gespeichert als ./{area}/ - wird gespeichert als `./{area}/`

View File

@@ -1,3 +0,0 @@
- Max 64 Zeichen
- wird als ./core.md gespeichert
- beschreibt kompakt was das projekt ist

View File

@@ -1,9 +0,0 @@
- kann ich projekt oder subprojekt zu einem großen projekt sein
- ist abgeschlossen, also hat keine abhängigkeit
- ist der root ordner
- ordnername ist der projektname
kerndatei
- wird als project.md gespeichert
- enthält kern, zweck, stories und kategorien als md dateien
- enthält bereiche als ordner

View File

@@ -1,5 +0,0 @@
- beschreibt wie das fertige projekt in der regel verwendet werden soll
- Max 1024 Zeichen
- wird als ./story.md gespeichert
- nur die hauptanwendung
- abfolge von schritten (start ... ende)

View File

@@ -1,6 +0,0 @@
- beschreibt simpel welches problem das projekt löst
- Was ist das Problem und die Folge davon?
- Wie löst das Projekt das Problem und was ist die Folge davon?
- der zweck muss klar erkennbar sein
- max 255 zeichen
- wird als ./target.md gespeichert

View File

@@ -1 +1,3 @@
Obsidian Plugin - Projektkontext managen - Max 64 Zeichen
- wird als `./_core.md` gespeichert
- beschreibt kompakt was das projekt ist

View File

@@ -5,7 +5,7 @@ layout: features
features features
- alle features zum area als cards - alle features zum area als cards
- LMB auf feature öffnet die {feature}.md - LMB auf feature öffnet die `{feature}.md`
- RMB auf feature öffnet die feature optionen - RMB auf feature öffnet die feature optionen
feature option create feature option create

View File

@@ -1,3 +1,3 @@
- beschreibt eine funktionalität der area - beschreibt eine funktionalität der area
- wie ein git commit - wie ein git commit
- wird gespeichert als ./{area}/{feature}.md - wird gespeichert als `./{area}/{feature}.md`

View File

@@ -1,24 +1,9 @@
**Core** - kann ich projekt oder subprojekt zu einem großen projekt sein
- Obsidian Plugin - Projektkontext managen - ist abgeschlossen, also hat keine abhängigkeit
**Target** - ist der root ordner
- Für große Projekte geht der Kontext schnell verloren. - ordnername ist der projektname
- Sie werden unübersichtlich und schwer zu managen.
- Das Plugin hilft Projekte kontextbezogen zu strukturieren. kerndatei
- So lassen sich große Projekte wieder managen. - wird als project.md gespeichert
**Story** - enthält kern, zweck, stories und kategorien als md dateien
- Großes Projekt soll geplannt werden - enthält bereiche als ordner
- Core definieren (Worum geht es?)
- Target beschreiben (Welches Problem löst es?)
- Story schreiben (Wie wird es verwendet?)
- Areas und Features definieren (Welche Funktionen hat es?)
- Features implementieren
- Projekt v1
**Areas**
- **Core**: Kompakter Titel zum Projekt
- **Target**: Problem -> Folge -> Lösung -> Ergebnis zum Projekt
- **Story**: Kernverwendung vom Projekt
- **Area**: Sammlung von Features
- **Feature**: Abgeschlossene Funktion im Bereich
- **Projects**: Übersicht der Projekte
- **Overview**: Übersicht zu einem Projekt
- **Detail**: Übersicht zu einem Area

View File

@@ -1,7 +1,5 @@
- Großes Projekt soll geplannt werden - beschreibt wie das fertige projekt in der regel verwendet werden soll
- Core definieren (Worum geht es?) - Max 1024 Zeichen
- Target beschreiben (Welches Problem löst es?) - wird als `./_story.md` gespeichert
- Story schreiben (Wie wird es verwendet?) - nur die hauptanwendung
- Areas und Features definieren (Welche Funktionen hat es?) - abfolge von schritten (start ... ende)
- Features implementieren
- Projekt v1

View File

@@ -1,2 +1,6 @@
Für große Projekte geht der Kontext schnell verloren, sie werden unübersichtlich und schwer zu managen. - beschreibt simpel welches problem das projekt löst
Das Plugin hilft Projekte kontextbezogen zu strukturieren, so lassen sich große Projekte wieder managen. - Was ist das Problem und die Folge davon?
- Wie löst das Projekt das Problem und was ist die Folge davon?
- der zweck muss klar erkennbar sein
- max 255 zeichen
- wird als `./_target.md` gespeichert

View File

@@ -6,4 +6,9 @@ export const VIEW_TYPE_DETAILS = "projektkontext-details";
export const RIBBON_ICON = "layout-grid"; export const RIBBON_ICON = "layout-grid";
export const PROJECT_FILES = ["core.md", "target.md", "story.md"] as const; export const PROJECT_FILE = "_project.md";
export const CORE_FILE = "_core.md";
export const TARGET_FILE = "_target.md";
export const STORY_FILE = "_story.md";
export const PROJECT_FILES = [PROJECT_FILE, CORE_FILE, TARGET_FILE, STORY_FILE] as const;

View File

@@ -94,6 +94,19 @@ export async function createFeature(
return await ensureFile(app, featurePath(project, area, feature), ""); return await ensureFile(app, featurePath(project, area, feature), "");
} }
export function projectFeaturePath(project: string, feature: string): string {
const file = feature.endsWith(".md") ? feature : `${feature}.md`;
return normalizePath(`${PROJECTS_ROOT}/${project}/${file}`);
}
export async function createProjectFeature(
app: App,
project: string,
feature: string,
): Promise<TFile> {
return await ensureFile(app, projectFeaturePath(project, feature), "");
}
export function isProjectRootFile(name: string): boolean { export function isProjectRootFile(name: string): boolean {
return (PROJECT_FILES as readonly string[]).includes(name); return (PROJECT_FILES as readonly string[]).includes(name);
} }

View File

@@ -1,14 +1,19 @@
import { ItemView, WorkspaceLeaf, ViewStateResult, normalizePath } from "obsidian"; import { ItemView, Notice, WorkspaceLeaf, ViewStateResult, normalizePath } from "obsidian";
import { import {
VIEW_TYPE_OVERVIEW, VIEW_TYPE_OVERVIEW,
VIEW_TYPE_DETAILS, VIEW_TYPE_DETAILS,
VIEW_TYPE_PROJECTS, VIEW_TYPE_PROJECTS,
RIBBON_ICON, RIBBON_ICON,
PROJECT_FILES,
CORE_FILE,
TARGET_FILE,
STORY_FILE,
} from "../const"; } from "../const";
import { import {
projectPath, projectPath,
areaPath, areaPath,
featurePath, featurePath,
projectFeaturePath,
listFolders, listFolders,
listMarkdownFiles, listMarkdownFiles,
readFile, readFile,
@@ -16,8 +21,9 @@ import {
deleteRecursive, deleteRecursive,
createArea, createArea,
createFeature, createFeature,
createProjectFeature,
} from "../fs"; } from "../fs";
import { card, menu, breadcrumb, emptyState, openMarkdown } from "../ui"; import { menu, breadcrumb, emptyState, openMarkdown } from "../ui";
import { NameModal } from "../modals/NameModal"; import { NameModal } from "../modals/NameModal";
import { ConfirmModal } from "../modals/ConfirmModal"; import { ConfirmModal } from "../modals/ConfirmModal";
@@ -26,6 +32,7 @@ export interface OverviewState extends Record<string, unknown> {
} }
const NAME_RX = /^[^\\/:*?"<>|]+$/; const NAME_RX = /^[^\\/:*?"<>|]+$/;
const FEATURE_DND_MIME = "application/x-pk-feature";
function validateName(name: string, taken: string[], current?: string): string | null { function validateName(name: string, taken: string[], current?: string): string | null {
if (!name) return "Name darf nicht leer sein."; if (!name) return "Name darf nicht leer sein.";
@@ -89,9 +96,9 @@ export class OverviewView extends ItemView {
} }
const projRoot = projectPath(this.project); const projRoot = projectPath(this.project);
const corePath = normalizePath(`${projRoot}/core.md`); const corePath = normalizePath(`${projRoot}/${CORE_FILE}`);
const targetPath = normalizePath(`${projRoot}/target.md`); const targetPath = normalizePath(`${projRoot}/${TARGET_FILE}`);
const storyPath = normalizePath(`${projRoot}/story.md`); const storyPath = normalizePath(`${projRoot}/${STORY_FILE}`);
const [core, target, story] = await Promise.all([ const [core, target, story] = await Promise.all([
readFile(this.app, corePath), readFile(this.app, corePath),
readFile(this.app, targetPath), readFile(this.app, targetPath),
@@ -108,22 +115,24 @@ export class OverviewView extends ItemView {
]); ]);
const stack = root.createDiv({ cls: "pk-stack" }); const stack = root.createDiv({ cls: "pk-stack" });
this.renderInfoCard(stack, "Core", core, corePath); this.renderInfoCard(stack, core, corePath);
this.renderInfoCard(stack, "Target", target, targetPath); this.renderInfoCard(stack, target, targetPath);
this.renderStoryCard(stack, story, storyPath); this.renderStoryCard(stack, story, storyPath);
this.renderAreas(root); this.renderAreas(root);
} }
private renderInfoCard(parent: HTMLElement, title: string, content: string, path: string): void { private renderInfoCard(parent: HTMLElement, content: string, path: string): void {
card(parent, { const btn = parent.createDiv({
title, cls: "pk-btn-card",
body: content || "(leer)", attr: { role: "button", tabindex: "0" },
onClick: () =>
openMarkdown(this.app, path, {
type: VIEW_TYPE_OVERVIEW,
state: { project: this.project },
}),
}); });
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 { private renderStoryCard(parent: HTMLElement, content: string, path: string): void {
@@ -154,12 +163,36 @@ export class OverviewView extends ItemView {
ev.preventDefault(); ev.preventDefault();
menu(ev, [ menu(ev, [
{ title: "Neue Area", icon: "plus", onClick: () => this.openCreateArea() }, { title: "Neue Area", icon: "plus", onClick: () => this.openCreateArea() },
{ title: "Neues Feature", icon: "plus", onClick: () => this.openCreateProjectFeature() },
]); ]);
}); });
section.addEventListener("dragover", (ev) => {
if (!ev.dataTransfer?.types.includes(FEATURE_DND_MIME)) return;
ev.preventDefault();
ev.dataTransfer.dropEffect = "move";
section.addClass("pk-drop-zone");
});
section.addEventListener("dragleave", (ev) => {
const next = ev.relatedTarget as Node | null;
if (next && section.contains(next)) return;
section.removeClass("pk-drop-zone");
});
section.addEventListener("drop", async (ev) => {
section.removeClass("pk-drop-zone");
const raw = ev.dataTransfer?.getData(FEATURE_DND_MIME);
if (!raw) return;
ev.preventDefault();
await this.handleFeatureDropToProject(raw);
});
const areas = listFolders(this.app, projectPath(this.project)); const areas = listFolders(this.app, projectPath(this.project));
if (areas.length === 0) { const projectFeatures = listMarkdownFiles(
emptyState(section, "Noch keine Areas. Rechtsklick → Neue Area."); this.app,
projectPath(this.project),
[...PROJECT_FILES],
);
if (areas.length === 0 && projectFeatures.length === 0) {
emptyState(section, "Noch leer. Rechtsklick → Neue Area / Neues Feature.");
return; return;
} }
const takenAreas = areas.map((a) => a.name); const takenAreas = areas.map((a) => a.name);
@@ -170,44 +203,188 @@ export class OverviewView extends ItemView {
areaPath(this.project, area.name), areaPath(this.project, area.name),
[], [],
); );
card(stack, { const areaCard = stack.createDiv({
title: area.name, cls: "pk-btn-card pk-area-card",
cls: "pk-area-card", attr: { role: "button", tabindex: "0" },
onClick: () => this.openDetails(area.name), });
onContextMenu: (ev) => areaCard.createEl("strong", { text: area.name });
menu(ev, [ areaCard.addEventListener("click", () => this.openDetails(area.name));
{ title: "Neues Feature", icon: "plus", onClick: () => this.openCreateFeature(area.name) }, areaCard.addEventListener("contextmenu", (ev) => {
{ title: "Area umbenennen", icon: "pencil", onClick: () => this.openRenameArea(area.name, takenAreas) }, ev.preventDefault();
{ title: "Area löschen", icon: "trash", onClick: () => this.openDeleteArea(area.name) }, ev.stopPropagation();
]), menu(ev, [
body: (body) => { { title: "Neues Feature", icon: "plus", onClick: () => this.openCreateFeature(area.name) },
if (features.length === 0) { { title: "Area umbenennen", icon: "pencil", onClick: () => this.openRenameArea(area.name, takenAreas) },
body.createDiv({ cls: "pk-empty", text: "Keine Features." }); { title: "Area löschen", icon: "trash", onClick: () => this.openDeleteArea(area.name) },
return; ]);
} });
const list = body.createDiv({ cls: "pk-features" }); areaCard.addEventListener("dragover", (ev) => {
for (const f of features) { if (!ev.dataTransfer?.types.includes(FEATURE_DND_MIME)) return;
const chip = list.createDiv({ cls: "pk-feature-chip", text: f.basename }); ev.preventDefault();
chip.addEventListener("click", (ev) => { ev.dataTransfer.dropEffect = "move";
ev.stopPropagation(); areaCard.addClass("pk-drop-target");
openMarkdown(this.app, f.path, { });
type: VIEW_TYPE_OVERVIEW, areaCard.addEventListener("dragleave", (ev) => {
state: { project: this.project }, 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(); chip.addEventListener("contextmenu", (ev) => {
ev.stopPropagation(); ev.preventDefault();
menu(ev, [ ev.stopPropagation();
{ title: "Feature löschen", icon: "trash", onClick: () => this.openDeleteFeature(area.name, f.basename) }, menu(ev, [
]); { title: "Feature löschen", icon: "trash", onClick: () => this.openDeleteFeature(area.name, f.basename) },
}); ]);
} });
}, }
}
}
for (const f of projectFeatures) {
const fc = stack.createDiv({
cls: "pk-btn-card pk-area-card",
attr: { role: "button", tabindex: "0", draggable: "true" },
});
fc.createEl("strong", { text: f.basename });
fc.addEventListener("dragstart", (ev) => {
if (!ev.dataTransfer) return;
ev.dataTransfer.setData(
FEATURE_DND_MIME,
JSON.stringify({ sourceArea: null, feature: f.basename }),
);
ev.dataTransfer.effectAllowed = "move";
fc.addClass("pk-feature-chip-dragging");
});
fc.addEventListener("dragend", () => {
fc.removeClass("pk-feature-chip-dragging");
});
fc.addEventListener("click", () =>
openMarkdown(this.app, f.path, {
type: VIEW_TYPE_OVERVIEW,
state: { project: this.project },
}),
);
fc.addEventListener("contextmenu", (ev) => {
ev.preventDefault();
ev.stopPropagation();
menu(ev, [
{ title: "Feature löschen", icon: "trash", onClick: () => this.openDeleteProjectFeature(f.basename) },
]);
}); });
} }
} }
private openCreateProjectFeature(): void {
const taken = listMarkdownFiles(this.app, projectPath(this.project), []).map((f) => f.basename);
new NameModal(this.app, {
title: "Neues Feature",
label: "Feature-Name",
cta: "Erstellen",
validate: (n) => validateName(n, taken),
onSubmit: async (name) => {
await createProjectFeature(this.app, this.project, name);
await this.render();
},
}).open();
}
private openDeleteProjectFeature(feature: string): void {
new ConfirmModal(this.app, {
title: "Feature löschen",
message: `Feature „${feature}" wird gelöscht. Fortfahren?`,
cta: "Löschen",
destructive: true,
onConfirm: async () => {
await deleteRecursive(this.app, projectFeaturePath(this.project, feature));
await this.render();
},
}).open();
}
private async handleFeatureDropToProject(raw: string): Promise<void> {
let data: { sourceArea: string | null; feature: string };
try {
data = JSON.parse(raw);
} catch {
return;
}
if (!data?.feature) return;
if (data.sourceArea === null) return;
const newPath = projectFeaturePath(this.project, data.feature);
if (this.app.vault.getAbstractFileByPath(newPath)) {
new Notice(`${data.feature}" existiert im Projekt 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 async handleFeatureDrop(raw: string, targetArea: string): Promise<void> {
let data: { sourceArea: string | null; feature: string };
try {
data = JSON.parse(raw);
} catch {
return;
}
if (!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 = data.sourceArea === null
? projectFeaturePath(this.project, data.feature)
: 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 { private openCreateArea(): void {
const taken = listFolders(this.app, projectPath(this.project)).map((a) => a.name); const taken = listFolders(this.app, projectPath(this.project)).map((a) => a.name);
new NameModal(this.app, { new NameModal(this.app, {

View File

@@ -1,5 +1,5 @@
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 { VIEW_TYPE_PROJECTS, VIEW_TYPE_OVERVIEW, RIBBON_ICON, CORE_FILE } from "../const";
import { import {
projectsPath, projectsPath,
projectPath, projectPath,
@@ -8,8 +8,9 @@ import {
createProject, createProject,
rename, rename,
deleteRecursive, deleteRecursive,
readFile,
} from "../fs"; } from "../fs";
import { card, menu, breadcrumb, emptyState } from "../ui"; import { menu, breadcrumb, emptyState } from "../ui";
import { NameModal } from "../modals/NameModal"; import { NameModal } from "../modals/NameModal";
import { ConfirmModal } from "../modals/ConfirmModal"; 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("create", () => this.render()));
this.registerEvent(this.app.vault.on("delete", () => 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("rename", () => this.render()));
this.registerEvent(this.app.vault.on("modify", (f) => {
if (f.path.endsWith("/" + CORE_FILE) && f.path.startsWith(projectsPath() + "/")) {
this.render();
}
}));
this.registerDomEvent(this.containerEl, "contextmenu", (ev) => { this.registerDomEvent(this.containerEl, "contextmenu", (ev) => {
if (ev.defaultPrevented) return; if (ev.defaultPrevented) return;
ev.preventDefault(); ev.preventDefault();
@@ -70,16 +76,29 @@ export class ProjectsView extends ItemView {
} }
const taken = projects.map((p) => p.name); const taken = projects.map((p) => p.name);
const cores = await Promise.all(
projects.map((p) =>
readFile(this.app, normalizePath(`${projectPath(p.name)}/${CORE_FILE}`)),
),
);
const grid = root.createDiv({ cls: "pk-grid" }); const grid = root.createDiv({ cls: "pk-grid" });
for (const proj of projects) { for (let i = 0; i < projects.length; i++) {
card(grid, { const proj = projects[i];
title: proj.name, const core = cores[i].trim();
onClick: () => this.openOverview(proj.name), const btn = grid.createDiv({
onContextMenu: (ev) => cls: "pk-btn-card",
menu(ev, [ attr: { role: "button", tabindex: "0" },
{ title: "Umbenennen", icon: "pencil", onClick: () => this.openRename(proj.name, taken) }, });
{ title: "Löschen", icon: "trash", onClick: () => this.openDelete(proj.name) }, 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) },
]);
}); });
} }
} }

View File

@@ -136,10 +136,47 @@
font-weight: 400; 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-drop-zone {
background: var(--background-modifier-hover);
border-radius: 6px;
}
.pk-feature-chip-dragging {
opacity: 0.4;
}
.pk-areas-section { .pk-areas-section {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 10px; gap: 10px;
padding: 5px;
} }
.pk-areas-flex { .pk-areas-flex {