sandtable 0.3.1 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,740 +3,955 @@
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>沙盘 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="decision">决策</button>
289
- <button data-c="spec">规格</button>
290
- <button data-c="convention">纪律</button>
291
- <button data-c="ops">运维</button>
292
- <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>
293
402
  </div>
294
- <div id="tree"></div>
295
403
  </section>
296
- <section id="right">
297
- <div class="panel-title">事件流 <span class="count" id="evcnt"></span></div>
298
- <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>
299
- <div id="tag-bar"></div>
300
- <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>
301
429
  </section>
302
- </div>
430
+ </main>
303
431
 
432
+ <!-- Floating Preview Panel -->
304
433
  <div id="overlay" class="hide"></div>
305
434
  <div id="float" class="hide">
306
- <div id="float-head"><span id="ftitle">预览</span><button id="float-close">&times;</button></div>
307
- <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>
308
437
  </div>
309
438
 
310
439
  <script>
311
440
  // ====== DATA ======
312
441
  var T = null; // timeline data
442
+ var R = null; // roadmap data (tracks + phases)
313
443
  var activeCat = 'all';
314
- var activeDocFilter = null; // current doc filter (null = show all)
315
- var activeTagFilter = null; // current tag filter (null = show all)
316
- var expandedThreads = {}; // threads user has manually expanded
317
- var expandedRpCards = {}; // roadmap version cards user has manually expanded
318
- 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'
319
451
 
320
452
  var TAG_CATS = {
321
- "模块":["CLI","dashboard","builder","server","skill","npm","扫描器"],
322
- "主题":["事件流","token","UX","安装方案","安全","编码","配置","IDE适配","双视图","设计原则","libero"],
323
- "阶段":["v0.3","MVP","里程碑","集成","方向决策"],
324
- "动作":["架构","设计","实现","调研","文档","流程","教训","命令","IDE","init","uninstall","summarize","scan"]
453
+ "模块": ["CLI","dashboard","builder","server","skill","npm","扫描器"],
454
+ "主题": ["事件流","token","UX","安装方案","安全","编码","配置","IDE适配","双视图","设计原则","libero"],
455
+ "阶段": ["v0.3","MVP","里程碑","集成","方向决策"],
456
+ "动作": ["架构","设计","实现","调研","文档","流程","教训","命令","IDE","init","uninstall","summarize","scan"]
325
457
  };
326
- function catOfTag(tag){for(var c in TAG_CATS){if(TAG_CATS[c].indexOf(tag)!==-1)return c}return "其他"}
327
- function evHasTag(ev,tag){return ev.tags&&ev.tags.indexOf(tag)!==-1}
328
- function setTagFilter(tag){if(activeTagFilter===tag){activeTagFilter=null}else{activeTagFilter=tag};renderTagBar();renderEvents()}
329
- function renderTagBar(){
330
- var bar=document.getElementById("tag-bar");
331
- if(!T||!T.events){bar.innerHTML="";return}
332
- var allEvs=T.events||[],tagCount={};
333
- for(var i=0;i<allEvs.length;i++){var tags=allEvs[i].tags||[];for(var j=0;j<tags.length;j++)tagCount[tags[j]]=(tagCount[tags[j]]||0)+1}
334
- var catTags={};
335
- for(var tag in tagCount){var c=catOfTag(tag);if(!catTags[c])catTags[c]=[];catTags[c].push({tag:tag,count:tagCount[tag]})}
336
- var catOrder=["模块","主题","阶段","动作","其他"];
337
- var h="";
338
- for(var ci=0;ci<catOrder.length;ci++){
339
- var cn=catOrder[ci],items=catTags[cn];
340
- if(!items||!items.length)continue;
341
- items.sort(function(a,b){return b.count-a.count});
342
- var expand=!!expandedTagCats[cn];
343
- var limit=expand?items.length:Math.min(3,items.length);
344
- h+="<div class=\"tag-row\"><span class=\"tag-cat\">"+cn+"</span>";
345
- for(var ii=0;ii<limit;ii++){
346
- var item=items[ii],isOn=activeTagFilter===item.tag;
347
- h+="<span class=\"tag-btn"+(isOn?" on":"")+"\" data-tag=\""+esc(item.tag)+"\" onclick=\"setTagFilter('"+esc(item.tag)+"');event.stopPropagation()\">"+esc(item.tag)+" <em>"+item.count+"</em></span>";
348
- }
349
- if(items.length>3&&!expand)h+="<span class=\"tag-more\" onclick=\"toggleTagCat('"+esc(cn)+"');event.stopPropagation()\">+"+(items.length-3)+"</span>";
350
- if(expand)h+="<span class=\"tag-more\" onclick=\"toggleTagCat('"+esc(cn)+"');event.stopPropagation()\">收起</span>";
351
- h+="</div>";
352
- }
353
- bar.innerHTML=h;
354
- }
458
+ var ETYPES = {'对齐与拍板':['ev-t1','t1'],'规格演进':['ev-t2','t2'],'代码变更':['ev-t3','t3'],'测试与质量':['ev-t4','t4'],'审批与交接':['ev-t5','t5'],'运维与基建':['ev-t6','t6'],'教训沉淀':['ev-t7','t7']};
355
459
 
