sandtable 0.4.0 → 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.
@@ -3,742 +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>沙盘 v0.3 · Sandtable</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
- :root {
9
- --bg: #f0f2f5; --card: #fff; --border: #e2e6ea;
10
- --text: #1a1d23; --muted: #8b919b; --light: #5a6072;
11
- --header-bg: #111827; --brief-bg: #16213e;
12
- --past-bg: #e9edf2; --past-border: #c5cdd8;
13
- --current-bg: #fff9e6; --current-border: #f0c000;
14
- --future-bg: #f4f4f5; --future-border: #dcdce0;
15
- --accent: #00b894; --accent2: #0984e3;
16
- --completed: #00b894; --in_progress: #f0a000; --pending: #a0a7b4; --blocked: #e17055;
17
- --secondary: #6c5ce7; --link: #0984e3;
18
- --priority-must: #e17055; --priority-should: #f0a000; --priority-can: #a0a7b4;
19
- --shadow-sm: 0 1px 3px rgba(0,0,0,.06); --shadow-md: 0 4px 12px rgba(0,0,0,.08);
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
- *{margin:0;padding:0;box-sizing:border-box}
22
- body{font-family:Inter,-apple-system,BlinkMacSystemFont,'Segoe UI','Noto Sans SC',sans-serif;background:var(--bg);color:var(--text);min-height:100vh;display:flex;flex-direction:column;font-size:13px;line-height:1.5;-webkit-font-smoothing:antialiased}
23
-
24
- /* Header */
25
- #header{background:linear-gradient(135deg,#0f1419,#1a2332);color:#e8eaed;padding:10px 24px;display:flex;justify-content:space-between;align-items:center;box-shadow:0 1px 4px rgba(0,0,0,.2);z-index:10}
26
- #header h1{font-size:15px;font-weight:700;letter-spacing:-.3px;display:flex;align-items:center;gap:8px}
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
- /* Floating panel */
129
- #float{position:fixed;top:0;left:0;width:480px;max-width:94vw;height:100vh;background:#fff;box-shadow:8px 0 30px rgba(0,0,0,.18);z-index:1000;display:flex;flex-direction:column;transition:transform .3s cubic-bezier(.4,0,.2,1)}
130
- #float.hide{transform:translateX(-110vw)}
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
- /* Thread card */
158
- .th-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}
159
- .th-card:hover{box-shadow:var(--shadow-md)}.th-card.th-active{border-color:#4a90d9;box-shadow:0 0 0 2px rgba(74,144,217,.3)}
160
- .th-head{padding:10px 14px;background:#f4f6f9;cursor:pointer;display:flex;justify-content:space-between;align-items:center;gap:8px;user-select:none;transition:background .15s}
161
- .th-head:hover{background:#edf0f4}
162
- .th-head .th-title{font-weight:600;font-size:12px;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
163
- .th-head .th-count{font-size:10px;color:var(--muted);white-space:nowrap;background:rgba(0,0,0,.06);padding:2px 8px;border-radius:10px}
164
- .th-head .th-arrow{font-size:10px;color:var(--muted);transition:transform .2s}
165
- .th-card.open .th-arrow{transform:rotate(90deg)}
166
- .th-body{display:none;border-top:1px solid var(--border)}
167
- .th-card.open .th-body{display:block}
168
- .th-body .ev-item{margin:0;border-radius:0;border-left-width:3px;box-shadow:none}
169
- .th-body .ev-item:last-child{border-radius:0 0 8px 0}
170
-
171
- /* Tag filter bar */
172
- #tag-bar{padding:8px 0;margin-bottom:10px;border-bottom:1px solid var(--border)}
173
- .tag-row{display:flex;flex-wrap:wrap;align-items:center;gap:5px;padding:2px 0}
174
- .tag-row .tag-cat{font-size:9px;color:var(--muted);font-weight:600;white-space:nowrap;padding:2px 6px 2px 0;min-width:32px;text-align:right}
175
- .tag-row .tag-btn{font-size:10px;padding:2px 8px;border-radius:10px;border:1px solid #e0e3e8;background:#fff;cursor:pointer;color:var(--light);transition:all .15s;white-space:nowrap;font-weight:500}
176
- .tag-row .tag-btn em{font-style:normal;font-size:8px;color:var(--muted);margin-left:2px}
177
- .tag-row .tag-btn:hover{border-color:var(--accent);color:var(--accent);background:#f0fdf9}
178
- .tag-row .tag-btn.on{background:var(--accent);color:#fff;border-color:var(--accent);font-weight:600}
179
- .tag-row .tag-btn.on em{color:rgba(255,255,255,.7)}
180
- .tag-row .tag-more{font-size:9px;color:var(--link);padding:2px 4px;cursor:pointer;text-decoration:underline}
181
- .tag-row .tag-more:hover{color:#0066cc}
182
-
183
- /* Semi-expanded thread card */
184
- .th-card.semi .th-body{display:block}
185
- /* semi mode: JS renders only latest event + view-more button, no CSS hiding needed */
186
- .th-more{cursor:pointer;padding:8px 12px;text-align:center;font-size:11px;color:var(--link);font-weight:500;background:#f8f9fb;border-top:1px dashed var(--border);transition:background .15s}
187
- .th-more:hover{background:#edf0f4;color:#0066cc}
188
- .th-collapse{cursor:pointer;padding:8px 12px;text-align:center;font-size:11px;color:var(--muted);font-weight:500;background:#f8f9fb;border-top:1px solid var(--border);transition:all .15s}
189
- .th-collapse:hover{background:#edf0f4;color:var(--light)}
190
- .th-typebar{font-size:10px;color:var(--muted);padding:4px 12px;background:#f8f9fb;border-bottom:1px solid var(--border)}
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:var(--accent);color:#fff}
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
- <header id="header">
276
- <div><h1>沙盘 · Sandtable</h1><span class="badge">v0.3</span></div>
277
- <div class="right"><span id="token-today">工具token:--<span id="token-tip"></span></span><span id="upd">加载中...</span><span class="dot"></span></div>
278
- </header>
279
-
280
- <div id="brief"><strong>简报</strong><span id="btext">加载中...</span></div>
281
-
282
- <div id="main">
283
- <section id="left">
284
- <div class="panel-title">节点树 <span class="count" id="cnt">0 项</span></div>
285
- <div id="tree-tabs">
286
- <button class="on" data-c="all">全部</button>
287
- <button data-c="roadmap">路线图</button>
288
- <button data-c="todo">待办</button>
289
- <button data-c="decision">决策</button>
290
- <button data-c="spec">规格</button>
291
- <button data-c="convention">纪律</button>
292
- <button data-c="ops">运维</button>
293
-
294
- <button data-c="archive">档案</button>
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>
295
402
  </div>
296
- <div id="tree"></div>
297
403
  </section>
298
- <section id="right">
299
- <div class="panel-title">事件流 <span class="count" id="evcnt"></span></div>
300
- <div id="toolbar"><span class="sort-label">排列</span><span class="sort-group"><button class="sort-btn on" data-mode="grouped">按事件分类</button><button class="sort-btn" data-mode="flat">按触发时间</button></span><span class="ftext"></span><button class="fclear">✕ 清除</button></div>
301
- <div id="tag-bar"></div>
302
- <div id="events"></div>
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>
303
429
  </section>
304
- </div>
430
+ </main>
305
431
 
432
+ <!-- Floating Preview Panel -->
306
433
  <div id="overlay" class="hide"></div>
307
434
  <div id="float" class="hide">
308
- <div id="float-head"><span id="ftitle">预览</span><button id="float-close">&times;</button></div>
309
- <div id="float-body">加载中...</div>
435
+ <div id="float-head"><span id="ftitle">Preview</span><button id="float-close">&times;</button></div>
436
+ <div id="float-body">Loading...</div>
310
437
  </div>
311
438
 
312
439
  <script>
313
440
  // ====== DATA ======
314
441
  var T = null; // timeline data
442
+ var R = null; // roadmap data (tracks + phases)
315
443
  var activeCat = 'all';
316
- var activeDocFilter = null; // current doc filter (null = show all)
317
- var activeTagFilter = null; // current tag filter (null = show all)
318
- var expandedThreads = {}; // threads user has manually expanded
319
- var expandedRpCards = {}; // roadmap version cards user has manually expanded
320
- var sortMode = 'grouped'; // 'grouped' | 'flat'
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'
321
451
 
322
452
  var TAG_CATS = {
323
- "模块":["CLI","dashboard","builder","server","skill","npm","扫描器"],
324
- "主题":["事件流","token","UX","安装方案","安全","编码","配置","IDE适配","双视图","设计原则","libero"],
325
- "阶段":["v0.3","MVP","里程碑","集成","方向决策"],
326
- "动作":["架构","设计","实现","调研","文档","流程","教训","命令","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"]
327
457
  };
328
- function catOfTag(tag){for(var c in TAG_CATS){if(TAG_CATS[c].indexOf(tag)!==-1)return c}return "其他"}
329
- function evHasTag(ev,tag){return ev.tags&&ev.tags.indexOf(tag)!==-1}
330
- function setTagFilter(tag){if(activeTagFilter===tag){activeTagFilter=null}else{activeTagFilter=tag};renderTagBar();renderEvents()}
331
- function renderTagBar(){
332
- var bar=document.getElementById("tag-bar");
333
- if(!T||!T.events){bar.innerHTML="";return}
334
- var allEvs=T.events||[],tagCount={};
335
- 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}
336
- var catTags={};
337
- for(var tag in tagCount){var c=catOfTag(tag);if(!catTags[c])catTags[c]=[];catTags[c].push({tag:tag,count:tagCount[tag]})}
338
- var catOrder=["模块","主题","阶段","动作","其他"];
339
- var h="";
340
- for(var ci=0;ci<catOrder.length;ci++){
341
- var cn=catOrder[ci],items=catTags[cn];
342
- if(!items||!items.length)continue;
343
- items.sort(function(a,b){return b.count-a.count});
344
- var expand=!!expandedTagCats[cn];
345
- var limit=expand?items.length:Math.min(3,items.length);
346
- h+="<div class=\"tag-row\"><span class=\"tag-cat\">"+cn+"</span>";
347
- for(var ii=0;ii<limit;ii++){
348
- var item=items[ii],isOn=activeTagFilter===item.tag;
349
- 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>";
350
- }
351
- if(items.length>3&&!expand)h+="<span class=\"tag-more\" onclick=\"toggleTagCat('"+esc(cn)+"');event.stopPropagation()\">+"+(items.length-3)+"</span>";
352
- if(expand)h+="<span class=\"tag-more\" onclick=\"toggleTagCat('"+esc(cn)+"');event.stopPropagation()\">收起</span>";
353
- h+="</div>";
354
- }
355
- bar.innerHTML=h;
356
- }
458
+ var ETYPES = {'对齐与拍板':['ev-t1','t1'],'规格演进':['ev-t2','t2'],'代码变更':['ev-t3','t3'],'测试与质量':['ev-t4','t4'],'审批与交接':['ev-t5','t5'],'运维与基建':['ev-t6','t6'],'教训沉淀':['ev-t7','t7']};
357
459
 
358
- var expandedTagCats = {};
359
- function toggleTagCat(cn){
360
- if(expandedTagCats[cn]){expandedTagCats[cn]=false}else{expandedTagCats[cn]=true}
361
- renderTagBar();
362
- }
363
460
  var CLABEL = {roadmap:'路线图与进度',todo:'待办清单',decision:'决策记录',spec:'业务规格',convention:'协作纪律',ops:'运维与基建',archive:'历史档案',template:'工具模板',unknown:'未分类'};
364
461
  var PRIMARY = {roadmap:1,decision:1,todo:1};
365
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'};
366
- 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}
367
- function normTg(tg){if(!tg||tg==='archived')return'past';return tg}
368
-
369
- var SICON = {completed:'',in_progress:'⏳',pending:'⬜',blocked:'🚫',cancelled:'❌'};
370
- function sicon(s){return SICON[s]||'⬜'}
371
- function esc(s){if(!s)return'';var d=document.createElement('div');d.textContent=String(s);return d.innerHTML}
372
-
373
- function generateBrief(){
374
- var els=T.elements||[];
375
- var allMilestones=[];
376
- for(var i=0;i<els.length;i++){
377
- var children=els[i].children||[];
378
- for(var j=0;j<children.length;j++){
379
- if(children[j].elementType==="milestone")allMilestones.push(children[j]);
380
- }
381
- }
382
- var verStats={};
383
- for(var i=0;i<allMilestones.length;i++){
384
- var ch=allMilestones[i],ver=extractVersion(ch.name);
385
- if(!verStats[ver])verStats[ver]={total:0,done:0,inProgress:[]};
386
- verStats[ver].total++;
387
- if(ch.status==="completed"||ch.status==="cancelled")verStats[ver].done++;
388
- else if(ch.status==="in_progress")verStats[ver].inProgress.push(ch.name.replace(/\s*\([^)]*\)\s*/g,'').replace(/^M\d+:\s*/,''));
389
- else verStats[ver].inProgress.push(ch.name.replace(/\s*\([^)]*\)\s*/g,'').replace(/^M\d+:\s*/,''));
390
- }
391
- var versions=Object.keys(verStats).sort(function(a,b){return versionOrder(b)-versionOrder(a)});
392
- // Describe each version's milestone completion
393
- var parts=[];
394
- for(var i=0;i<versions.length;i++){
395
- var v=versions[i],vs=verStats[v];
396
- if(vs.done===vs.total){
397
- parts.push(v+' '+vs.done+'/'+vs.total+' 里程碑全部完成');
398
- }else{
399
- var names=vs.inProgress.slice(0,3).join('、');
400
- parts.push(v+' '+vs.done+'/'+vs.total+' 完成'+(names?','+names+'等推进中':''));
401
- }
402
- }
403
- return parts.join(';')+'。'||'暂无简报';
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 "其他";
476
+ }
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;
404
496
  }
405
497
 
406
498
  // ====== LOAD ======
407
- fetch('../data/timeline.json').then(function(r){return r.json()}).then(function(t){
408
- T = t;
409
- document.getElementById('upd').textContent = '更新:'+fmtTime(t.updated||'');
410
- document.getElementById('btext').innerHTML = generateBrief();
411
- renderTree();
412
- renderEvents();
413
- setupTabs();
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();
414
508
  setupToolbar();
415
509
  loadTokenToday();
416
- }).catch(function(e){
417
- document.getElementById('btext').textContent = '加载失败:'+e.message;
510
+ }).catch(function(e) {
511
+ document.getElementById('btext').textContent = 'Load failed: ' + e.message;
418
512
  console.error(e);
419
513
  });
420
514
 
421
- // ====== TABS ======
422
- function setupTabs(){
423
- var btns = document.querySelectorAll("#tree-tabs button[data-c]");
424
- for(var i=0;i<btns.length;i++){
425
- btns[i].addEventListener("click",function(){
426
- for(var j=0;j<btns.length;j++)btns[j].classList.remove("on");
427
- this.classList.add("on");
428
- activeCat = this.getAttribute("data-c");
429
- renderTree();
430
- });
431
- }
515
+ function renderAll() {
516
+ renderTrackSwitcher();
517
+ renderInlineTabs();
518
+ renderTree();
519
+ renderEvents();
432
520
  }
433
521
 
434
- // ====== TOOLBAR ======
435
- function setupToolbar(){
436
- var sortBtns=document.querySelectorAll("#toolbar .sort-btn");
437
- for(var i=0;i<sortBtns.length;i++){
438
- sortBtns[i].addEventListener("click",function(){
439
- for(var j=0;j<sortBtns.length;j++)sortBtns[j].classList.remove("on");
440
- this.classList.add("on");
441
- sortMode=this.getAttribute("data-mode");
442
- renderEvents();
443
- });
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
+ }
444
534
  }
445
- var fclear=document.querySelector("#toolbar .fclear");
446
- if(fclear){
447
- fclear.addEventListener("click",function(){
448
- activeDocFilter=null;
449
- activeTagFilter=null;
450
- this.classList.remove("show");
451
- document.querySelector("#toolbar .ftext").textContent="";
452
- document.body.classList.remove("filter-active");
453
- renderTree();
454
- renderTagBar();
455
- renderEvents();
456
- });
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*/,''));
457
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;
458
553
  }
459
554
 
460
- // ====== TREE ======
461
- function catOf(el){return normCat(el.category || ETYPE[el.elementType] || "unknown")}
462
-
463
- function hasChildCat(el,cat){
464
- if(catOf(el)===cat)return true;
465
- if(el.children)for(var i=0;i<el.children.length;i++){if(hasChildCat(el.children[i],cat))return true}
466
- return false
467
- }
468
-
469
- function extractVersion(name){
470
- var m=name.match(/\(v\d+\.\d+\)/);
471
- if(m)return m[0].replace(/[()]/g,"");
472
- m=name.match(/\(([A-D])\)/);
473
- if(m)return "v0.3";
474
- return "其他";
475
- }
476
-
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
-
484
- function fmtTime(ts){
485
- if(!ts)return"";
486
- var d=new Date(ts);
487
- var mm=String(d.getMonth()+1).padStart(2,'0');
488
- var dd=String(d.getDate()).padStart(2,'0');
489
- var hh=String(d.getHours()).padStart(2,'0');
490
- var mi=String(d.getMinutes()).padStart(2,'0');
491
- return mm+'-'+dd+' '+hh+':'+mi;
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
+ }
492
590
  }
493
591
 
494
- function getVersionTitle(ver,children,phaseName){
495
- if(phaseName){
496
- var parts=phaseName.split(/[·;;]/);
497
- for(var i=0;i<parts.length;i++){
498
- if(parts[i].indexOf(ver)!==-1)return parts[i].trim();
499
- }
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>';
500
607
  }
501
- var first=children[0];
502
- if(first){
503
- var nm=first.name.replace(/\s*\([^)]*\)\s*/g,"").replace(/^M\d+:\s*/,"");
504
- return ver+" · "+nm;
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
+ });
505
617
  }
506
- return ver;
507
618
  }
508
619
 
509
- function getVersionUpdateTime(ver,children){
510
- var latest="";
511
- if(T&&T.events){
512
- for(var i=0;i<T.events.length;i++){
513
- var ev=T.events[i];
514
- if(ev.tags&&ev.tags.indexOf(ver)!==-1){
515
- if(!latest||ev.timestamp>latest)latest=ev.timestamp;
516
- }
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';
517
639
  }
518
640
  }
519
- if(!latest){
520
- for(var i=0;i<children.length;i++){
521
- var d=children[i].date;
522
- if(d&&(!latest||d>latest))latest=d;
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';
523
649
  }
524
650
  }
525
- return fmtTime(latest);
526
651
  }
527
652
 
528
- function renderTree(){
653
+ // ====== TREE ======
654
+ function renderTree() {
655
+ syncNavTabs();
529
656
  var els = T.elements||[];
530
- var roadmapEls=[], otherEls=[];
531
- for(var i=0;i<els.length;i++){
532
- var el=els[i];
533
- if(el.category==="roadmap" || el.elementType==="phase" || el.elementType==="milestone"){
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") {
534
665
  roadmapEls.push(el);
535
- }else{
536
- if(activeCat!=="all" && catOf(el)!==activeCat && !hasChildCat(el,activeCat))continue;
666
+ } else {
667
+ if(activeCat!=="all" && catOf(el)!==activeCat && !hasChildCat(el,activeCat)) continue;
537
668
  otherEls.push(el);
538
669
  }
539
670
  }
540
671
 
541
- var total=otherEls.length;
542
- var h="";
543
-
544
- // ===== Roadmap section (路线图与进度) =====
545
- if((activeCat==="all" || activeCat==="roadmap") && roadmapEls.length>0){
546
- h+=renderRoadmap(roadmapEls);
547
- for(var ri=0;ri<roadmapEls.length;ri++){
548
- if(roadmapEls[ri].children)total+=roadmapEls[ri].children.length;
549
- else total+=1;
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
+ }
550
691
  }
551
692
 
552
- // 分支优化 归入 roadmap 板块,收纳所有未关联路线图的事件
553
- var unmatchedCount=countUnmatchedEvents();
554
- var branchNode={
555
- id:"virtual-branch-optimizations",
556
- kind:"primary",elementType:"catch-all",category:"roadmap",
557
- name:"分支优化",status:"in_progress",timeGroup:"current",
558
- timeLabel:"Bug修复 / 功能微调 / 零散优化",
559
- source:{file:"__unmatched__",title:"分支优化"},
560
- summary:"兜底节点:收纳所有未关联到具体路线图里程碑的事件(Bug修复、功能微调、零散优化等)",
561
- tags:["兜底"],children:[],order:999,
562
- 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)
563
703
  };
564
- h+='<div class="cat-section"><div class="cat-header primary"><span>分支优化</span><span>'+unmatchedCount+' 条事件</span></div>';
565
- h+=renderTreeCard(branchNode);
566
- h+="</div>";
567
- total+=1;
568
- }
569
-
570
- // ===== 待办清单 section — 路线图之后、决策记录之前 =====
571
- if(activeCat==="all"){
572
- var todoNode={
573
- id:"virtual-todo-list",
574
- kind:"primary",elementType:"todo",category:"todo",
575
- name:"待办清单",status:"pending",timeGroup:"future",
576
- timeLabel:"发现但未解决的优化点 / 待办项",
577
- source:{file:"__todo__",title:"待办清单"},
578
- summary:"记录所有已发现但尚未解决的优化点、待办项,避免遗漏",
579
- tags:["待办"],children:[],order:998,
580
- date:new Date().toISOString().substring(0,10)
581
- };
582
- h+='<div class="cat-section"><div class="cat-header primary"><span>待办清单</span><span>待整理</span></div>';
583
- h+=renderTreeCard(todoNode);
584
- h+="</div>";
585
- 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
+ }
586
725
  }
587
726
 
588
- if(activeCat==="all" || activeCat!=="roadmap"){
589
- var groups={},order=[];
590
- for(var i=0;i<otherEls.length;i++){
591
- var c=catOf(otherEls[i]);
592
- if(!groups[c]){groups[c]=[];order.push(c)}
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); }
593
733
  groups[c].push(otherEls[i]);
594
734
  }
595
- for(var k in groups){
596
- groups[k].sort(function(a,b){
597
- if(a.date&&b.date)return b.date.localeCompare(a.date);
598
- return(b.order||0)-(a.order||0)
599
- });
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); });
600
737
  }
