backlog-mcp 0.8.0 → 0.9.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.
- package/dist/schema.d.ts +14 -2
- package/dist/schema.js +26 -12
- package/dist/schema.js.map +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.js +38 -14
- package/dist/server.js.map +1 -1
- package/dist/viewer/components/task-badge.js +20 -0
- package/dist/viewer/components/task-detail.js +26 -2
- package/dist/viewer/components/task-filter-bar.js +31 -5
- package/dist/viewer/components/task-item.js +23 -11
- package/dist/viewer/components/task-list.js +42 -5
- package/dist/viewer/icons/index.js +3 -0
- package/dist/viewer/main.js +17 -19
- package/dist/viewer/utils/url-state.js +42 -0
- package/dist/viewer.js +3 -1
- package/dist/viewer.js.map +1 -1
- package/package.json +2 -2
- package/viewer/styles.css +148 -6
package/dist/schema.d.ts
CHANGED
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
export declare function isValidTaskId(id: unknown): id is string;
|
|
2
2
|
export declare function parseTaskId(id: string): number | null;
|
|
3
|
-
export declare function formatTaskId(num: number): string;
|
|
3
|
+
export declare function formatTaskId(num: number, type?: 'task' | 'epic'): string;
|
|
4
4
|
export declare function nextTaskId(existingTasks: ReadonlyArray<{
|
|
5
5
|
id: string;
|
|
6
|
-
}
|
|
6
|
+
}>, type?: 'task' | 'epic'): string;
|
|
7
7
|
export declare const STATUSES: readonly ["open", "in_progress", "blocked", "done", "cancelled"];
|
|
8
8
|
export type Status = (typeof STATUSES)[number];
|
|
9
|
+
export declare const TASK_TYPES: readonly ["task", "epic"];
|
|
10
|
+
export type TaskType = (typeof TASK_TYPES)[number];
|
|
11
|
+
export interface Reference {
|
|
12
|
+
url: string;
|
|
13
|
+
title?: string;
|
|
14
|
+
}
|
|
9
15
|
export interface Task {
|
|
10
16
|
id: string;
|
|
11
17
|
title: string;
|
|
12
18
|
description?: string;
|
|
13
19
|
status: Status;
|
|
20
|
+
type?: TaskType;
|
|
21
|
+
epic_id?: string;
|
|
22
|
+
references?: Reference[];
|
|
14
23
|
created_at: string;
|
|
15
24
|
updated_at: string;
|
|
16
25
|
blocked_reason?: string;
|
|
@@ -20,6 +29,9 @@ export interface CreateTaskInput {
|
|
|
20
29
|
id?: string;
|
|
21
30
|
title: string;
|
|
22
31
|
description?: string;
|
|
32
|
+
type?: TaskType;
|
|
33
|
+
epic_id?: string;
|
|
34
|
+
references?: Reference[];
|
|
23
35
|
}
|
|
24
36
|
export declare function createTask(input: CreateTaskInput, existingTasks?: ReadonlyArray<{
|
|
25
37
|
id: string;
|
package/dist/schema.js
CHANGED
|
@@ -2,41 +2,55 @@
|
|
|
2
2
|
// Task ID
|
|
3
3
|
// ============================================================================
|
|
4
4
|
const TASK_ID_PATTERN = /^TASK-(\d{4,})$/;
|
|
5
|
+
const EPIC_ID_PATTERN = /^EPIC-(\d{4,})$/;
|
|
5
6
|
export function isValidTaskId(id) {
|
|
6
|
-
return typeof id === 'string' && TASK_ID_PATTERN.test(id);
|
|
7
|
+
return typeof id === 'string' && (TASK_ID_PATTERN.test(id) || EPIC_ID_PATTERN.test(id));
|
|
7
8
|
}
|
|
8
9
|
export function parseTaskId(id) {
|
|
9
|
-
const match = TASK_ID_PATTERN.exec(id);
|
|
10
|
+
const match = TASK_ID_PATTERN.exec(id) || EPIC_ID_PATTERN.exec(id);
|
|
10
11
|
if (!match?.[1])
|
|
11
12
|
return null;
|
|
12
13
|
return parseInt(match[1], 10);
|
|
13
14
|
}
|
|
14
|
-
export function formatTaskId(num) {
|
|
15
|
-
|
|
15
|
+
export function formatTaskId(num, type) {
|
|
16
|
+
const prefix = type === 'epic' ? 'EPIC' : 'TASK';
|
|
17
|
+
return `${prefix}-${num.toString().padStart(4, '0')}`;
|
|
16
18
|
}
|
|
17
|
-
export function nextTaskId(existingTasks) {
|
|
19
|
+
export function nextTaskId(existingTasks, type) {
|
|
20
|
+
const pattern = type === 'epic' ? EPIC_ID_PATTERN : TASK_ID_PATTERN;
|
|
18
21
|
let maxNum = 0;
|
|
19
22
|
for (const task of existingTasks) {
|
|
20
|
-
const
|
|
21
|
-
if (
|
|
22
|
-
|
|
23
|
+
const match = pattern.exec(task.id);
|
|
24
|
+
if (match?.[1]) {
|
|
25
|
+
const num = parseInt(match[1], 10);
|
|
26
|
+
if (num > maxNum)
|
|
27
|
+
maxNum = num;
|
|
23
28
|
}
|
|
24
29
|
}
|
|
25
|
-
return formatTaskId(maxNum + 1);
|
|
30
|
+
return formatTaskId(maxNum + 1, type);
|
|
26
31
|
}
|
|
27
32
|
// ============================================================================
|
|
28
33
|
// Status
|
|
29
34
|
// ============================================================================
|
|
30
35
|
export const STATUSES = ['open', 'in_progress', 'blocked', 'done', 'cancelled'];
|
|
36
|
+
export const TASK_TYPES = ['task', 'epic'];
|
|
31
37
|
export function createTask(input, existingTasks = []) {
|
|
32
38
|
const now = new Date().toISOString();
|
|
33
|
-
|
|
34
|
-
id: input.id ?? nextTaskId(existingTasks),
|
|
39
|
+
const task = {
|
|
40
|
+
id: input.id ?? nextTaskId(existingTasks, input.type),
|
|
35
41
|
title: input.title,
|
|
36
|
-
description: input.description,
|
|
37
42
|
status: 'open',
|
|
38
43
|
created_at: now,
|
|
39
44
|
updated_at: now,
|
|
40
45
|
};
|
|
46
|
+
if (input.description)
|
|
47
|
+
task.description = input.description;
|
|
48
|
+
if (input.type)
|
|
49
|
+
task.type = input.type;
|
|
50
|
+
if (input.epic_id)
|
|
51
|
+
task.epic_id = input.epic_id;
|
|
52
|
+
if (input.references?.length)
|
|
53
|
+
task.references = input.references;
|
|
54
|
+
return task;
|
|
41
55
|
}
|
|
42
56
|
//# sourceMappingURL=schema.js.map
|
package/dist/schema.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,UAAU;AACV,+EAA+E;AAE/E,MAAM,eAAe,GAAG,iBAAiB,CAAC;AAE1C,MAAM,UAAU,aAAa,CAAC,EAAW;IACvC,OAAO,OAAO,EAAE,KAAK,QAAQ,IAAI,eAAe,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"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"}
|
package/dist/server.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
export {};
|
package/dist/server.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
try {
|
|
3
|
+
await import('dotenv/config');
|
|
4
|
+
}
|
|
5
|
+
catch { }
|
|
3
6
|
import { readFileSync } from 'node:fs';
|
|
4
7
|
import { dirname, join } from 'node:path';
|
|
5
8
|
import { fileURLToPath } from 'node:url';
|
|
6
9
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
7
10
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
8
11
|
import { z } from 'zod';
|
|
9
|
-
import { createTask, STATUSES } from './schema.js';
|
|
12
|
+
import { createTask, STATUSES, TASK_TYPES } from './schema.js';
|
|
10
13
|
import { storage } from './backlog.js';
|
|
11
14
|
import { startViewer } from './viewer.js';
|
|
12
15
|
// Read version from package.json
|
|
@@ -26,15 +29,21 @@ server.registerTool('backlog_list', {
|
|
|
26
29
|
description: 'List tasks from backlog. Shows open/in_progress/blocked by default. Use status=["done"] to see completed tasks.',
|
|
27
30
|
inputSchema: {
|
|
28
31
|
status: z.array(z.enum(STATUSES)).optional().describe('Filter: open, in_progress, blocked, done, cancelled. Default: open, in_progress, blocked'),
|
|
32
|
+
type: z.enum(TASK_TYPES).optional().describe('Filter by type: task, epic, or omit for all'),
|
|
33
|
+
epic_id: z.string().optional().describe('Filter tasks belonging to a specific epic'),
|
|
29
34
|
counts: z.boolean().optional().describe('Return counts per status instead of task list'),
|
|
30
35
|
limit: z.number().optional().describe('Max tasks to return. Default: 20'),
|
|
31
36
|
},
|
|
32
|
-
}, async ({ status, counts, limit }) => {
|
|
33
|
-
|
|
37
|
+
}, async ({ status, type, epic_id, counts, limit }) => {
|
|
38
|
+
let tasks = storage.list({ status, limit });
|
|
39
|
+
if (type)
|
|
40
|
+
tasks = tasks.filter(t => (t.type ?? 'task') === type);
|
|
41
|
+
if (epic_id)
|
|
42
|
+
tasks = tasks.filter(t => t.epic_id === epic_id);
|
|
34
43
|
if (counts) {
|
|
35
44
|
return { content: [{ type: 'text', text: JSON.stringify(storage.counts(), null, 2) }] };
|
|
36
45
|
}
|
|
37
|
-
const list = tasks.map((t) => ({ id: t.id, title: t.title, status: t.status }));
|
|
46
|
+
const list = tasks.map((t) => ({ id: t.id, title: t.title, status: t.status, type: t.type ?? 'task', epic_id: t.epic_id }));
|
|
38
47
|
return { content: [{ type: 'text', text: JSON.stringify(list, null, 2) }] };
|
|
39
48
|
});
|
|
40
49
|
server.registerTool('backlog_get', {
|
|
@@ -55,9 +64,12 @@ server.registerTool('backlog_create', {
|
|
|
55
64
|
inputSchema: {
|
|
56
65
|
title: z.string().describe('Task title'),
|
|
57
66
|
description: z.string().optional().describe('Task description in markdown'),
|
|
67
|
+
type: z.enum(TASK_TYPES).optional().describe('Type: task (default) or epic'),
|
|
68
|
+
epic_id: z.string().optional().describe('Parent epic ID to link this task to'),
|
|
69
|
+
references: z.array(z.object({ url: z.string(), title: z.string().optional() })).optional().describe('Reference links with optional titles'),
|
|
58
70
|
},
|
|
59
|
-
}, async ({ title, description }) => {
|
|
60
|
-
const task = createTask({ title, description }, storage.list());
|
|
71
|
+
}, async ({ title, description, type, epic_id, references }) => {
|
|
72
|
+
const task = createTask({ title, description, type, epic_id, references }, storage.list());
|
|
61
73
|
storage.add(task);
|
|
62
74
|
return { content: [{ type: 'text', text: `Created ${task.id}` }] };
|
|
63
75
|
});
|
|
@@ -68,20 +80,32 @@ server.registerTool('backlog_update', {
|
|
|
68
80
|
title: z.string().optional().describe('New title'),
|
|
69
81
|
description: z.string().optional().describe('New description'),
|
|
70
82
|
status: z.enum(STATUSES).optional().describe('New status'),
|
|
83
|
+
epic_id: z.string().nullable().optional().describe('Parent epic ID (null to unlink)'),
|
|
84
|
+
references: z.array(z.object({ url: z.string(), title: z.string().optional() })).optional().describe('Reference links with optional titles'),
|
|
71
85
|
blocked_reason: z.string().optional().describe('Reason if status is blocked'),
|
|
72
86
|
evidence: z.array(z.string()).optional().describe('Proof of completion when marking done - links to PRs, docs, or notes'),
|
|
73
87
|
},
|
|
74
|
-
}, async ({ id, title, description, status, blocked_reason, evidence }) => {
|
|
88
|
+
}, async ({ id, title, description, status, epic_id, references, blocked_reason, evidence }) => {
|
|
75
89
|
const task = storage.get(id);
|
|
76
90
|
if (!task) {
|
|
77
91
|
return { content: [{ type: 'text', text: `Not found: ${id}` }], isError: true };
|
|
78
92
|
}
|
|
79
|
-
const updates = {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
93
|
+
const updates = {};
|
|
94
|
+
if (title !== undefined)
|
|
95
|
+
updates.title = title;
|
|
96
|
+
if (description !== undefined)
|
|
97
|
+
updates.description = description;
|
|
98
|
+
if (status !== undefined)
|
|
99
|
+
updates.status = status;
|
|
100
|
+
if (epic_id !== undefined)
|
|
101
|
+
updates.epic_id = epic_id ?? undefined;
|
|
102
|
+
if (references !== undefined)
|
|
103
|
+
updates.references = references;
|
|
104
|
+
if (blocked_reason !== undefined)
|
|
105
|
+
updates.blocked_reason = blocked_reason;
|
|
106
|
+
if (evidence !== undefined)
|
|
107
|
+
updates.evidence = evidence;
|
|
108
|
+
const updated = { ...task, ...updates, updated_at: new Date().toISOString() };
|
|
85
109
|
storage.save(updated);
|
|
86
110
|
return { content: [{ type: 'text', text: `Updated ${id}` }] };
|
|
87
111
|
});
|
package/dist/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":";AAEA,
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { epicIcon, taskIcon } from '../icons/index.js';
|
|
2
|
+
export class TaskBadge extends HTMLElement {
|
|
3
|
+
connectedCallback() {
|
|
4
|
+
this.render();
|
|
5
|
+
}
|
|
6
|
+
static get observedAttributes() {
|
|
7
|
+
return ['task-id', 'type'];
|
|
8
|
+
}
|
|
9
|
+
attributeChangedCallback() {
|
|
10
|
+
this.render();
|
|
11
|
+
}
|
|
12
|
+
render() {
|
|
13
|
+
const id = this.getAttribute('task-id') || '';
|
|
14
|
+
const type = this.getAttribute('type') || 'task';
|
|
15
|
+
const icon = type === 'epic' ? epicIcon : taskIcon;
|
|
16
|
+
this.className = `task-badge type-${type}`;
|
|
17
|
+
this.innerHTML = `<span class="task-badge-icon">${icon}</span><span class="task-badge-id">${id}</span>`;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
customElements.define('task-badge', TaskBadge);
|
|
@@ -21,7 +21,8 @@ export class TaskDetail extends HTMLElement {
|
|
|
21
21
|
const task = await fetchTask(taskId);
|
|
22
22
|
const headerHtml = `
|
|
23
23
|
<div class="task-header-left">
|
|
24
|
-
|
|
24
|
+
${task.epic_id ? `<button class="btn-outline epic-id-btn" onclick="navigator.clipboard.writeText('${task.epic_id}')" title="Copy Epic ID"><task-badge task-id="${task.epic_id}" type="epic"></task-badge> ${copyIcon}</button>` : ''}
|
|
25
|
+
<button class="btn-outline task-id-btn" onclick="navigator.clipboard.writeText('${task.id}')" title="Copy ID"><task-badge task-id="${task.id}" type="${task.type || 'task'}"></task-badge> ${copyIcon}</button>
|
|
25
26
|
<span class="status-badge status-${task.status || 'open'}">${(task.status || 'open').replace('_', ' ')}</span>
|
|
26
27
|
${task.filePath ? `
|
|
27
28
|
<div class="task-meta-path">
|
|
@@ -38,10 +39,21 @@ export class TaskDetail extends HTMLElement {
|
|
|
38
39
|
const metaHtml = `
|
|
39
40
|
<div class="task-meta-card">
|
|
40
41
|
<h1 class="task-meta-title">${task.title || ''}</h1>
|
|
41
|
-
<div class="task-meta-
|
|
42
|
+
<div class="task-meta-row">
|
|
42
43
|
<span>Created: ${task.created_at ? new Date(task.created_at).toLocaleDateString() : ''}</span>
|
|
43
44
|
<span>Updated: ${task.updated_at ? new Date(task.updated_at).toLocaleDateString() : ''}</span>
|
|
45
|
+
${task.epic_id ? `<span class="task-meta-epic"><span class="task-meta-epic-label">Epic:</span><a href="#" class="epic-link" data-epic-id="${task.epic_id}"><task-badge task-id="${task.epic_id}" type="epic"></task-badge></a>${task.epicTitle ? `<span class="epic-title">${task.epicTitle}</span>` : ''}</span>` : ''}
|
|
44
46
|
</div>
|
|
47
|
+
${task.references?.length ? `
|
|
48
|
+
<div class="task-meta-evidence">
|
|
49
|
+
<div class="task-meta-evidence-label">References:</div>
|
|
50
|
+
<ul>${task.references.map((r) => {
|
|
51
|
+
const url = typeof r === 'string' ? r : r.url;
|
|
52
|
+
const title = typeof r === 'string' ? r : (r.title || r.url);
|
|
53
|
+
return `<li><a href="${url}" target="_blank" rel="noopener">${title}</a></li>`;
|
|
54
|
+
}).join('')}</ul>
|
|
55
|
+
</div>
|
|
56
|
+
` : ''}
|
|
45
57
|
${task.evidence?.length ? `
|
|
46
58
|
<div class="task-meta-evidence">
|
|
47
59
|
<div class="task-meta-evidence-label">Evidence:</div>
|
|
@@ -58,6 +70,18 @@ export class TaskDetail extends HTMLElement {
|
|
|
58
70
|
article.appendChild(mdBlock);
|
|
59
71
|
this.innerHTML = '';
|
|
60
72
|
this.appendChild(article);
|
|
73
|
+
// Bind epic link click to navigate
|
|
74
|
+
const epicLink = this.querySelector('.epic-link');
|
|
75
|
+
if (epicLink) {
|
|
76
|
+
epicLink.addEventListener('click', (e) => {
|
|
77
|
+
e.preventDefault();
|
|
78
|
+
const epicId = epicLink.dataset.epicId;
|
|
79
|
+
if (epicId) {
|
|
80
|
+
this.loadTask(epicId);
|
|
81
|
+
document.dispatchEvent(new CustomEvent('task-selected', { detail: { taskId: epicId } }));
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
61
85
|
// Bind copy raw button (in pane header)
|
|
62
86
|
const copyRawBtn = paneHeader?.querySelector('.copy-raw');
|
|
63
87
|
if (copyRawBtn && task.raw) {
|
|
@@ -3,31 +3,57 @@ const FILTERS = [
|
|
|
3
3
|
{ key: 'completed', label: 'Completed' },
|
|
4
4
|
{ key: 'all', label: 'All' },
|
|
5
5
|
];
|
|
6
|
+
const TYPE_FILTERS = [
|
|
7
|
+
{ key: 'task', label: 'Tasks' },
|
|
8
|
+
{ key: 'epic', label: 'Epics' },
|
|
9
|
+
{ key: 'all', label: 'All' },
|
|
10
|
+
];
|
|
6
11
|
export class TaskFilterBar extends HTMLElement {
|
|
7
12
|
currentFilter = 'active';
|
|
13
|
+
currentType = 'all';
|
|
8
14
|
connectedCallback() {
|
|
9
15
|
this.render();
|
|
10
16
|
this.attachListeners();
|
|
11
17
|
}
|
|
12
18
|
render() {
|
|
13
|
-
const
|
|
14
|
-
|
|
19
|
+
const statusButtons = FILTERS.map(f => `<button class="filter-btn ${this.currentFilter === f.key ? 'active' : ''}" data-filter="${f.key}">${f.label}</button>`).join('');
|
|
20
|
+
const typeButtons = TYPE_FILTERS.map(f => `<button class="filter-btn ${this.currentType === f.key ? 'active' : ''}" data-type="${f.key}">${f.label}</button>`).join('');
|
|
21
|
+
this.innerHTML = `
|
|
22
|
+
<div class="filter-bar"><span class="filter-label">Status</span>${statusButtons}</div>
|
|
23
|
+
<div class="filter-bar type-filter"><span class="filter-label">Type</span>${typeButtons}</div>
|
|
24
|
+
`;
|
|
15
25
|
}
|
|
16
26
|
attachListeners() {
|
|
17
|
-
this.querySelectorAll('
|
|
27
|
+
this.querySelectorAll('[data-filter]').forEach(btn => {
|
|
18
28
|
btn.addEventListener('click', (e) => {
|
|
19
29
|
const filter = e.target.dataset.filter;
|
|
20
30
|
if (filter)
|
|
21
31
|
this.setFilter(filter);
|
|
22
32
|
});
|
|
23
33
|
});
|
|
34
|
+
this.querySelectorAll('[data-type]').forEach(btn => {
|
|
35
|
+
btn.addEventListener('click', (e) => {
|
|
36
|
+
const type = e.target.dataset.type;
|
|
37
|
+
if (type)
|
|
38
|
+
this.setType(type);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
24
41
|
}
|
|
25
42
|
setFilter(filter) {
|
|
43
|
+
document.dispatchEvent(new CustomEvent('filter-change', { detail: { filter, type: this.currentType } }));
|
|
44
|
+
}
|
|
45
|
+
setType(type) {
|
|
46
|
+
document.dispatchEvent(new CustomEvent('filter-change', { detail: { filter: this.currentFilter, type } }));
|
|
47
|
+
}
|
|
48
|
+
setState(filter, type) {
|
|
26
49
|
this.currentFilter = filter;
|
|
27
|
-
this.
|
|
50
|
+
this.currentType = type;
|
|
51
|
+
this.querySelectorAll('[data-filter]').forEach(btn => {
|
|
28
52
|
btn.classList.toggle('active', btn.dataset.filter === filter);
|
|
29
53
|
});
|
|
30
|
-
|
|
54
|
+
this.querySelectorAll('[data-type]').forEach(btn => {
|
|
55
|
+
btn.classList.toggle('active', btn.dataset.type === type);
|
|
56
|
+
});
|
|
31
57
|
}
|
|
32
58
|
}
|
|
33
59
|
customElements.define('task-filter-bar', TaskFilterBar);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { pinIcon } from '../icons/index.js';
|
|
1
2
|
export class TaskItem extends HTMLElement {
|
|
2
3
|
connectedCallback() {
|
|
3
4
|
this.render();
|
|
@@ -7,39 +8,50 @@ export class TaskItem extends HTMLElement {
|
|
|
7
8
|
const id = this.dataset.id || '';
|
|
8
9
|
const title = this.dataset.title || '';
|
|
9
10
|
const status = this.dataset.status || 'open';
|
|
11
|
+
const type = this.dataset.type || 'task';
|
|
12
|
+
const isChild = this.dataset.child === 'true';
|
|
13
|
+
const isPinned = this.hasAttribute('pinned');
|
|
10
14
|
const isSelected = this.hasAttribute('selected');
|
|
11
|
-
this.className = `task-item ${
|
|
15
|
+
this.className = `task-item-wrapper ${isPinned ? 'pinned' : ''} ${isChild ? 'child' : ''}`;
|
|
12
16
|
this.innerHTML = `
|
|
13
|
-
<div class="task-item
|
|
14
|
-
<
|
|
17
|
+
<div class="task-item ${isSelected ? 'selected' : ''} type-${type}">
|
|
18
|
+
<task-badge task-id="${id}" type="${type}"></task-badge>
|
|
15
19
|
<span class="task-title">${title}</span>
|
|
16
20
|
<span class="status-badge status-${status}">${status.replace('_', ' ')}</span>
|
|
17
21
|
</div>
|
|
22
|
+
${type === 'epic' ? `<button class="pin-btn ${isPinned ? 'pinned' : ''}" title="${isPinned ? 'Unpin' : 'Pin to filter'}">${pinIcon}</button>` : ''}
|
|
18
23
|
`;
|
|
19
24
|
}
|
|
20
25
|
attachListeners() {
|
|
21
|
-
this.
|
|
26
|
+
const taskItem = this.querySelector('.task-item');
|
|
27
|
+
taskItem?.addEventListener('click', () => {
|
|
22
28
|
const taskId = this.dataset.id;
|
|
23
29
|
if (!taskId)
|
|
24
30
|
return;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const htmlItem = item;
|
|
28
|
-
item.classList.toggle('selected', htmlItem.dataset.id === taskId);
|
|
31
|
+
document.querySelectorAll('task-item .task-item').forEach(item => {
|
|
32
|
+
item.classList.toggle('selected', item.closest('task-item')?.dataset.id === taskId);
|
|
29
33
|
});
|
|
30
|
-
// Notify detail pane
|
|
31
34
|
const detailPane = document.querySelector('task-detail');
|
|
32
35
|
if (detailPane) {
|
|
33
36
|
detailPane.loadTask(taskId);
|
|
34
37
|
}
|
|
35
|
-
// Emit event for URL state
|
|
36
38
|
document.dispatchEvent(new CustomEvent('task-selected', { detail: { taskId } }));
|
|
37
|
-
// Update task list's selected state
|
|
38
39
|
const taskList = document.querySelector('task-list');
|
|
39
40
|
if (taskList) {
|
|
40
41
|
taskList.setSelected(taskId);
|
|
41
42
|
}
|
|
42
43
|
});
|
|
44
|
+
const pinBtn = this.querySelector('.pin-btn');
|
|
45
|
+
if (pinBtn) {
|
|
46
|
+
pinBtn.addEventListener('click', (e) => {
|
|
47
|
+
e.stopPropagation();
|
|
48
|
+
const epicId = this.dataset.id;
|
|
49
|
+
if (!epicId)
|
|
50
|
+
return;
|
|
51
|
+
const isPinned = this.hasAttribute('pinned');
|
|
52
|
+
document.dispatchEvent(new CustomEvent('epic-pin', { detail: { epicId: isPinned ? null : epicId } }));
|
|
53
|
+
});
|
|
54
|
+
}
|
|
43
55
|
}
|
|
44
56
|
}
|
|
45
57
|
customElements.define('task-item', TaskItem);
|
|
@@ -4,26 +4,48 @@ function escapeAttr(text) {
|
|
|
4
4
|
}
|
|
5
5
|
export class TaskList extends HTMLElement {
|
|
6
6
|
currentFilter = 'active';
|
|
7
|
+
currentType = 'all';
|
|
8
|
+
pinnedEpicId = null;
|
|
7
9
|
selectedTaskId = null;
|
|
8
10
|
connectedCallback() {
|
|
9
|
-
// Get initial selection from URL
|
|
10
11
|
const params = new URLSearchParams(window.location.search);
|
|
11
12
|
this.selectedTaskId = params.get('task');
|
|
13
|
+
this.pinnedEpicId = params.get('epic');
|
|
12
14
|
this.loadTasks();
|
|
13
15
|
setInterval(() => this.loadTasks(), 5000);
|
|
14
|
-
// Listen for filter changes
|
|
15
16
|
document.addEventListener('filter-change', ((e) => {
|
|
16
17
|
this.currentFilter = e.detail.filter;
|
|
18
|
+
this.currentType = e.detail.type ?? 'all';
|
|
17
19
|
this.loadTasks();
|
|
18
20
|
}));
|
|
19
|
-
// Listen for task selection
|
|
20
21
|
document.addEventListener('task-selected', ((e) => {
|
|
21
22
|
this.setSelected(e.detail.taskId);
|
|
22
23
|
}));
|
|
24
|
+
document.addEventListener('epic-pin', ((e) => {
|
|
25
|
+
this.pinnedEpicId = e.detail.epicId;
|
|
26
|
+
this.loadTasks();
|
|
27
|
+
}));
|
|
28
|
+
}
|
|
29
|
+
setState(filter, type, epicId, taskId) {
|
|
30
|
+
this.currentFilter = filter;
|
|
31
|
+
this.currentType = type;
|
|
32
|
+
this.pinnedEpicId = epicId;
|
|
33
|
+
this.selectedTaskId = taskId;
|
|
34
|
+
this.loadTasks();
|
|
23
35
|
}
|
|
24
36
|
async loadTasks() {
|
|
25
37
|
try {
|
|
26
|
-
|
|
38
|
+
let tasks = await fetchTasks(this.currentFilter);
|
|
39
|
+
// Type filter
|
|
40
|
+
if (this.currentType !== 'all') {
|
|
41
|
+
tasks = tasks.filter(t => (t.type ?? 'task') === this.currentType);
|
|
42
|
+
}
|
|
43
|
+
// Epic pin filter
|
|
44
|
+
if (this.pinnedEpicId) {
|
|
45
|
+
const pinnedEpic = tasks.find(t => t.id === this.pinnedEpicId);
|
|
46
|
+
const children = tasks.filter(t => t.epic_id === this.pinnedEpicId);
|
|
47
|
+
tasks = pinnedEpic ? [pinnedEpic, ...children] : children;
|
|
48
|
+
}
|
|
27
49
|
this.render(tasks);
|
|
28
50
|
}
|
|
29
51
|
catch (error) {
|
|
@@ -40,14 +62,29 @@ export class TaskList extends HTMLElement {
|
|
|
40
62
|
`;
|
|
41
63
|
return;
|
|
42
64
|
}
|
|
65
|
+
// Group: epics first with their children, then orphan tasks
|
|
66
|
+
const epics = tasks.filter(t => (t.type ?? 'task') === 'epic');
|
|
67
|
+
const childTasks = tasks.filter(t => t.epic_id && epics.some(e => e.id === t.epic_id));
|
|
68
|
+
const orphanTasks = tasks.filter(t => (t.type ?? 'task') === 'task' && !childTasks.includes(t));
|
|
69
|
+
const grouped = [];
|
|
70
|
+
for (const epic of epics) {
|
|
71
|
+
grouped.push(epic);
|
|
72
|
+
for (const child of childTasks.filter(t => t.epic_id === epic.id)) {
|
|
73
|
+
grouped.push({ ...child, isChild: true });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
grouped.push(...orphanTasks);
|
|
43
77
|
this.innerHTML = `
|
|
44
78
|
<div class="task-list">
|
|
45
|
-
${
|
|
79
|
+
${grouped.map(task => `
|
|
46
80
|
<task-item
|
|
47
81
|
data-id="${task.id}"
|
|
48
82
|
data-title="${escapeAttr(task.title)}"
|
|
49
83
|
data-status="${task.status}"
|
|
84
|
+
data-type="${task.type ?? 'task'}"
|
|
85
|
+
${task.isChild ? 'data-child="true"' : ''}
|
|
50
86
|
${this.selectedTaskId === task.id ? 'selected' : ''}
|
|
87
|
+
${this.pinnedEpicId === task.id ? 'pinned' : ''}
|
|
51
88
|
></task-item>
|
|
52
89
|
`).join('')}
|
|
53
90
|
</div>
|
|
@@ -1 +1,4 @@
|
|
|
1
1
|
export const copyIcon = `<svg viewBox="0 0 16 16" width="14" height="14" fill="currentColor"><path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 010 1.5h-1.5a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-1.5a.75.75 0 011.5 0v1.5A1.75 1.75 0 019.25 16h-7.5A1.75 1.75 0 010 14.25v-7.5z"/><path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0114.25 11h-7.5A1.75 1.75 0 015 9.25v-7.5zm1.75-.25a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-7.5a.25.25 0 00-.25-.25h-7.5z"/></svg>`;
|
|
2
|
+
export const pinIcon = `<svg viewBox="0 0 16 16" width="14" height="14" fill="currentColor"><path d="M4.456.734a1.75 1.75 0 012.826.504l.613 1.327a3.08 3.08 0 002.084 1.707l2.454.584c1.332.317 1.8 1.972.832 2.94L11.06 10l3.72 3.72a.75.75 0 11-1.06 1.06L10 11.06l-2.204 2.205c-.968.968-2.623.5-2.94-.832l-.584-2.454a3.08 3.08 0 00-1.707-2.084l-1.327-.613a1.75 1.75 0 01-.504-2.826L4.456.734z"/></svg>`;
|
|
3
|
+
export const epicIcon = `<svg viewBox="0 0 16 16" width="14" height="14" fill="none" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="epicGrad" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#f0b429"/><stop offset="100%" stop-color="#ff6b2d"/></linearGradient></defs><polygon points="8,1 14,4.5 14,11.5 8,15 2,11.5 2,4.5" stroke="url(#epicGrad)" stroke-width="1.5" fill="none"/><circle cx="8" cy="8" r="2" fill="url(#epicGrad)"/></svg>`;
|
|
4
|
+
export const taskIcon = `<svg viewBox="0 0 16 16" width="14" height="14" fill="none" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="taskGrad" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#00d4ff"/><stop offset="100%" stop-color="#7b2dff"/></linearGradient></defs><polygon points="8,1 15,8 8,15 1,8" stroke="url(#taskGrad)" stroke-width="1.5" fill="none"/></svg>`;
|
package/dist/viewer/main.js
CHANGED
|
@@ -2,30 +2,28 @@ import './components/task-filter-bar.js';
|
|
|
2
2
|
import './components/task-list.js';
|
|
3
3
|
import './components/task-item.js';
|
|
4
4
|
import './components/task-detail.js';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
// Set initial filter
|
|
10
|
-
document.addEventListener('DOMContentLoaded', () => {
|
|
5
|
+
import './components/task-badge.js';
|
|
6
|
+
import { urlState } from './utils/url-state.js';
|
|
7
|
+
// Subscribe components to URL state changes - single source of truth
|
|
8
|
+
urlState.subscribe((state) => {
|
|
11
9
|
const filterBar = document.querySelector('task-filter-bar');
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
filterBar?.setState?.(state.filter, state.type);
|
|
11
|
+
const taskList = document.querySelector('task-list');
|
|
12
|
+
taskList?.setState?.(state.filter, state.type, state.epic, state.task);
|
|
13
|
+
if (state.task) {
|
|
15
14
|
const detail = document.querySelector('task-detail');
|
|
16
|
-
|
|
17
|
-
detail.loadTask(initialTask);
|
|
15
|
+
detail?.loadTask?.(state.task);
|
|
18
16
|
}
|
|
19
17
|
});
|
|
20
|
-
//
|
|
18
|
+
// Initialize on load
|
|
19
|
+
document.addEventListener('DOMContentLoaded', () => urlState.init());
|
|
20
|
+
// Component events -> URL updates
|
|
21
21
|
document.addEventListener('filter-change', ((e) => {
|
|
22
|
-
|
|
23
|
-
url.searchParams.set('filter', e.detail.filter);
|
|
24
|
-
history.replaceState(null, '', url);
|
|
22
|
+
urlState.set({ filter: e.detail.filter, type: e.detail.type });
|
|
25
23
|
}));
|
|
26
|
-
// Update URL on task selection
|
|
27
24
|
document.addEventListener('task-selected', ((e) => {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
urlState.set({ task: e.detail.taskId });
|
|
26
|
+
}));
|
|
27
|
+
document.addEventListener('epic-pin', ((e) => {
|
|
28
|
+
urlState.set({ epic: e.detail.epicId });
|
|
31
29
|
}));
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
class UrlState {
|
|
2
|
+
listeners = [];
|
|
3
|
+
pushing = false;
|
|
4
|
+
constructor() {
|
|
5
|
+
window.addEventListener('popstate', () => this.notify());
|
|
6
|
+
}
|
|
7
|
+
get() {
|
|
8
|
+
const params = new URLSearchParams(window.location.search);
|
|
9
|
+
return {
|
|
10
|
+
filter: params.get('filter') || 'active',
|
|
11
|
+
type: params.get('type') || 'all',
|
|
12
|
+
task: params.get('task'),
|
|
13
|
+
epic: params.get('epic'),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
set(updates) {
|
|
17
|
+
if (this.pushing)
|
|
18
|
+
return;
|
|
19
|
+
this.pushing = true;
|
|
20
|
+
const url = new URL(window.location.href);
|
|
21
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
22
|
+
if (value)
|
|
23
|
+
url.searchParams.set(key, value);
|
|
24
|
+
else
|
|
25
|
+
url.searchParams.delete(key);
|
|
26
|
+
}
|
|
27
|
+
history.pushState(null, '', url);
|
|
28
|
+
this.notify();
|
|
29
|
+
this.pushing = false;
|
|
30
|
+
}
|
|
31
|
+
subscribe(listener) {
|
|
32
|
+
this.listeners.push(listener);
|
|
33
|
+
}
|
|
34
|
+
notify() {
|
|
35
|
+
const state = this.get();
|
|
36
|
+
this.listeners.forEach(fn => fn(state));
|
|
37
|
+
}
|
|
38
|
+
init() {
|
|
39
|
+
this.notify();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export const urlState = new UrlState();
|
package/dist/viewer.js
CHANGED
|
@@ -87,8 +87,10 @@ export async function startViewer(port = 3030) {
|
|
|
87
87
|
}
|
|
88
88
|
const filePath = storage.getFilePath(taskId);
|
|
89
89
|
const raw = storage.getMarkdown(taskId);
|
|
90
|
+
const epic = task.epic_id ? storage.get(task.epic_id) : null;
|
|
91
|
+
const epicTitle = epic?.title;
|
|
90
92
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
91
|
-
res.end(JSON.stringify({ ...task, filePath, raw }));
|
|
93
|
+
res.end(JSON.stringify({ ...task, filePath, raw, epicTitle }));
|
|
92
94
|
return;
|
|
93
95
|
}
|
|
94
96
|
// GET /open/:id - open file in default editor
|
package/dist/viewer.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"viewer.js","sourceRoot":"","sources":["../src/viewer.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"viewer.js","sourceRoot":"","sources":["../src/viewer.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "backlog-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Minimal task backlog MCP server for Claude and AI agents",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"start": "node dist/server.js",
|
|
45
45
|
"test": "vitest run",
|
|
46
46
|
"test:watch": "vitest",
|
|
47
|
-
"dev": "tsc -p viewer/tsconfig.json && concurrently \"tsx watch src/server.ts\" \"tsc -p viewer/tsconfig.json --watch --preserveWatchOutput\""
|
|
47
|
+
"dev": "lsof -ti:3030 | xargs kill -9 2>/dev/null; tsc -p viewer/tsconfig.json && concurrently \"tsx watch src/server.ts\" \"tsc -p viewer/tsconfig.json --watch --preserveWatchOutput\""
|
|
48
48
|
},
|
|
49
49
|
"engines": {
|
|
50
50
|
"node": ">=18.0.0"
|
package/viewer/styles.css
CHANGED
|
@@ -72,10 +72,24 @@
|
|
|
72
72
|
/* Filter Bar */
|
|
73
73
|
.filter-bar {
|
|
74
74
|
display: flex;
|
|
75
|
+
align-items: center;
|
|
75
76
|
gap: 8px;
|
|
76
77
|
flex-wrap: wrap;
|
|
77
78
|
}
|
|
78
79
|
|
|
80
|
+
.filter-bar.type-filter {
|
|
81
|
+
margin-top: 8px;
|
|
82
|
+
padding-top: 8px;
|
|
83
|
+
border-top: 1px solid #3e3e42;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.filter-label {
|
|
87
|
+
font-size: 11px;
|
|
88
|
+
color: #888;
|
|
89
|
+
text-transform: uppercase;
|
|
90
|
+
min-width: 40px;
|
|
91
|
+
}
|
|
92
|
+
|
|
79
93
|
.filter-btn {
|
|
80
94
|
padding: 6px 12px;
|
|
81
95
|
border: 1px solid #3e3e42;
|
|
@@ -101,16 +115,60 @@
|
|
|
101
115
|
.task-list {
|
|
102
116
|
display: flex;
|
|
103
117
|
flex-direction: column;
|
|
104
|
-
gap:
|
|
118
|
+
gap: 6px;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.task-item-wrapper {
|
|
122
|
+
display: flex;
|
|
123
|
+
align-items: stretch;
|
|
124
|
+
gap: 4px;
|
|
125
|
+
min-width: 0;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.task-item-wrapper.child {
|
|
129
|
+
margin-left: 16px;
|
|
130
|
+
padding-left: 12px;
|
|
131
|
+
position: relative;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.task-item-wrapper.child::before {
|
|
135
|
+
content: '';
|
|
136
|
+
position: absolute;
|
|
137
|
+
left: 0;
|
|
138
|
+
top: 50%;
|
|
139
|
+
width: 8px;
|
|
140
|
+
height: 1px;
|
|
141
|
+
background: #3e3e42;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.task-item-wrapper.child::after {
|
|
145
|
+
content: '';
|
|
146
|
+
position: absolute;
|
|
147
|
+
left: 0;
|
|
148
|
+
top: -6px;
|
|
149
|
+
bottom: 50%;
|
|
150
|
+
width: 1px;
|
|
151
|
+
background: #3e3e42;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.task-item-wrapper.pinned .task-item {
|
|
155
|
+
border-color: #f0b429;
|
|
156
|
+
background: #3d3522;
|
|
105
157
|
}
|
|
106
158
|
|
|
107
159
|
.task-item {
|
|
160
|
+
flex: 1;
|
|
161
|
+
display: flex;
|
|
162
|
+
align-items: center;
|
|
163
|
+
gap: 8px;
|
|
108
164
|
background: #252526;
|
|
109
165
|
border: 1px solid #3e3e42;
|
|
110
166
|
border-radius: 6px;
|
|
111
167
|
padding: 12px;
|
|
112
168
|
cursor: pointer;
|
|
113
169
|
transition: all 0.2s;
|
|
170
|
+
min-width: 0;
|
|
171
|
+
overflow: hidden;
|
|
114
172
|
}
|
|
115
173
|
|
|
116
174
|
.task-item:hover {
|
|
@@ -123,11 +181,28 @@
|
|
|
123
181
|
border-color: #007acc;
|
|
124
182
|
}
|
|
125
183
|
|
|
126
|
-
.task-item-
|
|
184
|
+
.task-item-wrapper > .pin-btn {
|
|
185
|
+
background: #252526;
|
|
186
|
+
border: 1px solid #3e3e42;
|
|
187
|
+
border-radius: 6px;
|
|
188
|
+
padding: 0 8px;
|
|
189
|
+
cursor: pointer;
|
|
190
|
+
color: #888;
|
|
127
191
|
display: flex;
|
|
128
|
-
justify-content: space-between;
|
|
129
192
|
align-items: center;
|
|
130
|
-
|
|
193
|
+
transition: all 0.2s;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.task-item-wrapper > .pin-btn:hover {
|
|
197
|
+
background: #2d2d30;
|
|
198
|
+
border-color: #f0b429;
|
|
199
|
+
color: #f0b429;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.task-item-wrapper > .pin-btn.pinned {
|
|
203
|
+
background: #3d3522;
|
|
204
|
+
border-color: #f0b429;
|
|
205
|
+
color: #f0b429;
|
|
131
206
|
}
|
|
132
207
|
|
|
133
208
|
.task-id {
|
|
@@ -139,6 +214,7 @@
|
|
|
139
214
|
.task-title {
|
|
140
215
|
font-size: 13px;
|
|
141
216
|
flex: 1;
|
|
217
|
+
min-width: 0;
|
|
142
218
|
white-space: nowrap;
|
|
143
219
|
overflow: hidden;
|
|
144
220
|
text-overflow: ellipsis;
|
|
@@ -150,6 +226,7 @@
|
|
|
150
226
|
border-radius: 10px;
|
|
151
227
|
font-weight: 500;
|
|
152
228
|
text-transform: uppercase;
|
|
229
|
+
flex-shrink: 0;
|
|
153
230
|
}
|
|
154
231
|
|
|
155
232
|
.status-open { background: #1f6feb; color: white; }
|
|
@@ -158,6 +235,49 @@
|
|
|
158
235
|
.status-done { background: #1a7f37; color: white; }
|
|
159
236
|
.status-cancelled { background: #57606a; color: white; }
|
|
160
237
|
|
|
238
|
+
/* Task Badge */
|
|
239
|
+
.task-badge {
|
|
240
|
+
display: inline-flex;
|
|
241
|
+
align-items: center;
|
|
242
|
+
gap: 4px;
|
|
243
|
+
font-family: monospace;
|
|
244
|
+
font-size: inherit;
|
|
245
|
+
color: inherit;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.task-badge-icon {
|
|
249
|
+
display: inline-flex;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.task-badge-id {
|
|
253
|
+
font-weight: 600;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.task-item .task-badge {
|
|
257
|
+
color: #888;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.type-icon {
|
|
261
|
+
margin-right: 6px;
|
|
262
|
+
display: inline-flex;
|
|
263
|
+
vertical-align: middle;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.type-icon.type-epic {
|
|
267
|
+
color: #f0b429;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.type-icon.type-task {
|
|
271
|
+
color: #3b82f6;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.epic-icon-inline {
|
|
275
|
+
display: inline-flex;
|
|
276
|
+
vertical-align: middle;
|
|
277
|
+
margin-right: 4px;
|
|
278
|
+
color: #f0b429;
|
|
279
|
+
}
|
|
280
|
+
|
|
161
281
|
/* Task Detail */
|
|
162
282
|
.task-detail-header {
|
|
163
283
|
margin-bottom: 20px;
|
|
@@ -310,11 +430,33 @@
|
|
|
310
430
|
margin: 0 0 8px 0;
|
|
311
431
|
font-weight: 600;
|
|
312
432
|
}
|
|
313
|
-
.task-meta-
|
|
433
|
+
.task-meta-row {
|
|
314
434
|
font-size: 12px;
|
|
315
435
|
color: #8b949e;
|
|
316
436
|
display: flex;
|
|
437
|
+
align-items: center;
|
|
317
438
|
gap: 16px;
|
|
439
|
+
flex-wrap: wrap;
|
|
440
|
+
}
|
|
441
|
+
.task-meta-epic {
|
|
442
|
+
display: inline-flex;
|
|
443
|
+
align-items: center;
|
|
444
|
+
gap: 4px;
|
|
445
|
+
}
|
|
446
|
+
.task-meta-epic-label {
|
|
447
|
+
margin-right: 0;
|
|
448
|
+
}
|
|
449
|
+
.epic-link {
|
|
450
|
+
color: #f0b429;
|
|
451
|
+
text-decoration: none;
|
|
452
|
+
display: inline-flex;
|
|
453
|
+
align-items: center;
|
|
454
|
+
}
|
|
455
|
+
.epic-link:hover {
|
|
456
|
+
text-decoration: underline;
|
|
457
|
+
}
|
|
458
|
+
.epic-title {
|
|
459
|
+
color: #d4d4d4;
|
|
318
460
|
}
|
|
319
461
|
.task-meta-path {
|
|
320
462
|
display: flex;
|
|
@@ -430,7 +572,7 @@ task-detail .markdown-body {
|
|
|
430
572
|
}
|
|
431
573
|
|
|
432
574
|
|
|
433
|
-
.task-id-btn {
|
|
575
|
+
.task-id-btn, .epic-id-btn {
|
|
434
576
|
font-family: monospace;
|
|
435
577
|
font-weight: 600;
|
|
436
578
|
}
|