356
- var expandedTagCats = {};
357
- function toggleTagCat(cn){
358
- if(expandedTagCats[cn]){expandedTagCats[cn]=false}else{expandedTagCats[cn]=true}
359
- renderTagBar();
460
+ var CLABEL = {roadmap:'路线图与进度',todo:'待办清单',decision:'决策记录',spec:'业务规格',convention:'协作纪律',ops:'运维与基建',archive:'历史档案',template:'工具模板',unknown:'未分类'};
461
+ var PRIMARY = {roadmap:1,decision:1,todo:1};
462
+ var ETYPE = {phase:'roadmap',milestone:'roadmap',task:'roadmap',subtask:'roadmap',roadmap:'roadmap',backlog:'todo',todo:'todo',conclusion:'roadmap',decision:'decision',refactor:'decision',spec:'spec',intent:'spec',prompt:'spec',convention:'convention',agent:'ops',runbook:'ops',optimization:'roadmap',journal:'archive',handover:'archive',plan_doc:'archive',template:'template'};
463
+
464
+ function normCat(c) { if(!c) return 'unknown'; var m = {conventions:'convention',specs:'spec',decisions:'decision',plans:'roadmap',journals:'archive',journal:'archive'}; return m[c]||c; }
465
+ function normTg(tg) { if(!tg||tg==='archived') return 'past'; return tg; }
466
+ function esc(s) { if(!s) return ''; var d = document.createElement('div'); d.textContent = String(s); return d.innerHTML; }
467
+ function catOf(el) { return normCat(el.category || ETYPE[el.elementType] || "unknown"); }
468
+ function catOfTag(tag) { for(var c in TAG_CATS) { if(TAG_CATS[c].indexOf(tag)!==-1) return c; } return "其他"; }
469
+ function evHasTag(ev, tag) { return ev.tags && ev.tags.indexOf(tag)!==-1; }
470
+ function extractVersion(name) {
471
+ var m = name.match(/\(v\d+\.\d+\)/);
472
+ if(m) return m[0].replace(/[()]/g,"");
473
+ m = name.match(/\(([A-D])\)/);
474
+ if(m) return "v0.3";
475
+ return "其他";
360
476
  }
361
- var CLABEL = {roadmap:'路线图与进度',decision:'决策记录',spec:'业务规格',convention:'协作纪律',ops:'运维与基建',archive:'历史档案',template:'工具模板',unknown:'未分类'};
362
- var PRIMARY = {roadmap:1,decision:1};
363
- var ETYPE = {phase:'roadmap',milestone:'roadmap',task:'roadmap',subtask:'roadmap',roadmap:'roadmap',backlog:'roadmap',conclusion:'roadmap',decision:'decision',refactor:'decision',spec:'spec',intent:'spec',prompt:'spec',convention:'convention',agent:'ops',runbook:'ops',journal:'archive',handover:'archive',plan_doc:'archive',template:'template'};
364
- 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}
365
- function normTg(tg){if(!tg||tg==='archived')return'past';return tg}
366
-
367
- var SICON = {completed:'✅',in_progress:'⏳',pending:'⬜',blocked:'🚫',cancelled:'❌'};
368
- function sicon(s){return SICON[s]||'⬜'}
369
- function esc(s){if(!s)return'';var d=document.createElement('div');d.textContent=String(s);return d.innerHTML}
370
-
371
- function generateBrief(){
372
- var els=T.elements||[];
373
- var allMilestones=[];
374
- for(var i=0;i<els.length;i++){
375
- var children=els[i].children||[];
376
- for(var j=0;j<children.length;j++){
377
- if(children[j].elementType==="milestone")allMilestones.push(children[j]);
378
- }
379
- }
380
- var verStats={};
381
- for(var i=0;i<allMilestones.length;i++){
382
- var ch=allMilestones[i],ver=extractVersion(ch.name);
383
- if(!verStats[ver])verStats[ver]={total:0,done:0,inProgress:[]};
384
- verStats[ver].total++;
385
- if(ch.status==="completed"||ch.status==="cancelled")verStats[ver].done++;
386
- else if(ch.status==="in_progress")verStats[ver].inProgress.push(ch.name.replace(/\s*\([^)]*\)\s*/g,'').replace(/^M\d+:\s*/,''));
387
- else verStats[ver].inProgress.push(ch.name.replace(/\s*\([^)]*\)\s*/g,'').replace(/^M\d+:\s*/,''));
388
- }
389
- var versions=Object.keys(verStats).sort(function(a,b){return versionOrder(b)-versionOrder(a)});
390
- // Describe each version's milestone completion
391
- var parts=[];
392
- for(var i=0;i<versions.length;i++){
393
- var v=versions[i],vs=verStats[v];
394
- if(vs.done===vs.total){
395
- parts.push(v+' '+vs.done+'/'+vs.total+' 里程碑全部完成');
396
- }else{
397
- var names=vs.inProgress.slice(0,3).join('、');
398
- parts.push(v+' '+vs.done+'/'+vs.total+' 完成'+(names?','+names+'等推进中':''));
399
- }
400
- }
401
- return parts.join(';')+'。'||'暂无简报';
477
+ function versionOrder(v) {
478
+ if(v==="v0.1") return 1;
479
+ if(v==="v0.2") return 2;
480
+ if(v==="v0.3") return 3;
481
+ return 99;
482
+ }
483
+ function fmtTime(ts) {
484
+ if(!ts) return "";
485
+ var d = new Date(ts);
486
+ var mm = String(d.getMonth()+1).padStart(2,'0');
487
+ var dd = String(d.getDate()).padStart(2,'0');
488
+ var hh = String(d.getHours()).padStart(2,'0');
489
+ var mi = String(d.getMinutes()).padStart(2,'0');
490
+ return mm+'-'+dd+' '+hh+':'+mi;
491
+ }
492
+ function hasChildCat(el, cat) {
493
+ if(catOf(el)===cat) return true;
494
+ if(el.children) for(var i=0;i<el.children.length;i++) { if(hasChildCat(el.children[i],cat)) return true; }
495
+ return false;
402
496
  }
403
497
 
404
498
  // ====== LOAD ======
405
- fetch('../data/timeline.json').then(function(r){return r.json()}).then(function(t){
406
- T = t;
407
- document.getElementById('upd').textContent = '更新:'+fmtTime(t.updated||'');
408
- document.getElementById('btext').innerHTML = generateBrief();
409
- renderTree();
410
- renderEvents();
411
- 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();
412
508
  setupToolbar();
413
509
  loadTokenToday();
414
- }).catch(function(e){
415
- document.getElementById('btext').textContent = '加载失败:'+e.message;
510
+ }).catch(function(e) {
511
+ document.getElementById('btext').textContent = 'Load failed: ' + e.message;
416
512
  console.error(e);
417
513
  });
418
514
 