601
- order.sort(function(a,b){
602
- var pa=PRIMARY[a]?0:1,pb=PRIMARY[b]?0:1;
603
- if(pa!==pb)return pa-pb;
604
- 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;
605
742
  return 0;
606
743
  });
607
-
608
- for(var i=0;i<order.length;i++){
609
- var g=order[i],items=groups[g],label=CLABEL[g]||g,isP=PRIMARY[g];
610
- h+='<div class="cat-section"><div class="cat-header '+(isP?"primary":"secondary")+'"><span>'+esc(label)+'</span><span>'+items.length+' 项</span></div>';
611
- for(var j=0;j<items.length;j++){
612
- h+=renderTreeCard(items[j]);
613
- }
614
- 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>';
615
749
  }
616
750
  }
617
751
 
618
- if(total===0){document.getElementById("tree").innerHTML='<div class="empty"><h3>暂无内容</h3><p>选择其他分类或运行 build 刷新</p></div>';return}
619
- document.getElementById("cnt").textContent=total+" ";
620
- document.getElementById("tree").innerHTML=h;
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;
621
757
  bindDocClicks();
622
- (function(){
623
- var mores=document.querySelectorAll(".rp-more");
624
- for(var i=0;i<mores.length;i++){
625
- mores[i].onclick=function(e){
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) {
626
764
  e.stopPropagation();
627
- var card=this.parentNode.parentNode;
628
- var ver=card.getAttribute("data-ver");
629
- if(ver)expandedRpCards[ver]=true;
765
+ var card = this.parentNode.parentNode;
766
+ var ver = card.getAttribute('data-ver');
767
+ if(ver) expandedRpCards[ver]=true;
630
768
  renderTree();
631
769
  };
632
770
  }
633
771
  })();
