sandtable 0.3.1 → 1.0.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/README.md +157 -22
- package/dashboard/dashboard.html +1320 -834
- package/harness/install-hooks.sh +40 -4
- package/package.json +1 -1
- package/server.js +54 -3
- package/src/builder/build.js +121 -230
- package/src/check/check.js +137 -0
- package/src/cli/sandtable.js +202 -8
- package/src/contract/default-contract.json +21 -0
- package/src/contract/loader.js +203 -0
- package/src/progress/parser.js +302 -0
- package/src/scanner/scan.js +47 -251
- package/src/scanner/scan.js.v0.4.bak +415 -0
- package/templates/.sandtable.template.json +24 -26
package/dashboard/dashboard.html
CHANGED
|
@@ -3,740 +3,955 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<title
|
|
6
|
+
<title>Sandtable IDE · 沙盘 v0.3</title>
|
|
7
|
+
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
|
8
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
|
9
|
+
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet">
|
|
10
|
+
<script>
|
|
11
|
+
tailwind.config = {
|
|
12
|
+
darkMode: "class",
|
|
13
|
+
theme: {
|
|
14
|
+
extend: {
|
|
15
|
+
colors: {
|
|
16
|
+
"on-primary-container": "#fefcff",
|
|
17
|
+
"on-primary": "#ffffff",
|
|
18
|
+
"primary": "#0058be",
|
|
19
|
+
"surface-container": "#eceef0",
|
|
20
|
+
"on-primary-fixed": "#001a42",
|
|
21
|
+
"on-secondary-fixed": "#002113",
|
|
22
|
+
"on-secondary-fixed-variant": "#005236",
|
|
23
|
+
"surface-bright": "#f7f9fb",
|
|
24
|
+
"inverse-on-surface": "#eff1f3",
|
|
25
|
+
"on-error": "#ffffff",
|
|
26
|
+
"primary-fixed-dim": "#adc6ff",
|
|
27
|
+
"surface-variant": "#e0e3e5",
|
|
28
|
+
"tertiary-container": "#a36700",
|
|
29
|
+
"on-surface": "#191c1e",
|
|
30
|
+
"on-tertiary-fixed-variant": "#653e00",
|
|
31
|
+
"secondary": "#006c49",
|
|
32
|
+
"secondary-fixed": "#6ffbbe",
|
|
33
|
+
"on-secondary": "#ffffff",
|
|
34
|
+
"background": "#f7f9fb",
|
|
35
|
+
"on-background": "#191c1e",
|
|
36
|
+
"on-error-container": "#93000a",
|
|
37
|
+
"surface-container-lowest": "#ffffff",
|
|
38
|
+
"surface-tint": "#005ac2",
|
|
39
|
+
"surface-dim": "#d8dadc",
|
|
40
|
+
"inverse-surface": "#2d3133",
|
|
41
|
+
"on-tertiary": "#ffffff",
|
|
42
|
+
"error": "#ba1a1a",
|
|
43
|
+
"surface-container-highest": "#e0e3e5",
|
|
44
|
+
"outline-design": "#727785",
|
|
45
|
+
"primary-fixed": "#d8e2ff",
|
|
46
|
+
"inverse-primary": "#adc6ff",
|
|
47
|
+
"surface": "#f7f9fb",
|
|
48
|
+
"secondary-container": "#6cf8bb",
|
|
49
|
+
"surface-container-high": "#e6e8ea",
|
|
50
|
+
"tertiary-fixed": "#ffddb8",
|
|
51
|
+
"surface-container-low": "#f2f4f6",
|
|
52
|
+
"on-surface-variant": "#424754",
|
|
53
|
+
"tertiary-fixed-dim": "#ffb95f",
|
|
54
|
+
"on-tertiary-container": "#fffbff",
|
|
55
|
+
"secondary-fixed-dim": "#4edea3",
|
|
56
|
+
"on-secondary-container": "#00714d",
|
|
57
|
+
"on-primary-fixed-variant": "#004395",
|
|
58
|
+
"error-container": "#ffdad6",
|
|
59
|
+
"primary-container": "#2170e4",
|
|
60
|
+
"outline-variant": "#c2c6d6",
|
|
61
|
+
"tertiary": "#825100",
|
|
62
|
+
"on-tertiary-fixed": "#2a1700"
|
|
63
|
+
},
|
|
64
|
+
borderRadius: {
|
|
65
|
+
"DEFAULT": "0.25rem",
|
|
66
|
+
"lg": "0.5rem",
|
|
67
|
+
"xl": "0.75rem",
|
|
68
|
+
"full": "9999px"
|
|
69
|
+
},
|
|
70
|
+
spacing: {
|
|
71
|
+
"base": "4px",
|
|
72
|
+
"xl": "32px",
|
|
73
|
+
"md": "16px",
|
|
74
|
+
"xs": "4px",
|
|
75
|
+
"container-margin": "24px",
|
|
76
|
+
"sm": "8px",
|
|
77
|
+
"lg": "24px",
|
|
78
|
+
"gutter": "16px"
|
|
79
|
+
},
|
|
80
|
+
fontFamily: {
|
|
81
|
+
"code-md": ["JetBrains Mono"],
|
|
82
|
+
"code-sm": ["JetBrains Mono"],
|
|
83
|
+
"display-lg": ["Inter"],
|
|
84
|
+
"headline-sm": ["Inter"],
|
|
85
|
+
"headline-md": ["Inter"],
|
|
86
|
+
"body-md": ["Inter"],
|
|
87
|
+
"title-md": ["Inter"],
|
|
88
|
+
"body-lg": ["Inter"],
|
|
89
|
+
"label-caps": ["Inter"]
|
|
90
|
+
},
|
|
91
|
+
fontSize: {
|
|
92
|
+
"code-md": ["14px", { lineHeight: "20px", fontWeight: "500" }],
|
|
93
|
+
"code-sm": ["12px", { lineHeight: "16px", fontWeight: "500" }],
|
|
94
|
+
"display-lg": ["32px", { lineHeight: "40px", letterSpacing: "-0.02em", fontWeight: "600" }],
|
|
95
|
+
"headline-sm": ["20px", { lineHeight: "28px", fontWeight: "600" }],
|
|
96
|
+
"headline-md": ["24px", { lineHeight: "32px", letterSpacing: "-0.01em", fontWeight: "600" }],
|
|
97
|
+
"body-md": ["14px", { lineHeight: "20px", fontWeight: "400" }],
|
|
98
|
+
"title-md": ["16px", { lineHeight: "24px", fontWeight: "600" }],
|
|
99
|
+
"body-lg": ["16px", { lineHeight: "26px", fontWeight: "400" }],
|
|
100
|
+
"label-caps": ["12px", { lineHeight: "16px", letterSpacing: "0.05em", fontWeight: "700" }]
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
</script>
|
|
7
106
|
<style>
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
107
|
+
.material-symbols-outlined {
|
|
108
|
+
font-family: 'Material Symbols Outlined', sans-serif;
|
|
109
|
+
font-weight: normal; font-style: normal;
|
|
110
|
+
font-size: 20px; line-height: 1;
|
|
111
|
+
letter-spacing: normal; text-transform: none;
|
|
112
|
+
display: inline-block; white-space: nowrap;
|
|
113
|
+
word-wrap: normal; direction: ltr;
|
|
114
|
+
-webkit-font-smoothing: antialiased;
|
|
115
|
+
}
|
|
116
|
+
.pulse-border {
|
|
117
|
+
animation: pulse-border 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
|
118
|
+
}
|
|
119
|
+
@keyframes pulse-border {
|
|
120
|
+
0%, 100% { border-color: rgba(59, 130, 246, 1); box-shadow: 0 0 0px rgba(59, 130, 246, 0); }
|
|
121
|
+
50% { border-color: rgba(59, 130, 246, 0.3); box-shadow: 0 0 8px rgba(59, 130, 246, 0.4); }
|
|
122
|
+
}
|
|
123
|
+
.accordion-content {
|
|
124
|
+
transition: max-height 0.3s ease-in-out, opacity 0.3s ease-in-out;
|
|
125
|
+
max-height: 2000px; opacity: 1; overflow: hidden;
|
|
126
|
+
}
|
|
127
|
+
.accordion-content.collapsed {
|
|
128
|
+
max-height: 0; opacity: 0;
|
|
20
129
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
#header h1::before{content:'◆';color:var(--accent);font-size:10px}
|
|
28
|
-
.badge{background:rgba(255,255,255,.1);padding:2px 10px;border-radius:10px;font-size:10px;font-weight:500;letter-spacing:.3px}
|
|
29
|
-
#header .right{font-size:11px;display:flex;align-items:center;gap:12px;color:#9aa0ab;position:relative}
|
|
30
|
-
.dot{width:8px;height:8px;border-radius:50%;background:var(--accent);display:inline-block;box-shadow:0 0 6px rgba(0,184,148,.4);animation:pulse 2s infinite}
|
|
31
|
-
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.5}}
|
|
32
|
-
|
|
33
|
-
/* Brief */
|
|
34
|
-
#brief{background:linear-gradient(135deg,#16213e,#0f3460);color:#b0bec5;padding:10px 24px;font-size:12px;line-height:1.6;border-bottom:2px solid rgba(0,184,148,.2)}
|
|
35
|
-
#brief strong{color:var(--accent);font-size:11px;margin-right:10px;text-transform:uppercase;letter-spacing:.5px}
|
|
36
|
-
#brief a{color:#64b5f6;text-decoration:none;cursor:pointer;transition:color .15s}
|
|
37
|
-
#brief a:hover{color:#90caf9;text-decoration:underline}
|
|
38
|
-
|
|
39
|
-
/* Tree tab bar — inside left panel */
|
|
40
|
-
#tree-tabs{display:flex;gap:4px;margin-bottom:10px;flex-wrap:wrap;align-items:center}
|
|
41
|
-
#tree-tabs button{padding:4px 10px;border:1px solid #d5dce6;background:#fff;border-radius:7px;font-size:10.5px;cursor:pointer;color:var(--muted);transition:all .15s;white-space:nowrap;font-weight:500}
|
|
42
|
-
#tree-tabs button:hover{border-color:var(--accent);color:var(--accent);background:#f0fdf9}
|
|
43
|
-
#tree-tabs button.on{background:var(--accent);color:#fff;border-color:var(--accent);font-weight:600}
|
|
44
|
-
|
|
45
|
-
/* Main dual layout */
|
|
46
|
-
#main{display:flex;flex:1;min-height:0}
|
|
47
|
-
#left{width:40%;border-right:1px solid var(--border);overflow-y:auto;padding:20px 24px;background:#fafbfc;transition:background .5s ease}
|
|
48
|
-
#right{width:60%;overflow-y:auto;padding:20px 24px;background:#f8f9fb;transition:background .5s ease}
|
|
49
|
-
#left::-webkit-scrollbar,#right::-webkit-scrollbar{width:5px}
|
|
50
|
-
#left::-webkit-scrollbar-thumb,#right::-webkit-scrollbar-thumb{background:#d0d5dd;border-radius:3px}
|
|
51
|
-
|
|
52
|
-
/* Filter-connected: unified panel tint when doc filter is active */
|
|
53
|
-
body.filter-active #left, body.filter-active #right{background:#eef2f7}
|
|
54
|
-
body.filter-active #left{border-right-color:#d5dce6}
|
|
55
|
-
.tree-card.filter-match{border-color:#4a90d9!important;box-shadow:0 0 0 3px rgba(74,144,217,.2)!important;background:#fff!important}
|
|
56
|
-
.tree-card.filter-match .tc-head{background:rgba(74,144,217,.06)}
|
|
57
|
-
.rp-card.filter-match{border-color:#4a90d9!important;box-shadow:0 0 0 3px rgba(74,144,217,.2)!important}
|
|
58
|
-
|
|
59
|
-
.panel-title{font-size:13px;font-weight:700;margin-bottom:14px;display:flex;justify-content:space-between;align-items:center;color:var(--text);letter-spacing:-.2px}
|
|
60
|
-
.panel-title .count{font-size:11px;color:var(--muted);font-weight:500}
|
|
61
|
-
|
|
62
|
-
/* Category section */
|
|
63
|
-
.cat-section{margin-bottom:16px}
|
|
64
|
-
.cat-header{font-size:11px;font-weight:600;padding:5px 10px;border-radius:5px;margin-bottom:8px;display:flex;justify-content:space-between;letter-spacing:.2px}
|
|
65
|
-
.cat-header.primary{background:linear-gradient(135deg,#e6f9f1,#d4f5e8);color:#00855a}
|
|
66
|
-
.cat-header.secondary{background:#f1f2f5;color:var(--muted)}
|
|
67
|
-
|
|
68
|
-
/* Element card */
|
|
69
|
-
.card{border:1px solid var(--border);border-radius:8px;margin-bottom:10px;overflow:hidden;box-shadow:var(--shadow-sm);transition:box-shadow .2s,transform .15s;cursor:default}
|
|
70
|
-
.card:hover{box-shadow:var(--shadow-md);transform:translateY(-1px)}
|
|
71
|
-
.card.active-filter{border-color:var(--link);box-shadow:0 0 0 2px rgba(9,132,227,.25)}
|
|
72
|
-
.card[data-tg="past"]{background:var(--past-bg);border-color:var(--past-border)}
|
|
73
|
-
.card[data-tg="current"]{background:var(--current-bg);border-color:var(--current-border);box-shadow:0 2px 8px rgba(240,192,0,.12)}
|
|
74
|
-
.card[data-tg="future"]{background:var(--future-bg);border-color:var(--future-border)}
|
|
75
|
-
.card.secondary{border-left:3px solid var(--secondary)}
|
|
76
|
-
.card-head{padding:8px 14px;font-size:12.5px;font-weight:600;display:flex;justify-content:space-between;align-items:center;gap:8px}
|
|
77
|
-
.card-head.completed{background:linear-gradient(135deg,#e6f9f1,#d4f5e8);color:#00855a}
|
|
78
|
-
.card-head.in_progress{background:linear-gradient(135deg,#fff8e6,#fff3d4);color:#b06d00}
|
|
79
|
-
.card-head.pending{background:#f8f9fb;color:var(--muted)}
|
|
80
|
-
.card-head.blocked{background:linear-gradient(135deg,#fff0ed,#ffe8e3);color:#c04030}
|
|
81
|
-
.card-head > span:first-child{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
82
|
-
.card-meta{display:flex;align-items:center;gap:6px;font-size:10px;font-weight:400;flex-shrink:0}
|
|
83
|
-
.card-type{background:rgba(0,0,0,.06);padding:2px 6px;border-radius:4px;font-size:9px;color:var(--muted);font-weight:500}
|
|
84
|
-
.card-src{font-size:9px;background:#e8f1fd;padding:2px 7px;border-radius:4px;color:var(--link);cursor:pointer;text-decoration:none;max-width:110px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:500;transition:all .15s}
|
|
85
|
-
.card-src:hover{background:var(--link);color:#fff}
|
|
86
|
-
.card-summary{padding:6px 14px;font-size:11.5px;color:var(--light);line-height:1.5}
|
|
87
|
-
.card-tags{padding:3px 14px 8px}
|
|
88
|
-
.tag{display:inline-block;background:rgba(0,0,0,.05);padding:2px 7px;border-radius:4px;font-size:9.5px;color:var(--light);margin-right:4px;margin-bottom:2px;font-weight:500}
|
|
89
|
-
.card-children{border-top:1px solid rgba(0,0,0,.06);padding:4px 12px 8px}
|
|
90
|
-
.sub{font-size:11.5px;padding:4px 0;display:flex;align-items:center;gap:6px;color:var(--light);transition:background .15s;border-radius:4px;cursor:default}
|
|
91
|
-
.sub:hover{background:rgba(0,0,0,.03)}
|
|
92
|
-
|
|
93
|
-
/* Time badge */
|
|
94
|
-
.tbadge{font-size:9px;padding:1px 7px;border-radius:4px;font-weight:600;letter-spacing:.2px}
|
|
95
|
-
.tb-past{background:rgba(96,125,139,.12);color:#546e7a}
|
|
96
|
-
.tb-current{background:rgba(240,160,0,.12);color:#b06d00}
|
|
97
|
-
.tb-future{background:rgba(0,0,0,.06);color:var(--muted)}
|
|
98
|
-
|
|
99
|
-
/* Event stream */
|
|
100
|
-
.ev-item{padding:10px 12px;border-left:4px solid var(--border);margin-bottom:6px;border-radius:0 6px 6px 0;background:#fff;font-size:11.5px;box-shadow:var(--shadow-sm);transition:box-shadow .2s,transform .15s}
|
|
101
|
-
.ev-item:hover{box-shadow:var(--shadow-md)}
|
|
102
|
-
.ev-item.must{border-left-width:6px;background:#fffef7;box-shadow:0 2px 8px rgba(225,112,85,.1)}
|
|
103
|
-
.ev-item.t1{border-left-color:#e17055} .ev-item.t2{border-left-color:#6c5ce7}
|
|
104
|
-
.ev-item.t3{border-left-color:#0984e3} .ev-item.t4{border-left-color:#00b894}
|
|
105
|
-
.ev-item.t5{border-left-color:#f0a000} .ev-item.t6{border-left-color:#795548}
|
|
106
|
-
.ev-item.t7{border-left-color:#e84393}
|
|
107
|
-
.ev-head{display:flex;justify-content:space-between;align-items:flex-start;gap:8px}
|
|
108
|
-
.ev-title{font-weight:600;flex:1;line-height:1.4;font-size:12px}
|
|
109
|
-
.ev-type{font-size:9.5px;padding:2px 7px;border-radius:4px;color:#fff;font-weight:600;white-space:nowrap;letter-spacing:.3px}
|
|
110
|
-
.ev-t1{background:#e17055} .ev-t2{background:#6c5ce7} .ev-t3{background:#0984e3}
|
|
111
|
-
.ev-t4{background:#00b894} .ev-t5{background:#f0a000} .ev-t6{background:#795548} .ev-t7{background:#e84393}
|
|
112
|
-
.ev-meta{font-size:10px;color:var(--muted);margin-top:4px;display:flex;gap:8px;flex-wrap:wrap;align-items:center}
|
|
113
|
-
.ev-time{font-family:'SF Mono',Consolas,monospace;font-size:11px;color:var(--text);font-weight:600;background:rgba(0,0,0,.04);padding:1px 6px;border-radius:3px}
|
|
114
|
-
.ev-ref{color:var(--link);cursor:pointer;text-decoration:none;font-weight:500}
|
|
115
|
-
.ev-ref:hover{text-decoration:underline;color:#0066cc}
|
|
116
|
-
.ev-actor{font-style:italic;color:var(--light)}
|
|
117
|
-
.ev-tag{background:rgba(0,0,0,.05);padding:1px 5px;border-radius:3px;font-size:9px;color:var(--light)}
|
|
118
|
-
.ev-pdot{width:7px;height:7px;border-radius:50%;display:inline-block;margin-right:3px;flex-shrink:0}
|
|
119
|
-
.ev-p1{background:var(--priority-must);box-shadow:0 0 4px rgba(225,112,85,.4)}
|
|
120
|
-
.ev-p2{background:var(--priority-should)} .ev-p3{background:var(--priority-can)}
|
|
121
|
-
.ev-day{font-size:11px;font-weight:700;padding:12px 0 6px;border-bottom:1.5px solid var(--border);margin-bottom:6px;color:var(--light);letter-spacing:.3px}
|
|
122
|
-
.ev-day:first-child{padding-top:0}
|
|
123
|
-
.ev-item.hl{animation:flash .8s ease-in-out 3;background:#fff9c4!important}
|
|
124
|
-
@keyframes flash{0%,100%{background:#fff9c4}50%{background:#ffe082}}
|
|
125
|
-
|
|
126
|
-
/* Footer — removed conventions bar */
|
|
130
|
+
.hover-scale {
|
|
131
|
+
transition: transform 0.2s ease, background-color 0.2s ease, color 0.2s ease;
|
|
132
|
+
}
|
|
133
|
+
.hover-scale:hover { transform: scale(1.02); }
|
|
134
|
+
.hide-scrollbar::-webkit-scrollbar { display: none; }
|
|
135
|
+
.hide-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
|
|
127
136
|
|
|
128
|
-
/*
|
|
129
|
-
#
|
|
130
|
-
#
|
|
131
|
-
#float.fr{left:auto;right:0;box-shadow:-8px 0 30px rgba(0,0,0,.18)}
|
|
132
|
-
#float.fr.hide{transform:translateX(110vw)}
|
|
133
|
-
#float-head{display:flex;justify-content:space-between;align-items:center;padding:12px 16px;background:var(--header-bg);color:#fff;font-size:13px;font-weight:600}
|
|
134
|
-
#float-body{flex:1;overflow-y:auto;padding:16px 20px;font-size:12.5px;line-height:1.8;white-space:pre-wrap;word-break:break-word;font-family:'SF Mono',Consolas,monospace;color:var(--text)}
|
|
135
|
-
#float-body::-webkit-scrollbar{width:5px}
|
|
136
|
-
#float-body::-webkit-scrollbar-thumb{background:#d0d5dd;border-radius:3px}
|
|
137
|
-
#float-close{background:none;border:none;color:#fff;font-size:22px;cursor:pointer;opacity:.8;transition:opacity .15s;padding:0 4px}
|
|
138
|
-
#float-close:hover{opacity:1}
|
|
139
|
-
#overlay{position:fixed;top:0;left:0;width:100vw;height:100vh;background:rgba(0,0,0,.25);z-index:999;backdrop-filter:blur(2px)}
|
|
140
|
-
#overlay.hide{display:none}
|
|
141
|
-
|
|
142
|
-
/* Milestone tooltip */
|
|
143
|
-
.mtip{display:none;position:absolute;background:#fff;border:1px solid var(--border);border-radius:6px;padding:8px 12px;font-size:10.5px;box-shadow:var(--shadow-md);z-index:50;max-width:260px;line-height:1.5;left:100%;top:0;margin-left:10px}
|
|
144
|
-
.sub:hover .mtip{display:block}
|
|
145
|
-
|
|
146
|
-
.empty{text-align:center;padding:50px 20px;color:var(--muted)}
|
|
147
|
-
.empty h3{font-size:15px;margin-bottom:8px;font-weight:600}
|
|
148
|
-
.empty p{font-size:12px;color:var(--muted)}
|
|
149
|
-
|
|
150
|
-
/* Filter bar */
|
|
151
|
-
#filter-bar{display:none;padding:8px 12px;background:#e8f1fd;border:1px solid #b8d4f7;border-radius:8px;margin-bottom:12px;font-size:11px;align-items:center;gap:8px}
|
|
152
|
-
#filter-bar.show{display:flex}
|
|
153
|
-
#filter-bar .ftext{flex:1;color:var(--link);font-weight:500}
|
|
154
|
-
#filter-bar .fclear{cursor:pointer;background:var(--link);color:#fff;border:none;padding:3px 10px;border-radius:4px;font-size:10px;font-weight:600;transition:opacity .15s}
|
|
155
|
-
#filter-bar .fclear:hover{opacity:.85}
|
|
137
|
+
/* Scrollbar */
|
|
138
|
+
#left::-webkit-scrollbar, #right::-webkit-scrollbar { width: 5px; }
|
|
139
|
+
#left::-webkit-scrollbar-thumb, #right::-webkit-scrollbar-thumb { background: #d0d5dd; border-radius: 3px; }
|
|
156
140
|
|
|
157
|
-
/*
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
.
|
|
162
|
-
.
|
|
163
|
-
.
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
.
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
.
|
|
187
|
-
.
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
.
|
|
191
|
-
|
|
192
|
-
/* Sort + filter toolbar */
|
|
193
|
-
#toolbar{display:flex;align-items:center;gap:8px;padding:6px 0;margin-bottom:6px;flex-wrap:wrap}
|
|
194
|
-
#toolbar .sort-group{display:flex;gap:0;border:1px solid #d5dce6;border-radius:6px;overflow:hidden}
|
|
195
|
-
#toolbar .sort-btn{padding:3px 10px;font-size:10px;border:none;background:#fff;color:var(--muted);cursor:pointer;font-weight:500;transition:all .15s}
|
|
196
|
-
#toolbar .sort-btn.on{background:var(--accent);color:#fff}
|
|
197
|
-
#toolbar .sort-btn+.sort-btn{border-left:1px solid #d5dce6}
|
|
198
|
-
#toolbar .sort-label{font-size:9px;color:var(--muted);font-weight:600;text-transform:uppercase;letter-spacing:.3px}
|
|
199
|
-
#toolbar .fclear{display:none;cursor:pointer;background:var(--link);color:#fff;border:none;padding:3px 10px;border-radius:4px;font-size:10px;font-weight:600}
|
|
200
|
-
#toolbar .fclear.show{display:inline-block}
|
|
201
|
-
#toolbar .fclear:hover{opacity:.85}
|
|
202
|
-
#toolbar .ftext{font-size:10px;color:var(--link);font-weight:500;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
141
|
+
/* Floating panel */
|
|
142
|
+
#float { position: fixed; top: 0; left: 0; width: 480px; max-width: 94vw; height: 100vh;
|
|
143
|
+
background: #fff; box-shadow: 8px 0 30px rgba(0,0,0,.18); z-index: 1000;
|
|
144
|
+
display: flex; flex-direction: column; transition: transform .3s cubic-bezier(.4,0,.2,1); }
|
|
145
|
+
#float.hide { transform: translateX(-110vw); }
|
|
146
|
+
#float.fr { left: auto; right: 0; box-shadow: -8px 0 30px rgba(0,0,0,.18); }
|
|
147
|
+
#float.fr.hide { transform: translateX(110vw); }
|
|
148
|
+
#float-head { display: flex; justify-content: space-between; align-items: center;
|
|
149
|
+
padding: 12px 16px; background: #111827; color: #fff; font-size: 13px; font-weight: 600; }
|
|
150
|
+
#float-body { flex: 1; overflow-y: auto; padding: 16px 20px; font-size: 12.5px;
|
|
151
|
+
line-height: 1.8; white-space: pre-wrap; word-break: break-word;
|
|
152
|
+
font-family: 'JetBrains Mono', 'SF Mono', Consolas, monospace; color: #191c1e; }
|
|
153
|
+
#float-body::-webkit-scrollbar { width: 5px; }
|
|
154
|
+
#float-body::-webkit-scrollbar-thumb { background: #d0d5dd; border-radius: 3px; }
|
|
155
|
+
#float-close { background: none; border: none; color: #fff; font-size: 22px;
|
|
156
|
+
cursor: pointer; opacity: .8; transition: opacity .15s; padding: 0 4px; }
|
|
157
|
+
#float-close:hover { opacity: 1; }
|
|
158
|
+
#overlay { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;
|
|
159
|
+
background: rgba(0,0,0,.25); z-index: 999; backdrop-filter: blur(2px); }
|
|
160
|
+
#overlay.hide { display: none; }
|
|
161
|
+
|
|
162
|
+
/* Token tooltip */
|
|
163
|
+
#token-tip { display: none; position: absolute; top: 100%; right: 0; margin-top: 8px;
|
|
164
|
+
background: #1a2332; color: #e8eaed; padding: 10px 14px; border-radius: 8px;
|
|
165
|
+
font-size: 11px; line-height: 1.6; z-index: 100;
|
|
166
|
+
box-shadow: 0 4px 20px rgba(0,0,0,.35); min-width: 200px; white-space: nowrap; }
|
|
167
|
+
#token-tip.show { display: block; }
|
|
168
|
+
#token-tip .tip-row { display: flex; justify-content: space-between; gap: 16px; padding: 2px 0; }
|
|
169
|
+
#token-tip .tip-row .tip-name { color: #b0bec5; }
|
|
170
|
+
#token-tip .tip-row .tip-val { color: #fff; font-weight: 600; font-family: 'JetBrains Mono', 'SF Mono', Consolas, monospace; }
|
|
171
|
+
#token-tip .tip-divider { border-top: 1px solid rgba(255,255,255,.1); margin: 4px 0; }
|
|
172
|
+
|
|
173
|
+
/* Filter-connected panels */
|
|
174
|
+
body.filter-active #left, body.filter-active #right { background: #eef2f7; }
|
|
175
|
+
body.filter-active #left { border-right-color: #d5dce6; }
|
|
203
176
|
|
|
204
177
|
/* Event tag clickable */
|
|
205
|
-
.ev-tag{cursor:pointer;transition:all .15s}
|
|
206
|
-
.ev-tag:hover{background:
|
|
207
|
-
|
|
208
|
-
/* Roadmap version card */
|
|
209
|
-
.rp-card{border:1.5px solid #d5dce6;border-radius:10px;margin-bottom:10px;overflow:hidden;background:#fff;box-shadow:var(--shadow-sm);transition:box-shadow .2s,border-color .3s}
|
|
210
|
-
.rp-card:hover{box-shadow:var(--shadow-md)}
|
|
211
|
-
.rp-card.rp-done{background:#f0fdf6;border-color:#b7e4cf}
|
|
212
|
-
.rp-card.rp-mixed{background:#fffef7;border-color:#f0d000}
|
|
213
|
-
.rp-head{padding:10px 14px;cursor:pointer;display:flex;justify-content:space-between;align-items:center;gap:8px;user-select:none;transition:background .15s;border-bottom:1px solid transparent}
|
|
214
|
-
.rp-card.open .rp-head{border-bottom-color:var(--border)}
|
|
215
|
-
.rp-head:hover{background:rgba(0,0,0,.02)}
|
|
216
|
-
.rp-head .rp-title{font-weight:600;font-size:12.5px;flex:1;min-width:0}
|
|
217
|
-
.rp-head .rp-meta{display:flex;align-items:center;gap:6px;font-size:10px;color:var(--muted)}
|
|
218
|
-
.rp-head .rp-ratio{font-size:10px;font-weight:600;padding:2px 8px;border-radius:10px}
|
|
219
|
-
.rp-head .rp-ratio.all-done{background:#d4f5e8;color:#00855a}
|
|
220
|
-
.rp-head .rp-ratio.partial{background:#fff3d4;color:#b06d00}
|
|
221
|
-
.rp-head .rp-arrow{font-size:10px;color:var(--muted);transition:transform .2s}
|
|
222
|
-
.rp-card.open .rp-arrow{transform:rotate(90deg)}
|
|
223
|
-
.rp-body{display:none}
|
|
224
|
-
.rp-card.open .rp-body{display:block}
|
|
225
|
-
.rp-card.semi .rp-body{display:block}
|
|
226
|
-
.rp-child{padding:7px 14px;border-bottom:1px dotted #eef0f4;font-size:11px;display:flex;align-items:center;gap:8px;transition:background .1s}
|
|
227
|
-
.rp-child:hover{background:rgba(0,0,0,.015)}
|
|
228
|
-
.rp-child:last-child{border-bottom:none}
|
|
229
|
-
.rp-child .rp-cicon{font-size:11px;flex-shrink:0}
|
|
230
|
-
.rp-child .rp-cname{flex:1;font-weight:500;color:var(--text)}
|
|
231
|
-
.rp-child .rp-ctime{font-size:9.5px;color:var(--muted);font-family:'SF Mono',Consolas,monospace;white-space:nowrap}
|
|
232
|
-
.rp-child .rp-clabel{font-size:9px;padding:1px 6px;border-radius:3px;font-weight:500;white-space:nowrap}
|
|
233
|
-
.rp-child .rp-clabel.done{background:#e6f9f1;color:#00855a}
|
|
234
|
-
.rp-child .rp-clabel.progress{background:#fff8e6;color:#b06d00}
|
|
235
|
-
.rp-child .rp-clabel.pending-label{background:#f1f2f5;color:var(--muted)}
|
|
236
|
-
.rp-more{cursor:pointer;padding:8px 12px;text-align:center;font-size:11px;color:var(--link);font-weight:500;border-top:1px dashed var(--border);transition:background .15s}
|
|
237
|
-
.rp-more:hover{background:#edf0f4;color:#0066cc}
|
|
238
|
-
|
|
239
|
-
/* Enhanced tree card */
|
|
240
|
-
.tree-card{border:1px solid var(--border);border-radius:8px;margin-bottom:8px;overflow:hidden;background:#fff;box-shadow:var(--shadow-sm);transition:box-shadow .2s,transform .15s,border-color .3s;cursor:pointer}
|
|
241
|
-
.tree-card:hover{box-shadow:var(--shadow-md);transform:translateY(-1px)}
|
|
242
|
-
.tree-card.secondary{border-left:3px solid var(--secondary)}
|
|
243
|
-
.tree-card[data-tg="past"]{background:var(--past-bg);border-color:var(--past-border)}
|
|
244
|
-
.tree-card[data-tg="current"]{background:var(--current-bg);border-color:var(--current-border)}
|
|
245
|
-
.tree-card[data-tg="future"]{background:var(--future-bg);border-color:var(--future-border)}
|
|
246
|
-
.tree-card .tc-head{padding:10px 14px;font-size:12px;font-weight:600;display:flex;justify-content:space-between;align-items:center;gap:8px}
|
|
247
|
-
.tree-card .tc-head > span:first-child{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
248
|
-
.tree-card .tc-meta{display:flex;align-items:center;gap:6px;font-size:9.5px;flex-shrink:0;color:var(--muted)}
|
|
249
|
-
.tree-card .tc-time{font-family:'SF Mono',Consolas,monospace;font-size:9.5px;color:var(--muted)}
|
|
250
|
-
.tc-head .tc-time{font-size:10px}
|
|
251
|
-
.tree-card .tc-summary{padding:4px 12px 8px;font-size:11px;color:var(--light);line-height:1.5}
|
|
252
|
-
.tree-card .tc-tags{padding:2px 12px 6px}
|
|
253
|
-
.tree-card .tc-src{padding:2px 12px 6px}
|
|
254
|
-
.tree-card .tc-src a{font-size:9.5px;background:#e8f1fd;padding:2px 7px;border-radius:4px;color:var(--link);cursor:pointer;text-decoration:none;transition:all .15s}
|
|
255
|
-
.tree-card .tc-src a:hover{background:var(--link);color:#fff}
|
|
256
|
-
.tree-card .tc-type{font-size:9px;padding:1px 6px;border-radius:4px;font-weight:500}
|
|
257
|
-
.tree-card .tc-type.status-done{background:#e6f9f1;color:#00855a}
|
|
258
|
-
.tree-card .tc-type.status-progress{background:#fff8e6;color:#b06d00}
|
|
259
|
-
.tree-card .tc-type.status-pending{background:#f1f2f5;color:var(--muted)}
|
|
260
|
-
|
|
261
|
-
/* Token display in header */
|
|
262
|
-
#token-today{font-size:10px;color:#9aa0ab;cursor:default;position:relative;white-space:nowrap}
|
|
263
|
-
#token-today:hover{color:#b8c0c8}
|
|
264
|
-
#token-tip{display:none;position:absolute;top:100%;right:0;margin-top:8px;background:#1a2332;color:#e8eaed;padding:10px 14px;border-radius:8px;font-size:11px;line-height:1.6;z-index:100;box-shadow:0 4px 20px rgba(0,0,0,.35);min-width:200px;white-space:nowrap}
|
|
265
|
-
#token-tip.show{display:block}
|
|
266
|
-
#token-tip .tip-row{display:flex;justify-content:space-between;gap:16px;padding:2px 0}
|
|
267
|
-
#token-tip .tip-row .tip-name{color:#b0bec5}
|
|
268
|
-
#token-tip .tip-row .tip-val{color:#fff;font-weight:600;font-family:'SF Mono',Consolas,monospace}
|
|
269
|
-
#token-tip .tip-divider{border-top:1px solid rgba(255,255,255,.1);margin:4px 0}
|
|
178
|
+
.ev-tag { cursor: pointer; transition: all .15s; }
|
|
179
|
+
.ev-tag:hover { background: #10B981; color: #fff; }
|
|
270
180
|
|
|
181
|
+
/* Thread card */
|
|
182
|
+
.th-card { border: 1.5px solid #c2c6d6; border-radius: 10px; margin-bottom: 10px;
|
|
183
|
+
overflow: hidden; background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,.06);
|
|
184
|
+
transition: box-shadow .2s; }
|
|
185
|
+
.th-card:hover { box-shadow: 0 4px 12px rgba(0,0,0,.08); }
|
|
186
|
+
.th-card.th-active { border-color: #4a90d9; box-shadow: 0 0 0 2px rgba(74,144,217,.3); }
|
|
187
|
+
.th-head { padding: 10px 14px; background: #f2f4f6; cursor: pointer;
|
|
188
|
+
display: flex; justify-content: space-between; align-items: center; gap: 8px;
|
|
189
|
+
user-select: none; transition: background .15s; }
|
|
190
|
+
.th-head:hover { background: #eceef0; }
|
|
191
|
+
.th-head .th-title { font-weight: 600; font-size: 12px; flex: 1; min-width: 0;
|
|
192
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
193
|
+
.th-head .th-count { font-size: 10px; color: #727785; white-space: nowrap;
|
|
194
|
+
background: rgba(0,0,0,.06); padding: 2px 8px; border-radius: 10px; }
|
|
195
|
+
.th-body { display: none; border-top: 1px solid #c2c6d6; }
|
|
196
|
+
.th-card.open .th-body { display: block; }
|
|
197
|
+
.th-card.semi .th-body { display: block; }
|
|
198
|
+
.th-body .ev-item { margin: 0; border-radius: 0; border-left-width: 3px; box-shadow: none; }
|
|
199
|
+
.th-body .ev-item:last-child { border-radius: 0 0 8px 0; }
|
|
200
|
+
|
|
201
|
+
/* Event items (non-threaded) */
|
|
202
|
+
.ev-item { padding: 10px 12px; border-left: 4px solid #c2c6d6; margin-bottom: 6px;
|
|
203
|
+
border-radius: 0 6px 6px 0; background: #fff; font-size: 11.5px;
|
|
204
|
+
box-shadow: 0 1px 3px rgba(0,0,0,.06); transition: box-shadow .2s, transform .15s; }
|
|
205
|
+
.ev-item:hover { box-shadow: 0 4px 12px rgba(0,0,0,.08); }
|
|
206
|
+
.ev-item.t1 { border-left-color: #e17055; } .ev-item.t2 { border-left-color: #6c5ce7; }
|
|
207
|
+
.ev-item.t3 { border-left-color: #0984e3; } .ev-item.t4 { border-left-color: #00b894; }
|
|
208
|
+
.ev-item.t5 { border-left-color: #f0a000; } .ev-item.t6 { border-left-color: #795548; }
|
|
209
|
+
.ev-item.t7 { border-left-color: #e84393; }
|
|
210
|
+
.ev-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 8px; }
|
|
211
|
+
.ev-title { font-weight: 600; flex: 1; line-height: 1.4; font-size: 12px; }
|
|
212
|
+
.ev-type { font-size: 9.5px; padding: 2px 7px; border-radius: 4px; color: #fff;
|
|
213
|
+
font-weight: 600; white-space: nowrap; letter-spacing: .3px; }
|
|
214
|
+
.ev-t1 { background: #e17055; } .ev-t2 { background: #6c5ce7; }
|
|
215
|
+
.ev-t3 { background: #0984e3; } .ev-t4 { background: #00b894; }
|
|
216
|
+
.ev-t5 { background: #f0a000; } .ev-t6 { background: #795548; } .ev-t7 { background: #e84393; }
|
|
217
|
+
.ev-meta { font-size: 10px; color: #727785; margin-top: 4px; display: flex;
|
|
218
|
+
gap: 8px; flex-wrap: wrap; align-items: center; }
|
|
219
|
+
.ev-time { font-family: 'JetBrains Mono', 'SF Mono', Consolas, monospace; font-size: 11px;
|
|
220
|
+
color: #191c1e; font-weight: 600; background: rgba(0,0,0,.04);
|
|
221
|
+
padding: 1px 6px; border-radius: 3px; }
|
|
222
|
+
.ev-ref { color: #0984e3; cursor: pointer; text-decoration: none; font-weight: 500; }
|
|
223
|
+
.ev-ref:hover { text-decoration: underline; color: #0066cc; }
|
|
224
|
+
.ev-actor { font-style: italic; color: #5a6072; }
|
|
225
|
+
.ev-tag { background: rgba(0,0,0,.05); padding: 1px 5px; border-radius: 3px;
|
|
226
|
+
font-size: 9px; color: #5a6072; }
|
|
227
|
+
.ev-day { font-size: 12px; font-weight: 700; padding: 12px 0 6px;
|
|
228
|
+
border-bottom: 1.5px solid #c2c6d6; margin-bottom: 6px;
|
|
229
|
+
color: #5a6072; letter-spacing: .3px; }
|
|
230
|
+
.ev-day:first-child { padding-top: 0; }
|
|
231
|
+
|
|
232
|
+
/* Thread more/collapse buttons */
|
|
233
|
+
.th-more { cursor: pointer; padding: 8px 12px; text-align: center; font-size: 11px;
|
|
234
|
+
color: #0984e3; font-weight: 500; background: #f8f9fb;
|
|
235
|
+
border-top: 1px dashed #c2c6d6; transition: background .15s; }
|
|
236
|
+
.th-more:hover { background: #edf0f4; color: #0066cc; }
|
|
237
|
+
.th-collapse { cursor: pointer; padding: 8px 12px; text-align: center; font-size: 11px;
|
|
238
|
+
color: #727785; font-weight: 500; background: #f8f9fb;
|
|
239
|
+
border-top: 1px solid #c2c6d6; transition: all .15s; }
|
|
240
|
+
.th-collapse:hover { background: #edf0f4; color: #5a6072; }
|
|
241
|
+
.th-typebar { font-size: 10px; color: #727785; padding: 4px 12px;
|
|
242
|
+
background: #f8f9fb; border-bottom: 1px solid #c2c6d6; }
|
|
243
|
+
|
|
244
|
+
/* Roadmap version cards (old format fallback) */
|
|
245
|
+
.rp-card { border: 1.5px solid #c2c6d6; border-radius: 10px; margin-bottom: 10px;
|
|
246
|
+
overflow: hidden; background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,.06);
|
|
247
|
+
transition: box-shadow .2s, border-color .3s; }
|
|
248
|
+
.rp-card:hover { box-shadow: 0 4px 12px rgba(0,0,0,.08); }
|
|
249
|
+
.rp-card.rp-done { background: #f0fdf6; border-color: #b7e4cf; }
|
|
250
|
+
.rp-card.rp-mixed { background: #fffef7; border-color: #f0d000; }
|
|
251
|
+
.rp-head { padding: 10px 14px; cursor: pointer; display: flex;
|
|
252
|
+
justify-content: space-between; align-items: center; gap: 8px;
|
|
253
|
+
user-select: none; transition: background .15s; border-bottom: 1px solid transparent; }
|
|
254
|
+
.rp-card.open .rp-head { border-bottom-color: #c2c6d6; }
|
|
255
|
+
.rp-head:hover { background: rgba(0,0,0,.02); }
|
|
256
|
+
.rp-head .rp-title { font-weight: 600; font-size: 12.5px; flex: 1; min-width: 0; }
|
|
257
|
+
.rp-body { display: none; }
|
|
258
|
+
.rp-card.open .rp-body { display: block; }
|
|
259
|
+
.rp-card.semi .rp-body { display: block; }
|
|
260
|
+
.rp-child { padding: 7px 14px; border-bottom: 1px dotted #eef0f4; font-size: 11px;
|
|
261
|
+
display: flex; align-items: center; gap: 8px; transition: background .1s; }
|
|
262
|
+
.rp-child:hover { background: rgba(0,0,0,.015); }
|
|
263
|
+
.rp-child:last-child { border-bottom: none; }
|
|
264
|
+
.rp-more { cursor: pointer; padding: 8px 12px; text-align: center; font-size: 11px;
|
|
265
|
+
color: #0984e3; font-weight: 500; border-top: 1px dashed #c2c6d6; transition: background .15s; }
|
|
266
|
+
.rp-more:hover { background: #edf0f4; color: #0066cc; }
|
|
267
|
+
|
|
268
|
+
/* Tree cards */
|
|
269
|
+
.tree-card { border: 1px solid #c2c6d6; border-radius: 8px; margin-bottom: 8px;
|
|
270
|
+
overflow: hidden; background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,.06);
|
|
271
|
+
transition: box-shadow .2s, transform .15s, border-color .3s; cursor: pointer; }
|
|
272
|
+
.tree-card:hover { box-shadow: 0 4px 12px rgba(0,0,0,.08); transform: translateY(-1px); }
|
|
273
|
+
.tree-card.filter-match { border-color: #4a90d9 !important;
|
|
274
|
+
box-shadow: 0 0 0 3px rgba(74,144,217,.2) !important; background: #fff !important; }
|
|
275
|
+
.tc-head { padding: 10px 14px; font-size: 12px; font-weight: 600;
|
|
276
|
+
display: flex; justify-content: space-between; align-items: center; gap: 8px; }
|
|
277
|
+
.tc-head > span:first-child { flex: 1; min-width: 0; overflow: hidden;
|
|
278
|
+
text-overflow: ellipsis; white-space: nowrap; }
|
|
279
|
+
.tc-meta { display: flex; align-items: center; gap: 6px; font-size: 9.5px;
|
|
280
|
+
flex-shrink: 0; color: #727785; }
|
|
281
|
+
.tc-time { font-family: 'JetBrains Mono', 'SF Mono', Consolas, monospace; font-size: 9.5px; color: #727785; }
|
|
282
|
+
.tc-summary { padding: 4px 12px 8px; font-size: 11px; color: #5a6072; line-height: 1.5; }
|
|
283
|
+
.tc-tags { padding: 2px 12px 6px; }
|
|
284
|
+
.tc-src { padding: 2px 12px 6px; }
|
|
285
|
+
.tc-src a { font-size: 9.5px; background: #e8f1fd; padding: 2px 7px; border-radius: 4px;
|
|
286
|
+
color: #0984e3; cursor: pointer; text-decoration: none; transition: all .15s; }
|
|
287
|
+
.tc-src a:hover { background: #0984e3; color: #fff; }
|
|
288
|
+
.tc-type { font-size: 9px; padding: 1px 6px; border-radius: 4px; font-weight: 500; }
|
|
289
|
+
.tc-type.status-done { background: #e6f9f1; color: #00855a; }
|
|
290
|
+
.tc-type.status-progress { background: #fff8e6; color: #b06d00; }
|
|
291
|
+
.tc-type.status-pending { background: #f1f2f5; color: #727785; }
|
|
292
|
+
.tree-card.secondary { border-left: 3px solid #6c5ce7; }
|
|
293
|
+
|
|
294
|
+
/* Tag bar */
|
|
295
|
+
#tag-bar { padding: 8px 0; margin-bottom: 10px; border-bottom: 1px solid #c2c6d6; }
|
|
296
|
+
.tag-row { display: flex; flex-wrap: wrap; align-items: center; gap: 5px; padding: 2px 0; }
|
|
297
|
+
.tag-row .tag-cat { font-size: 9px; color: #727785; font-weight: 600; white-space: nowrap;
|
|
298
|
+
padding: 2px 6px 2px 0; min-width: 32px; text-align: right; }
|
|
299
|
+
.tag-row .tag-btn { font-size: 10px; padding: 2px 8px; border-radius: 10px;
|
|
300
|
+
border: 1px solid #e0e3e8; background: #fff; cursor: pointer; color: #5a6072;
|
|
301
|
+
transition: all .15s; white-space: nowrap; font-weight: 500; }
|
|
302
|
+
.tag-row .tag-btn em { font-style: normal; font-size: 8px; color: #727785; margin-left: 2px; }
|
|
303
|
+
.tag-row .tag-btn:hover { border-color: #10B981; color: #10B981; background: #f0fdf9; }
|
|
304
|
+
.tag-row .tag-btn.on { background: #10B981; color: #fff; border-color: #10B981; font-weight: 600; }
|
|
305
|
+
.tag-row .tag-btn.on em { color: rgba(255,255,255,.7); }
|
|
306
|
+
.tag-row .tag-more { font-size: 9px; color: #0984e3; padding: 2px 4px; cursor: pointer; text-decoration: underline; }
|
|
307
|
+
.tag-row .tag-more:hover { color: #0066cc; }
|
|
308
|
+
|
|
309
|
+
/* Category header */
|
|
310
|
+
.cat-header { font-size: 11px; font-weight: 600; padding: 5px 10px; border-radius: 5px;
|
|
311
|
+
margin-bottom: 8px; display: flex; justify-content: space-between; letter-spacing: .2px; }
|
|
312
|
+
.cat-header.primary { background: linear-gradient(135deg,#e6f9f1,#d4f5e8); color: #00855a; }
|
|
313
|
+
.cat-header.secondary { background: #f1f2f5; color: #727785; }
|
|
314
|
+
.cat-section { margin-bottom: 16px; }
|
|
315
|
+
|
|
316
|
+
/* Tag chip */
|
|
317
|
+
.tag { display: inline-block; background: rgba(0,0,0,.05); padding: 2px 7px; border-radius: 4px;
|
|
318
|
+
font-size: 9.5px; color: #5a6072; margin-right: 4px; margin-bottom: 2px; font-weight: 500; }
|
|
319
|
+
|
|
320
|
+
/* Empty state */
|
|
321
|
+
.empty { text-align: center; padding: 50px 20px; color: #727785; }
|
|
322
|
+
|
|
323
|
+
/* Filter bar (inside toolbar area) */
|
|
324
|
+
#toolbar .fclear { display: none; cursor: pointer; background: #0984e3; color: #fff;
|
|
325
|
+
border: none; padding: 3px 10px; border-radius: 4px; font-size: 10px; font-weight: 600; }
|
|
326
|
+
#toolbar .fclear.show { display: inline-block; }
|
|
327
|
+
#toolbar .fclear:hover { opacity: .85; }
|
|
328
|
+
#toolbar .sort-btn.on { background: #10B981; color: #fff; border-color: #10B981; }
|
|
329
|
+
#toolbar .ftext { font-size: 10px; color: #0984e3; font-weight: 500;
|
|
330
|
+
max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
331
|
+
|
|
332
|
+
.rp-head .rp-meta { display: flex; align-items: center; gap: 6px; font-size: 10px; color: #727785; }
|
|
333
|
+
.rp-head .rp-ratio { font-size: 10px; font-weight: 600; padding: 2px 8px; border-radius: 10px; }
|
|
334
|
+
.rp-head .rp-ratio.all-done { background: #d4f5e8; color: #00855a; }
|
|
335
|
+
.rp-head .rp-ratio.partial { background: #fff3d4; color: #b06d00; }
|
|
336
|
+
.rp-head .rp-arrow { font-size: 10px; color: #727785; transition: transform .2s; }
|
|
337
|
+
.rp-card.open .rp-arrow { transform: rotate(90deg); }
|
|
338
|
+
.rp-child .rp-cicon { font-size: 11px; flex-shrink: 0; }
|
|
339
|
+
.rp-child .rp-cname { flex: 1; font-weight: 500; color: #191c1e; }
|
|
340
|
+
.rp-child .rp-ctime { font-size: 9.5px; color: #727785;
|
|
341
|
+
font-family: 'JetBrains Mono', 'SF Mono', Consolas, monospace; white-space: nowrap; }
|
|
342
|
+
.rp-child .rp-clabel { font-size: 9px; padding: 1px 6px; border-radius: 3px; font-weight: 500; white-space: nowrap; }
|
|
343
|
+
.rp-child .rp-clabel.done { background: #e6f9f1; color: #00855a; }
|
|
344
|
+
.rp-child .rp-clabel.progress { background: #fff8e6; color: #b06d00; }
|
|
345
|
+
.rp-child .rp-clabel.pending-label { background: #f1f2f5; color: #727785; }
|
|
346
|
+
|
|
347
|
+
/* Responsive stacking */
|
|
348
|
+
@media (max-width: 768px) {
|
|
349
|
+
#main { flex-direction: column; }
|
|
350
|
+
#left { width: 100%; border-right: none; border-bottom: 1px solid #c2c6d6; flex: none; max-height: 50vh; }
|
|
351
|
+
#right { width: 100%; flex: 1; }
|
|
352
|
+
}
|
|
271
353
|
</style>
|
|
272
354
|
</head>
|
|
273
|
-
<body>
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
<div class="
|
|
278
|
-
</
|
|
279
|
-
|
|
280
|
-
<
|
|
281
|
-
|
|
282
|
-
<
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
<button class="on" data-c="
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
355
|
+
<body class="bg-surface text-on-surface font-body-md h-screen overflow-hidden flex flex-col relative">
|
|
356
|
+
|
|
357
|
+
<!-- TopNavBar -->
|
|
358
|
+
<nav class="bg-surface border-b border-outline-variant flex justify-between items-center w-full px-lg h-16 shrink-0 relative z-10">
|
|
359
|
+
<div class="flex items-center gap-xl h-full">
|
|
360
|
+
<div class="font-headline-md text-headline-md font-extrabold text-primary shrink-0">Sandtable IDE</div>
|
|
361
|
+
<div class="flex overflow-x-auto hide-scrollbar h-full items-end" id="nav-tabs">
|
|
362
|
+
<button class="px-md pb-md pt-sm font-title-md text-title-md text-on-surface-variant whitespace-nowrap hover:text-on-surface border-b-2 border-transparent hover-scale" data-c="all">All</button>
|
|
363
|
+
<button class="px-md pb-md pt-sm font-title-md text-title-md text-on-surface-variant whitespace-nowrap hover:text-on-surface border-b-2 border-transparent hover-scale" data-c="roadmap">Roadmap</button>
|
|
364
|
+
<button class="px-md pb-md pt-sm font-title-md text-title-md text-on-surface-variant whitespace-nowrap hover:text-on-surface border-b-2 border-transparent hover-scale" data-c="decision">Decisions</button>
|
|
365
|
+
<button class="px-md pb-md pt-sm font-title-md text-title-md text-on-surface-variant whitespace-nowrap hover:text-on-surface border-b-2 border-transparent hover-scale" data-c="spec">Specs</button>
|
|
366
|
+
<button class="px-md pb-md pt-sm font-title-md text-title-md text-on-surface-variant whitespace-nowrap hover:text-on-surface border-b-2 border-transparent hover-scale" data-c="convention">Conventions</button>
|
|
367
|
+
<button class="px-md pb-md pt-sm font-title-md text-title-md text-on-surface-variant whitespace-nowrap hover:text-on-surface border-b-2 border-transparent hover-scale" data-c="ops">Ops</button>
|
|
368
|
+
<button class="px-md pb-md pt-sm font-title-md text-title-md text-on-surface-variant whitespace-nowrap hover:text-on-surface border-b-2 border-transparent hover-scale" data-c="archive">Archive</button>
|
|
369
|
+
</div>
|
|
370
|
+
</div>
|
|
371
|
+
<div class="flex items-center gap-md shrink-0">
|
|
372
|
+
<span class="font-label-caps text-label-caps text-on-surface-variant relative" id="token-today">
|
|
373
|
+
Usage: --<span id="token-tip"></span>
|
|
374
|
+
</span>
|
|
375
|
+
<span class="w-2 h-2 rounded-full bg-emerald-500 shadow-[0_0_6px_rgba(16,185,129,.4)] animate-pulse"></span>
|
|
376
|
+
</div>
|
|
377
|
+
</nav>
|
|
378
|
+
|
|
379
|
+
<!-- Briefing Bar -->
|
|
380
|
+
<div class="bg-surface-container-low border-b border-outline-variant px-lg py-sm flex items-center shrink-0 relative z-10">
|
|
381
|
+
<div class="flex items-center gap-sm">
|
|
382
|
+
<span class="material-symbols-outlined text-on-surface-variant text-[18px]">info</span>
|
|
383
|
+
<span class="font-code-md text-code-md text-on-surface-variant" id="btext">Loading...</span>
|
|
384
|
+
</div>
|
|
385
|
+
</div>
|
|
386
|
+
|
|
387
|
+
<!-- Main Split -->
|
|
388
|
+
<main class="flex-1 flex overflow-hidden relative z-10" id="main">
|
|
389
|
+
<!-- Left Column (40%) -->
|
|
390
|
+
<section class="w-[40%] max-md:w-full flex flex-col border-r border-outline-variant bg-surface-container-lowest overflow-y-auto" id="left">
|
|
391
|
+
<!-- Track Switcher (hidden when no tracks) -->
|
|
392
|
+
<div class="px-lg py-md flex gap-sm bg-surface-bright border-b border-outline-variant shrink-0 hidden" id="track-switcher">
|
|
393
|
+
<!-- dynamically filled -->
|
|
394
|
+
</div>
|
|
395
|
+
<!-- Category Tabs (secondary nav inside left panel) -->
|
|
396
|
+
<div class="px-lg py-md flex gap-sm bg-surface-bright border-b border-outline-variant shrink-0 hidden" id="tree-tabs-inline">
|
|
397
|
+
<!-- dynamically filled -->
|
|
398
|
+
</div>
|
|
399
|
+
<!-- Tree Content -->
|
|
400
|
+
<div class="flex-1 overflow-y-auto p-lg" id="tree">
|
|
401
|
+
<div class="empty"><h3 class="font-headline-sm text-headline-sm mb-sm">Loading...</h3><p class="font-body-md text-body-md text-on-surface-variant">Fetching data...</p></div>
|
|
293
402
|
</div>
|
|
294
|
-
<div id="tree"></div>
|
|
295
403
|
</section>
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
404
|
+
|
|
405
|
+
<!-- Right Column (60%) -->
|
|
406
|
+
<section class="w-[60%] max-md:w-full flex flex-col bg-surface-bright overflow-y-auto" id="right">
|
|
407
|
+
<!-- Filter Bar -->
|
|
408
|
+
<div class="h-14 border-b border-outline-variant flex items-center justify-between px-lg bg-surface-container-lowest shrink-0">
|
|
409
|
+
<div class="flex items-center gap-md">
|
|
410
|
+
<span class="material-symbols-outlined text-on-surface-variant">filter_list</span>
|
|
411
|
+
<span class="font-title-md text-title-md text-on-surface">Event Stream</span>
|
|
412
|
+
<span class="font-code-sm text-code-sm text-on-surface-variant" id="evcnt"></span>
|
|
413
|
+
</div>
|
|
414
|
+
<div class="flex gap-sm" id="toolbar">
|
|
415
|
+
<span class="font-code-sm text-code-sm text-on-surface-variant flex items-center gap-xs">
|
|
416
|
+
<span class="material-symbols-outlined text-[16px]">sort</span>
|
|
417
|
+
</span>
|
|
418
|
+
<button class="text-on-surface-variant hover:text-on-surface font-code-sm text-code-sm border border-outline-variant px-sm py-xs rounded hover:bg-surface-container-low transition-colors hover-scale sort-btn on" data-mode="grouped">By Category</button>
|
|
419
|
+
<button class="text-on-surface-variant hover:text-on-surface font-code-sm text-code-sm border border-outline-variant px-sm py-xs rounded hover:bg-surface-container-low transition-colors hover-scale sort-btn" data-mode="track">By Track</button>
|
|
420
|
+
<button class="text-on-surface-variant hover:text-on-surface font-code-sm text-code-sm border border-outline-variant px-sm py-xs rounded hover:bg-surface-container-low transition-colors hover-scale sort-btn" data-mode="flat">By Time</button>
|
|
421
|
+
<span class="ftext"></span>
|
|
422
|
+
<button class="fclear">Clear</button>
|
|
423
|
+
</div>
|
|
424
|
+
</div>
|
|
425
|
+
<!-- Tag Bar -->
|
|
426
|
+
<div class="px-lg" id="tag-bar"></div>
|
|
427
|
+
<!-- Events Content -->
|
|
428
|
+
<div class="flex-1 overflow-y-auto px-lg pb-lg" id="events"></div>
|
|
301
429
|
</section>
|
|
302
|
-
</
|
|
430
|
+
</main>
|
|
303
431
|
|
|
432
|
+
<!-- Floating Preview Panel -->
|
|
304
433
|
<div id="overlay" class="hide"></div>
|
|
305
434
|
<div id="float" class="hide">
|
|
306
|
-
<div id="float-head"><span id="ftitle"
|
|
307
|
-
<div id="float-body"
|
|
435
|
+
<div id="float-head"><span id="ftitle">Preview</span><button id="float-close">×</button></div>
|
|
436
|
+
<div id="float-body">Loading...</div>
|
|
308
437
|
</div>
|
|
309
438
|
|
|
310
439
|
<script>
|
|
311
440
|
// ====== DATA ======
|
|
312
441
|
var T = null; // timeline data
|
|
442
|
+
var R = null; // roadmap data (tracks + phases)
|
|
313
443
|
var activeCat = 'all';
|
|
314
|
-
var
|
|
315
|
-
var
|
|
316
|
-
var
|
|
317
|
-
var
|
|
318
|
-
var
|
|
444
|
+
var activeTrack = null; // current track filter (null = All)
|
|
445
|
+
var activeDocFilter = null;
|
|
446
|
+
var activeTagFilter = null;
|
|
447
|
+
var expandedThreads = {};
|
|
448
|
+
var expandedRpCards = {};
|
|
449
|
+
var expandedMilestones = {}; // track milestone accordion state
|
|
450
|
+
var sortMode = 'grouped'; // 'grouped' | 'track' | 'flat'
|
|
319
451
|
|
|
320
452
|
var TAG_CATS = {
|
|
321
|
-
"模块":["CLI","dashboard","builder","server","skill","npm","扫描器"],
|
|
322
|
-
"主题":["事件流","token","UX","安装方案","安全","编码","配置","IDE适配","双视图","设计原则","libero"],
|
|
323
|
-
"阶段":["v0.3","MVP","里程碑","集成","方向决策"],
|
|
324
|
-
"动作":["架构","设计","实现","调研","文档","流程","教训","命令","IDE","init","uninstall","summarize","scan"]
|
|
453
|
+
"模块": ["CLI","dashboard","builder","server","skill","npm","扫描器"],
|
|
454
|
+
"主题": ["事件流","token","UX","安装方案","安全","编码","配置","IDE适配","双视图","设计原则","libero"],
|
|
455
|
+
"阶段": ["v0.3","MVP","里程碑","集成","方向决策"],
|
|
456
|
+
"动作": ["架构","设计","实现","调研","文档","流程","教训","命令","IDE","init","uninstall","summarize","scan"]
|
|
325
457
|
};
|
|
326
|
-
|
|
327
|
-
function evHasTag(ev,tag){return ev.tags&&ev.tags.indexOf(tag)!==-1}
|
|
328
|
-
function setTagFilter(tag){if(activeTagFilter===tag){activeTagFilter=null}else{activeTagFilter=tag};renderTagBar();renderEvents()}
|
|
329
|
-
function renderTagBar(){
|
|
330
|
-
var bar=document.getElementById("tag-bar");
|
|
331
|
-
if(!T||!T.events){bar.innerHTML="";return}
|
|
332
|
-
var allEvs=T.events||[],tagCount={};
|
|
333
|
-
for(var i=0;i<allEvs.length;i++){var tags=allEvs[i].tags||[];for(var j=0;j<tags.length;j++)tagCount[tags[j]]=(tagCount[tags[j]]||0)+1}
|
|
334
|
-
var catTags={};
|
|
335
|
-
for(var tag in tagCount){var c=catOfTag(tag);if(!catTags[c])catTags[c]=[];catTags[c].push({tag:tag,count:tagCount[tag]})}
|
|
336
|
-
var catOrder=["模块","主题","阶段","动作","其他"];
|
|
337
|
-
var h="";
|
|
338
|
-
for(var ci=0;ci<catOrder.length;ci++){
|
|
339
|
-
var cn=catOrder[ci],items=catTags[cn];
|
|
340
|
-
if(!items||!items.length)continue;
|
|
341
|
-
items.sort(function(a,b){return b.count-a.count});
|
|
342
|
-
var expand=!!expandedTagCats[cn];
|
|
343
|
-
var limit=expand?items.length:Math.min(3,items.length);
|
|
344
|
-
h+="<div class=\"tag-row\"><span class=\"tag-cat\">"+cn+"</span>";
|
|
345
|
-
for(var ii=0;ii<limit;ii++){
|
|
346
|
-
var item=items[ii],isOn=activeTagFilter===item.tag;
|
|
347
|
-
h+="<span class=\"tag-btn"+(isOn?" on":"")+"\" data-tag=\""+esc(item.tag)+"\" onclick=\"setTagFilter('"+esc(item.tag)+"');event.stopPropagation()\">"+esc(item.tag)+" <em>"+item.count+"</em></span>";
|
|
348
|
-
}
|
|
349
|
-
if(items.length>3&&!expand)h+="<span class=\"tag-more\" onclick=\"toggleTagCat('"+esc(cn)+"');event.stopPropagation()\">+"+(items.length-3)+"</span>";
|
|
350
|
-
if(expand)h+="<span class=\"tag-more\" onclick=\"toggleTagCat('"+esc(cn)+"');event.stopPropagation()\">收起</span>";
|
|
351
|
-
h+="</div>";
|
|
352
|
-
}
|
|
353
|
-
bar.innerHTML=h;
|
|
354
|
-
}
|
|
458
|
+
var ETYPES = {'对齐与拍板':['ev-t1','t1'],'规格演进':['ev-t2','t2'],'代码变更':['ev-t3','t3'],'测试与质量':['ev-t4','t4'],'审批与交接':['ev-t5','t5'],'运维与基建':['ev-t6','t6'],'教训沉淀':['ev-t7','t7']};
|
|
355
459
|
|
|
356
|
-
var
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
460
|
+
var CLABEL = {roadmap:'路线图与进度',todo:'待办清单',decision:'决策记录',spec:'业务规格',convention:'协作纪律',ops:'运维与基建',archive:'历史档案',template:'工具模板',unknown:'未分类'};
|
|
461
|
+
var PRIMARY = {roadmap:1,decision:1,todo:1};
|
|
462
|
+
var ETYPE = {phase:'roadmap',milestone:'roadmap',task:'roadmap',subtask:'roadmap',roadmap:'roadmap',backlog:'todo',todo:'todo',conclusion:'roadmap',decision:'decision',refactor:'decision',spec:'spec',intent:'spec',prompt:'spec',convention:'convention',agent:'ops',runbook:'ops',optimization:'roadmap',journal:'archive',handover:'archive',plan_doc:'archive',template:'template'};
|
|
463
|
+
|
|
464
|
+
function normCat(c) { if(!c) return 'unknown'; var m = {conventions:'convention',specs:'spec',decisions:'decision',plans:'roadmap',journals:'archive',journal:'archive'}; return m[c]||c; }
|
|
465
|
+
function normTg(tg) { if(!tg||tg==='archived') return 'past'; return tg; }
|
|
466
|
+
function esc(s) { if(!s) return ''; var d = document.createElement('div'); d.textContent = String(s); return d.innerHTML; }
|
|
467
|
+
function catOf(el) { return normCat(el.category || ETYPE[el.elementType] || "unknown"); }
|
|
468
|
+
function catOfTag(tag) { for(var c in TAG_CATS) { if(TAG_CATS[c].indexOf(tag)!==-1) return c; } return "其他"; }
|
|
469
|
+
function evHasTag(ev, tag) { return ev.tags && ev.tags.indexOf(tag)!==-1; }
|
|
470
|
+
function extractVersion(name) {
|
|
471
|
+
var m = name.match(/\(v\d+\.\d+\)/);
|
|
472
|
+
if(m) return m[0].replace(/[()]/g,"");
|
|
473
|
+
m = name.match(/\(([A-D])\)/);
|
|
474
|
+
if(m) return "v0.3";
|
|
475
|
+
return "其他";
|
|
360
476
|
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
var
|
|
373
|
-
var
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
var verStats={};
|
|
381
|
-
for(var i=0;i<allMilestones.length;i++){
|
|
382
|
-
var ch=allMilestones[i],ver=extractVersion(ch.name);
|
|
383
|
-
if(!verStats[ver])verStats[ver]={total:0,done:0,inProgress:[]};
|
|
384
|
-
verStats[ver].total++;
|
|
385
|
-
if(ch.status==="completed"||ch.status==="cancelled")verStats[ver].done++;
|
|
386
|
-
else if(ch.status==="in_progress")verStats[ver].inProgress.push(ch.name.replace(/\s*\([^)]*\)\s*/g,'').replace(/^M\d+:\s*/,''));
|
|
387
|
-
else verStats[ver].inProgress.push(ch.name.replace(/\s*\([^)]*\)\s*/g,'').replace(/^M\d+:\s*/,''));
|
|
388
|
-
}
|
|
389
|
-
var versions=Object.keys(verStats).sort(function(a,b){return versionOrder(b)-versionOrder(a)});
|
|
390
|
-
// Describe each version's milestone completion
|
|
391
|
-
var parts=[];
|
|
392
|
-
for(var i=0;i<versions.length;i++){
|
|
393
|
-
var v=versions[i],vs=verStats[v];
|
|
394
|
-
if(vs.done===vs.total){
|
|
395
|
-
parts.push(v+' '+vs.done+'/'+vs.total+' 里程碑全部完成');
|
|
396
|
-
}else{
|
|
397
|
-
var names=vs.inProgress.slice(0,3).join('、');
|
|
398
|
-
parts.push(v+' '+vs.done+'/'+vs.total+' 完成'+(names?','+names+'等推进中':''));
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
return parts.join(';')+'。'||'暂无简报';
|
|
477
|
+
function versionOrder(v) {
|
|
478
|
+
if(v==="v0.1") return 1;
|
|
479
|
+
if(v==="v0.2") return 2;
|
|
480
|
+
if(v==="v0.3") return 3;
|
|
481
|
+
return 99;
|
|
482
|
+
}
|
|
483
|
+
function fmtTime(ts) {
|
|
484
|
+
if(!ts) return "";
|
|
485
|
+
var d = new Date(ts);
|
|
486
|
+
var mm = String(d.getMonth()+1).padStart(2,'0');
|
|
487
|
+
var dd = String(d.getDate()).padStart(2,'0');
|
|
488
|
+
var hh = String(d.getHours()).padStart(2,'0');
|
|
489
|
+
var mi = String(d.getMinutes()).padStart(2,'0');
|
|
490
|
+
return mm+'-'+dd+' '+hh+':'+mi;
|
|
491
|
+
}
|
|
492
|
+
function hasChildCat(el, cat) {
|
|
493
|
+
if(catOf(el)===cat) return true;
|
|
494
|
+
if(el.children) for(var i=0;i<el.children.length;i++) { if(hasChildCat(el.children[i],cat)) return true; }
|
|
495
|
+
return false;
|
|
402
496
|
}
|
|
403
497
|
|
|
404
498
|
// ====== LOAD ======
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
499
|
+
Promise.all([
|
|
500
|
+
fetch('../data/timeline.json').then(function(r){ return r.json(); }),
|
|
501
|
+
fetch('../data/roadmap.json').then(function(r){ return r.json(); }).catch(function(){ return null; })
|
|
502
|
+
]).then(function(results) {
|
|
503
|
+
T = results[0];
|
|
504
|
+
R = results[1];
|
|
505
|
+
document.getElementById('btext').textContent = generateBrief();
|
|
506
|
+
renderAll();
|
|
507
|
+
setupNavTabs();
|
|
412
508
|
setupToolbar();
|
|
413
509
|
loadTokenToday();
|
|
414
|
-
}).catch(function(e){
|
|
415
|
-
document.getElementById('btext').textContent = '
|
|
510
|
+
}).catch(function(e) {
|
|
511
|
+
document.getElementById('btext').textContent = 'Load failed: ' + e.message;
|
|
416
512
|
console.error(e);
|
|
417
513
|
});
|
|
418
514
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
for(var j=0;j<btns.length;j++)btns[j].classList.remove("on");
|
|
425
|
-
this.classList.add("on");
|
|
426
|
-
activeCat = this.getAttribute("data-c");
|
|
427
|
-
renderTree();
|
|
428
|
-
});
|
|
429
|
-
}
|
|
515
|
+
function renderAll() {
|
|
516
|
+
renderTrackSwitcher();
|
|
517
|
+
renderInlineTabs();
|
|
518
|
+
renderTree();
|
|
519
|
+
renderEvents();
|
|
430
520
|
}
|
|
431
521
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
522
|
+
function generateBrief() {
|
|
523
|
+
// Use roadmap.json brief if available
|
|
524
|
+
if(R && R.brief) return R.brief;
|
|
525
|
+
// Fallback: generate from timeline elements
|
|
526
|
+
if(!T || !T.elements) return 'Loading...';
|
|
527
|
+
var els = T.elements||[];
|
|
528
|
+
var allMilestones = [];
|
|
529
|
+
for(var i=0;i<els.length;i++) {
|
|
530
|
+
var children = els[i].children||[];
|
|
531
|
+
for(var j=0;j<children.length;j++) {
|
|
532
|
+
if(children[j].elementType==="milestone") allMilestones.push(children[j]);
|
|
533
|
+
}
|
|
442
534
|
}
|
|
443
|
-
var
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
document.body.classList.remove("filter-active");
|
|
451
|
-
renderTree();
|
|
452
|
-
renderTagBar();
|
|
453
|
-
renderEvents();
|
|
454
|
-
});
|
|
535
|
+
var verStats = {};
|
|
536
|
+
for(var i=0;i<allMilestones.length;i++) {
|
|
537
|
+
var ch=allMilestones[i], ver=extractVersion(ch.name);
|
|
538
|
+
if(!verStats[ver]) verStats[ver]={total:0,done:0,inProgress:[]};
|
|
539
|
+
verStats[ver].total++;
|
|
540
|
+
if(ch.status==="completed"||ch.status==="cancelled") verStats[ver].done++;
|
|
541
|
+
else verStats[ver].inProgress.push(ch.name.replace(/\s*\([^)]*\)\s*/g,'').replace(/^M\d+:\s*/,''));
|
|
455
542
|
}
|
|
543
|
+
var versions = Object.keys(verStats).sort(function(a,b){ return versionOrder(b)-versionOrder(a); });
|
|
544
|
+
var parts = [];
|
|
545
|
+
for(var i=0;i<versions.length;i++) {
|
|
546
|
+
var v=versions[i], vs=verStats[v];
|
|
547
|
+
if(vs.done===vs.total) parts.push(v+' '+vs.done+'/'+vs.total+' done');
|
|
548
|
+
else { var names=vs.inProgress.slice(0,3).join(', '); parts.push(v+' '+vs.done+'/'+vs.total+(names?' ('+names+')':'')); }
|
|
549
|
+
}
|
|
550
|
+
var brief = parts.join(' | ') || 'No data';
|
|
551
|
+
if(R && R.currentNode) brief += ' | Current: '+R.currentNode;
|
|
552
|
+
return brief;
|
|
456
553
|
}
|
|
457
554
|
|
|
458
|
-
// ======
|
|
459
|
-
function
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
if(
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
var
|
|
486
|
-
var
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
555
|
+
// ====== TRACK SWITCHER ======
|
|
556
|
+
function renderTrackSwitcher() {
|
|
557
|
+
var container = document.getElementById('track-switcher');
|
|
558
|
+
var tracks = (R && R.tracks) ? R.tracks : [];
|
|
559
|
+
if(!tracks.length) { container.classList.add('hidden'); return; }
|
|
560
|
+
container.classList.remove('hidden');
|
|
561
|
+
|
|
562
|
+
var h = '';
|
|
563
|
+
// "All" button
|
|
564
|
+
h += '<button class="px-md py-sm rounded font-title-md text-title-md hover-scale ' +
|
|
565
|
+
(activeTrack===null
|
|
566
|
+
? 'bg-primary-container text-on-primary-container'
|
|
567
|
+
: 'border border-outline-variant text-on-surface-variant hover:bg-surface-container-low') +
|
|
568
|
+
'" data-track="">All</button>';
|
|
569
|
+
// Track buttons
|
|
570
|
+
for(var i=0;i<tracks.length;i++) {
|
|
571
|
+
var t = tracks[i];
|
|
572
|
+
var isActive = activeTrack===t.id;
|
|
573
|
+
h += '<button class="px-md py-sm rounded font-title-md text-title-md hover-scale ' +
|
|
574
|
+
(isActive
|
|
575
|
+
? 'bg-primary-container text-on-primary-container'
|
|
576
|
+
: 'border border-outline-variant text-on-surface-variant hover:bg-surface-container-low') +
|
|
577
|
+
'" data-track="'+esc(t.id)+'">'+esc(t.label||t.id)+'</button>';
|
|
578
|
+
}
|
|
579
|
+
container.innerHTML = h;
|
|
580
|
+
|
|
581
|
+
// Bind clicks
|
|
582
|
+
var btns = container.querySelectorAll('button[data-track]');
|
|
583
|
+
for(var i=0;i<btns.length;i++) {
|
|
584
|
+
btns[i].addEventListener('click', function() {
|
|
585
|
+
var tid = this.getAttribute('data-track');
|
|
586
|
+
activeTrack = tid || null;
|
|
587
|
+
renderAll();
|
|
588
|
+
});
|
|
589
|
+
}
|
|
490
590
|
}
|
|
491
591
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
592
|
+
// ====== INLINE CATEGORY TABS ======
|
|
593
|
+
function renderInlineTabs() {
|
|
594
|
+
var container = document.getElementById('tree-tabs-inline');
|
|
595
|
+
var categories = ['all','roadmap','todo','decision','spec','convention','ops','archive'];
|
|
596
|
+
var labels = {all:'All',roadmap:'Roadmap',todo:'Todo',decision:'Decisions',spec:'Specs',convention:'Conventions',ops:'Ops',archive:'Archive'};
|
|
597
|
+
container.classList.remove('hidden');
|
|
598
|
+
var h = '';
|
|
599
|
+
for(var i=0;i<categories.length;i++) {
|
|
600
|
+
var c = categories[i];
|
|
601
|
+
var isOn = activeCat===c;
|
|
602
|
+
h += '<button class="px-md py-sm rounded font-title-md text-title-md hover-scale ' +
|
|
603
|
+
(isOn
|
|
604
|
+
? 'bg-primary-container text-on-primary-container'
|
|
605
|
+
: 'border border-outline-variant text-on-surface-variant hover:bg-surface-container-low') +
|
|
606
|
+
'" data-c="'+c+'">'+labels[c]+'</button>';
|
|
498
607
|
}
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
608
|
+
container.innerHTML = h;
|
|
609
|
+
|
|
610
|
+
// Bind clicks
|
|
611
|
+
var btns = container.querySelectorAll('button[data-c]');
|
|
612
|
+
for(var i=0;i<btns.length;i++) {
|
|
613
|
+
btns[i].addEventListener('click', function() {
|
|
614
|
+
activeCat = this.getAttribute('data-c');
|
|
615
|
+
renderAll();
|
|
616
|
+
});
|
|
503
617
|
}
|
|
504
|
-
return ver;
|
|
505
618
|
}
|
|
506
619
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
620
|
+
// ====== NAV TABS (top bar — mirror and sync) ======
|
|
621
|
+
function setupNavTabs() {
|
|
622
|
+
var btns = document.querySelectorAll('#nav-tabs button[data-c]');
|
|
623
|
+
for(var i=0;i<btns.length;i++) {
|
|
624
|
+
btns[i].addEventListener('click', function() {
|
|
625
|
+
activeCat = this.getAttribute('data-c');
|
|
626
|
+
renderAll();
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
syncNavTabs();
|
|
630
|
+
}
|
|
631
|
+
function syncNavTabs() {
|
|
632
|
+
var navBtns = document.querySelectorAll('#nav-tabs button[data-c]');
|
|
633
|
+
for(var i=0;i<navBtns.length;i++) {
|
|
634
|
+
var btn = navBtns[i];
|
|
635
|
+
if(btn.getAttribute('data-c')===activeCat) {
|
|
636
|
+
btn.className = 'px-md pb-md pt-sm font-title-md text-title-md text-primary border-b-2 border-primary whitespace-nowrap hover-scale';
|
|
637
|
+
} else {
|
|
638
|
+
btn.className = 'px-md pb-md pt-sm font-title-md text-title-md text-on-surface-variant whitespace-nowrap hover:text-on-surface border-b-2 border-transparent hover-scale';
|
|
515
639
|
}
|
|
516
640
|
}
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
641
|
+
// Also refresh inline tabs
|
|
642
|
+
var inlineBtns = document.querySelectorAll('#tree-tabs-inline button[data-c]');
|
|
643
|
+
for(var i=0;i<inlineBtns.length;i++) {
|
|
644
|
+
var ib = inlineBtns[i];
|
|
645
|
+
if(ib.getAttribute('data-c')===activeCat) {
|
|
646
|
+
ib.className = 'px-md py-sm rounded font-title-md text-title-md hover-scale bg-primary-container text-on-primary-container';
|
|
647
|
+
} else {
|
|
648
|
+
ib.className = 'px-md py-sm rounded font-title-md text-title-md hover-scale border border-outline-variant text-on-surface-variant hover:bg-surface-container-low';
|
|
521
649
|
}
|
|
522
650
|
}
|
|
523
|
-
return fmtTime(latest);
|
|
524
651
|
}
|
|
525
652
|
|
|
526
|
-
|
|
653
|
+
// ====== TREE ======
|
|
654
|
+
function renderTree() {
|
|
655
|
+
syncNavTabs();
|
|
527
656
|
var els = T.elements||[];
|
|
528
|
-
var roadmapEls=[], otherEls=[];
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
657
|
+
var roadmapEls = [], otherEls = [];
|
|
658
|
+
|
|
659
|
+
// Check if we have tracks data from roadmap.json
|
|
660
|
+
var hasTracks = R && R.tracks && R.tracks.length > 0;
|
|
661
|
+
|
|
662
|
+
for(var i=0;i<els.length;i++) {
|
|
663
|
+
var el = els[i];
|
|
664
|
+
if(el.category==="roadmap" || el.elementType==="phase" || el.elementType==="milestone") {
|
|
532
665
|
roadmapEls.push(el);
|
|
533
|
-
}else{
|
|
534
|
-
if(activeCat!=="all" && catOf(el)!==activeCat && !hasChildCat(el,activeCat))continue;
|
|
666
|
+
} else {
|
|
667
|
+
if(activeCat!=="all" && catOf(el)!==activeCat && !hasChildCat(el,activeCat)) continue;
|
|
535
668
|
otherEls.push(el);
|
|
536
669
|
}
|
|
537
670
|
}
|
|
538
671
|
|
|
539
|
-
var total=otherEls.length;
|
|
540
|
-
var h=
|
|
541
|
-
|
|
542
|
-
// ===== Roadmap section
|
|
543
|
-
if((activeCat==="all" || activeCat==="roadmap")
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
672
|
+
var total = otherEls.length;
|
|
673
|
+
var h = '';
|
|
674
|
+
|
|
675
|
+
// ===== Roadmap section =====
|
|
676
|
+
if((activeCat==="all" || activeCat==="roadmap")) {
|
|
677
|
+
if(hasTracks) {
|
|
678
|
+
// NEW: Render from tracks data
|
|
679
|
+
h += renderTracks();
|
|
680
|
+
for(var ti=0;ti<R.tracks.length;ti++) {
|
|
681
|
+
var phases = R.tracks[ti].phases||[];
|
|
682
|
+
total += phases.length;
|
|
683
|
+
}
|
|
684
|
+
} else if(roadmapEls.length>0) {
|
|
685
|
+
// OLD: Render from timeline elements (fallback)
|
|
686
|
+
h += renderRoadmapLegacy(roadmapEls);
|
|
687
|
+
for(var ri=0;ri<roadmapEls.length;ri++) {
|
|
688
|
+
if(roadmapEls[ri].children) total += roadmapEls[ri].children.length;
|
|
689
|
+
else total += 1;
|
|
690
|
+
}
|
|
548
691
|
}
|
|
549
692
|
|
|
550
|
-
//
|
|
551
|
-
var unmatchedCount=countUnmatchedEvents();
|
|
552
|
-
var branchNode={
|
|
553
|
-
id:"virtual-branch-optimizations",
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
date:new Date().toISOString().substring(0,10)
|
|
561
|
-
};
|
|
562
|
-
h+='<div class="cat-section"><div class="cat-header primary"><span>分支优化</span><span>'+unmatchedCount+' 条事件</span></div>';
|
|
563
|
-
h+=renderTreeCard(branchNode);
|
|
564
|
-
h+="</div>";
|
|
565
|
-
total+=1;
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
// ===== 待办清单 section — 路线图之后、决策记录之前 =====
|
|
569
|
-
if(activeCat==="all"){
|
|
570
|
-
var todoNode={
|
|
571
|
-
id:"virtual-todo-list",
|
|
572
|
-
kind:"primary",elementType:"todo",category:"todo",
|
|
573
|
-
name:"待办清单",status:"pending",timeGroup:"future",
|
|
574
|
-
timeLabel:"发现但未解决的优化点 / 待办项",
|
|
575
|
-
source:{file:"__todo__",title:"待办清单"},
|
|
576
|
-
summary:"记录所有已发现但尚未解决的优化点、待办项,避免遗漏",
|
|
577
|
-
tags:["待办"],children:[],order:998,
|
|
578
|
-
date:new Date().toISOString().substring(0,10)
|
|
693
|
+
// Branch optimizations catch-all
|
|
694
|
+
var unmatchedCount = countUnmatchedEvents();
|
|
695
|
+
var branchNode = {
|
|
696
|
+
id:"virtual-branch-optimizations", kind:"primary", elementType:"catch-all", category:"roadmap",
|
|
697
|
+
name:"Branch Optimizations", status:"in_progress", timeGroup:"current",
|
|
698
|
+
timeLabel:"Bug fixes / micro-tweaks / scattered opts",
|
|
699
|
+
source:{file:"__unmatched__",title:"Branch Optimizations"},
|
|
700
|
+
summary:"Catch-all node: collects all events not linked to a specific roadmap milestone",
|
|
701
|
+
tags:["catch-all"], children:[], order:999,
|
|
702
|
+
date: new Date().toISOString().substring(0,10)
|
|
579
703
|
};
|
|
580
|
-
h+='<div class="cat-section"><div class="cat-header primary"><span
|
|
581
|
-
h+=renderTreeCard(
|
|
582
|
-
h+=
|
|
583
|
-
total+=1;
|
|
704
|
+
h += '<div class="cat-section"><div class="cat-header primary"><span>Branch Optimizations</span><span>'+unmatchedCount+' events</span></div>';
|
|
705
|
+
h += renderTreeCard(branchNode);
|
|
706
|
+
h += '</div>';
|
|
707
|
+
total += 1;
|
|
708
|
+
|
|
709
|
+
// Todo section
|
|
710
|
+
if(activeCat==="all") {
|
|
711
|
+
var todoNode = {
|
|
712
|
+
id:"virtual-todo-list", kind:"primary", elementType:"todo", category:"todo",
|
|
713
|
+
name:"Todo List", status:"pending", timeGroup:"future",
|
|
714
|
+
timeLabel:"Discovered but unresolved optimization points",
|
|
715
|
+
source:{file:"__todo__",title:"Todo List"},
|
|
716
|
+
summary:"Records all discovered but unresolved optimization points and todo items",
|
|
717
|
+
tags:["todo"], children:[], order:998,
|
|
718
|
+
date: new Date().toISOString().substring(0,10)
|
|
719
|
+
};
|
|
720
|
+
h += '<div class="cat-section"><div class="cat-header primary"><span>Todo List</span><span>Pending</span></div>';
|
|
721
|
+
h += renderTreeCard(todoNode);
|
|
722
|
+
h += '</div>';
|
|
723
|
+
total += 1;
|
|
724
|
+
}
|
|
584
725
|
}
|
|
585
726
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
727
|
+
// ===== Other categories =====
|
|
728
|
+
if(activeCat==="all" || activeCat!=="roadmap") {
|
|
729
|
+
var groups = {}, order = [];
|
|
730
|
+
for(var i=0;i<otherEls.length;i++) {
|
|
731
|
+
var c = catOf(otherEls[i]);
|
|
732
|
+
if(!groups[c]) { groups[c]=[]; order.push(c); }
|
|
591
733
|
groups[c].push(otherEls[i]);
|
|
592
734
|
}
|
|
593
|
-
for(var k in groups){
|
|
594
|
-
groups[k].sort(function(a,b){
|
|
595
|
-
if(a.date&&b.date)return b.date.localeCompare(a.date);
|
|
596
|
-
return(b.order||0)-(a.order||0)
|
|
597
|
-
});
|
|
735
|
+
for(var k in groups) {
|
|
736
|
+
groups[k].sort(function(a,b) { if(a.date&&b.date) return b.date.localeCompare(a.date); return (b.order||0)-(a.order||0); });
|
|
598
737
|
}
|
|
599
|
-
order.sort(function(a,b){
|
|
600
|
-
var pa=PRIMARY[a]?0:1,pb=PRIMARY[b]?0:1;
|
|
601
|
-
if(pa!==pb)return pa-pb;
|
|
602
|
-
if(a==="unknown")return 1;if(b==="unknown")return -1;
|
|
738
|
+
order.sort(function(a,b) {
|
|
739
|
+
var pa=PRIMARY[a]?0:1, pb=PRIMARY[b]?0:1;
|
|
740
|
+
if(pa!==pb) return pa-pb;
|
|
741
|
+
if(a==="unknown") return 1; if(b==="unknown") return -1;
|
|
603
742
|
return 0;
|
|
604
743
|
});
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
h+=renderTreeCard(items[j]);
|
|
611
|
-
}
|
|
612
|
-
h+="</div>";
|
|
744
|
+
for(var i=0;i<order.length;i++) {
|
|
745
|
+
var g=order[i], items=groups[g], label=CLABEL[g]||g, isP=PRIMARY[g];
|
|
746
|
+
h += '<div class="cat-section"><div class="cat-header '+(isP?"primary":"secondary")+'"><span>'+esc(label)+'</span><span>'+items.length+' items</span></div>';
|
|
747
|
+
for(var j=0;j<items.length;j++) { h += renderTreeCard(items[j]); }
|
|
748
|
+
h += '</div>';
|
|
613
749
|
}
|
|
614
750
|
}
|
|
615
751
|
|
|
616
|
-
if(total===0
|
|
617
|
-
|
|
618
|
-
|
|
752
|
+
if(total===0 && h==='') {
|
|
753
|
+
document.getElementById('tree').innerHTML = '<div class="empty"><h3 class="font-headline-sm text-headline-sm mb-sm">No content</h3><p class="font-body-md text-body-md text-on-surface-variant">Select another category or run build to refresh</p></div>';
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
document.getElementById('tree').innerHTML = h;
|
|
619
757
|
bindDocClicks();
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
758
|
+
|
|
759
|
+
// Bind rp-more buttons for legacy roadmap
|
|
760
|
+
(function() {
|
|
761
|
+
var mores = document.querySelectorAll('.rp-more');
|
|
762
|
+
for(var i=0;i<mores.length;i++) {
|
|
763
|
+
mores[i].onclick = function(e) {
|
|
624
764
|
e.stopPropagation();
|
|
625
|
-
var card=this.parentNode.parentNode;
|
|
626
|
-
var ver=card.getAttribute(
|
|
627
|
-
if(ver)expandedRpCards[ver]=true;
|
|
765
|
+
var card = this.parentNode.parentNode;
|
|
766
|
+
var ver = card.getAttribute('data-ver');
|
|
767
|
+
if(ver) expandedRpCards[ver]=true;
|
|
628
768
|
renderTree();
|
|
629
769
|
};
|
|
630
770
|
}
|
|
631
771
|
})();
|
|
632
772
|
}
|
|
633
773
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
774
|
+
// ====== NEW: Render tracks from roadmap.json ======
|
|
775
|
+
function renderTracks() {
|
|
776
|
+
var tracks = R.tracks;
|
|
777
|
+
var h = '';
|
|
778
|
+
for(var ti=0;ti<tracks.length;ti++) {
|
|
779
|
+
var track = tracks[ti];
|
|
780
|
+
// Filter by activeTrack
|
|
781
|
+
if(activeTrack && track.id !== activeTrack) continue;
|
|
782
|
+
|
|
783
|
+
h += '<div class="mb-lg">';
|
|
784
|
+
h += '<h4 class="font-label-caps text-label-caps text-on-surface-variant mb-md tracking-wider">'+esc(track.label||track.id)+'</h4>';
|
|
785
|
+
|
|
786
|
+
var phases = track.phases||[];
|
|
787
|
+
for(var pi=0;pi<phases.length;pi++) {
|
|
788
|
+
h += renderTrackPhase(track, phases[pi]);
|
|
789
|
+
}
|
|
790
|
+
h += '</div>';
|
|
791
|
+
}
|
|
792
|
+
return h;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function renderTrackPhase(track, phase) {
|
|
796
|
+
var tasks = phase.tasks||[];
|
|
797
|
+
var completed = 0;
|
|
798
|
+
for(var i=0;i<tasks.length;i++) {
|
|
799
|
+
if(tasks[i].status==='completed') completed++;
|
|
800
|
+
}
|
|
801
|
+
var allDone = completed===tasks.length;
|
|
802
|
+
var milId = 'ms-'+esc(phase.id);
|
|
803
|
+
var isOpen = !!expandedMilestones[phase.id];
|
|
804
|
+
|
|
805
|
+
// Status color
|
|
806
|
+
var statusColor = allDone ? 'text-emerald-500' : (completed>0 ? 'text-amber-500' : 'text-slate-400');
|
|
807
|
+
var statusDot = allDone ? 'bg-emerald-500' : (completed>0 ? 'bg-amber-500' : 'bg-slate-400');
|
|
808
|
+
var statusText = allDone ? 'Completed' : (phase.status==='in_progress' ? 'In Progress' : 'Pending');
|
|
809
|
+
var borderLeft = allDone ? 'border-l-emerald-500' : (phase.status==='in_progress' ? 'border-l-amber-500' : 'border-l-slate-300');
|
|
810
|
+
|
|
811
|
+
var h = '<div class="bg-surface-container-lowest border border-outline-variant rounded-lg overflow-hidden border-l-4 '+borderLeft+' relative shadow-sm hover:shadow-md transition-shadow mb-md cursor-pointer" onclick="toggleMilestone(\''+esc(phase.id)+'\')">';
|
|
812
|
+
// Header
|
|
813
|
+
h += '<div class="p-md border-b border-outline-variant bg-surface-bright flex justify-between items-center hover:bg-surface-container-low transition-colors">';
|
|
814
|
+
h += '<div>';
|
|
815
|
+
h += '<h3 class="font-headline-sm text-headline-sm text-on-surface flex items-center gap-sm">';
|
|
816
|
+
h += '<span class="material-symbols-outlined '+statusColor+'">flag</span>';
|
|
817
|
+
h += esc(phase.id)+' · '+esc(phase.name);
|
|
818
|
+
h += '</h3>';
|
|
819
|
+
h += '</div>';
|
|
820
|
+
h += '<div class="flex items-center gap-sm">';
|
|
821
|
+
h += '<span class="w-2 h-2 rounded-full '+statusDot+'"></span>';
|
|
822
|
+
h += '<span class="font-code-sm text-code-sm text-on-surface-variant">'+statusText+', '+completed+'/'+tasks.length+'</span>';
|
|
823
|
+
h += '<span class="material-symbols-outlined text-on-surface-variant transition-transform duration-300" id="icon-'+milId+'">'+(isOpen?'expand_less':'expand_more')+'</span>';
|
|
824
|
+
h += '</div>';
|
|
825
|
+
h += '</div>';
|
|
826
|
+
|
|
827
|
+
// Body (tasks)
|
|
828
|
+
h += '<div class="p-md flex flex-col gap-sm accordion-content'+(isOpen?'':' collapsed')+'" id="'+milId+'">';
|
|
829
|
+
for(var i=0;i<tasks.length;i++) {
|
|
830
|
+
h += renderTrackSubtask(tasks[i], phase);
|
|
831
|
+
}
|
|
832
|
+
h += '</div>';
|
|
833
|
+
h += '</div>';
|
|
834
|
+
return h;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function renderTrackSubtask(task, phase) {
|
|
838
|
+
var isDone = task.status==='completed';
|
|
839
|
+
var isNext = task.status==='in_progress';
|
|
840
|
+
var iconName = isDone ? 'check_circle' : 'radio_button_unchecked';
|
|
841
|
+
var iconColor = isDone ? 'text-emerald-500' : (isNext ? 'text-primary' : 'text-outline-design');
|
|
842
|
+
var textStyle = isDone ? 'line-through text-on-surface-variant' : (isNext ? 'font-semibold text-on-surface' : 'text-on-surface');
|
|
843
|
+
var rowExtra = isNext ? 'bg-primary-fixed border border-primary pulse-border shadow-sm' : 'hover:bg-surface-container-low';
|
|
844
|
+
|
|
845
|
+
var h = '<div class="flex items-center justify-between p-sm rounded group transition-colors hover-scale '+rowExtra+'">';
|
|
846
|
+
h += '<div class="flex items-center gap-md">';
|
|
847
|
+
h += '<span class="material-symbols-outlined '+iconColor+'">'+iconName+'</span>';
|
|
848
|
+
h += '<span class="font-body-md text-body-md '+textStyle+'">'+esc(task.id)+' '+esc(task.title)+'</span>';
|
|
849
|
+
h += '</div>';
|
|
850
|
+
h += '<div class="flex items-center gap-sm">';
|
|
851
|
+
if(isNext) {
|
|
852
|
+
h += '<span class="font-label-caps text-label-caps bg-primary text-on-primary px-xs rounded animate-pulse shadow-inner">NEXT</span>';
|
|
853
|
+
}
|
|
854
|
+
if(task.actor) {
|
|
855
|
+
h += '<span class="font-code-sm text-code-sm bg-surface-variant text-on-surface-variant px-sm py-xs rounded">'+esc(task.actor)+'</span>';
|
|
856
|
+
}
|
|
857
|
+
h += '</div>';
|
|
858
|
+
h += '</div>';
|
|
859
|
+
return h;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
function toggleMilestone(id) {
|
|
863
|
+
if(expandedMilestones[id]) {
|
|
864
|
+
expandedMilestones[id] = false;
|
|
865
|
+
} else {
|
|
866
|
+
expandedMilestones[id] = true;
|
|
867
|
+
}
|
|
868
|
+
renderTree();
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// ====== LEGACY: Render roadmap from timeline elements (fallback) ======
|
|
872
|
+
function renderRoadmapLegacy(roadmapEls) {
|
|
873
|
+
var verGroups={}, verOrder=[];
|
|
874
|
+
for(var i=0;i<roadmapEls.length;i++) {
|
|
637
875
|
var el=roadmapEls[i];
|
|
638
876
|
var children=el.children||[];
|
|
639
|
-
for(var j=0;j<children.length;j++){
|
|
877
|
+
for(var j=0;j<children.length;j++) {
|
|
640
878
|
var ch=children[j];
|
|
641
879
|
var ver=extractVersion(ch.name);
|
|
642
|
-
if(!verGroups[ver]){verGroups[ver]=[];verOrder.push(ver)}
|
|
880
|
+
if(!verGroups[ver]){verGroups[ver]=[];verOrder.push(ver);}
|
|
643
881
|
verGroups[ver].push(ch);
|
|
644
882
|
}
|
|
645
883
|
}
|
|
646
884
|
verOrder.sort(function(a,b){return versionOrder(a)-versionOrder(b)});
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
verGroups[v].sort(function(a,b){
|
|
650
|
-
var da=a.date||"",db=b.date||"";
|
|
651
|
-
return db.localeCompare(da);
|
|
652
|
-
});
|
|
885
|
+
for(var v in verGroups) {
|
|
886
|
+
verGroups[v].sort(function(a,b){var da=a.date||"",db=b.date||"";return db.localeCompare(da);});
|
|
653
887
|
}
|
|
654
|
-
|
|
655
888
|
var vgList=[];
|
|
656
|
-
for(var vi=0;vi<verOrder.length;vi++){
|
|
889
|
+
for(var vi=0;vi<verOrder.length;vi++) {
|
|
657
890
|
var v=verOrder[vi],g=verGroups[v];
|
|
658
891
|
vgList.push({ver:v,children:g});
|
|
659
892
|
}
|
|
660
|
-
// Sort by version descending (v0.3 → v0.2 → v0.1)
|
|
661
893
|
vgList.sort(function(a,b){return versionOrder(b.ver)-versionOrder(a.ver)});
|
|
662
|
-
|
|
663
894
|
var top3={};
|
|
664
|
-
for(var i=0;i<Math.min(3,vgList.length);i++){top3[vgList[i].ver]=true}
|
|
895
|
+
for(var i=0;i<Math.min(3,vgList.length);i++){top3[vgList[i].ver]=true;}
|
|
665
896
|
|
|
666
|
-
// Derive title parts from phase element name
|
|
667
897
|
var phaseName="";
|
|
668
|
-
for(var i=0;i<roadmapEls.length;i++){
|
|
669
|
-
if(roadmapEls[i].elementType==="phase"){phaseName=roadmapEls[i].name;break}
|
|
898
|
+
for(var i=0;i<roadmapEls.length;i++) {
|
|
899
|
+
if(roadmapEls[i].elementType==="phase"){phaseName=roadmapEls[i].name;break;}
|
|
670
900
|
}
|
|
671
901
|
|
|
672
902
|
var h="";
|
|
673
|
-
for(var i=0;i<vgList.length;i++){
|
|
903
|
+
for(var i=0;i<vgList.length;i++) {
|
|
674
904
|
var vg=vgList[i],children=vg.children,ver=vg.ver;
|
|
675
905
|
var done=0,total=children.length;
|
|
676
906
|
for(var j=0;j<children.length;j++){
|
|
677
907
|
if(children[j].status==="completed"||children[j].status==="cancelled")done++;
|
|
678
908
|
}
|
|
679
909
|
var allDone=(done===total);
|
|
680
|
-
var ratio=Math.round(done/total*100);
|
|
681
|
-
|
|
682
910
|
var stateCls=allDone?"rp-done":(done>0?"rp-mixed":"");
|
|
683
911
|
var ratioCls=allDone?"all-done":(done>0?"partial":"");
|
|
684
912
|
var ratioText=done+"/"+total;
|
|
685
|
-
|
|
686
913
|
var isTop3=!!top3[ver];
|
|
687
914
|
var manualOpen=!!expandedRpCards[ver];
|
|
688
915
|
var isSemi=isTop3 && !allDone && !manualOpen;
|
|
689
916
|
var isOpen=manualOpen;
|
|
690
|
-
|
|
691
917
|
var title=getVersionTitle(ver,children,phaseName);
|
|
692
918
|
var upTime=getVersionUpdateTime(ver,children);
|
|
693
919
|
|
|
694
|
-
|
|
695
|
-
h+='<div class="rp-card '+stateCls+(isSemi?" semi":"")+(isOpen?" open":"")+(isFilterMatch?" filter-match":"")+'" data-ver="'+esc(ver)+'">';
|
|
920
|
+
h+='<div class="rp-card '+stateCls+(isSemi?" semi":"")+(isOpen?" open":"")+'" data-ver="'+esc(ver)+'">';
|
|
696
921
|
h+='<div class="rp-head">';
|
|
697
|
-
h+='<span class="rp-arrow" onclick="toggleRpCard(this);event.stopPropagation()"
|
|
698
|
-
h+='<span class="rp-title" onclick="setDocFilter(\''+esc(ver)+'\');event.stopPropagation()" title="
|
|
922
|
+
h+='<span class="rp-arrow" onclick="toggleRpCard(this);event.stopPropagation()">▶</span>';
|
|
923
|
+
h+='<span class="rp-title" onclick="setDocFilter(\''+esc(ver)+'\');event.stopPropagation()" title="Filter events for this version">'+esc(title)+'</span>';
|
|
699
924
|
h+='<span class="rp-meta"><span class="rp-ratio '+ratioCls+'">'+ratioText+'</span><span class="tc-time">'+esc(upTime)+'</span></span>';
|
|
700
925
|
h+='</div><div class="rp-body">';
|
|
701
|
-
|
|
702
|
-
if(isSemi){
|
|
926
|
+
if(isSemi) {
|
|
703
927
|
var incChildren=[],comChildren=[];
|
|
704
|
-
for(var j=0;j<children.length;j++){
|
|
705
|
-
if(children[j].status==="completed"||children[j].status==="cancelled")
|
|
706
|
-
|
|
707
|
-
}else{
|
|
708
|
-
incChildren.push(children[j]);
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
for(var j=0;j<incChildren.length;j++){
|
|
712
|
-
h+=renderRoadmapChild(incChildren[j]);
|
|
713
|
-
}
|
|
714
|
-
if(comChildren.length>0){
|
|
715
|
-
h+='<div class="rp-more">\u25bc 查看全部 '+total+' 里程碑(含 '+comChildren.length+' 已完成)</div>';
|
|
716
|
-
}
|
|
717
|
-
}else{
|
|
718
|
-
for(var j=0;j<children.length;j++){
|
|
719
|
-
h+=renderRoadmapChild(children[j]);
|
|
928
|
+
for(var j=0;j<children.length;j++) {
|
|
929
|
+
if(children[j].status==="completed"||children[j].status==="cancelled") comChildren.push(children[j]);
|
|
930
|
+
else incChildren.push(children[j]);
|
|
720
931
|
}
|
|
932
|
+
for(var j=0;j<incChildren.length;j++) h+=renderRoadmapChild(incChildren[j]);
|
|
933
|
+
if(comChildren.length>0) h+='<div class="rp-more">▼ View all '+total+' milestones ('+comChildren.length+' completed)</div>';
|
|
934
|
+
} else {
|
|
935
|
+
for(var j=0;j<children.length;j++) h+=renderRoadmapChild(children[j]);
|
|
721
936
|
}
|
|
722
|
-
h+=
|
|
937
|
+
h+='</div></div>';
|
|
723
938
|
}
|
|
724
939
|
return h;
|
|
725
940
|
}
|
|
726
941
|
|
|
727
|
-
function toggleRpCard(el){
|
|
942
|
+
function toggleRpCard(el) {
|
|
728
943
|
var card=el.parentNode.parentNode;
|
|
729
944
|
var ver=card.getAttribute("data-ver");
|
|
730
|
-
if(expandedRpCards[ver]){expandedRpCards[ver]=false}
|
|
731
|
-
else{expandedRpCards[ver]=true}
|
|
945
|
+
if(expandedRpCards[ver]){expandedRpCards[ver]=false;}
|
|
946
|
+
else{expandedRpCards[ver]=true;}
|
|
732
947
|
renderTree();
|
|
733
948
|
}
|
|
734
949
|
|
|
735
|
-
function renderRoadmapChild(ch){
|
|
950
|
+
function renderRoadmapChild(ch) {
|
|
736
951
|
var done=ch.status==="completed"||ch.status==="cancelled";
|
|
737
|
-
var icon=done?"
|
|
952
|
+
var icon=done?"✅":(ch.status==="in_progress"?"⏳":"⬜");
|
|
738
953
|
var labelCls=done?"done":(ch.status==="in_progress"?"progress":"pending-label");
|
|
739
|
-
var labelText=done?"
|
|
954
|
+
var labelText=done?"Done":(ch.status==="in_progress"?"In Progress":"Pending");
|
|
740
955
|
var time=fmtTime(ch.date)||ch.timeLabel||"";
|
|
741
956
|
var ver=extractVersion(ch.name);
|
|
742
957
|
var h='<div class="rp-child" data-ver="'+esc(ver)+'" data-doc="'+(ch.source&&ch.source.file?esc(ch.source.file):"")+'">';
|
|
@@ -744,342 +959,613 @@ function renderRoadmapChild(ch){
|
|
|
744
959
|
h+='<span class="rp-cname">'+esc(ch.name||ch.id)+'</span>';
|
|
745
960
|
h+='<span class="rp-ctime">'+esc(time)+'</span>';
|
|
746
961
|
h+='<span class="rp-clabel '+labelCls+'">'+labelText+'</span>';
|
|
747
|
-
h+=
|
|
962
|
+
h+='</div>';
|
|
748
963
|
return h;
|
|
749
964
|
}
|
|
750
965
|
|
|
751
|
-
function
|
|
966
|
+
function getVersionTitle(ver,children,phaseName) {
|
|
967
|
+
if(phaseName) {
|
|
968
|
+
var parts=phaseName.split(/[·;;]/);
|
|
969
|
+
for(var i=0;i<parts.length;i++) {
|
|
970
|
+
if(parts[i].indexOf(ver)!==-1) return parts[i].trim();
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
var first=children[0];
|
|
974
|
+
if(first) {
|
|
975
|
+
var nm=first.name.replace(/\s*\([^)]*\)\s*/g,"").replace(/^M\d+:\s*/,"");
|
|
976
|
+
return ver+" · "+nm;
|
|
977
|
+
}
|
|
978
|
+
return ver;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
function getVersionUpdateTime(ver,children) {
|
|
982
|
+
var latest="";
|
|
983
|
+
if(T&&T.events) {
|
|
984
|
+
for(var i=0;i<T.events.length;i++) {
|
|
985
|
+
var ev=T.events[i];
|
|
986
|
+
if(ev.tags&&ev.tags.indexOf(ver)!==-1) {
|
|
987
|
+
if(!latest||ev.timestamp>latest) latest=ev.timestamp;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
if(!latest) {
|
|
992
|
+
for(var i=0;i<children.length;i++) {
|
|
993
|
+
var d=children[i].date;
|
|
994
|
+
if(d&&(!latest||d>latest)) latest=d;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
return fmtTime(latest);
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// ====== TREE CARDS (for non-roadmap elements) ======
|
|
1001
|
+
function renderTreeCard(el) {
|
|
752
1002
|
var isDoc=!!(el.source&&el.source.file);
|
|
753
1003
|
var tg=normTg(el.timeGroup);
|
|
754
1004
|
var time=fmtTime(el.date)||el.timeLabel||"";
|
|
755
|
-
var unk=catOf(el)==="unknown"?'<span style="background:#fff3e0;color:#e65100;font-size:9px;padding:1px 5px;border-radius:3px"
|
|
1005
|
+
var unk=catOf(el)==="unknown"?'<span style="background:#fff3e0;color:#e65100;font-size:9px;padding:1px 5px;border-radius:3px">unclassified</span>':"";
|
|
756
1006
|
|
|
757
|
-
// "新建"/"更新" badge for doc-type cards without status
|
|
758
1007
|
var docBadge="";
|
|
759
|
-
if(isDoc || !el.status){
|
|
1008
|
+
if(isDoc || !el.status) {
|
|
760
1009
|
var elDate=el.date||"",updDate=T&&T.updated?T.updated:"";
|
|
761
1010
|
var daysOld=99;
|
|
762
|
-
if(elDate&&updDate){daysOld=(new Date(updDate)-new Date(elDate))/(86400000)}
|
|
763
|
-
if(daysOld<=1)docBadge='<span class="tc-type status-done"
|
|
764
|
-
else if(daysOld<=3)docBadge='<span class="tc-type status-progress"
|
|
1011
|
+
if(elDate&&updDate){daysOld=(new Date(updDate)-new Date(elDate))/(86400000);}
|
|
1012
|
+
if(daysOld<=1) docBadge='<span class="tc-type status-done">New</span>';
|
|
1013
|
+
else if(daysOld<=3) docBadge='<span class="tc-type status-progress">Updated</span>';
|
|
765
1014
|
}
|
|
766
1015
|
|
|
767
|
-
var isFilterMatch=activeDocFilter && (isDoc&&el.source.file===activeDocFilter || (el.tags&&el.tags.indexOf(activeDocFilter)!==-1));
|
|
1016
|
+
var isFilterMatch = activeDocFilter && (isDoc&&el.source.file===activeDocFilter || (el.tags&&el.tags.indexOf(activeDocFilter)!==-1));
|
|
768
1017
|
var h='<div class="tree-card'+(isFilterMatch?" filter-match":"")+'" data-tg="'+esc(tg)+'" data-doc="'+(isDoc?esc(el.source.file):"")+'" onclick="onTreeCardClick(event,\''+esc(isDoc?el.source.file:"")+'\')">';
|
|
769
1018
|
|
|
770
1019
|
h+='<div class="tc-head">';
|
|
771
|
-
if(!isDoc && el.status){
|
|
772
|
-
h+=
|
|
1020
|
+
if(!isDoc && el.status) {
|
|
1021
|
+
h+='<span>'+sicon(el.status)+' '+esc(el.name)+'</span>';
|
|
773
1022
|
var stCls=el.status==="completed"?"status-done":(el.status==="in_progress"?"status-progress":"status-pending");
|
|
774
|
-
h+='<span class="tc-type '+stCls+'">'+(el.status==="completed"?"
|
|
775
|
-
}else{
|
|
776
|
-
h+=
|
|
1023
|
+
h+='<span class="tc-type '+stCls+'">'+(el.status==="completed"?"Done":(el.status==="in_progress"?"In Progress":el.status))+'</span>';
|
|
1024
|
+
} else {
|
|
1025
|
+
h+='<span>'+esc(el.name)+'</span>';
|
|
777
1026
|
h+='<span class="tc-meta">'+unk+docBadge+'<span class="tc-time">'+esc(time)+'</span></span>';
|
|
778
1027
|
}
|
|
779
|
-
h+=
|
|
1028
|
+
h+='</div>';
|
|
780
1029
|
|
|
781
1030
|
h+='<div class="tc-meta" style="padding:0 12px 4px">';
|
|
782
|
-
if(el.status)h+='<span class="tc-time">'+esc(time)+'</span>';
|
|
783
|
-
if(el.tags&&el.tags.length){
|
|
784
|
-
for(var i=0;i<el.tags.length;i++)
|
|
785
|
-
h+='<span class="tag">'+esc(el.tags[i])+'</span>';
|
|
786
|
-
}
|
|
1031
|
+
if(el.status) h+='<span class="tc-time">'+esc(time)+'</span>';
|
|
1032
|
+
if(el.tags&&el.tags.length) {
|
|
1033
|
+
for(var i=0;i<el.tags.length;i++) h+='<span class="tag">'+esc(el.tags[i])+'</span>';
|
|
787
1034
|
}
|
|
788
|
-
h+=
|
|
789
|
-
|
|
790
|
-
if(el.summary)h+='<div class="tc-summary">'+esc(el.summary)+'</div>';
|
|
1035
|
+
h+='</div>';
|
|
791
1036
|
|
|
792
|
-
if(
|
|
1037
|
+
if(el.summary) h+='<div class="tc-summary">'+esc(el.summary)+'</div>';
|
|
1038
|
+
if(isDoc) {
|
|
793
1039
|
h+='<div class="tc-src"><a href="/'+esc(el.source.file)+'" onclick="preview(event,\''+esc(el.source.file)+'\',\'left\');event.stopPropagation()" title="'+esc(el.source.file)+'">'+esc(el.source.file.substring(0,45))+'</a></div>';
|
|
794
1040
|
}
|
|
795
|
-
|
|
796
|
-
h+="</div>";
|
|
1041
|
+
h+='</div>';
|
|
797
1042
|
return h;
|
|
798
1043
|
}
|
|
799
1044
|
|
|
800
|
-
function
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
1045
|
+
function sicon(s) {
|
|
1046
|
+
var map = {completed:'✅',in_progress:'⏳',pending:'⬜',blocked:'🚫',cancelled:'❌'};
|
|
1047
|
+
return map[s]||'⬜';
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
// ====== DOC FILTER ======
|
|
1051
|
+
function setDocFilter(doc) {
|
|
1052
|
+
if(activeDocFilter===doc) { activeDocFilter=null; }
|
|
1053
|
+
else { activeDocFilter=doc; }
|
|
1054
|
+
var ft = document.querySelector('#toolbar .ftext');
|
|
1055
|
+
var fc = document.querySelector('#toolbar .fclear');
|
|
1056
|
+
if(activeDocFilter) {
|
|
1057
|
+
var label = activeDocFilter==='__unmatched__'?'Branch Opts (unclassified)':activeDocFilter==='__todo__'?'Todo List':activeDocFilter;
|
|
1058
|
+
ft.textContent = 'Filter: '+label; fc.classList.add('show');
|
|
1059
|
+
document.body.classList.add('filter-active');
|
|
1060
|
+
} else {
|
|
1061
|
+
ft.textContent = ''; fc.classList.remove('show');
|
|
1062
|
+
document.body.classList.remove('filter-active');
|
|
812
1063
|
}
|
|
813
1064
|
renderTree();
|
|
814
1065
|
renderEvents();
|
|
815
1066
|
}
|
|
816
1067
|
|
|
817
|
-
function onTreeCardClick(e,doc){
|
|
818
|
-
if(e.target.closest(
|
|
819
|
-
if(doc){setDocFilter(doc)}
|
|
1068
|
+
function onTreeCardClick(e, doc) {
|
|
1069
|
+
if(e.target.closest('a')) return;
|
|
1070
|
+
if(doc) { setDocFilter(doc); }
|
|
820
1071
|
}
|
|
821
1072
|
|
|
822
|
-
function bindDocClicks(){
|
|
823
|
-
var rpChildren=document.querySelectorAll(
|
|
824
|
-
for(var i=0;i<rpChildren.length;i++){
|
|
825
|
-
rpChildren[i].addEventListener(
|
|
1073
|
+
function bindDocClicks() {
|
|
1074
|
+
var rpChildren = document.querySelectorAll('.rp-child');
|
|
1075
|
+
for(var i=0;i<rpChildren.length;i++) {
|
|
1076
|
+
rpChildren[i].addEventListener('click', function(e) {
|
|
826
1077
|
e.stopPropagation();
|
|
827
|
-
var ver=this.getAttribute(
|
|
828
|
-
if(ver){setDocFilter(ver);return}
|
|
829
|
-
var doc=this.getAttribute(
|
|
830
|
-
if(doc)setDocFilter(doc);
|
|
1078
|
+
var ver = this.getAttribute('data-ver');
|
|
1079
|
+
if(ver) { setDocFilter(ver); return; }
|
|
1080
|
+
var doc = this.getAttribute('data-doc');
|
|
1081
|
+
if(doc) setDocFilter(doc);
|
|
831
1082
|
});
|
|
832
1083
|
}
|
|
833
1084
|
}
|
|
834
1085
|
|
|
835
|
-
// ======
|
|
836
|
-
|
|
837
|
-
var
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
if(ev.title&&ev.title.indexOf(doc)!==-1)return true;
|
|
846
|
-
return false;
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
function evHasAnyElementMatch(ev){
|
|
850
|
-
if(!T||!T.elements)return false;
|
|
851
|
-
for(var i=0;i<T.elements.length;i++){
|
|
852
|
-
var el=T.elements[i];
|
|
853
|
-
if(el.source&&el.source.file){
|
|
854
|
-
if(ev.ref&&ev.ref.doc===el.source.file)return true;
|
|
855
|
-
}
|
|
856
|
-
var elVer=extractVersion(el.name||'');
|
|
857
|
-
if(elVer&&ev.tags&&ev.tags.indexOf(elVer)!==-1)return true;
|
|
858
|
-
if(elVer&&ev.title&&ev.title.indexOf(elVer)!==-1)return true;
|
|
859
|
-
if(el.children){
|
|
860
|
-
for(var j=0;j<el.children.length;j++){
|
|
861
|
-
var ch=el.children[j];
|
|
862
|
-
if(ch.source&&ch.source.file){
|
|
863
|
-
if(ev.ref&&ev.ref.doc===ch.source.file)return true;
|
|
864
|
-
}
|
|
865
|
-
var chVer=extractVersion(ch.name||'');
|
|
866
|
-
if(chVer&&ev.tags&&ev.tags.indexOf(chVer)!==-1)return true;
|
|
867
|
-
if(chVer&&ev.title&&ev.title.indexOf(chVer)!==-1)return true;
|
|
868
|
-
}
|
|
869
|
-
}
|
|
1086
|
+
// ====== TOOLBAR ======
|
|
1087
|
+
function setupToolbar() {
|
|
1088
|
+
var sortBtns = document.querySelectorAll('#toolbar .sort-btn');
|
|
1089
|
+
for(var i=0;i<sortBtns.length;i++) {
|
|
1090
|
+
sortBtns[i].addEventListener('click', function() {
|
|
1091
|
+
for(var j=0;j<sortBtns.length;j++) sortBtns[j].classList.remove('on');
|
|
1092
|
+
this.classList.add('on');
|
|
1093
|
+
sortMode = this.getAttribute('data-mode');
|
|
1094
|
+
renderEvents();
|
|
1095
|
+
});
|
|
870
1096
|
}
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
1097
|
+
var fclear = document.querySelector('#toolbar .fclear');
|
|
1098
|
+
if(fclear) {
|
|
1099
|
+
fclear.addEventListener('click', function() {
|
|
1100
|
+
activeDocFilter = null;
|
|
1101
|
+
activeTagFilter = null;
|
|
1102
|
+
this.classList.remove('show');
|
|
1103
|
+
document.querySelector('#toolbar .ftext').textContent = '';
|
|
1104
|
+
document.body.classList.remove('filter-active');
|
|
1105
|
+
renderTree();
|
|
1106
|
+
renderTagBar();
|
|
1107
|
+
renderEvents();
|
|
1108
|
+
});
|
|
879
1109
|
}
|
|
880
|
-
return c;
|
|
881
1110
|
}
|
|
882
1111
|
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
var
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
if(ev.ref.
|
|
1112
|
+
// ====== EVENTS ======
|
|
1113
|
+
function renderSingleEv(ev) {
|
|
1114
|
+
var tinfo = ETYPES[ev.type]||['ev-t3','t3'];
|
|
1115
|
+
var refs = '';
|
|
1116
|
+
if(ev.ref) {
|
|
1117
|
+
if(ev.ref.commit) refs += '<code style="font-size:10px;background:#f5f5f5;padding:0 3px;border-radius:2px">'+esc(ev.ref.commit)+'</code> ';
|
|
1118
|
+
if(ev.ref.doc) refs += '<a class="ev-ref" data-d="'+esc(ev.ref.doc)+'" href="/'+esc(ev.ref.doc)+'" onclick="preview(event,\''+esc(ev.ref.doc)+'\',\'right\')">'+esc(ev.ref.doc.substring(0,35))+'</a> ';
|
|
889
1119
|
}
|
|
890
|
-
var tags='';
|
|
891
|
-
if(ev.tags&&ev.tags.length)
|
|
892
|
-
|
|
893
|
-
|
|
1120
|
+
var tags = '';
|
|
1121
|
+
if(ev.tags&&ev.tags.length) {
|
|
1122
|
+
for(var ti=0;ti<ev.tags.length;ti++) {
|
|
1123
|
+
tags += '<span class="ev-tag" data-tag="'+esc(ev.tags[ti])+'" onclick="setTagFilter(\''+esc(ev.tags[ti])+'\');event.stopPropagation()">'+esc(ev.tags[ti])+'</span>';
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
var time = fmtTime(ev.timestamp||'');
|
|
894
1127
|
return '<div class="ev-item '+tinfo[1]+'">'+
|
|
895
1128
|
'<div class="ev-head"><span class="ev-title">'+esc(ev.title)+'</span><span class="ev-type '+tinfo[0]+'">'+esc(ev.type||'')+'</span></div>'+
|
|
896
1129
|
'<div class="ev-meta">'+(time?'<span class="ev-time">'+esc(time)+'</span>':'')+refs+tags+'</div>'+
|
|
897
1130
|
'</div>';
|
|
898
1131
|
}
|
|
899
1132
|
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
1133
|
+
// Assign track to an event based on tags/ref
|
|
1134
|
+
function guessEventTrack(ev) {
|
|
1135
|
+
if(!R||!R.tracks||!R.tracks.length) return null;
|
|
1136
|
+
var tracks = R.tracks;
|
|
1137
|
+
for(var i=0;i<tracks.length;i++) {
|
|
1138
|
+
var t = tracks[i];
|
|
1139
|
+
if(ev.tags && ev.tags.indexOf(t.id)!==-1) return t.id;
|
|
1140
|
+
if(ev.title && ev.title.indexOf(t.id+' ')!==-1) return t.id;
|
|
1141
|
+
if(ev.title && ev.title.indexOf(t.id+'.')!==-1) return t.id;
|
|
1142
|
+
// Check phases
|
|
1143
|
+
var phases = t.phases||[];
|
|
1144
|
+
for(var j=0;j<phases.length;j++) {
|
|
1145
|
+
if(ev.tags && ev.tags.indexOf(phases[j].id)!==-1) return t.id;
|
|
1146
|
+
if(ev.title && ev.title.indexOf(phases[j].id)!==-1) return t.id;
|
|
1147
|
+
// Check tasks
|
|
1148
|
+
var tasks = phases[j].tasks||[];
|
|
1149
|
+
for(var k=0;k<tasks.length;k++) {
|
|
1150
|
+
if(ev.tags && ev.tags.indexOf(tasks[k].id)!==-1) return t.id;
|
|
1151
|
+
if(ev.title && ev.title.indexOf(tasks[k].id)!==-1) return t.id;
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
return null;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
function renderEvents() {
|
|
1159
|
+
var container = document.getElementById('events');
|
|
1160
|
+
var evs = T ? T.events||[] : [];
|
|
1161
|
+
if(!evs.length) {
|
|
1162
|
+
container.innerHTML = '<div class="empty"><h3 class="font-headline-sm text-headline-sm mb-sm">No events</h3><p class="font-body-md text-body-md text-on-surface-variant">Run build to refresh event log</p></div>';
|
|
1163
|
+
renderTagBar();
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
// Apply doc filter
|
|
1168
|
+
if(activeDocFilter) {
|
|
1169
|
+
var f=[];
|
|
1170
|
+
for(var i=0;i<evs.length;i++) { if(evMatchesDoc(evs[i],activeDocFilter)) f.push(evs[i]); }
|
|
1171
|
+
evs=f;
|
|
1172
|
+
}
|
|
906
1173
|
// Apply tag filter
|
|
907
|
-
if(activeTagFilter)
|
|
908
|
-
|
|
1174
|
+
if(activeTagFilter) {
|
|
1175
|
+
var f=[];
|
|
1176
|
+
for(var i=0;i<evs.length;i++) { if(evHasTag(evs[i],activeTagFilter)) f.push(evs[i]); }
|
|
1177
|
+
evs=f;
|
|
1178
|
+
}
|
|
1179
|
+
if(evs.length===0) {
|
|
1180
|
+
container.innerHTML = '<div class="empty"><h3 class="font-headline-sm text-headline-sm mb-sm">No matching events</h3><p class="font-body-md text-body-md text-on-surface-variant">Try clearing filters or run build to refresh</p></div>';
|
|
1181
|
+
renderTagBar();
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
909
1185
|
// Build doc name map
|
|
910
1186
|
var docNameMap={};
|
|
911
|
-
if(T&&T.elements)
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
1187
|
+
if(T&&T.elements) {
|
|
1188
|
+
for(var ei=0;ei<T.elements.length;ei++) {
|
|
1189
|
+
var el=T.elements[ei];
|
|
1190
|
+
if(el.source&&el.source.file&&el.name) docNameMap[el.source.file]=el.name;
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
// FLAT mode: all events by time
|
|
1195
|
+
if(sortMode==='flat') {
|
|
1196
|
+
evs.sort(function(a,b){ return (b.timestamp||"").localeCompare(a.timestamp||""); });
|
|
1197
|
+
var h='', curDay='';
|
|
1198
|
+
for(var i=0;i<evs.length;i++) {
|
|
917
1199
|
var dk3=(evs[i].timestamp||"").substring(0,10);
|
|
918
|
-
if(dk3!==curDay){curDay=dk3;h+=
|
|
1200
|
+
if(dk3!==curDay){ curDay=dk3; h+='<div class="ev-day">'+esc(curDay)+'</div>'; }
|
|
919
1201
|
h+=renderSingleEv(evs[i]);
|
|
920
1202
|
}
|
|
921
|
-
container.innerHTML=h;
|
|
1203
|
+
container.innerHTML = h;
|
|
1204
|
+
renderTagBar();
|
|
1205
|
+
document.getElementById('evcnt').textContent = evs.length+' events';
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
// TRACK mode: group events by guessed track, then within track by thread
|
|
1210
|
+
if(sortMode==='track' && R && R.tracks && R.tracks.length) {
|
|
1211
|
+
var trackGroups = {};
|
|
1212
|
+
var untracked = [];
|
|
1213
|
+
for(var i=0;i<evs.length;i++) {
|
|
1214
|
+
var tk = guessEventTrack(evs[i]);
|
|
1215
|
+
if(tk) {
|
|
1216
|
+
if(!trackGroups[tk]) trackGroups[tk] = [];
|
|
1217
|
+
trackGroups[tk].push(evs[i]);
|
|
1218
|
+
} else {
|
|
1219
|
+
untracked.push(evs[i]);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
var h = '';
|
|
1223
|
+
var trackOrder = [];
|
|
1224
|
+
for(var i=0;i<R.tracks.length;i++) { trackOrder.push(R.tracks[i].id); }
|
|
1225
|
+
for(var ti=0;ti<trackOrder.length;ti++) {
|
|
1226
|
+
var tid = trackOrder[ti];
|
|
1227
|
+
var tevs = trackGroups[tid]||[];
|
|
1228
|
+
if(!tevs.length) continue;
|
|
1229
|
+
h += renderTrackEventGroup(tid, tevs, docNameMap);
|
|
1230
|
+
}
|
|
1231
|
+
if(untracked.length) {
|
|
1232
|
+
h += renderTrackEventGroup('Other', untracked, docNameMap);
|
|
1233
|
+
}
|
|
1234
|
+
container.innerHTML = h;
|
|
922
1235
|
renderTagBar();
|
|
923
|
-
document.getElementById(
|
|
1236
|
+
document.getElementById('evcnt').textContent = evs.length+' events';
|
|
924
1237
|
return;
|
|
925
1238
|
}
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
for(var
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
1239
|
+
|
|
1240
|
+
// GROUPED mode (default): merge events by thread/doc
|
|
1241
|
+
var docThreads={}, orphanThreads={}, singles=[];
|
|
1242
|
+
for(var i=0;i<evs.length;i++) {
|
|
1243
|
+
var ev=evs[i];
|
|
1244
|
+
if(ev.ref&&ev.ref.doc) {
|
|
1245
|
+
if(!docThreads[ev.ref.doc]) docThreads[ev.ref.doc]=[];
|
|
1246
|
+
docThreads[ev.ref.doc].push(ev);
|
|
1247
|
+
} else if(ev.threadId) {
|
|
1248
|
+
if(!orphanThreads[ev.threadId]) orphanThreads[ev.threadId]=[];
|
|
1249
|
+
orphanThreads[ev.threadId].push(ev);
|
|
1250
|
+
} else {
|
|
1251
|
+
singles.push(ev);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
for(var d in docThreads) { docThreads[d].sort(function(a,b){ return (b.timestamp||"").localeCompare(a.timestamp||""); }); }
|
|
1255
|
+
for(var t in orphanThreads) { orphanThreads[t].sort(function(a,b){ return (b.timestamp||"").localeCompare(a.timestamp||""); }); }
|
|
1256
|
+
singles.sort(function(a,b){ return (b.timestamp||"").localeCompare(a.timestamp||""); });
|
|
1257
|
+
|
|
933
1258
|
var merged=[];
|
|
934
|
-
for(var doc in docThreads){
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
1259
|
+
for(var doc in docThreads) {
|
|
1260
|
+
merged.push({tk:"doc:"+doc, te:docThreads[doc], title:docNameMap[doc]||doc.replace(/^.*[\\\/]/,"").replace(/\.md$/i,""), dataDoc:doc, ts:docThreads[doc][0].timestamp});
|
|
1261
|
+
}
|
|
1262
|
+
for(var tid in orphanThreads) {
|
|
1263
|
+
merged.push({tk:"tid:"+tid, te:orphanThreads[tid], title:"Thread: "+tid, dataDoc:null, ts:orphanThreads[tid][0].timestamp});
|
|
1264
|
+
}
|
|
1265
|
+
merged.sort(function(a,b){ return (b.ts||"").localeCompare(a.ts||""); });
|
|
1266
|
+
|
|
938
1267
|
var top3={};
|
|
939
|
-
for(var ti=0;ti<Math.min(3,merged.length);ti++){top3[merged[ti].tk]=true}
|
|
940
|
-
|
|
941
|
-
function rtc(tk,te,title,dataDoc,isSemi){
|
|
942
|
-
var tc2={};
|
|
943
|
-
var
|
|
944
|
-
var
|
|
945
|
-
var
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
r
|
|
949
|
-
r+=
|
|
950
|
-
r+=
|
|
951
|
-
r+=
|
|
952
|
-
|
|
1268
|
+
for(var ti=0;ti<Math.min(3,merged.length);ti++) { top3[merged[ti].tk]=true; }
|
|
1269
|
+
|
|
1270
|
+
function rtc(tk, te, title, dataDoc, isSemi) {
|
|
1271
|
+
var tc2={};
|
|
1272
|
+
for(var i=0;i<te.length;i++) { var tt=te[i].type; tc2[tt]=(tc2[tt]||0)+1; }
|
|
1273
|
+
var ts=[]; for(var tk2 in tc2) { ts.push(tk2+"\u00d7"+tc2[tk2]); }
|
|
1274
|
+
var manualOpen=!!expandedThreads[tk];
|
|
1275
|
+
var cls=(isSemi&&!manualOpen)?"semi":(manualOpen?"open":"");
|
|
1276
|
+
if(manualOpen) expandedThreads[tk]=true;
|
|
1277
|
+
var r='<div class="th-card '+cls+'" data-thread="'+esc(tk)+'"'+(dataDoc?' data-doc="'+esc(dataDoc)+'"':'')+'>';
|
|
1278
|
+
r+='<div class="th-head" onclick="toggleThread(this)">';
|
|
1279
|
+
r+='<span style="font-size:10px;color:#727785;transition:transform .2s;display:inline-block">▶</span>';
|
|
1280
|
+
r+='<span class="th-title">'+esc(title)+'</span>';
|
|
1281
|
+
r+='<span class="th-count">'+te.length+' events</span>';
|
|
1282
|
+
r+='</div><div class="th-body">';
|
|
1283
|
+
r+='<div class="th-typebar">'+esc(ts.join(" · "))+'</div>';
|
|
1284
|
+
if(isSemi&&!manualOpen) {
|
|
953
1285
|
r+=renderSingleEv(te[0]);
|
|
954
|
-
if(te.length>1)r+=
|
|
955
|
-
}else{
|
|
956
|
-
for(var i=0;i<te.length;i++)r+=renderSingleEv(te[i]);
|
|
957
|
-
r+=
|
|
1286
|
+
if(te.length>1) r+='<div class="th-more" onclick="expandSemi(this);event.stopPropagation()">▼ View all '+te.length+' events</div>';
|
|
1287
|
+
} else {
|
|
1288
|
+
for(var i=0;i<te.length;i++) r+=renderSingleEv(te[i]);
|
|
1289
|
+
r+='<div class="th-collapse" onclick="collapseThread(this);event.stopPropagation()">▲ Collapse</div>';
|
|
958
1290
|
}
|
|
959
|
-
r+=
|
|
1291
|
+
r+='</div></div>';
|
|
1292
|
+
return r;
|
|
960
1293
|
}
|
|
961
|
-
|
|
962
|
-
var h=
|
|
963
|
-
for(var mi=0;mi<merged.length;mi++){var m=merged[mi];h+=rtc(m.tk,m.te,m.title,m.dataDoc,!!top3[m.tk])}
|
|
964
|
-
for(var ui=0;ui<singles.length;ui++)h+=renderSingleEv(singles[ui]);
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
1294
|
+
|
|
1295
|
+
var h='';
|
|
1296
|
+
for(var mi=0;mi<merged.length;mi++) { var m=merged[mi]; h+=rtc(m.tk,m.te,m.title,m.dataDoc,!!top3[m.tk]); }
|
|
1297
|
+
for(var ui=0;ui<singles.length;ui++) h+=renderSingleEv(singles[ui]);
|
|
1298
|
+
|
|
1299
|
+
container.innerHTML = h;
|
|
1300
|
+
if(activeDocFilter) {
|
|
1301
|
+
var mc = container.querySelector('.th-card[data-doc="'+CSS.escape(activeDocFilter)+'"]');
|
|
1302
|
+
if(mc) { mc.classList.add('th-active'); mc.scrollIntoView({behavior:'smooth',block:'center'}); }
|
|
969
1303
|
}
|
|
970
1304
|
renderTagBar();
|
|
971
|
-
document.getElementById(
|
|
1305
|
+
document.getElementById('evcnt').textContent = evs.length+' events';
|
|
972
1306
|
}
|
|
973
1307
|
|
|
1308
|
+
function renderTrackEventGroup(trackId, tevs, docNameMap) {
|
|
1309
|
+
// Within a track, group by thread/doc
|
|
1310
|
+
var docThreads={}, singles=[];
|
|
1311
|
+
for(var i=0;i<tevs.length;i++) {
|
|
1312
|
+
var ev=tevs[i];
|
|
1313
|
+
if(ev.ref&&ev.ref.doc) {
|
|
1314
|
+
if(!docThreads[ev.ref.doc]) docThreads[ev.ref.doc]=[];
|
|
1315
|
+
docThreads[ev.ref.doc].push(ev);
|
|
1316
|
+
} else {
|
|
1317
|
+
singles.push(ev);
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
for(var d in docThreads) { docThreads[d].sort(function(a,b){ return (b.timestamp||"").localeCompare(a.timestamp||""); }); }
|
|
1321
|
+
singles.sort(function(a,b){ return (b.timestamp||"").localeCompare(a.timestamp||""); });
|
|
1322
|
+
|
|
1323
|
+
// Get track label
|
|
1324
|
+
var trackLabel = trackId;
|
|
1325
|
+
if(R&&R.tracks) {
|
|
1326
|
+
for(var i=0;i<R.tracks.length;i++) {
|
|
1327
|
+
if(R.tracks[i].id===trackId) { trackLabel = R.tracks[i].label||trackId; break; }
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
974
1330
|
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
1331
|
+
var h = '<div class="mb-lg">';
|
|
1332
|
+
h += '<h4 class="font-label-caps text-label-caps text-on-surface-variant mb-md tracking-wider">'+esc(trackLabel)+' ('+tevs.length+' events)</h4>';
|
|
1333
|
+
h += '<div class="flex flex-col gap-sm border-l border-outline-variant ml-sm pl-lg relative">';
|
|
1334
|
+
|
|
1335
|
+
for(var d in docThreads) {
|
|
1336
|
+
var te = docThreads[d];
|
|
1337
|
+
var title = docNameMap[d]||d.replace(/^.*[\\\/]/,"").replace(/\.md$/i,"");
|
|
1338
|
+
for(var i=0;i<te.length;i++) {
|
|
1339
|
+
h += '<div class="relative py-xs">';
|
|
1340
|
+
h += '<span class="absolute -left-[25px] top-2 w-2 h-2 rounded-full bg-outline-design outline outline-4 outline-surface-bright"></span>';
|
|
1341
|
+
h += '<div class="bg-surface-container-lowest border border-outline-variant p-md rounded hover:shadow-md transition-all hover-scale">';
|
|
1342
|
+
h += '<div class="flex justify-between items-start mb-xs">';
|
|
1343
|
+
h += '<span class="font-code-md text-code-md text-on-surface flex items-center gap-xs">';
|
|
1344
|
+
if(te[i].ref&&te[i].ref.commit) h += '<span class="material-symbols-outlined text-tertiary-container text-[16px]">commit</span>';
|
|
1345
|
+
h += esc(te[i].title)+'</span>';
|
|
1346
|
+
h += '<span class="font-code-sm text-code-sm text-on-surface-variant">'+esc(fmtTime(te[i].timestamp||''))+'</span>';
|
|
1347
|
+
h += '</div>';
|
|
1348
|
+
if(te[i].type) {
|
|
1349
|
+
h += '<div class="flex flex-wrap gap-xs mt-sm">';
|
|
1350
|
+
h += '<span class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] uppercase tracking-wider font-code-sm bg-surface-container-low text-on-surface-variant border border-outline-variant">'+esc(te[i].type)+'</span>';
|
|
1351
|
+
if(te[i].tags) for(var j=0;j<Math.min(3,te[i].tags.length);j++) {
|
|
1352
|
+
h += '<span class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] uppercase tracking-wider font-code-sm bg-surface-container-low text-on-surface-variant border border-outline-variant">'+esc(te[i].tags[j])+'</span>';
|
|
1353
|
+
}
|
|
1354
|
+
h += '</div>';
|
|
1355
|
+
}
|
|
1356
|
+
h += '</div></div>';
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
for(var i=0;i<singles.length;i++) {
|
|
1360
|
+
h += '<div class="relative py-xs">';
|
|
1361
|
+
h += '<span class="absolute -left-[25px] top-2 w-2 h-2 rounded-full bg-outline-design outline outline-4 outline-surface-bright"></span>';
|
|
1362
|
+
h += '<div class="bg-surface-container-lowest border border-outline-variant p-md rounded hover:shadow-md transition-all hover-scale">';
|
|
1363
|
+
h += '<div class="flex justify-between items-start mb-xs">';
|
|
1364
|
+
h += '<span class="font-code-md text-code-md text-on-surface">'+esc(singles[i].title)+'</span>';
|
|
1365
|
+
h += '<span class="font-code-sm text-code-sm text-on-surface-variant">'+esc(fmtTime(singles[i].timestamp||''))+'</span>';
|
|
1366
|
+
h += '</div>';
|
|
1367
|
+
if(singles[i].type) {
|
|
1368
|
+
h += '<div class="flex flex-wrap gap-xs mt-sm">';
|
|
1369
|
+
h += '<span class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] uppercase tracking-wider font-code-sm bg-surface-container-low text-on-surface-variant border border-outline-variant">'+esc(singles[i].type)+'</span>';
|
|
1370
|
+
if(singles[i].tags) for(var j=0;j<Math.min(3,singles[i].tags.length);j++) {
|
|
1371
|
+
h += '<span class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] uppercase tracking-wider font-code-sm bg-surface-container-low text-on-surface-variant border border-outline-variant">'+esc(singles[i].tags[j])+'</span>';
|
|
1372
|
+
}
|
|
1373
|
+
h += '</div>';
|
|
1374
|
+
}
|
|
1375
|
+
h += '</div></div>';
|
|
981
1376
|
}
|
|
1377
|
+
h += '</div></div>';
|
|
1378
|
+
return h;
|
|
982
1379
|
}
|
|
983
1380
|
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
var
|
|
987
|
-
if(
|
|
988
|
-
card.classList.
|
|
989
|
-
|
|
1381
|
+
// Thread controls
|
|
1382
|
+
function toggleThread(headEl) {
|
|
1383
|
+
var card = headEl.parentNode;
|
|
1384
|
+
if(card.classList.contains('open')) { card.classList.remove('open'); }
|
|
1385
|
+
else { card.classList.add('open'); }
|
|
1386
|
+
}
|
|
1387
|
+
function expandSemi(el) {
|
|
1388
|
+
var card = el.parentNode.parentNode;
|
|
1389
|
+
var tk = card.getAttribute('data-thread');
|
|
1390
|
+
if(tk) expandedThreads[tk]=true;
|
|
1391
|
+
card.classList.remove('semi');
|
|
1392
|
+
card.classList.add('open');
|
|
990
1393
|
renderEvents();
|
|
991
1394
|
}
|
|
1395
|
+
function collapseThread(el) {
|
|
1396
|
+
var card = el.parentNode.parentNode;
|
|
1397
|
+
card.classList.remove('open');
|
|
1398
|
+
}
|
|
992
1399
|
|
|
993
|
-
function
|
|
994
|
-
|
|
995
|
-
|
|
1400
|
+
function evMatchesDoc(ev, doc) {
|
|
1401
|
+
if(!doc) return true;
|
|
1402
|
+
if(doc==='__unmatched__') return !evHasAnyElementMatch(ev);
|
|
1403
|
+
if(doc==='__todo__') return ev.tags&&ev.tags.indexOf('todo')!==-1;
|
|
1404
|
+
if(ev.ref&&ev.ref.doc===doc) return true;
|
|
1405
|
+
if(ev.tags&&ev.tags.indexOf(doc)!==-1) return true;
|
|
1406
|
+
if(ev.title&&ev.title.indexOf(doc)!==-1) return true;
|
|
1407
|
+
return false;
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
function evHasAnyElementMatch(ev) {
|
|
1411
|
+
if(!T||!T.elements) return false;
|
|
1412
|
+
for(var i=0;i<T.elements.length;i++) {
|
|
1413
|
+
var el=T.elements[i];
|
|
1414
|
+
if(el.source&&el.source.file) { if(ev.ref&&ev.ref.doc===el.source.file) return true; }
|
|
1415
|
+
var elVer=extractVersion(el.name||'');
|
|
1416
|
+
if(elVer&&ev.tags&&ev.tags.indexOf(elVer)!==-1) return true;
|
|
1417
|
+
if(elVer&&ev.title&&ev.title.indexOf(elVer)!==-1) return true;
|
|
1418
|
+
if(el.children) {
|
|
1419
|
+
for(var j=0;j<el.children.length;j++) {
|
|
1420
|
+
var ch=el.children[j];
|
|
1421
|
+
if(ch.source&&ch.source.file) { if(ev.ref&&ev.ref.doc===ch.source.file) return true; }
|
|
1422
|
+
var chVer=extractVersion(ch.name||'');
|
|
1423
|
+
if(chVer&&ev.tags&&ev.tags.indexOf(chVer)!==-1) return true;
|
|
1424
|
+
if(chVer&&ev.title&&ev.title.indexOf(chVer)!==-1) return true;
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
return false;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
function countUnmatchedEvents() {
|
|
1432
|
+
if(!T||!T.events) return 0;
|
|
1433
|
+
var c=0;
|
|
1434
|
+
for(var i=0;i<T.events.length;i++) { if(!evHasAnyElementMatch(T.events[i])) c++; }
|
|
1435
|
+
return c;
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
// ====== TAG FILTER ======
|
|
1439
|
+
function setTagFilter(tag) {
|
|
1440
|
+
if(activeTagFilter===tag) { activeTagFilter=null; }
|
|
1441
|
+
else { activeTagFilter=tag; }
|
|
1442
|
+
renderTagBar();
|
|
1443
|
+
renderEvents();
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
var expandedTagCats = {};
|
|
1447
|
+
function toggleTagCat(cn) {
|
|
1448
|
+
if(expandedTagCats[cn]) { expandedTagCats[cn]=false; }
|
|
1449
|
+
else { expandedTagCats[cn]=true; }
|
|
1450
|
+
renderTagBar();
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
function renderTagBar() {
|
|
1454
|
+
var bar = document.getElementById('tag-bar');
|
|
1455
|
+
if(!T||!T.events) { bar.innerHTML=''; return; }
|
|
1456
|
+
var allEvs=T.events||[], tagCount={};
|
|
1457
|
+
for(var i=0;i<allEvs.length;i++) {
|
|
1458
|
+
var tags=allEvs[i].tags||[];
|
|
1459
|
+
for(var j=0;j<tags.length;j++) tagCount[tags[j]]=(tagCount[tags[j]]||0)+1;
|
|
1460
|
+
}
|
|
1461
|
+
var catTags={};
|
|
1462
|
+
for(var tag in tagCount) {
|
|
1463
|
+
var c=catOfTag(tag);
|
|
1464
|
+
if(!catTags[c]) catTags[c]=[];
|
|
1465
|
+
catTags[c].push({tag:tag,count:tagCount[tag]});
|
|
1466
|
+
}
|
|
1467
|
+
var catOrder=["模块","主题","阶段","动作","其他"];
|
|
1468
|
+
var h='';
|
|
1469
|
+
for(var ci=0;ci<catOrder.length;ci++) {
|
|
1470
|
+
var cn=catOrder[ci], items=catTags[cn];
|
|
1471
|
+
if(!items||!items.length) continue;
|
|
1472
|
+
items.sort(function(a,b){ return b.count-a.count; });
|
|
1473
|
+
var expand=!!expandedTagCats[cn];
|
|
1474
|
+
var limit=expand?items.length:Math.min(3,items.length);
|
|
1475
|
+
h+='<div class="tag-row"><span class="tag-cat">'+cn+'</span>';
|
|
1476
|
+
for(var ii=0;ii<limit;ii++) {
|
|
1477
|
+
var item=items[ii], isOn=activeTagFilter===item.tag;
|
|
1478
|
+
h+='<span class="tag-btn'+(isOn?' on':'')+'" data-tag="'+esc(item.tag)+'" onclick="setTagFilter(\''+esc(item.tag)+'\');event.stopPropagation()">'+esc(item.tag)+' <em>'+item.count+'</em></span>';
|
|
1479
|
+
}
|
|
1480
|
+
if(items.length>3&&!expand) h+='<span class="tag-more" onclick="toggleTagCat(\''+esc(cn)+'\');event.stopPropagation()">+'+(items.length-3)+'</span>';
|
|
1481
|
+
if(expand) h+='<span class="tag-more" onclick="toggleTagCat(\''+esc(cn)+'\');event.stopPropagation()">Collapse</span>';
|
|
1482
|
+
h+='</div>';
|
|
1483
|
+
}
|
|
1484
|
+
bar.innerHTML = h;
|
|
996
1485
|
}
|
|
997
1486
|
|
|
998
1487
|
// ====== FLOATING PREVIEW ======
|
|
999
|
-
function preview(e,path,from){
|
|
1488
|
+
function preview(e, path, from) {
|
|
1000
1489
|
e.preventDefault();
|
|
1001
|
-
var float=document.getElementById('float'),overlay=document.getElementById('overlay');
|
|
1002
|
-
var needFr=(from==='left');
|
|
1003
|
-
var hasFr=float.classList.contains('fr');
|
|
1004
|
-
|
|
1005
|
-
document.getElementById('ftitle').textContent=path;
|
|
1006
|
-
document.getElementById('float-body').textContent='
|
|
1007
|
-
fetch('/'+encodeURI(path)).then(function(r)
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
}).
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1490
|
+
var float = document.getElementById('float'), overlay = document.getElementById('overlay');
|
|
1491
|
+
var needFr = (from==='left');
|
|
1492
|
+
var hasFr = float.classList.contains('fr');
|
|
1493
|
+
|
|
1494
|
+
document.getElementById('ftitle').textContent = path;
|
|
1495
|
+
document.getElementById('float-body').textContent = 'Loading...';
|
|
1496
|
+
fetch('/'+encodeURI(path)).then(function(r) {
|
|
1497
|
+
if(!r.ok) throw Error('HTTP '+r.status);
|
|
1498
|
+
return r.text();
|
|
1499
|
+
}).then(function(t) {
|
|
1500
|
+
t = t.replace(/<!--[\s\S]*?-->/g,'').trim();
|
|
1501
|
+
document.getElementById('float-body').textContent = t||'(empty file)';
|
|
1502
|
+
}).catch(function(err) {
|
|
1503
|
+
document.getElementById('float-body').textContent = 'Load failed: '+err.message;
|
|
1504
|
+
});
|
|
1505
|
+
|
|
1506
|
+
if(!float.classList.contains('hide') && needFr!==hasFr) {
|
|
1014
1507
|
float.classList.add('hide');
|
|
1015
|
-
setTimeout(function(){
|
|
1016
|
-
if(needFr){float.classList.add('fr')}else{float.classList.remove('fr')}
|
|
1508
|
+
setTimeout(function() {
|
|
1509
|
+
if(needFr) { float.classList.add('fr'); } else { float.classList.remove('fr'); }
|
|
1017
1510
|
float.classList.remove('hide');
|
|
1018
|
-
},300);
|
|
1019
|
-
}else{
|
|
1020
|
-
if(needFr){float.classList.add('fr')}else{float.classList.remove('fr')}
|
|
1511
|
+
}, 300);
|
|
1512
|
+
} else {
|
|
1513
|
+
if(needFr) { float.classList.add('fr'); } else { float.classList.remove('fr'); }
|
|
1021
1514
|
float.classList.remove('hide');
|
|
1022
1515
|
}
|
|
1023
1516
|
overlay.classList.remove('hide');
|
|
1024
1517
|
}
|
|
1025
1518
|
|
|
1026
|
-
document.getElementById('float-close').addEventListener('click',function(){
|
|
1519
|
+
document.getElementById('float-close').addEventListener('click', function() {
|
|
1027
1520
|
document.getElementById('float').classList.add('hide');
|
|
1028
1521
|
document.getElementById('overlay').classList.add('hide');
|
|
1029
1522
|
});
|
|
1030
|
-
document.getElementById('overlay').addEventListener('click',function(){
|
|
1523
|
+
document.getElementById('overlay').addEventListener('click', function() {
|
|
1031
1524
|
document.getElementById('float').classList.add('hide');
|
|
1032
1525
|
document.getElementById('overlay').classList.add('hide');
|
|
1033
1526
|
});
|
|
1034
1527
|
|
|
1035
1528
|
// ====== TOKEN PANEL ======
|
|
1036
|
-
function loadTokenToday(){
|
|
1037
|
-
fetch('../data/token-summary.json').then(function(r){return r.json()}).then(function(d){
|
|
1529
|
+
function loadTokenToday() {
|
|
1530
|
+
fetch('../data/token-summary.json').then(function(r) { return r.json(); }).then(function(d) {
|
|
1038
1531
|
renderTokenToday(d);
|
|
1039
|
-
}).catch(function(){
|
|
1040
|
-
document.getElementById('token-today').childNodes[0].textContent='
|
|
1532
|
+
}).catch(function() {
|
|
1533
|
+
document.getElementById('token-today').childNodes[0].textContent = 'Usage: --';
|
|
1041
1534
|
});
|
|
1042
1535
|
}
|
|
1043
1536
|
|
|
1044
|
-
function renderTokenToday(d){
|
|
1045
|
-
var today=new Date().toISOString().substring(0,10);
|
|
1046
|
-
var td=d&&d.byDate?d.byDate[today]:null;
|
|
1047
|
-
var todayTokens=td?td.tokens:0;
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
var h='';
|
|
1060
|
-
if(d&&d.totalTokens>0){
|
|
1061
|
-
h+='<div class="tip-row"><span class="tip-name">今日</span><span class="tip-val">'+(todayTokens?todayTokens.toLocaleString()+' tk':'--')+'</span></div>';
|
|
1062
|
-
h+='<div class="tip-row"><span class="tip-name">累计</span><span class="tip-val">'+d.totalTokens.toLocaleString()+' tk</span></div>';
|
|
1063
|
-
h+='<div class="tip-row"><span class="tip-name">估算费用</span><span class="tip-val">¥'+(d.totalCost||0).toFixed(3)+'</span></div>';
|
|
1064
|
-
if(d.bySkill){
|
|
1537
|
+
function renderTokenToday(d) {
|
|
1538
|
+
var today = new Date().toISOString().substring(0,10);
|
|
1539
|
+
var td = d&&d.byDate ? d.byDate[today] : null;
|
|
1540
|
+
var todayTokens = td ? td.tokens : 0;
|
|
1541
|
+
|
|
1542
|
+
var label = document.getElementById('token-today');
|
|
1543
|
+
label.childNodes[0].textContent = 'Usage: '+(todayTokens ? todayTokens.toLocaleString()+' tk' : '--');
|
|
1544
|
+
|
|
1545
|
+
var tip = document.getElementById('token-tip');
|
|
1546
|
+
var h = '';
|
|
1547
|
+
if(d&&d.totalTokens>0) {
|
|
1548
|
+
h += '<div class="tip-row"><span class="tip-name">Today</span><span class="tip-val">'+(todayTokens?todayTokens.toLocaleString()+' tk':'--')+'</span></div>';
|
|
1549
|
+
h += '<div class="tip-row"><span class="tip-name">Total</span><span class="tip-val">'+d.totalTokens.toLocaleString()+' tk</span></div>';
|
|
1550
|
+
h += '<div class="tip-row"><span class="tip-name">Est. Cost</span><span class="tip-val">$'+(d.totalCost||0).toFixed(3)+'</span></div>';
|
|
1551
|
+
if(d.bySkill) {
|
|
1065
1552
|
var skills=[];
|
|
1066
|
-
for(var k in d.bySkill){skills.push({name:k,tokens:d.bySkill[k].tokens||0,calls:d.bySkill[k].calls||0})}
|
|
1067
|
-
skills.sort(function(a,b){return b.tokens-a.tokens});
|
|
1068
|
-
if(skills.length>0){
|
|
1069
|
-
h+='<div class="tip-divider"></div>';
|
|
1070
|
-
for(var i=0;i<skills.length;i++){
|
|
1071
|
-
h+='<div class="tip-row"><span class="tip-name">'+esc(skills[i].name)+'</span><span class="tip-val">'+skills[i].tokens.toLocaleString()+' tk
|
|
1553
|
+
for(var k in d.bySkill) { skills.push({name:k,tokens:d.bySkill[k].tokens||0,calls:d.bySkill[k].calls||0}); }
|
|
1554
|
+
skills.sort(function(a,b){ return b.tokens-a.tokens; });
|
|
1555
|
+
if(skills.length>0) {
|
|
1556
|
+
h += '<div class="tip-divider"></div>';
|
|
1557
|
+
for(var i=0;i<skills.length;i++) {
|
|
1558
|
+
h += '<div class="tip-row"><span class="tip-name">'+esc(skills[i].name)+'</span><span class="tip-val">'+skills[i].tokens.toLocaleString()+' tk x'+skills[i].calls+'</span></div>';
|
|
1072
1559
|
}
|
|
1073
1560
|
}
|
|
1074
1561
|
}
|
|
1075
|
-
}else{
|
|
1076
|
-
h+='<div class="tip-row"><span class="tip-name"
|
|
1562
|
+
} else {
|
|
1563
|
+
h += '<div class="tip-row"><span class="tip-name">No data</span></div>';
|
|
1077
1564
|
}
|
|
1078
|
-
tip.innerHTML=h;
|
|
1565
|
+
tip.innerHTML = h;
|
|
1079
1566
|
|
|
1080
|
-
|
|
1081
|
-
label.
|
|
1082
|
-
label.onmouseleave=function(){tip.classList.remove('show')};
|
|
1567
|
+
label.onmouseenter = function() { tip.classList.add('show'); };
|
|
1568
|
+
label.onmouseleave = function() { tip.classList.remove('show'); };
|
|
1083
1569
|
}
|
|
1084
1570
|
</script>
|
|
1085
1571
|
</body>
|