faultline-rails 0.4.0 → 0.7.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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +14 -0
  3. data/app/assets/stylesheets/faultline/application.css +0 -41
  4. data/app/assets/stylesheets/faultline/dashboard.css +961 -0
  5. data/app/controllers/faultline/logged_exceptions_controller.rb +81 -8
  6. data/app/helpers/faultline/logged_exceptions_helper.rb +84 -5
  7. data/app/javascript/controllers/faultline_controller.js +349 -7
  8. data/app/javascript/faultline/controllers/sidebar_controller.js +21 -0
  9. data/app/javascript/faultline/controllers/theme_controller.js +48 -0
  10. data/app/models/faultline/logged_exception.rb +10 -0
  11. data/app/views/faultline/logged_exceptions/_exceptions.html.erb +132 -84
  12. data/app/views/faultline/logged_exceptions/_show.html.erb +281 -60
  13. data/app/views/faultline/logged_exceptions/index.html.erb +192 -123
  14. data/app/views/faultline/logged_exceptions/show.html.erb +1 -1
  15. data/app/views/layouts/faultline/application.html.erb +116 -51
  16. data/faultline-rails.gemspec +1 -0
  17. data/lib/faultline/cleanup.rb +32 -0
  18. data/lib/faultline/configuration.rb +172 -4
  19. data/lib/faultline/engine.rb +22 -0
  20. data/lib/faultline/logger.rb +188 -0
  21. data/lib/faultline/notifications.rb +80 -0
  22. data/lib/faultline/ransack_config.rb +2 -24
  23. data/lib/faultline/version.rb +1 -1
  24. data/lib/faultline.rb +10 -0
  25. data/lib/generators/faultline/customize_generator.rb +117 -0
  26. data/lib/generators/faultline/install_generator.rb +15 -0
  27. data/lib/generators/faultline/templates/faultline.rb +70 -37
  28. data/lib/generators/faultline/templates/javascript/controllers/faultline_controller.js +310 -0
  29. data/lib/generators/faultline/templates/views/faultline/logged_exceptions/_exceptions.html.erb +124 -0
  30. data/lib/generators/faultline/templates/views/faultline/logged_exceptions/_show.html.erb +83 -0
  31. data/lib/generators/faultline/templates/views/faultline/logged_exceptions/index.html.erb +113 -0
  32. data/lib/generators/faultline/templates/views/layouts/faultline/application.html.erb +133 -0
  33. data/lib/tasks/faultline.rake +147 -0
  34. metadata +15 -1