634
772
  }
635
773
 
636
- function renderRoadmap(roadmapEls){
637
- var verGroups={},verOrder=[];
638
- for(var i=0;i<roadmapEls.length;i++){
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++) {
639
875
  var el=roadmapEls[i];
640
876
  var children=el.children||[];
641
- for(var j=0;j<children.length;j++){
877
+ for(var j=0;j<children.length;j++) {
642
878
  var ch=children[j];
643
879
  var ver=extractVersion(ch.name);
644
- if(!verGroups[ver]){verGroups[ver]=[];verOrder.push(ver)}
880
+ if(!verGroups[ver]){verGroups[ver]=[];verOrder.push(ver);}
645
881
  verGroups[ver].push(ch);
646
882
  }
647
883
  }
648
884
  verOrder.sort(function(a,b){return versionOrder(a)-versionOrder(b)});
649
-
650
- for(var v in verGroups){
651
- verGroups[v].sort(function(a,b){
652
- var da=a.date||"",db=b.date||"";
653
- return db.localeCompare(da);
654
- });
885
+ for(var v in verGroups) {
886
+ verGroups[v].sort(function(a,b){var da=a.date||"",db=b.date||"";return db.localeCompare(da);});
655
887
  }
656
-
657
888
  var vgList=[];
658
- for(var vi=0;vi<verOrder.length;vi++){
889
+ for(var vi=0;vi<verOrder.length;vi++) {
659
890
  var v=verOrder[vi],g=verGroups[v];
660
891
  vgList.push({ver:v,children:g});
661
892
  }
662
- // Sort by version descending (v0.3 → v0.2 → v0.1)
663
893
  vgList.sort(function(a,b){return versionOrder(b.ver)-versionOrder(a.ver)});
664
-
665
894
  var top3={};
666
- 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;}
667
896
 
668
- // Derive title parts from phase element name
669
897
  var phaseName="";
670
- for(var i=0;i<roadmapEls.length;i++){
671
- 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;}
672
900
  }
673
901
 
674
902
  var h="";
675
- for(var i=0;i<vgList.length;i++){
903
+ for(var i=0;i<vgList.length;i++) {
676
904
  var vg=vgList[i],children=vg.children,ver=vg.ver;
677
905
  var done=0,total=children.length;
678
906
  for(var j=0;j<children.length;j++){
679
907
  if(children[j].status==="completed"||children[j].status==="cancelled")done++;
680
908
  }
681
909
  var allDone=(done===total);
682
- var ratio=Math.round(done/total*100);
683
-
684
910
  var stateCls=allDone?"rp-done":(done>0?"rp-mixed":"");
685
911
  var ratioCls=allDone?"all-done":(done>0?"partial":"");
686
912
  var ratioText=done+"/"+total;
687
-
688
913
  var isTop3=!!top3[ver];
689
914
  var manualOpen=!!expandedRpCards[ver];
690
915
  var isSemi=isTop3 && !allDone && !manualOpen;
691
916
  var isOpen=manualOpen;
692
-
693
917
  var title=getVersionTitle(ver,children,phaseName);
694
918
  var upTime=getVersionUpdateTime(ver,children);
695
919
 
696
- var isFilterMatch=activeDocFilter && ver===activeDocFilter;
697
- 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)+'">';
698
921
  h+='<div class="rp-head">';
699
- h+='<span class="rp-arrow" onclick="toggleRpCard(this);event.stopPropagation()">\u25b6</span>';
700
- h+='<span class="rp-title" onclick="setDocFilter(\''+esc(ver)+'\');event.stopPropagation()" title="筛选此版本事件">'+esc(title)+'</span>';
922
+ h+='<span class="rp-arrow" onclick="toggleRpCard(this);event.stopPropagation()">&#9654;</span>';
923
+ h+='<span class="rp-title" onclick="setDocFilter(\''+esc(ver)+'\');event.stopPropagation()" title="Filter events for this version">'+esc(title)+'</span>';
701
924
  h+='<span class="rp-meta"><span class="rp-ratio '+ratioCls+'">'+ratioText+'</span><span class="tc-time">'+esc(upTime)+'</span></span>';
702
925
  h+='</div><div class="rp-body">';
703
-
704
- if(isSemi){
926
+ if(isSemi) {
705
927
  var incChildren=[],comChildren=[];
706
- for(var j=0;j<children.length;j++){
707
- if(children[j].status==="completed"||children[j].status==="cancelled"){
708
- comChildren.push(children[j]);
709
- }else{
710
- incChildren.push(children[j]);
711
- }
712
- }
713
- for(var j=0;j<incChildren.length;j++){
714
- h+=renderRoadmapChild(incChildren[j]);
715
- }
716
- if(comChildren.length>0){
717
- h+='<div class="rp-more">\u25bc 查看全部 '+total+' 里程碑(含 '+comChildren.length+' 已完成)</div>';
718
- }
719
- }else{
720
- for(var j=0;j<children.length;j++){
721
- 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]);
722
931
  }
932
+ for(var j=0;j<incChildren.length;j++) h+=renderRoadmapChild(incChildren[j]);
933
+ if(comChildren.length>0) h+='<div class="rp-more">&#9660; View all '+total+' milestones ('+comChildren.length+' completed)</div>';
934
+ } else {
935
+ for(var j=0;j<children.length;j++) h+=renderRoadmapChild(children[j]);
723
936
  }
724
- h+="</div></div>";
937
+ h+='</div></div>';
725
938
  }
726
939
  return h;
727
940
  }
