solidstats 1.0.0 → 2.0.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 (97) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +85 -0
  3. data/README.md +35 -0
  4. data/app/assets/javascripts/solidstats/application.js +257 -0
  5. data/app/assets/javascripts/solidstats/dashboard.js +225 -0
  6. data/app/assets/javascripts/solidstats/gem_metadata.js +554 -0
  7. data/app/assets/stylesheets/solidstats/application.css +6 -1
  8. data/app/assets/stylesheets/solidstats/components/action_button.css +99 -0
  9. data/app/assets/stylesheets/solidstats/components/dashboard.css +151 -0
  10. data/app/assets/stylesheets/solidstats/components/dashboard_header.css +93 -0
  11. data/app/assets/stylesheets/solidstats/components/dashboard_layout.css +97 -0
  12. data/app/assets/stylesheets/solidstats/components/gem_metadata.css +1403 -0
  13. data/app/assets/stylesheets/solidstats/components/navigation.css +80 -0
  14. data/app/assets/stylesheets/solidstats/components/quick_navigation.css +54 -0
  15. data/app/assets/stylesheets/solidstats/components/security.css +332 -0
  16. data/app/assets/stylesheets/solidstats/components/status_badge.css +58 -0
  17. data/app/assets/stylesheets/solidstats/components/summary_card.css +66 -0
  18. data/app/assets/stylesheets/solidstats/components/tab_navigation.css +95 -0
  19. data/app/components/solidstats/base_component.rb +88 -0
  20. data/app/components/solidstats/code_quality/code_quality_section_component.html.erb +0 -0
  21. data/app/components/solidstats/code_quality/code_quality_section_component.rb +0 -0
  22. data/app/components/solidstats/code_quality/section_component.html.erb +45 -0
  23. data/app/components/solidstats/code_quality/section_component.rb +34 -0
  24. data/app/components/solidstats/dashboard_header_component.html.erb +39 -0
  25. data/app/components/solidstats/dashboard_header_component.rb +33 -0
  26. data/app/components/solidstats/previews/action_button_component_preview/button_vs_link.html.erb +6 -0
  27. data/app/components/solidstats/previews/action_button_component_preview/sizes.html.erb +6 -0
  28. data/app/components/solidstats/previews/action_button_component_preview/variants.html.erb +6 -0
  29. data/app/components/solidstats/previews/action_button_component_preview/with_icons.html.erb +6 -0
  30. data/app/components/solidstats/previews/action_button_component_preview.rb +64 -0
  31. data/app/components/solidstats/previews/navigation_component_preview.rb +74 -0
  32. data/app/components/solidstats/previews/stats_overview_component_preview.rb +100 -0
  33. data/app/components/solidstats/previews/status_badge_component_preview/sizes.html.erb +6 -0
  34. data/app/components/solidstats/previews/status_badge_component_preview/statuses.html.erb +6 -0
  35. data/app/components/solidstats/previews/status_badge_component_preview/with_icons.html.erb +6 -0
  36. data/app/components/solidstats/previews/status_badge_component_preview.rb +49 -0
  37. data/app/components/solidstats/previews/summary_card_component_preview/clickable.html.erb +9 -0
  38. data/app/components/solidstats/previews/summary_card_component_preview/dashboard_layout.html.erb +9 -0
  39. data/app/components/solidstats/previews/summary_card_component_preview/statuses.html.erb +6 -0
  40. data/app/components/solidstats/previews/summary_card_component_preview/value_formats.html.erb +6 -0
  41. data/app/components/solidstats/previews/summary_card_component_preview.rb +67 -0
  42. data/app/components/solidstats/quick_navigation_component.html.erb +8 -0
  43. data/app/components/solidstats/quick_navigation_component.rb +21 -0
  44. data/app/components/solidstats/security/gem_impact_analysis_component.html.erb +44 -0
  45. data/app/components/solidstats/security/gem_impact_analysis_component.rb +45 -0
  46. data/app/components/solidstats/security/overview_component.html.erb +21 -0
  47. data/app/components/solidstats/security/overview_component.rb +104 -0
  48. data/app/components/solidstats/security/section_component.html.erb +26 -0
  49. data/app/components/solidstats/security/section_component.rb +52 -0
  50. data/app/components/solidstats/security/timeline_component.html.erb +39 -0
  51. data/app/components/solidstats/security/timeline_component.rb +43 -0
  52. data/app/components/solidstats/tasks_section_component.html.erb +17 -0
  53. data/app/components/solidstats/tasks_section_component.rb +22 -0
  54. data/app/components/solidstats/ui/action_button_component.html.erb +6 -0
  55. data/app/components/solidstats/ui/action_button_component.rb +71 -0
  56. data/app/components/solidstats/ui/dashboard_layout_component.html.erb +19 -0
  57. data/app/components/solidstats/ui/dashboard_layout_component.rb +85 -0
  58. data/app/components/solidstats/ui/navigation_component.html.erb +34 -0
  59. data/app/components/solidstats/ui/navigation_component.rb +72 -0
  60. data/app/components/solidstats/ui/stats_overview_component.html.erb +14 -0
  61. data/app/components/solidstats/ui/stats_overview_component.rb +78 -0
  62. data/app/components/solidstats/ui/status_badge_component.html.erb +6 -0
  63. data/app/components/solidstats/ui/status_badge_component.rb +42 -0
  64. data/app/components/solidstats/ui/summary_card_component.html.erb +12 -0
  65. data/app/components/solidstats/ui/summary_card_component.rb +63 -0
  66. data/app/components/solidstats/ui/tab_navigation_component.html.erb +22 -0
  67. data/app/components/solidstats/ui/tab_navigation_component.rb +79 -0
  68. data/app/controllers/solidstats/dashboard_controller.rb +22 -0
  69. data/app/controllers/solidstats/gem_metadata_controller.rb +12 -0
  70. data/app/helpers/solidstats/application_helper.rb +42 -0
  71. data/app/services/solidstats/gem_metadata/fetcher_service.rb +136 -0
  72. data/app/services/solidstats/log_size_monitor_service.rb +94 -0
  73. data/app/views/layouts/solidstats/application.html.erb +2 -1
  74. data/app/views/solidstats/dashboard/_log_monitor.html.erb +759 -0
  75. data/app/views/solidstats/dashboard/index.html.erb +67 -1323
  76. data/app/views/solidstats/gem_metadata/_panel.html.erb +419 -0
  77. data/config/routes.rb +7 -0
  78. data/lib/generators/solidstats/feature/feature_generator.rb +170 -0
  79. data/lib/generators/solidstats/feature/templates/component.html.erb +84 -0
  80. data/lib/generators/solidstats/feature/templates/component.rb.erb +103 -0
  81. data/lib/generators/solidstats/feature/templates/component.scss +243 -0
  82. data/lib/generators/solidstats/feature/templates/component_test.rb.erb +183 -0
  83. data/lib/generators/solidstats/feature/templates/controller.rb.erb +44 -0
  84. data/lib/generators/solidstats/feature/templates/controller_test.rb.erb +111 -0
  85. data/lib/generators/solidstats/feature/templates/detail_view.html.erb +755 -0
  86. data/lib/generators/solidstats/feature/templates/preview.rb.erb +107 -0
  87. data/lib/generators/solidstats/feature/templates/service.rb.erb +132 -0
  88. data/lib/generators/solidstats/feature/templates/service_test.rb.erb +109 -0
  89. data/lib/generators/solidstats/install_generator.rb +109 -0
  90. data/lib/generators/solidstats/templates/initializer.rb +112 -0
  91. data/lib/solidstats/asset_compatibility.rb +238 -0
  92. data/lib/solidstats/asset_manifest.rb +205 -0
  93. data/lib/solidstats/engine.rb +114 -9
  94. data/lib/solidstats/version.rb +1 -1
  95. data/lib/solidstats.rb +299 -2
  96. data/lib/tasks/solidstats_install.rake +122 -2
  97. metadata +99 -2
