beads-ui 0.3.0 → 0.4.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.
Files changed (61) hide show
  1. package/CHANGES.md +26 -0
  2. package/README.md +15 -6
  3. package/app/main.bundle.js +617 -0
  4. package/app/main.bundle.js.map +7 -0
  5. package/bin/bdui.js +2 -1
  6. package/package.json +27 -16
  7. package/server/app.js +39 -35
  8. package/server/bd.js +6 -2
  9. package/server/cli/commands.js +12 -8
  10. package/server/cli/daemon.js +20 -5
  11. package/server/cli/index.js +19 -31
  12. package/server/cli/open.js +3 -0
  13. package/server/cli/usage.js +4 -2
  14. package/server/config.js +3 -2
  15. package/server/db.js +9 -6
  16. package/server/index.js +10 -4
  17. package/server/list-adapters.js +9 -3
  18. package/server/logging.js +23 -0
  19. package/server/subscriptions.js +12 -0
  20. package/server/validators.js +2 -0
  21. package/server/watcher.js +10 -5
  22. package/server/ws.js +31 -10
  23. package/app/data/list-selectors.js +0 -98
  24. package/app/data/providers.js +0 -76
  25. package/app/data/sort.js +0 -45
  26. package/app/data/subscription-issue-store.js +0 -161
  27. package/app/data/subscription-issue-stores.js +0 -102
  28. package/app/data/subscriptions-store.js +0 -219
  29. package/app/main.js +0 -702
  30. package/app/protocol.js +0 -196
  31. package/app/protocol.md +0 -66
  32. package/app/router.js +0 -114
  33. package/app/state.js +0 -103
  34. package/app/utils/issue-id-renderer.js +0 -71
  35. package/app/utils/issue-id.js +0 -10
  36. package/app/utils/issue-type.js +0 -27
  37. package/app/utils/issue-url.js +0 -9
  38. package/app/utils/markdown.js +0 -22
  39. package/app/utils/priority-badge.js +0 -47
  40. package/app/utils/priority.js +0 -1
  41. package/app/utils/status-badge.js +0 -32
  42. package/app/utils/status.js +0 -23
  43. package/app/utils/toast.js +0 -34
  44. package/app/utils/type-badge.js +0 -33
  45. package/app/views/board.js +0 -535
  46. package/app/views/detail.js +0 -1249
  47. package/app/views/epics.js +0 -280
  48. package/app/views/issue-dialog.js +0 -163
  49. package/app/views/issue-row.js +0 -190
  50. package/app/views/list.js +0 -464
  51. package/app/views/nav.js +0 -67
  52. package/app/views/new-issue-dialog.js +0 -345
  53. package/app/ws.js +0 -279
  54. package/docs/adr/001-push-only-lists.md +0 -134
  55. package/docs/adr/002-per-subscription-stores-and-full-issue-push.md +0 -200
  56. package/docs/architecture.md +0 -194
  57. package/docs/data-exchange-subscription-plan.md +0 -198
  58. package/docs/db-watching.md +0 -30
  59. package/docs/migration-v2.md +0 -54
  60. package/docs/protocol/issues-push-v2.md +0 -179
  61. package/docs/subscription-issue-store.md +0 -112