728
941
 
729
- function toggleRpCard(el){
942
+ function toggleRpCard(el) {
730
943
  var card=el.parentNode.parentNode;
731
944
  var ver=card.getAttribute("data-ver");
732
- if(expandedRpCards[ver]){expandedRpCards[ver]=false}
733
- else{expandedRpCards[ver]=true}
945
+ if(expandedRpCards[ver]){expandedRpCards[ver]=false;}
946
+ else{expandedRpCards[ver]=true;}
734
947
  renderTree();
735
948
  }
736
949
 
737
- function renderRoadmapChild(ch){
950
+ function renderRoadmapChild(ch) {
738
951
  var done=ch.status==="completed"||ch.status==="cancelled";
739
- var icon=done?"\u2705":(ch.status==="in_progress"?"\u23f3":"\u2b1c");
952
+ var icon=done?"&#x2705;":(ch.status==="in_progress"?"&#x23f3;":"&#x2b1c;");
740
953
  var labelCls=done?"done":(ch.status==="in_progress"?"progress":"pending-label");
741
- var labelText=done?"完成":(ch.status==="in_progress"?"进行中":"待开始");
954
+ var labelText=done?"Done":(ch.status==="in_progress"?"In Progress":"Pending");
742
955
  var time=fmtTime(ch.date)||ch.timeLabel||"";
743
956
  var ver=extractVersion(ch.name);
744
957
  var h='<div class="rp-child" data-ver="'+esc(ver)+'" data-doc="'+(ch.source&&ch.source.file?esc(ch.source.file):"")+'">';
@@ -746,342 +959,613 @@ function renderRoadmapChild(ch){
746
959
  h+='<span class="rp-cname">'+esc(ch.name||ch.id)+'</span>';
747
960
  h+='<span class="rp-ctime">'+esc(time)+'</span>';
748
961
  h+='<span class="rp-clabel '+labelCls+'">'+labelText+'</span>';
749
- h+="</div>";
962
+ h+='</div>';
750
963
  return h;
751
964
  }
752
965
 
753
- function renderTreeCard(el){
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) {
754
1002
  var isDoc=!!(el.source&&el.source.file);
755
1003
  var tg=normTg(el.timeGroup);
756
1004
  var time=fmtTime(el.date)||el.timeLabel||"";
757
- var unk=catOf(el)==="unknown"?'<span style="background:#fff3e0;color:#e65100;font-size:9px;padding:1px 5px;border-radius:3px">未分类</span>':"";
1005
+ var unk=catOf(el)==="unknown"?'<span style="background:#fff3e0;color:#e65100;font-size:9px;padding:1px 5px;border-radius:3px">unclassified</span>':"";
758
1006
 
759
- // "新建"/"更新" badge for doc-type cards without status
760
1007
  var docBadge="";
761
- if(isDoc || !el.status){
1008
+ if(isDoc || !el.status) {
762
1009
  var elDate=el.date||"",updDate=T&&T.updated?T.updated:"";
763
1010
  var daysOld=99;
764
- if(elDate&&updDate){daysOld=(new Date(updDate)-new Date(elDate))/(86400000)}
765
- if(daysOld<=1)docBadge='<span class="tc-type status-done">新建</span>';
766
- else if(daysOld<=3)docBadge='<span class="tc-type status-progress">更新</span>';
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>';
767
1014
  }
768
1015
 
769
- 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));
770
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:"")+'\')">';
771
1018
 
772
1019
  h+='<div class="tc-head">';
773
- if(!isDoc && el.status){
774
- h+="<span>"+sicon(el.status)+" "+esc(el.name)+"</span>";
1020
+ if(!isDoc && el.status) {
1021
+ h+='<span>'+sicon(el.status)+' '+esc(el.name)+'</span>';
775
1022
  var stCls=el.status==="completed"?"status-done":(el.status==="in_progress"?"status-progress":"status-pending");
776
- h+='<span class="tc-type '+stCls+'">'+(el.status==="completed"?"完成":(el.status==="in_progress"?"进行中":el.status))+'</span>';
777
- }else{
778
- h+="<span>"+esc(el.name)+"</span>";
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>';
779
1026
  h+='<span class="tc-meta">'+unk+docBadge+'<span class="tc-time">'+esc(time)+'</span></span>';
780
1027
  }
781
- h+="</div>";
1028
+ h+='</div>';
782
1029
 
783
1030
  h+='<div class="tc-meta" style="padding:0 12px 4px">';
784
- if(el.status)h+='<span class="tc-time">'+esc(time)+'</span>';
785
- if(el.tags&&el.tags.length){
786
- for(var i=0;i<el.tags.length;i++){
787
- h+='<span class="tag">'+esc(el.tags[i])+'</span>';
788
- }
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>';
789
1034
  }
790
- h+="</div>";
791
-
792
- if(el.summary)h+='<div class="tc-summary">'+esc(el.summary)+'</div>';
1035
+ h+='</div>';
793
1036
 
794
- if(isDoc){
1037
+ if(el.summary) h+='<div class="tc-summary">'+esc(el.summary)+'</div>';
1038
+ if(isDoc) {
795
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>';
796
1040
  }
797
-
798
- h+="</div>";
1041
+ h+='</div>';
799
1042
  return h;
800
1043
  }
801
1044
 
802
- function setDocFilter(doc){
803
- if(activeDocFilter===doc){activeDocFilter=null}
804
- else{activeDocFilter=doc}
805
- var ft=document.querySelector("#toolbar .ftext");
806
- var fc=document.querySelector("#toolbar .fclear");
807
- if(activeDocFilter){
808
- var label=activeDocFilter==='__unmatched__'?'分支优化(未归类事件)':activeDocFilter==='__todo__'?'待办清单':activeDocFilter;
809
- ft.textContent="筛选: "+label;fc.classList.add("show");
810
- document.body.classList.add("filter-active");
811
- }else{
812
- ft.textContent="";fc.classList.remove("show");
813
- document.body.classList.remove("filter-active");
1045
+ function sicon(s) {
1046
+ var map = {completed:'&#x2705;',in_progress:'&#x23f3;',pending:'&#x2b1c;',blocked:'&#x1f6ab;',cancelled:'&#x274c;'};
1047
+ return map[s]||'&#x2b1c;';
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');
814
1063
  }
815
1064
  renderTree();
816
1065
  renderEvents();
817
1066
  }
818
1067
 
819
- function onTreeCardClick(e,doc){
820
- if(e.target.closest("a"))return;
821
- if(doc){setDocFilter(doc)}
1068
+ function onTreeCardClick(e, doc) {
1069
+ if(e.target.closest('a')) return;
1070
+ if(doc) { setDocFilter(doc); }
822
1071
  }
823
1072
 
824
- function bindDocClicks(){
825
- var rpChildren=document.querySelectorAll(".rp-child");
826
- for(var i=0;i<rpChildren.length;i++){
827
- rpChildren[i].addEventListener("click",function(e){
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) {
828
1077
  e.stopPropagation();
829
- var ver=this.getAttribute("data-ver");
830
- if(ver){setDocFilter(ver);return}
831
- var doc=this.getAttribute("data-doc");
832
- 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);
833
1082
  });
834
1083
  }
835
1084
  }
836
1085
 
837
- // ====== EVENTS ======
838
- var ETYPES = {'对齐与拍板':['ev-t1','t1'],'规格演进':['ev-t2','t2'],'代码变更':['ev-t3','t3'],'测试与质量':['ev-t4','t4'],'审批与交接':['ev-t5','t5'],'运维与基建':['ev-t6','t6'],'教训沉淀':['ev-t7','t7']};
839
- var PCLASS = {'必记':'ev-p1','应记':'ev-p2','可记':'ev-p3'};
840
-
841
- function evMatchesDoc(ev, doc){
842
- if(!doc)return true;
843
- if(doc==='__unmatched__')return !evHasAnyElementMatch(ev);
844
- if(doc==='__todo__')return ev.tags&&ev.tags.indexOf('待办')!==-1;
845
- if(ev.ref&&ev.ref.doc===doc)return true;
846
- if(ev.tags&&ev.tags.indexOf(doc)!==-1)return true;
847
- if(ev.title&&ev.title.indexOf(doc)!==-1)return true;
848
- return false;
849
- }
850
-
851
- function evHasAnyElementMatch(ev){
852
- if(!T||!T.elements)return false;
853
- for(var i=0;i<T.elements.length;i++){
854
- var el=T.elements[i];
855
- if(el.source&&el.source.file){
856
- if(ev.ref&&ev.ref.doc===el.source.file)return true;
857
- }
858
- var elVer=extractVersion(el.name||'');
859
- if(elVer&&ev.tags&&ev.tags.indexOf(elVer)!==-1)return true;
860
- if(elVer&&ev.title&&ev.title.indexOf(elVer)!==-1)return true;
861
- if(el.children){
862
- for(var j=0;j<el.children.length;j++){
863
- var ch=el.children[j];
864
- if(ch.source&&ch.source.file){
865
- if(ev.ref&&ev.ref.doc===ch.source.file)return true;
866
- }
867
- var chVer=extractVersion(ch.name||'');
868
- if(chVer&&ev.tags&&ev.tags.indexOf(chVer)!==-1)return true;
869
- if(chVer&&ev.title&&ev.title.indexOf(chVer)!==-1)return true;
870
- }
871
- }
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
+ });
872
1096
  }
873
- return false;
874
- }
875
-
876
- function countUnmatchedEvents(){
877
- if(!T||!T.events)return 0;
878
- var c=0;
879
- for(var i=0;i<T.events.length;i++){
880
- if(!evHasAnyElementMatch(T.events[i]))c++;
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
+ });
881
1109
  }
882
- return c;
883
1110
  }