@@ -0,0 +1,554 @@
1
+ // Enhanced Gem Metadata Interactive Features
2
+ document.addEventListener('DOMContentLoaded', function() {
3
+ // Enhanced search and filter functionality
4
+ const searchInput = document.getElementById('gem-search');
5
+ const clearSearchBtn = document.getElementById('clear-search');
6
+ const statusFilter = document.getElementById('status-filter');
7
+ const sortFilter = document.getElementById('sort-filter');
8
+ const resetFiltersBtn = document.getElementById('reset-filters');
9
+ const gemsGrid = document.getElementById('gems-grid');
10
+ const gemsTableContainer = document.getElementById('gems-table-container');
11
+ const gemsTable = document.getElementById('gems-table');
12
+ const gemsTableBody = document.getElementById('gems-table-body');
13
+ const resultsInfo = document.getElementById('results-info');
14
+ const resultsCount = document.getElementById('results-count');
15
+
16
+ // View toggle elements
17
+ const gridViewBtn = document.getElementById('grid-view-btn');
18
+ const tableViewBtn = document.getElementById('table-view-btn');
19
+ const exportGroup = document.getElementById('export-group');
20
+ const exportTableBtn = document.getElementById('export-table-btn');
21
+
22
+ // Export functionality
23
+ function exportTableToCSV() {
24
+ const csvData = [];
25
+
26
+ // Header row
27
+ csvData.push(['Gem Name', 'Status', 'Current Version', 'Latest Version', 'Released', 'Description', 'Dependencies']);
28
+
29
+ // Data rows from filtered results
30
+ filteredTableRows.forEach(row => {
31
+ const cells = row.element.querySelectorAll('td');
32
+ const rowData = [
33
+ cells[0]?.textContent?.trim() || '',
34
+ cells[1]?.textContent?.trim() || '',
35
+ cells[2]?.textContent?.trim() || '',
36
+ cells[3]?.textContent?.trim() || '',
37
+ cells[4]?.textContent?.trim() || '',
38
+ cells[5]?.textContent?.trim() || '',
39
+ cells[6]?.textContent?.trim() || ''
40
+ ];
41
+ csvData.push(rowData);
42
+ });
43
+
44
+ // Convert to CSV format
45
+ const csvContent = csvData.map(row =>
46
+ row.map(field => `"${field.replace(/"/g, '""')}"`).join(',')
47
+ ).join('\n');
48
+
49
+ // Create and download file
50
+ const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
51
+ const link = document.createElement('a');
52
+ const url = URL.createObjectURL(blob);
53
+ link.setAttribute('href', url);
54
+ link.setAttribute('download', `gem-metadata-${new Date().toISOString().split('T')[0]}.csv`);
55
+ link.style.visibility = 'hidden';
56
+ document.body.appendChild(link);
57
+ link.click();
58
+ document.body.removeChild(link);
59
+
60
+ showToast('Table data exported successfully!', 'success');
61
+ }
62
+
63
+ // Loading state management
64
+ const loadingOverlay = document.getElementById('loading-overlay');
65
+
66
+ function showLoading() {
67
+ if (loadingOverlay) {
68
+ loadingOverlay.style.display = 'flex';
69
+ }
70
+ }
71
+
72
+ function hideLoading() {
73
+ if (loadingOverlay) {
74
+ loadingOverlay.style.display = 'none';
75
+ }
76
+ }
77
+
78
+ // Performance optimizations
79
+ let searchTimeout;
80
+ const SEARCH_DELAY = 300; // Debounce search for better performance
81
+
82
+ // Optimized search with debouncing
83
+ function debouncedSearch() {
84
+ clearTimeout(searchTimeout);
85
+ searchTimeout = setTimeout(() => {
86
+ performSearch();
87
+ }, SEARCH_DELAY);
88
+ }
89
+
90
+ let allGems = [];
91
+ let allTableRows = [];
92
+ let filteredGems = [];
93
+ let filteredTableRows = [];
94
+ let currentView = 'grid'; // Default to grid view
95
+
96
+ // Initialize gems data
97
+ function initializeGems() {
98
+ // Initialize grid cards
99
+ allGems = Array.from(document.querySelectorAll('.gem-card-full')).map(card => {
100
+ return {
101
+ element: card,
102
+ name: card.dataset.gemName,
103
+ status: card.dataset.status,
104
+ releaseDate: parseInt(card.dataset.releaseDate) || 0
105
+ };
106
+ });
107
+
108
+ // Initialize table rows
109
+ allTableRows = Array.from(document.querySelectorAll('.gem-table-row')).map(row => {
110
+ return {
111
+ element: row,
112
+ name: row.dataset.gemName,
113
+ status: row.dataset.status,
114
+ releaseDate: parseInt(row.dataset.releaseDate) || 0
115
+ };
116
+ });
117
+
118
+ filteredGems = [...allGems];
119
+ filteredTableRows = [...allTableRows];
120
+ updateResults();
121
+ }
122
+
123
+ // View toggle functionality
124
+ function switchView(view) {
125
+ currentView = view;
126
+
127
+ if (view === 'grid') {
128
+ // Show grid, hide table
129
+ gemsGrid.style.display = 'grid';
130
+ gemsTableContainer.style.display = 'none';
131
+ gridViewBtn.classList.add('active');
132
+ tableViewBtn.classList.remove('active');
133
+ if (exportGroup) exportGroup.style.display = 'none';
134
+
135
+ // Save preference
136
+ localStorage.setItem('gems-view-preference', 'grid');
137
+ } else {
138
+ // Hide grid, show table
139
+ gemsGrid.style.display = 'none';
140
+ gemsTableContainer.style.display = 'block';
141
+ tableViewBtn.classList.add('active');
142
+ gridViewBtn.classList.remove('active');
143
+ if (exportGroup) exportGroup.style.display = 'flex';
144
+
145
+ // Save preference
146
+ localStorage.setItem('gems-view-preference', 'table');
147
+ }
148
+
149
+ // Update results for current view
150
+ updateResults();
151
+ }
152
+
153
+ // Load saved view preference
154
+ function loadViewPreference() {
155
+ const savedView = localStorage.getItem('gems-view-preference') || 'grid';
156
+ switchView(savedView);
157
+ }
158
+
159
+ // Search functionality
160
+ function performSearch() {
161
+ const searchTerm = searchInput.value.toLowerCase().trim();
162
+ const statusValue = statusFilter.value;
163
+
164
+ // Filter grid cards
165
+ filteredGems = allGems.filter(gem => {
166
+ const matchesSearch = !searchTerm || gem.name.includes(searchTerm);
167
+ const matchesStatus = !statusValue || gem.status === statusValue;
168
+ return matchesSearch && matchesStatus;
169
+ });
170
+
171
+ // Filter table rows
172
+ filteredTableRows = allTableRows.filter(row => {
173
+ const matchesSearch = !searchTerm || row.name.includes(searchTerm);
174
+ const matchesStatus = !statusValue || row.status === statusValue;
175
+ return matchesSearch && matchesStatus;
176
+ });
177
+
178
+ applySorting();
179
+ updateResults();
180
+ updateClearButton();
181
+ }
182
+
183
+ // Sorting functionality
184
+ function applySorting() {
185
+ const sortValue = sortFilter.value;
186
+
187
+ const sortFunction = (a, b) => {
188
+ switch (sortValue) {
189
+ case 'name-asc':
190
+ return a.name.localeCompare(b.name);
191
+ case 'name-desc':
192
+ return b.name.localeCompare(a.name);
193
+ case 'status-desc':
194
+ const statusOrder = { 'outdated': 0, 'unavailable': 1, 'up-to-date': 2 };
195
+ return (statusOrder[a.status] || 3) - (statusOrder[b.status] || 3);
196
+ case 'release-desc':
197
+ return b.releaseDate - a.releaseDate;
198
+ case 'release-asc':
199
+ return a.releaseDate - b.releaseDate;
200
+ default:
201
+ return 0;
202
+ }
203
+ };
204
+
205
+ filteredGems.sort(sortFunction);
206
+ filteredTableRows.sort(sortFunction);
207
+ }
208
+
209
+ // Update display
210
+ function updateResults() {
211
+ if (currentView === 'grid') {
212
+ updateGridView();
213
+ } else {
214
+ updateTableView();
215
+ }
216
+
217
+ // Update results count
218
+ const count = currentView === 'grid' ? filteredGems.length : filteredTableRows.length;
219
+ if (resultsCount) {
220
+ resultsCount.textContent = count;
221
+ }
222
+
223
+ // Show/hide empty state
224
+ const isEmpty = count === 0;
225
+ if (isEmpty && !document.querySelector('.filter-empty-state')) {
226
+ showFilterEmptyState();
227
+ } else if (!isEmpty) {
228
+ hideFilterEmptyState();
229
+ }
230
+ }
231
+
232
+ // Update grid view
233
+ function updateGridView() {
234
+ // Hide all gems first
235
+ allGems.forEach(gem => {
236
+ gem.element.style.display = 'none';
237
+ gem.element.style.order = '';
238
+ });
239
+
240
+ // Show and order filtered gems
241
+ filteredGems.forEach((gem, index) => {
242
+ gem.element.style.display = 'block';
243
+ gem.element.style.order = index;
244
+
245
+ // Reset animation delay for visible items
246
+ gem.element.style.animationDelay = `${index * 0.05}s`;
247
+ });
248
+ }
249
+
250
+ // Update table view
251
+ function updateTableView() {
252
+ // Hide all table rows first
253
+ allTableRows.forEach(row => {
254
+ row.element.style.display = 'none';
255
+ });
256
+
257
+ // Show filtered rows
258
+ filteredTableRows.forEach((row, index) => {
259
+ row.element.style.display = '';
260
+ });
261
+ }
262
+
263
+ // Show empty state for filters
264
+ function showFilterEmptyState() {
265
+ const emptyState = document.createElement('div');
266
+ emptyState.className = 'filter-empty-state';
267
+ emptyState.innerHTML = `
268
+ <div class="empty-icon">🔍</div>
269
+ <div class="empty-title">No matching gems found</div>
270
+ <div class="empty-description">
271
+ Try adjusting your search terms or filters to find what you're looking for.
272
+ </div>
273
+ <button class="action-btn refresh-btn" onclick="resetAllFilters()">
274
+ <i class="fas fa-undo"></i>
275
+ Clear Filters
276
+ </button>
277
+ `;
278
+
279
+ if (currentView === 'grid') {
280
+ emptyState.style.cssText = `
281
+ grid-column: 1 / -1;
282
+ `;
283
+ gemsGrid.appendChild(emptyState);
284
+ } else {
285
+ // Add fallback class for better browser support
286
+ gemsTableContainer.classList.add('has-empty-state');
287
+
288
+ // For table view, create a properly centered empty state
289
+ const emptyRow = document.createElement('tr');
290
+ const emptyCell = document.createElement('td');
291
+ emptyCell.colSpan = 7; // Span all columns
292
+ emptyCell.className = 'empty-state-cell';
293
+ emptyCell.style.height = '400px'; // Set fixed height for better centering
294
+ emptyCell.appendChild(emptyState);
295
+ emptyRow.appendChild(emptyCell);
296
+ emptyRow.className = 'filter-empty-state-row';
297
+ gemsTableBody.appendChild(emptyRow);
298
+ }
299
+ }
300
+
301
+ // Hide filter empty state
302
+ function hideFilterEmptyState() {
303
+ const emptyState = document.querySelector('.filter-empty-state');
304
+ const emptyRow = document.querySelector('.filter-empty-state-row');
305
+
306
+ if (emptyState) {
307
+ emptyState.remove();
308
+ }
309
+
310
+ if (emptyRow) {
311
+ emptyRow.remove();
312
+ }
313
+
314
+ // Remove fallback class
315
+ if (gemsTableContainer) {
316
+ gemsTableContainer.classList.remove('has-empty-state');
317
+ }
318
+ }
319
+
320
+ // Update clear search button visibility
321
+ function updateClearButton() {
322
+ const hasValue = searchInput.value.trim().length > 0;
323
+ clearSearchBtn.style.display = hasValue ? 'block' : 'none';
324
+ }
325
+
326
+ // Reset all filters
327
+ function resetAllFilters() {
328
+ searchInput.value = '';
329
+ statusFilter.value = '';
330
+ sortFilter.value = 'name-asc';
331
+ performSearch();
332
+ updateClearButton();
333
+ }
334
+
335
+ // Global function for empty state button
336
+ window.resetAllFilters = resetAllFilters;
337
+
338
+ // Event listeners
339
+ if (searchInput) {
340
+ searchInput.addEventListener('input', debouncedSearch);
341
+ searchInput.addEventListener('input', updateClearButton);
342
+ }
343
+
344
+ if (clearSearchBtn) {
345
+ clearSearchBtn.addEventListener('click', function() {
346
+ searchInput.value = '';
347
+ performSearch();
348
+ updateClearButton();
349
+ searchInput.focus();
350
+ });
351
+ }
352
+
353
+ if (statusFilter) {
354
+ statusFilter.addEventListener('change', performSearch);
355
+ }
356
+
357
+ if (sortFilter) {
358
+ sortFilter.addEventListener('change', function() {
359
+ applySorting();
360
+ updateResults();
361
+ });
362
+ }
363
+
364
+ if (resetFiltersBtn) {
365
+ resetFiltersBtn.addEventListener('click', resetAllFilters);
366
+ }
367
+
368
+ // View toggle event listeners
369
+ if (gridViewBtn) {
370
+ gridViewBtn.addEventListener('click', function() {
371
+ switchView('grid');
372
+ });
373
+ }
374
+
375
+ if (tableViewBtn) {
376
+ tableViewBtn.addEventListener('click', function() {
377
+ switchView('table');
378
+ });
379
+ }
380
+
381
+ // Export functionality event listener
382
+ if (exportTableBtn) {
383
+ exportTableBtn.addEventListener('click', exportTableToCSV);
384
+ }
385
+
386
+ // Table sorting functionality
387
+ if (gemsTable) {
388
+ const sortableHeaders = gemsTable.querySelectorAll('th.sortable');
389
+ sortableHeaders.forEach(header => {
390
+ header.addEventListener('click', function() {
391
+ const sortType = this.dataset.sort;
392
+ const currentSort = sortFilter.value;
393
+
394
+ // Toggle between asc/desc for the same column
395
+ if (currentSort.startsWith(sortType)) {
396
+ const isAsc = currentSort.endsWith('-asc');
397
+ sortFilter.value = `${sortType}-${isAsc ? 'desc' : 'asc'}`;
398
+ } else {
399
+ sortFilter.value = `${sortType}-asc`;
400
+ }
401
+
402
+ // Update visual indicators
403
+ sortableHeaders.forEach(h => h.classList.remove('sorted'));
404
+ this.classList.add('sorted');
405
+
406
+ applySorting();
407
+ updateResults();
408
+ });
409
+ });
410
+ }
411
+
412
+ // Refresh button functionality with loading state
413
+ const refreshBtn = document.querySelector('.refresh-btn');
414
+ if (refreshBtn) {
415
+ refreshBtn.addEventListener('click', function(e) {
416
+ this.classList.add('loading');
417
+ this.setAttribute('aria-busy', 'true');
418
+
419
+ // Reset loading state after a delay (or when page reloads)
420
+ setTimeout(() => {
421
+ this.classList.remove('loading');
422
+ this.removeAttribute('aria-busy');
423
+ }, 3000);
424
+ });
425
+ }
426
+
427
+ // Keyboard shortcuts
428
+ document.addEventListener('keydown', function(e) {
429
+ // Ctrl/Cmd + K to focus search
430
+ if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
431
+ e.preventDefault();
432
+ searchInput.focus();
433
+ searchInput.select();
434
+ }
435
+
436
+ // Escape to clear search when focused
437
+ if (e.key === 'Escape' && document.activeElement === searchInput) {
438
+ resetAllFilters();
439
+ searchInput.blur();
440
+ }
441
+ });
442
+
443
+ // Intersection Observer for staggered animations
444
+ if ('IntersectionObserver' in window) {
445
+ const observer = new IntersectionObserver((entries) => {
446
+ entries.forEach((entry, index) => {
447
+ if (entry.isIntersecting) {
448
+ entry.target.style.animationPlayState = 'running';
449
+ observer.unobserve(entry.target);
450
+ }
451
+ });
452
+ }, {
453
+ threshold: 0.1,
454
+ rootMargin: '50px'
455
+ });
456
+
457
+ // Observe all gem cards
458
+ document.querySelectorAll('.gem-card-full').forEach(card => {
459
+ card.style.animationPlayState = 'paused';
460
+ observer.observe(card);
461
+ });
462
+ }
463
+
464
+ // Update last updated time
465
+ function updateLastUpdatedTime() {
466
+ const lastUpdatedElement = document.getElementById('last-updated');
467
+ if (lastUpdatedElement) {
468
+ const now = new Date();
469
+ const timeString = now.toLocaleTimeString([], {
470
+ hour: '2-digit',
471
+ minute: '2-digit'
472
+ });
473
+ lastUpdatedElement.textContent = `${timeString}`;
474
+ }
475
+ }
476
+
477
+ // Toast notifications
478
+ function showToast(message, type = 'info') {
479
+ const toast = document.createElement('div');
480
+ toast.className = `toast-notification ${type}`;
481
+ toast.textContent = message;
482
+
483
+ document.body.appendChild(toast);
484
+
485
+ // Trigger animation
486
+ setTimeout(() => toast.classList.add('visible'), 100);
487
+
488
+ // Remove after delay
489
+ setTimeout(() => {
490
+ toast.classList.remove('visible');
491
+ setTimeout(() => document.body.removeChild(toast), 300);
492
+ }, 3000);
493
+ }
494
+
495
+ // Handle form submissions with toast feedback
496
+ document.addEventListener('turbo:submit-start', function() {
497
+ showToast('Refreshing gem metadata...', 'info');
498
+ });
499
+
500
+ document.addEventListener('turbo:submit-end', function(event) {
501
+ if (event.detail.success) {
502
+ showToast('Gem metadata updated successfully!', 'success');
503
+ updateLastUpdatedTime();
504
+ } else {
505
+ showToast('Failed to update gem metadata. Please try again.', 'error');
506
+ }
507
+ });
508
+
509
+ // Add loading animation to stats cards
510
+ document.querySelectorAll('.stat-card').forEach((card, index) => {
511
+ card.style.animationDelay = `${index * 0.1}s`;
512
+ card.style.animation = 'fadeInUp 0.6s ease-out both';
513
+ });
514
+
515
+ // Enhanced keyboard navigation
516
+ let currentCardIndex = -1;
517
+ const cards = document.querySelectorAll('.gem-card-full');
518
+
519
+ searchInput.addEventListener('keydown', function(e) {
520
+ if (e.key === 'ArrowDown' && filteredGems.length > 0) {
521
+ e.preventDefault();
522
+ currentCardIndex = 0;
523
+ filteredGems[0].element.scrollIntoView({ behavior: 'smooth', block: 'center' });
524
+ filteredGems[0].element.focus();
525
+ }
526
+ });
527
+
528
+ // Add tabindex to cards for keyboard navigation
529
+ cards.forEach((card, index) => {
530
+ card.setAttribute('tabindex', '0');
531
+ card.addEventListener('keydown', function(e) {
532
+ if (e.key === 'ArrowDown' && currentCardIndex < filteredGems.length - 1) {
533
+ e.preventDefault();
534
+ currentCardIndex++;
535
+ filteredGems[currentCardIndex].element.scrollIntoView({ behavior: 'smooth', block: 'center' });
536
+ filteredGems[currentCardIndex].element.focus();
537
+ } else if (e.key === 'ArrowUp' && currentCardIndex > 0) {
538
+ e.preventDefault();
539
+ currentCardIndex--;
540
+ filteredGems[currentCardIndex].element.scrollIntoView({ behavior: 'smooth', block: 'center' });
541
+ filteredGems[currentCardIndex].element.focus();
542
+ } else if (e.key === 'Escape') {
543
+ searchInput.focus();
544
+ currentCardIndex = -1;
545
+ }
546
+ });
547
+ });
548
+
549
+ // Initialize on page load
550
+ initializeGems();
551
+ loadViewPreference(); // Load saved view preference
552
+ updateClearButton();
553
+ updateLastUpdatedTime();
554
+ });
@@ -10,6 +10,11 @@
10
10
  * files in this directory. Styles in this file should be added after the last require_* statement.
