@pi-unipi/kanboard 0.1.2

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.
@@ -0,0 +1,182 @@
1
+ /**
2
+ * @pi-unipi/kanboard — Workflow Page
3
+ *
4
+ * Renders the workflow page with cards grouped by doc type.
5
+ */
6
+
7
+ import type { ParsedDoc, ParsedItem, DocType } from "../../types.js";
8
+ import { renderLayout } from "../layouts/base.js";
9
+
10
+ /** Escape HTML */
11
+ function esc(text: string): string {
12
+ return text
13
+ .replace(/&/g, "&")
14
+ .replace(/</g, "&lt;")
15
+ .replace(/>/g, "&gt;")
16
+ .replace(/"/g, "&quot;");
17
+ }
18
+
19
+ /** Doc type display config */
20
+ const DOC_TYPE_CONFIG: Record<
21
+ DocType,
22
+ { icon: string; label: string; color: string }
23
+ > = {
24
+ spec: { icon: "◈", label: "Specs", color: "accent" },
25
+ plan: { icon: "◈", label: "Plans", color: "accent" },
26
+ milestone: { icon: "◉", label: "Milestones", color: "green" },
27
+ "quick-work": { icon: "⚡", label: "Quick Work", color: "yellow" },
28
+ debug: { icon: "◆", label: "Debug", color: "red" },
29
+ fix: { icon: "◊", label: "Fixes", color: "green" },
30
+ chore: { icon: "◇", label: "Chores", color: "yellow" },
31
+ review: { icon: "◈", label: "Reviews", color: "accent" },
32
+ };
33
+
34
+ /** Render status badge */
35
+ function statusBadge(status: string): string {
36
+ const labels: Record<string, string> = {
37
+ done: "Done",
38
+ "in-progress": "In Progress",
39
+ todo: "To Do",
40
+ reviewed: "Reviewed",
41
+ };
42
+ return `<span class="badge badge-${status}">${labels[status] ?? status}</span>`;
43
+ }
44
+
45
+ /** Render status icon */
46
+ function statusIcon(status: string): string {
47
+ switch (status) {
48
+ case "done":
49
+ return "✓";
50
+ case "in-progress":
51
+ return "◐";
52
+ case "reviewed":
53
+ return "◉";
54
+ default:
55
+ return "○";
56
+ }
57
+ }
58
+
59
+ /** Render a single doc card */
60
+ function renderDocCard(doc: ParsedDoc): string {
61
+ const config = DOC_TYPE_CONFIG[doc.type] ?? { icon: "◈", label: doc.type, color: "accent" };
62
+ const total = doc.items.length;
63
+ const done = doc.items.filter((i) => i.status === "done" || i.status === "reviewed").length;
64
+ const percent = total > 0 ? Math.round((done / total) * 100) : 0;
65
+
66
+ const itemsHtml = doc.items
67
+ .map(
68
+ (item) => `
69
+ <li class="checklist-item" data-status="${item.status}">
70
+ <span class="checklist-status ${item.status}" aria-label="${item.status}">
71
+ <span aria-hidden="true">${statusIcon(item.status)}</span>
72
+ <span class="visually-hidden">${item.status}</span>
73
+ </span>
74
+ <span class="checklist-text${item.status === "done" || item.status === "reviewed" ? " done" : ""}">${esc(item.text)}</span>
75
+ ${
76
+ item.command
77
+ ? `<button class="copy-btn" onclick="copyToClipboard('${esc(item.command)}', event)" aria-label="Copy ${esc(item.command)} to clipboard">${esc(item.command)}</button>`
78
+ : ""
79
+ }
80
+ </li>`,
81
+ )
82
+ .join("\n");
83
+
84
+ return `
85
+ <div class="card" x-data="{ open: false }">
86
+ <button type="button" class="card-header" @click="open = !open" :aria-expanded="open">
87
+ <div class="card-header-inner">
88
+ <span class="doc-type-icon">${config.icon}</span>
89
+ <h3 class="card-title">${esc(doc.title)}</h3>
90
+ </div>
91
+ <div class="card-meta">
92
+ ${statusBadge(total === 0 ? (doc.type === "quick-work" ? "done" : "todo") : done === total ? "done" : "in-progress")}
93
+ ${total > 0 ? `<span class="progress-text">${done}/${total}</span>` : ""}
94
+ </div>
95
+ </button>
96
+ <div class="progress-bar" style="--progress: ${percent};">
97
+ <div class="progress-fill"></div>
98
+ </div>
99
+ <template x-if="open">
100
+ <ul class="checklist">
101
+ ${itemsHtml}
102
+ </ul>
103
+ </template>
104
+ ${doc.warnings.length > 0
105
+ ? `<div class="warnings">${doc.warnings.map((w) => `<div class="warning-item">${esc(w)}</div>`).join("")}</div>`
106
+ : ""}
107
+ </div>`;
108
+ }
109
+
110
+ /** Render the workflow page */
111
+ export function renderWorkflowPage(docs: ParsedDoc[]): string {
112
+ const groups: Record<string, ParsedDoc[]> = {};
113
+ for (const doc of docs) {
114
+ if (!groups[doc.type]) groups[doc.type] = [];
115
+ groups[doc.type].push(doc);
116
+ }
117
+
118
+ const totalDocs = docs.length;
119
+ const totalItems = docs.reduce((sum, d) => sum + d.items.length, 0);
120
+ const totalDone = docs.reduce(
121
+ (sum, d) => sum + d.items.filter((i) => i.status === "done" || i.status === "reviewed").length,
122
+ 0,
123
+ );
124
+
125
+ let sectionsHtml = "";
126
+ for (const [type, typeDocs] of Object.entries(groups)) {
127
+ const config = DOC_TYPE_CONFIG[type as DocType] ?? {
128
+ icon: "◈",
129
+ label: type,
130
+ color: "accent",
131
+ };
132
+
133
+ const typeItems = typeDocs.reduce((sum, d) => sum + d.items.length, 0);
134
+ const typeDone = typeDocs.reduce(
135
+ (sum, d) => sum + d.items.filter((i) => i.status === "done" || i.status === "reviewed").length,
136
+ 0,
137
+ );
138
+
139
+ sectionsHtml += `
140
+ <div class="section">
141
+ <button type="button" class="section-header" aria-expanded="true" onclick="toggleSection(event)">
142
+ <span class="section-toggle">▶</span>
143
+ <div class="section-title-wrap">
144
+ <h2 class="section-title">${config.icon} ${config.label}</h2>
145
+ </div>
146
+ <span class="section-count">${typeDocs.length} docs · ${typeDone}/${typeItems} items</span>
147
+ </button>
148
+ <div class="section-content">
149
+ <div class="card-grid">
150
+ ${typeDocs.map(renderDocCard).join("\n")}
151
+ </div>
152
+ </div>
153
+ </div>`;
154
+ }
155
+
156
+ if (totalDocs === 0) {
157
+ sectionsHtml = `
158
+ <div class="empty-state">
159
+ <div class="empty-state-icon">✦</div>
160
+ <p class="empty-state-text">No workflow documents found</p>
161
+ </div>`;
162
+ }
163
+
164
+ const content = `
165
+ <div class="page-header">
166
+ <h1 class="page-title">Workflow</h1>
167
+ <p class="page-subtitle">${totalDocs} documents · ${totalDone}/${totalItems} items complete</p>
168
+ <div class="page-header-stat">${totalItems > 0 ? Math.round((totalDone / totalItems) * 100) : 0}% across all documents</div>
169
+ </div>
170
+
171
+ <div class="filters" x-data="kanboardFilters()">
172
+ <button class="filter-btn" :class="{ active: filter === 'all' }" @click="setFilter('all')">All</button>
173
+ <button class="filter-btn" :class="{ active: filter === 'todo' }" @click="setFilter('todo')">To Do</button>
174
+ <button class="filter-btn" :class="{ active: filter === 'in-progress' }" @click="setFilter('in-progress')">In Progress</button>
175
+ <button class="filter-btn" :class="{ active: filter === 'reviewed' }" @click="setFilter('reviewed')">Reviewed</button>
176
+ <button class="filter-btn" :class="{ active: filter === 'done' }" @click="setFilter('done')">Done</button>
177
+ </div>
178
+
179
+ ${sectionsHtml}`;
180
+
181
+ return renderLayout("Workflow", content, "workflow");
182
+ }