884
1111
 
885
- function renderSingleEv(ev){
886
- var tinfo=ETYPES[ev.type]||['ev-t3','t3'];
887
- var refs='';
888
- if(ev.ref){
889
- if(ev.ref.commit)refs+='<code style="font-size:10px;background:#f5f5f5;padding:0 3px;border-radius:2px">'+esc(ev.ref.commit)+'</code> ';
890
- 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> ';
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> ';
891
1119
  }
892
- var tags='';
893
- if(ev.tags&&ev.tags.length)for(var ti=0;ti<ev.tags.length;ti++)tags+='<span class="ev-tag" data-tag="'+esc(ev.tags[ti])+'" onclick="setTagFilter(\''+esc(ev.tags[ti])+'\');event.stopPropagation()">'+esc(ev.tags[ti])+'</span>';
894
- var time=fmtTime(ev.timestamp||'');
895
-
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||'');
896
1127
  return '<div class="ev-item '+tinfo[1]+'">'+
897
1128
  '<div class="ev-head"><span class="ev-title">'+esc(ev.title)+'</span><span class="ev-type '+tinfo[0]+'">'+esc(ev.type||'')+'</span></div>'+
898
1129
  '<div class="ev-meta">'+(time?'<span class="ev-time">'+esc(time)+'</span>':'')+refs+tags+'</div>'+