11
11
  * It is generally better to create a new file per style scope.
12
12
  *
13
- *= require_tree .
13
+ *= require_tree ./components
14
14
  *= require_self
15
15
  */
16
+
17
+ /* Base Solidstats styles */
18
+ .solidstats-dashboard {
19
+ font-family: system-ui, -apple-system, sans-serif;
20
+ }
@@ -0,0 +1,99 @@
1
+ /* ActionButtonComponent Styles */
2
+ .action-button {
3
+ display: inline-flex;
4
+ align-items: center;
5
+ gap: 0.5rem;
6
+ padding: 0.5rem 1rem;
7
+ border: 1px solid transparent;
8
+ border-radius: 0.375rem;
9
+ font-size: 0.875rem;
10
+ font-weight: 500;
11
+ line-height: 1.25rem;
12
+ text-decoration: none;
13
+ cursor: pointer;
14
+ transition: all 0.15s ease-in-out;
15
+ }
16
+
17
+ .action-button:hover {
18
+ text-decoration: none;
19
+ }
20
+
21
+ .action-button:focus {
22
+ outline: 2px solid transparent;
23
+ outline-offset: 2px;
24
+ box-shadow: 0 0 0 2px #3b82f6;
25
+ }
26
+
27
+ .action-button--primary {
28
+ background-color: #3b82f6;
29
+ color: #ffffff;
30
+ border-color: #3b82f6;
31
+ }
32
+
33
+ .action-button--primary:hover {
34
+ background-color: #2563eb;
35
+ border-color: #2563eb;
36
+ }
37
+
38
+ .action-button--secondary {
39
+ background-color: #6b7280;
40
+ color: #ffffff;
41
+ border-color: #6b7280;
42
+ }
43
+
44
+ .action-button--secondary:hover {
45
+ background-color: #4b5563;
46
+ border-color: #4b5563;
47
+ }
48
+
49
+ .action-button--outline {
50
+ background-color: transparent;
51
+ color: #374151;
52
+ border-color: #d1d5db;
53
+ }
54
+
55
+ .action-button--outline:hover {
56
+ background-color: #f9fafb;
57
+ border-color: #9ca3af;
58
+ }
59
+
60
+ .action-button--ghost {
61
+ background-color: transparent;
62
+ color: #374151;
63
+ border-color: transparent;
64
+ }
65
+
66
+ .action-button--ghost:hover {
67
+ background-color: #f3f4f6;
68
+ }
69
+
70
+ .action-button--danger {
71
+ background-color: #ef4444;
72
+ color: #ffffff;
73
+ border-color: #ef4444;
74
+ }
75
+
76
+ .action-button--danger:hover {
77
+ background-color: #dc2626;
78
+ border-color: #dc2626;
79
+ }
80
+
81
+ .action-button--small {
82
+ padding: 0.25rem 0.75rem;
83
+ font-size: 0.75rem;
84
+ }
85
+
86
+ .action-button--large {
87
+ padding: 0.75rem 1.5rem;
88
+ font-size: 1rem;
89
+ }
90
+
91
+ .action-button__icon {
92
+ display: inline-flex;
93
+ align-items: center;
94
+ font-size: 1em;
95
+ }
96
+
97
+ .action-button__text {
98
+ display: inline-block;
99
+ }