返回 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 a:focus-visible,button:focus-visible,summary:focus-visible{outline:3px solid color-mix(in oklch,var(--accent) 72%,white);outline-offset:3px}
117 .pill{display:inline-block;font-size:12px;font-weight:500;padding:2px 10px;border-radius:999px;background:var(--accent-soft);color:var(--accent)}
118 .pill.crash{background:oklch(0.95 0.03 25);color:oklch(0.55 0.18 25)}
119 .pill.open{background:oklch(0.96 0.025 250);color:var(--ink-2)}
120 .pill.status-open{background:oklch(0.96 0.025 250);color:var(--ink-2)}
121 .pill.resolved{background:oklch(0.95 0.05 150);color:oklch(0.48 0.13 150)}
122 .pill.status-resolved{background:oklch(0.95 0.05 150);color:oklch(0.48 0.13 150)}
123 .pill.ignored{background:var(--bg-soft);color:var(--ink-3)}
124 .pill.status-ignored{background:var(--bg-soft);color:var(--ink-3)}
125 .back{display:inline-block;margin:18px 0 0;color:var(--accent);text-decoration:none;font-size:14px}
126 .report{margin-top:20px}
127 .report .meta{display:flex;flex-wrap:wrap;gap:8px 18px;font-size:13px;color:var(--ink-2);margin-bottom:10px}
128 .report .meta b{color:var(--ink);font-weight:500}
129 pre{background:var(--term-bg);color:#e6e8f0;border-radius:12px;padding:16px 18px;font-family:var(--mono);
130 font-size:12.5px;line-height:1.55;overflow:auto;white-space:pre-wrap;word-break:break-word}
131 .metrics{display:grid;grid-template-columns:1fr 1fr;gap:8px 28px}
132 .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}
133 .metric-block h3 span{font-family:var(--mono);font-size:11px;color:var(--ink-3);font-weight:600}
134 .preference-dashboard{display:flex;flex-direction:column;gap:14px}
135 .preference-compare{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;align-items:start}
136 .preference-panel{background:#fff}
137 .preference-panel.active{border-color:color-mix(in oklch,var(--accent) 42%,var(--line));box-shadow:0 0 0 3px var(--accent-soft)}
138 .pref-section{border:1px solid var(--line);border-radius:14px;background:#fff;padding:14px 15px}
139 .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}
140 .pref-section>h3 span,.pref-section summary h3 span{font-size:11.5px;color:var(--ink-3);font-weight:700;white-space:nowrap}
141 .pref-section summary{cursor:pointer;list-style:none}
142 .pref-section summary::-webkit-details-marker{display:none}
143 .pref-section summary h3{margin:0}
144 .pref-section summary h3::after{content:"+";font-family:var(--mono);font-size:13px;color:var(--accent);line-height:1}
145 .pref-section[open] summary h3::after{content:"-"}
146 .pref-section[open] summary h3{margin-bottom:10px}
147 .pref-section-collapsed summary:hover h3{color:var(--accent)}
148 .pref-section .metric-block h3{color:var(--ink-3)}
149 .pref-metrics{grid-template-columns:1fr;gap:12px}
150 .pref-metrics .metric-block{min-width:0}
151 .pref-metrics .row{grid-template-columns:minmax(7rem,1.2fr) minmax(5rem,.9fr) minmax(4.4em,max-content)}
152 .health-grid{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:12px}
153 .health-card{border:1px solid var(--line);border-radius:16px;background:var(--bg-soft);padding:14px 15px;min-width:0}
154 .health-card.good{background:oklch(0.985 0.015 150);border-color:oklch(0.88 0.05 150)}
155 .health-card.warn{background:oklch(0.985 0.018 80);border-color:oklch(0.9 0.07 80)}
156 .health-card.bad{background:oklch(0.985 0.016 25);border-color:oklch(0.89 0.06 25)}
157 .health-top{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px}
158 .health-top span{font-size:12.5px;color:var(--ink-2);font-weight:700}
159 .health-top b{font-size:11px;text-transform:uppercase;color:var(--ink-3);font-weight:800}
160 .health-card strong{display:block;font-family:var(--mono);font-size:22px;color:var(--ink);margin-bottom:4px}
161 .health-card small{display:block;color:var(--ink-3);font-family:var(--mono);font-size:11.5px;margin-bottom:9px}
162 .health-card p{color:var(--ink-2);font-size:12.5px;line-height:1.35;word-break:break-word}
163 .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}
164 .filter-head{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-bottom:14px}
165 .filter-head h2{margin:0}
166 .filter-head span{font-family:var(--mono);font-size:12.5px;color:var(--ink-3);white-space:nowrap}
167 .filter-tabs{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:18px}
168 .filter-tab{display:inline-flex;align-items:center;min-height:34px;padding:7px 13px;border-radius:12px;border:1px solid var(--line);
169 background:var(--bg-soft);color:var(--ink-2);text-decoration:none;font-size:13.5px;font-weight:600}
170 .filter-tab:hover{border-color:color-mix(in oklch,var(--accent) 34%,var(--line));color:var(--accent)}
171 .filter-tab.active{background:var(--accent);border-color:var(--accent);color:#fff}
172 .facet-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:16px}
173 .facet-grid section{min-width:0}
174 .facet-grid h3{font-size:12px;font-weight:600;color:var(--ink-3);margin:0 0 8px;text-transform:uppercase}
175 .facet-list{display:flex;flex-wrap:wrap;gap:7px;min-width:0}
176 .facet-chip{display:inline-grid;grid-template-columns:minmax(0,auto) auto;align-items:center;gap:8px;max-width:100%;min-width:0;
177 padding:6px 9px;border:1px solid var(--line);border-radius:11px;background:#fff;color:var(--ink-2);text-decoration:none;font-size:13px}
178 .facet-chip:hover{border-color:color-mix(in oklch,var(--accent) 30%,var(--line));color:var(--accent)}
179 .facet-chip.active{background:var(--accent-soft);border-color:color-mix(in oklch,var(--accent) 45%,var(--line));color:var(--accent)}
180 .facet-chip b{font-family:var(--mono);font-size:12px;color:inherit}
181 .facet-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
182 .facet-more{flex-basis:100%;min-width:0}
183 .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}
184 .facet-more summary::-webkit-details-marker{display:none}
185 .facet-more summary:hover{border-color:color-mix(in oklch,var(--accent) 34%,var(--line))}
186 .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}
187 .filter-empty{font-size:13px;color:var(--ink-3)}
188 .crash-card{overflow:hidden}
189 .crash-list{display:flex;flex-direction:column;gap:2px}
190 .crash-head,.crash-item{display:grid;grid-template-columns:minmax(280px,1.35fr) minmax(170px,.72fr) minmax(130px,.55fr) minmax(250px,1.15fr);gap:16px;align-items:center}
191 .crash-head{padding:0 12px 9px;color:var(--ink-3);font-size:12.5px;font-weight:600;border-bottom:1px solid var(--line)}
192 .crash-item{margin:0 -12px;padding:14px 12px;border-radius:14px;color:var(--ink);text-decoration:none;border-bottom:1px solid var(--line)}
193 .crash-item:hover{background:var(--bg-soft)}
194 .crash-fingerprint{display:flex;flex-direction:column;gap:3px;min-width:0}
195 .crash-fingerprint b{font-family:var(--mono);font-size:13px;color:var(--accent);font-weight:700}
196 .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}
197 .crash-summary{display:flex;flex-direction:column;gap:5px;min-width:0}
198 .crash-summary>span{display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;font-family:var(--mono);font-size:12.8px;line-height:1.45;overflow:hidden;white-space:normal;overflow-wrap:anywhere}
199 .crash-summary small{font-family:var(--mono);font-size:11.5px;color:var(--ink-3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
200 .crash-summary em{font-size:12px;font-style:normal;color:oklch(0.55 0.18 25)}
201 .crash-scope{display:flex;flex-direction:column;gap:4px;min-width:0}
202 .crash-meta{display:flex;gap:6px;min-width:0;line-height:1.35;white-space:normal}
203 .crash-meta b{flex:0 0 auto;font-family:var(--sans);font-size:10.5px;font-weight:700;color:var(--ink-3)}
204 .crash-meta span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
205 .crash-health{display:flex;flex-wrap:wrap;gap:6px;align-items:center}
206 .crash-count{font-family:var(--mono);font-size:14px;font-weight:700;color:var(--ink);text-align:right;min-width:0}
207 .crash-metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:7px 12px;text-align:left}
208 .crash-metric{display:flex;flex-direction:column;gap:2px;min-width:0}
209 .crash-metric b{font-family:var(--mono);font-size:14px;line-height:1.1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
210 .crash-metric small{font-family:var(--sans);font-size:11px;font-weight:600;line-height:1.2;color:var(--ink-3);white-space:normal}
211 .crash-metric.primary b{font-size:18px;color:var(--accent)}
212 .crash-metric.full{grid-column:1/-1;flex-direction:row;align-items:baseline;gap:6px}
213 .crash-metric.full b{font-size:12.5px}
214 .panel-title{display:flex;align-items:baseline;justify-content:space-between;gap:12px;flex-wrap:wrap}
215 .panel-title h3{margin-bottom:0}
216 .panel-context{font-size:12px;font-weight:600;color:var(--ink-3)}
217 .group-hero{padding:28px 0 22px}
218 .group-nav{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:18px}
219 .group-nav .back{margin:0}
220 .group-title{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-bottom:8px}
221 .group-title h1{margin:0;font-family:var(--mono);font-size:34px}
222 .group-summary{font-size:17px;margin:0 0 12px}
223 .group-tags{display:flex;flex-wrap:wrap;gap:8px;margin:0 0 18px}
224 .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)}
225 .group-tags b{font-size:12px;color:var(--ink-3);font-weight:600}
226 .crash-list.compact .crash-head{display:grid}
227 .crash-list.compact .crash-item{padding:12px;margin:0 -12px;grid-template-columns:minmax(280px,1.35fr) minmax(170px,.72fr) minmax(130px,.55fr) minmax(250px,1.15fr)}
228 .group-metrics{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px}
229 .group-metrics div{border:1px solid var(--line);border-radius:14px;background:var(--bg-soft);padding:12px 13px;min-width:0}
230 .group-metrics span{display:block;font-size:12px;color:var(--ink-3);margin-bottom:5px}
231 .group-metrics b{display:block;font-family:var(--mono);font-size:13px;color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
232 .group-note{margin-top:12px;color:var(--ink-2);font-size:13.5px}
233 .sample-card h2{display:flex;align-items:center;justify-content:space-between;gap:12px}
234 .sample-list{display:flex;flex-direction:column;gap:10px}
235 .sample{border:1px solid var(--line);border-radius:16px;background:#fff;overflow:hidden}
236 .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}
237 .sample summary::-webkit-details-marker{display:none}
238 .sample summary:hover{background:var(--bg-soft)}
239 .sample[open] summary{border-bottom:1px solid var(--line)}
240 .sample-id{display:flex;flex-direction:column;gap:3px;min-width:0}
241 .sample-id b{font-family:var(--mono);font-size:14px;color:var(--ink)}
242 .sample-id small,.sample-time{font-family:var(--mono);font-size:12px;color:var(--ink-3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
243 .sample-title{font-family:var(--mono);font-size:12.8px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
244 .sample-time{text-align:right}
245 .sample-body{padding:14px 16px 16px}
246 .sample-meta{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:12px}
247 .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}
248 .sample-meta b{color:var(--ink-3);font-weight:600}
249 .sample-actions{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:12px}
250 .sample-more{border:1px dashed var(--line);border-radius:16px;background:var(--bg-soft);padding:10px}
251 .sample-more>summary{cursor:pointer;list-style:none;color:var(--accent);font-size:13.5px;font-weight:700;padding:4px 6px}
252 .sample-more>summary::-webkit-details-marker{display:none}
253 .sample-more-list{display:flex;flex-direction:column;gap:10px;margin-top:10px}
254 .copy-btn[data-state="copied"]{background:oklch(0.95 0.05 150);color:oklch(0.48 0.13 150)}
255 .copy-btn[data-state="copied"] .copy-label{display:none}
256 .copy-btn[data-state="copied"]::after{content:"Copied"}
257 body[data-lang="zh"] .copy-btn[data-state="copied"]::after{content:"已复制"}
258 .copy-btn[data-state="failed"]{background:oklch(0.96 0.03 25);color:oklch(0.55 0.18 25)}
259 .copy-btn[data-state="failed"] .copy-label{display:none}
260 .copy-btn[data-state="failed"]::after{content:"Copy failed"}
261 body[data-lang="zh"] .copy-btn[data-state="failed"]::after{content:"复制失败"}
262 .sample-nested{margin-top:10px}
263 .sample-nested summary{cursor:pointer;color:var(--accent);font-size:13px;margin-bottom:8px}
264 .sample-nested pre{margin-top:8px}
265 .manage-card{margin-top:20px}
266 .manage-head{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:16px}
267 .manage-head h2{margin:0}
268 .manage-actions{display:flex;gap:8px;flex-wrap:wrap;justify-content:flex-end}
269 .manage-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px}
270 .manage-form{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:10px;align-items:end}
271 .manage-form.wide{grid-column:1/-1}
272 .manage-form label{display:flex;flex-direction:column;gap:6px;font-size:12.5px;color:var(--ink-3);font-weight:600}
273 .manage-form input,.manage-form select{min-width:0}
274 form.inline{display:inline}
275 .authwrap{max-width:400px;margin:6vh auto 0}
276 .authcard{background:#fff;border:1px solid var(--line);border-radius:24px;padding:32px 34px;box-shadow:var(--shadow-1)}
277 .authcard h1{font-size:23px;margin:0 0 4px}
278 .authcard .sub{margin-bottom:22px}
279 .field{display:block;margin-bottom:14px}
280 .field label{display:block;font-size:13px;color:var(--ink-2);margin-bottom:6px}
281 .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}
282 .field input:focus,select:focus{border-color:var(--accent)}
283 .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}
284 .btn:hover{filter:brightness(1.05)}
285 .btn.block{width:100%}
286 .btn.ghost{background:var(--accent-soft);color:var(--accent)}
287 .btn.danger{background:oklch(0.95 0.03 25);color:oklch(0.55 0.18 25)}
288 .btn.sm{font-size:13px;padding:6px 12px;border-radius:10px}
289 .notice{font-size:13.5px;padding:10px 14px;border-radius:12px;margin-bottom:16px}
290 .notice.err{background:oklch(0.96 0.03 25);color:oklch(0.5 0.18 25)}
291 .notice.ok{background:oklch(0.95 0.05 150);color:oklch(0.45 0.13 150)}
292 .alt{margin-top:18px;font-size:13.5px;color:var(--ink-3);text-align:center}
293 .alt a{color:var(--accent);text-decoration:none}
294 .actions{display:flex;gap:8px;align-items:center}
295 td select{width:auto;padding:6px 10px;border-radius:10px}
296 .note-edit{margin-top:12px;display:flex;gap:8px;max-width:560px}
297 .note-edit input{flex:1}
298 @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}}
299 @media(max-width:900px){.hero-line{flex-direction:column}.facet-grid{grid-template-columns:1fr}.crash-list.compact .crash-head,.crash-list:not(.compact) .crash-head{display:none}.crash-item,.crash-list.compact .crash-item{grid-template-columns:1fr;gap:10px}
300 .crash-health{justify-content:flex-start}.crash-count{text-align:left}.crash-metrics{max-width:360px}.filter-head,.module-head{align-items:flex-start;flex-direction:column;gap:8px}.module-actions{justify-content:flex-start}
301 .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}}
302 @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}}
303 `;
304
305 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>`;
306
307 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>`;
308
309 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>`;
310
311 export function page(title: string, crumb: string, body: string, nav = ""): string {
312 return `<!doctype html><html lang="en"><head><meta charset="utf-8">
313 <meta name="viewport" content="width=device-width,initial-scale=1"><meta name="robots" content="noindex">
314 <title>${esc(title)}</title><style>${CSS}</style></head><body><div class="wrap">
315 <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>
316 ${body}</div>${LANG_SCRIPT}${STATS_ROUTE_SCRIPT}</body></html>`;
317 }
318
319 export function html(body: string): Response {
320 return new Response(body, { headers: { "content-type": "text/html; charset=utf-8" } });
321 }
322
323 export function redirect(location: string, cookie?: string): Response {
324 const headers: Record<string, string> = { location };
325 if (cookie) headers["set-cookie"] = cookie;
326 return new Response(null, { status: 303, headers });
327 }
328
328 lines TYPESCRIPT