pg_reports 0.8.2 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,30 +5,69 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <meta name="pg-reports-root" content="<%= request.script_name.presence || PgReports::Engine.routes.url_helpers.root_path %>">
7
7
  <title><%= t("pg_reports.ui.branding.page_title") %></title>
8
- <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%234f46e5'/%3E%3Crect x='5' y='18' width='5' height='9' rx='1' fill='%23fff'/%3E%3Crect x='13.5' y='12' width='5' height='15' rx='1' fill='%23fff'/%3E%3Crect x='22' y='6' width='5' height='21' rx='1' fill='%23fff'/%3E%3C/svg%3E">
8
+ <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%235561d4'/%3E%3Crect x='5' y='18' width='5' height='9' rx='1' fill='%23fff'/%3E%3Crect x='13.5' y='12' width='5' height='15' rx='1' fill='%23fff'/%3E%3Crect x='22' y='6' width='5' height='21' rx='1' fill='%23fff'/%3E%3C/svg%3E">
9
9
  <% if PgReports.config.load_external_fonts %>
10
10
  <link rel="preconnect" href="https://fonts.googleapis.com">
11
11
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
12
- <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
12
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
13
13
  <% end %>
14
14
  <style>
15
15
  :root {
16
- --bg-primary: #151719;
17
- --bg-secondary: #1c1f22;
18
- --bg-tertiary: #232629;
19
- --bg-card: #1c1f22;
20
- --border-color: #2e3235;
21
- --text-primary: #c9cbcd;
22
- --text-secondary: #888d93;
23
- --text-muted: #5e6369;
16
+ --bg-primary: #141618;
17
+ --bg-secondary: #1a1d20;
18
+ --bg-tertiary: #212427;
19
+ --bg-card: #1a1d20;
20
+ --bg-hover: #272b2f;
21
+ --border-color: #2c3034;
22
+ --border-strong: #3b4045;
23
+
24
+ --text-primary: #d3d6d9;
25
+ --text-secondary: #8d9299;
26
+ --text-muted: #666b71;
27
+ --text-tertiary: #666b71;
28
+
24
29
  --accent-purple: #7c8af6;
25
30
  --accent-blue: #5c9eed;
26
31
  --accent-green: #45a87a;
27
32
  --accent-amber: #c89030;
28
33
  --accent-rose: #d45d6e;
29
- --accent-indigo: #7c8af6;
30
- --gradient-start: #7c8af6;
31
- --gradient-end: #7c8af6;
34
+ --accent-red: #d45d6e;
35
+
36
+ /* Tints derived from the accents above. Every status surface uses these
37
+ two levels — fill and hairline — so tinted areas stay in one family. */
38
+ --purple-soft: rgba(124, 138, 246, 0.10);
39
+ --purple-line: rgba(124, 138, 246, 0.32);
40
+ --blue-soft: rgba(92, 158, 237, 0.10);
41
+ --blue-line: rgba(92, 158, 237, 0.32);
42
+ --green-soft: rgba(69, 168, 122, 0.10);
43
+ --green-line: rgba(69, 168, 122, 0.32);
44
+ --amber-soft: rgba(200, 144, 48, 0.10);
45
+ --amber-line: rgba(200, 144, 48, 0.34);
46
+ --rose-soft: rgba(212, 93, 110, 0.10);
47
+ --rose-line: rgba(212, 93, 110, 0.34);
48
+
49
+ --btn-primary-bg: #5561d4;
50
+ --btn-primary-bg-hover: #616de0;
51
+ --btn-primary-bg-active: #4a55c2;
52
+ --btn-danger-bg: #b64d5c;
53
+ --btn-danger-bg-hover: #c45a6a;
54
+ --btn-danger-bg-active: #a34351;
55
+
56
+ --flow-gap: 0.875rem;
57
+ --flow-gap-lg: 1.25rem;
58
+
59
+ --radius-sm: 4px;
60
+ --radius: 6px;
61
+ --radius-lg: 8px;
62
+ --control-h: 2.125rem;
63
+ --control-h-sm: 1.75rem;
64
+
65
+ --shadow-1: 0 1px 2px rgba(0, 0, 0, 0.3);
66
+ --shadow-2: 0 6px 20px rgba(0, 0, 0, 0.38);
67
+ --shadow-3: 0 12px 40px rgba(0, 0, 0, 0.45);
68
+
69
+ --font-sans: <%= PgReports.config.load_external_fonts ? "'Inter', " : "" %>-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
70
+ --font-mono: <%= PgReports.config.load_external_fonts ? "'JetBrains Mono', " : "" %>ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
32
71
  }
33
72
 
34
73
  * {
@@ -38,11 +77,28 @@
38
77
  }
39
78
 
40
79
  body {
41
- font-family: <%= PgReports.config.load_external_fonts ? "'Plus Jakarta Sans', " : "" %>-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
80
+ font-family: var(--font-sans);
42
81
  background: var(--bg-primary);
43
82
  color: var(--text-primary);
44
83
  min-height: 100vh;
45
- line-height: 1.6;
84
+ line-height: 1.5;
85
+ font-size: 0.875rem;
86
+ -webkit-font-smoothing: antialiased;
87
+ -moz-osx-font-smoothing: grayscale;
88
+ }
89
+
90
+ /* Numbers that refresh in place must not reflow as digits change. */
91
+ .results-table,
92
+ .query-results-table,
93
+ .metric-value,
94
+ .explain-stat-value {
95
+ font-variant-numeric: tabular-nums;
96
+ }
97
+
98
+ :focus-visible {
99
+ outline: 2px solid var(--accent-blue);
100
+ outline-offset: 2px;
101
+ border-radius: var(--radius-sm);
46
102
  }
47
103
 
48
104
  .container {
@@ -62,89 +118,72 @@
62
118
  }
63
119
 
64
120
  .logo {
65
- display: flex;
66
- align-items: center;
67
- gap: 0.75rem;
68
- }
69
-
70
- .logo-icon {
71
- width: 32px;
72
- height: 32px;
73
- background: var(--accent-purple);
74
- border-radius: 6px;
75
- display: flex;
76
- align-items: center;
77
- justify-content: center;
78
- font-size: 1rem;
79
- }
80
-
81
- .logo-text {
82
121
  display: flex;
83
122
  flex-direction: column;
84
- gap: 5px;
85
- line-height: 1.15;
123
+ gap: 0.125rem;
124
+ min-width: 0;
86
125
  }
87
126
 