@@ -0,0 +1,961 @@
1
+ /* ═══════════════════════════════════════════════════════════════════
2
+ Faultline Dashboard — Advanced Error Logger
3
+ Inspired by Sentry, Datadog, Honeybadger
4
+ ═══════════════════════════════════════════════════════════════════ */
5
+
6
+ /* ─── Light Theme (default) ─────────────────────────────────── */
7
+ :root, .light {
8
+ --fl-bg: #f8fafc;
9
+ --fl-bg-secondary: #f1f5f9;
10
+ --fl-surface: #ffffff;
11
+ --fl-surface-hover: #f8fafc;
12
+ --fl-surface-active: #f1f5f9;
13
+ --fl-border: #e2e8f0;
14
+ --fl-border-light: #f1f5f9;
15
+ --fl-text: #0f172a;
16
+ --fl-text-secondary: #475569;
17
+ --fl-text-muted: #94a3b8;
18
+ --fl-primary: #4f46e5;
19
+ --fl-primary-hover: #4338ca;
20
+ --fl-primary-light: #eef2ff;
21
+ --fl-primary-ring: rgba(79,70,229,0.15);
22
+ --fl-danger: #dc2626;
23
+ --fl-danger-hover: #b91c1c;
24
+ --fl-danger-light: #fef2f2;
25
+ --fl-danger-ring: rgba(220,38,38,0.15);
26
+ --fl-success: #059669;
27
+ --fl-success-light: #f0fdf4;
28
+ --fl-warning: #d97706;
29
+ --fl-warning-light: #fffbeb;
30
+ --fl-info: #2563eb;
31
+ --fl-info-light: #eff6ff;
32
+ --fl-badge-bg: #f1f5f9;
33
+ --fl-badge-text: #475569;
34
+ --fl-badge-danger-bg: #fef2f2;
35
+ --fl-badge-danger-text: #991b1b;
36
+ --fl-badge-danger-border: #fecaca;
37
+ --fl-badge-warning-bg: #fffbeb;
38
+ --fl-badge-warning-text: #92400e;
39
+ --fl-badge-info-bg: #eff6ff;
40
+ --fl-badge-info-text: #1e40af;
41
+ --fl-code-bg: #f8fafc;
42
+ --fl-code-text: #334155;
43
+ --fl-pre-bg: #0f172a;
44
+ --fl-pre-text: #e2e8f0;
45
+ --fl-sidebar-bg: #0f172a;
46
+ --fl-sidebar-text: #94a3b8;
47
+ --fl-sidebar-text-hover: #f8fafc;
48
+ --fl-sidebar-active-bg: #1e293b;
49
+ --fl-sidebar-border: #1e293b;
50
+ --fl-shadow-sm: 0 1px 2px rgba(0,0,0,0.04);
51
+ --fl-shadow: 0 1px 3px rgba(0,0,0,0.06), 0 1px 2px rgba(0,0,0,0.04);
52
+ --fl-shadow-md: 0 4px 6px -1px rgba(0,0,0,0.07), 0 2px 4px -2px rgba(0,0,0,0.05);
53
+ --fl-shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.08), 0 4px 6px -4px rgba(0,0,0,0.04);
54
+ --fl-radius: 0.75rem;
55
+ --fl-radius-sm: 0.5rem;
56
+ --fl-radius-lg: 1rem;
57
+ --fl-radius-xl: 1.25rem;
58
+ --fl-transition: 150ms cubic-bezier(0.4, 0, 0.2, 1);
59
+ --fl-transition-slow: 300ms cubic-bezier(0.4, 0, 0.2, 1);
60
+ }
61
+
62
+ /* ─── Dark Theme ────────────────────────────────────────────── */
63
+ .dark {
64
+ --fl-bg: #0f172a;
65
+ --fl-bg-secondary: #1e293b;
66
+ --fl-surface: #1e293b;
67
+ --fl-surface-hover: #334155;
68
+ --fl-surface-active: #334155;
69
+ --fl-border: #334155;
70
+ --fl-border-light: #1e293b;
71
+ --fl-text: #f1f5f9;
72
+ --fl-text-secondary: #94a3b8;
73
+ --fl-text-muted: #64748b;
74
+ --fl-primary: #818cf8;
75
+ --fl-primary-hover: #6366f1;
76
+ --fl-primary-light: rgba(99,102,241,0.1);
77
+ --fl-primary-ring: rgba(129,140,248,0.2);
78
+ --fl-danger: #f87171;
79
+ --fl-danger-hover: #ef4444;
80
+ --fl-danger-light: rgba(248,113,113,0.1);
81
+ --fl-danger-ring: rgba(248,113,113,0.2);
82
+ --fl-success: #34d399;
83
+ --fl-success-light: rgba(52,211,153,0.1);
84
+ --fl-warning: #fbbf24;
85
+ --fl-warning-light: rgba(251,191,36,0.1);
86
+ --fl-info: #60a5fa;
87
+ --fl-info-light: rgba(96,165,250,0.1);
88
+ --fl-badge-bg: #334155;
89
+ --fl-badge-text: #94a3b8;
90
+ --fl-badge-danger-bg: rgba(248,113,113,0.1);
91
+ --fl-badge-danger-text: #fca5a5;
92
+ --fl-badge-danger-border: rgba(248,113,113,0.2);
93
+ --fl-badge-warning-bg: rgba(251,191,36,0.1);
94
+ --fl-badge-warning-text: #fcd34d;
95
+ --fl-badge-info-bg: rgba(96,165,250,0.1);
96
+ --fl-badge-info-text: #93c5fd;
97
+ --fl-code-bg: #0f172a;
98
+ --fl-code-text: #94a3b8;
99
+ --fl-sidebar-bg: #020617;
100
+ --fl-shadow-sm: 0 1px 2px rgba(0,0,0,0.2);
101
+ --fl-shadow: 0 1px 3px rgba(0,0,0,0.3), 0 1px 2px rgba(0,0,0,0.2);
102
+ --fl-shadow-md: 0 4px 6px -1px rgba(0,0,0,0.4), 0 2px 4px -2px rgba(0,0,0,0.3);
103
+ --fl-shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.5), 0 4px 6px -4px rgba(0,0,0,0.4);
104
+ }
105
+
106
+ /* ─── Base Reset ────────────────────────────────────────────── */
107
+ *, *::before, *::after { box-sizing: border-box; }
108
+ body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; line-height: 1.5; color: var(--fl-text); background: var(--fl-bg); transition: background var(--fl-transition), color var(--fl-transition); }
109
+ a { color: var(--fl-primary); text-decoration: none; transition: color var(--fl-transition); }
110
+ a:hover { color: var(--fl-primary-hover); }
111
+ h1, h2, h3, h4, p { margin: 0; }
112
+ table { border-collapse: collapse; width: 100%; }
113
+ code, pre { font-family: "SF Mono", "Fira Code", "Fira Mono", "Roboto Mono", monospace; }
114
+
115
+ /* ─── Sidebar ───────────────────────────────────────────────── */
116
+ .fl-sidebar {
117
+ width: 16rem;
118
+ background: var(--fl-sidebar-bg);
119
+ color: var(--fl-sidebar-text);
120
+ display: flex;
121
+ flex-direction: column;
122
+ flex-shrink: 0;
123
+ border-right: 1px solid var(--fl-sidebar-border);
124
+ position: sticky;
125
+ top: 0;
126
+ height: 100vh;
127
+ z-index: 30;
128
+ }
129
+ @media (max-width: 1023px) { .fl-sidebar { display: none; } }
130
+
131
+ .fl-sidebar-logo {
132
+ padding: 1rem;
133
+ border-bottom: 1px solid var(--fl-sidebar-border);
134
+ display: flex;
135
+ align-items: center;
136
+ justify-content: space-between;
137
+ min-height: 3.5rem;
138
+ }
139
+ .fl-logo-icon {
140
+ width: 2rem;
141
+ height: 2rem;
142
+ border-radius: 0.5rem;
143
+ background: var(--fl-primary);
144
+ color: white;
145
+ display: flex;
146
+ align-items: center;
147
+ justify-content: center;
148
+ font-weight: 700;
149
+ font-size: 0.875rem;
150
+ flex-shrink: 0;
151
+ }
152
+ .fl-logo-text {
153
+ font-size: 0.9375rem;
154
+ font-weight: 700;
155
+ letter-spacing: -0.01em;
156
+ color: #f8fafc;
157
+ }
158
+
159
+ .fl-sidebar-nav {
160
+ flex: 1;
161
+ padding: 0.75rem;
162
+ display: flex;
163
+ flex-direction: column;
164
+ gap: 0.125rem;
165
+ overflow-y: auto;
166
+ }
167
+
168
+ .fl-nav-item {
169
+ display: flex;
170
+ align-items: center;
171
+ gap: 0.625rem;
172
+ padding: 0.5rem 0.75rem;
173
+ border-radius: 0.5rem;
174
+ font-size: 0.875rem;
175
+ font-weight: 500;
176
+ color: var(--fl-sidebar-text);
177
+ transition: all var(--fl-transition);
178
+ text-decoration: none;
179
+ }
180
+ .fl-nav-item:hover {
181
+ color: var(--fl-sidebar-text-hover);
182
+ background: var(--fl-sidebar-active-bg);
183
+ text-decoration: none;
184
+ }
185
+ .fl-nav-item.active {
186
+ color: #f8fafc;
187
+ background: var(--fl-sidebar-active-bg);
188
+ }
189
+
190
+ .fl-sidebar-footer {
191
+ padding: 0.75rem 1rem;
192
+ border-top: 1px solid var(--fl-sidebar-border);
193
+ font-size: 0.6875rem;
194
+ color: #475569;
195
+ }
196
+
197
+ /* ─── Mobile Sidebar ────────────────────────────────────────── */
198
+ .fl-mobile-sidebar {
199
+ position: fixed;
200
+ inset-y: 0;
201
+ left: 0;
202
+ width: 16rem;
203
+ background: var(--fl-sidebar-bg);
204
+ z-index: 50;
205
+ transform: translateX(-100%);
206
+ transition: transform 200ms ease-out;
207
+ }
208
+ .fl-mobile-sidebar.open { transform: translateX(0); }
209
+
210
+ .fl-overlay {
211
+ position: fixed;
212
+ inset: 0;
213
+ background: rgba(0,0,0,0.5);
214
+ z-index: 40;
215
+ display: none;
216
+ backdrop-filter: blur(4px);
217
+ }
218
+ .fl-overlay.open { display: block; }
219
+
220
+ /* ─── Top Bar ───────────────────────────────────────────────── */
221
+ .fl-topbar {
222
+ background: var(--fl-surface);
223
+ border-bottom: 1px solid var(--fl-border);
224
+ padding: 0 1.5rem;
225
+ height: 3.5rem;
226
+ display: flex;
227
+ align-items: center;
228
+ justify-content: space-between;
229
+ flex-shrink: 0;
230
+ position: sticky;
231
+ top: 0;
232
+ z-index: 20;
233
+ transition: background var(--fl-transition), border-color var(--fl-transition);
234
+ backdrop-filter: blur(8px);
235
+ background: color-mix(in srgb, var(--fl-surface) 85%, transparent);
236
+ }
237
+
238
+ .fl-icon-btn {
239
+ display: inline-flex;
240
+ align-items: center;
241
+ justify-content: center;
242
+ width: 2rem;
243
+ height: 2rem;
244
+ border-radius: 0.5rem;
245
+ border: none;
246
+ background: transparent;
247
+ color: var(--fl-text-secondary);
248
+ cursor: pointer;
249
+ transition: all var(--fl-transition);
250
+ }
251
+ .fl-icon-btn:hover {
252
+ background: var(--fl-bg-secondary);
253
+ color: var(--fl-text);
254
+ }
255
+
256
+ /* ─── Alerts ────────────────────────────────────────────────── */
257
+ .fl-alert {
258
+ display: flex;
259
+ align-items: center;
260
+ gap: 0.75rem;
261
+ padding: 0.75rem 1rem;
262
+ border-radius: 0.5rem;
263
+ }
264
+ .fl-alert-success {
265
+ background: var(--fl-success-light);
266
+ color: var(--fl-success);
267
+ border: 1px solid color-mix(in srgb, var(--fl-success) 20%, transparent);
268
+ }
269
+ .fl-alert-error {
270
+ background: var(--fl-danger-light);
271
+ color: var(--fl-danger);
272
+ border: 1px solid color-mix(in srgb, var(--fl-danger) 20%, transparent);
273
+ }
274
+
275
+ /* ─── Cards ─────────────────────────────────────────────────── */
276
+ .fl-card {
277
+ background: var(--fl-surface);
278
+ border: 1px solid var(--fl-border);
279
+ border-radius: var(--fl-radius-lg);
280
+ box-shadow: var(--fl-shadow);
281
+ transition: background var(--fl-transition), border-color var(--fl-transition), box-shadow var(--fl-transition);
282
+ }
283
+
284
+ /* ─── Stats Cards (Sentry-style overview) ───────────────────── */
285
+ .fl-stats {
286
+ display: grid;
287
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
288
+ gap: 1rem;
289
+ }
290
+ .fl-stat {
291
+ background: var(--fl-surface);
292
+ border: 1px solid var(--fl-border);
293
+ border-radius: var(--fl-radius);
294
+ padding: 1.25rem;
295
+ display: flex;
296
+ flex-direction: column;
297
+ gap: 0.25rem;
298
+ transition: all var(--fl-transition);
299
+ }
300
+ .fl-stat:hover {
301
+ border-color: var(--fl-primary);
302
+ box-shadow: 0 0 0 3px var(--fl-primary-ring);
303
+ }
304
+ .fl-stat-label {
305
+ font-size: 0.6875rem;
306
+ font-weight: 600;
307
+ text-transform: uppercase;
308
+ letter-spacing: 0.05em;
309
+ color: var(--fl-text-muted);
310
+ }
311
+ .fl-stat-value {
312
+ font-size: 1.75rem;
313
+ font-weight: 700;
314
+ color: var(--fl-text);
315
+ line-height: 1.2;
316
+ }
317
+ .fl-stat-value.danger { color: var(--fl-danger); }
318
+ .fl-stat-value.success { color: var(--fl-success); }
319
+ .fl-stat-value.warning { color: var(--fl-warning); }
320
+ .fl-stat-change {
321
+ font-size: 0.75rem;
322
+ color: var(--fl-text-muted);
323
+ display: flex;
324
+ align-items: center;
325
+ gap: 0.25rem;
326
+ }
327
+ .fl-stat-change.up { color: var(--fl-danger); }
328
+ .fl-stat-change.down { color: var(--fl-success); }
329
+
330
+ /* ─── Tabs (Sentry-style) ──────────────────────────────────── */
331
+ .fl-tabs {
332
+ display: flex;
333
+ border-bottom: 1px solid var(--fl-border);
334
+ padding: 0 1rem;
335
+ gap: 0;
336
+ overflow-x: auto;
337
+ }
338
+ .fl-tab {
339
+ padding: 0.75rem 1rem;
340
+ font-size: 0.875rem;
341
+ font-weight: 500;
342
+ color: var(--fl-text-muted);
343
+ border-bottom: 2px solid transparent;
344
+ cursor: pointer;
345
+ transition: all var(--fl-transition);
346
+ white-space: nowrap;
347
+ text-decoration: none;
348
+ background: none;
349
+ border-top: none;
350
+ border-left: none;
351
+ border-right: none;
352
+ }
353
+ .fl-tab:hover {
354
+ color: var(--fl-text);
355
+ text-decoration: none;
356
+ }
357
+ .fl-tab.active {
358
+ color: var(--fl-primary);
359
+ border-bottom-color: var(--fl-primary);
360
+ }
361
+ .fl-tab-badge {
362
+ display: inline-flex;
363
+ align-items: center;
364
+ justify-content: center;
365
+ min-width: 1.25rem;
366
+ height: 1.25rem;
367
+ padding: 0 0.375rem;
368
+ border-radius: 0.625rem;
369
+ font-size: 0.6875rem;
370
+ font-weight: 600;
371
+ margin-left: 0.375rem;
372
+ background: var(--fl-badge-bg);
373
+ color: var(--fl-badge-text);
374
+ }
375
+ .fl-tab.active .fl-tab-badge {
376
+ background: var(--fl-primary-light);
377
+ color: var(--fl-primary);
378
+ }
379
+
380
+ /* ─── Breadcrumbs ───────────────────────────────────────────── */
381
+ .fl-breadcrumbs {
382
+ display: flex;
383
+ align-items: center;
384
+ gap: 0.375rem;
385
+ font-size: 0.8125rem;
386
+ padding: 0.75rem 0;
387
+ }
388
+ .fl-breadcrumb {
389
+ color: var(--fl-text-muted);
390
+ text-decoration: none;
391
+ transition: color var(--fl-transition);
392
+ }
393
+ .fl-breadcrumb:hover { color: var(--fl-primary); text-decoration: none; }
394
+ .fl-breadcrumb-current { color: var(--fl-text); font-weight: 500; }
395
+ .fl-breadcrumb-sep {
396
+ color: var(--fl-text-muted);
397
+ opacity: 0.5;
398
+ user-select: none;
399
+ }
400
+
401
+ /* ─── Filters Bar ───────────────────────────────────────────── */
402
+ .fl-filters {
403
+ display: flex;
404
+ flex-wrap: wrap;
405
+ align-items: flex-end;
406
+ gap: 0.75rem;
407
+ padding: 1rem;
408
+ border-bottom: 1px solid var(--fl-border-light);
409
+ }
410
+ .fl-filters:last-child { border-bottom: none; }
411
+ .fl-filter-group {
412
+ flex: 1;
413
+ min-width: 140px;
414
+ }
415
+ .fl-filter-group label {
416
+ display: block;
417
+ font-size: 0.6875rem;
418
+ font-weight: 600;
419
+ text-transform: uppercase;
420
+ letter-spacing: 0.05em;
421
+ color: var(--fl-text-muted);
422
+ margin-bottom: 0.25rem;
423
+ }
424
+
425
+ /* ─── Active Filter Pills ───────────────────────────────────── */
426
+ .fl-filter-pills {
427
+ display: flex;
428
+ flex-wrap: wrap;
429
+ gap: 0.375rem;
430
+ padding: 0.75rem 1rem;
431
+ }
432
+ .fl-filter-pill {
433
+ display: inline-flex;
434
+ align-items: center;
435
+ gap: 0.375rem;
436
+ padding: 0.25rem 0.625rem;
437
+ background: var(--fl-primary-light);
438
+ color: var(--fl-primary);
439
+ border-radius: 1rem;
440
+ font-size: 0.75rem;
441
+ font-weight: 500;
442
+ text-decoration: none;
443
+ transition: all var(--fl-transition);
444
+ }
445
+ .fl-filter-pill:hover { opacity: 0.8; text-decoration: none; }
446
+ .fl-filter-pill-x {
447
+ display: inline-flex;
448
+ width: 1rem;
449
+ height: 1rem;
450
+ align-items: center;
451
+ justify-content: center;
452
+ border-radius: 50%;
453
+ font-size: 0.75rem;
454
+ line-height: 1;
455
+ }
456
+
457
+ /* ─── Form Elements ─────────────────────────────────────────── */
458
+ .fl-input,
459
+ .fl-select {
460
+ display: block;
461
+ width: 100%;
462
+ padding: 0.5rem 0.75rem;
463
+ border: 1px solid var(--fl-border);
464
+ border-radius: var(--fl-radius-sm);
465
+ font-size: 0.875rem;
466
+ color: var(--fl-text);
467
+ background: var(--fl-surface);
468
+ transition: all var(--fl-transition);
469
+ outline: none;
470
+ appearance: none;
471
+ }
472
+ .fl-input:focus,
473
+ .fl-select:focus {
474
+ border-color: var(--fl-primary);
475
+ box-shadow: 0 0 0 3px var(--fl-primary-ring);
476
+ }
477
+ .fl-input::placeholder { color: var(--fl-text-muted); }
478
+ .fl-select {
479
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3E%3C/svg%3E");
480
+ background-position: right 0.5rem center;
481
+ background-repeat: no-repeat;
482
+ background-size: 1.25rem;
483
+ padding-right: 2.5rem;
484
+ }
485
+
486
+ /* ─── Search Input (with icon) ──────────────────────────────── */
487
+ .fl-search {
488
+ position: relative;
489
+ }
490
+ .fl-search-icon {
491
+ position: absolute;
492
+ left: 0.75rem;
493
+ top: 50%;
494
+ transform: translateY(-50%);
495
+ color: var(--fl-text-muted);
496
+ pointer-events: none;
497
+ }
498
+ .fl-search .fl-input {
499
+ padding-left: 2.25rem;
500
+ }
501
+ .fl-search-kbd {
502
+ position: absolute;
503
+ right: 0.5rem;
504
+ top: 50%;
505
+ transform: translateY(-50%);
506
+ padding: 0.125rem 0.375rem;
507
+ border: 1px solid var(--fl-border);
508
+ border-radius: 0.25rem;
509
+ font-size: 0.625rem;
510
+ color: var(--fl-text-muted);
511
+ background: var(--fl-bg-secondary);
512
+ pointer-events: none;
513
+ }
514
+
515
+ /* ─── Buttons ───────────────────────────────────────────────── */
516
+ .fl-btn {
517
+ display: inline-flex;
518
+ align-items: center;
519
+ justify-content: center;
520
+ gap: 0.375rem;
521
+ padding: 0.5rem 1rem;
522
+ border: none;
523
+ border-radius: var(--fl-radius-sm);
524
+ font-size: 0.875rem;
525
+ font-weight: 600;
526
+ cursor: pointer;
527
+ text-decoration: none;
528
+ transition: all var(--fl-transition);
529
+ white-space: nowrap;
530
+ }
531
+ .fl-btn-primary {
532
+ background: var(--fl-primary);
533
+ color: white;
534
+ }
535
+ .fl-btn-primary:hover {
536
+ background: var(--fl-primary-hover);
537
+ text-decoration: none;
538
+ box-shadow: var(--fl-shadow-md);
539
+ }
540
+ .fl-btn-danger {
541
+ background: var(--fl-danger);
542
+ color: white;
543
+ }
544
+ .fl-btn-danger:hover {
545
+ background: var(--fl-danger-hover);
546
+ text-decoration: none;
547
+ }
548
+ .fl-btn-ghost {
549
+ background: transparent;
550
+ color: var(--fl-text-secondary);
551
+ border: 1px solid var(--fl-border);
552
+ }
553
+ .fl-btn-ghost:hover {
554
+ background: var(--fl-bg-secondary);
555
+ text-decoration: none;
556
+ }
557
+ .fl-btn-sm {
558
+ padding: 0.25rem 0.625rem;
559
+ font-size: 0.75rem;
560
+ }
561
+ .fl-btn-xs {
562
+ padding: 0.125rem 0.375rem;
563
+ font-size: 0.6875rem;
564
+ }
565
+
566
+ /* ─── Badges ────────────────────────────────────────────────── */
567
+ .fl-badge {
568
+ display: inline-flex;
569
+ align-items: center;
570
+ padding: 0.125rem 0.5rem;
571
+ border-radius: 0.375rem;
572
+ font-size: 0.75rem;
573
+ font-weight: 600;
574
+ white-space: nowrap;
575
+ }
576
+ .fl-badge-danger {
577
+ background: var(--fl-badge-danger-bg);
578
+ color: var(--fl-badge-danger-text);
579
+ border: 1px solid var(--fl-badge-danger-border);
580
+ }
581
+ .fl-badge-success {
582
+ background: var(--fl-success-light);
583
+ color: var(--fl-success);
584
+ }
585
+ .fl-badge-warning {
586
+ background: var(--fl-badge-warning-bg);
587
+ color: var(--fl-badge-warning-text);
588
+ }
589
+ .fl-badge-info {
590
+ background: var(--fl-badge-info-bg);
591
+ color: var(--fl-badge-info-text);
592
+ }
593
+ .fl-badge-muted {
594
+ background: var(--fl-badge-bg);
595
+ color: var(--fl-badge-text);
596
+ }
597
+
598
+ /* ─── Table ─────────────────────────────────────────────────── */
599
+ .fl-table-wrap { overflow-x: auto; }
600
+ .fl-table { min-width: 100%; }
601
+ .fl-table thead { background: var(--fl-bg-secondary); }
602
+ .fl-table th {
603
+ padding: 0.625rem 1rem;
604
+ text-align: left;
605
+ font-size: 0.6875rem;
606
+ font-weight: 600;
607
+ text-transform: uppercase;
608
+ letter-spacing: 0.05em;
609
+ color: var(--fl-text-muted);
610
+ border-bottom: 1px solid var(--fl-border);
611
+ white-space: nowrap;
612
+ user-select: none;
613
+ }
614
+ .fl-table th.sortable {
615
+ cursor: pointer;
616
+ transition: color var(--fl-transition);
617
+ }
618
+ .fl-table th.sortable:hover { color: var(--fl-text); }
619
+ .fl-table th.sorted-asc::after { content: " ▲"; font-size: 0.5rem; }
620
+ .fl-table th.sorted-desc::after { content: " ▼"; font-size: 0.5rem; }
621
+ .fl-table td {
622
+ padding: 0.75rem 1rem;
623
+ vertical-align: top;
624
+ border-bottom: 1px solid var(--fl-border-light);
625
+ }
626
+ .fl-table tbody tr {
627
+ transition: background var(--fl-transition);
628
+ cursor: pointer;
629
+ }
630
+ .fl-table tbody tr:hover {
631
+ background: var(--fl-surface-hover);
632
+ }
633
+ .fl-table tbody tr.selected {
634
+ background: var(--fl-primary-light);
635
+ }
636
+ .fl-table tbody tr.selected td:first-child::before {
637
+ content: "";
638
+ position: absolute;
639
+ left: 0;
640
+ top: 0;
641
+ bottom: 0;
642
+ width: 3px;
643
+ background: var(--fl-primary);
644
+ }
645
+ .fl-table .fl-name { font-weight: 500; color: var(--fl-primary); }
646
+ .fl-table .fl-name:hover { text-decoration: underline; }
647
+ .fl-table .fl-msg {
648
+ margin-top: 0.125rem;
649
+ font-size: 0.875rem;
650
+ color: var(--fl-text-secondary);
651
+ overflow: hidden;
652
+ text-overflow: ellipsis;
653
+ white-space: nowrap;
654
+ max-width: 40rem;
655
+ }
656
+ .fl-table .fl-date {
657
+ font-size: 0.75rem;
658
+ color: var(--fl-text-muted);
659
+ white-space: nowrap;
660
+ }
661
+
662
+ /* ─── Row count indicator ───────────────────────────────────── */
663
+ .fl-count-indicator {
664
+ width: 1.5rem;
665
+ height: 1.5rem;
666
+ border-radius: 50%;
667
+ display: inline-flex;
668
+ align-items: center;
669
+ justify-content: center;
670
+ font-size: 0.625rem;
671
+ font-weight: 700;
672
+ }
673
+
674
+ /* ─── Detail Panel (split view) ─────────────────────────────── */
675
+ .fl-detail-panel {
676
+ border-left: 1px solid var(--fl-border);
677
+ background: var(--fl-surface);
678
+ overflow-y: auto;
679
+ transition: background var(--fl-transition);
680
+ }
681
+ .fl-detail-header {
682
+ padding: 1rem 1.5rem;
683
+ border-bottom: 1px solid var(--fl-border-light);
684
+ display: flex;
685
+ align-items: flex-start;
686
+ justify-content: space-between;
687
+ gap: 1rem;
688
+ position: sticky;
689
+ top: 0;
690
+ background: var(--fl-surface);
691
+ z-index: 5;
692
+ }
693
+ .fl-detail-body { padding: 1.5rem; }
694
+
695
+ /* ─── Detail Sections ───────────────────────────────────────── */
696
+ .fl-detail-section {
697
+ margin-bottom: 1.5rem;
698
+ }
699
+ .fl-detail-section-header {
700
+ display: flex;
701
+ align-items: center;
702
+ justify-content: space-between;
703
+ margin-bottom: 0.75rem;
704
+ }
705
+ .fl-detail-section h3 {
706
+ font-size: 0.6875rem;
707
+ font-weight: 600;
708
+ text-transform: uppercase;
709
+ letter-spacing: 0.05em;
710
+ color: var(--fl-text-muted);
711
+ }
712
+ .fl-detail-code {
713
+ background: var(--fl-code-bg);
714
+ color: var(--fl-code-text);
715
+ padding: 0.75rem;
716
+ border-radius: var(--fl-radius-sm);
717
+ overflow-x: auto;
718
+ white-space: pre-wrap;
719
+ word-break: break-all;
720
+ font-size: 0.8125rem;
721
+ border: 1px solid var(--fl-border-light);
722
+ }
723
+ .fl-detail-pre {
724
+ background: var(--fl-pre-bg);
725
+ color: var(--fl-pre-text);
726
+ padding: 0.75rem;
727
+ border-radius: var(--fl-radius-sm);
728
+ overflow: auto;
729
+ max-height: 24rem;
730
+ font-size: 0.75rem;
731
+ line-height: 1.625;
732
+ }
733
+
734
+ /* ─── Metadata Grid ─────────────────────────────────────────── */
735
+ .fl-meta {
736
+ display: grid;
737
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
738
+ gap: 0.75rem;
739
+ }
740
+ .fl-meta-item {
741
+ padding: 0.75rem;
742
+ background: var(--fl-bg-secondary);
743
+ border-radius: var(--fl-radius-sm);
744
+ }
745
+ .fl-meta-label {
746
+ font-size: 0.6875rem;
747
+ font-weight: 600;
748
+ text-transform: uppercase;
749
+ letter-spacing: 0.05em;
750
+ color: var(--fl-text-muted);
751
+ margin-bottom: 0.25rem;
752
+ }
753
+ .fl-meta-value {
754
+ font-size: 0.875rem;
755
+ color: var(--fl-text);
756
+ font-weight: 500;
757
+ word-break: break-all;
758
+ }
759
+ .fl-meta-value.mono {
760
+ font-family: "SF Mono", "Fira Code", monospace;
761
+ font-size: 0.8125rem;
762
+ }
763
+
764
+ /* ─── Navigation (prev/next) ────────────────────────────────── */
765
+ .fl-nav-row {
766
+ display: flex;
767
+ align-items: center;
768
+ justify-content: space-between;
769
+ padding: 1rem 1.5rem;
770
+ border-top: 1px solid var(--fl-border-light);
771
+ background: var(--fl-bg-secondary);
772
+ }
773
+ .fl-nav-link {
774
+ display: flex;
775
+ align-items: center;
776
+ gap: 0.5rem;
777
+ padding: 0.5rem 1rem;
778
+ background: var(--fl-surface);
779
+ border: 1px solid var(--fl-border);
780
+ border-radius: var(--fl-radius-sm);
781
+ font-size: 0.8125rem;
782
+ color: var(--fl-text-secondary);
783
+ text-decoration: none;
784
+ transition: all var(--fl-transition);
785
+ max-width: 240px;
786
+ overflow: hidden;
787
+ }
788
+ .fl-nav-link:hover {
789
+ border-color: var(--fl-primary);
790
+ color: var(--fl-primary);
791
+ text-decoration: none;
792
+ }
793
+ .fl-nav-link-label {
794
+ font-size: 0.6875rem;
795
+ color: var(--fl-text-muted);
796
+ font-weight: 600;
797
+ text-transform: uppercase;
798
+ letter-spacing: 0.05em;
799
+ }
800
+ .fl-nav-link-title {
801
+ overflow: hidden;
802
+ text-overflow: ellipsis;
803
+ white-space: nowrap;
804
+ }
805
+
806
+ /* ─── Copy button ───────────────────────────────────────────── */
807
+ .fl-copy-btn {
808
+ display: inline-flex;
809
+ align-items: center;
810
+ gap: 0.25rem;
811
+ padding: 0.25rem 0.5rem;
812
+ background: transparent;
813
+ border: 1px solid var(--fl-border);
814
+ border-radius: 0.375rem;
815
+ font-size: 0.6875rem;
816
+ color: var(--fl-text-muted);
817
+ cursor: pointer;
818
+ transition: all var(--fl-transition);
819
+ }
820
+ .fl-copy-btn:hover {
821
+ color: var(--fl-text);
822
+ border-color: var(--fl-text-muted);
823
+ }
824
+ .fl-copy-btn.copied {
825
+ color: var(--fl-success);
826
+ border-color: var(--fl-success);
827
+ }
828
+
829
+ /* ─── Keyboard shortcut hints ───────────────────────────────── */
830
+ .fl-kbd {
831
+ display: inline-flex;
832
+ align-items: center;
833
+ justify-content: center;
834
+ min-width: 1.25rem;
835
+ height: 1.25rem;
836
+ padding: 0 0.25rem;
837
+ border: 1px solid var(--fl-border);
838
+ border-radius: 0.25rem;
839
+ font-size: 0.625rem;
840
+ font-family: inherit;
841
+ color: var(--fl-text-muted);
842
+ background: var(--fl-bg-secondary);
843
+ }
844
+
845
+ /* ─── Keyboard shortcuts dialog ─────────────────────────────── */
846
+ .fl-shortcuts-overlay {
847
+ position: fixed;
848
+ inset: 0;
849
+ background: rgba(0,0,0,0.5);
850
+ z-index: 100;
851
+ display: none;
852
+ align-items: center;
853
+ justify-content: center;
854
+ backdrop-filter: blur(4px);
855
+ }
856
+ .fl-shortcuts-overlay.open { display: flex; }
857
+ .fl-shortcuts {
858
+ background: var(--fl-surface);
859
+ border: 1px solid var(--fl-border);
860
+ border-radius: var(--fl-radius-xl);
861
+ box-shadow: var(--fl-shadow-lg);
862
+ padding: 1.5rem;
863
+ width: 100%;
864
+ max-width: 28rem;
865
+ max-height: 80vh;
866
+ overflow-y: auto;
867
+ }
868
+ .fl-shortcuts h3 {
869
+ font-size: 1rem;
870
+ font-weight: 700;
871
+ margin-bottom: 1rem;
872
+ }
873
+ .fl-shortcut-row {
874
+ display: flex;
875
+ align-items: center;
876
+ justify-content: space-between;
877
+ padding: 0.5rem 0;
878
+ border-bottom: 1px solid var(--fl-border-light);
879
+ }
880
+ .fl-shortcut-row:last-child { border-bottom: none; }
881
+ .fl-shortcut-label {
882
+ font-size: 0.875rem;
883
+ color: var(--fl-text-secondary);
884
+ }
885
+ .fl-shortcut-keys {
886
+ display: flex;
887
+ align-items: center;
888
+ gap: 0.25rem;
889
+ }
890
+
891
+ /* ─── Empty State ───────────────────────────────────────────── */
892
+ .fl-empty {
893
+ padding: 3rem 1rem;
894
+ text-align: center;
895
+ color: var(--fl-text-muted);
896
+ font-size: 0.875rem;
897
+ }
898
+
899
+ /* ─── Status Indicator ──────────────────────────────────────── */
900
+ .fl-pulse {
901
+ display: inline-block;
902
+ width: 0.375rem;
903
+ height: 0.375rem;
904
+ border-radius: 50%;
905
+ background: var(--fl-danger);
906
+ animation: fl-pulse 2s ease-in-out infinite;
907
+ }
908
+ @keyframes fl-pulse {
909
+ 0%, 100% { opacity: 1; }
910
+ 50% { opacity: 0.4; }
911
+ }
912
+
913
+ /* ─── Pagination ────────────────────────────────────────────── */
914
+ .fl-pagination {
915
+ border-top: 1px solid var(--fl-border-light);
916
+ padding: 0.75rem 1rem;
917
+ font-size: 0.875rem;
918
+ display: flex;
919
+ align-items: center;
920
+ justify-content: space-between;
921
+ }
922
+ .fl-pagination .pagination {
923
+ display: flex;
924
+ flex-wrap: wrap;
925
+ gap: 0.25rem;
926
+ align-items: center;
927
+ list-style: none;
928
+ margin: 0;
929
+ padding: 0;
930
+ }
931
+ .fl-pagination .pagination a,
932
+ .fl-pagination .pagination span { padding: 0.25rem 0.5rem; border-radius: var(--fl-radius-sm); }
933
+ .fl-pagination .pagination a { color: var(--fl-primary); }
934
+ .fl-pagination .pagination a:hover { background: var(--fl-primary-light); text-decoration: none; }
935
+ .fl-pagination .pagination .current {
936
+ background: var(--fl-primary);
937
+ color: white;
938
+ font-weight: 600;
939
+ }
940
+
941
+ /* ─── Utilities ─────────────────────────────────────────────── */
942
+ .sr-only {
943
+ position: absolute;
944
+ width: 1px;
945
+ height: 1px;
946
+ padding: 0;
947
+ margin: -1px;
948
+ overflow: hidden;
949
+ clip: rect(0,0,0,0);
950
+ border: 0;
951
+ }
952
+ .fl-truncate {
953
+ overflow: hidden;
954
+ text-overflow: ellipsis;
955
+ white-space: nowrap;
956
+ }
957
+ .fl-mono { font-family: "SF Mono", "Fira Code", monospace; }
958
+ .fl-text-xs { font-size: 0.75rem; }
959
+ .fl-text-sm { font-size: 0.875rem; }
960
+ .fl-text-muted { color: var(--fl-text-muted); }
961
+ .fl-font-semibold { font-weight: 600; }