beads-ui 0.1.0 → 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.
Files changed (57) hide show
  1. package/CHANGES.md +8 -0
  2. package/README.md +7 -3
  3. package/package.json +12 -2
  4. package/.beads/issues.jsonl +0 -107
  5. package/.editorconfig +0 -10
  6. package/.eslintrc.json +0 -36
  7. package/.github/workflows/ci.yml +0 -38
  8. package/.prettierignore +0 -5
  9. package/AGENTS.md +0 -85
  10. package/app/data/providers.test.js +0 -126
  11. package/app/main.board-switch.test.js +0 -94
  12. package/app/main.deep-link.test.js +0 -64
  13. package/app/main.live-updates.test.js +0 -229
  14. package/app/main.test.js +0 -17
  15. package/app/main.theme.test.js +0 -41
  16. package/app/main.view-sync.test.js +0 -54
  17. package/app/protocol.test.js +0 -57
  18. package/app/router.test.js +0 -34
  19. package/app/state.test.js +0 -21
  20. package/app/utils/markdown.test.js +0 -103
  21. package/app/utils/type-badge.test.js +0 -30
  22. package/app/views/board.test.js +0 -184
  23. package/app/views/detail.acceptance-notes.test.js +0 -67
  24. package/app/views/detail.assignee.test.js +0 -161
  25. package/app/views/detail.deps.test.js +0 -97
  26. package/app/views/detail.edits.test.js +0 -146
  27. package/app/views/detail.labels.test.js +0 -73
  28. package/app/views/detail.priority.test.js +0 -86
  29. package/app/views/detail.test.js +0 -188
  30. package/app/views/detail.ui47.test.js +0 -78
  31. package/app/views/epics.test.js +0 -283
  32. package/app/views/list.inline-edits.test.js +0 -84
  33. package/app/views/list.test.js +0 -479
  34. package/app/views/nav.test.js +0 -43
  35. package/app/ws.test.js +0 -168
  36. package/eslint.config.js +0 -59
  37. package/media/bdui-board.png +0 -0
  38. package/media/bdui-epics.png +0 -0
  39. package/media/bdui-issues.png +0 -0
  40. package/prettier.config.js +0 -13
  41. package/server/app.test.js +0 -29
  42. package/server/bd.test.js +0 -93
  43. package/server/cli/cli.test.js +0 -109
  44. package/server/cli/commands.integration.test.js +0 -155
  45. package/server/cli/commands.unit.test.js +0 -94
  46. package/server/cli/open.test.js +0 -26
  47. package/server/db.test.js +0 -70
  48. package/server/protocol.test.js +0 -87
  49. package/server/watcher.test.js +0 -100
  50. package/server/ws.handlers.test.js +0 -174
  51. package/server/ws.labels.test.js +0 -95
  52. package/server/ws.mutations.test.js +0 -261
  53. package/server/ws.subscriptions.test.js +0 -116
  54. package/server/ws.test.js +0 -52
  55. package/test/setup-vitest.js +0 -12
  56. package/tsconfig.json +0 -23
  57. package/vitest.config.mjs +0 -14
