返回 DeepSeek-Reasonix
shell.ts
根目录 / workers / crash-report / src / shell.ts
1 // Shared page shell + UI kit. Visual language mirrors site/src/styles/global.css
2 // — white, blue accent, large radii; crash stacks use the site's dark terminal.
3
4 export function esc(s: unknown): string {
5 return String(s).replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
6 }
7
8 const LOGO = `<svg viewBox="0 0 64 64" class="logo"><defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#4f9dff"/><stop offset=".55" stop-color="#7a6bff"/><stop offset="1" stop-color="#c46bff"/></linearGradient></defs><rect width="64" height="64" rx="15" fill="url(#g)"/><text x="32" y="46" font-family="Inter,Segoe UI,Arial,sans-serif" font-size="42" font-weight="800" fill="#fff" text-anchor="middle">R</text></svg>`;
9
10 const CSS = `
11 :root{--accent:oklch(0.55 0.17 257);--accent-soft:color-mix(in oklch,var(--accent) 8%,white);
12 --bg-soft:oklch(0.976 0.0025 247);--ink:oklch(0.25 0.006 260);--ink-2:oklch(0.47 0.008 260);
13 --ink-3:oklch(0.6 0.008 260);--line:oklch(0.925 0.004 247);--term-bg:oklch(0.25 0.006 260);
14 --shadow-1:0 1px 2px oklch(0.35 0.01 260/0.18),0 1px 3px 1px oklch(0.35 0.01 260/0.08);
15 --sans:"Outfit","Helvetica Neue","PingFang SC","Microsoft YaHei",sans-serif;
16 --mono:"JetBrains Mono","SF Mono",Menlo,Consolas,monospace}
17 *{margin:0;padding:0;box-sizing:border-box}
18 html{scroll-behavior:smooth}
19 body{font-family:var(--sans);background:#fff;color:var(--ink);-webkit-font-smoothing:antialiased}
20 .wrap{max-width:1060px;margin:0 auto;padding:0 28px 64px}
21 header{display:flex;align-items:center;gap:10px;height:68px}
22 .logo{width:28px;height:28px;border-radius:8px}
23 header .brand{font-size:17px;font-weight:600;color:var(--ink);text-decoration:none}
24 header .crumb{color:var(--ink-3);font-size:15px}
25 header .nav{display:flex;align-items:center;gap:14px}
26 .navlink{color:var(--accent);text-decoration:none;font-size:14px;font-weight:500}
27 .navlink:hover{text-decoration:underline}
28 .site-nav{display:flex;align-items:center;gap:6px;margin:4px 0 10px}
29 .site-nav a{display:inline-flex;align-items:center;min-height:34px;padding:7px 13px;border-radius:999px;color:var(--ink-2);text-decoration:none;font-size:14px;font-weight:600}
30 .site-nav a:hover{background:var(--bg-soft);color:var(--ink)}
31 .site-nav a.active,.site-nav a[aria-current="page"]{background:var(--accent-soft);color:var(--accent)}
32 .chip{display:inline-flex;align-items:center;gap:8px;font-size:13.5px;color:var(--ink-2)}
33 .lang-switch{margin-left:auto;display:inline-flex;align-items:center;gap:2px;padding:3px;border:1px solid var(--line);border-radius:12px;background:var(--bg-soft)}
34 .lang-switch button{font:inherit;font-size:12.5px;font-weight:700;color:var(--ink-2);background:transparent;border:0;border-radius:9px;padding:5px 10px;cursor:pointer}
35 .lang-switch button.active{background:#fff;color:var(--accent);box-shadow:var(--shadow-1)}
36 body[data-lang="en"] [data-i18n="zh"],body[data-lang="zh"] [data-i18n="en"]{display:none!important}
37 .badge{font-size:11px;font-weight:600;letter-spacing:.02em;text-transform:uppercase;padding:2px 8px;border-radius:999px}
38 .badge.pending{background:oklch(0.95 0.04 80);color:oklch(0.5 0.12 80)}
39 .badge.viewer{background:var(--accent-soft);color:var(--accent)}
40 .badge.admin{background:oklch(0.95 0.05 150);color:oklch(0.48 0.13 150)}
41 h1{font-size:30px;font-weight:600;letter-spacing:-0.02em;margin:18px 0 6px}
42 .sub{color:var(--ink-2);font-size:14.5px;margin-bottom:28px}
43 .hero-line{display:flex;align-items:flex-start;justify-content:space-between;gap:18px;margin-top:6px}
44 .hero-line .sub{margin-bottom:18px}
45 .segmented{display:inline-flex;align-items:center;gap:3px;padding:3px;border:1px solid var(--line);border-radius:13px;background:var(--bg-soft);white-space:nowrap}
46 .segmented a{display:inline-flex;align-items:center;justify-content:center;min-height:30px;padding:6px 11px;border-radius:10px;color:var(--ink-2);text-decoration:none;font-size:12.5px;font-weight:700}
47 .segmented a.active,.segmented a[aria-current="true"]{background:#fff;color:var(--accent);box-shadow:var(--shadow-1)}
48 .overview-grid{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:10px;margin:0 0 20px}
49 .overview-card{border:1px solid var(--line);border-radius:16px;background:linear-gradient(180deg,#fff,var(--bg-soft));padding:14px 15px;min-width:0;text-decoration:none}
50 .overview-card:hover{border-color:color-mix(in oklch,var(--accent) 36%,var(--line));transform:translateY(-1px)}
51 .overview-card span,.overview-card small{display:block;color:var(--ink-3);font-size:12.5px;line-height:1.3}
52 .overview-card strong{display:block;color:var(--ink);font-family:var(--mono);font-size:24px;line-height:1.15;margin:6px 0 4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
53 .overview-card.good{border-color:oklch(0.88 0.05 150);background:oklch(0.985 0.015 150)}
54 .overview-card.warn{border-color:oklch(0.9 0.07 80);background:oklch(0.985 0.018 80)}
55 .overview-card.bad{border-color:oklch(0.89 0.06 25);background:oklch(0.985 0.016 25)}
56 .module-nav{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin:0 0 20px}
57 .module-link{display:grid;grid-template-columns:1fr auto;grid-template-areas:"label value" "note note";gap:4px 10px;border:1px solid var(--line);border-radius:16px;background:#fff;color:var(--ink);text-decoration:none;padding:13px 14px;box-shadow:var(--shadow-1)}
58 .module-link:hover{border-color:color-mix(in oklch,var(--accent) 36%,var(--line));background:var(--accent-soft)}
59 .module-link.active,.module-link[aria-current="page"]{border-color:color-mix(in oklch,var(--accent) 45%,var(--line));background:var(--accent-soft)}
60 .module-link span{grid-area:label;color:var(--ink-2);font-size:13px;font-weight:700}
61 .module-link b{grid-area:value;font-family:var(--mono);font-size:13px;color:var(--accent)}
62 .module-link small{grid-area:note;color:var(--ink-3);font-size:12.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
63 .grid{display:grid;grid-template-columns:1fr 1fr;gap:20px}
64 .card{background:#fff;border:1px solid var(--line);border-radius:24px;padding:24px 28px;box-shadow:var(--shadow-1)}
65 .card.full{grid-column:1/-1}
66 .card h2{font-size:15px;font-weight:600;color:var(--ink-2);margin-bottom:16px}
67 .card h2 b{color:var(--ink)}
68 .module-card{scroll-margin-top:18px}
69 .module-card+.module-card{margin-top:2px}
70 .module-head{display:flex;align-items:center;justify-content:space-between;gap:14px;margin-bottom:16px}
71 .module-head span{display:block;font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:var(--ink-3);font-weight:800;margin-bottom:4px}
72 .module-head h2{margin:0;color:var(--ink);font-size:19px}
73 .module-actions{display:flex;align-items:center;justify-content:flex-end;gap:10px;flex-wrap:wrap}
74 .module-action{color:var(--accent);text-decoration:none;font-size:13.5px;font-weight:700;white-space:nowrap}
75 .module-action:hover{text-decoration:underline}
76 .module-panel{border:1px solid var(--line);border-radius:18px;background:var(--bg-soft);padding:16px 18px;margin-top:12px;min-width:0}
77 .module-panel.wide{margin-top:0}
78 .module-panel h3{font-size:13px;font-weight:700;color:var(--ink-2);margin:0 0 12px}
79 .module-panel h3 b{color:var(--ink)}
80 .module-split{display:grid;grid-template-columns:1fr 1fr;gap:12px;align-items:start}
81 .module-split.wide{grid-template-columns:minmax(0,1fr) minmax(0,1fr)}
82 .card-title-row{display:flex;align-items:center;justify-content:space-between;gap:14px;margin-bottom:16px}
83 .card-title-row h2{margin:0}
84 .empty{color:var(--ink-3);font-size:14px;padding:14px 0}
85 svg.chart{width:100%;height:auto;display:block}
86 .bars-list{min-width:0}
87 .row{display:grid;grid-template-columns:minmax(0,1.15fr) minmax(76px,1fr) minmax(3.8em,max-content);align-items:center;gap:12px;padding:7px 0;font-size:14px;min-width:0}
88 .row+.row{border-top:1px solid var(--line)}
89 .row-label{display:flex;align-items:center;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
90 .row-bar{min-width:0}
91 .row .bar{height:10px;border-radius:999px;background:linear-gradient(90deg,#4f9dff,#7a6bff);min-width:4px}
92 .row .n{text-align:right;font-family:var(--mono);font-size:13px;color:var(--ink-2);white-space:nowrap}
93 .bucket-prefix{display:inline-flex;align-items:center;margin-right:6px;padding:1px 6px;border-radius:8px;background:var(--accent-soft);color:var(--accent);font-size:12px;font-weight:700}
94 .bucket-main{display:inline-block;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
95 .bars-more{margin-top:6px}
96 .bars-more summary{display:inline-flex;align-items:center;min-height:30px;padding:5px 9px;border:1px dashed var(--line);border-radius:10px;background:#fff;color:var(--accent);font-size:12.5px;font-weight:700;cursor:pointer;list-style:none}
97 .bars-more summary::-webkit-details-marker{display:none}
98 .bars-more summary:hover{border-color:color-mix(in oklch,var(--accent) 34%,var(--line))}
99 .bars-more .more-open{display:none}
100 .bars-more[open] .more-closed{display:none}
101 .bars-more[open] .more-open{display:inline}
102 .bars-more-list{max-height:260px;overflow-y:auto;overflow-x:hidden;margin-top:7px;padding:5px 8px;border:1px solid var(--line);border-radius:12px;background:#fff}
103 table{border-collapse:collapse;width:100%;font-size:14px}
104 th{text-align:left;color:var(--ink-3);font-weight:500;font-size:12.5px;padding:0 14px 8px 0}
105 td{padding:8px 14px 8px 0;border-top:1px solid var(--line);vertical-align:middle}
106 td.n{font-family:var(--mono);font-size:13px}
107 td.summary{max-width:480px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-family:var(--mono);font-size:12.5px}
108 .reg-table{table-layout:fixed}
109 .reg-table .c-pkg{width:32%}.reg-table .c-kind{width:8%}.reg-table .c-pub{width:14%}
110 .reg-table .c-ver{width:16%}.reg-table .c-sub{width:10%}.reg-table .c-act{width:20%}
111 .rowlinks{display:flex;align-items:center;flex-wrap:wrap;gap:8px;margin-top:6px}
112 .muted{color:var(--ink-3)}
113 p.summary{font-family:var(--mono);font-size:14px;margin:2px 0 6px;word-break:break-word}
114 a.fp{font-family:var(--mono);font-size:13px;color:var(--accent);text-decoration:none}
115 a.fp:hover{text-decoration:underline}
116 .pill{display:inline-block;font-size:12px;font-weight:500;padding:2px 10px;border-radius:999px;background:var(--accent-soft);color:var(--accent)}
117 .pill.crash{background:oklch(0.95 0.03 25);color:oklch(0.55 0.18 25)}
118 .pill.open{background:oklch(0.96 0.025 250);color:var(--ink-2)}
119 .pill.resolved{background:oklch(0.95 0.05 150);color:oklch(0.48 0.13 150)}
120 .pill.ignored{background:var(--bg-soft);color:var(--ink-3)}
121 .back{display:inline-block;margin:18px 0 0;color:var(--accent);text-decoration:none;font-size:14px}
122 .report{margin-top:20px}
123 .report .meta{display:flex;flex-wrap:wrap;gap:8px 18px;font-size:13px;color:var(--ink-2);margin-bottom:10px}
124 .report .meta b{color:var(--ink);font-weight:500}
125 pre{background:var(--term-bg);color:#e6e8f0;border-radius:12px;padding:16px 18px;font-family:var(--mono);
126 font-size:12.5px;line-height:1.55;overflow:auto;white-space:pre-wrap;word-break:break-word}
127 .metrics{display:grid;grid-template-columns:1fr 1fr;gap:8px 28px}
128 .metric-block h3{display:flex;align-items:center;justify-content:space-between;gap:8px;font-family:var(--mono);font-size:12.5px;font-weight:600;color:var(--ink-2);margin:6px 0 4px}
129 .metric-block h3 span{font-family:var(--mono);font-size:11px;color:var(--ink-3);font-weight:600}
130 .preference-dashboard{display:flex;flex-direction:column;gap:14px}
131 .preference-compare{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;align-items:start}
132 .preference-panel{background:#fff}
133 .preference-panel.active{border-color:color-mix(in oklch,var(--accent) 42%,var(--line));box-shadow:0 0 0 3px var(--accent-soft)}
134 .pref-section{border:1px solid var(--line);border-radius:14px;background:#fff;padding:14px 15px}
135 .pref-section>h3,.pref-section summary h3{display:flex;align-items:center;justify-content:space-between;gap:12px;font-size:13px;font-weight:700;color:var(--ink);margin:0 0 10px}
136 .pref-section>h3 span,.pref-section summary h3 span{font-size:11.5px;color:var(--ink-3);font-weight:700;white-space:nowrap}
137 .pref-section summary{cursor:pointer;list-style:none}
138 .pref-section summary::-webkit-details-marker{display:none}
139 .pref-section summary h3{margin:0}
140 .pref-section summary h3::after{content:"+";font-family:var(--mono);font-size:13px;color:var(--accent);line-height:1}
141 .pref-section[open] summary h3::after{content:"-"}
142 .pref-section[open] summary h3{margin-bottom:10px}
143 .pref-section-collapsed summary:hover h3{color:var(--accent)}
144 .pref-section .metric-block h3{color:var(--ink-3)}
145 .pref-metrics{grid-template-columns:1fr;gap:12px}
146 .pref-metrics .metric-block{min-width:0}
147 .pref-metrics .row{grid-template-columns:minmax(7rem,1.2fr) minmax(5rem,.9fr) minmax(4.4em,max-content)}
148 .health-grid{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:12px}
149 .health-card{border:1px solid var(--line);border-radius:16px;background:var(--bg-soft);padding:14px 15px;min-width:0}
150 .health-card.good{background:oklch(0.985 0.015 150);border-color:oklch(0.88 0.05 150)}
151 .health-card.warn{background:oklch(0.985 0.018 80);border-color:oklch(0.9 0.07 80)}
152 .health-card.bad{background:oklch(0.985 0.016 25);border-color:oklch(0.89 0.06 25)}
153 .health-top{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px}
154 .health-top span{font-size:12.5px;color:var(--ink-2);font-weight:700}
155 .health-top b{font-size:11px;text-transform:uppercase;color:var(--ink-3);font-weight:800}
156 .health-card strong{display:block;font-family:var(--mono);font-size:22px;color:var(--ink);margin-bottom:4px}
157 .health-card small{display:block;color:var(--ink-3);font-family:var(--mono);font-size:11.5px;margin-bottom:9px}
158 .health-card p{color:var(--ink-2);font-size:12.5px;line-height:1.35;word-break:break-word}
159 .filter-card{margin-top:12px;padding:16px 18px;border:1px solid var(--line);border-radius:18px;background:#fff;position:sticky;top:10px;z-index:2}
160 .filter-head{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-bottom:14px}
161 .filter-head h2{margin:0}
162 .filter-head span{font-family:var(--mono);font-size:12.5px;color:var(--ink-3);white-space:nowrap}
163 .filter-tabs{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:18px}
164 .filter-tab{display:inline-flex;align-items:center;min-height:34px;padding:7px 13px;border-radius:12px;border:1px solid var(--line);
165 background:var(--bg-soft);color:var(--ink-2);text-decoration:none;font-size:13.5px;font-weight:600}
166 .filter-tab:hover{border-color:color-mix(in oklch,var(--accent) 34%,var(--line));color:var(--accent)}
167 .filter-tab.active{background:var(--accent);border-color:var(--accent);color:#fff}
168 .facet-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:16px}
169 .facet-grid section{min-width:0}
170 .facet-grid h3{font-size:12px;font-weight:600;color:var(--ink-3);margin:0 0 8px;text-transform:uppercase}
171 .facet-list{display:flex;flex-wrap:wrap;gap:7px;min-width:0}
172 .facet-chip{display:inline-grid;grid-template-columns:minmax(0,auto) auto;align-items:center;gap:8px;max-width:100%;min-width:0;
173 padding:6px 9px;border:1px solid var(--line);border-radius:11px;background:#fff;color:var(--ink-2);text-decoration:none;font-size:13px}
174 .facet-chip:hover{border-color:color-mix(in oklch,var(--accent) 30%,var(--line));color:var(--accent)}
175 .facet-chip.active{background:var(--accent-soft);border-color:color-mix(in oklch,var(--accent) 45%,var(--line));color:var(--accent)}
176 .facet-chip b{font-family:var(--mono);font-size:12px;color:inherit}
177 .facet-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
178 .facet-more{flex-basis:100%;min-width:0}
179 .facet-more summary{display:inline-flex;align-items:center;min-height:32px;padding:6px 10px;border:1px dashed var(--line);border-radius:11px;background:var(--bg-soft);color:var(--accent);font-size:13px;font-weight:700;cursor:pointer;list-style:none}
180 .facet-more summary::-webkit-details-marker{display:none}
181 .facet-more summary:hover{border-color:color-mix(in oklch,var(--accent) 34%,var(--line))}
182 .facet-more-list{display:flex;flex-wrap:wrap;gap:7px;max-height:150px;overflow:auto;margin-top:8px;padding:8px;border:1px solid var(--line);border-radius:13px;background:#fff}
183 .filter-empty{font-size:13px;color:var(--ink-3)}
184 .crash-card{overflow:hidden}
185 .crash-list{display:flex;flex-direction:column;gap:2px}
186 .crash-head,.crash-item{display:grid;grid-template-columns:minmax(300px,1fr) minmax(185px,.55fr) 150px 62px;gap:16px;align-items:center}
187 .crash-head{padding:0 12px 9px;color:var(--ink-3);font-size:12.5px;font-weight:600;border-bottom:1px solid var(--line)}
188 .crash-item{margin:0 -12px;padding:14px 12px;border-radius:14px;color:var(--ink);text-decoration:none;border-bottom:1px solid var(--line)}
189 .crash-item:hover{background:var(--bg-soft)}
190 .crash-fingerprint{display:flex;flex-direction:column;gap:3px;min-width:0}
191 .crash-fingerprint b{font-family:var(--mono);font-size:13px;color:var(--accent);font-weight:700}
192 .crash-fingerprint small,.crash-scope small{font-family:var(--mono);font-size:11.5px;color:var(--ink-3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
193 .crash-summary{display:flex;flex-direction:column;gap:5px;min-width:0}
194 .crash-summary>span{font-family:var(--mono);font-size:12.8px;line-height:1.45;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
195 .crash-summary small{font-family:var(--mono);font-size:11.5px;color:var(--ink-3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
196 .crash-summary em{font-size:12px;font-style:normal;color:oklch(0.55 0.18 25)}
197 .crash-scope{display:flex;flex-direction:column;gap:4px;min-width:0}
198 .crash-health{display:flex;flex-wrap:wrap;gap:6px;align-items:center}
199 .crash-count{font-family:var(--mono);font-size:14px;font-weight:700;color:var(--ink);text-align:right}
200 .group-hero{padding:28px 0 22px}
201 .group-nav{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:18px}
202 .group-nav .back{margin:0}
203 .group-title{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-bottom:8px}
204 .group-title h1{margin:0;font-family:var(--mono);font-size:34px}
205 .group-summary{font-size:17px;margin:0 0 12px}
206 .group-tags{display:flex;flex-wrap:wrap;gap:8px;margin:0 0 18px}
207 .group-tags span{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--line);border-radius:11px;background:#fff;padding:6px 9px;font-size:13px;color:var(--ink-2)}
208 .group-tags b{font-size:12px;color:var(--ink-3);font-weight:600}
209 .crash-list.compact .crash-head{display:none}
210 .crash-list.compact .crash-item{padding:12px;margin:0 -12px;grid-template-columns:minmax(260px,1fr) minmax(160px,.5fr) 140px 52px}
211 .group-metrics{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px}
212 .group-metrics div{border:1px solid var(--line);border-radius:14px;background:var(--bg-soft);padding:12px 13px;min-width:0}
213 .group-metrics span{display:block;font-size:12px;color:var(--ink-3);margin-bottom:5px}
214 .group-metrics b{display:block;font-family:var(--mono);font-size:13px;color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
215 .group-note{margin-top:12px;color:var(--ink-2);font-size:13.5px}
216 .sample-card h2{display:flex;align-items:center;justify-content:space-between;gap:12px}
217 .sample-list{display:flex;flex-direction:column;gap:10px}
218 .sample{border:1px solid var(--line);border-radius:16px;background:#fff;overflow:hidden}
219 .sample summary{display:grid;grid-template-columns:140px minmax(0,1fr) 170px;gap:14px;align-items:center;padding:14px 16px;cursor:pointer;list-style:none}
220 .sample summary::-webkit-details-marker{display:none}
221 .sample summary:hover{background:var(--bg-soft)}
222 .sample[open] summary{border-bottom:1px solid var(--line)}
223 .sample-id{display:flex;flex-direction:column;gap:3px;min-width:0}
224 .sample-id b{font-family:var(--mono);font-size:14px;color:var(--ink)}
225 .sample-id small,.sample-time{font-family:var(--mono);font-size:12px;color:var(--ink-3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
226 .sample-title{font-family:var(--mono);font-size:12.8px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
227 .sample-time{text-align:right}
228 .sample-body{padding:14px 16px 16px}
229 .sample-meta{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:12px}
230 .sample-meta span{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--line);border-radius:10px;padding:5px 8px;color:var(--ink-2);font-size:12.5px}
231 .sample-meta b{color:var(--ink-3);font-weight:600}
232 .sample-actions{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:12px}
233 .sample-more{border:1px dashed var(--line);border-radius:16px;background:var(--bg-soft);padding:10px}
234 .sample-more>summary{cursor:pointer;list-style:none;color:var(--accent);font-size:13.5px;font-weight:700;padding:4px 6px}
235 .sample-more>summary::-webkit-details-marker{display:none}
236 .sample-more-list{display:flex;flex-direction:column;gap:10px;margin-top:10px}
237 .copy-btn[data-state="copied"]{background:oklch(0.95 0.05 150);color:oklch(0.48 0.13 150)}
238 .copy-btn[data-state="copied"] .copy-label{display:none}
239 .copy-btn[data-state="copied"]::after{content:"Copied"}
240 body[data-lang="zh"] .copy-btn[data-state="copied"]::after{content:"已复制"}
241 .copy-btn[data-state="failed"]{background:oklch(0.96 0.03 25);color:oklch(0.55 0.18 25)}
242 .copy-btn[data-state="failed"] .copy-label{display:none}
243 .copy-btn[data-state="failed"]::after{content:"Copy failed"}
244 body[data-lang="zh"] .copy-btn[data-state="failed"]::after{content:"复制失败"}
245 .sample-nested{margin-top:10px}
246 .sample-nested summary{cursor:pointer;color:var(--accent);font-size:13px;margin-bottom:8px}
247 .sample-nested pre{margin-top:8px}
248 .manage-card{margin-top:20px}
249 .manage-head{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:16px}
250 .manage-head h2{margin:0}
251 .manage-actions{display:flex;gap:8px;flex-wrap:wrap;justify-content:flex-end}
252 .manage-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px}
253 .manage-form{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:10px;align-items:end}
254 .manage-form.wide{grid-column:1/-1}
255 .manage-form label{display:flex;flex-direction:column;gap:6px;font-size:12.5px;color:var(--ink-3);font-weight:600}
256 .manage-form input,.manage-form select{min-width:0}
257 form.inline{display:inline}
258 .authwrap{max-width:400px;margin:6vh auto 0}
259 .authcard{background:#fff;border:1px solid var(--line);border-radius:24px;padding:32px 34px;box-shadow:var(--shadow-1)}
260 .authcard h1{font-size:23px;margin:0 0 4px}
261 .authcard .sub{margin-bottom:22px}
262 .field{display:block;margin-bottom:14px}
263 .field label{display:block;font-size:13px;color:var(--ink-2);margin-bottom:6px}
264 .field input,select{width:100%;font:inherit;font-size:14.5px;color:var(--ink);background:#fff;border:1px solid var(--line);border-radius:12px;padding:10px 13px;outline:none}
265 .field input:focus,select:focus{border-color:var(--accent)}
266 .btn{font:inherit;font-size:14.5px;font-weight:600;color:#fff;background:var(--accent);border:none;border-radius:12px;padding:10px 18px;cursor:pointer}
267 .btn:hover{filter:brightness(1.05)}
268 .btn.block{width:100%}
269 .btn.ghost{background:var(--accent-soft);color:var(--accent)}
270 .btn.danger{background:oklch(0.95 0.03 25);color:oklch(0.55 0.18 25)}
271 .btn.sm{font-size:13px;padding:6px 12px;border-radius:10px}
272 .notice{font-size:13.5px;padding:10px 14px;border-radius:12px;margin-bottom:16px}
273 .notice.err{background:oklch(0.96 0.03 25);color:oklch(0.5 0.18 25)}
274 .notice.ok{background:oklch(0.95 0.05 150);color:oklch(0.45 0.13 150)}
275 .alt{margin-top:18px;font-size:13.5px;color:var(--ink-3);text-align:center}
276 .alt a{color:var(--accent);text-decoration:none}
277 .actions{display:flex;gap:8px;align-items:center}
278 td select{width:auto;padding:6px 10px;border-radius:10px}
279 .note-edit{margin-top:12px;display:flex;gap:8px;max-width:560px}
280 .note-edit input{flex:1}
281 @media(max-width:1100px){.overview-grid{grid-template-columns:repeat(3,minmax(0,1fr))}.module-nav{grid-template-columns:repeat(2,minmax(0,1fr))}.health-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.module-split,.module-split.wide,.preference-compare{grid-template-columns:1fr}}
282 @media(max-width:900px){.hero-line{flex-direction:column}.facet-grid{grid-template-columns:1fr}.crash-head{display:none}.crash-item,.crash-list.compact .crash-item{grid-template-columns:1fr;gap:10px}
283 .crash-health{justify-content:flex-start}.crash-count{text-align:left}.filter-head,.module-head{align-items:flex-start;flex-direction:column;gap:8px}.module-actions{justify-content:flex-start}
284 .group-metrics{grid-template-columns:1fr 1fr}.sample summary{grid-template-columns:1fr}.sample-time{text-align:left}.manage-head{flex-direction:column}.manage-actions{justify-content:flex-start}.manage-grid{grid-template-columns:1fr}.manage-form.wide{grid-column:auto}}
285 @media(max-width:760px){header{height:auto;min-height:68px;flex-wrap:wrap;padding:14px 0}.lang-switch{margin-left:0}.nav{width:100%;flex-wrap:wrap;gap:8px}.site-nav{overflow-x:auto;padding-bottom:2px}.site-nav a{white-space:nowrap}.chip{min-width:0;max-width:100%;overflow:hidden;text-overflow:ellipsis}.grid{grid-template-columns:1fr}.overview-grid,.module-nav,.health-grid{grid-template-columns:1fr}.metrics,.pref-metrics{grid-template-columns:1fr}.row{grid-template-columns:minmax(0,1fr) minmax(70px,.8fr) 3.2em}.card-title-row{align-items:flex-start;flex-direction:column}.filter-card{position:static}.wrap{padding:0 16px 48px}}
286 `;
287
288 const LANG_SWITCH = `<span class="lang-switch" role="group" aria-label="Language"><button type="button" data-lang-option="zh">中文</button><button type="button" data-lang-option="en">EN</button></span>`;
289
290 const LANG_SCRIPT = `<script>(()=>{const k="reasonix.stats.lang";const b=[...document.querySelectorAll("[data-lang-option]")];const pick=()=>{try{return localStorage.getItem(k)}catch{return""}};const fallback=()=>((navigator.language||"").toLowerCase().startsWith("zh")?"zh":"en");const set=(v)=>{const lang=v==="en"?"en":"zh";document.body.dataset.lang=lang;b.forEach((x)=>{const on=x.dataset.langOption===lang;x.classList.toggle("active",on);x.setAttribute("aria-pressed",String(on))});try{localStorage.setItem(k,lang)}catch{}};b.forEach((x)=>x.addEventListener("click",()=>set(x.dataset.langOption||"zh")));set(pick()||fallback())})();(()=>{const fallback=(t)=>{const a=document.createElement("textarea");a.value=t;a.setAttribute("readonly","");a.style.position="fixed";a.style.opacity="0";document.body.appendChild(a);a.select();let ok=false;try{ok=document.execCommand("copy")}catch{}a.remove();return ok};document.addEventListener("click",async(e)=>{const b=e.target.closest("[data-copy]");if(!b)return;const t=b.dataset.copy||"";let ok=false;try{if(navigator.clipboard){await navigator.clipboard.writeText(t);ok=true}}catch{}if(!ok)ok=fallback(t);b.dataset.state=ok?"copied":"failed";setTimeout(()=>{delete b.dataset.state},1400)})})()</script>`;
291
292 const STATS_ROUTE_SCRIPT = `<script>(()=>{if(location.pathname!=="/stats"||!location.hash)return;const m={diagnostics:"diagnostics",usage:"",preferences:"preferences",health:"health"};const key=location.hash.slice(1);if(m[key]===undefined)return;location.replace("/stats"+(m[key]?"/"+m[key]:"")+location.search)})()</script>`;
293
294 export function page(title: string, crumb: string, body: string, nav = ""): string {
295 return `<!doctype html><html lang="en"><head><meta charset="utf-8">
296 <meta name="viewport" content="width=device-width,initial-scale=1"><meta name="robots" content="noindex">
297 <title>${esc(title)}</title><style>${CSS}</style></head><body><div class="wrap">
298 <header><a class="brand" href="https://reasonix.io">${LOGO}</a><a class="brand" href="https://reasonix.io">Reasonix</a><span class="crumb">/ ${esc(crumb)}</span>${LANG_SWITCH}${nav ? `<span class="nav">${nav}</span>` : ""}</header>
299 ${body}</div>${LANG_SCRIPT}${STATS_ROUTE_SCRIPT}</body></html>`;
300 }
301
302 export function html(body: string): Response {
303 return new Response(body, { headers: { "content-type": "text/html; charset=utf-8" } });
304 }
305
306 export function redirect(location: string, cookie?: string): Response {
307 const headers: Record<string, string> = { location };
308 if (cookie) headers["set-cookie"] = cookie;
309 return new Response(null, { status: 303, headers });
310 }
311
311 lines TYPESCRIPT