419
- // ====== TABS ======
420
- function setupTabs(){
421
- var btns = document.querySelectorAll("#tree-tabs button[data-c]");
422
- for(var i=0;i<btns.length;i++){
423
- btns[i].addEventListener("click",function(){
424
- for(var j=0;j<btns.length;j++)btns[j].classList.remove("on");
425
- this.classList.add("on");
426
- activeCat = this.getAttribute("data-c");
427
- renderTree();
428
- });
429
- }
515
+ function renderAll() {
516
+ renderTrackSwitcher();
517
+ renderInlineTabs();
518
+ renderTree();
519
+ renderEvents();
430
520
  }
431
521
 
432
- // ====== TOOLBAR ======
433
- function setupToolbar(){
434
- var sortBtns=document.querySelectorAll("#toolbar .sort-btn");
435
- for(var i=0;i<sortBtns.length;i++){
436
- sortBtns[i].addEventListener("click",function(){
437
- for(var j=0;j<sortBtns.length;j++)sortBtns[j].classList.remove("on");
438
- this.classList.add("on");
439
- sortMode=this.getAttribute("data-mode");
440
- renderEvents();
441
- });
522
+ function generateBrief() {
523
+ // Use roadmap.json brief if available
524
+ if(R && R.brief) return R.brief;
525
+ // Fallback: generate from timeline elements
526
+ if(!T || !T.elements) return 'Loading...';
527
+ var els = T.elements||[];
528
+ var allMilestones = [];
529
+ for(var i=0;i<els.length;i++) {
530
+ var children = els[i].children||[];
531
+ for(var j=0;j<children.length;j++) {
532
+ if(children[j].elementType==="milestone") allMilestones.push(children[j]);
533
+ }
442
534
  }
443
- var fclear=document.querySelector("#toolbar .fclear");
444
- if(fclear){
445
- fclear.addEventListener("click",function(){
446
- activeDocFilter=null;
447
- activeTagFilter=null;
448
- this.classList.remove("show");
449
- document.querySelector("#toolbar .ftext").textContent="";
450
- document.body.classList.remove("filter-active");
451
- renderTree();
452
- renderTagBar();
453
- renderEvents();
454
- });
535
+ var verStats = {};
536
+ for(var i=0;i<allMilestones.length;i++) {
537
+ var ch=allMilestones[i], ver=extractVersion(ch.name);
538
+ if(!verStats[ver]) verStats[ver]={total:0,done:0,inProgress:[]};
539
+ verStats[ver].total++;
540
+ if(ch.status==="completed"||ch.status==="cancelled") verStats[ver].done++;
541
+ else verStats[ver].inProgress.push(ch.name.replace(/\s*\([^)]*\)\s*/g,'').replace(/^M\d+:\s*/,''));
455
542
  }
543
+ var versions = Object.keys(verStats).sort(function(a,b){ return versionOrder(b)-versionOrder(a); });
544
+ var parts = [];
545
+ for(var i=0;i<versions.length;i++) {
546
+ var v=versions[i], vs=verStats[v];
547
+ if(vs.done===vs.total) parts.push(v+' '+vs.done+'/'+vs.total+' done');
548
+ else { var names=vs.inProgress.slice(0,3).join(', '); parts.push(v+' '+vs.done+'/'+vs.total+(names?' ('+names+')':'')); }
549
+ }
550
+ var brief = parts.join(' | ') || 'No data';
551
+ if(R && R.currentNode) brief += ' | Current: '+R.currentNode;
552
+ return brief;
456
553
  }
457
554
 
458
- // ====== TREE ======
459
- function catOf(el){return normCat(el.category || ETYPE[el.elementType] || "unknown")}
460
-
461
- function hasChildCat(el,cat){
462
- if(catOf(el)===cat)return true;
463
- if(el.children)for(var i=0;i<el.children.length;i++){if(hasChildCat(el.children[i],cat))return true}
464
- return false
465
- }
466
-
467
- function extractVersion(name){
468
- var m=name.match(/\(v\d+\.\d+\)/);
469
- if(m)return m[0].replace(/[()]/g,"");
470
- m=name.match(/\(([A-D])\)/);
471
- if(m)return "v0.3";
472
- return "其他";
473
- }
474
-
475
- function versionOrder(v){
476
- if(v==="v0.1")return 1;
477
- if(v==="v0.2")return 2;
478
- if(v==="v0.3")return 3;
479
- return 99;
480
- }
481
-
482
- function fmtTime(ts){
483
- if(!ts)return"";
484
- var d=new Date(ts);
485
- var mm=String(d.getMonth()+1).padStart(2,'0');
486
- var dd=String(d.getDate()).padStart(2,'0');
487
- var hh=String(d.getHours()).padStart(2,'0');
488
- var mi=String(d.getMinutes()).padStart(2,'0');
489
- 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
+ }
490
590
  }
491
591
 
492
- function getVersionTitle(ver,children,phaseName){
493
- if(phaseName){
494
- var parts=phaseName.split(/[·;;]/);
495
- for(var i=0;i<parts.length;i++){
496
- if(parts[i].indexOf(ver)!==-1)return parts[i].trim();
497
- }
592
+ // ====== INLINE CATEGORY TABS ======
593
+ function renderInlineTabs() {
594
+ var container = document.getElementById('tree-tabs-inline');
595
+ var categories = ['all','roadmap','todo','decision','spec','convention','ops','archive'];
596
+ var labels = {all:'All',roadmap:'Roadmap',todo:'Todo',decision:'Decisions',spec:'Specs',convention:'Conventions',ops:'Ops',archive:'Archive'};
597
+ container.classList.remove('hidden');
598
+ var h = '';
599
+ for(var i=0;i<categories.length;i++) {
600
+ var c = categories[i];
601
+ var isOn = activeCat===c;
602
+ h += '<button class="px-md py-sm rounded font-title-md text-title-md hover-scale ' +
603
+ (isOn
604
+ ? 'bg-primary-container text-on-primary-container'
605
+ : 'border border-outline-variant text-on-surface-variant hover:bg-surface-container-low') +
606
+ '" data-c="'+c+'">'+labels[c]+'</button>';
498
607
  }
499
- var first=children[0];
500
- if(first){
501
- var nm=first.name.replace(/\s*\([^)]*\)\s*/g,"").replace(/^M\d+:\s*/,"");
502
- 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
+ });
503
617
  }
504
- return ver;
505
618
  }
506
619
 
507
- function getVersionUpdateTime(ver,children){
508
- var latest="";
509
- if(T&&T.events){
510
- for(var i=0;i<T.events.length;i++){
511
- var ev=T.events[i];
512
- if(ev.tags&&ev.tags.indexOf(ver)!==-1){
513
- if(!latest||ev.timestamp>latest)latest=ev.timestamp;
514
- }
620
+ // ====== NAV TABS (top bar — mirror and sync) ======
621
+ function setupNavTabs() {
622
+ var btns = document.querySelectorAll('#nav-tabs button[data-c]');
623
+ for(var i=0;i<btns.length;i++) {
624
+ btns[i].addEventListener('click', function() {
625
+ activeCat = this.getAttribute('data-c');
626
+ renderAll();
627
+ });
628
+ }
629
+ syncNavTabs();
630
+ }
631
+ function syncNavTabs() {
632
+ var navBtns = document.querySelectorAll('#nav-tabs button[data-c]');
633
+ for(var i=0;i<navBtns.length;i++) {
634
+ var btn = navBtns[i];
635
+ if(btn.getAttribute('data-c')===activeCat) {
636
+ btn.className = 'px-md pb-md pt-sm font-title-md text-title-md text-primary border-b-2 border-primary whitespace-nowrap hover-scale';
637
+ } else {
638
+ btn.className = 'px-md pb-md pt-sm font-title-md text-title-md text-on-surface-variant whitespace-nowrap hover:text-on-surface border-b-2 border-transparent hover-scale';
515
639
  }
516
640
  }
517
- if(!latest){
518
- for(var i=0;i<children.length;i++){
519
- var d=children[i].date;
520
- 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';
521
649
  }
522
650
  }
523
- return fmtTime(latest);
524
651
  }
525
652
 
526
- function renderTree(){
653
+ // ====== TREE ======
654
+ function renderTree() {
655
+ syncNavTabs();
527
656
  var els = T.elements||[];
528
- var roadmapEls=[], otherEls=[];
529
- for(var i=0;i<els.length;i++){
530
- var el=els[i];
531
- 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") {
532
665
  roadmapEls.push(el);
533
- }else{
534
- if(activeCat!=="all" && catOf(el)!==activeCat && !hasChildCat(el,activeCat))continue;
666
+ } else {
667
+ if(activeCat!=="all" && catOf(el)!==activeCat && !hasChildCat(el,activeCat)) continue;
535
668
  otherEls.push(el);
536
669
  }
537
670
  }
538
671
 
539
- var total=otherEls.length;
540
- var h="";
541
-
542
- // ===== Roadmap section (路线图与进度) =====
543
- if((activeCat==="all" || activeCat==="roadmap") && roadmapEls.length>0){
544
- h+=renderRoadmap(roadmapEls);
545
- for(var ri=0;ri<roadmapEls.length;ri++){
546
- if(roadmapEls[ri].children)total+=roadmapEls[ri].children.length;
547
- 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
+ }
548
691
  }
549
692
 
550
- // 分支优化 归入 roadmap 板块,收纳所有未关联路线图的事件
551
- var unmatchedCount=countUnmatchedEvents();
552
- var branchNode={
553
- id:"virtual-branch-optimizations",
554
- kind:"primary",elementType:"catch-all",category:"roadmap",
555
- name:"分支优化",status:"in_progress",timeGroup:"current",
556
- timeLabel:"Bug修复 / 功能微调 / 零散优化",
557
- source:{file:"__unmatched__",title:"分支优化"},
558
- summary:"兜底节点:收纳所有未关联到具体路线图里程碑的事件(Bug修复、功能微调、零散优化等)",
559
- tags:["兜底"],children:[],order:999,
560
- date:new Date().toISOString().substring(0,10)
561
- };
562
- h+='<div class="cat-section"><div class="cat-header primary"><span>分支优化</span><span>'+unmatchedCount+' 条事件</span></div>';
563
- h+=renderTreeCard(branchNode);
564
- h+="</div>";
565
- total+=1;
566
- }
567
-
568
- // ===== 待办清单 section — 路线图之后、决策记录之前 =====
569
- if(activeCat==="all"){
570
- var todoNode={
571
- id:"virtual-todo-list",
572
- kind:"primary",elementType:"todo",category:"todo",
573
- name:"待办清单",status:"pending",timeGroup:"future",
574
- timeLabel:"发现但未解决的优化点 / 待办项",
575
- source:{file:"__todo__",title:"待办清单"},
576
- summary:"记录所有已发现但尚未解决的优化点、待办项,避免遗漏",
577
- tags:["待办"],children:[],order:998,
578
- date:new Date().toISOString().substring(0,10)
693
+ // Branch optimizations catch-all
694
+ var unmatchedCount = countUnmatchedEvents();
695
+ var branchNode = {
696
+ id:"virtual-branch-optimizations", kind:"primary", elementType:"catch-all", category:"roadmap",
697
+ name:"Branch Optimizations", status:"in_progress", timeGroup:"current",
698
+ timeLabel:"Bug fixes / micro-tweaks / scattered opts",
699
+ source:{file:"__unmatched__",title:"Branch Optimizations"},
700
+ summary:"Catch-all node: collects all events not linked to a specific roadmap milestone",
701
+ tags:["catch-all"], children:[], order:999,
702
+ date: new Date().toISOString().substring(0,10)
579
703
  };
580
- h+='<div class="cat-section"><div class="cat-header primary"><span>待办清单</span><span>待整理</span></div>';
581
- h+=renderTreeCard(todoNode);
582
- h+="</div>";
583
- total+=1;
704
+ h += '<div class="cat-section"><div class="cat-header primary"><span>Branch Optimizations</span><span>'+unmatchedCount+' events</span></div>';
705
+ h += renderTreeCard(branchNode);
706
+ h += '</div>';
707
+ total += 1;
708
+
709
+ // Todo section
710
+ if(activeCat==="all") {
711
+ var todoNode = {
712
+ id:"virtual-todo-list", kind:"primary", elementType:"todo", category:"todo",
713
+ name:"Todo List", status:"pending", timeGroup:"future",
714
+ timeLabel:"Discovered but unresolved optimization points",
715
+ source:{file:"__todo__",title:"Todo List"},
716
+ summary:"Records all discovered but unresolved optimization points and todo items",
717
+ tags:["todo"], children:[], order:998,
718
+ date: new Date().toISOString().substring(0,10)
719
+ };
720
+ h += '<div class="cat-section"><div class="cat-header primary"><span>Todo List</span><span>Pending</span></div>';
721
+ h += renderTreeCard(todoNode);
722
+ h += '</div>';
723
+ total += 1;
724
+ }
584
725
  }
585
726
 
586
- if(activeCat==="all" || activeCat!=="roadmap"){
587
- var groups={},order=[];
588
- for(var i=0;i<otherEls.length;i++){
589
- var c=catOf(otherEls[i]);
590
- 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); }
591
733
  groups[c].push(otherEls[i]);
592
734
  }
593
- for(var k in groups){
594
- groups[k].sort(function(a,b){
595
- if(a.date&&b.date)return b.date.localeCompare(a.date);
596
- return(b.order||0)-(a.order||0)
597
- });
735
+ for(var k in groups) {
736
+ groups[k].sort(function(a,b) { if(a.date&&b.date) return b.date.localeCompare(a.date); return (b.order||0)-(a.order||0); });
598
737
  }
599
- order.sort(function(a,b){
600
- var pa=PRIMARY[a]?0:1,pb=PRIMARY[b]?0:1;
601
- if(pa!==pb)return pa-pb;
602
- if(a==="unknown")return 1;if(b==="unknown")return -1;
738
+ order.sort(function(a,b) {
739
+ var pa=PRIMARY[a]?0:1, pb=PRIMARY[b]?0:1;
740
+ if(pa!==pb) return pa-pb;
741
+ if(a==="unknown") return 1; if(b==="unknown") return -1;
603
742
  return 0;
604
743
  });
605
-
606
- for(var i=0;i<order.length;i++){
607
- var g=order[i],items=groups[g],label=CLABEL[g]||g,isP=PRIMARY[g];
608
- h+='<div class="cat-section"><div class="cat-header '+(isP?"primary":"secondary")+'"><span>'+esc(label)+'</span><span>'+items.length+' 项</span></div>';
609
- for(var j=0;j<items.length;j++){
610
- h+=renderTreeCard(items[j]);
611
- }
612
- h+="</div>";
744
+ for(var i=0;i<order.length;i++) {
745
+ var g=order[i], items=groups[g], label=CLABEL[g]||g, isP=PRIMARY[g];
746
+ h += '<div class="cat-section"><div class="cat-header '+(isP?"primary":"secondary")+'"><span>'+esc(label)+'</span><span>'+items.length+' items</span></div>';
747
+ for(var j=0;j<items.length;j++) { h += renderTreeCard(items[j]); }
748
+ h += '</div>';
613
749
  }
614
750
  }
615
751
 
616
- if(total===0){document.getElementById("tree").innerHTML='<div class="empty"><h3>暂无内容</h3><p>选择其他分类或运行 build 刷新</p></div>';return}
617
- document.getElementById("cnt").textContent=total+" ";
618
- 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;
619
757
  bindDocClicks();
620
- (function(){
621
- var mores=document.querySelectorAll(".rp-more");
622
- for(var i=0;i<mores.length;i++){
623
- 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) {
624
764
  e.stopPropagation();
625
- var card=this.parentNode.parentNode;
626
- var ver=card.getAttribute("data-ver");
627
- if(ver)expandedRpCards[ver]=true;
765
+ var card = this.parentNode.parentNode;
766
+ var ver = card.getAttribute('data-ver');
767
+ if(ver) expandedRpCards[ver]=true;
628
768
  renderTree();
629
769
  };
630
770
  }
631
771
  })();
632
772
  }
633
773
 
634
- function renderRoadmap(roadmapEls){
635
- var verGroups={},verOrder=[];
636
- 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++) {
637
875
  var el=roadmapEls[i];
638
876
  var children=el.children||[];
639
- for(var j=0;j<children.length;j++){
877
+ for(var j=0;j<children.length;j++) {
640
878
  var ch=children[j];
641
879
  var ver=extractVersion(ch.name);
642
- if(!verGroups[ver]){verGroups[ver]=[];verOrder.push(ver)}
880
+ if(!verGroups[ver]){verGroups[ver]=[];verOrder.push(ver);}
643
881
  verGroups[ver].push(ch);
644
882
  }
645
883
  }
646
884
  verOrder.sort(function(a,b){return versionOrder(a)-versionOrder(b)});
647
-
648
- for(var v in verGroups){
649
- verGroups[v].sort(function(a,b){
650
- var da=a.date||"",db=b.date||"";
651
- return db.localeCompare(da);
652
- });
885
+ for(var v in verGroups) {
886
+ verGroups[v].sort(function(a,b){var da=a.date||"",db=b.date||"";return db.localeCompare(da);});
653
887
  }
654
-
655
888
  var vgList=[];
656
- for(var vi=0;vi<verOrder.length;vi++){
889
+ for(var vi=0;vi<verOrder.length;vi++) {
657
890
  var v=verOrder[vi],g=verGroups[v];
658
891
  vgList.push({ver:v,children:g});
659
892
  }
660
- // Sort by version descending (v0.3 → v0.2 → v0.1)
661
893
  vgList.sort(function(a,b){return versionOrder(b.ver)-versionOrder(a.ver)});
662
-
663
894
  var top3={};
664
- for(var i=0;i<Math.min(3,vgList.length);i++){top3[vgList[i].ver]=true}
895
+ for(var i=0;i<Math.min(3,vgList.length);i++){top3[vgList[i].ver]=true;}
665
896
 
666
- // Derive title parts from phase element name
667
897
  var phaseName="";
668
- for(var i=0;i<roadmapEls.length;i++){
669
- if(roadmapEls[i].elementType==="phase"){phaseName=roadmapEls[i].name;break}
898
+ for(var i=0;i<roadmapEls.length;i++) {
899
+ if(roadmapEls[i].elementType==="phase"){phaseName=roadmapEls[i].name;break;}
670
900
  }
671
901
 
672
902
  var h="";
673
- for(var i=0;i<vgList.length;i++){
903
+ for(var i=0;i<vgList.length;i++) {
674
904
  var vg=vgList[i],children=vg.children,ver=vg.ver;
675
905
  var done=0,total=children.length;
676
906
  for(var j=0;j<children.length;j++){
677
907
  if(children[j].status==="completed"||children[j].status==="cancelled")done++;
678
908
  }
679
909
  var allDone=(done===total);
680
- var ratio=Math.round(done/total*100);
681
-
682
910
  var stateCls=allDone?"rp-done":(done>0?"rp-mixed":"");
683
911
  var ratioCls=allDone?"all-done":(done>0?"partial":"");
684
912
  var ratioText=done+"/"+total;
685
-
686
913
  var isTop3=!!top3[ver];
687
914
  var manualOpen=!!expandedRpCards[ver];
688
915
  var isSemi=isTop3 && !allDone && !manualOpen;
689
916
  var isOpen=manualOpen;
690
-
691
917
  var title=getVersionTitle(ver,children,phaseName);
692
918
  var upTime=getVersionUpdateTime(ver,children);
693
919
 
694
- var isFilterMatch=activeDocFilter && ver===activeDocFilter;
695
- h+='<div class="rp-card '+stateCls+(isSemi?" semi":"")+(isOpen?" open":"")+(isFilterMatch?" filter-match":"")+'" data-ver="'+esc(ver)+'">';
920
+ h+='<div class="rp-card '+stateCls+(isSemi?" semi":"")+(isOpen?" open":"")+'" data-ver="'+esc(ver)+'">';
696
921
  h+='<div class="rp-head">';
697
- h+='<span class="rp-arrow" onclick="toggleRpCard(this);event.stopPropagation()">\u25b6</span>';
698
- 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>';
699
924
  h+='<span class="rp-meta"><span class="rp-ratio '+ratioCls+'">'+ratioText+'</span><span class="tc-time">'+esc(upTime)+'</span></span>';
700
925
  h+='</div><div class="rp-body">';
701
-
702
- if(isSemi){
926
+ if(isSemi) {
703
927
  var incChildren=[],comChildren=[];
704
- for(var j=0;j<children.length;j++){
705
- if(children[j].status==="completed"||children[j].status==="cancelled"){
706
- comChildren.push(children[j]);
707
- }else{
708
- incChildren.push(children[j]);
709
- }
710
- }
711
- for(var j=0;j<incChildren.length;j++){
712
- h+=renderRoadmapChild(incChildren[j]);
713
- }
714
- if(comChildren.length>0){
715
- h+='<div class="rp-more">\u25bc 查看全部 '+total+' 里程碑(含 '+comChildren.length+' 已完成)</div>';
716
- }
717
- }else{
718
- for(var j=0;j<children.length;j++){
719
- h+=renderRoadmapChild(children[j]);
928
+ for(var j=0;j<children.length;j++) {
929
+ if(children[j].status==="completed"||children[j].status==="cancelled") comChildren.push(children[j]);
930
+ else incChildren.push(children[j]);
720
931
  }
932
+ for(var j=0;j<incChildren.length;j++) h+=renderRoadmapChild(incChildren[j]);
933
+ if(comChildren.length>0) h+='<div class="rp-more">&#9660; View all '+total+' milestones ('+comChildren.length+' completed)</div>';
934
+ } else {
935
+ for(var j=0;j<children.length;j++) h+=renderRoadmapChild(children[j]);
721
936
  }
722
- h+="</div></div>";
937
+ h+='</div></div>';
723
938
  }
724
939
  return h;
725
940
  }
726
941
 
727
- function toggleRpCard(el){
942
+ function toggleRpCard(el) {
728
943
  var card=el.parentNode.parentNode;
729
944
  var ver=card.getAttribute("data-ver");
730
- if(expandedRpCards[ver]){expandedRpCards[ver]=false}
731
- else{expandedRpCards[ver]=true}
945
+ if(expandedRpCards[ver]){expandedRpCards[ver]=false;}
946
+ else{expandedRpCards[ver]=true;}
732
947
  renderTree();
733
948
  }
734
949
 
735
- function renderRoadmapChild(ch){
950
+ function renderRoadmapChild(ch) {
736
951
  var done=ch.status==="completed"||ch.status==="cancelled";
737
- var icon=done?"\u2705":(ch.status==="in_progress"?"\u23f3":"\u2b1c");
952
+ var icon=done?"&#x2705;":(ch.status==="in_progress"?"&#x23f3;":"&#x2b1c;");
738
953
  var labelCls=done?"done":(ch.status==="in_progress"?"progress":"pending-label");
739
- var labelText=done?"完成":(ch.status==="in_progress"?"进行中":"待开始");
954
+ var labelText=done?"Done":(ch.status==="in_progress"?"In Progress":"Pending");
740
955
  var time=fmtTime(ch.date)||ch.timeLabel||"";
741
956
  var ver=extractVersion(ch.name);
742
957
  var h='<div class="rp-child" data-ver="'+esc(ver)+'" data-doc="'+(ch.source&&ch.source.file?esc(ch.source.file):"")+'">';
@@ -744,342 +959,613 @@ function renderRoadmapChild(ch){
744
959
  h+='<span class="rp-cname">'+esc(ch.name||ch.id)+'</span>';
745
960
  h+='<span class="rp-ctime">'+esc(time)+'</span>';
746
961
  h+='<span class="rp-clabel '+labelCls+'">'+labelText+'</span>';
747
- h+="</div>";
962
+ h+='</div>';
748
963
  return h;
749
964
  }
750
965
 
751
- 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) {
752
1002
  var isDoc=!!(el.source&&el.source.file);
753
1003
  var tg=normTg(el.timeGroup);
754
1004
  var time=fmtTime(el.date)||el.timeLabel||"";
755
- var unk=catOf(el)==="unknown"?'<span style="background:#fff3e0;color:#e65100;font-size:9px;padding:1px 5px;border-radius:3px">未分类</span>':"";
1005
+ var unk=catOf(el)==="unknown"?'<span style="background:#fff3e0;color:#e65100;font-size:9px;padding:1px 5px;border-radius:3px">unclassified</span>':"";
756
1006
 
757
- // "新建"/"更新" badge for doc-type cards without status
758
1007
  var docBadge="";
759
- if(isDoc || !el.status){
1008
+ if(isDoc || !el.status) {
760
1009
  var elDate=el.date||"",updDate=T&&T.updated?T.updated:"";
761
1010
  var daysOld=99;
762
- if(elDate&&updDate){daysOld=(new Date(updDate)-new Date(elDate))/(86400000)}
763
- if(daysOld<=1)docBadge='<span class="tc-type status-done">新建</span>';
764
- 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>';
765
1014
  }
766
1015
 
767
- var isFilterMatch=activeDocFilter && (isDoc&&el.source.file===activeDocFilter || (el.tags&&el.tags.indexOf(activeDocFilter)!==-1));
1016
+ var isFilterMatch = activeDocFilter && (isDoc&&el.source.file===activeDocFilter || (el.tags&&el.tags.indexOf(activeDocFilter)!==-1));
768
1017
  var h='<div class="tree-card'+(isFilterMatch?" filter-match":"")+'" data-tg="'+esc(tg)+'" data-doc="'+(isDoc?esc(el.source.file):"")+'" onclick="onTreeCardClick(event,\''+esc(isDoc?el.source.file:"")+'\')">';
769
1018
 
770
1019
  h+='<div class="tc-head">';
771
- if(!isDoc && el.status){
772
- h+="<span>"+sicon(el.status)+" "+esc(el.name)+"</span>";
1020
+ if(!isDoc && el.status) {
1021
+ h+='<span>'+sicon(el.status)+' '+esc(el.name)+'</span>';
773
1022
  var stCls=el.status==="completed"?"status-done":(el.status==="in_progress"?"status-progress":"status-pending");
774
- h+='<span class="tc-type '+stCls+'">'+(el.status==="completed"?"完成":(el.status==="in_progress"?"进行中":el.status))+'</span>';
775
- }else{
776
- 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>';
777
1026
  h+='<span class="tc-meta">'+unk+docBadge+'<span class="tc-time">'+esc(time)+'</span></span>';
778
1027
  }
779
- h+="</div>";
1028
+ h+='</div>';
780
1029
 
781
1030
  h+='<div class="tc-meta" style="padding:0 12px 4px">';
782
- if(el.status)h+='<span class="tc-time">'+esc(time)+'</span>';
783
- if(el.tags&&el.tags.length){
784
- for(var i=0;i<el.tags.length;i++){
785
- h+='<span class="tag">'+esc(el.tags[i])+'</span>';
786
- }
1031
+ if(el.status) h+='<span class="tc-time">'+esc(time)+'</span>';
1032
+ if(el.tags&&el.tags.length) {
1033
+ for(var i=0;i<el.tags.length;i++) h+='<span class="tag">'+esc(el.tags[i])+'</span>';
787
1034
  }
788
- h+="</div>";
789
-
790
- if(el.summary)h+='<div class="tc-summary">'+esc(el.summary)+'</div>';
1035
+ h+='</div>';
791
1036
 
792
- if(isDoc){
1037
+ if(el.summary) h+='<div class="tc-summary">'+esc(el.summary)+'</div>';
1038
+ if(isDoc) {
793
1039
  h+='<div class="tc-src"><a href="/'+esc(el.source.file)+'" onclick="preview(event,\''+esc(el.source.file)+'\',\'left\');event.stopPropagation()" title="'+esc(el.source.file)+'">'+esc(el.source.file.substring(0,45))+'</a></div>';
794
1040
  }
795
-
796
- h+="</div>";
1041
+ h+='</div>';
797
1042
  return h;
798
1043
  }
799
1044
 
800
- function setDocFilter(doc){
801
- if(activeDocFilter===doc){activeDocFilter=null}
802
- else{activeDocFilter=doc}
803
- var ft=document.querySelector("#toolbar .ftext");
804
- var fc=document.querySelector("#toolbar .fclear");
805
- if(activeDocFilter){
806
- var label=activeDocFilter==='__unmatched__'?'分支优化(未归类事件)':activeDocFilter==='__todo__'?'待办清单':activeDocFilter;
807
- ft.textContent="筛选: "+label;fc.classList.add("show");
808
- document.body.classList.add("filter-active");
809
- }else{
810
- ft.textContent="";fc.classList.remove("show");
811
- 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');
812
1063
  }
813
1064
  renderTree();
814
1065
  renderEvents();
815
1066
  }
816
1067
 
817
- function onTreeCardClick(e,doc){
818
- if(e.target.closest("a"))return;
819
- if(doc){setDocFilter(doc)}
1068
+ function onTreeCardClick(e, doc) {
1069
+ if(e.target.closest('a')) return;
1070
+ if(doc) { setDocFilter(doc); }
820
1071
  }
821
1072
 
822
- function bindDocClicks(){
823
- var rpChildren=document.querySelectorAll(".rp-child");
824
- for(var i=0;i<rpChildren.length;i++){
825
- 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) {
826
1077
  e.stopPropagation();
827
- var ver=this.getAttribute("data-ver");
828
- if(ver){setDocFilter(ver);return}
829
- var doc=this.getAttribute("data-doc");
830
- if(doc)setDocFilter(doc);
1078
+ var ver = this.getAttribute('data-ver');
1079
+ if(ver) { setDocFilter(ver); return; }
1080
+ var doc = this.getAttribute('data-doc');
1081
+ if(doc) setDocFilter(doc);
831
1082
  });
832
1083
  }
833
1084
  }
834
1085
 
835
- // ====== EVENTS ======
836
- var ETYPES = {'对齐与拍板':['ev-t1','t1'],'规格演进':['ev-t2','t2'],'代码变更':['ev-t3','t3'],'测试与质量':['ev-t4','t4'],'审批与交接':['ev-t5','t5'],'运维与基建':['ev-t6','t6'],'教训沉淀':['ev-t7','t7']};
837
- var PCLASS = {'必记':'ev-p1','应记':'ev-p2','可记':'ev-p3'};
838
-
839
- function evMatchesDoc(ev, doc){
840
- if(!doc)return true;
841
- if(doc==='__unmatched__')return !evHasAnyElementMatch(ev);
842
- if(doc==='__todo__')return ev.tags&&ev.tags.indexOf('待办')!==-1;
843
- if(ev.ref&&ev.ref.doc===doc)return true;
844
- if(ev.tags&&ev.tags.indexOf(doc)!==-1)return true;
845
- if(ev.title&&ev.title.indexOf(doc)!==-1)return true;
846
- return false;
847
- }
848
-
849
- function evHasAnyElementMatch(ev){
850
- if(!T||!T.elements)return false;
851
- for(var i=0;i<T.elements.length;i++){
852
- var el=T.elements[i];
853
- if(el.source&&el.source.file){
854
- if(ev.ref&&ev.ref.doc===el.source.file)return true;
855
- }
856
- var elVer=extractVersion(el.name||'');
857
- if(elVer&&ev.tags&&ev.tags.indexOf(elVer)!==-1)return true;
858
- if(elVer&&ev.title&&ev.title.indexOf(elVer)!==-1)return true;
859
- if(el.children){
860
- for(var j=0;j<el.children.length;j++){
861
- var ch=el.children[j];
862
- if(ch.source&&ch.source.file){
863
- if(ev.ref&&ev.ref.doc===ch.source.file)return true;
864
- }
865
- var chVer=extractVersion(ch.name||'');
866
- if(chVer&&ev.tags&&ev.tags.indexOf(chVer)!==-1)return true;
867
- if(chVer&&ev.title&&ev.title.indexOf(chVer)!==-1)return true;
868
- }
869
- }
1086
+ // ====== TOOLBAR ======
1087
+ function setupToolbar() {
1088
+ var sortBtns = document.querySelectorAll('#toolbar .sort-btn');
1089
+ for(var i=0;i<sortBtns.length;i++) {
1090
+ sortBtns[i].addEventListener('click', function() {
1091
+ for(var j=0;j<sortBtns.length;j++) sortBtns[j].classList.remove('on');
1092
+ this.classList.add('on');
1093
+ sortMode = this.getAttribute('data-mode');
1094
+ renderEvents();
1095
+ });
870
1096
  }
871
- return false;
872
- }
873
-
874
- function countUnmatchedEvents(){
875
- if(!T||!T.events)return 0;
876
- var c=0;
877
- for(var i=0;i<T.events.length;i++){
878
- 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
+ });
879
1109
  }
880
- return c;
881
1110
  }
882
1111
 
883
- function renderSingleEv(ev){
884
- var tinfo=ETYPES[ev.type]||['ev-t3','t3'];
885
- var refs='';
886
- if(ev.ref){
887
- if(ev.ref.commit)refs+='<code style="font-size:10px;background:#f5f5f5;padding:0 3px;border-radius:2px">'+esc(ev.ref.commit)+'</code> ';
888
- 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> ';
889
1119
  }
890
- var tags='';
891
- 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>';
892
- var time=fmtTime(ev.timestamp||'');
893
-
1120
+ var tags = '';
1121
+ if(ev.tags&&ev.tags.length) {
1122
+ for(var ti=0;ti<ev.tags.length;ti++) {
1123
+ tags += '<span class="ev-tag" data-tag="'+esc(ev.tags[ti])+'" onclick="setTagFilter(\''+esc(ev.tags[ti])+'\');event.stopPropagation()">'+esc(ev.tags[ti])+'</span>';
1124
+ }
1125
+ }
1126
+ var time = fmtTime(ev.timestamp||'');
894
1127
  return '<div class="ev-item '+tinfo[1]+'">'+
895
1128
  '<div class="ev-head"><span class="ev-title">'+esc(ev.title)+'</span><span class="ev-type '+tinfo[0]+'">'+esc(ev.type||'')+'</span></div>'+
896
1129
  '<div class="ev-meta">'+(time?'<span class="ev-time">'+esc(time)+'</span>':'')+refs+tags+'</div>'+
897
1130
  '</div>';
898
1131
  }
899
1132
 
900
- function renderEvents(){
901
- var container=document.getElementById("events");
902
- var evs=T?T.events||[]:[];
903
- if(!evs.length){container.innerHTML="<div class=\"empty\"><h3>暂无事件</h3><p>运行 build 后 git log 事件将显示在这里</p></div>";renderTagBar();return}
904
- // Apply doc filter (matches ref.doc OR tags)
905
- 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
+ }
906
1173
  // Apply tag filter
907
- if(activeTagFilter){var f=[];for(var i=0;i<evs.length;i++){if(evHasTag(evs[i],activeTagFilter))f.push(evs[i])}evs=f}
908
- 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
+
909
1185
  // Build doc name map
910
1186
  var docNameMap={};
911
- 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}}
912
- // Flat mode: all events sorted by timestamp, no grouping
913
- if(sortMode==='flat'){
914
- evs.sort(function(a,b){return (b.timestamp||"").localeCompare(a.timestamp||"")});
915
- var h="",curDay="";
916
- 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++) {
917
1199
  var dk3=(evs[i].timestamp||"").substring(0,10);
918
- 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>'; }
919
1201
  h+=renderSingleEv(evs[i]);
920
1202
  }
921
- container.innerHTML=h;
1203
+ container.innerHTML = h;
1204
+ renderTagBar();
1205
+ document.getElementById('evcnt').textContent = evs.length+' events';
1206
+ return;
1207
+ }
1208
+
1209
+ // TRACK mode: group events by guessed track, then within track by thread
1210
+ if(sortMode==='track' && R && R.tracks && R.tracks.length) {
1211
+ var trackGroups = {};
1212
+ var untracked = [];
1213
+ for(var i=0;i<evs.length;i++) {
1214
+ var tk = guessEventTrack(evs[i]);
1215
+ if(tk) {
1216
+ if(!trackGroups[tk]) trackGroups[tk] = [];
1217
+ trackGroups[tk].push(evs[i]);
1218
+ } else {
1219
+ untracked.push(evs[i]);
1220
+ }
1221
+ }
1222
+ var h = '';
1223
+ var trackOrder = [];
1224
+ for(var i=0;i<R.tracks.length;i++) { trackOrder.push(R.tracks[i].id); }
1225
+ for(var ti=0;ti<trackOrder.length;ti++) {
1226
+ var tid = trackOrder[ti];
1227
+ var tevs = trackGroups[tid]||[];
1228
+ if(!tevs.length) continue;
1229
+ h += renderTrackEventGroup(tid, tevs, docNameMap);
1230
+ }
1231
+ if(untracked.length) {
1232
+ h += renderTrackEventGroup('Other', untracked, docNameMap);
1233
+ }
1234
+ container.innerHTML = h;
922
1235
  renderTagBar();
923
- document.getElementById("evcnt").textContent=evs.length+" 条";
1236
+ document.getElementById('evcnt').textContent = evs.length+' events';
924
1237
  return;
925
1238
  }
926
- // Grouped mode: merge all events by thread/doc across ALL dates (no daily headers)
927
- var docThreads={},orphanThreads={},singles=[];
928
- 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)}}
929
- for(var d in docThreads){docThreads[d].sort(function(a,b){return (b.timestamp||"").localeCompare(a.timestamp||"")})}
930
- for(var t in orphanThreads){orphanThreads[t].sort(function(a,b){return (b.timestamp||"").localeCompare(a.timestamp||"")})}
931
- singles.sort(function(a,b){return (b.timestamp||"").localeCompare(a.timestamp||"")});
932
- // Merge doc + orphan threads into one list sorted by latest event timestamp
1239
+
1240
+ // GROUPED mode (default): merge events by thread/doc
1241
+ var docThreads={}, orphanThreads={}, singles=[];
1242
+ for(var i=0;i<evs.length;i++) {
1243
+ var ev=evs[i];
1244
+ if(ev.ref&&ev.ref.doc) {
1245
+ if(!docThreads[ev.ref.doc]) docThreads[ev.ref.doc]=[];
1246
+ docThreads[ev.ref.doc].push(ev);
1247
+ } else if(ev.threadId) {
1248
+ if(!orphanThreads[ev.threadId]) orphanThreads[ev.threadId]=[];
1249
+ orphanThreads[ev.threadId].push(ev);
1250
+ } else {
1251
+ singles.push(ev);
1252
+ }
1253
+ }
1254
+ for(var d in docThreads) { docThreads[d].sort(function(a,b){ return (b.timestamp||"").localeCompare(a.timestamp||""); }); }
1255
+ for(var t in orphanThreads) { orphanThreads[t].sort(function(a,b){ return (b.timestamp||"").localeCompare(a.timestamp||""); }); }
1256
+ singles.sort(function(a,b){ return (b.timestamp||"").localeCompare(a.timestamp||""); });
1257
+
933
1258
  var merged=[];
934
- for(var doc in docThreads){merged.push({tk:"doc:"+doc,te:docThreads[doc],title:docNameMap[doc]||doc.replace(/^.*[\\\/]/,"").replace(/\.md$/i,""),dataDoc:doc,ts:docThreads[doc][0].timestamp})}
935
- for(var tid in orphanThreads){merged.push({tk:"tid:"+tid,te:orphanThreads[tid],title:"线程: "+tid,dataDoc:null,ts:orphanThreads[tid][0].timestamp})}
936
- merged.sort(function(a,b){return (b.ts||"").localeCompare(a.ts||"")});
937
- // 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
+
938
1267
  var top3={};
939
- for(var ti=0;ti<Math.min(3,merged.length);ti++){top3[merged[ti].tk]=true}
940
- // Render thread card helper
941
- function rtc(tk,te,title,dataDoc,isSemi){
942
- var tc2={};for(var i=0;i<te.length;i++){var tt=te[i].type;tc2[tt]=(tc2[tt]||0)+1}
943
- var ts=[];for(var tk2 in tc2){ts.push(tk2+"\u00d7"+tc2[tk2])}
944
- var manualOpen=!!expandedThreads[tk];var cls=(isSemi&&!manualOpen)?"semi":(manualOpen?"open":"");if(manualOpen)expandedThreads[tk]=true;
945
- var r="<div class=\"th-card "+cls+"\" data-thread=\""+esc(tk)+"\""+(dataDoc?" data-doc=\""+esc(dataDoc)+"\"":"")+">";
946
- r+="<div class=\"th-head\" onclick=\"toggleThread(this)\">";
947
- r+="<span class=\"th-arrow\">\u25b6</span>";
948
- r+="<span class=\"th-title\">"+esc(title)+"</span>";
949
- r+="<span class=\"th-count\">"+te.length+" \u6761</span>";
950
- r+="</div><div class=\"th-body\">";
951
- r+="<div class=\"th-typebar\">"+esc(ts.join(" \u00b7 "))+"</div>";
952
- 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) {
953
1285
  r+=renderSingleEv(te[0]);
954
- 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>";
955
- }else{
956
- for(var i=0;i<te.length;i++)r+=renderSingleEv(te[i]);
957
- 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>';
958
1290
  }
959
- r+="</div></div>";return r;
1291
+ r+='</div></div>';
1292
+ return r;
960
1293
  }
961
- // Render: threads first, then orphan singles
962
- var h="";
963
- for(var mi=0;mi<merged.length;mi++){var m=merged[mi];h+=rtc(m.tk,m.te,m.title,m.dataDoc,!!top3[m.tk])}
964
- for(var ui=0;ui<singles.length;ui++)h+=renderSingleEv(singles[ui]);
965
- container.innerHTML=h;
966
- if(activeDocFilter){
967
- var mc=container.querySelector(".th-card[data-doc=\""+CSS.escape(activeDocFilter)+"\"]");
968
- 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'}); }
969
1303
  }
970
1304
  renderTagBar();
971
- document.getElementById("evcnt").textContent=evs.length+" 条";
1305
+ document.getElementById('evcnt').textContent = evs.length+' events';
972
1306
  }
973
1307
 
1308
+ function renderTrackEventGroup(trackId, tevs, docNameMap) {
1309
+ // Within a track, group by thread/doc
1310
+ var docThreads={}, singles=[];
1311
+ for(var i=0;i<tevs.length;i++) {
1312
+ var ev=tevs[i];
1313
+ if(ev.ref&&ev.ref.doc) {
1314
+ if(!docThreads[ev.ref.doc]) docThreads[ev.ref.doc]=[];
1315
+ docThreads[ev.ref.doc].push(ev);
1316
+ } else {
1317
+ singles.push(ev);
1318
+ }
1319
+ }
1320
+ for(var d in docThreads) { docThreads[d].sort(function(a,b){ return (b.timestamp||"").localeCompare(a.timestamp||""); }); }
1321
+ singles.sort(function(a,b){ return (b.timestamp||"").localeCompare(a.timestamp||""); });
1322
+
1323
+ // Get track label
1324
+ var trackLabel = trackId;
1325
+ if(R&&R.tracks) {
1326
+ for(var i=0;i<R.tracks.length;i++) {
1327
+ if(R.tracks[i].id===trackId) { trackLabel = R.tracks[i].label||trackId; break; }
1328
+ }
1329
+ }
974
1330
 
975
- function toggleThread(headEl){
976
- var card=headEl.parentNode;
977
- if(card.classList.contains('open')){
978
- card.classList.remove('open');
979
- }else{
980
- 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>';
981
1376
  }
1377
+ h += '</div></div>';
1378
+ return h;
982
1379
  }
983
1380
 
984
- function expandSemi(el){
985
- var card=el.parentNode.parentNode;
986
- var tk=card.getAttribute("data-thread");
987
- if(tk)expandedThreads[tk]=true;
988
- card.classList.remove("semi");
989
- 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');
990
1393
  renderEvents();
991
1394
  }
1395
+ function collapseThread(el) {
1396
+ var card = el.parentNode.parentNode;
1397
+ card.classList.remove('open');
1398
+ }
992
1399
 
993
- function collapseThread(el){
994
- var card=el.parentNode.parentNode;
995
- 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;
996
1485
  }
997
1486
 
998
1487
  // ====== FLOATING PREVIEW ======
999
- function preview(e,path,from){
1488
+ function preview(e, path, from) {
1000
1489
  e.preventDefault();
1001
- var float=document.getElementById('float'),overlay=document.getElementById('overlay');
1002
- var needFr=(from==='left'); // tree → fr (right:0), events → no fr (left:0)
1003
- var hasFr=float.classList.contains('fr');
1004
-
1005
- document.getElementById('ftitle').textContent=path;
1006
- document.getElementById('float-body').textContent='加载中...';
1007
- fetch('/'+encodeURI(path)).then(function(r){if(!r.ok)throw Error('HTTP '+r.status);return r.text()}).then(function(t){
1008
- t=t.replace(/<!--[\s\S]*?-->/g,'').trim();
1009
- document.getElementById('float-body').textContent=t||'(空文件)';
1010
- }).catch(function(err){document.getElementById('float-body').textContent='加载失败:'+err.message});
1011
-
1012
- // If switching sides, hide first to avoid position jump
1013
- 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) {
1014
1507
  float.classList.add('hide');
1015
- setTimeout(function(){
1016
- if(needFr){float.classList.add('fr')}else{float.classList.remove('fr')}
1508
+ setTimeout(function() {
1509
+ if(needFr) { float.classList.add('fr'); } else { float.classList.remove('fr'); }
1017
1510
  float.classList.remove('hide');
1018
- },300);
1019
- }else{
1020
- if(needFr){float.classList.add('fr')}else{float.classList.remove('fr')}
1511
+ }, 300);
1512
+ } else {
1513
+ if(needFr) { float.classList.add('fr'); } else { float.classList.remove('fr'); }
1021
1514
  float.classList.remove('hide');
1022
1515
  }
1023
1516
  overlay.classList.remove('hide');
1024
1517
  }
1025
1518
 
1026
- document.getElementById('float-close').addEventListener('click',function(){
1519
+ document.getElementById('float-close').addEventListener('click', function() {
1027
1520
  document.getElementById('float').classList.add('hide');
1028
1521
  document.getElementById('overlay').classList.add('hide');
1029
1522
  });
1030
- document.getElementById('overlay').addEventListener('click',function(){
1523
+ document.getElementById('overlay').addEventListener('click', function() {
1031
1524
  document.getElementById('float').classList.add('hide');
1032
1525
  document.getElementById('overlay').classList.add('hide');
1033
1526
  });
1034
1527
 
1035
1528
  // ====== TOKEN PANEL ======
1036
- function loadTokenToday(){
1037
- fetch('../data/token-summary.json').then(function(r){return r.json()}).then(function(d){
1529
+ function loadTokenToday() {
1530
+ fetch('../data/token-summary.json').then(function(r) { return r.json(); }).then(function(d) {
1038
1531
  renderTokenToday(d);
1039
- }).catch(function(){
1040
- document.getElementById('token-today').childNodes[0].textContent='工具token:--';
1532
+ }).catch(function() {
1533
+ document.getElementById('token-today').childNodes[0].textContent = 'Usage: --';
1041
1534
  });
1042
1535
  }
1043
1536
 
1044
- function renderTokenToday(d){
1045
- var today=new Date().toISOString().substring(0,10);
1046
- var td=d&&d.byDate?d.byDate[today]:null;
1047
- var todayTokens=td?td.tokens:0;
1048
- var todayCost=0;
1049
- if(td&&d.bySkill){
1050
- // Estimate today's cost from skills if available
1051
- todayCost=d.totalCost||0;
1052
- }
1053
-
1054
- var label=document.getElementById('token-today');
1055
- label.childNodes[0].textContent='工具token:'+(todayTokens?todayTokens.toLocaleString()+' tk':'--');
1056
-
1057
- // Build tooltip content
1058
- var tip=document.getElementById('token-tip');
1059
- var h='';
1060
- if(d&&d.totalTokens>0){
1061
- h+='<div class="tip-row"><span class="tip-name">今日</span><span class="tip-val">'+(todayTokens?todayTokens.toLocaleString()+' tk':'--')+'</span></div>';
1062
- h+='<div class="tip-row"><span class="tip-name">累计</span><span class="tip-val">'+d.totalTokens.toLocaleString()+' tk</span></div>';
1063
- h+='<div class="tip-row"><span class="tip-name">估算费用</span><span class="tip-val">¥'+(d.totalCost||0).toFixed(3)+'</span></div>';
1064
- if(d.bySkill){
1537
+ function renderTokenToday(d) {
1538
+ var today = new Date().toISOString().substring(0,10);
1539
+ var td = d&&d.byDate ? d.byDate[today] : null;
1540
+ var todayTokens = td ? td.tokens : 0;
1541
+
1542
+ var label = document.getElementById('token-today');
1543
+ label.childNodes[0].textContent = 'Usage: '+(todayTokens ? todayTokens.toLocaleString()+' tk' : '--');
1544
+
1545
+ var tip = document.getElementById('token-tip');
1546
+ var h = '';
1547
+ if(d&&d.totalTokens>0) {
1548
+ h += '<div class="tip-row"><span class="tip-name">Today</span><span class="tip-val">'+(todayTokens?todayTokens.toLocaleString()+' tk':'--')+'</span></div>';
1549
+ h += '<div class="tip-row"><span class="tip-name">Total</span><span class="tip-val">'+d.totalTokens.toLocaleString()+' tk</span></div>';
1550
+ h += '<div class="tip-row"><span class="tip-name">Est. Cost</span><span class="tip-val">$'+(d.totalCost||0).toFixed(3)+'</span></div>';
1551
+ if(d.bySkill) {
1065
1552
  var skills=[];
1066
- for(var k in d.bySkill){skills.push({name:k,tokens:d.bySkill[k].tokens||0,calls:d.bySkill[k].calls||0})}
1067
- skills.sort(function(a,b){return b.tokens-a.tokens});
1068
- if(skills.length>0){
1069
- h+='<div class="tip-divider"></div>';
1070
- for(var i=0;i<skills.length;i++){
1071
- h+='<div class="tip-row"><span class="tip-name">'+esc(skills[i].name)+'</span><span class="tip-val">'+skills[i].tokens.toLocaleString()+' tk ×'+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>';
1072
1559
  }
1073
1560
  }
1074
1561
  }
1075
- }else{
1076
- 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>';
1077
1564
  }
1078
- tip.innerHTML=h;
1565
+ tip.innerHTML = h;
1079
1566
 
1080
- // Hover toggling
1081
- label.onmouseenter=function(){tip.classList.add('show')};
1082
- label.onmouseleave=function(){tip.classList.remove('show')};
1567
+ label.onmouseenter = function() { tip.classList.add('show'); };
1568
+ label.onmouseleave = function() { tip.classList.remove('show'); };
1083
1569
  }
1084
1570
  </script>
1085
1571
  </body>