88
- .logo-text h1 {
89
- font-size: 1.1rem;
90
- font-weight: 600;
91
- color: var(--text-primary);
92
- line-height: 1.15;
93
- margin-top: -3px;
127
+ .logo h1 {
94
128
  display: flex;
95
129
  align-items: baseline;
96
130
  gap: 0.5rem;
131
+ font-size: 1.0625rem;
132
+ font-weight: 600;
133
+ letter-spacing: -0.01em;
134
+ color: var(--text-primary);
135
+ line-height: 1.2;
97
136
  }
98
137
 
99
138
  .logo-version {
100
- font-size: 0.7rem;
101
- font-weight: 500;
139
+ font-family: var(--font-mono);
140
+ font-size: 0.6875rem;
141
+ font-weight: 400;
102
142
  color: var(--text-muted);
103
- letter-spacing: 0.02em;
104
143
  }
105
144
 
106
145
  .logo-subtitle {
107
- font-size: 0.7rem;
146
+ font-size: 0.75rem;
108
147
  color: var(--text-muted);
109
- line-height: 1.15;
148
+ line-height: 1.2;
110
149
  }
111
150
 
112
151
  .header-badge {
113
- display: flex;
152
+ display: inline-flex;
114
153
  align-items: center;
115
154
  gap: 0.4rem;
116
- height: 2.125rem;
155
+ height: var(--control-h);
117
156
  padding: 0 0.75rem;
118
157
  background: var(--bg-tertiary);
119
158
  border: 1px solid var(--border-color);
120
- border-radius: 4px;
159
+ border-radius: var(--radius-sm);
121
160
  font-size: 0.75rem;
161
+ color: var(--text-secondary);
122
162
  }
123
163
 
124
164
  .header-badge-clickable {
125
165
  cursor: pointer;
126
166
  font-family: inherit;
127
- color: inherit;
128
- transition: border-color 0.15s ease, background 0.15s ease;
167
+ transition: border-color 0.12s ease, background-color 0.12s ease;
129
168
  }
130
169
 
131
170
  .header-badge-clickable:hover {
132
- background: var(--bg-card);
133
- border-color: var(--text-muted);
171
+ background: var(--bg-hover);
172
+ border-color: var(--border-strong);
173
+ color: var(--text-primary);
134
174
  }
135
175
 
136
176
  .badge-info-icon {
137
177
  color: var(--text-muted);
138
- font-size: 0.85rem;
139
- line-height: 1;
178
+ line-height: 0;
140
179
  }
141
180
 
142
181
  .badge-dot {
143
- width: 8px;
144
- height: 8px;
182
+ width: 7px;
183
+ height: 7px;
184
+ flex: none;
145
185
  border-radius: 50%;
146
186
  background: var(--accent-green);
147
- animation: pulse 2s infinite;
148
187
  }
149
188
 
150
189
  .badge-dot.warning {
@@ -153,13 +192,14 @@
153
192
 
154
193
  @keyframes pulse {
155
194
  0%, 100% { opacity: 1; }
156
- 50% { opacity: 0.5; }
195
+ 50% { opacity: 0.45; }
157
196
  }
158
197
 
159
198
  /* Categories Grid */
160
199
  .categories-grid {
161
200
  display: grid;
162
- grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
201
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
202
+ align-items: start;
163
203
  gap: 1rem;
164
204
  margin-bottom: 1.5rem;
165
205
  }
@@ -167,12 +207,13 @@
167
207
  .category-card {
168
208
  background: var(--bg-card);
169
209
  border: 1px solid var(--border-color);
170
- border-radius: 6px;
210
+ border-radius: var(--radius);
171
211
  padding: 1rem;
212
+ transition: border-color 0.12s ease;
172
213
  }
173
214
 
174
215
  .category-card:hover {
175
- border-color: #3e4347;
216
+ border-color: var(--border-strong);
176
217
  }
177
218
 
178
219
  .category-header {
@@ -183,28 +224,25 @@
183
224
  }
184
225
 
185
226
  .category-icon {
186
- width: 28px;
187
- height: 28px;
188
- border-radius: 4px;
189
- display: flex;
190
- align-items: center;
191
- justify-content: center;
192
- font-size: 0.95rem;
193
- background: transparent !important;
194
- color: var(--text-secondary) !important;
227
+ width: 1rem;
228
+ height: 1rem;
229
+ color: var(--text-secondary);
195
230
  }
196
231
 
197
232
  .category-title {
198
- font-size: 1rem;
233
+ font-size: 0.9375rem;
199
234
  font-weight: 600;
235
+ letter-spacing: -0.01em;
200
236
  }
201
237
 
202
238
  .category-count {
203
239
  margin-left: auto;
204
- padding: 0.2rem 0.6rem;
240
+ padding: 0.15rem 0.45rem;
205
241
  background: var(--bg-tertiary);
206
- border-radius: 4px;
207
- font-size: 0.7rem;
242
+ border: 1px solid var(--border-color);
243
+ border-radius: var(--radius-sm);
244
+ font-size: 0.6875rem;
245
+ font-variant-numeric: tabular-nums;
208
246
  color: var(--text-muted);
209
247
  }
210
248
 
@@ -217,15 +255,16 @@
217
255
  .report-link {
218
256
  display: flex;
219
257
  align-items: center;
258
+ gap: 0.75rem;
220
259
  justify-content: space-between;
221
- padding: 0.45rem 0.7rem;
260
+ padding: 0.4rem 0.6rem;
222
261
  background: transparent;
223
262
  border: 1px solid transparent;
224
- border-radius: 4px;
263
+ border-radius: var(--radius-sm);
225
264
  color: var(--text-secondary);
226
265
  text-decoration: none;
227
- font-size: 0.82rem;
228
- transition: background 0.1s, color 0.1s;
266
+ font-size: 0.8125rem;
267
+ transition: background-color 0.1s ease, color 0.1s ease;
229
268
  }
230
269
 
231
270
  .report-link:hover {
@@ -234,26 +273,29 @@
234
273
  }
235
274
 
236
275
  .report-link .arrow {
276
+ width: 0.8125rem;
277
+ height: 0.8125rem;
278
+ color: var(--text-muted);
237
279
  opacity: 0;
238
- transform: translateX(-4px);
239
- transition: all 0.15s ease;
280
+ transition: opacity 0.12s ease;
240
281
  }
241
282
 
242
- .report-link:hover .arrow {
283
+ .report-link:hover .arrow,
284
+ .report-link:focus-visible .arrow {
243
285
  opacity: 1;
244
- transform: translateX(0);
245
286
  }
246
287
 
247
288
  .report-badge-new {
248
289
  display: inline-block;
249
290
  margin-left: 0.4rem;
250
- padding: 0.1rem 0.35rem;
251
- font-size: 0.55rem;
291
+ padding: 0.05rem 0.3rem;
292
+ font-size: 0.5625rem;
252
293
  font-weight: 600;
253
- letter-spacing: 0.04em;
254
- line-height: 1.2;
294
+ letter-spacing: 0.06em;
295
+ line-height: 1.4;
255
296
  color: var(--accent-green);
256
- background: rgba(69, 168, 122, 0.12);
297
+ background: var(--green-soft);
298
+ border: 1px solid var(--green-line);
257
299
  border-radius: 3px;
258
300
  vertical-align: middle;
259
301
  }
@@ -269,6 +311,7 @@
269
311
  display: flex;
270
312
  align-items: center;
271
313
  gap: 0.4rem;
314
+ min-height: var(--control-h);
272
315
  font-size: 0.75rem;
273
316
  color: var(--text-muted);
274
317
  }
@@ -276,13 +319,41 @@
276
319
  .breadcrumb a {
277
320
  color: var(--text-secondary);
278
321
  text-decoration: none;
279
- transition: color 0.15s;
322
+ transition: color 0.12s ease, background-color 0.12s ease;
280
323
  }
281
324
 
282
325
  .breadcrumb a:hover {
283
326
  color: var(--text-primary);
284
327
  }
285
328
 
329
+ /* First crumb doubles as the back action — the arrow makes the affordance
330
+ explicit without a second control duplicating it in the header. */
331
+ .breadcrumb-back {
332
+ display: inline-flex;
333
+ align-items: center;
334
+ gap: 0.35rem;
335
+ margin-left: -0.4rem;
336
+ padding: 0.2rem 0.45rem;
337
+ border-radius: var(--radius-sm);
338
+ }
339
+
340
+ .breadcrumb-back:hover {
341
+ background: var(--bg-tertiary);
342
+ }
343
+
344
+ .breadcrumb-back .icon {
345
+ width: 0.8125rem;
346
+ height: 0.8125rem;
347
+ }
348
+
349
+ .breadcrumb-sep {
350
+ color: var(--border-strong);
351
+ }
352
+
353
+ .breadcrumb-current {
354
+ color: var(--text-secondary);
355
+ }
356
+
286
357
  .report-header {
287
358
  display: flex;
288
359
  align-items: flex-start;
@@ -306,62 +377,163 @@
306
377
  gap: 0.5rem;
307
378
  }
308
379
 
380
+ /* Buttons — one solid, classic control. Height, radius and weight come from
381
+ the tokens so every variant and every page lines up on the same grid. */
309
382
  .btn {
310
383
  display: inline-flex;
311
384
  align-items: center;
312
385
  justify-content: center;
313
386
  gap: 0.4rem;
314
- height: 36px;
315
- padding: 0 1rem;
387
+ height: var(--control-h);
388
+ padding: 0 0.875rem;
316
389
  border: 1px solid transparent;
317
- border-radius: 4px;
390
+ border-radius: var(--radius-sm);
318
391
  font-family: inherit;
319
- font-size: 0.8rem;
392
+ font-size: 0.8125rem;
320
393
  font-weight: 500;
394
+ line-height: 1;
321
395
  cursor: pointer;
322
- transition: background 0.1s;
396
+ user-select: none;
323
397
  white-space: nowrap;
324
398
  text-decoration: none;
399
+ transition: background-color 0.12s ease, border-color 0.12s ease, color 0.12s ease;
325
400
  }
326
401
 
327
402
  .btn-primary {
328
- background: rgba(124, 138, 246, 0.14);
329
- color: var(--accent-purple);
330
- border-color: rgba(124, 138, 246, 0.35);
403
+ background: var(--btn-primary-bg);
404
+ border-color: var(--btn-primary-bg);
405
+ color: #fff;
331
406
  }
332
407
 
333
408
  .btn-primary:hover {
334
- background: rgba(124, 138, 246, 0.24);
335
- border-color: rgba(124, 138, 246, 0.55);
409
+ background: var(--btn-primary-bg-hover);
410
+ border-color: var(--btn-primary-bg-hover);
336
411
  }
337
412
 
338
- .btn-secondary {
413
+ .btn-primary:active {
414
+ background: var(--btn-primary-bg-active);
415
+ border-color: var(--btn-primary-bg-active);
416
+ }
417
+
418
+ .btn-secondary,
419
+ .btn-telegram {
339
420
  background: var(--bg-tertiary);
421
+ color: var(--text-primary);
422
+ border-color: var(--border-color);
423
+ }
424
+
425
+ .btn-secondary:hover,
426
+ .btn-telegram:hover {
427
+ background: var(--bg-hover);
428
+ border-color: var(--border-strong);
429
+ }
430
+
431
+ .btn-secondary:active,
432
+ .btn-telegram:active {
433
+ background: var(--bg-tertiary);
434
+ }
435
+
436
+ .btn-ghost {
437
+ background: transparent;
340
438
  color: var(--text-secondary);
341
- border: 1px solid var(--border-color);
439
+ border-color: transparent;
342
440
  }
343
441
 
344
- .btn-secondary:hover {
345
- background: #2a2d31;
442
+ .btn-ghost:hover {
443
+ background: var(--bg-tertiary);
346
444
  color: var(--text-primary);
347
445
  }
348
446
 
349
- .btn-telegram {
447
+ .btn-danger {
448
+ background: var(--btn-danger-bg);
449
+ border-color: var(--btn-danger-bg);
450
+ color: #fff;
451
+ }
452
+
453
+ .btn-danger:hover {
454
+ background: var(--btn-danger-bg-hover);
455
+ border-color: var(--btn-danger-bg-hover);
456
+ }
457
+
458
+ .btn-danger:active {
459
+ background: var(--btn-danger-bg-active);
460
+ border-color: var(--btn-danger-bg-active);
461
+ }
462
+
463
+ /* Destructive, but not the page's main action: neutral until hovered. */
464
+ .btn-muted {
350
465
  background: var(--bg-tertiary);
466
+ border-color: var(--border-color);
351
467
  color: var(--text-secondary);
468
+ }
469
+
470
+ .btn-muted:hover {
471
+ background: var(--rose-soft);
472
+ border-color: var(--rose-line);
473
+ color: var(--accent-rose);
474
+ }
475
+
476
+ .btn-small {
477
+ height: var(--control-h-sm);
478
+ padding: 0 0.625rem;
479
+ font-size: 0.75rem;
480
+ }
481
+
482
+ /* Square control sized to match .btn — for a lone icon with no label. */
483
+ .btn-icon {
484
+ width: var(--control-h);
485
+ height: var(--control-h);
486
+ padding: 0;
487
+ flex: none;
488
+ display: inline-flex;
489
+ align-items: center;
490
+ justify-content: center;
491
+ background: var(--bg-tertiary);
352
492
  border: 1px solid var(--border-color);
493
+ border-radius: var(--radius-sm);
494
+ color: var(--text-secondary);
495
+ cursor: pointer;
496
+ transition: background-color 0.12s ease, border-color 0.12s ease, color 0.12s ease;
353
497
  }
354
498
 
355
- .btn-telegram:hover {
356
- background: #2a2d31;
499
+ .btn-icon:hover {
500
+ background: var(--bg-hover);
501
+ border-color: var(--border-strong);
357
502
  color: var(--text-primary);
358
503
  }
359
504
 
360
- .btn:disabled {
361
- opacity: 0.5;
505
+ .btn-small.btn-icon {
506
+ width: var(--control-h-sm);
507
+ height: var(--control-h-sm);
508
+ }
509
+
510
+ .btn:disabled,
511
+ .btn-icon:disabled {
512
+ opacity: 0.45;
362
513
  cursor: not-allowed;
363
514
  }
364
515
 
516
+ .btn:disabled:hover,
517
+ .btn-icon:disabled:hover {
518
+ background: inherit;
519
+ border-color: inherit;
520
+ }
521
+
522
+ /* Icons — stroke-drawn, inherit the parent's colour and optical size. */
523
+ .icon {
524
+ width: 1em;
525
+ height: 1em;
526
+ flex: none;
527
+ fill: none;
528
+ stroke: currentColor;
529
+ stroke-width: 1.75;
530
+ stroke-linecap: round;
531
+ stroke-linejoin: round;
532
+ }
533
+
534
+ .icon-lg { width: 1.125rem; height: 1.125rem; }
535
+ .icon-sm { width: 0.875rem; height: 0.875rem; }
536
+
365
537
  /* Results Table */
366
538
  .results-container {
367
539
  background: var(--bg-card);
@@ -468,63 +640,177 @@
468
640
  }
469
641
 
470
642
  /* Error state */
643
+ .error-title {
644
+ font-weight: 600;
645
+ color: var(--accent-rose);
646
+ }
647
+
471
648
  .error-message {
472
- padding: 0.875rem;
473
- background: rgba(244, 63, 94, 0.1);
474
- border: 1px solid rgba(244, 63, 94, 0.3);
475
- border-radius: 6px;
649
+ padding: 0.875rem 1rem;
650
+ background: var(--rose-soft);
651
+ border: 1px solid var(--rose-line);
652
+ border-radius: var(--radius);
476
653
  color: var(--accent-rose);
477
- font-size: 0.85rem;
654
+ font-size: 0.8125rem;
655
+ line-height: 1.6;
656
+ }
657
+
658
+ .error-message .code-block {
659
+ color: var(--text-primary);
660
+ }
661
+
662
+ /* Vertical rhythm. The global `* { margin: 0 }` reset leaves prose
663
+ containers with no spacing of their own, and per-element margins drifted
664
+ into asymmetric gaps (1rem above a code block, 0 below). One rule per
665
+ container spaces any two siblings identically, whatever their tags. */
666
+ .modal-body > * + *,
667
+ .modal-body li > * + *,
668
+ .error-message > * + *,
669
+ .problem-modal-body > * + *,
670
+ .doc-block > * + *,
671
+ .explain-query-section > * + *,
672
+ .explain-params-section > * + *,
673
+ #explain-content > * + *,
674
+ #run-query-content > * + *,
675
+ .callout-body > * + * {
676
+ margin-top: var(--flow-gap);
677
+ }
678
+
679
+ .modal-body > ol > li + li {
680
+ margin-top: var(--flow-gap-lg);
681
+ }
682
+
683
+ .modal-body > .modal-actions,
684
+ .modal-body > .migration-actions {
685
+ margin-top: var(--flow-gap-lg);
686
+ }
687
+
688
+ /* Code block — one definition for every fenced snippet in the UI. */
689
+ .code-block,
690
+ .modal-body pre {
691
+ display: block;
692
+ padding: 0.6875rem 0.875rem;
693
+ background: var(--bg-primary);
694
+ border: 1px solid var(--border-color);
695
+ border-radius: var(--radius-sm);
696
+ font-family: var(--font-mono);
697
+ font-size: 0.78rem;
698
+ line-height: 1.6;
699
+ color: var(--text-primary);
700
+ white-space: pre;
701
+ overflow-x: auto;
702
+ tab-size: 2;
703
+ }
704
+
705
+ .code-block-wrap {
706
+ white-space: pre-wrap;
707
+ word-break: break-word;
708
+ }
709
+
710
+ /* Callout — an inline notice inside a panel or modal. */
711
+ .callout {
712
+ display: flex;
713
+ align-items: flex-start;
714
+ gap: 0.5rem;
715
+ padding: 0.75rem 0.875rem;
716
+ border: 1px solid var(--border-color);
717
+ border-radius: var(--radius-sm);
718
+ margin-bottom: 1rem;
719
+ font-size: 0.8125rem;
720
+ line-height: 1.6;
721
+ }
722
+
723
+ .callout .icon {
724
+ margin-top: 0.22rem;
725
+ width: 0.875rem;
726
+ height: 0.875rem;
478
727
  }
479
728
 
480
- /* Toast notification */
729
+ .callout p {
730
+ margin: 0.25rem 0 0;
731
+ color: var(--text-secondary);
732
+ }
733
+
734
+ .callout-warning {
735
+ background: var(--amber-soft);
736
+ border-color: var(--amber-line);
737
+ color: var(--accent-amber);
738
+ }
739
+
740
+ .callout-danger {
741
+ background: var(--rose-soft);
742
+ border-color: var(--rose-line);
743
+ color: var(--accent-rose);
744
+ }
745
+
746
+ /* Page-level flash sits above the header, like .db-error-banner. */
747
+ .page-notice {
748
+ margin-bottom: 1.25rem;
749
+ }
750
+
751
+ /* Toast notification — one definition for every page. */
481
752
  .toast {
482
753
  position: fixed;
483
754
  bottom: 1.5rem;
484
755
  right: 1.5rem;
485
- padding: 0.75rem 1rem;
486
- background: var(--bg-card);
487
- border: 1px solid var(--border-color);
488
- border-radius: 6px;
756
+ max-width: 26rem;
757
+ padding: 0.625rem 0.875rem;
758
+ background: var(--bg-tertiary);
759
+ border: 1px solid var(--border-strong);
760
+ border-radius: var(--radius);
489
761
  color: var(--text-primary);
490
- font-size: 0.85rem;
491
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
492
- transform: translateY(100px);
762
+ font-size: 0.8125rem;
763
+ box-shadow: var(--shadow-2);
764
+ transform: translateY(0.5rem);
493
765
  opacity: 0;
494
- transition: all 0.3s ease;
495
- z-index: 1000;
766
+ visibility: hidden;
767
+ transition: opacity 0.18s ease, transform 0.18s ease, visibility 0.18s;
768
+ z-index: 2000;
496
769
  }
497
770
 
498
771
  .toast.show {
499
772
  transform: translateY(0);
500
773
  opacity: 1;
774
+ visibility: visible;
501
775
  }
502
776
 
503
- .toast.success {
504
- border-color: var(--accent-green);
777
+ .toast.success,
778
+ .toast.toast-success {
779
+ border-color: var(--green-line);
780
+ background: var(--green-soft);
505
781
  }
506
782
 
507
- .toast.error {
508
- border-color: var(--accent-rose);
783
+ .toast.error,
784
+ .toast.toast-error {
785
+ border-color: var(--rose-line);
786
+ background: var(--rose-soft);
787
+ }
788
+
789
+ .toast.warning,
790
+ .toast.toast-warning {
791
+ border-color: var(--amber-line);
792
+ background: var(--amber-soft);
509
793
  }
510
794
 
511
795
  /* Modal */
512
796
  .modal {
513
797
  position: fixed;
514
798
  inset: 0;
515
- background: rgba(0, 0, 0, 0.6);
799
+ background: rgba(10, 11, 12, 0.72);
516
800
  display: flex;
517
801
  align-items: center;
518
802
  justify-content: center;
803
+ padding: 1.5rem;
519
804
  z-index: 1000;
520
805
  }
521
806
 
522
807
  .modal-content {
523
808
  background: var(--bg-card);
524
- border: 1px solid var(--border-color);
525
- border-radius: 6px;
809
+ border: 1px solid var(--border-strong);
810
+ border-radius: var(--radius);
811
+ box-shadow: var(--shadow-3);
526
812
  max-width: 900px;
527
- width: 90%;
813
+ width: 100%;
528
814
  max-height: 90vh;
529
815
  overflow: auto;
530
816
  scrollbar-width: thin;
@@ -578,21 +864,25 @@
578
864
  }
579
865
 
580
866
  .modal-close {
581
- width: 28px;
582
- height: 28px;
583
- background: var(--bg-tertiary);
584
- border: 1px solid var(--border-color);
585
- border-radius: 4px;
867
+ width: 1.75rem;
868
+ height: 1.75rem;
869
+ display: inline-flex;
870
+ align-items: center;
871
+ justify-content: center;
872
+ background: transparent;
873
+ border: 1px solid transparent;
874
+ border-radius: var(--radius-sm);
586
875
  color: var(--text-muted);
587
876
  font-size: 1.125rem;
877
+ line-height: 1;
588
878
  cursor: pointer;
589
- transition: all 0.15s;
879
+ transition: background-color 0.12s ease, color 0.12s ease;
590
880
  }
591
881
 
592
882
  .modal-close:hover {
593
- background: var(--accent-rose);
594
- border-color: var(--accent-rose);
595
- color: white;
883
+ background: var(--bg-tertiary);
884
+ border-color: var(--border-color);
885
+ color: var(--text-primary);
596
886
  }
597
887
 
598
888
  .modal-body {
@@ -611,18 +901,66 @@
611
901
  align-items: flex-start;
612
902
  }
613
903
 
904
+ .header-actions {
905
+ width: 100%;
906
+ flex-wrap: wrap;
907
+ }
908
+
614
909
  .report-header {
615
910
  flex-direction: column;
911
+ gap: 0.75rem;
616
912
  }
617
913
 
618
914
  .report-actions {
619
915
  width: 100%;
916
+ flex-wrap: wrap;
620
917
  }
621
918
 
622
- .btn {
919
+ /* Only action bars stretch their buttons — elsewhere a stretched button
920
+ overruns the label sitting next to it. */
921
+ .header-actions .btn,
922
+ .report-actions .btn,
923
+ .modal-actions .btn {
623
924
  flex: 1;
624
925
  justify-content: center;
625
926
  }
927
+
928
+ /* The selectors take a line of their own instead of squeezing the trail. */
929
+ .breadcrumb {
930
+ flex-wrap: wrap;
931
+ row-gap: 0.5rem;
932
+ }
933
+
934
+ .breadcrumb-spacer {
935
+ flex-basis: 100%;
936
+ }
937
+
938
+ .results-header {
939
+ flex-direction: column;
940
+ align-items: flex-start;
941
+ gap: 0.5rem;
942
+ }
943
+
944
+ .results-meta {
945
+ flex-wrap: wrap;
946
+ gap: 0.25rem 0.75rem;
947
+ }
948
+
949
+ .modal {
950
+ padding: 0.75rem;
951
+ }
952
+
953
+ .modal-large {
954
+ width: 100%;
955
+ }
956
+
957
+ /* Placed by `right`, a toast wider than the screen hangs off the left. */
958
+ .toast {
959
+ left: 0.875rem;
960
+ right: 0.875rem;
961
+ bottom: 0.875rem;
962
+ max-width: none;
963
+ }
626
964
  }
627
965
 
628
966
  /* ============================================
@@ -677,7 +1015,7 @@
677
1015
  color: var(--text-secondary);
678
1016
  border: 1px solid var(--border-color);
679
1017
  border-radius: 6px;
680
- box-shadow: 0 6px 18px rgba(0, 0, 0, 0.35);
1018
+ box-shadow: var(--shadow-2);
681
1019
  font-size: 0.75rem;
682
1020
  font-weight: 400;
683
1021
  line-height: 1.5;
@@ -797,7 +1135,7 @@
797
1135
  }
798
1136
 
799
1137
  .query-item:hover {
800
- border-color: var(--accent-purple);
1138
+ border-color: var(--border-strong);
801
1139
  }
802
1140
 
803
1141
  .query-item:last-child {
@@ -917,7 +1255,7 @@
917
1255
  background: var(--bg-card);
918
1256
  border: 1px solid var(--border-color);
919
1257
  border-radius: 6px;
920
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
1258
+ box-shadow: var(--shadow-2);
921
1259
  z-index: 1000;
922
1260
  min-width: 160px;
923
1261
  overflow: hidden;
@@ -961,7 +1299,7 @@
961
1299
  background: var(--bg-card);
962
1300
  border: 1px solid var(--border-color);
963
1301
  border-radius: 6px;
964
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
1302
+ box-shadow: var(--shadow-2);
965
1303
  z-index: 100;
966
1304
  overflow: hidden;
967
1305
  }
@@ -985,24 +1323,6 @@
985
1323
  color: var(--text-primary);
986
1324
  }
987
1325
 
988
- .btn-small {
989
- height: 30px;
990
- padding: 0 0.875rem;
991
- font-size: 0.75rem;
992
- line-height: 1;
993
- }
994
-
995
- .btn-danger {
996
- background: var(--accent-rose);
997
- color: white;
998
- border-color: var(--accent-rose);
999
- }
1000
-
1001
- .btn-danger:hover {
1002
- opacity: 0.9;
1003
- transform: translateY(-1px);
1004
- }
1005
-
1006
1326
  /* IDE Settings Modal */
1007
1327
  .settings-label {
1008
1328
  color: var(--text-secondary);
@@ -1010,44 +1330,57 @@
1010
1330
  font-size: 0.9rem;
1011
1331
  }
1012
1332
 
1013
- .ide-options {
1333
+ /* Settings modal sections. The option rows are shared by every section —
1334
+ language, IDE, and whatever is added next. */
1335
+ .settings-section-title {
1336
+ font-size: 0.6875rem;
1337
+ font-weight: 600;
1338
+ line-height: 1.4;
1339
+ color: var(--text-muted);
1340
+ text-transform: uppercase;
1341
+ letter-spacing: 0.06em;
1342
+ margin-bottom: 0.5rem;
1343
+ }
1344
+
1345
+ .settings-options {
1014
1346
  display: flex;
1015
1347
  flex-direction: column;
1016
- gap: 0.5rem;
1348
+ gap: 0.375rem;
1017
1349
  }
1018
1350
 
1019
- .ide-option {
1351
+ .settings-option {
1020
1352
  display: flex;
1021
1353
  align-items: center;
1022
- gap: 0.75rem;
1023
- padding: 0.75rem 1rem;
1354
+ gap: 0.625rem;
1355
+ padding: 0.5rem 0.75rem;
1024
1356
  background: var(--bg-tertiary);
1025
1357
  border: 1px solid var(--border-color);
1026
- border-radius: 6px;
1358
+ border-radius: var(--radius-sm);
1027
1359
  cursor: pointer;
1028
- transition: all 0.15s;
1360
+ transition: background-color 0.12s ease, border-color 0.12s ease;
1029
1361
  }
1030
1362
 
1031
- .ide-option:hover {
1032
- background: var(--bg-secondary);
1033
- border-color: var(--accent-purple);
1363
+ .settings-option:hover {
1364
+ background: var(--bg-hover);
1365
+ border-color: var(--border-strong);
1034
1366
  }
1035
1367
 
1036
- .ide-option:has(input:checked) {
1037
- background: rgba(157, 140, 214, 0.15);
1038
- border-color: var(--accent-purple);
1368
+ .settings-option:has(input:checked) {
1369
+ background: var(--purple-soft);
1370
+ border-color: var(--purple-line);
1039
1371
  }
1040
1372
 
1041
- .ide-option input[type="radio"] {
1373
+ .settings-option input[type="radio"] {
1042
1374
  accent-color: var(--accent-purple);
1375
+ flex: none;
1043
1376
  }
1044
1377
 
1045
- .ide-option span {
1378
+ .settings-option span {
1046
1379
  color: var(--text-secondary);
1047
- font-size: 0.875rem;
1380
+ font-size: 0.8125rem;
1048
1381
  }
1049
1382
 
1050
- .ide-option:has(input:checked) span {
1383
+ .settings-option:has(input:checked) span {
1051
1384
  color: var(--text-primary);
1052
1385
  }
1053
1386
 
@@ -1081,12 +1414,14 @@
1081
1414
  }
1082
1415
 
1083
1416
  .db-selector {
1084
- background: var(--bg-card, rgba(255, 255, 255, 0.04));
1417
+ background: var(--bg-tertiary);
1085
1418
  color: var(--text-primary);
1086
- border: 1px solid var(--border-color, rgba(255, 255, 255, 0.1));
1087
- border-radius: 6px;
1088
- padding: 0.375rem 0.625rem;
1089
- font-size: 0.875rem;
1419
+ border: 1px solid var(--border-color);
1420
+ border-radius: var(--radius-sm);
1421
+ height: var(--control-h-sm);
1422
+ padding: 0 0.5rem;
1423
+ font-family: inherit;
1424
+ font-size: 0.8125rem;
1090
1425
  cursor: pointer;
1091
1426
  width: auto;
1092
1427
  max-width: 100%;
@@ -1098,13 +1433,12 @@
1098
1433
  }
1099
1434
 
1100
1435
  .db-selector:hover {
1101
- border-color: var(--accent-blue, #6b9fe8);
1436
+ border-color: var(--border-strong);
1102
1437
  }
1103
1438
 
1104
- .db-selector:focus {
1105
- outline: none;
1106
- border-color: var(--accent-blue, #6b9fe8);
1107
- box-shadow: 0 0 0 2px rgba(107, 159, 232, 0.2);
1439
+ .db-selector:focus-visible {
1440
+ outline: 2px solid var(--accent-blue);
1441
+ outline-offset: 1px;
1108
1442
  }
1109
1443
 
1110
1444
  .db-selector-static {
@@ -1124,11 +1458,11 @@
1124
1458
  align-items: center;
1125
1459
  gap: 0.375rem;
1126
1460
  padding: 0.375rem 0.625rem;
1127
- background: rgba(232, 107, 107, 0.1);
1128
- border: 1px solid rgba(232, 107, 107, 0.3);
1129
- border-radius: 6px;
1461
+ background: var(--rose-soft);
1462
+ border: 1px solid var(--rose-line);
1463
+ border-radius: var(--radius-sm);
1130
1464
  font-size: 0.8125rem;
1131
- color: var(--accent-red, #e86b6b);
1465
+ color: var(--accent-red);
1132
1466
  cursor: help;
1133
1467
  }
1134
1468
 
@@ -1136,47 +1470,123 @@
1136
1470
  flex: 1;
1137
1471
  }
1138
1472
 
1139
- .breadcrumb {
1140
- display: flex;
1141
- align-items: center;
1142
- gap: 0.5rem;
1143
- }
1144
-
1145
1473
  .db-error-banner {
1146
- background: rgba(232, 107, 107, 0.08);
1147
- border: 1px solid rgba(232, 107, 107, 0.3);
1148
- border-radius: 8px;
1149
- padding: 0.875rem 1.125rem;
1474
+ background: var(--rose-soft);
1475
+ border: 1px solid var(--rose-line);
1476
+ border-radius: var(--radius);
1477
+ padding: 0.875rem 1rem;
1150
1478
  margin-bottom: 1rem;
1151
1479
  display: flex;
1152
1480
  flex-direction: column;
1153
- gap: 0.375rem;
1481
+ align-items: flex-start;
1482
+ gap: var(--flow-gap);
1154
1483
  }
1155
1484
 
1156
1485
  .db-error-banner strong {
1157
- color: var(--accent-red, #e86b6b);
1158
- font-size: 0.9375rem;
1486
+ color: var(--accent-red);
1487
+ font-weight: 600;
1159
1488
  }
1160
1489
 
1161
1490
  .db-error-banner span {
1162
- font-size: 0.875rem;
1163
1491
  color: var(--text-secondary);
1164
1492
  }
1165
1493
 
1166
1494
  .db-error-banner code {
1167
- font-family: ui-monospace, SFMono-Regular, monospace;
1168
- font-size: 0.8125rem;
1169
- background: rgba(0, 0, 0, 0.25);
1170
- padding: 0.375rem 0.625rem;
1171
- border-radius: 4px;
1495
+ font-family: var(--font-mono);
1496
+ font-size: 0.78rem;
1497
+ line-height: 1.6;
1498
+ background: var(--bg-primary);
1499
+ border: 1px solid var(--border-color);
1500
+ padding: 0.5rem 0.75rem;
1501
+ border-radius: var(--radius-sm);
1172
1502
  color: var(--text-primary);
1173
- display: inline-block;
1174
- width: fit-content;
1503
+ max-width: 100%;
1504
+ overflow-x: auto;
1505
+ }
1506
+
1507
+ /* Responsive, for the panels defined above this point. */
1508
+ @media (max-width: 768px) {
1509
+ .query-monitoring-header {
1510
+ flex-direction: column;
1511
+ align-items: flex-start;
1512
+ gap: 0.75rem;
1513
+ }
1514
+
1515
+ .query-monitoring-title {
1516
+ flex-wrap: wrap;
1517
+ }
1518
+
1519
+ .query-monitoring-controls {
1520
+ width: 100%;
1521
+ flex-wrap: wrap;
1522
+ }
1523
+
1524
+ /* Anchored to its chip, this tooltip runs off the screen and stretches
1525
+ the page with it; on a phone it reads as a bottom sheet instead. */
1526
+ .query-monitoring-scope::after {
1527
+ position: fixed;
1528
+ inset: auto 0.875rem 0.875rem;
1529
+ width: auto;
1530
+ max-width: none;
1531
+ }
1175
1532
  }
1176
1533
  </style>
1177
1534
  </head>
1178
1535
  <body>
1536
+ <%# Icon sprite. One definition per glyph, referenced with
1537
+ <svg class="icon"><use href="#i-name"></use></svg>. Stroke-drawn and
1538
+ colourless so every icon inherits its context's colour and size. %>
1539
+ <svg width="0" height="0" aria-hidden="true" focusable="false" style="position:absolute">
1540
+ <defs>
1541
+ <symbol id="i-queries" viewBox="0 0 24 24"><path d="M13 2 4 14h7l-1 8 9-12h-7l1-8Z"/></symbol>
1542
+ <symbol id="i-indexes" viewBox="0 0 24 24"><path d="M8 6h13M8 12h13M8 18h13M3.5 6h.01M3.5 12h.01M3.5 18h.01"/></symbol>
1543
+ <symbol id="i-tables" viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M3 9.5h18M9 9.5V20"/></symbol>
1544
+ <symbol id="i-connections" viewBox="0 0 24 24"><path d="M9 2v6M15 2v6M6 8h12v4a6 6 0 0 1-12 0V8ZM12 18v4"/></symbol>
1545
+ <symbol id="i-system" viewBox="0 0 24 24"><rect x="2.5" y="4" width="19" height="12" rx="2"/><path d="M8 20h8M12 16v4"/></symbol>
1546
+ <symbol id="i-schema" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m20 20-4.2-4.2"/></symbol>
1547
+ <symbol id="i-link" viewBox="0 0 24 24"><path d="M10 13a5 5 0 0 0 7.5.5l3-3a5 5 0 0 0-7-7L11.8 5.2"/><path d="M14 11a5 5 0 0 0-7.5-.5l-3 3a5 5 0 0 0 7 7l1.7-1.7"/></symbol>
1548
+ <symbol id="i-activity" viewBox="0 0 24 24"><path d="M3 12h4l3 8 4-16 3 8h4"/></symbol>
1549
+ <symbol id="i-database" viewBox="0 0 24 24"><ellipse cx="12" cy="5.5" rx="8" ry="3.5"/><path d="M4 5.5v13c0 1.9 3.6 3.5 8 3.5s8-1.6 8-3.5v-13M4 12c0 1.9 3.6 3.5 8 3.5s8-1.6 8-3.5"/></symbol>
1550
+ <symbol id="i-clock" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 7v5.2l3.2 2"/></symbol>
1551
+ <symbol id="i-lock" viewBox="0 0 24 24"><rect x="4" y="10.5" width="16" height="10.5" rx="2"/><path d="M8 10.5V7a4 4 0 0 1 8 0v3.5"/></symbol>
1552
+ <symbol id="i-settings" viewBox="0 0 24 24"><circle cx="12" cy="12" r="3.2"/><path d="M19.4 14.6a1.7 1.7 0 0 0 .34 1.87l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.7 1.7 0 0 0-1.87-.34 1.7 1.7 0 0 0-1.03 1.56V21a2 2 0 1 1-4 0v-.1A1.7 1.7 0 0 0 8.9 19.3a1.7 1.7 0 0 0-1.87.34l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.7 1.7 0 0 0 .34-1.87 1.7 1.7 0 0 0-1.56-1.03H3a2 2 0 1 1 0-4h.1A1.7 1.7 0 0 0 4.7 8.9a1.7 1.7 0 0 0-.34-1.87l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.7 1.7 0 0 0 1.87.34H9.1A1.7 1.7 0 0 0 10.13 3V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1.03 1.56 1.7 1.7 0 0 0 1.87-.34l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.7 1.7 0 0 0-.34 1.87v.08A1.7 1.7 0 0 0 21 10.13H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1.03Z"/></symbol>
1553
+ <symbol id="i-check" viewBox="0 0 24 24"><path d="m4.5 12.5 5 5 10-11"/></symbol>
1554
+ <symbol id="i-alert" viewBox="0 0 24 24"><path d="M12 3.5 2.8 19.5h18.4L12 3.5Z"/><path d="M12 9.5v4.2M12 17h.01"/></symbol>
1555
+ <symbol id="i-info" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 11v5.5M12 7.8h.01"/></symbol>
1556
+ <symbol id="i-play" viewBox="0 0 24 24"><path d="M7 4.5v15l13-7.5-13-7.5Z"/></symbol>
1557
+ <symbol id="i-pause" viewBox="0 0 24 24"><path d="M9 4.5v15M15 4.5v15"/></symbol>
1558
+ <symbol id="i-chevron-right" viewBox="0 0 24 24"><path d="m9.5 5.5 6.5 6.5-6.5 6.5"/></symbol>
1559
+ <symbol id="i-arrow-left" viewBox="0 0 24 24"><path d="M20 12H4.5M10.5 5.5 4 12l6.5 6.5"/></symbol>
1560
+ <symbol id="i-download" viewBox="0 0 24 24"><path d="M12 3.5v12M7 11l5 5 5-5M4 20h16"/></symbol>
1561
+ <symbol id="i-copy" viewBox="0 0 24 24"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M15 6V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h1"/></symbol>
1562
+ <symbol id="i-send" viewBox="0 0 24 24"><path d="M21 3 3 10.5l7 3 3 7L21 3Z"/></symbol>
1563
+ <symbol id="i-trash" viewBox="0 0 24 24"><path d="M4 6.5h16M9.5 6.5V4h5v2.5M6.5 6.5 7.4 20a1 1 0 0 0 1 1h7.2a1 1 0 0 0 1-1l.9-13.5"/></symbol>
1564
+ <symbol id="i-pin" viewBox="0 0 24 24"><path d="M9 3.5h6l-.8 5.2 3.3 3.3H6.5l3.3-3.3L9 3.5ZM12 12v8.5"/></symbol>
1565
+ <symbol id="i-file" viewBox="0 0 24 24"><path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8l-5-5Z"/><path d="M14 3v5h5"/></symbol>
1566
+ <symbol id="i-book" viewBox="0 0 24 24"><path d="M4 4.5A1.5 1.5 0 0 1 5.5 3H19v15H5.5A1.5 1.5 0 0 0 4 19.5v-15ZM4 19.5A1.5 1.5 0 0 0 5.5 21H19v-3"/></symbol>
1567
+ <symbol id="i-external" viewBox="0 0 24 24"><path d="M14 4h6v6M20 4l-8.5 8.5"/><path d="M18 14v5a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h5"/></symbol>
1568
+ <symbol id="i-bulb" viewBox="0 0 24 24"><path d="M9.5 18h5M10 21h4"/><path d="M12 3a6 6 0 0 0-3.5 10.9c.6.5 1 1.3 1 2.1h5c0-.8.4-1.6 1-2.1A6 6 0 0 0 12 3Z"/></symbol>
1569
+ <symbol id="i-question" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M9.5 9.5a2.5 2.5 0 1 1 3.4 2.3c-.6.2-.9.8-.9 1.4v.6M12 16.8h.01"/></symbol>
1570
+ <symbol id="i-sliders" viewBox="0 0 24 24"><path d="M4 7h10M18 7h2M4 17h4M12 17h8"/><circle cx="16" cy="7" r="2"/><circle cx="10" cy="17" r="2"/></symbol>
1571
+ </defs>
1572
+ </svg>
1573
+
1179
1574
  <div class="container">
1575
+ <%# Redirects out of an unavailable report set flash[:alert]; without this
1576
+ the user would just land back on the dashboard with no explanation. %>
1577
+ <% if flash[:alert].present? %>
1578
+ <div class="callout callout-warning page-notice" role="alert">
1579
+ <svg class="icon" aria-hidden="true"><use href="#i-alert"></use></svg>
1580
+ <span><%= flash[:alert] %></span>
1581
+ </div>
1582
+ <% end %>
1583
+ <% if flash[:notice].present? %>
1584
+ <div class="callout page-notice" role="status">
1585
+ <svg class="icon" aria-hidden="true"><use href="#i-info"></use></svg>
1586
+ <span><%= flash[:notice] %></span>
1587
+ </div>
1588
+ <% end %>
1589
+
1180
1590
  <%= yield %>
1181
1591
  </div>
1182
1592
 
@@ -1219,6 +1629,94 @@
1219
1629
  }
1220
1630
  });
1221
1631
 
1632
+ // The "feature is disabled, here is how to enable it" panel. Four callers
1633
+ // had drifted into hand-spaced copies of this markup (<br><br>, &nbsp;
1634
+ // indentation, inline styles); they all render through here now.
1635
+ window.pgReportsDisabledNotice = function(title) {
1636
+ const i18n = PG_REPORTS_I18N.modals;
1637
+ const esc = (t) => String(t == null ? '' : t)
1638
+ .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
1639
+ return [
1640
+ '<div class="error-message">',
1641
+ '<p class="error-title">' + esc(title) + '</p>',
1642
+ '<p>' + esc(i18n.query_execution_disabled_intro) + '</p>',
1643
+ '<pre class="code-block">PgReports.configure do |config|\n config.allow_raw_query_execution = true\nend</pre>',
1644
+ '<p>' + esc(i18n.query_execution_disabled_env_note) + '</p>',
1645
+ '<pre class="code-block">PG_REPORTS_ALLOW_RAW_QUERY_EXECUTION=true</pre>',
1646
+ '</div>'
1647
+ ].join('');
1648
+ };
1649
+
1650
+ // Settings modal. Shared by the dashboard and the report pages; the language
1651
+ // section posts a form (server-side session), the IDE section only writes
1652
+ // localStorage, so there is nothing to submit for it.
1653
+ // Picking a language posts a form and the page reloads (everything on it has
1654
+ // to re-render in the new language), which would leave the user staring at a
1655
+ // dashboard with the settings they were editing gone. Remember that the
1656
+ // modal was open and bring it back on the way in.
1657
+ const SETTINGS_REOPEN_KEY = 'pgReportsSettingsReopen';
1658
+
1659
+ function rememberSettingsOpen() {
1660
+ try {
1661
+ sessionStorage.setItem(SETTINGS_REOPEN_KEY, '1');
1662
+ } catch (e) {
1663
+ // Storage can be unavailable (private mode, blocked cookies); losing the
1664
+ // reopen is a cosmetic downgrade, not a reason to block the switch.
1665
+ }
1666
+ }
1667
+
1668
+ function consumeSettingsReopen() {
1669
+ try {
1670
+ if (!sessionStorage.getItem(SETTINGS_REOPEN_KEY)) return false;
1671
+ sessionStorage.removeItem(SETTINGS_REOPEN_KEY);
1672
+ return true;
1673
+ } catch (e) {
1674
+ return false;
1675
+ }
1676
+ }
1677
+
1678
+ function showSettingsModal() {
1679
+ const modal = document.getElementById('settings-modal');
1680
+ if (!modal) return;
1681
+
1682
+ modal.style.display = 'flex';
1683
+ loadSettingsState();
1684
+ }
1685
+
1686
+ function closeSettingsModal() {
1687
+ const modal = document.getElementById('settings-modal');
1688
+ if (modal) modal.style.display = 'none';
1689
+ }
1690
+
1691
+ function setDefaultIde(ideKey) {
1692
+ if (ideKey) {
1693
+ localStorage.setItem('pgReportsDefaultIde', ideKey);
1694
+ } else {
1695
+ localStorage.removeItem('pgReportsDefaultIde');
1696
+ }
1697
+ }
1698
+
1699
+ function getDefaultIde() {
1700
+ return localStorage.getItem('pgReportsDefaultIde') || '';
1701
+ }
1702
+
1703
+ // The language radios are rendered already checked by the server; only the
1704
+ // IDE choice lives in localStorage and has to be restored on open.
1705
+ function loadSettingsState() {
1706
+ const currentIde = getDefaultIde();
1707
+ document.querySelectorAll('input[name="default-ide"]').forEach(radio => {
1708
+ radio.checked = (radio.value === currentIde);
1709
+ });
1710
+ }
1711
+
1712
+ document.addEventListener('DOMContentLoaded', function() {
1713
+ document.getElementById('settings-modal')?.addEventListener('click', function(e) {
1714
+ if (e.target === this) closeSettingsModal();
1715
+ });
1716
+
1717
+ if (consumeSettingsReopen()) showSettingsModal();
1718
+ });
1719
+
1222
1720
  function showToast(message, type = 'success') {
1223
1721
  const toast = document.getElementById('toast');
1224
1722
  toast.textContent = message;