package/app/protocol.js DELETED
@@ -1,196 +0,0 @@
1
- /**
2
- * Protocol definitions for beads-ui WebSocket communication.
3
- *
4
- * Conventions
5
- * - All messages are JSON objects.
6
- * - Client → Server uses RequestEnvelope.
7
- * - Server → Client uses ReplyEnvelope.
8
- * - Every request is correlated by `id` in replies.
9
- * - Server can also send unsolicited events (e.g., subscription `snapshot`).
10
- */
11
-
12
- /** @typedef {'list-issues'|'update-status'|'edit-text'|'update-priority'|'create-issue'|'list-ready'|'dep-add'|'dep-remove'|'epic-status'|'update-assignee'|'label-add'|'label-remove'|'subscribe-list'|'unsubscribe-list'|'snapshot'|'upsert'|'delete'} MessageType */
13
-
14
- /**
15
- * @typedef {Object} RequestEnvelope
16
- * @property {string} id - Unique id to correlate request/response.
17
- * @property {MessageType} type - Message type.
18
- * @property {unknown} [payload] - Message payload.
19
- */
20
-
21
- /**
22
- * @typedef {Object} ErrorObject
23
- * @property {string} code - Stable error code.
24
- * @property {string} message - Human-readable message.
25
- * @property {unknown} [details] - Optional extra info for debugging.
26
- */
27
-
28
- /**
29
- * @typedef {Object} ReplyEnvelope
30
- * @property {string} id - Correlates to the originating request.
31
- * @property {boolean} ok - True when request succeeded; false on error.
32
- * @property {MessageType} type - Echoes request type (or event type).
33
- * @property {unknown} [payload] - Response payload.
34
- * @property {ErrorObject} [error] - Present when ok=false.
35
- */
36
-
37
- /** @type {MessageType[]} */
38
- export const MESSAGE_TYPES = /** @type {const} */ ([
39
- 'list-issues',
40
- 'update-status',
41
- 'edit-text',
42
- 'update-priority',
43
- 'create-issue',
44
- 'list-ready',
45
- 'dep-add',
46
- 'dep-remove',
47
- 'epic-status',
48
- 'update-assignee',
49
- 'label-add',
50
- 'label-remove',
51
- 'subscribe-list',
52
- 'unsubscribe-list',
53
- // vNext per-subscription full-issue push events
54
- 'snapshot',
55
- 'upsert',
56
- 'delete'
57
- ]);
58
-
59
- /**
60
- * Generate a lexically sortable request id.
61
- * @returns {string}
62
- */
63
- export function nextId() {
64
- const now = Date.now().toString(36);
65
- const rand = Math.random().toString(36).slice(2, 8);
66
- return `${now}-${rand}`;
67
- }
68
-
69
- /**
70
- * Create a request envelope.
71
- * @param {MessageType} type - Message type.
72
- * @param {unknown} [payload] - Message payload.
73
- * @param {string} [id] - Optional id; generated if omitted.
74
- * @returns {RequestEnvelope}
75
- */
76
- export function makeRequest(type, payload, id = nextId()) {
77
- return { id, type, payload };
78
- }
79
-
80
- /**
81
- * Create a successful reply envelope for a given request.
82
- * @param {RequestEnvelope} req - Original request.
83
- * @param {unknown} [payload] - Reply payload.
84
- * @returns {ReplyEnvelope}
85
- */
86
- export function makeOk(req, payload) {
87
- return { id: req.id, ok: true, type: req.type, payload };
88
- }
89
-
90
- /**
91
- * Create an error reply envelope for a given request.
92
- * @param {RequestEnvelope} req - Original request.
93
- * @param {string} code - Error code.
94
- * @param {string} message - Error message.
95
- * @param {unknown} [details] - Extra details.
96
- * @returns {ReplyEnvelope}
97
- */
98
- export function makeError(req, code, message, details) {
99
- return {
100
- id: req.id,
101
- ok: false,
102
- type: req.type,
103
- error: { code, message, details }
104
- };
105
- }
106
-
107
- /**
108
- * Check if a value is a plain object.
109
- * @param {unknown} value
110
- * @returns {value is Record<string, unknown>}
111
- */
112
- function isRecord(value) {
113
- return !!value && typeof value === 'object' && !Array.isArray(value);
114
- }
115
-
116
- /**
117
- * Type guard for MessageType values.
118
- * @param {unknown} value
119
- * @returns {value is MessageType}
120
- */
121
- export function isMessageType(value) {
122
- return (
123
- typeof value === 'string' &&
124
- MESSAGE_TYPES.includes(/** @type {MessageType} */ (value))
125
- );
126
- }
127
-
128
- /**
129
- * Type guard for RequestEnvelope.
130
- * @param {unknown} value
131
- * @returns {value is RequestEnvelope}
132
- */
133
- export function isRequest(value) {
134
- if (!isRecord(value)) {
135
- return false;
136
- }
137
- return (
138
- typeof value.id === 'string' &&
139
- typeof value.type === 'string' &&
140
- (value.payload === undefined || 'payload' in value)
141
- );
142
- }
143
-
144
- /**
145
- * Type guard for ReplyEnvelope.
146
- * @param {unknown} value
147
- * @returns {value is ReplyEnvelope}
148
- */
149
- export function isReply(value) {
150
- if (!isRecord(value)) {
151
- return false;
152
- }
153
- if (
154
- typeof value.id !== 'string' ||
155
- typeof value.ok !== 'boolean' ||
156
- !isMessageType(value.type)
157
- ) {
158
- return false;
159
- }
160
- if (value.ok === false) {
161
- const err = value.error;
162
- if (
163
- !isRecord(err) ||
164
- typeof err.code !== 'string' ||
165
- typeof err.message !== 'string'
166
- ) {
167
- return false;
168
- }
169
- }
170
- return true;
171
- }
172
-
173
- /**
174
- * Normalize and validate an incoming JSON value as a RequestEnvelope.
175
- * Throws a user-friendly error if invalid.
176
- * @param {unknown} json
177
- * @returns {RequestEnvelope}
178
- */
179
- export function decodeRequest(json) {
180
- if (!isRequest(json)) {
181
- throw new Error('Invalid request envelope');
182
- }
183
- return json;
184
- }
185
-
186
- /**
187
- * Normalize and validate an incoming JSON value as a ReplyEnvelope.
188
- * @param {unknown} json
189
- * @returns {ReplyEnvelope}
190
- */
191
- export function decodeReply(json) {
192
- if (!isReply(json)) {
193
- throw new Error('Invalid reply envelope');
194
- }
195
- return json;
196
- }
package/app/protocol.md DELETED
@@ -1,66 +0,0 @@
1
- # beads-ui WebSocket Protocol (v2.0.0)
2
-
3
- Note (2025-10-26)
4
-
5
- - The server no longer implements legacy read RPCs `list-issues` and
6
- `epic-status`. Clients must use the push-only protocol described in
7
- `docs/protocol/issues-push-v2.md` (subscribe-list with per-subscription events
8
- snapshot/upsert/delete). The shapes below are retained for historical
9
- reference of v1.
10
-
11
- This document defines the JSON messages exchanged between the browser client and
12
- the local server.
13
-
14
- - Transport: single WebSocket connection
15
- - Encoding: JSON text frames
16
- - Correlation: all request/response pairs share the same `id`
17
-
18
- ## Envelope Shapes
19
-
20
- - RequestEnvelope: `{ id: string, type: string, payload?: any }`
21
- - ReplyEnvelope:
22
- `{ id: string, ok: boolean, type: string, payload?: any, error?: { code: string, message: string, details?: any } }`
23
-
24
- Server may send unsolicited events (e.g., subscription
25
- `snapshot`/`upsert`/`delete`) using the ReplyEnvelope shape with `ok: true` and
26
- a generated `id`.
27
-
28
- ## Message Types
29
-
30
- - Removed in v2: `list-issues` (use subscriptions + push stores)
31
- - `update-status` payload:
32
- `{ id: string, status: 'open'|'in_progress'|'closed' }`
33
- - `edit-text` payload:
34
- `{ id: string, field: 'title'|'description'|'acceptance'|'notes'|'design', value: string }`
35
- - `update-priority` payload: `{ id: string, priority: 0|1|2|3|4 }`
36
- - `create-issue` payload:
37
- `{ title: string, type?: 'bug'|'feature'|'task'|'epic'|'chore', priority?: 0|1|2|3|4, description?: string }`
38
- - `list-ready` payload: `{}`
39
- - Removed in v2: `subscribe-updates` and the `issues-changed` event. All list
40
- and detail updates flow via per-subscription push envelopes
41
- (`snapshot`/`upsert`/`delete`).
42
- - `dep-add` payload: `{ a: string, b: string, view_id?: string }` where `a`
43
- depends on `b` (i.e., `a` is blocked by `b`). Reply payload is the updated
44
- issue for `view_id` (or `a` when omitted).
45
- - `dep-remove` payload: `{ a: string, b: string, view_id?: string }` removing
46
- the `a` depends on `b` link. Reply payload is the updated issue for `view_id`
47
- (or `a`).
48
-
49
- ## Mapping to `bd` CLI
50
-
51
- - Removed in v2: `list-issues` → use subscriptions and push
52
- (`docs/protocol/issues-push-v2.md`)
53
- - `update-status` → `bd update <id> --status <status>`
54
- - `edit-text` → `bd update <id> --title <t>` or `--description <d>` or
55
- `--acceptance-criteria <a>` or `--notes <n>` or `--design <z>`
56
- - `update-priority` → `bd update <id> --priority <n>`
57
- - `create-issue` → `bd create "title" -t <type> -p <prio> -d "desc"`
58
- - `list-ready` → `bd ready --json`
59
-
60
- ## Errors
61
-
62
- Errors follow the shape `{ code, message, details? }`. Common codes:
63
-
64
- - `bad_request` – malformed payload or unknown type
65
- - `not_found` – entity not found (e.g., issue id)
66
- - `bd_error` – underlying `bd` command failed
package/app/router.js DELETED
@@ -1,114 +0,0 @@
1
- import { issueHashFor } from './utils/issue-url.js';
2
-
3
- /**
4
- * Hash-based router for tabs (issues/epics/board) and deep-linked issue ids.
5
- */
6
-
7
- /**
8
- * Parse an application hash and extract the selected issue id.
9
- * Supports canonical form "#/(issues|epics|board)?issue=<id>" and legacy
10
- * "#/issue/<id>" which we will rewrite to the canonical form.
11
- * @param {string} hash
12
- * @returns {string | null}
13
- */
14
- export function parseHash(hash) {
15
- const h = String(hash || '');
16
- // Extract the fragment sans leading '#'
17
- const frag = h.startsWith('#') ? h.slice(1) : h;
18
- const qIndex = frag.indexOf('?');
19
- const query = qIndex >= 0 ? frag.slice(qIndex + 1) : '';
20
- if (query) {
21
- const params = new URLSearchParams(query);
22
- const id = params.get('issue');
23
- if (id) {
24
- return decodeURIComponent(id);
25
- }
26
- }
27
- // Legacy pattern: #/issue/<id>
28
- const m = /^\/issue\/([^\s?#]+)/.exec(frag);
29
- return m && m[1] ? decodeURIComponent(m[1]) : null;
30
- }
31
-
32
- /**
33
- * Parse the current view from hash.
34
- * @param {string} hash
35
- * @returns {'issues'|'epics'|'board'}
36
- */
37
- export function parseView(hash) {
38
- const h = String(hash || '');
39
- if (/^#\/epics(\b|\/|$)/.test(h)) {
40
- return 'epics';
41
- }
42
- if (/^#\/board(\b|\/|$)/.test(h)) {
43
- return 'board';
44
- }
45
- // Default to issues (also covers #/issues and unknown/empty)
46
- return 'issues';
47
- }
48
-
49
- /**
50
- * @param {{ getState: () => any, setState: (patch: any) => void }} store
51
- */
52
- export function createHashRouter(store) {
53
- /** @type {(ev?: HashChangeEvent) => any} */
54
- const onHashChange = () => {
55
- const hash = window.location.hash || '';
56
- // Rewrite legacy #/issue/<id> to canonical #/issues?issue=<id>
57
- const legacyMatch = /^#\/issue\/([^\s?#]+)/.exec(hash);
58
- if (legacyMatch && legacyMatch[1]) {
59
- const id = decodeURIComponent(legacyMatch[1]);
60
- // Update state immediately for consumers expecting sync selection
61
- store.setState({ selected_id: id, view: 'issues' });
62
- const next = `#/issues?issue=${encodeURIComponent(id)}`;
63
- if (window.location.hash !== next) {
64
- window.location.hash = next;
65
- return; // will trigger handler again
66
- }
67
- }
68
- const id = parseHash(hash);
69
- const view = parseView(hash);
70
- store.setState({ selected_id: id, view });
71
- };
72
-
73
- return {
74
- start() {
75
- window.addEventListener('hashchange', onHashChange);
76
- onHashChange();
77
- },
78
- stop() {
79
- window.removeEventListener('hashchange', onHashChange);
80
- },
81
- /**
82
- * @param {string} id
83
- */
84
- gotoIssue(id) {
85
- // Keep current view in hash and append issue param via helper
86
- const s = store.getState ? store.getState() : { view: 'issues' };
87
- const view = s.view || 'issues';
88
- const next = issueHashFor(view, id);
89
- if (window.location.hash !== next) {
90
- window.location.hash = next;
91
- } else {
92
- // Force state update even if hash is the same
93
- store.setState({ selected_id: id, view });
94
- }
95
- },
96
- /**
97
- * Navigate to a top-level view.
98
- * @param {'issues'|'epics'|'board'} view
99
- */
100
- /**
101
- * @param {'issues'|'epics'|'board'} view
102
- */
103
- gotoView(view) {
104
- const s = store.getState ? store.getState() : { selected_id: null };
105
- const id = s.selected_id;
106
- const next = id ? issueHashFor(view, id) : `#/${view}`;
107
- if (window.location.hash !== next) {
108
- window.location.hash = next;
109
- } else {
110
- store.setState({ view, selected_id: null });
111
- }
112
- }
113
- };
114
- }
package/app/state.js DELETED
@@ -1,103 +0,0 @@
1
- /**
2
- * Minimal app state store with subscription.
3
- */
4
-
5
- /**
6
- * @typedef {'all'|'open'|'in_progress'|'closed'|'ready'} StatusFilter
7
- */
8
-
9
- /**
10
- * @typedef {{ status: StatusFilter, search: string, type: string }} Filters
11
- */
12
-
13
- /**
14
- * @typedef {'issues'|'epics'|'board'} ViewName
15
- */
16
-
17
- /**
18
- * @typedef {'today'|'3'|'7'} ClosedFilter
19
- */
20
-
21
- /**
22
- * @typedef {{ closed_filter: ClosedFilter }} BoardState
23
- */
24
-
25
- /**
26
- * @typedef {{ selected_id: string | null, view: ViewName, filters: Filters, board: BoardState }} AppState
27
- */
28
-
29
- /**
30
- * Create a simple store for application state.
31
- * @param {Partial<AppState>} [initial]
32
- * @returns {{ getState: () => AppState, setState: (patch: { selected_id?: string | null, filters?: Partial<Filters> }) => void, subscribe: (fn: (s: AppState) => void) => () => void }}
33
- */
34
- export function createStore(initial = {}) {
35
- /** @type {AppState} */
36
- let state = {
37
- selected_id: initial.selected_id ?? null,
38
- view: initial.view ?? 'issues',
39
- filters: {
40
- status: initial.filters?.status ?? 'all',
41
- search: initial.filters?.search ?? '',
42
- type:
43
- typeof initial.filters?.type === 'string' ? initial.filters?.type : ''
44
- },
45
- board: {
46
- closed_filter:
47
- initial.board?.closed_filter === '3' ||
48
- initial.board?.closed_filter === '7' ||
49
- initial.board?.closed_filter === 'today'
50
- ? initial.board?.closed_filter
51
- : 'today'
52
- }
53
- };
54
-
55
- /** @type {Set<(s: AppState) => void>} */
56
- const subs = new Set();
57
-
58
- function emit() {
59
- for (const fn of Array.from(subs)) {
60
- try {
61
- fn(state);
62
- } catch {
63
- // ignore
64
- }
65
- }
66
- }
67
-
68
- return {
69
- getState() {
70
- return state;
71
- },
72
- /**
73
- * Update state. Nested filters can be partial.
74
- * @param {{ selected_id?: string | null, filters?: Partial<Filters>, board?: Partial<BoardState> }} patch
75
- */
76
- setState(patch) {
77
- /** @type {AppState} */
78
- const next = {
79
- ...state,
80
- ...patch,
81
- filters: { ...state.filters, ...(patch.filters || {}) },
82
- board: { ...state.board, ...(patch.board || {}) }
83
- };
84
- // Avoid emitting if nothing changed (shallow compare)
85
- if (
86
- next.selected_id === state.selected_id &&
87
- next.view === state.view &&
88
- next.filters.status === state.filters.status &&
89
- next.filters.search === state.filters.search &&
90
- next.filters.type === state.filters.type &&
91
- next.board.closed_filter === state.board.closed_filter
92
- ) {
93
- return;
94
- }
95
- state = next;
96
- emit();
97
- },
98
- subscribe(fn) {
99
- subs.add(fn);
100
- return () => subs.delete(fn);
101
- }
102
- };
103
- }
@@ -1,71 +0,0 @@
1
- import { issueDisplayId } from './issue-id.js';
2
-
3
- /**
4
- * Create a reusable, copy-to-clipboard issue ID renderer.
5
- * Looks like the current inline ID (monospace `#123`) but acts as a button
6
- * that copies the full, prefixed ID (e.g., `UI-123`) when activated.
7
- * Shows transient "Copied" feedback and then restores the ID.
8
- * @param {string} id - Full issue id including the prefix (e.g., "UI-123").
9
- * @param {{ class_name?: string, duration_ms?: number }} [opts]
10
- * @returns {HTMLButtonElement}
11
- */
12
- export function createIssueIdRenderer(id, opts) {
13
- /** @type {number} */
14
- const duration =
15
- typeof opts?.duration_ms === 'number' ? opts.duration_ms : 1200;
16
- /** @type {HTMLButtonElement} */
17
- const btn = document.createElement('button');
18
- // Visual: match inline ID look; keep it neutral and text-like
19
- btn.className =
20
- (opts?.class_name ? opts.class_name + ' ' : '') + 'mono id-copy';
21
- btn.type = 'button';
22
- btn.setAttribute('aria-live', 'polite');
23
- btn.setAttribute('title', 'Copy issue ID');
24
- btn.setAttribute('aria-label', `Copy issue ID ${id}`);
25
- const label = issueDisplayId(id);
26
- btn.textContent = label;
27
-
28
- /** Copy handler with feedback */
29
- async function doCopy() {
30
- // Prevent accidental row navigation and parent handlers
31
- // (click/key handlers call this inside an event context)
32
- try {
33
- if (
34
- navigator.clipboard &&
35
- typeof navigator.clipboard.writeText === 'function'
36
- ) {
37
- await navigator.clipboard.writeText(String(id));
38
- }
39
- const prev = btn.textContent || label;
40
- btn.textContent = 'Copied';
41
- // Keep accessible label consistent with feedback
42
- const oldAria = btn.getAttribute('aria-label') || '';
43
- btn.setAttribute('aria-label', 'Copied');
44
- setTimeout(
45
- () => {
46
- btn.textContent = prev;
47
- btn.setAttribute('aria-label', oldAria);
48
- },
49
- Math.max(80, duration)
50
- );
51
- } catch {
52
- // On failure, leave text as-is; no throw to avoid disruptive UX
53
- }
54
- }
55
-
56
- btn.addEventListener('click', (ev) => {
57
- ev.preventDefault();
58
- ev.stopPropagation();
59
- void doCopy();
60
- });
61
- btn.addEventListener('keydown', (ev) => {
62
- // Ensure keyboard activation works even in non-interactive test envs
63
- if (ev.key === 'Enter' || ev.key === ' ') {
64
- ev.preventDefault();
65
- ev.stopPropagation();
66
- void doCopy();
67
- }
68
- });
69
-
70
- return btn;
71
- }
@@ -1,10 +0,0 @@
1
- /**
2
- * Format a beads issue id as a user-facing display string `#${n}`.
3
- * Extracts the trailing numeric portion of the id and prefixes with '#'.
4
- * @param {string | null | undefined} id
5
- * @returns {string}
6
- */
7
- export function issueDisplayId(id) {
8
- const m = String(id || '').match(/(\d+)$/);
9
- return m ? `#${m[1]}` : '#';
10
- }
@@ -1,27 +0,0 @@
1
- /**
2
- * Known issue types in canonical order for dropdowns.
3
- * @type {Array<'bug'|'feature'|'task'|'epic'|'chore'>}
4
- */
5
- export const ISSUE_TYPES = ['bug', 'feature', 'task', 'epic', 'chore'];
6
-
7
- /**
8
- * Return a human-friendly label for an issue type.
9
- * @param {string | null | undefined} type
10
- * @returns {string}
11
- */
12
- export function typeLabel(type) {
13
- switch ((type || '').toString().toLowerCase()) {
14
- case 'bug':
15
- return 'Bug';
16
- case 'feature':
17
- return 'Feature';
18
- case 'task':
19
- return 'Task';
20
- case 'epic':
21
- return 'Epic';
22
- case 'chore':
23
- return 'Chore';
24
- default:
25
- return '';
26
- }
27
- }
@@ -1,9 +0,0 @@
1
- /**
2
- * Build a canonical issue hash that retains the view.
3
- * @param {'issues'|'epics'|'board'} view
4
- * @param {string} id
5
- */
6
- export function issueHashFor(view, id) {
7
- const v = view === 'epics' || view === 'board' ? view : 'issues';
8
- return `#/${v}?issue=${encodeURIComponent(id)}`;
9
- }
@@ -1,22 +0,0 @@
1
- import DOMPurify from 'dompurify';
2
- import { html } from 'lit-html';
3
- import { unsafeHTML } from 'lit-html/directives/unsafe-html.js';
4
- import { marked } from 'marked';
5
-
6
- /**
7
- * Render Markdown safely as HTML using marked and DOMPurify.
8
- * Returns a lit-html TemplateResult via the unsafeHTML directive so it can be
9
- * embedded directly in templates.
10
- * @function renderMarkdown
11
- * @param {string} src Markdown source text
12
- * @returns {import('lit-html').TemplateResult}
13
- */
14
- export function renderMarkdown(src) {
15
- /** @type {string} */
16
- const markdown = String(src || '');
17
- /** @type {string} */
18
- const parsed = /** @type {string} */ (marked.parse(markdown));
19
- /** @type {string} */
20
- const html_string = DOMPurify.sanitize(parsed);
21
- return html`${unsafeHTML(html_string)}`;
22
- }
@@ -1,47 +0,0 @@
1
- import { priority_levels } from './priority.js';
2
-
3
- /**
4
- * Create a colored badge for a priority value (0..4).
5
- * @param {number | null | undefined} priority
6
- * @returns {HTMLSpanElement}
7
- */
8
- export function createPriorityBadge(priority) {
9
- const p = typeof priority === 'number' ? priority : 2;
10
- const el = document.createElement('span');
11
- el.className = 'priority-badge';
12
- el.classList.add(`is-p${Math.max(0, Math.min(4, p))}`);
13
- el.setAttribute('role', 'img');
14
- const label = labelForPriority(p);
15
- el.setAttribute('title', label);
16
- el.setAttribute('aria-label', `Priority: ${label}`);
17
- el.textContent = emojiForPriority(p) + ' ' + label;
18
- return el;
19
- }
20
-
21
- /**
22
- * @param {number} p
23
- */
24
- function labelForPriority(p) {
25
- const i = Math.max(0, Math.min(4, p));
26
- return priority_levels[i] || 'Medium';
27
- }
28
-
29
- /**
30
- * @param {number} p
31
- */
32
- export function emojiForPriority(p) {
33
- switch (p) {
34
- case 0:
35
- return '🔥';
36
- case 1:
37
- return '⚡️';
38
- case 2:
39
- return '🔧';
40
- case 3:
41
- return '🪶';
42
- case 4:
43
- return '💤';
44
- default:
45
- return '🔧';
46
- }
47
- }
@@ -1 +0,0 @@
1
- export const priority_levels = ['Critical', 'High', 'Medium', 'Low', 'Backlog'];