@@ -1,161 +0,0 @@
1
- import { describe, expect, test, vi } from 'vitest';
2
- import { createDetailView } from './detail.js';
3
-
4
- /** @type {(impl: (type: string, payload?: unknown) => Promise<any>) => (type: string, payload?: unknown) => Promise<any>} */
5
- const mockSend = (impl) => vi.fn(impl);
6
-
7
- describe('views/detail assignee edit', () => {
8
- test('edits assignee via Properties control', async () => {
9
- document.body.innerHTML =
10
- '<section class="panel"><div id="mount"></div></section>';
11
- const mount = /** @type {HTMLElement} */ (document.getElementById('mount'));
12
-
13
- const issue = {
14
- id: 'UI-57',
15
- title: 'Detail screen',
16
- description: '',
17
- status: 'open',
18
- priority: 2,
19
- assignee: 'alice',
20
- dependencies: [],
21
- dependents: []
22
- };
23
-
24
- const send = mockSend(async (type, payload) => {
25
- if (type === 'show-issue') {
26
- return issue;
27
- }
28
- if (type === 'update-assignee') {
29
- expect(payload).toEqual({ id: 'UI-57', assignee: 'max' });
30
- const next = { ...issue, assignee: 'max' };
31
- return next;
32
- }
33
- throw new Error('Unexpected');
34
- });
35
-
36
- const view = createDetailView(mount, send);
37
- await view.load('UI-57');
38
-
39
- const assigneeSpan = /** @type {HTMLSpanElement} */ (
40
- mount.querySelector('#detail-root .prop.assignee .value .editable')
41
- );
42
- expect(assigneeSpan).toBeTruthy();
43
- expect(assigneeSpan.textContent).toBe('alice');
44
-
45
- assigneeSpan.click();
46
- const input = /** @type {HTMLInputElement} */ (
47
- mount.querySelector('#detail-root .prop.assignee input')
48
- );
49
- const saveBtn = /** @type {HTMLButtonElement} */ (
50
- mount.querySelector('#detail-root .prop.assignee button')
51
- );
52
- input.value = 'max';
53
- saveBtn.click();
54
-
55
- await Promise.resolve();
56
-
57
- const assigneeSpan2 = /** @type {HTMLSpanElement} */ (
58
- mount.querySelector('#detail-root .prop.assignee .value .editable')
59
- );
60
- expect(assigneeSpan2.textContent).toBe('max');
61
- });
62
-
63
- test('shows editable placeholder when unassigned', async () => {
64
- document.body.innerHTML =
65
- '<section class="panel"><div id="mount"></div></section>';
66
- const mount = /** @type {HTMLElement} */ (document.getElementById('mount'));
67
-
68
- const issue = {
69
- id: 'UI-88',
70
- title: 'No assignee yet',
71
- description: '',
72
- status: 'open',
73
- priority: 2,
74
- // no assignee field
75
- dependencies: [],
76
- dependents: []
77
- };
78
-
79
- const send = mockSend(async (type, payload) => {
80
- if (type === 'show-issue') {
81
- return issue;
82
- }
83
- if (type === 'update-assignee') {
84
- const next = {
85
- ...issue,
86
- assignee: /** @type {any} */ (payload).assignee
87
- };
88
- return next;
89
- }
90
- throw new Error('Unexpected');
91
- });
92
-
93
- const view = createDetailView(mount, send);
94
- await view.load('UI-88');
95
-
96
- const ph = /** @type {HTMLSpanElement} */ (
97
- mount.querySelector('#detail-root .prop.assignee .value .editable')
98
- );
99
- expect(ph).toBeTruthy();
100
- expect(ph.className).toContain('muted');
101
- expect(ph.textContent).toBe('Unassigned');
102
-
103
- ph.click();
104
- const input = /** @type {HTMLInputElement} */ (
105
- mount.querySelector('#detail-root .prop.assignee input')
106
- );
107
- expect(input).toBeTruthy();
108
- });
109
-
110
- test('clears assignee to empty string and shows placeholder', async () => {
111
- document.body.innerHTML =
112
- '<section class="panel"><div id="mount"></div></section>';
113
- const mount = /** @type {HTMLElement} */ (document.getElementById('mount'));
114
-
115
- const issue = {
116
- id: 'UI-31',
117
- title: 'Clearable',
118
- status: 'open',
119
- priority: 2,
120
- assignee: 'bob',
121
- dependencies: [],
122
- dependents: []
123
- };
124
-
125
- const send = mockSend(async (type, payload) => {
126
- if (type === 'show-issue') {
127
- return issue;
128
- }
129
- if (type === 'update-assignee') {
130
- const next = {
131
- ...issue,
132
- assignee: /** @type {any} */ (payload).assignee
133
- };
134
- return next;
135
- }
136
- throw new Error('Unexpected');
137
- });
138
-
139
- const view = createDetailView(mount, send);
140
- await view.load('UI-31');
141
-
142
- const span = /** @type {HTMLSpanElement} */ (
143
- mount.querySelector('#detail-root .prop.assignee .value .editable')
144
- );
145
- span.click();
146
- const input = /** @type {HTMLInputElement} */ (
147
- mount.querySelector('#detail-root .prop.assignee input')
148
- );
149
- const save = /** @type {HTMLButtonElement} */ (
150
- mount.querySelector('#detail-root .prop.assignee button')
151
- );
152
- input.value = '';
153
- save.click();
154
- await Promise.resolve();
155
- const span2 = /** @type {HTMLSpanElement} */ (
156
- mount.querySelector('#detail-root .prop.assignee .value .editable')
157
- );
158
- expect(span2.textContent).toBe('Unassigned');
159
- expect(span2.className).toContain('muted');
160
- });
161
- });
@@ -1,97 +0,0 @@
1
- import { describe, expect, test, vi } from 'vitest';
2
- import { createDetailView } from './detail.js';
3
-
4
- function setupDom() {
5
- const root = document.createElement('div');
6
- document.body.appendChild(root);
7
- return root;
8
- }
9
-
10
- describe('views/detail dependencies', () => {
11
- test('adds Dependencies link and re-renders', async () => {
12
- const mount = setupDom();
13
- const send = vi
14
- .fn()
15
- // initial show
16
- .mockResolvedValueOnce({
17
- id: 'UI-10',
18
- title: 'X',
19
- dependencies: [],
20
- dependents: []
21
- })
22
- // dep-add returns updated issue
23
- .mockResolvedValueOnce({
24
- id: 'UI-10',
25
- dependencies: [{ id: 'UI-2' }],
26
- dependents: []
27
- });
28
- const view = createDetailView(mount, /** @type {any} */ (send));
29
- await view.load('UI-10');
30
-
31
- const input = mount.querySelector('[data-testid="add-dependency"]');
32
- expect(input).toBeTruthy();
33
- /** @type {HTMLInputElement} */
34
- const el = /** @type {any} */ (input);
35
- el.value = 'UI-2';
36
- const addBtn = el.nextElementSibling;
37
- addBtn?.dispatchEvent(new window.Event('click'));
38
-
39
- // Next tick
40
- await Promise.resolve();
41
-
42
- // Should have called dep-add
43
- const calls = send.mock.calls.map((c) => c[0]);
44
- expect(calls.includes('dep-add')).toBe(true);
45
- });
46
-
47
- test('removes Blocks link', async () => {
48
- const mount = setupDom();
49
- const send = vi
50
- .fn()
51
- // initial show
52
- .mockResolvedValueOnce({
53
- id: 'UI-20',
54
- title: 'Y',
55
- dependencies: [],
56
- dependents: [{ id: 'UI-5' }]
57
- })
58
- // dep-remove returns updated issue
59
- .mockResolvedValueOnce({ id: 'UI-20', dependencies: [], dependents: [] });
60
- const view = createDetailView(mount, /** @type {any} */ (send));
61
- await view.load('UI-20');
62
-
63
- // Find the remove button next to link #5
64
- const btns = mount.querySelectorAll('button');
65
- const rm = Array.from(btns).find((b) =>
66
- b.getAttribute('aria-label')?.includes('#5')
67
- );
68
- expect(rm).toBeTruthy();
69
- rm?.dispatchEvent(new window.Event('click'));
70
-
71
- await Promise.resolve();
72
- const calls = send.mock.calls.map((c) => c[0]);
73
- expect(calls.includes('dep-remove')).toBe(true);
74
- });
75
-
76
- test('prevents duplicate link add', async () => {
77
- const mount = setupDom();
78
- const send = vi.fn().mockResolvedValueOnce({
79
- id: 'UI-30',
80
- dependencies: [{ id: 'UI-9' }],
81
- dependents: []
82
- });
83
- const view = createDetailView(mount, /** @type {any} */ (send));
84
- await view.load('UI-30');
85
-
86
- const input = mount.querySelector('[data-testid="add-dependency"]');
87
- const el = /** @type {HTMLInputElement} */ (/** @type {any} */ (input));
88
- el.value = 'UI-9';
89
- const addBtn = el.nextElementSibling;
90
- addBtn?.dispatchEvent(new window.Event('click'));
91
-
92
- await Promise.resolve();
93
- // send should not be called with dep-add
94
- const calls = send.mock.calls.map((c) => c[0]);
95
- expect(calls.includes('dep-add')).toBe(false);
96
- });
97
- });
@@ -1,146 +0,0 @@
1
- import { describe, expect, test, vi } from 'vitest';
2
- import { createDetailView } from './detail.js';
3
-
4
- /** @type {(impl: (type: string, payload?: unknown) => Promise<any>) => (type: string, payload?: unknown) => Promise<any>} */
5
- const mockSend = (impl) => vi.fn(impl);
6
-
7
- describe('views/detail edits', () => {
8
- test('updates status via dropdown and disables while pending', async () => {
9
- document.body.innerHTML =
10
- '<section class="panel"><div id="mount"></div></section>';
11
- const mount = /** @type {HTMLElement} */ (document.getElementById('mount'));
12
-
13
- const initial = {
14
- id: 'UI-7',
15
- title: 'T',
16
- description: 'D',
17
- status: 'open',
18
- priority: 2
19
- };
20
- const updated = { ...initial, status: 'in_progress' };
21
-
22
- const send = mockSend(async (type, payload) => {
23
- if (type === 'show-issue') {
24
- return initial;
25
- }
26
- if (type === 'update-status') {
27
- expect(payload).toEqual({ id: 'UI-7', status: 'in_progress' });
28
- // simulate server reconcile payload
29
- return updated;
30
- }
31
- throw new Error('Unexpected');
32
- });
33
-
34
- const view = createDetailView(mount, send);
35
- await view.load('UI-7');
36
-
37
- const select = /** @type {HTMLSelectElement} */ (
38
- mount.querySelector('select')
39
- );
40
- expect(select.value).toBe('open');
41
-
42
- // Trigger change
43
- select.value = 'in_progress';
44
- const beforeDisabled = select.disabled;
45
- select.dispatchEvent(new Event('change'));
46
- // After dispatch, the component sets disabled & will re-render upon reply
47
- expect(beforeDisabled || select.disabled).toBe(true);
48
-
49
- // After async flow, DOM should reflect updated status
50
- await Promise.resolve(); // allow microtasks
51
- const select2 = /** @type {HTMLSelectElement} */ (
52
- mount.querySelector('select')
53
- );
54
- expect(select2.value).toBe('in_progress');
55
- });
56
-
57
- test('saves title and re-renders from reply', async () => {
58
- document.body.innerHTML =
59
- '<section class="panel"><div id="mount"></div></section>';
60
- const mount = /** @type {HTMLElement} */ (document.getElementById('mount'));
61
- const initial = {
62
- id: 'UI-8',
63
- title: 'Old',
64
- description: '',
65
- status: 'open',
66
- priority: 1
67
- };
68
- const send = mockSend(async (type, payload) => {
69
- if (type === 'show-issue') {
70
- return initial;
71
- }
72
- if (type === 'edit-text') {
73
- const next = { ...initial, title: /** @type {any} */ (payload).value };
74
- return next;
75
- }
76
- throw new Error('Unexpected');
77
- });
78
- const view = createDetailView(mount, send);
79
- await view.load('UI-8');
80
- // Enter edit mode by clicking the span
81
- const titleSpan = /** @type {HTMLSpanElement} */ (
82
- mount.querySelector('h2 .editable')
83
- );
84
- titleSpan.click();
85
- const titleInput = /** @type {HTMLInputElement} */ (
86
- mount.querySelector('h2 input')
87
- );
88
- const titleSave = /** @type {HTMLButtonElement} */ (
89
- mount.querySelector('h2 button')
90
- );
91
- titleInput.value = 'New Title';
92
- titleSave.click();
93
- await Promise.resolve();
94
- // After save, returns to read mode with updated text
95
- const titleSpan2 = /** @type {HTMLSpanElement} */ (
96
- mount.querySelector('h2 .editable')
97
- );
98
- expect(titleSpan2.textContent).toBe('New Title');
99
- });
100
-
101
- test('shows toast on description save error and re-enables', async () => {
102
- vi.useFakeTimers();
103
- document.body.innerHTML =
104
- '<section class="panel"><div id="mount"></div></section>';
105
- const mount = /** @type {HTMLElement} */ (document.getElementById('mount'));
106
- const initial = {
107
- id: 'UI-9',
108
- title: 'T',
109
- description: 'D',
110
- status: 'open',
111
- priority: 2
112
- };
113
- const send = mockSend(async (type) => {
114
- if (type === 'show-issue') {
115
- return initial;
116
- }
117
- if (type === 'edit-text') {
118
- throw new Error('boom');
119
- }
120
- throw new Error('Unexpected');
121
- });
122
- const view = createDetailView(mount, send);
123
- await view.load('UI-9');
124
- // Enter edit mode
125
- const md = /** @type {HTMLDivElement} */ (mount.querySelector('.md'));
126
- md.click();
127
- const ta = /** @type {HTMLTextAreaElement} */ (
128
- mount.querySelector('textarea')
129
- );
130
- const btn = /** @type {HTMLButtonElement} */ (
131
- mount.querySelector('.editable-actions button')
132
- );
133
- ta.value = 'New D';
134
- btn.click();
135
- await Promise.resolve();
136
- // Toast appears
137
- const toast = /** @type {HTMLElement} */ (mount.querySelector('.toast'));
138
- expect(toast).not.toBeNull();
139
- expect((toast.textContent || '').toLowerCase()).toContain(
140
- 'failed to save description'
141
- );
142
- // Auto-dismiss after a while
143
- await vi.advanceTimersByTimeAsync(3000);
144
- vi.useRealTimers();
145
- });
146
- });
@@ -1,73 +0,0 @@
1
- import { describe, expect, test, vi } from 'vitest';
2
- import { createDetailView } from './detail.js';
3
-
4
- function mountDiv() {
5
- const div = document.createElement('div');
6
- document.body.appendChild(div);
7
- return div;
8
- }
9
-
10
- describe('detail view labels', () => {
11
- test('shows labels and allows add/remove', async () => {
12
- const mount = mountDiv();
13
- let current = {
14
- id: 'UI-5',
15
- title: 'With labels',
16
- status: 'open',
17
- priority: 2,
18
- labels: ['frontend']
19
- };
20
- const sendFn = vi.fn(async (type, payload) => {
21
- if (type === 'show-issue') {
22
- return current;
23
- }
24
- if (type === 'label-add') {
25
- current = { ...current, labels: [...current.labels, payload.label] };
26
- return current;
27
- }
28
- if (type === 'label-remove') {
29
- current = {
30
- ...current,
31
- labels: current.labels.filter((l) => l !== payload.label)
32
- };
33
- return current;
34
- }
35
- return current;
36
- });
37
-
38
- const view = createDetailView(mount, sendFn);
39
- await view.load('UI-5');
40
-
41
- // Initial chip present
42
- expect(mount.querySelectorAll('.prop.labels .badge').length).toBe(1);
43
-
44
- // Add a label via input + Enter
45
- const input = /** @type {HTMLInputElement} */ (
46
- mount.querySelector('.prop.labels input')
47
- );
48
- input.value = 'backend';
49
- input.dispatchEvent(new Event('input', { bubbles: true }));
50
- input.dispatchEvent(
51
- new KeyboardEvent('keydown', { key: 'Enter', bubbles: true })
52
- );
53
- await Promise.resolve();
54
-
55
- expect(sendFn).toHaveBeenCalledWith('label-add', {
56
- id: 'UI-5',
57
- label: 'backend'
58
- });
59
- expect(mount.querySelectorAll('.prop.labels .badge').length).toBe(2);
60
-
61
- // Remove the first label by clicking the × button
62
- const removeBtn = /** @type {HTMLButtonElement} */ (
63
- mount.querySelector('.prop.labels .badge button')
64
- );
65
- removeBtn.click();
66
- await Promise.resolve();
67
- expect(sendFn).toHaveBeenCalledWith('label-remove', {
68
- id: 'UI-5',
69
- label: 'frontend'
70
- });
71
- expect(mount.querySelectorAll('.prop.labels .badge').length).toBe(1);
72
- });
73
- });
@@ -1,86 +0,0 @@
1
- import { describe, expect, test, vi } from 'vitest';
2
- import { createDetailView } from './detail.js';
3
-
4
- /** @type {(impl: (type: string, payload?: unknown) => Promise<any>) => (type: string, payload?: unknown) => Promise<any>} */
5
- const mockSend = (impl) => vi.fn(impl);
6
-
7
- describe('views/detail priority edit', () => {
8
- test('updates priority via dropdown and re-renders from reply', async () => {
9
- document.body.innerHTML =
10
- '<section class="panel"><div id="mount"></div></section>';
11
- const mount = /** @type {HTMLElement} */ (document.getElementById('mount'));
12
-
13
- const initial = {
14
- id: 'UI-70',
15
- title: 'P',
16
- description: '',
17
- status: 'open',
18
- priority: 2
19
- };
20
- const send = mockSend(async (type, payload) => {
21
- if (type === 'show-issue') {
22
- return initial;
23
- }
24
- if (type === 'update-priority') {
25
- expect(payload).toEqual({ id: 'UI-70', priority: 4 });
26
- return { ...initial, priority: 4 };
27
- }
28
- throw new Error('Unexpected');
29
- });
30
-
31
- const view = createDetailView(mount, send);
32
- await view.load('UI-70');
33
-
34
- const selects = mount.querySelectorAll('select.badge--priority');
35
- expect(selects.length).toBe(1);
36
- const prio = /** @type {HTMLSelectElement} */ (selects[0]);
37
- expect(prio.value).toBe('2');
38
-
39
- prio.value = '4';
40
- prio.dispatchEvent(new Event('change'));
41
-
42
- await Promise.resolve();
43
-
44
- const prio2 = /** @type {HTMLSelectElement} */ (
45
- mount.querySelector('select.badge--priority')
46
- );
47
- expect(prio2.value).toBe('4');
48
- });
49
-
50
- test('shows toast on error and restores previous value', async () => {
51
- document.body.innerHTML =
52
- '<section class="panel"><div id="mount"></div></section>';
53
- const mount = /** @type {HTMLElement} */ (document.getElementById('mount'));
54
-
55
- const initial = { id: 'UI-71', title: 'Q', status: 'open', priority: 1 };
56
- const send = mockSend(async (type) => {
57
- if (type === 'show-issue') {
58
- return initial;
59
- }
60
- if (type === 'update-priority') {
61
- throw new Error('oops');
62
- }
63
- throw new Error('Unexpected');
64
- });
65
-
66
- const view = createDetailView(mount, send);
67
- await view.load('UI-71');
68
- const prio = /** @type {HTMLSelectElement} */ (
69
- mount.querySelector('select.badge--priority')
70
- );
71
- expect(prio.value).toBe('1');
72
-
73
- prio.value = '3';
74
- prio.dispatchEvent(new Event('change'));
75
-
76
- await Promise.resolve();
77
-
78
- const toast = mount.querySelector('.toast');
79
- expect(toast).toBeTruthy();
80
- // Should restore previous value
81
- const prio2 = /** @type {HTMLSelectElement} */ (
82
- mount.querySelector('select.badge--priority')
83
- );
84
- expect(prio2.value).toBe('1');
85
- });
86
- });