899
1130
  '</div>';
900
1131
  }
901
1132
 
902
- function renderEvents(){
903
- var container=document.getElementById("events");
904
- var evs=T?T.events||[]:[];
905
- if(!evs.length){container.innerHTML="<div class=\"empty\"><h3>暂无事件</h3><p>运行 build 后 git log 事件将显示在这里</p></div>";renderTagBar();return}
906
- // Apply doc filter (matches ref.doc OR tags)
907
- if(activeDocFilter){var f=[];for(var i=0;i<evs.length;i++){if(evMatchesDoc(evs[i],activeDocFilter))f.push(evs[i])}evs=f}
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
+ }
908
1173
  // Apply tag filter
909
- if(activeTagFilter){var f=[];for(var i=0;i<evs.length;i++){if(evHasTag(evs[i],activeTagFilter))f.push(evs[i])}evs=f}
910
- if(evs.length===0){container.innerHTML="<div class=\"empty\"><h3>没有匹配的事件</h3><p>尝试清除筛选或运行 build 刷新</p></div>";renderTagBar();return}
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
+
911
1185
  // Build doc name map
912
1186
  var docNameMap={};
913
- if(T&&T.elements){for(var ei=0;ei<T.elements.length;ei++){var el=T.elements[ei];if(el.source&&el.source.file&&el.name)docNameMap[el.source.file]=el.name}}
914
- // Flat mode: all events sorted by timestamp, no grouping
915
- if(sortMode==='flat'){
916
- evs.sort(function(a,b){return (b.timestamp||"").localeCompare(a.timestamp||"")});
917
- var h="",curDay="";
918
- for(var i=0;i<evs.length;i++){
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++) {
919
1199
  var dk3=(evs[i].timestamp||"").substring(0,10);
920
- if(dk3!==curDay){curDay=dk3;h+="<div class=\"ev-day\">"+esc(curDay)+"</div>"}
1200
+ if(dk3!==curDay){ curDay=dk3; h+='<div class="ev-day">'+esc(curDay)+'</div>'; }
921
1201
  h+=renderSingleEv(evs[i]);
922
1202
  }
923
- container.innerHTML=h;
1203
+ container.innerHTML = h;
924
1204
  renderTagBar();
925
- document.getElementById("evcnt").textContent=evs.length+" 条";
1205
+ document.getElementById('evcnt').textContent = evs.length+' events';
926
1206
  return;
927
1207
  }
