beads-ui 0.2.0 → 0.3.1
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.
- package/CHANGES.md +14 -0
- package/README.md +4 -4
- package/app/data/list-selectors.js +103 -0
- package/app/data/providers.js +7 -138
- package/app/data/sort.js +47 -0
- package/app/data/subscription-issue-store.js +161 -0
- package/app/data/subscription-issue-stores.js +128 -0
- package/app/data/subscriptions-store.js +227 -0
- package/app/main.js +346 -66
- package/app/protocol.js +23 -17
- package/app/protocol.md +18 -15
- package/app/router.js +3 -0
- package/app/state.js +2 -0
- package/app/styles.css +222 -197
- package/app/utils/issue-id-renderer.js +2 -1
- package/app/utils/issue-id.js +1 -0
- package/app/utils/issue-type.js +2 -0
- package/app/utils/issue-url.js +1 -0
- package/app/utils/markdown.js +13 -198
- package/app/utils/priority-badge.js +1 -2
- package/app/utils/status-badge.js +1 -1
- package/app/utils/status.js +2 -0
- package/app/utils/toast.js +1 -1
- package/app/utils/type-badge.js +1 -3
- package/app/views/board.js +172 -148
- package/app/views/detail.js +79 -66
- package/app/views/epics.js +127 -74
- package/app/views/issue-dialog.js +9 -15
- package/app/views/issue-row.js +2 -3
- package/app/views/list.js +105 -104
- package/app/views/nav.js +1 -0
- package/app/views/new-issue-dialog.js +30 -34
- package/app/ws.js +10 -10
- package/bin/bdui.js +1 -1
- package/docs/adr/001-push-only-lists.md +134 -0
- package/docs/adr/002-per-subscription-stores-and-full-issue-push.md +200 -0
- package/docs/architecture.md +34 -84
- package/docs/data-exchange-subscription-plan.md +198 -0
- package/docs/db-watching.md +2 -1
- package/docs/migration-v2.md +54 -0
- package/docs/protocol/issues-push-v2.md +179 -0
- package/docs/subscription-issue-store.md +112 -0
- package/package.json +5 -4
- package/server/app.js +2 -0
- package/server/bd.js +4 -2
- package/server/cli/commands.js +5 -2
- package/server/cli/daemon.js +19 -5
- package/server/cli/index.js +2 -2
- package/server/cli/open.js +3 -0
- package/server/cli/usage.js +2 -1
- package/server/config.js +13 -6
- package/server/db.js +3 -1
- package/server/index.js +9 -5
- package/server/list-adapters.js +224 -0
- package/server/subscriptions.js +289 -0
- package/server/validators.js +113 -0
- package/server/watcher.js +8 -8
- package/server/ws.js +457 -229
package/app/utils/markdown.js
CHANGED
|
@@ -1,201 +1,16 @@
|
|
|
1
|
+
import DOMPurify from 'dompurify';
|
|
2
|
+
import { unsafeHTML } from 'lit-html/directives/unsafe-html.js';
|
|
3
|
+
import { marked } from 'marked';
|
|
4
|
+
|
|
1
5
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* @param {string}
|
|
7
|
-
* @returns {DocumentFragment}
|
|
6
|
+
* Render Markdown safely as HTML using marked and DOMPurify.
|
|
7
|
+
* Returns a lit-html TemplateResult via the unsafeHTML directive so it can be
|
|
8
|
+
* embedded directly in templates.
|
|
9
|
+
*
|
|
10
|
+
* @param {string} markdown - Markdown source text
|
|
8
11
|
*/
|
|
9
|
-
export function renderMarkdown(
|
|
10
|
-
/** @type {
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
/** @type {string[]} */
|
|
14
|
-
const lines = (src || '').replace(/\r\n?/g, '\n').split('\n');
|
|
15
|
-
|
|
16
|
-
/** @type {boolean} */
|
|
17
|
-
let in_code = false;
|
|
18
|
-
/** @type {string[]} */
|
|
19
|
-
let code_acc = [];
|
|
20
|
-
|
|
21
|
-
/** @type {HTMLElement|null} */
|
|
22
|
-
let list_el = null;
|
|
23
|
-
/** @type {string} */
|
|
24
|
-
let list_type = '';
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Flush current paragraph buffer to <p> if any.
|
|
28
|
-
* @param {string[]} buf
|
|
29
|
-
*/
|
|
30
|
-
function flushParagraph(buf) {
|
|
31
|
-
if (buf.length === 0) {
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
const p = document.createElement('p');
|
|
35
|
-
appendInline(p, buf.join(' '));
|
|
36
|
-
frag.appendChild(p);
|
|
37
|
-
buf.length = 0;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** @type {string[]} */
|
|
41
|
-
const para = [];
|
|
42
|
-
|
|
43
|
-
for (let i = 0; i < lines.length; i++) {
|
|
44
|
-
const raw = lines[i];
|
|
45
|
-
const line = raw;
|
|
46
|
-
|
|
47
|
-
// fenced code blocks
|
|
48
|
-
if (/^```/.test(line)) {
|
|
49
|
-
if (!in_code) {
|
|
50
|
-
in_code = true;
|
|
51
|
-
code_acc = [];
|
|
52
|
-
} else {
|
|
53
|
-
// flush code block
|
|
54
|
-
const pre = document.createElement('pre');
|
|
55
|
-
const code = document.createElement('code');
|
|
56
|
-
code.textContent = code_acc.join('\n');
|
|
57
|
-
pre.appendChild(code);
|
|
58
|
-
frag.appendChild(pre);
|
|
59
|
-
in_code = false;
|
|
60
|
-
code_acc = [];
|
|
61
|
-
}
|
|
62
|
-
continue;
|
|
63
|
-
}
|
|
64
|
-
if (in_code) {
|
|
65
|
-
code_acc.push(raw);
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// blank line -> paragraph / list break
|
|
70
|
-
if (/^\s*$/.test(line)) {
|
|
71
|
-
flushParagraph(para);
|
|
72
|
-
if (list_el !== null) {
|
|
73
|
-
frag.appendChild(list_el);
|
|
74
|
-
list_el = null;
|
|
75
|
-
list_type = '';
|
|
76
|
-
}
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// heading
|
|
81
|
-
const hx = /^(#{1,6})\s+(.*)$/.exec(line);
|
|
82
|
-
if (hx) {
|
|
83
|
-
flushParagraph(para);
|
|
84
|
-
if (list_el !== null) {
|
|
85
|
-
frag.appendChild(list_el);
|
|
86
|
-
list_el = null;
|
|
87
|
-
list_type = '';
|
|
88
|
-
}
|
|
89
|
-
const level = Math.min(6, hx[1].length);
|
|
90
|
-
const h = /** @type {HTMLHeadingElement} */ (
|
|
91
|
-
document.createElement('h' + String(level))
|
|
92
|
-
);
|
|
93
|
-
appendInline(h, hx[2]);
|
|
94
|
-
frag.appendChild(h);
|
|
95
|
-
continue;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// list item
|
|
99
|
-
let m = /^\s*[-*]\s+(.*)$/.exec(line);
|
|
100
|
-
if (m) {
|
|
101
|
-
if (list_el === null || list_type !== 'ul') {
|
|
102
|
-
flushParagraph(para);
|
|
103
|
-
if (list_el !== null) {
|
|
104
|
-
frag.appendChild(list_el);
|
|
105
|
-
}
|
|
106
|
-
list_el = document.createElement('ul');
|
|
107
|
-
list_type = 'ul';
|
|
108
|
-
}
|
|
109
|
-
const li = document.createElement('li');
|
|
110
|
-
appendInline(li, m[1]);
|
|
111
|
-
list_el.appendChild(li);
|
|
112
|
-
continue;
|
|
113
|
-
}
|
|
114
|
-
m = /^\s*(\d+)\.\s+(.*)$/.exec(line);
|
|
115
|
-
if (m) {
|
|
116
|
-
if (list_el === null || list_type !== 'ol') {
|
|
117
|
-
flushParagraph(para);
|
|
118
|
-
if (list_el !== null) {
|
|
119
|
-
frag.appendChild(list_el);
|
|
120
|
-
}
|
|
121
|
-
list_el = document.createElement('ol');
|
|
122
|
-
list_type = 'ol';
|
|
123
|
-
}
|
|
124
|
-
const li = document.createElement('li');
|
|
125
|
-
appendInline(li, m[2]);
|
|
126
|
-
list_el.appendChild(li);
|
|
127
|
-
continue;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// otherwise: paragraph text; accumulate to join with spaces
|
|
131
|
-
para.push(line.trim());
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// flush leftovers
|
|
135
|
-
if (in_code) {
|
|
136
|
-
const pre = document.createElement('pre');
|
|
137
|
-
const code = document.createElement('code');
|
|
138
|
-
code.textContent = code_acc.join('\n');
|
|
139
|
-
pre.appendChild(code);
|
|
140
|
-
frag.appendChild(pre);
|
|
141
|
-
}
|
|
142
|
-
flushParagraph(para);
|
|
143
|
-
if (list_el !== null) {
|
|
144
|
-
frag.appendChild(list_el);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
return frag;
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Append inline content parsing backticks and links safely.
|
|
151
|
-
* - Inline code: `text`
|
|
152
|
-
* - Links: [text](url) where url starts with http, https, or mailto
|
|
153
|
-
* @param {HTMLElement} el
|
|
154
|
-
* @param {string} text
|
|
155
|
-
*/
|
|
156
|
-
function appendInline(el, text) {
|
|
157
|
-
/** @type {RegExp} */
|
|
158
|
-
const re = /(`[^`]+`)|(\[[^\]]+\]\([^)]+\))/g;
|
|
159
|
-
/** @type {number} */
|
|
160
|
-
let last = 0;
|
|
161
|
-
/** @type {any} */
|
|
162
|
-
let match = re.exec(text);
|
|
163
|
-
while (match) {
|
|
164
|
-
const idx = match.index;
|
|
165
|
-
if (idx > last) {
|
|
166
|
-
el.appendChild(document.createTextNode(text.slice(last, idx)));
|
|
167
|
-
}
|
|
168
|
-
const token = match[0];
|
|
169
|
-
if (token[0] === '\u0060') {
|
|
170
|
-
const code = document.createElement('code');
|
|
171
|
-
code.textContent = token.slice(1, -1);
|
|
172
|
-
el.appendChild(code);
|
|
173
|
-
} else if (token[0] === '[') {
|
|
174
|
-
// parse [text](url)
|
|
175
|
-
const m2 = /^\[([^\]]+)\]\(([^)]+)\)$/.exec(token);
|
|
176
|
-
if (m2) {
|
|
177
|
-
const a = document.createElement('a');
|
|
178
|
-
const href = m2[2].trim();
|
|
179
|
-
const allowed = /^(https?:|mailto:)/i.test(href);
|
|
180
|
-
if (allowed) {
|
|
181
|
-
a.setAttribute('href', href);
|
|
182
|
-
} else {
|
|
183
|
-
// if not allowed, render as text to avoid XSS vectors
|
|
184
|
-
a.remove();
|
|
185
|
-
el.appendChild(document.createTextNode(m2[1] + ' (' + href + ')'));
|
|
186
|
-
last = re.lastIndex;
|
|
187
|
-
match = re.exec(text);
|
|
188
|
-
continue;
|
|
189
|
-
}
|
|
190
|
-
a.appendChild(document.createTextNode(m2[1]));
|
|
191
|
-
el.appendChild(a);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
last = re.lastIndex;
|
|
195
|
-
match = re.exec(text);
|
|
196
|
-
}
|
|
197
|
-
if (last < text.length) {
|
|
198
|
-
el.appendChild(document.createTextNode(text.slice(last)));
|
|
199
|
-
}
|
|
200
|
-
}
|
|
12
|
+
export function renderMarkdown(markdown) {
|
|
13
|
+
const parsed = /** @type {string} */ (marked.parse(markdown));
|
|
14
|
+
const html_string = DOMPurify.sanitize(parsed);
|
|
15
|
+
return unsafeHTML(html_string);
|
|
201
16
|
}
|
|
@@ -2,13 +2,12 @@ import { priority_levels } from './priority.js';
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Create a colored badge for a priority value (0..4).
|
|
5
|
+
*
|
|
5
6
|
* @param {number | null | undefined} priority
|
|
6
7
|
* @returns {HTMLSpanElement}
|
|
7
8
|
*/
|
|
8
9
|
export function createPriorityBadge(priority) {
|
|
9
|
-
/** @type {number} */
|
|
10
10
|
const p = typeof priority === 'number' ? priority : 2;
|
|
11
|
-
/** @type {HTMLSpanElement} */
|
|
12
11
|
const el = document.createElement('span');
|
|
13
12
|
el.className = 'priority-badge';
|
|
14
13
|
el.classList.add(`is-p${Math.max(0, Math.min(4, p))}`);
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Create a colored badge for a status value.
|
|
3
|
+
*
|
|
3
4
|
* @param {string | null | undefined} status - 'open' | 'in_progress' | 'closed'
|
|
4
5
|
* @returns {HTMLSpanElement}
|
|
5
6
|
*/
|
|
6
7
|
export function createStatusBadge(status) {
|
|
7
|
-
/** @type {HTMLSpanElement} */
|
|
8
8
|
const el = document.createElement('span');
|
|
9
9
|
el.className = 'status-badge';
|
|
10
10
|
const s = String(status || 'open');
|
package/app/utils/status.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Known status values in canonical order.
|
|
3
|
+
*
|
|
3
4
|
* @type {Array<'open'|'in_progress'|'closed'>}
|
|
4
5
|
*/
|
|
5
6
|
export const STATUSES = ['open', 'in_progress', 'closed'];
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Map canonical status to display label.
|
|
10
|
+
*
|
|
9
11
|
* @param {string | null | undefined} status
|
|
10
12
|
* @returns {string}
|
|
11
13
|
*/
|
package/app/utils/toast.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Show a transient global toast message anchored to the viewport.
|
|
3
|
+
*
|
|
3
4
|
* @param {string} text - Message text.
|
|
4
5
|
* @param {'info'|'success'|'error'} [variant] - Visual variant.
|
|
5
6
|
* @param {number} [duration_ms] - Auto-dismiss delay in milliseconds.
|
|
6
7
|
*/
|
|
7
8
|
export function showToast(text, variant = 'info', duration_ms = 2800) {
|
|
8
|
-
/** @type {HTMLDivElement} */
|
|
9
9
|
const el = document.createElement('div');
|
|
10
10
|
el.className = 'toast';
|
|
11
11
|
el.textContent = text;
|
package/app/utils/type-badge.js
CHANGED
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Create a compact, colored badge for an issue type.
|
|
3
|
+
*
|
|
3
4
|
* @param {string | undefined | null} issue_type - One of: bug, feature, task, epic, chore
|
|
4
5
|
* @returns {HTMLSpanElement}
|
|
5
6
|
*/
|
|
6
7
|
export function createTypeBadge(issue_type) {
|
|
7
|
-
/** @type {HTMLSpanElement} */
|
|
8
8
|
const el = document.createElement('span');
|
|
9
9
|
el.className = 'type-badge';
|
|
10
10
|
|
|
11
|
-
/** @type {string} */
|
|
12
11
|
const t = (issue_type || '').toString().toLowerCase();
|
|
13
|
-
/** @type {Set<string>} */
|
|
14
12
|
const KNOWN = new Set(['bug', 'feature', 'task', 'epic', 'chore']);
|
|
15
13
|
const kind = KNOWN.has(t) ? t : 'neutral';
|
|
16
14
|
el.classList.add(`type-badge--${kind}`);
|