@homescriptone/opencode-viewer-extender 1.3.0 → 1.5.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/README.md +10 -6
- package/package.json +1 -1
- package/public/index.html +1 -0
- package/public/plugins/bulk-select.js +74 -48
- package/public/script.js +109 -6
- package/public/styles.css +89 -0
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ Reads and writes `~/.local/share/opencode/opencode.db`. Everything stays on your
|
|
|
11
11
|
bunx @homescriptone/opencode-viewer-extender
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
> On npm: [`@homescriptone/opencode-viewer-extender`](https://www.npmjs.com/package/@homescriptone/opencode-viewer-extender)
|
|
14
|
+
> On npm: [`@homescriptone/opencode-viewer-extender`](https://www.npmjs.com/package/@homescriptone/opencode-viewer-extender).
|
|
15
15
|
|
|
16
16
|
## Features
|
|
17
17
|
|
|
@@ -25,7 +25,7 @@ bunx @homescriptone/opencode-viewer-extender
|
|
|
25
25
|
|
|
26
26
|
## Requirements
|
|
27
27
|
|
|
28
|
-
- [Bun](https://bun.sh) runtime (`curl -fsSL https://bun.sh/install | bash`)
|
|
28
|
+
- [Bun](https://bun.sh) runtime (`curl -fsSL https://bun.sh/install | bash`)
|
|
29
29
|
- [OpenCode](https://opencode.ai) installed and used at least once
|
|
30
30
|
|
|
31
31
|
## Usage
|
|
@@ -58,7 +58,7 @@ The database is opened **read-only** by default. Writes only become available wh
|
|
|
58
58
|
## Export
|
|
59
59
|
|
|
60
60
|
- **Single session** — the **Export ▾** menu in the message header offers Markdown (`.md`) and JSON (`.json`), served by `GET /api/sessions/:id/export?format=md|json`.
|
|
61
|
-
- **Bulk** — turn on **Select**,
|
|
61
|
+
- **Bulk** — turn on **Select**, use **All projects** / **All sessions** (or pick individual items), and choose **Export selected (.zip)**. A progress indicator appears for larger exports. The archive is grouped into folders, and every item is written in three formats (`.json`, `.xml`, `.html`):
|
|
62
62
|
|
|
63
63
|
```
|
|
64
64
|
manifest.json
|
|
@@ -157,11 +157,15 @@ bun run typecheck
|
|
|
157
157
|
|
|
158
158
|
## Releasing
|
|
159
159
|
|
|
160
|
-
Publishing
|
|
160
|
+
Publishing uses npm **trusted publishing (OIDC)** — no tokens or secrets. Push a version tag:
|
|
161
161
|
|
|
162
|
-
|
|
162
|
+
```bash
|
|
163
|
+
git tag v1.3.0 && git push origin v1.3.0
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
[`.github/workflows/publish.yml`](.github/workflows/publish.yml) then type-checks and runs `npm publish` with provenance.
|
|
163
167
|
|
|
164
|
-
|
|
168
|
+
One-time setup on [npmjs.com](https://www.npmjs.com): open the package → **Settings** → **Trusted Publisher** → **GitHub Actions**, and set the repository (`homescriptone/opencode-viewer-extended`) and workflow filename (`publish.yml`). No `NPM_TOKEN` secret is required.
|
|
165
169
|
|
|
166
170
|
## Credits
|
|
167
171
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@homescriptone/opencode-viewer-extender",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Local GUI to browse, edit, and export your OpenCode sessions, messages, and tool calls from the SQLite database, with a plugin API",
|
|
5
5
|
"bin": {
|
|
6
6
|
"opencode-viewer": "bin/cli.js"
|
package/public/index.html
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
<main id="messages-panel" class="panel"></main>
|
|
17
17
|
</div>
|
|
18
18
|
<div id="bulk-bar" class="bulk-bar" hidden></div>
|
|
19
|
+
<div id="status-toast" class="status-toast" hidden></div>
|
|
19
20
|
<div id="modal-root"></div>
|
|
20
21
|
<script src="plugin-api.js"></script>
|
|
21
22
|
<script src="script.js"></script>
|
|
@@ -26,55 +26,73 @@ OCV.define({
|
|
|
26
26
|
const seenSessions = new Set();
|
|
27
27
|
const projectsById = new Map(host.getProjects().map((project) => [project.id, project]));
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (seenSessions.has(sessionId)) return;
|
|
32
|
-
seenSessions.add(sessionId);
|
|
29
|
+
try {
|
|
30
|
+
host.setStatus("Preparing export…");
|
|
33
31
|
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
};
|
|
32
|
+
const tasks = [];
|
|
33
|
+
for (const projectId of projectIds) {
|
|
34
|
+
const project = projectsById.get(projectId) || { id: projectId };
|
|
35
|
+
const folderName = sanitize(`${project.name || projectId}-${projectId}`);
|
|
36
|
+
const base = `projects/${folderName}`;
|
|
40
37
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const base = `projects/${folderName}`;
|
|
38
|
+
entries.push({ name: `${base}/project.json`, data: JSON.stringify(project, null, 2) });
|
|
39
|
+
entries.push({ name: `${base}/project.xml`, data: toXmlDocument("project", project) });
|
|
40
|
+
entries.push({ name: `${base}/project.html`, data: toProjectHtml(project) });
|
|
45
41
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
42
|
+
const sessions = await host.api.getProjectSessions(projectId);
|
|
43
|
+
for (const session of sessions) {
|
|
44
|
+
if (seenSessions.has(session.id)) continue;
|
|
45
|
+
seenSessions.add(session.id);
|
|
46
|
+
tasks.push({ sessionId: session.id, folder: `${base}/sessions/${sanitize(session.id)}` });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
49
|
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
for (const sessionId of sessionIds) {
|
|
51
|
+
if (seenSessions.has(sessionId)) continue;
|
|
52
|
+
seenSessions.add(sessionId);
|
|
53
|
+
tasks.push({ sessionId, folder: `sessions/${sanitize(sessionId)}` });
|
|
53
54
|
}
|
|
54
|
-
}
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
const total = tasks.length;
|
|
57
|
+
let done = 0;
|
|
58
|
+
if (total === 0) {
|
|
59
|
+
host.setStatus("Building archive…");
|
|
60
|
+
} else {
|
|
61
|
+
host.setProgress(0, total, "Collecting sessions…");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
for (const task of tasks) {
|
|
65
|
+
const detail = await host.api.getSessionDetail(task.sessionId);
|
|
66
|
+
const title = detail?.session?.title || task.sessionId;
|
|
67
|
+
entries.push({ name: `${task.folder}/session.json`, data: JSON.stringify(detail, null, 2) });
|
|
68
|
+
entries.push({ name: `${task.folder}/session.xml`, data: toXmlDocument("export", detail) });
|
|
69
|
+
entries.push({ name: `${task.folder}/session.html`, data: toHtmlDocument(title, detail) });
|
|
70
|
+
done += 1;
|
|
71
|
+
host.setProgress(done, total, "Collecting sessions…");
|
|
72
|
+
}
|
|
59
73
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
+
host.setStatus("Building archive…");
|
|
75
|
+
entries.push({
|
|
76
|
+
name: "manifest.json",
|
|
77
|
+
data: JSON.stringify(
|
|
78
|
+
{
|
|
79
|
+
exportedAt: new Date().toISOString(),
|
|
80
|
+
formats: ["json", "xml", "html"],
|
|
81
|
+
projectIds,
|
|
82
|
+
sessionIds,
|
|
83
|
+
files: entries.length + 1
|
|
84
|
+
},
|
|
85
|
+
null,
|
|
86
|
+
2
|
|
87
|
+
)
|
|
88
|
+
});
|
|
74
89
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
90
|
+
const blob = host.createZip(entries);
|
|
91
|
+
host.download(`ocv-export-${Date.now()}.zip`, blob, "application/zip");
|
|
92
|
+
host.notify(`Exported ${entries.length} files (json/xml/html).`);
|
|
93
|
+
} finally {
|
|
94
|
+
host.clearStatus();
|
|
95
|
+
}
|
|
78
96
|
}
|
|
79
97
|
});
|
|
80
98
|
|
|
@@ -92,14 +110,22 @@ OCV.define({
|
|
|
92
110
|
const ok = window.confirm(`Delete ${parts.join(" and ")}? This cannot be undone.`);
|
|
93
111
|
if (!ok) return;
|
|
94
112
|
|
|
113
|
+
const total = projectIds.length + sessionIds.length;
|
|
95
114
|
let deleted = 0;
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
115
|
+
try {
|
|
116
|
+
host.setProgress(0, total, "Deleting…");
|
|
117
|
+
for (const projectId of projectIds) {
|
|
118
|
+
await host.api.deleteProject(projectId);
|
|
119
|
+
deleted += 1;
|
|
120
|
+
host.setProgress(deleted, total, "Deleting…");
|
|
121
|
+
}
|
|
122
|
+
for (const sessionId of sessionIds) {
|
|
123
|
+
await host.api.deleteSession(sessionId);
|
|
124
|
+
deleted += 1;
|
|
125
|
+
host.setProgress(deleted, total, "Deleting…");
|
|
126
|
+
}
|
|
127
|
+
} finally {
|
|
128
|
+
host.clearStatus();
|
|
103
129
|
}
|
|
104
130
|
|
|
105
131
|
host.clearSelection();
|
package/public/script.js
CHANGED
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
selectedSessionIds: new Set(),
|
|
29
29
|
pluginsOpen: false,
|
|
30
30
|
writeEnabled: false,
|
|
31
|
-
editMode: false
|
|
31
|
+
editMode: false,
|
|
32
|
+
status: null
|
|
32
33
|
};
|
|
33
34
|
|
|
34
35
|
const el = {
|
|
@@ -36,6 +37,7 @@
|
|
|
36
37
|
sessions: document.getElementById("sessions-panel"),
|
|
37
38
|
messages: document.getElementById("messages-panel"),
|
|
38
39
|
bulkBar: document.getElementById("bulk-bar"),
|
|
40
|
+
statusToast: document.getElementById("status-toast"),
|
|
39
41
|
modal: document.getElementById("modal-root")
|
|
40
42
|
};
|
|
41
43
|
|
|
@@ -745,15 +747,49 @@
|
|
|
745
747
|
renderBulkBar();
|
|
746
748
|
}
|
|
747
749
|
|
|
750
|
+
function renderStatus() {
|
|
751
|
+
if (!el.statusToast) return;
|
|
752
|
+
const status = state.status;
|
|
753
|
+
if (!status || (!status.label && status.current === undefined)) {
|
|
754
|
+
el.statusToast.hidden = true;
|
|
755
|
+
el.statusToast.innerHTML = "";
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
const { label = "", current, total } = status;
|
|
760
|
+
let progressHtml = "";
|
|
761
|
+
if (typeof current === "number" && typeof total === "number" && total > 0) {
|
|
762
|
+
const pct = Math.max(0, Math.min(100, Math.round((current / total) * 100)));
|
|
763
|
+
progressHtml = `
|
|
764
|
+
<div class="status-progress"><div class="status-progress-fill" style="width:${pct}%"></div></div>
|
|
765
|
+
<div class="status-count">${current} / ${total}</div>
|
|
766
|
+
`;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
el.statusToast.hidden = false;
|
|
770
|
+
el.statusToast.innerHTML = `
|
|
771
|
+
<span class="status-spinner" aria-hidden="true"></span>
|
|
772
|
+
<div class="status-body">
|
|
773
|
+
<div class="status-label">${escapeHtml(label || "Working…")}</div>
|
|
774
|
+
${progressHtml}
|
|
775
|
+
</div>
|
|
776
|
+
`;
|
|
777
|
+
}
|
|
778
|
+
|
|
748
779
|
function renderBulkBar() {
|
|
749
780
|
if (!el.bulkBar) return;
|
|
750
|
-
|
|
751
|
-
if (!state.selectMode || count === 0) {
|
|
781
|
+
if (!state.selectMode) {
|
|
752
782
|
el.bulkBar.hidden = true;
|
|
753
783
|
el.bulkBar.innerHTML = "";
|
|
754
784
|
return;
|
|
755
785
|
}
|
|
756
786
|
|
|
787
|
+
const count = state.selectedProjectIds.size + state.selectedSessionIds.size;
|
|
788
|
+
const allProjectsSelected =
|
|
789
|
+
state.projects.length > 0 && state.projects.every((project) => state.selectedProjectIds.has(project.id));
|
|
790
|
+
const allSessionsSelected =
|
|
791
|
+
state.sessions.length > 0 && state.sessions.every((session) => state.selectedSessionIds.has(session.id));
|
|
792
|
+
|
|
757
793
|
const buttons = (window.OCV?.listBulkActions?.() ?? [])
|
|
758
794
|
.filter((action) => !action.write || (state.writeEnabled && state.editMode))
|
|
759
795
|
.map(
|
|
@@ -765,8 +801,10 @@
|
|
|
765
801
|
el.bulkBar.hidden = false;
|
|
766
802
|
el.bulkBar.innerHTML = `
|
|
767
803
|
<span class="bulk-count">${count} selected</span>
|
|
768
|
-
${
|
|
769
|
-
<button class="bulk-btn
|
|
804
|
+
<button class="bulk-btn ${allProjectsSelected ? "active" : ""}" data-action="bulk-select-all-projects"${state.projects.length === 0 ? " disabled" : ""}>All projects</button>
|
|
805
|
+
<button class="bulk-btn ${allSessionsSelected ? "active" : ""}" data-action="bulk-select-all-sessions"${state.sessions.length === 0 ? " disabled" : ""}>All sessions</button>
|
|
806
|
+
<button class="bulk-btn ghost" data-action="bulk-clear"${count === 0 ? " disabled" : ""}>Clear</button>
|
|
807
|
+
${count > 0 ? buttons : ""}
|
|
770
808
|
`;
|
|
771
809
|
}
|
|
772
810
|
|
|
@@ -1114,7 +1152,12 @@
|
|
|
1114
1152
|
)
|
|
1115
1153
|
.join("");
|
|
1116
1154
|
|
|
1117
|
-
|
|
1155
|
+
const editActions =
|
|
1156
|
+
state.writeEnabled && state.editMode
|
|
1157
|
+
? `<button class="header-btn danger" data-action="delete-session">Delete</button>`
|
|
1158
|
+
: "";
|
|
1159
|
+
|
|
1160
|
+
return `<div class="header-actions">${editActions}${pluginActions}${exportMenu}</div>`;
|
|
1118
1161
|
}
|
|
1119
1162
|
|
|
1120
1163
|
function renderMessagesPanel() {
|
|
@@ -1545,10 +1588,31 @@
|
|
|
1545
1588
|
return;
|
|
1546
1589
|
}
|
|
1547
1590
|
|
|
1591
|
+
if (action === "delete-session") {
|
|
1592
|
+
const sessionId = state.selectedSessionId;
|
|
1593
|
+
if (!sessionId) return;
|
|
1594
|
+
const title = state.sessionData?.title || getSelectedSession()?.title || sessionId;
|
|
1595
|
+
const ok = window.confirm(`Delete session "${title}"? This cannot be undone.`);
|
|
1596
|
+
if (!ok) return;
|
|
1597
|
+
try {
|
|
1598
|
+
window.OCV?.host?.setStatus?.("Deleting session…");
|
|
1599
|
+
await window.OCV.host.api.deleteSession(sessionId);
|
|
1600
|
+
clearSelectedSession();
|
|
1601
|
+
await loadSessions({ reset: true });
|
|
1602
|
+
} catch (error) {
|
|
1603
|
+
console.error("Delete session failed:", error);
|
|
1604
|
+
alert(`Delete failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1605
|
+
} finally {
|
|
1606
|
+
window.OCV?.host?.clearStatus?.();
|
|
1607
|
+
}
|
|
1608
|
+
return;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1548
1611
|
if (action === "toggle-edit-mode") {
|
|
1549
1612
|
state.editMode = !state.editMode;
|
|
1550
1613
|
renderProjectsPanel();
|
|
1551
1614
|
renderSessionsPanel();
|
|
1615
|
+
renderMessagesPanel();
|
|
1552
1616
|
renderBulkBar();
|
|
1553
1617
|
return;
|
|
1554
1618
|
}
|
|
@@ -1585,6 +1649,32 @@
|
|
|
1585
1649
|
return;
|
|
1586
1650
|
}
|
|
1587
1651
|
|
|
1652
|
+
if (action === "bulk-select-all-projects") {
|
|
1653
|
+
const allSelected =
|
|
1654
|
+
state.projects.length > 0 && state.projects.every((project) => state.selectedProjectIds.has(project.id));
|
|
1655
|
+
if (allSelected) {
|
|
1656
|
+
for (const project of state.projects) state.selectedProjectIds.delete(project.id);
|
|
1657
|
+
} else {
|
|
1658
|
+
for (const project of state.projects) state.selectedProjectIds.add(project.id);
|
|
1659
|
+
}
|
|
1660
|
+
renderProjectsPanel();
|
|
1661
|
+
renderBulkBar();
|
|
1662
|
+
return;
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
if (action === "bulk-select-all-sessions") {
|
|
1666
|
+
const allSelected =
|
|
1667
|
+
state.sessions.length > 0 && state.sessions.every((session) => state.selectedSessionIds.has(session.id));
|
|
1668
|
+
if (allSelected) {
|
|
1669
|
+
for (const session of state.sessions) state.selectedSessionIds.delete(session.id);
|
|
1670
|
+
} else {
|
|
1671
|
+
for (const session of state.sessions) state.selectedSessionIds.add(session.id);
|
|
1672
|
+
}
|
|
1673
|
+
renderSessionsPanel();
|
|
1674
|
+
renderBulkBar();
|
|
1675
|
+
return;
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1588
1678
|
if (action === "bulk-clear") {
|
|
1589
1679
|
state.selectedProjectIds.clear();
|
|
1590
1680
|
state.selectedSessionIds.clear();
|
|
@@ -1768,6 +1858,7 @@
|
|
|
1768
1858
|
renderMessagesPanel();
|
|
1769
1859
|
renderBulkBar();
|
|
1770
1860
|
renderPluginsDialog();
|
|
1861
|
+
renderStatus();
|
|
1771
1862
|
}
|
|
1772
1863
|
|
|
1773
1864
|
if (window.OCV?.host) {
|
|
@@ -1788,6 +1879,18 @@
|
|
|
1788
1879
|
download: (filename, content, mime) => downloadBlob(content, filename, mime),
|
|
1789
1880
|
createZip: (entries) => window.OCV.helpers.createZip(entries),
|
|
1790
1881
|
notify: (message) => console.log("[OCV]", message),
|
|
1882
|
+
setStatus: (label) => {
|
|
1883
|
+
state.status = { label };
|
|
1884
|
+
renderStatus();
|
|
1885
|
+
},
|
|
1886
|
+
setProgress: (current, total, label) => {
|
|
1887
|
+
state.status = { label, current, total };
|
|
1888
|
+
renderStatus();
|
|
1889
|
+
},
|
|
1890
|
+
clearStatus: () => {
|
|
1891
|
+
state.status = null;
|
|
1892
|
+
renderStatus();
|
|
1893
|
+
},
|
|
1791
1894
|
refresh: () => {
|
|
1792
1895
|
loadProjects();
|
|
1793
1896
|
if (state.selectedProjectId) loadSessions({ reset: true });
|
package/public/styles.css
CHANGED
|
@@ -995,6 +995,16 @@
|
|
|
995
995
|
color: var(--accent-ink);
|
|
996
996
|
}
|
|
997
997
|
|
|
998
|
+
.header-btn.danger {
|
|
999
|
+
color: #b23b3b;
|
|
1000
|
+
border-color: #e4bcbc;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
.header-btn.danger:hover {
|
|
1004
|
+
background: #fbeaea;
|
|
1005
|
+
border-color: #d99a9a;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
998
1008
|
.project-row.selectable,
|
|
999
1009
|
.session-card.selectable {
|
|
1000
1010
|
display: flex;
|
|
@@ -1083,6 +1093,22 @@
|
|
|
1083
1093
|
background: var(--accent-wash);
|
|
1084
1094
|
}
|
|
1085
1095
|
|
|
1096
|
+
.bulk-btn.active {
|
|
1097
|
+
border-color: var(--accent-hair);
|
|
1098
|
+
background: var(--accent-wash);
|
|
1099
|
+
color: var(--accent-ink);
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
.bulk-btn:disabled {
|
|
1103
|
+
opacity: 0.5;
|
|
1104
|
+
cursor: default;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
.bulk-btn:disabled:hover {
|
|
1108
|
+
background: none;
|
|
1109
|
+
border-color: var(--hair);
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1086
1112
|
.bulk-btn.danger {
|
|
1087
1113
|
color: #b23b3b;
|
|
1088
1114
|
border-color: #e4bcbc;
|
|
@@ -1097,6 +1123,69 @@
|
|
|
1097
1123
|
color: var(--muted);
|
|
1098
1124
|
}
|
|
1099
1125
|
|
|
1126
|
+
.status-toast {
|
|
1127
|
+
position: fixed;
|
|
1128
|
+
right: 20px;
|
|
1129
|
+
bottom: 20px;
|
|
1130
|
+
z-index: 450;
|
|
1131
|
+
display: flex;
|
|
1132
|
+
align-items: center;
|
|
1133
|
+
gap: 10px;
|
|
1134
|
+
min-width: 220px;
|
|
1135
|
+
max-width: 340px;
|
|
1136
|
+
padding: 12px 14px;
|
|
1137
|
+
background: var(--surface);
|
|
1138
|
+
border: 1px solid var(--hair-2);
|
|
1139
|
+
border-radius: 12px;
|
|
1140
|
+
box-shadow: 0 10px 30px rgba(20, 20, 20, 0.16);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
.status-spinner {
|
|
1144
|
+
flex: 0 0 auto;
|
|
1145
|
+
width: 16px;
|
|
1146
|
+
height: 16px;
|
|
1147
|
+
border: 2px solid var(--hair-2);
|
|
1148
|
+
border-top-color: var(--accent);
|
|
1149
|
+
border-radius: 50%;
|
|
1150
|
+
animation: status-spin 0.7s linear infinite;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
@keyframes status-spin {
|
|
1154
|
+
to { transform: rotate(360deg); }
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
.status-body {
|
|
1158
|
+
flex: 1;
|
|
1159
|
+
min-width: 0;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
.status-label {
|
|
1163
|
+
font-size: 12.5px;
|
|
1164
|
+
color: var(--ink);
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
.status-progress {
|
|
1168
|
+
margin-top: 8px;
|
|
1169
|
+
height: 6px;
|
|
1170
|
+
border-radius: 999px;
|
|
1171
|
+
background: var(--hair);
|
|
1172
|
+
overflow: hidden;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
.status-progress-fill {
|
|
1176
|
+
height: 100%;
|
|
1177
|
+
background: var(--accent);
|
|
1178
|
+
border-radius: 999px;
|
|
1179
|
+
transition: width 0.2s ease;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
.status-count {
|
|
1183
|
+
margin-top: 4px;
|
|
1184
|
+
font-size: 11px;
|
|
1185
|
+
color: var(--muted);
|
|
1186
|
+
font-variant-numeric: tabular-nums;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1100
1189
|
.modal-overlay {
|
|
1101
1190
|
position: fixed;
|
|
1102
1191
|
inset: 0;
|