928
- // Grouped mode: merge all events by thread/doc across ALL dates (no daily headers)
929
- var docThreads={},orphanThreads={},singles=[];
930
- for(var i=0;i<evs.length;i++){var ev=evs[i];if(ev.ref&&ev.ref.doc){if(!docThreads[ev.ref.doc])docThreads[ev.ref.doc]=[];docThreads[ev.ref.doc].push(ev)}else if(ev.threadId){if(!orphanThreads[ev.threadId])orphanThreads[ev.threadId]=[];orphanThreads[ev.threadId].push(ev)}else{singles.push(ev)}}
931
- for(var d in docThreads){docThreads[d].sort(function(a,b){return (b.timestamp||"").localeCompare(a.timestamp||"")})}
932
- for(var t in orphanThreads){orphanThreads[t].sort(function(a,b){return (b.timestamp||"").localeCompare(a.timestamp||"")})}
933
- singles.sort(function(a,b){return (b.timestamp||"").localeCompare(a.timestamp||"")});
934
- // Merge doc + orphan threads into one list sorted by latest event timestamp
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;
1235
+ renderTagBar();
1236
+ document.getElementById('evcnt').textContent = evs.length+' events';
1237
+ return;
1238
+ }
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
+
935
1258
  var merged=[];
936
- for(var doc in docThreads){merged.push({tk:"doc:"+doc,te:docThreads[doc],title:docNameMap[doc]||doc.replace(/^.*[\\\/]/,"").replace(/\.md$/i,""),dataDoc:doc,ts:docThreads[doc][0].timestamp})}
937
- for(var tid in orphanThreads){merged.push({tk:"tid:"+tid,te:orphanThreads[tid],title:"线程: "+tid,dataDoc:null,ts:orphanThreads[tid][0].timestamp})}
938
- merged.sort(function(a,b){return (b.ts||"").localeCompare(a.ts||"")});
939
- // Top 3 threads semi-collapsed
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
+
940
1267
  var top3={};
941
- for(var ti=0;ti<Math.min(3,merged.length);ti++){top3[merged[ti].tk]=true}
942
- // Render thread card helper
943
- function rtc(tk,te,title,dataDoc,isSemi){
944
- var tc2={};for(var i=0;i<te.length;i++){var tt=te[i].type;tc2[tt]=(tc2[tt]||0)+1}
945
- var ts=[];for(var tk2 in tc2){ts.push(tk2+"\u00d7"+tc2[tk2])}
946
- var manualOpen=!!expandedThreads[tk];var cls=(isSemi&&!manualOpen)?"semi":(manualOpen?"open":"");if(manualOpen)expandedThreads[tk]=true;
947
- var r="<div class=\"th-card "+cls+"\" data-thread=\""+esc(tk)+"\""+(dataDoc?" data-doc=\""+esc(dataDoc)+"\"":"")+">";
948
- r+="<div class=\"th-head\" onclick=\"toggleThread(this)\">";
949
- r+="<span class=\"th-arrow\">\u25b6</span>";
950
- r+="<span class=\"th-title\">"+esc(title)+"</span>";
951
- r+="<span class=\"th-count\">"+te.length+" \u6761</span>";
952
- r+="</div><div class=\"th-body\">";
953
- r+="<div class=\"th-typebar\">"+esc(ts.join(" \u00b7 "))+"</div>";
954
- if(isSemi&&!manualOpen){
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">&#9654;</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) {
955
1285
  r+=renderSingleEv(te[0]);
956
- if(te.length>1)r+="<div class=\"th-more\" onclick=\"expandSemi(this);event.stopPropagation()\">\u25bc \u67e5\u770b\u5168\u90e8 "+te.length+" \u6761\u4e8b\u4ef6</div>";
957
- }else{
958
- for(var i=0;i<te.length;i++)r+=renderSingleEv(te[i]);
959
- r+="<div class=\"th-collapse\" onclick=\"collapseThread(this);event.stopPropagation()\">\u25b2 \u6536\u8d77</div>";
1286
+ if(te.length>1) r+='<div class="th-more" onclick="expandSemi(this);event.stopPropagation()">&#9660; 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()">&#9650; Collapse</div>';
960
1290
  }
961
- r+="</div></div>";return r;
1291
+ r+='</div></div>';
1292
+ return r;
962
1293
  }
