project-mgr 0.2.1 → 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/CHANGELOG.md +27 -0
- package/README.md +21 -0
- package/dist/cli.js +37 -32
- package/dist/db.d.ts +33 -1
- package/dist/db.js +259 -10
- package/dist/domain.d.ts +12 -1
- package/dist/domain.js +9 -0
- package/dist/server.js +19 -2
- package/dist/web/i18n.d.ts +8 -0
- package/dist/web/i18n.js +77 -0
- package/dist/web/locales/en.d.ts +2 -0
- package/dist/web/locales/en.js +52 -0
- package/dist/web/locales/zh.d.ts +54 -0
- package/dist/web/locales/zh.js +52 -0
- package/dist/web/script.d.ts +2 -0
- package/dist/web/script.js +356 -0
- package/dist/web/styles.d.ts +1 -0
- package/dist/web/styles.js +636 -0
- package/dist/web/ui.d.ts +3 -1
- package/dist/web/ui.js +33 -924
- package/package.json +8 -2
- package/skills/project-mgr/SKILL.md +36 -17
- package/.claude/settings.local.json +0 -5
- package/.env_sample +0 -10
- package/.prettierignore +0 -4
- package/AGENTS.md +0 -5
- package/docs/architecture.md +0 -95
- package/ecosystem.config.cjs +0 -21
- package/eslint.config.js +0 -12
- package/pnpm-workspace.yaml +0 -3
- package/src/cli.ts +0 -399
- package/src/db.ts +0 -373
- package/src/domain.ts +0 -226
- package/src/index.ts +0 -5
- package/src/server.ts +0 -202
- package/src/web/ui.ts +0 -977
- package/test/core.test.ts +0 -739
- package/test/server.test.ts +0 -354
- package/tsconfig.json +0 -14
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type I18nKey, type LocaleMessages } from "./locales/zh.js";
|
|
2
|
+
export type { I18nKey, LocaleMessages };
|
|
3
|
+
export type Locale = "zh" | "en";
|
|
4
|
+
export declare const DEFAULT_LOCALE: Locale;
|
|
5
|
+
export declare const locales: Record<Locale, LocaleMessages>;
|
|
6
|
+
export declare function resolveLocale(lang?: unknown): Locale;
|
|
7
|
+
export declare function getMessages(lang: Locale): LocaleMessages;
|
|
8
|
+
export declare function getClientI18nScript(initialLang: Locale): string;
|
package/dist/web/i18n.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { en } from "./locales/en.js";
|
|
2
|
+
import { zh } from "./locales/zh.js";
|
|
3
|
+
export const DEFAULT_LOCALE = "zh";
|
|
4
|
+
export const locales = {
|
|
5
|
+
zh,
|
|
6
|
+
en,
|
|
7
|
+
};
|
|
8
|
+
export function resolveLocale(lang) {
|
|
9
|
+
return lang === "en" ? "en" : DEFAULT_LOCALE;
|
|
10
|
+
}
|
|
11
|
+
export function getMessages(lang) {
|
|
12
|
+
return locales[lang];
|
|
13
|
+
}
|
|
14
|
+
export function getClientI18nScript(initialLang) {
|
|
15
|
+
return ` const i18n = ${JSON.stringify(locales)};
|
|
16
|
+
|
|
17
|
+
let currentLang = "${initialLang}";
|
|
18
|
+
try {
|
|
19
|
+
const savedLang = localStorage.getItem("pmgr_lang");
|
|
20
|
+
if (savedLang === "zh" || savedLang === "en") {
|
|
21
|
+
currentLang = savedLang;
|
|
22
|
+
}
|
|
23
|
+
} catch {
|
|
24
|
+
/* ignore storage errors */
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function t(key) {
|
|
28
|
+
return (i18n[currentLang] && i18n[currentLang][key]) || key;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function formatStatus(status) {
|
|
32
|
+
const raw = status || "none";
|
|
33
|
+
const key = "status_" + raw;
|
|
34
|
+
const val = t(key);
|
|
35
|
+
return val === key ? raw : val;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function formatSource(source) {
|
|
39
|
+
const raw = source || "";
|
|
40
|
+
const key = "source_" + raw;
|
|
41
|
+
const val = t(key);
|
|
42
|
+
return val === key ? raw : val;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function formatUpdateCount(count) {
|
|
46
|
+
if (currentLang === "zh") {
|
|
47
|
+
return \`\${count} 条动态\`;
|
|
48
|
+
}
|
|
49
|
+
return \`\${count} update\${count > 1 ? "s" : ""}\`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function applyI18n() {
|
|
53
|
+
document.documentElement.lang = t("htmlLang");
|
|
54
|
+
document.title = t("pageTitle");
|
|
55
|
+
document.querySelectorAll("[data-i18n]").forEach((el) => {
|
|
56
|
+
el.textContent = t(el.dataset.i18n);
|
|
57
|
+
});
|
|
58
|
+
document.querySelectorAll("[data-i18n-placeholder]").forEach((el) => {
|
|
59
|
+
el.placeholder = t(el.dataset.i18nPlaceholder);
|
|
60
|
+
});
|
|
61
|
+
document.getElementById("langSwitch").setAttribute("aria-label", t("langSwitchAriaLabel"));
|
|
62
|
+
document.getElementById("btnLangZh").classList.toggle("active", currentLang === "zh");
|
|
63
|
+
document.getElementById("btnLangEn").classList.toggle("active", currentLang === "en");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function setLang(lang) {
|
|
67
|
+
if (lang !== "zh" && lang !== "en") return;
|
|
68
|
+
currentLang = lang;
|
|
69
|
+
try {
|
|
70
|
+
localStorage.setItem("pmgr_lang", currentLang);
|
|
71
|
+
} catch {
|
|
72
|
+
/* ignore storage errors */
|
|
73
|
+
}
|
|
74
|
+
applyI18n();
|
|
75
|
+
render();
|
|
76
|
+
}`;
|
|
77
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export const en = {
|
|
2
|
+
htmlLang: "en",
|
|
3
|
+
pageTitle: "Project Manager - Projects & Activity Feed",
|
|
4
|
+
appTitle: "Project Manager",
|
|
5
|
+
langSwitchAriaLabel: "Language Switcher",
|
|
6
|
+
statProjectsLabel: "Projects:",
|
|
7
|
+
statActiveLabel: "Active:",
|
|
8
|
+
statUpdatesLabel: "Updates:",
|
|
9
|
+
searchPlaceholder: "Search projects, status, or activity...",
|
|
10
|
+
projects: "Projects",
|
|
11
|
+
activityFeed: "Activity Feed",
|
|
12
|
+
refresh: "🔄 Refresh",
|
|
13
|
+
filteringByProject: "Filtering by project:",
|
|
14
|
+
dateLabel: "📅 Date:",
|
|
15
|
+
today: "Today",
|
|
16
|
+
showAllDates: "Show All Dates",
|
|
17
|
+
groupByProject: "📁 Group by Project",
|
|
18
|
+
flatList: "📜 Flat List",
|
|
19
|
+
noProjectsTitle: "No projects found",
|
|
20
|
+
noProjectsDesc: "No projects match your search or filter criteria.",
|
|
21
|
+
noActivityTitle: "No activity entries",
|
|
22
|
+
noActivityOnDate: "No activity entries on {date}",
|
|
23
|
+
noActivityDesc: "No status updates match the selected date or search filter.",
|
|
24
|
+
latestUpdate: "Latest Update",
|
|
25
|
+
createdLabel: "Created:",
|
|
26
|
+
viewActivity: "View Activity →",
|
|
27
|
+
progressLabel: "Progress:",
|
|
28
|
+
blockersTitle: "⛔ Blockers",
|
|
29
|
+
nextStepsTitle: "🎯 Next Steps",
|
|
30
|
+
decisionsTitle: "💡 Decisions",
|
|
31
|
+
backToActivities: "Back to activities",
|
|
32
|
+
currentStatus: "Current status",
|
|
33
|
+
superseded: "Superseded",
|
|
34
|
+
originalStatus: "Original status: ",
|
|
35
|
+
completedBy: "Completed by follow-up",
|
|
36
|
+
followUpTime: "Follow-up time: ",
|
|
37
|
+
completionRecorded: "Completion recorded: ",
|
|
38
|
+
originalRecord: "Original record",
|
|
39
|
+
entryLoadError: "Could not load activity. Please retry.",
|
|
40
|
+
sourcePrefix: "source: ",
|
|
41
|
+
status_idea: "idea",
|
|
42
|
+
status_planned: "planned",
|
|
43
|
+
status_active: "active",
|
|
44
|
+
status_blocked: "blocked",
|
|
45
|
+
status_paused: "paused",
|
|
46
|
+
status_completed: "completed",
|
|
47
|
+
status_archived: "archived",
|
|
48
|
+
status_none: "none",
|
|
49
|
+
source_human: "human",
|
|
50
|
+
source_agent: "agent",
|
|
51
|
+
source_import: "import",
|
|
52
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export declare const zh: {
|
|
2
|
+
readonly htmlLang: "zh-CN";
|
|
3
|
+
readonly pageTitle: "项目管理 - 项目与动态概览";
|
|
4
|
+
readonly appTitle: "项目管理";
|
|
5
|
+
readonly langSwitchAriaLabel: "切换语言";
|
|
6
|
+
readonly statProjectsLabel: "项目:";
|
|
7
|
+
readonly statActiveLabel: "进行中:";
|
|
8
|
+
readonly statUpdatesLabel: "动态:";
|
|
9
|
+
readonly searchPlaceholder: "搜索项目、状态或动态...";
|
|
10
|
+
readonly projects: "项目列表";
|
|
11
|
+
readonly activityFeed: "动态流";
|
|
12
|
+
readonly refresh: "🔄 刷新";
|
|
13
|
+
readonly filteringByProject: "按项目筛选:";
|
|
14
|
+
readonly dateLabel: "📅 日期:";
|
|
15
|
+
readonly today: "今天";
|
|
16
|
+
readonly showAllDates: "显示全部日期";
|
|
17
|
+
readonly groupByProject: "📁 按项目分组";
|
|
18
|
+
readonly flatList: "📜 平铺列表";
|
|
19
|
+
readonly noProjectsTitle: "未找到项目";
|
|
20
|
+
readonly noProjectsDesc: "没有符合搜索或筛选条件的项目。";
|
|
21
|
+
readonly noActivityTitle: "暂无动态记录";
|
|
22
|
+
readonly noActivityOnDate: "{date} 暂无动态记录";
|
|
23
|
+
readonly noActivityDesc: "所选日期或搜索条件下暂无状态更新。";
|
|
24
|
+
readonly latestUpdate: "最新动态";
|
|
25
|
+
readonly createdLabel: "创建于:";
|
|
26
|
+
readonly viewActivity: "查看动态 →";
|
|
27
|
+
readonly progressLabel: "进展:";
|
|
28
|
+
readonly blockersTitle: "⛔ 阻塞项";
|
|
29
|
+
readonly nextStepsTitle: "🎯 下一步";
|
|
30
|
+
readonly decisionsTitle: "💡 决策";
|
|
31
|
+
readonly backToActivities: "返回动态列表";
|
|
32
|
+
readonly currentStatus: "当前状态";
|
|
33
|
+
readonly superseded: "已有后续更新";
|
|
34
|
+
readonly originalStatus: "当时状态:";
|
|
35
|
+
readonly completedBy: "由后续动态完成";
|
|
36
|
+
readonly followUpTime: "后续动态时间:";
|
|
37
|
+
readonly completionRecorded: "完成关联记录时间:";
|
|
38
|
+
readonly originalRecord: "原始记录";
|
|
39
|
+
readonly entryLoadError: "无法加载动态,请重试。";
|
|
40
|
+
readonly sourcePrefix: "来源:";
|
|
41
|
+
readonly status_idea: "构想";
|
|
42
|
+
readonly status_planned: "计划中";
|
|
43
|
+
readonly status_active: "进行中";
|
|
44
|
+
readonly status_blocked: "受阻";
|
|
45
|
+
readonly status_paused: "已暂停";
|
|
46
|
+
readonly status_completed: "已完成";
|
|
47
|
+
readonly status_archived: "已归档";
|
|
48
|
+
readonly status_none: "无状态";
|
|
49
|
+
readonly source_human: "人工";
|
|
50
|
+
readonly source_agent: "Agent";
|
|
51
|
+
readonly source_import: "导入";
|
|
52
|
+
};
|
|
53
|
+
export type I18nKey = keyof typeof zh;
|
|
54
|
+
export type LocaleMessages = Record<I18nKey, string>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export const zh = {
|
|
2
|
+
htmlLang: "zh-CN",
|
|
3
|
+
pageTitle: "项目管理 - 项目与动态概览",
|
|
4
|
+
appTitle: "项目管理",
|
|
5
|
+
langSwitchAriaLabel: "切换语言",
|
|
6
|
+
statProjectsLabel: "项目:",
|
|
7
|
+
statActiveLabel: "进行中:",
|
|
8
|
+
statUpdatesLabel: "动态:",
|
|
9
|
+
searchPlaceholder: "搜索项目、状态或动态...",
|
|
10
|
+
projects: "项目列表",
|
|
11
|
+
activityFeed: "动态流",
|
|
12
|
+
refresh: "🔄 刷新",
|
|
13
|
+
filteringByProject: "按项目筛选:",
|
|
14
|
+
dateLabel: "📅 日期:",
|
|
15
|
+
today: "今天",
|
|
16
|
+
showAllDates: "显示全部日期",
|
|
17
|
+
groupByProject: "📁 按项目分组",
|
|
18
|
+
flatList: "📜 平铺列表",
|
|
19
|
+
noProjectsTitle: "未找到项目",
|
|
20
|
+
noProjectsDesc: "没有符合搜索或筛选条件的项目。",
|
|
21
|
+
noActivityTitle: "暂无动态记录",
|
|
22
|
+
noActivityOnDate: "{date} 暂无动态记录",
|
|
23
|
+
noActivityDesc: "所选日期或搜索条件下暂无状态更新。",
|
|
24
|
+
latestUpdate: "最新动态",
|
|
25
|
+
createdLabel: "创建于:",
|
|
26
|
+
viewActivity: "查看动态 →",
|
|
27
|
+
progressLabel: "进展:",
|
|
28
|
+
blockersTitle: "⛔ 阻塞项",
|
|
29
|
+
nextStepsTitle: "🎯 下一步",
|
|
30
|
+
decisionsTitle: "💡 决策",
|
|
31
|
+
backToActivities: "返回动态列表",
|
|
32
|
+
currentStatus: "当前状态",
|
|
33
|
+
superseded: "已有后续更新",
|
|
34
|
+
originalStatus: "当时状态:",
|
|
35
|
+
completedBy: "由后续动态完成",
|
|
36
|
+
followUpTime: "后续动态时间:",
|
|
37
|
+
completionRecorded: "完成关联记录时间:",
|
|
38
|
+
originalRecord: "原始记录",
|
|
39
|
+
entryLoadError: "无法加载动态,请重试。",
|
|
40
|
+
sourcePrefix: "来源:",
|
|
41
|
+
status_idea: "构想",
|
|
42
|
+
status_planned: "计划中",
|
|
43
|
+
status_active: "进行中",
|
|
44
|
+
status_blocked: "受阻",
|
|
45
|
+
status_paused: "已暂停",
|
|
46
|
+
status_completed: "已完成",
|
|
47
|
+
status_archived: "已归档",
|
|
48
|
+
status_none: "无状态",
|
|
49
|
+
source_human: "人工",
|
|
50
|
+
source_agent: "Agent",
|
|
51
|
+
source_import: "导入",
|
|
52
|
+
};
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import { getClientI18nScript } from "./i18n.js";
|
|
2
|
+
export function getWebUIScript(initialLang) {
|
|
3
|
+
return `${getClientI18nScript(initialLang)}
|
|
4
|
+
|
|
5
|
+
let projectsData = [];
|
|
6
|
+
let historyData = [];
|
|
7
|
+
let focusedEntry = null;
|
|
8
|
+
let activeProjectFilter = null;
|
|
9
|
+
let activeGroupMode = 'grouped';
|
|
10
|
+
|
|
11
|
+
function getTodayDateString() {
|
|
12
|
+
const now = new Date();
|
|
13
|
+
const year = now.getFullYear();
|
|
14
|
+
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
15
|
+
const day = String(now.getDate()).padStart(2, '0');
|
|
16
|
+
return \`\${year}-\${month}-\${day}\`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getLocalDateString(isoStr) {
|
|
20
|
+
if (!isoStr) return '';
|
|
21
|
+
const d = new Date(isoStr);
|
|
22
|
+
if (isNaN(d.getTime())) return '';
|
|
23
|
+
const year = d.getFullYear();
|
|
24
|
+
const month = String(d.getMonth() + 1).padStart(2, '0');
|
|
25
|
+
const day = String(d.getDate()).padStart(2, '0');
|
|
26
|
+
return \`\${year}-\${month}-\${day}\`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Set default date input value to Today on page load
|
|
30
|
+
document.getElementById('activityDateInput').value = getTodayDateString();
|
|
31
|
+
applyI18n();
|
|
32
|
+
|
|
33
|
+
async function fetchData() {
|
|
34
|
+
try {
|
|
35
|
+
const [projRes, histRes, statsRes] = await Promise.all([
|
|
36
|
+
fetch('/api/projects'),
|
|
37
|
+
fetch('/api/history?limit=1000'),
|
|
38
|
+
fetch('/api/stats')
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
if (projRes.ok) projectsData = await projRes.json();
|
|
42
|
+
if (histRes.ok) historyData = await histRes.json();
|
|
43
|
+
if (statsRes.ok) {
|
|
44
|
+
const stats = await statsRes.json();
|
|
45
|
+
document.getElementById('statProjects').textContent = stats.totalProjects;
|
|
46
|
+
document.getElementById('statActive').textContent = stats.statusCounts.active || 0;
|
|
47
|
+
document.getElementById('statUpdates').textContent = stats.totalEntries;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
render();
|
|
51
|
+
if (typeof location !== 'undefined') {
|
|
52
|
+
const ref = new URLSearchParams(location.search).get('entry');
|
|
53
|
+
if (ref) {
|
|
54
|
+
switchTab('activity');
|
|
55
|
+
const response = await fetch('/api/entries/' + encodeURIComponent(ref));
|
|
56
|
+
if (!response.ok) throw new Error('Entry unavailable');
|
|
57
|
+
focusedEntry = await response.json();
|
|
58
|
+
render();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
} catch (err) {
|
|
62
|
+
console.error('Failed to load data', err);
|
|
63
|
+
document.getElementById('timelineContainer').textContent = t('entryLoadError');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function switchTab(tab) {
|
|
68
|
+
document.getElementById('tabProjects').classList.toggle('active', tab === 'projects');
|
|
69
|
+
document.getElementById('tabActivity').classList.toggle('active', tab === 'activity');
|
|
70
|
+
document.getElementById('viewProjects').classList.toggle('active', tab === 'projects');
|
|
71
|
+
document.getElementById('viewActivity').classList.toggle('active', tab === 'activity');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function clearEntryFocus() {
|
|
75
|
+
focusedEntry = null;
|
|
76
|
+
if (typeof window !== 'undefined') {
|
|
77
|
+
const url = new URL(window.location.href);
|
|
78
|
+
url.searchParams.delete('entry');
|
|
79
|
+
window.history.replaceState(null, '', url);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function returnToActivityList() {
|
|
84
|
+
clearEntryFocus();
|
|
85
|
+
activeProjectFilter = null;
|
|
86
|
+
document.getElementById('searchInput').value = '';
|
|
87
|
+
document.getElementById('activityDateInput').value = '';
|
|
88
|
+
render();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function refreshData() {
|
|
92
|
+
clearEntryFocus();
|
|
93
|
+
return fetchData();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function changeActivityDate() {
|
|
97
|
+
clearEntryFocus();
|
|
98
|
+
render();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function setActivityDateToday() {
|
|
102
|
+
clearEntryFocus();
|
|
103
|
+
document.getElementById('activityDateInput').value = getTodayDateString();
|
|
104
|
+
render();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function clearActivityDate() {
|
|
108
|
+
clearEntryFocus();
|
|
109
|
+
document.getElementById('activityDateInput').value = '';
|
|
110
|
+
render();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function setActivityGroupMode(mode) {
|
|
114
|
+
activeGroupMode = mode;
|
|
115
|
+
document.getElementById('modeGrouped').classList.toggle('active', mode === 'grouped');
|
|
116
|
+
document.getElementById('modeFlat').classList.toggle('active', mode === 'flat');
|
|
117
|
+
render();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function filterByProject(slug) {
|
|
121
|
+
clearEntryFocus();
|
|
122
|
+
activeProjectFilter = slug;
|
|
123
|
+
switchTab('activity');
|
|
124
|
+
render();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function clearProjectFilter() {
|
|
128
|
+
clearEntryFocus();
|
|
129
|
+
activeProjectFilter = null;
|
|
130
|
+
render();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
document.getElementById('searchInput').addEventListener('input', () => { clearEntryFocus(); render(); });
|
|
134
|
+
|
|
135
|
+
function formatTime(isoStr) {
|
|
136
|
+
if (!isoStr) return '';
|
|
137
|
+
try {
|
|
138
|
+
const d = new Date(isoStr);
|
|
139
|
+
return d.toLocaleString(currentLang === 'zh' ? 'zh-CN' : 'en-US');
|
|
140
|
+
} catch {
|
|
141
|
+
return isoStr;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function renderTimelineItem(item) {
|
|
146
|
+
const status = item.effectiveStatus || item.status;
|
|
147
|
+
return \`
|
|
148
|
+
<div class="timeline-item" id="entry-\${escapeHtml(item.uid || item.id)}">
|
|
149
|
+
<div class="timeline-header">
|
|
150
|
+
<div class="timeline-meta">
|
|
151
|
+
<span class="project-tag" data-action="filter-project" data-project="\${escapeHtml(item.project)}">\${escapeHtml(item.project)}</span>
|
|
152
|
+
<span class="status-badge status-\${escapeHtml(status)}">\${escapeHtml(formatStatus(status))}</span>
|
|
153
|
+
<span class="source-badge">\${escapeHtml(t(item.isCurrent ? "currentStatus" : "superseded"))}</span>
|
|
154
|
+
<span class="source-badge">\${escapeHtml(t('sourcePrefix'))}\${escapeHtml(formatSource(item.source))}</span>
|
|
155
|
+
</div>
|
|
156
|
+
<div class="time-stamp">\${formatTime(item.occurredAt)}</div>
|
|
157
|
+
</div>
|
|
158
|
+
<div class="entry-summary">\${escapeHtml(item.summary)}</div>
|
|
159
|
+
\${item.completedBy ? \`<div class="completion-evidence">
|
|
160
|
+
<a class="link-btn" href="?entry=\${encodeURIComponent(item.completedBy.uid)}">\${escapeHtml(t('completedBy'))}: #\${item.completedBy.id} — \${escapeHtml(item.completedBy.summary)}</a>
|
|
161
|
+
<div>\${escapeHtml(t('followUpTime'))} \${formatTime(item.completedBy.occurredAt)}</div>
|
|
162
|
+
<div>\${escapeHtml(t('completionRecorded'))} \${formatTime(item.completedBy.recordedAt)}</div>
|
|
163
|
+
</div><details><summary>\${escapeHtml(t('originalRecord'))} · \${escapeHtml(t('originalStatus'))} \${escapeHtml(formatStatus(item.status))}</summary>\` : ''}
|
|
164
|
+
\${item.progress ? \`<div class="entry-progress"><strong>\${escapeHtml(t('progressLabel'))}</strong> \${escapeHtml(item.progress)}</div>\` : ''}
|
|
165
|
+
|
|
166
|
+
\${item.blockers && item.blockers.length ? \`
|
|
167
|
+
<div class="section-box">
|
|
168
|
+
<div class="section-title blockers">\${escapeHtml(t('blockersTitle'))}</div>
|
|
169
|
+
<ul class="bullet-list">
|
|
170
|
+
\${item.blockers.map(b => \`<li>\${escapeHtml(b)}</li>\`).join('')}
|
|
171
|
+
</ul>
|
|
172
|
+
</div>
|
|
173
|
+
\` : ''}
|
|
174
|
+
|
|
175
|
+
\${item.nextSteps && item.nextSteps.length ? \`
|
|
176
|
+
<div class="section-box">
|
|
177
|
+
<div class="section-title next-steps">\${escapeHtml(t('nextStepsTitle'))}</div>
|
|
178
|
+
<ul class="bullet-list">
|
|
179
|
+
\${item.nextSteps.map(n => \`<li>\${escapeHtml(n)}</li>\`).join('')}
|
|
180
|
+
</ul>
|
|
181
|
+
</div>
|
|
182
|
+
\` : ''}
|
|
183
|
+
|
|
184
|
+
\${item.decisions && item.decisions.length ? \`
|
|
185
|
+
<div class="section-box">
|
|
186
|
+
<div class="section-title decisions">\${escapeHtml(t('decisionsTitle'))}</div>
|
|
187
|
+
<ul class="bullet-list">
|
|
188
|
+
\${item.decisions.map(d => \`<li>\${escapeHtml(d)}</li>\`).join('')}
|
|
189
|
+
</ul>
|
|
190
|
+
</div>
|
|
191
|
+
\` : ''}
|
|
192
|
+
|
|
193
|
+
\${item.tags && item.tags.length ? \`
|
|
194
|
+
<div class="tags-container">
|
|
195
|
+
\${item.tags.map(tag => \`<span class="pill-tag">#\${escapeHtml(tag)}</span>\`).join('')}
|
|
196
|
+
</div>
|
|
197
|
+
\` : ''}
|
|
198
|
+
\${item.completedBy ? '</details>' : ''}
|
|
199
|
+
</div>
|
|
200
|
+
\`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
document.addEventListener('click', function(e) {
|
|
204
|
+
const filterTarget = e.target.closest('[data-action="filter-project"]');
|
|
205
|
+
if (filterTarget && filterTarget.dataset.project) {
|
|
206
|
+
filterByProject(filterTarget.dataset.project);
|
|
207
|
+
}
|
|
208
|
+
if (e.target && e.target.id === 'btnClearFilter') {
|
|
209
|
+
clearProjectFilter();
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
function render() {
|
|
214
|
+
const query = document.getElementById('searchInput').value.trim().toLowerCase();
|
|
215
|
+
const selectedDate = document.getElementById('activityDateInput').value;
|
|
216
|
+
|
|
217
|
+
// Render Active Filter Chip
|
|
218
|
+
const filterBar = document.getElementById('filterBarContainer');
|
|
219
|
+
const chip = document.getElementById('activeFilterChip');
|
|
220
|
+
if (activeProjectFilter) {
|
|
221
|
+
filterBar.style.display = 'flex';
|
|
222
|
+
chip.innerHTML = \`\${escapeHtml(activeProjectFilter)} <button id="btnClearFilter">✕</button>\`;
|
|
223
|
+
} else {
|
|
224
|
+
filterBar.style.display = 'none';
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Render Projects
|
|
228
|
+
const projContainer = document.getElementById('projectsContainer');
|
|
229
|
+
const filteredProjects = projectsData.filter(p => {
|
|
230
|
+
if (activeProjectFilter && p.project !== activeProjectFilter) return false;
|
|
231
|
+
if (!query) return true;
|
|
232
|
+
const status = p.latestStatus ? p.latestStatus.status : 'none';
|
|
233
|
+
return p.project.toLowerCase().includes(query) ||
|
|
234
|
+
p.displayName.toLowerCase().includes(query) ||
|
|
235
|
+
status.toLowerCase().includes(query) ||
|
|
236
|
+
formatStatus(status).toLowerCase().includes(query) ||
|
|
237
|
+
(p.description && p.description.toLowerCase().includes(query)) ||
|
|
238
|
+
(p.notes && p.notes.toLowerCase().includes(query)) ||
|
|
239
|
+
(p.latestStatus && p.latestStatus.summary.toLowerCase().includes(query));
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
if (filteredProjects.length === 0) {
|
|
243
|
+
projContainer.innerHTML = \`
|
|
244
|
+
<div class="empty-state" style="grid-column: 1 / -1;">
|
|
245
|
+
<h3>\${t('noProjectsTitle')}</h3>
|
|
246
|
+
<p>\${t('noProjectsDesc')}</p>
|
|
247
|
+
</div>\`;
|
|
248
|
+
} else {
|
|
249
|
+
projContainer.innerHTML = filteredProjects.map(p => {
|
|
250
|
+
const status = p.latestStatus ? p.latestStatus.status : 'none';
|
|
251
|
+
return \`
|
|
252
|
+
<div class="project-card">
|
|
253
|
+
<div>
|
|
254
|
+
<div class="card-header">
|
|
255
|
+
<div>
|
|
256
|
+
<div class="project-title">\${escapeHtml(p.displayName)}</div>
|
|
257
|
+
<div class="project-slug">\${escapeHtml(p.project)}</div>
|
|
258
|
+
</div>
|
|
259
|
+
<span class="status-badge status-\${status}">\${escapeHtml(formatStatus(status))}</span>
|
|
260
|
+
</div>
|
|
261
|
+
<div class="card-body">
|
|
262
|
+
\${p.description ? \`<div class="card-description">\${escapeHtml(p.description)}</div>\` : ''}
|
|
263
|
+
\${p.notes ? \`<div class="card-notes">\${escapeHtml(p.notes)}</div>\` : ''}
|
|
264
|
+
\${p.latestStatus ? \`
|
|
265
|
+
<div class="latest-summary">
|
|
266
|
+
<div class="latest-summary-title">\${escapeHtml(t('latestUpdate'))} (\${formatTime(p.latestStatus.occurredAt)})</div>
|
|
267
|
+
<div>\${escapeHtml(p.latestStatus.summary)}</div>
|
|
268
|
+
</div>
|
|
269
|
+
\` : ''}
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
272
|
+
<div class="card-footer">
|
|
273
|
+
<span>\${escapeHtml(t('createdLabel'))} \${formatTime(p.createdAt)}</span>
|
|
274
|
+
<a class="link-btn" data-action="filter-project" data-project="\${escapeHtml(p.project)}">\${escapeHtml(t('viewActivity'))}</a>
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
\`;
|
|
278
|
+
}).join('');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
document.getElementById('btnBackToActivities').hidden = !focusedEntry;
|
|
282
|
+
|
|
283
|
+
// Render Timeline Feed
|
|
284
|
+
const timelineContainer = document.getElementById('timelineContainer');
|
|
285
|
+
const filteredHistory = focusedEntry ? [focusedEntry] : historyData.filter(h => {
|
|
286
|
+
if (activeProjectFilter && h.project !== activeProjectFilter) return false;
|
|
287
|
+
if (selectedDate && getLocalDateString(h.occurredAt) !== selectedDate) return false;
|
|
288
|
+
if (!query) return true;
|
|
289
|
+
return h.project.toLowerCase().includes(query) ||
|
|
290
|
+
(h.effectiveStatus || h.status).toLowerCase().includes(query) ||
|
|
291
|
+
formatStatus(h.effectiveStatus || h.status).toLowerCase().includes(query) ||
|
|
292
|
+
h.status.toLowerCase().includes(query) ||
|
|
293
|
+
formatStatus(h.status).toLowerCase().includes(query) ||
|
|
294
|
+
h.summary.toLowerCase().includes(query) ||
|
|
295
|
+
(h.progress && h.progress.toLowerCase().includes(query)) ||
|
|
296
|
+
h.tags.some(tag => tag.toLowerCase().includes(query));
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
if (filteredHistory.length === 0) {
|
|
300
|
+
const emptyTitle = selectedDate
|
|
301
|
+
? t('noActivityOnDate').replace('{date}', selectedDate)
|
|
302
|
+
: t('noActivityTitle');
|
|
303
|
+
timelineContainer.innerHTML = \`
|
|
304
|
+
<div class="empty-state">
|
|
305
|
+
<h3>\${escapeHtml(emptyTitle)}</h3>
|
|
306
|
+
<p>\${t('noActivityDesc')}</p>
|
|
307
|
+
\${selectedDate ? \`<button class="btn btn-primary" onclick="clearActivityDate()">\${t('showAllDates')}</button>\` : ''}
|
|
308
|
+
</div>\`;
|
|
309
|
+
} else {
|
|
310
|
+
if (activeGroupMode === 'grouped') {
|
|
311
|
+
// Group by Project
|
|
312
|
+
const groups = {};
|
|
313
|
+
filteredHistory.forEach(item => {
|
|
314
|
+
if (!groups[item.project]) groups[item.project] = [];
|
|
315
|
+
groups[item.project].push(item);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
timelineContainer.innerHTML = Object.keys(groups).map(projSlug => {
|
|
319
|
+
const projMeta = projectsData.find(p => p.project === projSlug);
|
|
320
|
+
const titleName = projMeta ? projMeta.displayName : projSlug;
|
|
321
|
+
const entries = groups[projSlug];
|
|
322
|
+
|
|
323
|
+
return \`
|
|
324
|
+
<div class="project-group-card">
|
|
325
|
+
<div class="project-group-header">
|
|
326
|
+
<div class="project-group-title">
|
|
327
|
+
📁 <span>\${escapeHtml(titleName)}</span>
|
|
328
|
+
<span class="project-slug">(\${escapeHtml(projSlug)})</span>
|
|
329
|
+
</div>
|
|
330
|
+
<span class="project-group-count">\${escapeHtml(formatUpdateCount(entries.length))}</span>
|
|
331
|
+
</div>
|
|
332
|
+
<div class="timeline">
|
|
333
|
+
\${entries.map(renderTimelineItem).join('')}
|
|
334
|
+
</div>
|
|
335
|
+
</div>
|
|
336
|
+
\`;
|
|
337
|
+
}).join('');
|
|
338
|
+
} else {
|
|
339
|
+
// Flat Timeline
|
|
340
|
+
timelineContainer.innerHTML = filteredHistory.map(renderTimelineItem).join('');
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function escapeHtml(str) {
|
|
346
|
+
if (!str) return '';
|
|
347
|
+
return String(str)
|
|
348
|
+
.replace(/&/g, '&')
|
|
349
|
+
.replace(/</g, '<')
|
|
350
|
+
.replace(/>/g, '>')
|
|
351
|
+
.replace(/"/g, '"')
|
|
352
|
+
.replace(/'/g, ''');
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
fetchData();`;
|
|
356
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const WEB_UI_STYLES = " :root {\n --bg: #0d1117;\n --card-bg: #161b22;\n --border: #30363d;\n --text: #c9d1d9;\n --text-muted: #8b949e;\n --heading: #f0f6fc;\n --accent: #58a6ff;\n --accent-hover: #79c0ff;\n --success: #3fb950;\n --warning: #d29922;\n --danger: #f85149;\n --purple: #bc8cff;\n --cyan: #39c5cf;\n --radius: 8px;\n }\n\n * {\n box-sizing: border-box;\n margin: 0;\n padding: 0;\n }\n\n body {\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif;\n background-color: var(--bg);\n color: var(--text);\n line-height: 1.5;\n padding: 0;\n margin: 0;\n min-height: 100vh;\n }\n\n header {\n background-color: var(--card-bg);\n border-bottom: 1px solid var(--border);\n position: sticky;\n top: 0;\n z-index: 100;\n padding: 1rem 1.5rem;\n }\n\n .header-container {\n max-width: 1200px;\n margin: 0 auto;\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: space-between;\n gap: 1rem;\n }\n\n .brand {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n }\n\n .logo-icon {\n width: 32px;\n height: 32px;\n background: linear-gradient(135deg, var(--accent), var(--purple));\n border-radius: 8px;\n display: flex;\n align-items: center;\n justify-content: center;\n color: #fff;\n font-weight: bold;\n font-size: 1.1rem;\n }\n\n h1 {\n font-size: 1.25rem;\n font-weight: 600;\n color: var(--heading);\n }\n\n .header-right {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n flex-wrap: wrap;\n }\n\n .stats-bar {\n display: flex;\n gap: 0.75rem;\n flex-wrap: wrap;\n }\n\n .lang-switch {\n display: inline-flex;\n gap: 0.2rem;\n background: var(--bg);\n padding: 0.2rem;\n border-radius: 6px;\n border: 1px solid var(--border);\n }\n\n .lang-btn {\n background: transparent;\n border: none;\n color: var(--text-muted);\n padding: 0.25rem 0.65rem;\n font-size: 0.8rem;\n font-weight: 500;\n border-radius: 4px;\n cursor: pointer;\n transition: all 0.2s;\n }\n\n .lang-btn:hover {\n color: var(--heading);\n }\n\n .lang-btn.active {\n background: var(--accent);\n color: #ffffff;\n font-weight: 600;\n }\n\n .stat-pill {\n background: rgba(255, 255, 255, 0.05);\n border: 1px solid var(--border);\n border-radius: 20px;\n padding: 0.25rem 0.75rem;\n font-size: 0.85rem;\n display: flex;\n align-items: center;\n gap: 0.35rem;\n }\n\n .stat-val {\n font-weight: 600;\n color: var(--heading);\n }\n\n .controls {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n flex-wrap: wrap;\n width: 100%;\n max-width: 1200px;\n margin: 1rem auto 0 auto;\n padding: 0 1.5rem;\n }\n\n .search-box {\n flex: 1;\n min-width: 240px;\n position: relative;\n }\n\n .search-input {\n width: 100%;\n background: var(--card-bg);\n border: 1px solid var(--border);\n color: var(--text);\n padding: 0.5rem 0.75rem 0.5rem 2.25rem;\n border-radius: var(--radius);\n font-size: 0.9rem;\n outline: none;\n transition: border-color 0.2s;\n }\n\n .search-input:focus {\n border-color: var(--accent);\n }\n\n .search-icon {\n position: absolute;\n left: 0.75rem;\n top: 50%;\n transform: translateY(-50%);\n color: var(--text-muted);\n pointer-events: none;\n font-size: 0.85rem;\n }\n\n .nav-tabs {\n display: flex;\n gap: 0.5rem;\n background: var(--card-bg);\n padding: 0.25rem;\n border-radius: var(--radius);\n border: 1px solid var(--border);\n }\n\n .tab-btn {\n background: transparent;\n border: none;\n color: var(--text-muted);\n padding: 0.4rem 1rem;\n font-size: 0.875rem;\n font-weight: 500;\n border-radius: 6px;\n cursor: pointer;\n transition: all 0.2s;\n }\n\n .tab-btn:hover {\n color: var(--heading);\n }\n\n .tab-btn.active {\n background: var(--accent);\n color: #ffffff;\n }\n\n .btn[hidden] { display: none !important; }\n .btn {\n background: var(--card-bg);\n border: 1px solid var(--border);\n color: var(--text);\n padding: 0.45rem 0.85rem;\n border-radius: var(--radius);\n font-size: 0.875rem;\n cursor: pointer;\n display: inline-flex;\n align-items: center;\n gap: 0.35rem;\n transition: all 0.2s;\n }\n\n .btn:hover {\n border-color: var(--text-muted);\n color: var(--heading);\n }\n\n .btn-sm {\n padding: 0.3rem 0.6rem;\n font-size: 0.8rem;\n }\n\n .btn-primary {\n background: var(--accent);\n color: #ffffff;\n border-color: var(--accent);\n margin-top: 0.75rem;\n }\n\n .btn-primary:hover {\n background: var(--accent-hover);\n color: #ffffff;\n }\n\n main {\n max-width: 1200px;\n margin: 1.5rem auto;\n padding: 0 1.5rem;\n }\n\n .view-panel {\n display: none;\n }\n\n .view-panel.active {\n display: block;\n }\n\n /* Activity Feed Toolbar & Grouping Toggle */\n .activity-toolbar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 1rem;\n background: var(--card-bg);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n padding: 0.75rem 1rem;\n margin-bottom: 1.25rem;\n flex-wrap: wrap;\n }\n\n .date-picker-wrapper {\n display: flex;\n align-items: center;\n gap: 0.6rem;\n font-size: 0.9rem;\n color: var(--heading);\n font-weight: 500;\n flex-wrap: wrap;\n }\n\n .date-input {\n background: var(--bg);\n border: 1px solid var(--border);\n color: var(--heading);\n padding: 0.35rem 0.65rem;\n border-radius: 6px;\n font-size: 0.875rem;\n outline: none;\n font-family: inherit;\n }\n\n .date-input:focus {\n border-color: var(--accent);\n }\n\n .view-mode-toggle {\n display: flex;\n gap: 0.25rem;\n background: var(--bg);\n padding: 0.2rem;\n border-radius: 6px;\n border: 1px solid var(--border);\n }\n\n .mode-btn {\n background: transparent;\n border: none;\n color: var(--text-muted);\n padding: 0.3rem 0.75rem;\n font-size: 0.8rem;\n font-weight: 500;\n border-radius: 4px;\n cursor: pointer;\n transition: all 0.2s;\n }\n\n .mode-btn:hover {\n color: var(--heading);\n }\n\n .mode-btn.active {\n background: var(--accent);\n color: #ffffff;\n font-weight: 600;\n }\n\n /* Project Grouping Container */\n .project-group-card {\n background: var(--card-bg);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n padding: 1.25rem;\n margin-bottom: 1.25rem;\n }\n\n .project-group-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n border-bottom: 1px solid var(--border);\n padding-bottom: 0.75rem;\n margin-bottom: 1rem;\n }\n\n .project-group-title {\n display: flex;\n align-items: center;\n gap: 0.6rem;\n font-size: 1.1rem;\n font-weight: 600;\n color: var(--heading);\n }\n\n .project-group-count {\n font-size: 0.8rem;\n color: var(--text-muted);\n background: rgba(255, 255, 255, 0.06);\n padding: 0.15rem 0.55rem;\n border-radius: 12px;\n }\n\n /* Projects Grid */\n .projects-grid {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));\n gap: 1.25rem;\n }\n\n .project-card {\n background: var(--card-bg);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n padding: 1.25rem;\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n transition: transform 0.15s, border-color 0.15s;\n }\n\n .project-card:hover {\n border-color: var(--text-muted);\n transform: translateY(-2px);\n }\n\n .card-header {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: 0.75rem;\n margin-bottom: 0.75rem;\n }\n\n .project-title {\n font-size: 1.1rem;\n font-weight: 600;\n color: var(--heading);\n }\n\n .project-slug {\n font-size: 0.8rem;\n color: var(--text-muted);\n font-family: monospace;\n }\n\n .completion-evidence { margin: 12px 0; line-height: 1.7; }\n details > summary { cursor: pointer; margin: 12px 0; }\n .status-badge {\n font-size: 0.75rem;\n font-weight: 600;\n text-transform: uppercase;\n padding: 0.2rem 0.55rem;\n border-radius: 12px;\n display: inline-block;\n white-space: nowrap;\n }\n\n .status-active { background: rgba(63, 185, 80, 0.15); color: var(--success); border: 1px solid rgba(63, 185, 80, 0.3); }\n .status-planned { background: rgba(88, 166, 255, 0.15); color: var(--accent); border: 1px solid rgba(88, 166, 255, 0.3); }\n .status-idea { background: rgba(188, 140, 255, 0.15); color: var(--purple); border: 1px solid rgba(188, 140, 255, 0.3); }\n .status-blocked { background: rgba(248, 81, 73, 0.15); color: var(--danger); border: 1px solid rgba(248, 81, 73, 0.3); }\n .status-paused { background: rgba(210, 153, 34, 0.15); color: var(--warning); border: 1px solid rgba(210, 153, 34, 0.3); }\n .status-completed { background: rgba(57, 197, 207, 0.15); color: var(--cyan); border: 1px solid rgba(57, 197, 207, 0.3); }\n .status-archived { background: rgba(139, 148, 158, 0.15); color: var(--text-muted); border: 1px solid rgba(139, 148, 158, 0.3); }\n\n .card-body {\n margin-bottom: 1rem;\n font-size: 0.9rem;\n }\n\n .card-description {\n color: var(--text);\n margin-bottom: 0.5rem;\n }\n\n .card-notes {\n color: var(--text-muted);\n font-size: 0.85rem;\n font-style: italic;\n border-left: 2px solid var(--border);\n padding-left: 0.5rem;\n margin-top: 0.5rem;\n }\n\n .latest-summary {\n background: rgba(0, 0, 0, 0.2);\n padding: 0.6rem;\n border-radius: 6px;\n margin-top: 0.75rem;\n font-size: 0.85rem;\n }\n\n .latest-summary-title {\n font-size: 0.75rem;\n color: var(--text-muted);\n text-transform: uppercase;\n margin-bottom: 0.25rem;\n }\n\n .card-footer {\n display: flex;\n align-items: center;\n justify-content: space-between;\n font-size: 0.85rem;\n color: var(--text-muted);\n border-top: 1px solid var(--border);\n padding-top: 0.75rem;\n margin-top: auto;\n }\n\n .link-btn {\n color: var(--accent);\n text-decoration: none;\n cursor: pointer;\n font-weight: 500;\n }\n\n .link-btn:hover {\n text-decoration: underline;\n }\n\n /* Timeline View */\n .timeline {\n display: flex;\n flex-direction: column;\n gap: 1rem;\n position: relative;\n }\n\n .timeline-item {\n background: var(--card-bg);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n padding: 1.25rem;\n position: relative;\n }\n\n .timeline-header {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: space-between;\n gap: 0.5rem;\n margin-bottom: 0.75rem;\n }\n\n .timeline-meta {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n flex-wrap: wrap;\n }\n\n .project-tag {\n background: rgba(88, 166, 255, 0.1);\n color: var(--accent);\n border: 1px solid rgba(88, 166, 255, 0.2);\n padding: 0.15rem 0.5rem;\n border-radius: 4px;\n font-family: monospace;\n font-size: 0.85rem;\n cursor: pointer;\n }\n\n .time-stamp {\n font-size: 0.8rem;\n color: var(--text-muted);\n }\n\n .source-badge {\n font-size: 0.75rem;\n color: var(--text-muted);\n background: rgba(255, 255, 255, 0.05);\n padding: 0.1rem 0.4rem;\n border-radius: 4px;\n }\n\n .entry-summary {\n font-size: 1rem;\n font-weight: 500;\n color: var(--heading);\n margin-bottom: 0.5rem;\n }\n\n .entry-progress {\n font-size: 0.9rem;\n color: var(--text);\n margin-bottom: 0.75rem;\n }\n\n .section-box {\n margin-top: 0.5rem;\n font-size: 0.875rem;\n }\n\n .section-title {\n font-weight: 600;\n font-size: 0.8rem;\n text-transform: uppercase;\n margin-bottom: 0.25rem;\n display: flex;\n align-items: center;\n gap: 0.25rem;\n }\n\n .section-title.blockers { color: var(--danger); }\n .section-title.next-steps { color: var(--accent); }\n .section-title.decisions { color: var(--purple); }\n\n .bullet-list {\n list-style: disc inside;\n color: var(--text);\n padding-left: 0.25rem;\n }\n\n .bullet-list li {\n margin-bottom: 0.2rem;\n }\n\n .tags-container {\n display: flex;\n flex-wrap: wrap;\n gap: 0.35rem;\n margin-top: 0.75rem;\n }\n\n .pill-tag {\n background: rgba(255, 255, 255, 0.06);\n border: 1px solid var(--border);\n color: var(--text-muted);\n font-size: 0.75rem;\n padding: 0.1rem 0.45rem;\n border-radius: 12px;\n }\n\n .empty-state {\n text-align: center;\n padding: 3rem 1rem;\n color: var(--text-muted);\n }\n\n .empty-state h3 {\n color: var(--heading);\n margin-bottom: 0.5rem;\n }\n\n .filter-bar {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n margin-bottom: 1rem;\n font-size: 0.875rem;\n color: var(--text-muted);\n }\n\n .filter-chip {\n background: rgba(88, 166, 255, 0.15);\n color: var(--accent);\n padding: 0.2rem 0.6rem;\n border-radius: 12px;\n display: inline-flex;\n align-items: center;\n gap: 0.3rem;\n }\n\n .filter-chip button {\n background: none;\n border: none;\n color: var(--accent);\n cursor: pointer;\n font-weight: bold;\n }";
|