@pi-unipi/kanboard 2.20.4 → 3.0.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,71 +0,0 @@
1
- ---
2
- name: kanboard-doctor
3
- description: "Diagnose and fix kanboard parser issues — validates all workflow documents, reports errors, suggests fixes."
4
- ---
5
-
6
- # Kanboard Doctor
7
-
8
- Diagnose parser issues across all workflow documents. Non-destructive — only suggests fixes, asks user to confirm.
9
-
10
- ## Phase 1: Run All Parsers
11
-
12
- Execute each parser against its document type directory:
13
-
14
- 1. Load the parser registry from `@pi-unipi/kanboard`
15
- 2. Run `registry.parseAll(".unipi/docs")` to parse all documents
16
- 3. Collect all `ParsedDoc` results including their `warnings` arrays
17
-
18
- ## Phase 2: Collect Errors
19
-
20
- Group warnings and errors by file with line numbers:
21
-
22
- 1. For each `ParsedDoc` with `warnings.length > 0`:
23
- - Group by `filePath`
24
- - Include line numbers where available
25
- - Categorize: malformed checkboxes, empty fields, parse failures
26
- 2. Also flag documents that returned 0 items (may indicate parsing failure)
27
-
28
- ## Phase 3: Present Report
29
-
30
- Show a structured error report:
31
-
32
- ```
33
- 📋 Kanboard Doctor Report
34
-
35
- Files scanned: N
36
- Files with issues: M
37
-
38
- 📄 .unipi/docs/specs/example.md
39
- ⚠ Line 15: Empty checkbox text
40
- ⚠ Line 23: Malformed checkbox (missing bracket)
41
-
42
- 📄 .unipi/docs/plans/old-plan.md
43
- ⚠ Line 5: Empty task name after status
44
- ```
45
-
46
- ## Phase 4: Fix One by One
47
-
48
- For each issue, suggest a fix and ask user to confirm:
49
-
50
- 1. Show the problematic line with context
51
- 2. Suggest the corrected version
52
- 3. Ask: "Apply this fix? (y/n)"
53
- 4. If yes, apply the fix using the edit tool
54
- 5. Move to next issue
55
-
56
- **Non-destructive rules:**
57
- - Never modify without asking
58
- - Show before/after for each change
59
- - Allow skipping individual fixes
60
- - Allow "fix all" for simple patterns (e.g., trailing whitespace)
61
-
62
- ## Phase 5: Re-validate
63
-
64
- After each fix, re-run the parser on the modified file:
65
-
66
- 1. Parse the file again
67
- 2. Verify the specific error is resolved
68
- 3. Check that no new errors were introduced
69
- 4. Report: "✓ Fixed" or "✗ Still has issues"
70
-
71
- After all fixes, run full `registry.parseAll()` to confirm clean state.
package/types.ts DELETED
@@ -1,67 +0,0 @@
1
- /**
2
- * @pi-unipi/kanboard — Shared types
3
- */
4
-
5
- /** Document types that kanboard can parse */
6
- export type DocType =
7
- | "spec"
8
- | "plan"
9
- | "milestone"
10
- | "quick-work"
11
- | "debug"
12
- | "fix"
13
- | "chore"
14
- | "review";
15
-
16
- /** Status of a parsed item */
17
- export type ItemStatus = "todo" | "in-progress" | "done" | "reviewed";
18
-
19
- /** A single parsed item from a document */
20
- export interface ParsedItem {
21
- /** Item text content */
22
- text: string;
23
- /** Current status */
24
- status: ItemStatus;
25
- /** Line number in source file (1-indexed) */
26
- lineNumber: number;
27
- /** Source file path (relative) */
28
- sourceFile: string;
29
- /** Copy command for this item */
30
- command?: string;
31
- }
32
-
33
- /** A parsed document */
34
- export interface ParsedDoc {
35
- /** Document type */
36
- type: DocType;
37
- /** Document title (from frontmatter or filename) */
38
- title: string;
39
- /** File path (relative) */
40
- filePath: string;
41
- /** Parsed items */
42
- items: ParsedItem[];
43
- /** Frontmatter metadata */
44
- metadata: Record<string, string>;
45
- /** Warnings collected during parsing */
46
- warnings: string[];
47
- }
48
-
49
- /** Parser interface — each doc type implements this */
50
- export interface DocParser {
51
- /** Check if this parser can handle the given file */
52
- canParse(filePath: string): boolean;
53
- /** Parse the file and return a ParsedDoc */
54
- parse(filePath: string): ParsedDoc;
55
- }
56
-
57
- /** Kanboard configuration */
58
- export interface KanboardConfig {
59
- /** HTTP server port */
60
- port: number;
61
- /** Maximum port to try */
62
- maxPort: number;
63
- /** Root directory for docs */
64
- docsRoot: string;
65
- /** PID file path */
66
- pidFile: string;
67
- }
@@ -1,58 +0,0 @@
1
- /**
2
- * @pi-unipi/kanboard — Checklist Component
3
- *
4
- * Reusable checklist renderer with status indicators and copy buttons.
5
- */
6
-
7
- import type { ParsedItem } from "../../types.js";
8
-
9
- /** Escape HTML */
10
- function esc(text: string): string {
11
- return text
12
- .replace(/&/g, "&amp;")
13
- .replace(/</g, "&lt;")
14
- .replace(/>/g, "&gt;")
15
- .replace(/"/g, "&quot;")
16
- .replace(/'/g, "&#39;");
17
- }
18
-
19
- /** Status icon */
20
- function statusIcon(status: string): string {
21
- switch (status) {
22
- case "done":
23
- return "✓";
24
- case "in-progress":
25
- return "◐";
26
- default:
27
- return "○";
28
- }
29
- }
30
-
31
- /**
32
- * Render a checklist of items.
33
- */
34
- export function renderChecklist(items: ParsedItem[]): string {
35
- if (items.length === 0) {
36
- return '<div class="empty-state"><p class="empty-state-text">No items</p></div>';
37
- }
38
-
39
- const rows = items
40
- .map(
41
- (item) => `
42
- <li class="checklist-item">
43
- <span class="checklist-status ${item.status}" aria-label="${item.status}">
44
- <span aria-hidden="true">${statusIcon(item.status)}</span>
45
- <span class="visually-hidden">${item.status}</span>
46
- </span>
47
- <span class="checklist-text${item.status === "done" ? " done" : ""}">${esc(item.text)}</span>
48
- ${
49
- item.command
50
- ? `<button class="copy-btn" onclick="copyToClipboard('${esc(item.command)}', event)" aria-label="Copy ${esc(item.command)} to clipboard">${esc(item.command)}</button>`
51
- : ""
52
- }
53
- </li>`,
54
- )
55
- .join("\n");
56
-
57
- return `<ul class="checklist">${rows}</ul>`;
58
- }
@@ -1,23 +0,0 @@
1
- /**
2
- * @pi-unipi/kanboard — Copy Button Component
3
- *
4
- * Reusable copy-to-clipboard button with visual feedback.
5
- */
6
-
7
- /** Escape HTML */
8
- function esc(text: string): string {
9
- return text
10
- .replace(/&/g, "&amp;")
11
- .replace(/</g, "&lt;")
12
- .replace(/>/g, "&gt;")
13
- .replace(/"/g, "&quot;")
14
- .replace(/'/g, "&#39;");
15
- }
16
-
17
- /**
18
- * Render a copy-to-clipboard button.
19
- */
20
- export function renderCopyButton(text: string, label?: string): string {
21
- const displayLabel = label ?? (text.length > 40 ? text.slice(0, 37) + "..." : text);
22
- return `<button class="copy-btn" onclick="copyToClipboard('${esc(text)}', event)" title="${esc(text)}" aria-label="Copy ${esc(displayLabel)} to clipboard">${esc(displayLabel)}</button>`;
23
- }
@@ -1,21 +0,0 @@
1
- /**
2
- * @pi-unipi/kanboard — Status Badge Component
3
- *
4
- * Color-coded status badge for items.
5
- */
6
-
7
- /** Status badge labels */
8
- const BADGE_LABELS: Record<string, string> = {
9
- done: "Done",
10
- "in-progress": "In Progress",
11
- todo: "To Do",
12
- reviewed: "Reviewed",
13
- };
14
-
15
- /**
16
- * Render a status badge.
17
- */
18
- export function renderStatusBadge(status: string): string {
19
- const label = BADGE_LABELS[status] ?? status;
20
- return `<span class="badge badge-${status}">${label}</span>`;
21
- }
@@ -1,68 +0,0 @@
1
- /**
2
- * @pi-unipi/kanboard — Base HTML Layout
3
- *
4
- * Shared layout for all kanboard web pages.
5
- * Includes htmx + Alpine.js CDN, Google Fonts, and responsive layout.
6
- */
7
-
8
- /** Active page type */
9
- export type ActivePage = "milestones" | "workflow";
10
-
11
- /**
12
- * Render the full HTML layout wrapping page content.
13
- */
14
- export function renderLayout(
15
- title: string,
16
- content: string,
17
- activePage: ActivePage,
18
- ): string {
19
- return `<!DOCTYPE html>
20
- <html lang="en">
21
- <head>
22
- <meta charset="UTF-8">
23
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
24
- <title>${escapeHtml(title)} — Kanboard</title>
25
- <script src="https://unpkg.com/htmx.org@1.9.12"></script>
26
- <script defer src="https://unpkg.com/alpinejs@3.14.3/dist/cdn.min.js"></script>
27
- <link rel="preconnect" href="https://fonts.googleapis.com">
28
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
29
- <link href="https://fonts.googleapis.com/css2?family=Bodoni+Moda:ital,opsz,wght@1,6..96,400..900&family=Onest:wght@100..900&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
30
- <link rel="stylesheet" href="/static/style.css">
31
- </head>
32
- <body>
33
- <nav class="navbar">
34
- <div class="nav-brand">
35
- <span class="nav-icon">✦</span>
36
- <span class="nav-title">Kanboard</span>
37
- </div>
38
- <div class="nav-links">
39
- <a href="/" class="nav-link ${activePage === "milestones" ? "active" : ""}" ${activePage === "milestones" ? 'aria-current="page"' : ""}>
40
- Milestones
41
- </a>
42
- <a href="/workflow" class="nav-link ${activePage === "workflow" ? "active" : ""}" ${activePage === "workflow" ? 'aria-current="page"' : ""}>
43
- Workflow
44
- </a>
45
- </div>
46
- </nav>
47
-
48
- <main class="container">
49
- ${content}
50
- </main>
51
-
52
- <footer class="footer">
53
- <span>pi-unipi / kanboard</span>
54
- </footer>
55
-
56
- <script src="/static/app.js"></script>
57
- </body>
58
- </html>`;
59
- }
60
-
61
- /** Escape HTML special characters */
62
- function escapeHtml(text: string): string {
63
- return text
64
- .replace(/&/g, "&amp;")
65
- .replace(/</g, "&lt;")
66
- .replace(/>/g, "&gt;")
67
- .replace(/"/g, "&quot;");
68
- }
@@ -1,140 +0,0 @@
1
- /**
2
- * @pi-unipi/kanboard — Milestone Page
3
- *
4
- * Renders the milestone page with phases, progress bars, and checklists.
5
- */
6
-
7
- import type { ParsedDoc, ParsedItem } from "../../types.js";
8
- import { renderLayout } from "../layouts/base.js";
9
-
10
- /** Escape HTML special characters */
11
- function esc(text: string): string {
12
- return text
13
- .replace(/&/g, "&amp;")
14
- .replace(/</g, "&lt;")
15
- .replace(/>/g, "&gt;")
16
- .replace(/"/g, "&quot;");
17
- }
18
-
19
- /** Render status icon for an item */
20
- function statusIcon(status: string): string {
21
- switch (status) {
22
- case "done":
23
- return "✓";
24
- case "in-progress":
25
- return "◐";
26
- default:
27
- return "○";
28
- }
29
- }
30
-
31
- /** Render a single checklist item */
32
- function renderItem(item: ParsedItem): string {
33
- const doneClass = item.status === "done" ? " done" : "";
34
- const commandBtn = item.command
35
- ? `<button class="copy-btn" onclick="copyToClipboard('${esc(item.command)}', event)" aria-label="Copy ${esc(item.command)} to clipboard">${esc(item.command)}</button>`
36
- : "";
37
-
38
- return `
39
- <li class="checklist-item">
40
- <span class="checklist-status ${item.status}" aria-label="${item.status}">
41
- <span aria-hidden="true">${statusIcon(item.status)}</span>
42
- <span class="visually-hidden">${item.status}</span>
43
- </span>
44
- <span class="checklist-text${doneClass}">${esc(item.text)}</span>
45
- ${commandBtn}
46
- </li>`;
47
- }
48
-
49
- /** Render the milestone page */
50
- export function renderMilestonePage(docs: ParsedDoc[]): string {
51
- if (docs.length === 0) {
52
- const content = `
53
- <div class="page-header">
54
- <h1 class="page-title">Milestones</h1>
55
- <p class="page-subtitle">Track project progress</p>
56
- </div>
57
- <div class="empty-state">
58
- <div class="empty-state-icon">✦</div>
59
- <p class="empty-state-text">No MILESTONES.md found</p>
60
- </div>`;
61
- return renderLayout("Milestones", content, "milestones");
62
- }
63
-
64
- const doc = docs[0];
65
- const totalItems = doc.items.length;
66
- const doneItems = doc.items.filter((i) => i.status === "done").length;
67
- const percent = totalItems > 0 ? Math.round((doneItems / totalItems) * 100) : 0;
68
-
69
- const phases = groupByPhase(doc.items);
70
-
71
- let phasesHtml = "";
72
- for (const [phase, items] of Object.entries(phases)) {
73
- const phaseTotal = items.length;
74
- const phaseDone = items.filter((i) => i.status === "done").length;
75
- const phasePercent = phaseTotal > 0 ? Math.round((phaseDone / phaseTotal) * 100) : 0;
76
-
77
- phasesHtml += `
78
- <div class="section">
79
- <button type="button" class="section-header" aria-expanded="true" onclick="toggleSection(event)">
80
- <span class="section-toggle">▶</span>
81
- <div class="section-title-wrap">
82
- <h2 class="section-title">${esc(phase)}</h2>
83
- </div>
84
- <span class="section-count">${phaseDone}/${phaseTotal}</span>
85
- <div class="progress-bar" style="--progress: ${phasePercent};">
86
- <div class="progress-fill"></div>
87
- </div>
88
- </button>
89
- <div class="section-content">
90
- <ul class="checklist">
91
- ${items.map(renderItem).join("\n")}
92
- </ul>
93
- </div>
94
- </div>`;
95
- }
96
-
97
- const content = `
98
- <div class="page-header">
99
- <h1 class="page-title">${esc(doc.title)}</h1>
100
- <p class="page-subtitle">${doneItems}/${totalItems} items complete</p>
101
- <div class="progress-bar" style="--progress: ${percent};">
102
- <div class="progress-fill"></div>
103
- </div>
104
- <div class="page-header-stat">${percent}% complete</div>
105
- </div>
106
-
107
- ${phasesHtml}
108
-
109
- ${doc.warnings.length > 0 ? renderWarnings(doc.warnings) : ""}`;
110
-
111
- return renderLayout("Milestones", content, "milestones");
112
- }
113
-
114
- /** Group items by their phase prefix [Phase Name] */
115
- function groupByPhase(items: ParsedItem[]): Record<string, ParsedItem[]> {
116
- const phases: Record<string, ParsedItem[]> = {};
117
-
118
- for (const item of items) {
119
- const match = item.text.match(/^\[(.+?)\]\s*(.*)$/);
120
- if (match) {
121
- const phase = match[1];
122
- const text = match[2];
123
- if (!phases[phase]) phases[phase] = [];
124
- phases[phase].push({ ...item, text });
125
- } else {
126
- if (!phases["Other"]) phases["Other"] = [];
127
- phases["Other"].push(item);
128
- }
129
- }
130
-
131
- return phases;
132
- }
133
-
134
- /** Render warnings list */
135
- function renderWarnings(warnings: string[]): string {
136
- return `
137
- <div class="warnings">
138
- ${warnings.map((w) => `<div class="warning-item">${esc(w)}</div>`).join("\n")}
139
- </div>`;
140
- }
package/ui/static/app.js DELETED
@@ -1,73 +0,0 @@
1
- /* @pi-unipi/kanboard — Client-side JavaScript */
2
-
3
- /**
4
- * Copy text to clipboard and show feedback.
5
- */
6
- function copyToClipboard(text, event) {
7
- navigator.clipboard.writeText(text).then(() => {
8
- const btn = event.currentTarget;
9
- btn.classList.add("copied");
10
- const original = btn.innerHTML;
11
- btn.innerHTML = '<span>copied</span>';
12
- setTimeout(() => {
13
- btn.classList.remove("copied");
14
- btn.innerHTML = original;
15
- }, 1800);
16
- });
17
- }
18
-
19
- /**
20
- * Toggle a section's expanded state with animation.
21
- */
22
- function toggleSection(event) {
23
- const header = event.currentTarget;
24
- const content = header.nextElementSibling;
25
- if (!content) return;
26
-
27
- const wasOpen = header.getAttribute("aria-expanded") === "true";
28
- header.setAttribute("aria-expanded", wasOpen ? "false" : "true");
29
-
30
- const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
31
-
32
- if (!wasOpen) {
33
- content.style.display = "block";
34
- if (!prefersReducedMotion) {
35
- content.style.opacity = "0";
36
- content.style.transform = "translateY(-4px)";
37
- requestAnimationFrame(() => {
38
- content.style.transition = "opacity 0.2s ease, transform 0.2s ease";
39
- content.style.opacity = "1";
40
- content.style.transform = "translateY(0)";
41
- });
42
- } else {
43
- content.style.opacity = "1";
44
- content.style.transform = "translateY(0)";
45
- }
46
- } else {
47
- if (!prefersReducedMotion) {
48
- content.style.transition = "opacity 0.15s ease";
49
- content.style.opacity = "0";
50
- setTimeout(() => {
51
- content.style.display = "none";
52
- }, 150);
53
- } else {
54
- content.style.display = "none";
55
- }
56
- }
57
- }
58
-
59
- /**
60
- * Alpine.js data for filtering items by status.
61
- */
62
- function kanboardFilters() {
63
- return {
64
- filter: "all",
65
- setFilter(f) {
66
- this.filter = f;
67
- },
68
- isVisible(status) {
69
- if (this.filter === "all") return true;
70
- return status === this.filter;
71
- },
72
- };
73
- }