963
- // Render: threads first, then orphan singles
964
- var h="";
965
- 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])}
966
- for(var ui=0;ui<singles.length;ui++)h+=renderSingleEv(singles[ui]);
967
- container.innerHTML=h;
968
- if(activeDocFilter){
969
- var mc=container.querySelector(".th-card[data-doc=\""+CSS.escape(activeDocFilter)+"\"]");
970
- if(mc){mc.classList.add("th-active");mc.scrollIntoView({behavior:"smooth",block:"center"})}
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'}); }
971
1303
  }
972
1304
  renderTagBar();
973
- document.getElementById("evcnt").textContent=evs.length+" 条";
1305
+ document.getElementById('evcnt').textContent = evs.length+' events';
974
1306
  }
975
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
+ }
976
1330
 
977
- function toggleThread(headEl){
978
- var card=headEl.parentNode;
979
- if(card.classList.contains('open')){
980
- card.classList.remove('open');
981
- }else{
982
- card.classList.add('open');
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>';
983
1376
  }
1377
+ h += '</div></div>';
1378
+ return h;
984
1379
  }
985
1380
 
986
- function expandSemi(el){
987
- var card=el.parentNode.parentNode;
988
- var tk=card.getAttribute("data-thread");
989
- if(tk)expandedThreads[tk]=true;
990
- card.classList.remove("semi");
991
- card.classList.add("open");
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');
992
1393
  renderEvents();
993
1394
  }
1395
+ function collapseThread(el) {
1396
+ var card = el.parentNode.parentNode;
1397
+ card.classList.remove('open');
1398
+ }
994
1399
 
995
- function collapseThread(el){
996
- var card=el.parentNode.parentNode;
997
- card.classList.remove("open");
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;
998
1485
  }
999
1486
 
1000
1487
  // ====== FLOATING PREVIEW ======
1001
- function preview(e,path,from){
1488
+ function preview(e, path, from) {
1002
1489
  e.preventDefault();
1003
- var float=document.getElementById('float'),overlay=document.getElementById('overlay');
1004
- var needFr=(from==='left'); // tree → fr (right:0), events → no fr (left:0)
1005
- var hasFr=float.classList.contains('fr');
1006
-
1007
- document.getElementById('ftitle').textContent=path;
1008
- document.getElementById('float-body').textContent='加载中...';
1009
- fetch('/'+encodeURI(path)).then(function(r){if(!r.ok)throw Error('HTTP '+r.status);return r.text()}).then(function(t){
1010
- t=t.replace(/<!--[\s\S]*?-->/g,'').trim();
1011
- document.getElementById('float-body').textContent=t||'(空文件)';
1012
- }).catch(function(err){document.getElementById('float-body').textContent='加载失败:'+err.message});
1013
-
1014
- // If switching sides, hide first to avoid position jump
1015
- if(!float.classList.contains('hide') && needFr!==hasFr){
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) {
1016
1507
  float.classList.add('hide');
1017
- setTimeout(function(){
1018
- 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'); }
1019
1510
  float.classList.remove('hide');
1020
- },300);
1021
- }else{
1022
- 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'); }
1023
1514
  float.classList.remove('hide');
1024
1515
  }
1025
1516
  overlay.classList.remove('hide');
1026
1517
  }
1027
1518
 
1028
- document.getElementById('float-close').addEventListener('click',function(){
1519
+ document.getElementById('float-close').addEventListener('click', function() {
1029
1520
  document.getElementById('float').classList.add('hide');
1030
1521
  document.getElementById('overlay').classList.add('hide');
1031
1522
  });
1032
- document.getElementById('overlay').addEventListener('click',function(){
1523
+ document.getElementById('overlay').addEventListener('click', function() {
1033
1524
  document.getElementById('float').classList.add('hide');
1034
1525
  document.getElementById('overlay').classList.add('hide');
1035
1526
  });
1036
1527
 
1037
1528
  // ====== TOKEN PANEL ======
1038
- function loadTokenToday(){
1039
- 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) {
1040
1531
  renderTokenToday(d);
1041
- }).catch(function(){
1042
- document.getElementById('token-today').childNodes[0].textContent='工具token:--';
1532
+ }).catch(function() {
1533
+ document.getElementById('token-today').childNodes[0].textContent = 'Usage: --';
1043
1534
  });
1044
1535
  }
1045
1536
 
1046
- function renderTokenToday(d){
1047
- var today=new Date().toISOString().substring(0,10);
1048
- var td=d&&d.byDate?d.byDate[today]:null;
1049
- var todayTokens=td?td.tokens:0;
1050
- var todayCost=0;
1051
- if(td&&d.bySkill){
1052
- // Estimate today's cost from skills if available
1053
- todayCost=d.totalCost||0;
1054
- }
1055
-
1056
- var label=document.getElementById('token-today');
1057
- label.childNodes[0].textContent='工具token:'+(todayTokens?todayTokens.toLocaleString()+' tk':'--');
1058
-
1059
- // Build tooltip content
1060
- var tip=document.getElementById('token-tip');
1061
- var h='';
1062
- if(d&&d.totalTokens>0){
1063
- h+='<div class="tip-row"><span class="tip-name">今日</span><span class="tip-val">'+(todayTokens?todayTokens.toLocaleString()+' tk':'--')+'</span></div>';
1064
- h+='<div class="tip-row"><span class="tip-name">累计</span><span class="tip-val">'+d.totalTokens.toLocaleString()+' tk</span></div>';
1065
- h+='<div class="tip-row"><span class="tip-name">估算费用</span><span class="tip-val">¥'+(d.totalCost||0).toFixed(3)+'</span></div>';
1066
- 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) {
1067
1552
  var skills=[];
1068
- for(var k in d.bySkill){skills.push({name:k,tokens:d.bySkill[k].tokens||0,calls:d.bySkill[k].calls||0})}
1069
- skills.sort(function(a,b){return b.tokens-a.tokens});
1070
- if(skills.length>0){
1071
- h+='<div class="tip-divider"></div>';
1072
- for(var i=0;i<skills.length;i++){
1073
- h+='<div class="tip-row"><span class="tip-name">'+esc(skills[i].name)+'</span><span class="tip-val">'+skills[i].tokens.toLocaleString()+' tk ×'+skills[i].calls+'</span></div>';
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>';
1074
1559
  }
1075
1560
  }
1076
1561
  }
1077
- }else{
1078
- h+='<div class="tip-row"><span class="tip-name">暂无数据</span></div>';
1562
+ } else {
1563
+ h += '<div class="tip-row"><span class="tip-name">No data</span></div>';
1079
1564
  }
1080
- tip.innerHTML=h;
1565
+ tip.innerHTML = h;
1081
1566
 
1082
- // Hover toggling
1083
- label.onmouseenter=function(){tip.classList.add('show')};
1084
- label.onmouseleave=function(){tip.classList.remove('show')};
1567
+ label.onmouseenter = function() { tip.classList.add('show'); };
1568
+ label.onmouseleave = function() { tip.classList.remove('show'); };
1085
1569
  }
1086
1570
  </script>
1087
1571
  </body>