@stati/core 1.26.2 → 1.27.0

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.
@@ -2,433 +2,845 @@
2
2
  * Error overlay utility for displaying pretty errors in development mode.
3
3
  * Provides a user-friendly interface for template rendering and build errors.
4
4
  */
5
- /**
6
- * Creates a styled error overlay HTML that displays comprehensive error information.
7
- *
8
- * @param error - The error details to display
9
- * @param requestPath - The path that triggered the error
10
- * @returns HTML string for the error overlay
11
- */
12
- export function createErrorOverlay(error, requestPath) {
13
- const timestamp = new Date().toLocaleTimeString();
14
- return `<!DOCTYPE html>
15
- <html lang="en">
16
- <head>
17
- <meta charset="UTF-8">
18
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
19
- <title>Stati Development Error</title>
20
- <style>
21
- * {
22
- margin: 0;
23
- padding: 0;
5
+ const ERROR_HEADLINES = {
6
+ template: 'Template rendering failed',
7
+ build: 'Build process failed',
8
+ markdown: 'Markdown processing failed',
9
+ config: 'Configuration error',
10
+ };
11
+ const BOLT_PATH = 'M13 10V3L4 14h7v7l9-11h-7z';
12
+ /** Inline favicon, so the overlay never requests the site's own favicon. */
13
+ const FAVICON_HREF = `data:image/svg+xml,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#2563eb" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${BOLT_PATH}"/></svg>`)}`;
14
+ const OVERLAY_STYLES = `
15
+ :root {
16
+ --canvas: 255 255 255;
17
+ --raised: 248 250 252;
18
+ --sunken: 246 248 250;
19
+ --ink: 11 13 18;
20
+ --muted: 71 85 105;
21
+ --faint: 91 101 119;
22
+ --line: 226 232 240;
23
+ --line-strong: 203 213 225;
24
+ --accent: 37 99 235;
25
+ --accent-ink: 29 78 216;
26
+ --on-accent: 255 255 255;
27
+ --danger: 185 28 28;
28
+ --danger-wash: 254 242 242;
29
+ --font-display: 'Archivo', 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
30
+ --font-sans: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
31
+ --font-mono: 'Fira Code', ui-monospace, 'Cascadia Code', Menlo, Monaco, Consolas, 'Liberation Mono', monospace;
32
+ color-scheme: light;
33
+ }
34
+
35
+ @media (prefers-color-scheme: dark) {
36
+ :root {
37
+ --canvas: 2 6 23;
38
+ --raised: 15 23 42;
39
+ --sunken: 11 18 32;
40
+ --ink: 241 245 249;
41
+ --muted: 148 163 184;
42
+ --faint: 131 145 167;
43
+ --line: 30 41 59;
44
+ --line-strong: 51 65 85;
45
+ --accent: 96 165 250;
46
+ --accent-ink: 147 197 253;
47
+ --on-accent: 2 6 23;
48
+ --danger: 248 113 113;
49
+ --danger-wash: 38 12 20;
50
+ color-scheme: dark;
51
+ }
52
+ }
53
+
54
+ *,
55
+ *::before,
56
+ *::after {
24
57
  box-sizing: border-box;
25
58
  }
26
59
 
27
- body {
28
- font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
29
- background: linear-gradient(135deg, #1e1e1e 0%, #2d2d2d 100%);
30
- color: #ffffff;
31
- line-height: 1.6;
32
- min-height: 100vh;
33
- overflow-y: auto;
60
+ html {
61
+ background-color: rgb(var(--canvas));
62
+ scrollbar-color: rgb(var(--line-strong)) transparent;
34
63
  }
35
64
 
36
- .error-overlay {
65
+ body {
66
+ margin: 0;
37
67
  min-height: 100vh;
38
68
  display: flex;
39
69
  flex-direction: column;
70
+ background-color: rgb(var(--canvas));
71
+ color: rgb(var(--ink));
72
+ font-family: var(--font-sans);
73
+ font-size: 16px;
74
+ line-height: 1.6;
75
+ font-feature-settings: 'kern' 1, 'liga' 1, 'calt' 1, 'cv11' 1;
76
+ -webkit-font-smoothing: antialiased;
77
+ -moz-osx-font-smoothing: grayscale;
40
78
  }
41
79
 
42
- .error-header {
43
- background: linear-gradient(90deg, #ff6b6b 0%, #ee5a52 100%);
44
- padding: 24px 32px;
45
- box-shadow: 0 4px 20px rgba(255, 107, 107, 0.3);
80
+ ::selection {
81
+ background-color: rgb(var(--accent) / 0.25);
46
82
  }
47
83
 
48
- .error-title {
49
- font-size: 24px;
50
- font-weight: 600;
51
- margin-bottom: 8px;
84
+ :focus-visible {
85
+ outline: 2px solid rgb(var(--accent));
86
+ outline-offset: 2px;
87
+ }
88
+
89
+ h1,
90
+ h2,
91
+ p,
92
+ pre,
93
+ dl,
94
+ dd,
95
+ ul,
96
+ figure {
97
+ margin: 0;
98
+ }
99
+
100
+ .sr-only {
101
+ position: absolute;
102
+ width: 1px;
103
+ height: 1px;
104
+ padding: 0;
105
+ margin: -1px;
106
+ overflow: hidden;
107
+ clip: rect(0, 0, 0, 0);
108
+ white-space: nowrap;
109
+ border: 0;
110
+ }
111
+
112
+ .shell {
113
+ width: 100%;
114
+ max-width: 72rem;
115
+ margin: 0 auto;
116
+ padding-left: 1rem;
117
+ padding-right: 1rem;
118
+ }
119
+
120
+ /* Header bar */
121
+
122
+ .bar {
123
+ border-bottom: 1px solid rgb(var(--line));
124
+ }
125
+
126
+ .bar .shell {
127
+ min-height: 3.5rem;
52
128
  display: flex;
129
+ flex-wrap: wrap;
53
130
  align-items: center;
54
- gap: 12px;
131
+ justify-content: space-between;
132
+ gap: 0.75rem 1.5rem;
133
+ padding-top: 0.625rem;
134
+ padding-bottom: 0.625rem;
55
135
  }
56
136
 
57
- .error-icon {
58
- width: 32px;
59
- height: 32px;
60
- background: rgba(255, 255, 255, 0.2);
61
- border-radius: 50%;
137
+ .brand {
62
138
  display: flex;
63
139
  align-items: center;
64
- justify-content: center;
65
- font-size: 18px;
140
+ gap: 0.625rem;
141
+ min-width: 0;
66
142
  }
67
143
 
68
- .error-subtitle {
69
- font-size: 14px;
70
- opacity: 0.9;
71
- font-weight: 400;
144
+ .brand-mark {
145
+ width: 1.25rem;
146
+ height: 1.25rem;
147
+ flex: none;
148
+ color: rgb(var(--accent));
72
149
  }
73
150
 
74
- .error-content {
75
- flex: 1;
76
- padding: 32px;
77
- max-width: 1200px;
78
- margin: 0 auto;
79
- width: 100%;
151
+ .brand-name {
152
+ font-family: var(--font-display);
153
+ font-size: 1.125rem;
154
+ font-weight: 800;
155
+ font-stretch: 112%;
156
+ letter-spacing: -0.02em;
157
+ line-height: 1;
80
158
  }
81
159
 
82
- .error-section {
83
- background: rgba(255, 255, 255, 0.05);
84
- border-radius: 12px;
85
- padding: 24px;
86
- margin-bottom: 24px;
87
- border: 1px solid rgba(255, 255, 255, 0.1);
160
+ .brand-rule {
161
+ width: 1px;
162
+ height: 1rem;
163
+ background-color: rgb(var(--line-strong));
88
164
  }
89
165
 
90
- .section-title {
91
- font-size: 18px;
92
- font-weight: 600;
93
- margin-bottom: 16px;
94
- color: #ff6b6b;
166
+ .brand-context {
167
+ font-size: 0.875rem;
168
+ font-weight: 500;
169
+ color: rgb(var(--muted));
170
+ white-space: nowrap;
171
+ }
172
+
173
+ .bar-actions {
95
174
  display: flex;
96
175
  align-items: center;
97
- gap: 8px;
176
+ gap: 1rem;
177
+ }
178
+
179
+ .status {
180
+ display: inline-flex;
181
+ align-items: center;
182
+ gap: 0.5rem;
183
+ font-size: 0.8125rem;
184
+ color: rgb(var(--muted));
98
185
  }
99
186
 
100
- .error-message {
101
- background: rgba(255, 107, 107, 0.1);
102
- border: 1px solid rgba(255, 107, 107, 0.3);
187
+ .status-dot {
188
+ width: 0.5rem;
189
+ height: 0.5rem;
190
+ flex: none;
191
+ border-radius: 50%;
192
+ border: 1px solid rgb(var(--faint));
193
+ transition: background-color 150ms, border-color 150ms;
194
+ }
195
+
196
+ .status[data-state='live'] .status-dot {
197
+ background-color: rgb(var(--ink));
198
+ border-color: rgb(var(--ink));
199
+ }
200
+
201
+ .status[data-state='off'] .status-dot {
202
+ border-color: rgb(var(--danger));
203
+ }
204
+
205
+ .button {
206
+ display: inline-flex;
207
+ align-items: center;
208
+ gap: 0.625rem;
209
+ min-height: 2.25rem;
210
+ padding: 0.5rem 0.875rem;
211
+ border: 0;
103
212
  border-radius: 8px;
104
- padding: 16px;
105
- font-size: 16px;
106
- line-height: 1.5;
107
- margin-bottom: 16px;
213
+ background-color: rgb(var(--accent));
214
+ color: rgb(var(--on-accent));
215
+ font: inherit;
216
+ font-size: 0.875rem;
217
+ font-weight: 600;
218
+ line-height: 1.25;
219
+ cursor: pointer;
220
+ transition: background-color 150ms;
221
+ }
222
+
223
+ .button:hover {
224
+ background-color: rgb(var(--accent-ink));
225
+ }
226
+
227
+ .button kbd {
228
+ padding: 0.0625rem 0.3125rem;
229
+ border: 1px solid rgb(var(--on-accent) / 0.4);
230
+ border-radius: 4px;
231
+ font-family: var(--font-mono);
232
+ font-feature-settings: normal;
233
+ font-variant-ligatures: none;
234
+ font-size: 0.75rem;
235
+ font-weight: 400;
236
+ line-height: 1.4;
237
+ }
238
+
239
+ /* Main content */
240
+
241
+ .main {
242
+ flex: 1;
243
+ padding-top: 2.5rem;
244
+ padding-bottom: 4rem;
245
+ }
246
+
247
+ .summary {
248
+ max-width: 56rem;
108
249
  }
109
250
 
110
- .error-location {
251
+ .headline {
252
+ font-family: var(--font-display);
253
+ font-size: 1.875rem;
254
+ font-weight: 800;
255
+ font-stretch: 108%;
256
+ letter-spacing: -0.015em;
257
+ line-height: 1.1;
258
+ text-wrap: balance;
259
+ }
260
+
261
+ .meta {
111
262
  display: flex;
112
- gap: 16px;
113
- margin-bottom: 16px;
114
263
  flex-wrap: wrap;
264
+ align-items: center;
265
+ gap: 0.5rem 0.75rem;
266
+ margin-top: 0.875rem;
267
+ font-size: 0.875rem;
268
+ color: rgb(var(--muted));
115
269
  }
116
270
 
117
- .location-item {
118
- background: rgba(255, 255, 255, 0.1);
119
- padding: 8px 12px;
120
- border-radius: 6px;
121
- font-size: 14px;
122
- display: flex;
271
+ .type-badge {
272
+ display: inline-flex;
123
273
  align-items: center;
124
- gap: 6px;
274
+ padding: 0.125rem 0.5rem;
275
+ border: 1px solid rgb(var(--line-strong));
276
+ border-radius: 4px;
277
+ font-size: 0.75rem;
278
+ font-weight: 600;
279
+ line-height: 1.5;
280
+ color: rgb(var(--ink));
281
+ text-transform: capitalize;
125
282
  }
126
283
 
127
- .location-label {
128
- color: #888;
129
- font-weight: 500;
284
+ .meta-time {
285
+ font-family: var(--font-mono);
286
+ font-feature-settings: normal;
287
+ font-variant-ligatures: none;
288
+ font-size: 0.75rem;
289
+ font-variant-numeric: tabular-nums;
290
+ color: rgb(var(--faint));
130
291
  }
131
292
 
132
- .location-value {
133
- color: #ffffff;
293
+ .message {
294
+ margin-top: 1.5rem;
295
+ padding: 1rem 1.25rem;
296
+ border: 1px solid rgb(var(--danger) / 0.35);
297
+ border-radius: 8px;
298
+ background-color: rgb(var(--danger-wash));
299
+ }
300
+
301
+ .message-label {
302
+ display: block;
303
+ margin-bottom: 0.375rem;
304
+ font-size: 0.8125rem;
134
305
  font-weight: 600;
306
+ color: rgb(var(--danger));
135
307
  }
136
308
 
137
- .code-block {
138
- background: #1a1a1a;
139
- border: 1px solid rgba(255, 255, 255, 0.1);
140
- border-radius: 8px;
141
- overflow: hidden;
142
- font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
309
+ .message-text {
310
+ font-family: var(--font-mono);
311
+ font-feature-settings: normal;
312
+ font-variant-ligatures: none;
313
+ font-size: 0.875rem;
314
+ line-height: 1.65;
315
+ white-space: pre-wrap;
316
+ overflow-wrap: anywhere;
317
+ color: rgb(var(--ink));
143
318
  }
144
319
 
145
- .code-header {
146
- background: rgba(255, 255, 255, 0.05);
147
- padding: 12px 16px;
148
- border-bottom: 1px solid rgba(255, 255, 255, 0.1);
149
- font-size: 14px;
150
- color: #888;
320
+ .facts {
321
+ display: flex;
322
+ flex-wrap: wrap;
323
+ gap: 0.75rem 2.5rem;
324
+ margin-top: 1.25rem;
151
325
  }
152
326
 
153
- .code-content {
154
- padding: 0;
155
- overflow-x: auto;
327
+ .fact {
328
+ min-width: 0;
156
329
  }
157
330
 
158
- .code-line {
331
+ .fact dt {
332
+ font-size: 0.8125rem;
333
+ color: rgb(var(--faint));
334
+ }
335
+
336
+ .fact dd {
337
+ margin-top: 0.125rem;
338
+ font-family: var(--font-mono);
339
+ font-feature-settings: normal;
340
+ font-variant-ligatures: none;
341
+ font-size: 0.8125rem;
342
+ font-variant-numeric: tabular-nums;
343
+ overflow-wrap: anywhere;
344
+ color: rgb(var(--ink));
345
+ }
346
+
347
+ /* Sections */
348
+
349
+ .section {
350
+ margin-top: 2.5rem;
351
+ padding-top: 1.75rem;
352
+ border-top: 1px solid rgb(var(--line));
353
+ }
354
+
355
+ .section-title {
356
+ font-family: var(--font-display);
357
+ font-size: 1.125rem;
358
+ font-weight: 700;
359
+ font-stretch: 108%;
360
+ letter-spacing: -0.01em;
361
+ line-height: 1.25;
362
+ }
363
+
364
+ .section-title + * {
365
+ margin-top: 1rem;
366
+ }
367
+
368
+ /* Code panel */
369
+
370
+ .panel {
371
+ overflow: hidden;
372
+ border: 1px solid rgb(var(--line));
373
+ border-radius: 8px;
374
+ background-color: rgb(var(--sunken));
375
+ }
376
+
377
+ .panel-bar {
159
378
  display: flex;
160
379
  align-items: center;
161
- padding: 0 16px;
162
- min-height: 24px;
163
- font-size: 14px;
380
+ justify-content: space-between;
381
+ gap: 1rem;
382
+ padding: 0.5rem 1rem;
383
+ border-bottom: 1px solid rgb(var(--line));
384
+ background-color: rgb(var(--canvas));
385
+ font-family: var(--font-mono);
386
+ font-feature-settings: normal;
387
+ font-variant-ligatures: none;
388
+ font-size: 0.75rem;
389
+ line-height: 1.5;
390
+ color: rgb(var(--faint));
391
+ }
392
+
393
+ .panel-file {
394
+ min-width: 0;
395
+ overflow-wrap: anywhere;
396
+ }
397
+
398
+ .panel-pos {
399
+ flex: none;
400
+ font-variant-numeric: tabular-nums;
401
+ }
402
+
403
+ .code {
404
+ overflow-x: auto;
405
+ padding: 0.75rem 0;
406
+ font-family: var(--font-mono);
407
+ font-feature-settings: normal;
408
+ font-variant-ligatures: none;
409
+ font-size: 0.8125rem;
410
+ line-height: 1.65;
411
+ tab-size: 2;
164
412
  }
165
413
 
166
- .code-line:hover {
167
- background: rgba(255, 255, 255, 0.05);
414
+ .code-lines {
415
+ min-width: 100%;
416
+ width: max-content;
417
+ }
418
+
419
+ .code-line {
420
+ display: flex;
421
+ padding-right: 1.25rem;
168
422
  }
169
423
 
170
424
  .line-number {
171
- color: #666;
172
- margin-right: 16px;
173
- min-width: 40px;
425
+ flex: none;
426
+ width: 4.5ch;
427
+ margin-right: 1.25rem;
428
+ padding-left: 0.75rem;
174
429
  text-align: right;
175
- font-weight: 500;
430
+ font-variant-numeric: tabular-nums;
431
+ color: rgb(var(--faint));
432
+ user-select: none;
433
+ }
434
+
435
+ .line-content {
436
+ white-space: pre;
437
+ color: rgb(var(--muted));
176
438
  }
177
439
 
178
440
  .line-error {
179
- background: rgba(255, 107, 107, 0.2);
180
- border-left: 4px solid #ff6b6b;
441
+ background-color: rgb(var(--danger) / 0.1);
181
442
  }
182
443
 
183
444
  .line-error .line-number {
184
- color: #ff6b6b;
185
445
  font-weight: 600;
446
+ color: rgb(var(--danger));
186
447
  }
187
448
 
188
- .line-context {
189
- background: rgba(255, 255, 255, 0.03);
449
+ .line-error .line-content {
450
+ color: rgb(var(--ink));
190
451
  }
191
452
 
453
+ /* Suggestions */
454
+
192
455
  .suggestions {
193
- background: rgba(52, 211, 153, 0.1);
194
- border: 1px solid rgba(52, 211, 153, 0.3);
195
- border-radius: 8px;
196
- padding: 16px;
456
+ padding: 0;
457
+ list-style: none;
197
458
  }
198
459
 
199
- .suggestions-title {
200
- color: #34d399;
201
- font-weight: 600;
202
- margin-bottom: 12px;
460
+ .suggestion-item {
461
+ display: flex;
462
+ align-items: flex-start;
463
+ gap: 0.75rem;
464
+ padding: 0.75rem 0;
465
+ border-top: 1px solid rgb(var(--line));
466
+ color: rgb(var(--muted));
467
+ max-width: 48rem;
468
+ }
469
+
470
+ .suggestions .suggestion-item:first-child {
471
+ border-top-color: rgb(var(--ink));
472
+ }
473
+
474
+ .suggestion-icon {
475
+ width: 1rem;
476
+ height: 1rem;
477
+ flex: none;
478
+ margin-top: 0.3rem;
479
+ color: rgb(var(--faint));
480
+ }
481
+
482
+ /* Stack trace */
483
+
484
+ .stack summary {
203
485
  display: flex;
204
486
  align-items: center;
205
- gap: 8px;
487
+ gap: 0.75rem;
488
+ width: fit-content;
489
+ cursor: pointer;
490
+ list-style: none;
491
+ border-radius: 4px;
206
492
  }
207
493
 
208
- .suggestion-item {
209
- background: rgba(52, 211, 153, 0.05);
210
- border-radius: 6px;
211
- padding: 12px;
212
- margin-bottom: 8px;
213
- border-left: 3px solid #34d399;
494
+ .stack summary::-webkit-details-marker {
495
+ display: none;
214
496
  }
215
497
 
216
- .suggestion-item:last-child {
217
- margin-bottom: 0;
498
+ .stack-chevron {
499
+ width: 1rem;
500
+ height: 1rem;
501
+ flex: none;
502
+ color: rgb(var(--faint));
503
+ transition: transform 150ms;
504
+ }
505
+
506
+ .stack[open] .stack-chevron {
507
+ transform: rotate(90deg);
508
+ }
509
+
510
+ .stack-count {
511
+ font-family: var(--font-mono);
512
+ font-feature-settings: normal;
513
+ font-variant-ligatures: none;
514
+ font-size: 0.75rem;
515
+ font-variant-numeric: tabular-nums;
516
+ color: rgb(var(--faint));
517
+ }
518
+
519
+ .stack summary:hover .section-title {
520
+ color: rgb(var(--accent-ink));
218
521
  }
219
522
 
220
523
  .stack-trace {
221
- background: #1a1a1a;
222
- border: 1px solid rgba(255, 255, 255, 0.1);
223
- border-radius: 8px;
224
- padding: 16px;
225
- font-size: 13px;
226
- line-height: 1.4;
524
+ margin-top: 1rem;
525
+ padding: 1rem 1.25rem;
227
526
  overflow-x: auto;
228
- white-space: pre-wrap;
229
- color: #ccc;
527
+ border: 1px solid rgb(var(--line));
528
+ border-radius: 8px;
529
+ background-color: rgb(var(--sunken));
530
+ font-family: var(--font-mono);
531
+ font-feature-settings: normal;
532
+ font-variant-ligatures: none;
533
+ font-size: 0.8125rem;
534
+ line-height: 1.65;
535
+ white-space: pre;
536
+ color: rgb(var(--muted));
537
+ }
538
+
539
+ .stack-head {
540
+ color: rgb(var(--ink));
541
+ }
542
+
543
+ .stack-dim {
544
+ color: rgb(var(--faint));
230
545
  }
231
546
 
547
+ /* Footer */
548
+
232
549
  .footer {
233
- background: rgba(255, 255, 255, 0.05);
234
- padding: 16px 32px;
235
- border-top: 1px solid rgba(255, 255, 255, 0.1);
236
- display: flex;
237
- justify-content: space-between;
238
- align-items: center;
239
- font-size: 14px;
240
- color: #888;
550
+ border-top: 1px solid rgb(var(--line));
551
+ font-size: 0.8125rem;
552
+ color: rgb(var(--faint));
241
553
  }
242
554
 
243
- .footer-info {
555
+ .footer .shell {
244
556
  display: flex;
245
- gap: 24px;
557
+ flex-wrap: wrap;
558
+ gap: 0.25rem 0.75rem;
559
+ padding-top: 1rem;
560
+ padding-bottom: 1rem;
246
561
  }
247
562
 
248
- .refresh-button {
249
- background: #ff6b6b;
250
- color: white;
251
- border: none;
252
- padding: 8px 16px;
253
- border-radius: 6px;
254
- font-weight: 500;
255
- cursor: pointer;
256
- transition: background 0.2s;
563
+ .footer-name {
564
+ font-weight: 600;
565
+ color: rgb(var(--muted));
257
566
  }
258
567
 
259
- .refresh-button:hover {
260
- background: #ee5a52;
568
+ @media (prefers-reduced-motion: no-preference) {
569
+ .main {
570
+ animation: overlay-in 240ms cubic-bezier(0.16, 1, 0.3, 1) both;
571
+ }
572
+
573
+ @keyframes overlay-in {
574
+ from {
575
+ opacity: 0;
576
+ transform: translateY(6px);
577
+ }
578
+ }
261
579
  }
262
580
 
263
- .type-badge {
264
- background: rgba(255, 107, 107, 0.2);
265
- color: #ff6b6b;
266
- padding: 4px 8px;
267
- border-radius: 4px;
268
- font-size: 12px;
269
- font-weight: 600;
270
- text-transform: uppercase;
271
- letter-spacing: 0.5px;
581
+ @media (hover: none) {
582
+ .button kbd {
583
+ display: none;
584
+ }
272
585
  }
273
586
 
274
- .type-template { background: rgba(147, 51, 234, 0.2); color: #a855f7; }
275
- .type-build { background: rgba(245, 101, 101, 0.2); color: #f56565; }
276
- .type-markdown { background: rgba(59, 130, 246, 0.2); color: #3b82f6; }
277
- .type-config { background: rgba(251, 191, 36, 0.2); color: #fbbf24; }
587
+ @media (min-width: 640px) {
588
+ .shell {
589
+ padding-left: 1.5rem;
590
+ padding-right: 1.5rem;
591
+ }
278
592
 
279
- @media (max-width: 768px) {
280
- .error-content {
281
- padding: 16px;
593
+ .main {
594
+ padding-top: 3.5rem;
282
595
  }
283
596
 
284
- .error-header {
285
- padding: 16px 24px;
597
+ .headline {
598
+ font-size: 2.25rem;
286
599
  }
287
600
 
288
- .error-title {
289
- font-size: 20px;
601
+ .code {
602
+ font-size: 0.875rem;
290
603
  }
604
+ }
291
605
 
292
- .error-location {
293
- flex-direction: column;
606
+ @media (min-width: 1024px) {
607
+ .shell {
608
+ padding-left: 3rem;
609
+ padding-right: 3rem;
610
+ }
611
+
612
+ .headline {
613
+ font-size: 2.75rem;
294
614
  }
295
615
  }
616
+
617
+ @media (max-width: 639px) {
618
+ .bar-actions {
619
+ width: 100%;
620
+ justify-content: space-between;
621
+ }
622
+
623
+ .button {
624
+ min-height: 2.75rem;
625
+ }
626
+ }`;
627
+ /**
628
+ * Live reload and refresh controls for the overlay page. The overlay is served
629
+ * instead of the page, so it needs its own connection to the dev server.
630
+ */
631
+ const OVERLAY_SCRIPT = `
632
+ (function () {
633
+ var status = document.getElementById('stati-status');
634
+ var statusText = document.getElementById('stati-status-text');
635
+ var reloadButton = document.getElementById('stati-reload');
636
+ var shortcut = document.getElementById('stati-shortcut');
637
+
638
+ function reload() {
639
+ window.location.reload();
640
+ }
641
+
642
+ function setStatus(state, text) {
643
+ status.setAttribute('data-state', state);
644
+ statusText.textContent = text;
645
+ }
646
+
647
+ if (/Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent)) {
648
+ shortcut.textContent = '\\u2318R';
649
+ }
650
+
651
+ reloadButton.addEventListener('click', reload);
652
+
653
+ // Keyboard shortcut to refresh (Ctrl/Cmd + R)
654
+ document.addEventListener('keydown', function (e) {
655
+ if ((e.ctrlKey || e.metaKey) && (e.key === 'r' || e.key === 'R')) {
656
+ e.preventDefault();
657
+ reload();
658
+ }
659
+ });
660
+
661
+ // Auto-refresh when files change, over the dev server's WebSocket
662
+ if (typeof WebSocket === 'undefined') {
663
+ setStatus('off', 'Live reload unavailable');
664
+ return;
665
+ }
666
+
667
+ var connectedBefore = false;
668
+ var retryDelay = 1000;
669
+
670
+ function connect() {
671
+ var ws;
672
+ try {
673
+ var protocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
674
+ ws = new WebSocket(protocol + window.location.host + '/__ws');
675
+ } catch (e) {
676
+ setStatus('off', 'Live reload unavailable');
677
+ return;
678
+ }
679
+
680
+ ws.onopen = function () {
681
+ // The server restarted while we were away; load the fresh build
682
+ if (connectedBefore) {
683
+ reload();
684
+ return;
685
+ }
686
+ connectedBefore = true;
687
+ retryDelay = 1000;
688
+ setStatus('live', 'Watching for changes');
689
+ };
690
+
691
+ ws.onmessage = function (event) {
692
+ try {
693
+ if (JSON.parse(event.data).type === 'reload') {
694
+ reload();
695
+ }
696
+ } catch (e) {
697
+ // Ignore messages that are not JSON
698
+ }
699
+ };
700
+
701
+ ws.onclose = function () {
702
+ setStatus('off', 'Reconnecting to dev server');
703
+ setTimeout(connect, retryDelay);
704
+ retryDelay = Math.min(retryDelay * 2, 8000);
705
+ };
706
+ }
707
+
708
+ connect();
709
+ })();`;
710
+ /**
711
+ * Creates a styled error overlay HTML that displays comprehensive error information.
712
+ *
713
+ * @param error - The error details to display
714
+ * @param requestPath - The path that triggered the error
715
+ * @returns HTML string for the error overlay
716
+ */
717
+ export function createErrorOverlay(error, requestPath) {
718
+ const timestamp = new Date().toLocaleTimeString();
719
+ const position = error.line ? `${error.line}${error.column ? `:${error.column}` : ''}` : '';
720
+ return `<!DOCTYPE html>
721
+ <html lang="en">
722
+ <head>
723
+ <meta charset="UTF-8">
724
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
725
+ <meta name="color-scheme" content="light dark">
726
+ <link rel="icon" href="${FAVICON_HREF}">
727
+ <title>Stati Development Error</title>
728
+ <style>${OVERLAY_STYLES}
296
729
  </style>
297
730
  </head>
298
731
  <body>
299
- <div class="error-overlay">
300
- <div class="error-header">
301
- <div class="error-title">
302
- <div class="error-icon">⚠️</div>
303
- Stati Development Error
304
- <span class="type-badge type-${error.type}">${error.type}</span>
732
+ <header class="bar">
733
+ <div class="shell">
734
+ <div class="brand">
735
+ <svg class="brand-mark" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="${BOLT_PATH}"/></svg>
736
+ <span class="brand-name">Stati</span>
737
+ <span class="brand-rule" aria-hidden="true"></span>
738
+ <span class="brand-context">Development error</span>
305
739
  </div>
306
- <div class="error-subtitle">
307
- ${error.type === 'template'
308
- ? 'Template rendering failed'
309
- : error.type === 'build'
310
- ? 'Build process failed'
311
- : error.type === 'markdown'
312
- ? 'Markdown processing failed'
313
- : 'Configuration error'} • ${timestamp}
740
+ <div class="bar-actions">
741
+ <span class="status" id="stati-status" data-state="connecting" role="status">
742
+ <span class="status-dot" aria-hidden="true"></span>
743
+ <span id="stati-status-text">Connecting to dev server</span>
744
+ </span>
745
+ <button type="button" class="button" id="stati-reload">
746
+ Refresh
747
+ <kbd id="stati-shortcut" aria-hidden="true">Ctrl+R</kbd>
748
+ </button>
314
749
  </div>
315
750
  </div>
751
+ </header>
752
+
753
+ <main class="main shell">
754
+ <section class="summary" aria-labelledby="stati-error-title">
755
+ <h1 class="headline" id="stati-error-title">${ERROR_HEADLINES[error.type]}</h1>
756
+ <p class="meta">
757
+ <span class="type-badge">${error.type}</span>
758
+ <time class="meta-time">${escapeHtml(timestamp)}</time>
759
+ </p>
760
+
761
+ <div class="message">
762
+ <span class="message-label">Error details</span>
763
+ <pre class="message-text">${escapeHtml(error.message)}</pre>
764
+ </div>
316
765
 
317
- <div class="error-content">
318
- <div class="error-section">
319
- <div class="section-title">
320
- 🚨 Error Details
321
- </div>
322
- <div class="error-message">
323
- ${escapeHtml(error.message)}
766
+ <dl class="facts">
767
+ ${error.file
768
+ ? `<div class="fact">
769
+ <dt>File</dt>
770
+ <dd>${escapeHtml(error.file)}</dd>
771
+ </div>`
772
+ : ''}
773
+ ${position
774
+ ? `<div class="fact">
775
+ <dt>Line</dt>
776
+ <dd>${position}</dd>
777
+ </div>`
778
+ : ''}
779
+ <div class="fact">
780
+ <dt>Route</dt>
781
+ <dd>${escapeHtml(requestPath)}</dd>
324
782
  </div>
783
+ </dl>
784
+ </section>
325
785
 
326
- ${error.file || error.line
786
+ ${error.code || error.context
327
787
  ? `
328
- <div class="error-location">
329
- ${error.file
330
- ? `<div class="location-item">
331
- <span class="location-label">File:</span>
332
- <span class="location-value">${escapeHtml(error.file)}</span>
333
- </div>`
334
- : ''}
335
- ${error.line
336
- ? `<div class="location-item">
337
- <span class="location-label">Line:</span>
338
- <span class="location-value">${error.line}${error.column ? `:${error.column}` : ''}</span>
339
- </div>`
788
+ <section class="section" aria-labelledby="stati-code-title">
789
+ <h2 class="section-title" id="stati-code-title">Code context</h2>
790
+ <figure class="panel">
791
+ ${error.file
792
+ ? `<figcaption class="panel-bar">
793
+ <span class="panel-file">${escapeHtml(error.file)}</span>
794
+ ${position ? `<span class="panel-pos">${position}</span>` : ''}
795
+ </figcaption>`
340
796
  : ''}
341
- <div class="location-item">
342
- <span class="location-label">Route:</span>
343
- <span class="location-value">${escapeHtml(requestPath)}</span>
344
- </div>
797
+ <div class="code" tabindex="0" role="region" aria-label="Source code around the error">
798
+ <div class="code-lines">${formatCodeContext(error)}</div>
345
799
  </div>
346
- `
347
- : ''}
348
- </div>
349
-
350
- ${error.code || error.context
351
- ? `
352
- <div class="error-section">
353
- <div class="section-title">
354
- 📄 Code Context
355
- </div>
356
- <div class="code-block">
357
- ${error.file ? `<div class="code-header">${escapeHtml(error.file)}</div>` : ''}
358
- <div class="code-content">
359
- ${formatCodeContext(error)}
360
- </div>
361
- </div>
362
- </div>
363
- `
800
+ </figure>
801
+ </section>
802
+ `
364
803
  : ''}
365
804
 
366
- ${error.suggestions && error.suggestions.length > 0
805
+ ${error.suggestions && error.suggestions.length > 0
367
806
  ? `
368
- <div class="error-section">
369
- <div class="suggestions">
370
- <div class="suggestions-title">
371
- 💡 Suggestions
372
- </div>
373
- ${error.suggestions
807
+ <section class="section" aria-labelledby="stati-suggestions-title">
808
+ <h2 class="section-title" id="stati-suggestions-title">Suggestions</h2>
809
+ <ul class="suggestions">
810
+ ${error.suggestions
374
811
  .map((suggestion) => `
375
- <div class="suggestion-item">
376
- ${escapeHtml(suggestion)}
377
- </div>
378
- `)
812
+ <li class="suggestion-item">
813
+ <svg class="suggestion-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14M13 6l6 6-6 6"/></svg>
814
+ <span>${escapeHtml(suggestion)}</span>
815
+ </li>`)
379
816
  .join('')}
380
- </div>
381
- </div>
382
- `
817
+ </ul>
818
+ </section>
819
+ `
383
820
  : ''}
384
821
 
385
- ${error.stack
822
+ ${error.stack
386
823
  ? `
387
- <div class="error-section">
388
- <div class="section-title">
389
- 🔍 Stack Trace
390
- </div>
391
- <div class="stack-trace">${escapeHtml(error.stack)}</div>
392
- </div>
393
- `
824
+ <details class="section stack" open>
825
+ <summary>
826
+ <svg class="stack-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 6l6 6-6 6"/></svg>
827
+ <h2 class="section-title">Stack trace</h2>
828
+ ${formatFrameCount(error.stack)}
829
+ </summary>
830
+ <pre class="stack-trace">${formatStack(error.stack)}</pre>
831
+ </details>
832
+ `
394
833
  : ''}
395
- </div>
834
+ </main>
396
835
 
397
- <div class="footer">
398
- <div class="footer-info">
399
- <span>Stati Development Server</span>
400
- <span>•</span>
401
- <span>Fix the error and save to automatically reload</span>
402
- </div>
403
- <button class="refresh-button" onclick="window.location.reload()">
404
- Refresh
405
- </button>
836
+ <footer class="footer">
837
+ <div class="shell">
838
+ <span class="footer-name">Stati Development Server</span>
839
+ <span>Fix the error and save to automatically reload</span>
406
840
  </div>
407
- </div>
408
-
409
- <script>
410
- // Auto-refresh when files change (if WebSocket connection exists)
411
- if (typeof WebSocket !== 'undefined') {
412
- try {
413
- const ws = new WebSocket('ws://localhost:3000/__ws');
414
- ws.onmessage = function(event) {
415
- const data = JSON.parse(event.data);
416
- if (data.type === 'reload') {
417
- window.location.reload();
418
- }
419
- };
420
- } catch (e) {
421
- // WebSocket connection failed, that's okay
422
- }
423
- }
841
+ </footer>
424
842
 
425
- // Keyboard shortcut to refresh (Ctrl/Cmd + R)
426
- document.addEventListener('keydown', function(e) {
427
- if ((e.ctrlKey || e.metaKey) && e.key === 'r') {
428
- e.preventDefault();
429
- window.location.reload();
430
- }
431
- });
843
+ <script>${OVERLAY_SCRIPT}
432
844
  </script>
433
845
  </body>
434
846
  </html>`;
@@ -444,44 +856,67 @@ function escapeHtml(text) {
444
856
  .replace(/"/g, '&quot;')
445
857
  .replace(/'/g, '&#39;');
446
858
  }
859
+ /**
860
+ * Renders one numbered source line for the code context panel.
861
+ */
862
+ function formatCodeLine(lineNumber, content, isError) {
863
+ return `<div class="code-line${isError ? ' line-error' : ''}"><span class="line-number">${lineNumber}${isError ? '<span class="sr-only"> (error)</span>' : ''}</span><span class="line-content">${escapeHtml(content)}</span></div>`;
864
+ }
447
865
  /**
448
866
  * Formats code context with line numbers and highlighting.
449
867
  */
450
868
  function formatCodeContext(error) {
451
- if (!error.context && !error.code) {
452
- return '<div class="code-line"><span class="line-number">-</span>No code context available</div>';
453
- }
454
869
  const lines = [];
455
870
  const errorLine = error.line || 0;
456
871
  let currentLine = errorLine;
457
872
  if (error.context?.before) {
458
873
  currentLine = errorLine - error.context.before.length;
459
874
  error.context.before.forEach((line) => {
460
- lines.push(`<div class="code-line line-context">
461
- <span class="line-number">${currentLine}</span>
462
- <span class="line-content">${escapeHtml(line)}</span>
463
- </div>`);
875
+ lines.push(formatCodeLine(currentLine, line, false));
464
876
  currentLine++;
465
877
  });
466
878
  }
467
879
  if (error.code) {
468
- lines.push(`<div class="code-line line-error">
469
- <span class="line-number">${errorLine}</span>
470
- <span class="line-content">${escapeHtml(error.code)}</span>
471
- </div>`);
880
+ lines.push(formatCodeLine(errorLine, error.code, true));
472
881
  currentLine = errorLine + 1;
473
882
  }
474
883
  if (error.context?.after) {
475
884
  error.context.after.forEach((line) => {
476
- lines.push(`<div class="code-line line-context">
477
- <span class="line-number">${currentLine}</span>
478
- <span class="line-content">${escapeHtml(line)}</span>
479
- </div>`);
885
+ lines.push(formatCodeLine(currentLine, line, false));
480
886
  currentLine++;
481
887
  });
482
888
  }
483
889
  return lines.join('');
484
890
  }
891
+ const STACK_FRAME = /^\s+at\s/;
892
+ /**
893
+ * Formats a stack trace, keeping the first line prominent and dimming frames
894
+ * from dependencies and Node internals so the user's own frames stand out.
895
+ */
896
+ function formatStack(stack) {
897
+ return stack
898
+ .split('\n')
899
+ .map((line, index) => {
900
+ const escaped = escapeHtml(line);
901
+ if (index === 0 && !STACK_FRAME.test(line)) {
902
+ return `<span class="stack-head">${escaped}</span>`;
903
+ }
904
+ if (/node_modules|\(node:|\bat node:/.test(line)) {
905
+ return `<span class="stack-dim">${escaped}</span>`;
906
+ }
907
+ return escaped;
908
+ })
909
+ .join('\n');
910
+ }
911
+ /**
912
+ * Renders the frame count shown beside the stack trace heading.
913
+ */
914
+ function formatFrameCount(stack) {
915
+ const frames = stack.split('\n').filter((line) => STACK_FRAME.test(line)).length;
916
+ return frames > 0
917
+ ? `<span class="stack-count">${frames} ${frames === 1 ? 'frame' : 'frames'}</span>`
918
+ : '';
919
+ }
485
920
  /**
486
921
  * Parses error information from various error types to create structured ErrorDetails.
487
922
  */