serialbench 0.1.1 → 0.1.3

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 (100) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/benchmark.yml +273 -220
  3. data/.github/workflows/rake.yml +26 -0
  4. data/.github/workflows/windows-debug.yml +171 -0
  5. data/.gitignore +32 -0
  6. data/.rubocop.yml +1 -0
  7. data/.rubocop_todo.yml +274 -0
  8. data/Gemfile +14 -1
  9. data/README.adoc +292 -1118
  10. data/Rakefile +0 -55
  11. data/config/benchmarks/full.yml +29 -0
  12. data/config/benchmarks/short.yml +26 -0
  13. data/config/environments/asdf-ruby-3.2.yml +8 -0
  14. data/config/environments/asdf-ruby-3.3.yml +8 -0
  15. data/config/environments/docker-ruby-3.0.yml +9 -0
  16. data/config/environments/docker-ruby-3.1.yml +9 -0
  17. data/config/environments/docker-ruby-3.2.yml +9 -0
  18. data/config/environments/docker-ruby-3.3.yml +9 -0
  19. data/config/environments/docker-ruby-3.4.yml +9 -0
  20. data/data/schemas/result.yml +29 -0
  21. data/docker/Dockerfile.alpine +33 -0
  22. data/docker/{Dockerfile.benchmark → Dockerfile.ubuntu} +4 -3
  23. data/docker/README.md +2 -2
  24. data/docs/PLATFORM_VALIDATION_FIX.md +79 -0
  25. data/docs/SYCK_YAML_FIX.md +91 -0
  26. data/docs/WEBSITE_COMPLETION_PLAN.md +440 -0
  27. data/docs/WINDOWS_LIBXML_FIX.md +136 -0
  28. data/docs/WINDOWS_SETUP.md +122 -0
  29. data/exe/serialbench +1 -1
  30. data/lib/serialbench/benchmark_runner.rb +261 -423
  31. data/lib/serialbench/cli/base_cli.rb +51 -0
  32. data/lib/serialbench/cli/benchmark_cli.rb +453 -0
  33. data/lib/serialbench/cli/environment_cli.rb +181 -0
  34. data/lib/serialbench/cli/resultset_cli.rb +261 -0
  35. data/lib/serialbench/cli/ruby_build_cli.rb +225 -0
  36. data/lib/serialbench/cli/validate_cli.rb +88 -0
  37. data/lib/serialbench/cli.rb +61 -600
  38. data/lib/serialbench/config_manager.rb +129 -0
  39. data/lib/serialbench/models/benchmark_config.rb +75 -0
  40. data/lib/serialbench/models/benchmark_result.rb +81 -0
  41. data/lib/serialbench/models/environment_config.rb +72 -0
  42. data/lib/serialbench/models/platform.rb +111 -0
  43. data/lib/serialbench/models/result.rb +80 -0
  44. data/lib/serialbench/models/result_set.rb +79 -0
  45. data/lib/serialbench/models/result_store.rb +108 -0
  46. data/lib/serialbench/models.rb +54 -0
  47. data/lib/serialbench/ruby_build_manager.rb +149 -0
  48. data/lib/serialbench/runners/asdf_runner.rb +296 -0
  49. data/lib/serialbench/runners/base.rb +32 -0
  50. data/lib/serialbench/runners/docker_runner.rb +140 -0
  51. data/lib/serialbench/runners/local_runner.rb +71 -0
  52. data/lib/serialbench/serializers/base_serializer.rb +9 -17
  53. data/lib/serialbench/serializers/json/base_json_serializer.rb +4 -4
  54. data/lib/serialbench/serializers/json/json_serializer.rb +0 -2
  55. data/lib/serialbench/serializers/json/oj_serializer.rb +0 -2
  56. data/lib/serialbench/serializers/json/rapidjson_serializer.rb +1 -1
  57. data/lib/serialbench/serializers/json/yajl_serializer.rb +0 -2
  58. data/lib/serialbench/serializers/toml/base_toml_serializer.rb +5 -5
  59. data/lib/serialbench/serializers/toml/toml_rb_serializer.rb +1 -3
  60. data/lib/serialbench/serializers/toml/tomlib_serializer.rb +1 -3
  61. data/lib/serialbench/serializers/toml/tomlrb_serializer.rb +56 -0
  62. data/lib/serialbench/serializers/xml/base_xml_serializer.rb +4 -9
  63. data/lib/serialbench/serializers/xml/libxml_serializer.rb +4 -10
  64. data/lib/serialbench/serializers/xml/nokogiri_serializer.rb +2 -4
  65. data/lib/serialbench/serializers/xml/oga_serializer.rb +4 -10
  66. data/lib/serialbench/serializers/xml/ox_serializer.rb +2 -4
  67. data/lib/serialbench/serializers/xml/rexml_serializer.rb +3 -5
  68. data/lib/serialbench/serializers/yaml/base_yaml_serializer.rb +5 -1
  69. data/lib/serialbench/serializers/yaml/psych_serializer.rb +1 -1
  70. data/lib/serialbench/serializers/yaml/syck_serializer.rb +60 -23
  71. data/lib/serialbench/serializers.rb +23 -6
  72. data/lib/serialbench/site_generator.rb +283 -0
  73. data/lib/serialbench/templates/assets/css/benchmark_report.css +535 -0
  74. data/lib/serialbench/templates/assets/css/format_based.css +474 -0
  75. data/lib/serialbench/templates/assets/css/themes.css +589 -0
  76. data/lib/serialbench/templates/assets/js/chart_helpers.js +411 -0
  77. data/lib/serialbench/templates/assets/js/dashboard.js +795 -0
  78. data/lib/serialbench/templates/assets/js/navigation.js +142 -0
  79. data/lib/serialbench/templates/base.liquid +49 -0
  80. data/lib/serialbench/templates/format_based.liquid +507 -0
  81. data/lib/serialbench/templates/partials/chart_section.liquid +4 -0
  82. data/lib/serialbench/version.rb +1 -1
  83. data/lib/serialbench/yaml_validator.rb +36 -0
  84. data/lib/serialbench.rb +2 -31
  85. data/serialbench.gemspec +15 -3
  86. metadata +106 -25
  87. data/.github/workflows/ci.yml +0 -74
  88. data/.github/workflows/docker.yml +0 -246
  89. data/config/ci.yml +0 -22
  90. data/config/full.yml +0 -30
  91. data/docker/run-benchmarks.sh +0 -356
  92. data/lib/serialbench/chart_generator.rb +0 -821
  93. data/lib/serialbench/result_formatter.rb +0 -182
  94. data/lib/serialbench/result_merger.rb +0 -1201
  95. data/lib/serialbench/serializers/xml/base_parser.rb +0 -69
  96. data/lib/serialbench/serializers/xml/libxml_parser.rb +0 -98
  97. data/lib/serialbench/serializers/xml/nokogiri_parser.rb +0 -111
  98. data/lib/serialbench/serializers/xml/oga_parser.rb +0 -85
  99. data/lib/serialbench/serializers/xml/ox_parser.rb +0 -64
  100. data/lib/serialbench/serializers/xml/rexml_parser.rb +0 -129
@@ -0,0 +1,142 @@
1
+ // Navigation and UI interaction functions for Serialbench reports
2
+
3
+ /**
4
+ * Show a specific section and update navigation state
5
+ */
6
+ function showSection(sectionName) {
7
+ // Hide all sections
8
+ document.querySelectorAll('.section').forEach(section => {
9
+ section.classList.remove('active');
10
+ });
11
+
12
+ // Remove active class from all nav buttons
13
+ document.querySelectorAll('.nav-btn').forEach(btn => {
14
+ btn.classList.remove('active');
15
+ });
16
+
17
+ // Show selected section
18
+ const targetSection = document.getElementById(sectionName);
19
+ if (targetSection) {
20
+ targetSection.classList.add('active');
21
+ }
22
+
23
+ // Add active class to clicked button
24
+ if (event && event.target) {
25
+ event.target.classList.add('active');
26
+ }
27
+
28
+ // Smooth scroll to top of section
29
+ if (targetSection) {
30
+ targetSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Initialize navigation event listeners
36
+ */
37
+ function initializeNavigation() {
38
+ // Add click event listeners to navigation buttons
39
+ document.querySelectorAll('.nav-btn').forEach(btn => {
40
+ btn.addEventListener('click', function(e) {
41
+ e.preventDefault();
42
+ const sectionName = this.getAttribute('data-section') ||
43
+ this.textContent.toLowerCase().replace(/\s+/g, '').replace('performance', '');
44
+ showSection(sectionName);
45
+ });
46
+ });
47
+
48
+ // Add keyboard navigation support
49
+ document.addEventListener('keydown', function(e) {
50
+ if (e.altKey) {
51
+ const keyMap = {
52
+ '1': 'parsing',
53
+ '2': 'generation',
54
+ '3': 'streaming',
55
+ '4': 'memory',
56
+ '5': 'summary',
57
+ '6': 'environments'
58
+ };
59
+
60
+ if (keyMap[e.key]) {
61
+ e.preventDefault();
62
+ showSection(keyMap[e.key]);
63
+
64
+ // Update button state
65
+ document.querySelectorAll('.nav-btn').forEach(btn => {
66
+ btn.classList.remove('active');
67
+ if (btn.textContent.toLowerCase().includes(keyMap[e.key]) ||
68
+ (keyMap[e.key] === 'parsing' && btn.textContent.toLowerCase().includes('parsing')) ||
69
+ (keyMap[e.key] === 'generation' && btn.textContent.toLowerCase().includes('generation')) ||
70
+ (keyMap[e.key] === 'streaming' && btn.textContent.toLowerCase().includes('streaming')) ||
71
+ (keyMap[e.key] === 'memory' && btn.textContent.toLowerCase().includes('memory')) ||
72
+ (keyMap[e.key] === 'summary' && btn.textContent.toLowerCase().includes('summary')) ||
73
+ (keyMap[e.key] === 'environments' && btn.textContent.toLowerCase().includes('environment'))) {
74
+ btn.classList.add('active');
75
+ }
76
+ });
77
+ }
78
+ }
79
+ });
80
+ }
81
+
82
+ /**
83
+ * Add loading states to charts
84
+ */
85
+ function addChartLoadingStates() {
86
+ document.querySelectorAll('.chart-container canvas').forEach(canvas => {
87
+ const container = canvas.closest('.chart-container');
88
+ if (container) {
89
+ container.classList.add('loading');
90
+
91
+ // Remove loading state after chart is rendered
92
+ setTimeout(() => {
93
+ container.classList.remove('loading');
94
+ }, 1000);
95
+ }
96
+ });
97
+ }
98
+
99
+ /**
100
+ * Add tooltips to navigation buttons
101
+ */
102
+ function addNavigationTooltips() {
103
+ const tooltips = {
104
+ 'parsing': 'View parsing performance benchmarks (Alt+1)',
105
+ 'generation': 'View generation performance benchmarks (Alt+2)',
106
+ 'streaming': 'View streaming performance benchmarks (Alt+3)',
107
+ 'memory': 'View memory usage analysis (Alt+4)',
108
+ 'summary': 'View performance summary and recommendations (Alt+5)',
109
+ 'environments': 'View environment and version details (Alt+6)'
110
+ };
111
+
112
+ document.querySelectorAll('.nav-btn').forEach(btn => {
113
+ const text = btn.textContent.toLowerCase();
114
+ for (const [key, tooltip] of Object.entries(tooltips)) {
115
+ if (text.includes(key) || (key === 'environments' && text.includes('environment'))) {
116
+ btn.title = tooltip;
117
+ break;
118
+ }
119
+ }
120
+ });
121
+ }
122
+
123
+ /**
124
+ * Initialize all UI enhancements
125
+ */
126
+ function initializeUI() {
127
+ // Wait for DOM to be fully loaded
128
+ if (document.readyState === 'loading') {
129
+ document.addEventListener('DOMContentLoaded', function() {
130
+ initializeNavigation();
131
+ addNavigationTooltips();
132
+ addChartLoadingStates();
133
+ });
134
+ } else {
135
+ initializeNavigation();
136
+ addNavigationTooltips();
137
+ addChartLoadingStates();
138
+ }
139
+ }
140
+
141
+ // Auto-initialize when script loads
142
+ initializeUI();
@@ -0,0 +1,49 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>{{ page_title | default: "Serialbench Results" }}</title>
7
+
8
+ <!-- Preload fonts -->
9
+ <link rel="preconnect" href="https://fonts.googleapis.com">
10
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
12
+
13
+ <!-- Chart.js -->
14
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.js" defer></script>
15
+
16
+ <!-- CSS -->
17
+ <link rel="stylesheet" href="assets/css/benchmark_report.css">
18
+
19
+ <!-- Favicon -->
20
+ <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23F97316' viewBox='0 0 24 24'%3e%3cpath d='M12 2L2 7v10c0 5.55 3.84 9.95 9 11 5.16-1.05 9-5.45 9-11V7l-10-5z'/%3e%3c/svg%3e">
21
+ </head>
22
+ <body>
23
+ <!-- Navigation Bar -->
24
+ <nav class="navbar">
25
+ <div class="navbar-container">
26
+ <div class="navbar-header">
27
+ <a href="#" class="navbar-brand">
28
+ <div class="navbar-brand-icon">
29
+ <svg width="20" height="20" fill="currentColor" viewBox="0 0 24 24">
30
+ <path d="M12 2L2 7v10c0 5.55 3.84 9.95 9 11 5.16-1.05 9-5.45 9-11V7l-10-5z"/>
31
+ </svg>
32
+ </div>
33
+ Serialbench
34
+ </a>
35
+ </div>
36
+ </div>
37
+ </nav>
38
+
39
+ <!-- Main Content -->
40
+ <main class="main-content">
41
+ {{ content }}
42
+ </main>
43
+
44
+ <!-- JavaScript -->
45
+ <script src="assets/js/chart_helpers.js" defer></script>
46
+ <script src="assets/js/navigation.js" defer></script>
47
+
48
+ </body>
49
+ </html>
@@ -0,0 +1,507 @@
1
+ <!-- Filter Bar -->
2
+ <div class="filter-bar">
3
+ <div class="filter-container">
4
+ <!-- Platform Filter -->
5
+ <div class="filter-group">
6
+ <label class="filter-label" for="platform-filter">Platform:</label>
7
+ <div class="custom-select">
8
+ <select id="platform-filter">
9
+ <option value="">All Platforms</option>
10
+ </select>
11
+ </div>
12
+ </div>
13
+
14
+ <!-- Ruby Type Filter -->
15
+ <div class="filter-group">
16
+ <label class="filter-label" for="ruby-type-filter">Ruby Type:</label>
17
+ <div class="custom-select">
18
+ <select id="ruby-type-filter">
19
+ <option value="">All Types</option>
20
+ </select>
21
+ </div>
22
+ </div>
23
+
24
+ <!-- Ruby Version Filter -->
25
+ <div class="filter-group">
26
+ <label class="filter-label" for="ruby-version-filter">Version:</label>
27
+ <div class="custom-select">
28
+ <select id="ruby-version-filter">
29
+ <option value="">All Versions</option>
30
+ </select>
31
+ </div>
32
+ </div>
33
+
34
+ <!-- Format Tabs -->
35
+ <div class="format-tabs">
36
+ <button class="format-tab active" data-format="xml">XML</button>
37
+ <button class="format-tab" data-format="json">JSON</button>
38
+ <button class="format-tab" data-format="yaml">YAML</button>
39
+ <button class="format-tab" data-format="toml">TOML</button>
40
+ </div>
41
+ </div>
42
+ </div>
43
+
44
+ <!-- Dashboard Header -->
45
+ <header class="dashboard-header">
46
+ <h1>Performance Dashboard</h1>
47
+ <p class="dashboard-subtitle">
48
+ Comprehensive serialization performance analysis across formats, platforms, and Ruby versions
49
+ </p>
50
+ <div class="dashboard-meta">
51
+ <span><strong>Generated:</strong> <span id="metadata-timestamp">Unknown</span></span>
52
+ <span><strong>Ruby Versions:</strong> <span id="metadata-ruby-versions">Unknown</span></span>
53
+ <span><strong>Platforms:</strong> <span id="metadata-platforms">Unknown</span></span>
54
+ </div>
55
+ </header>
56
+
57
+ <!-- Charts Grid -->
58
+ <section class="dashboard-grid">
59
+ <!-- Parsing Performance Chart -->
60
+ <div class="chart-card fade-in-up">
61
+ <div class="chart-header">
62
+ <h2 class="chart-title">Parsing Performance</h2>
63
+ <p class="chart-subtitle">Operations per second across different file sizes</p>
64
+ </div>
65
+ <div class="chart-container">
66
+ <canvas id="chart-parsing"></canvas>
67
+ </div>
68
+ </div>
69
+
70
+ <!-- Generation Performance Chart -->
71
+ <div class="chart-card fade-in-up">
72
+ <div class="chart-header">
73
+ <h2 class="chart-title">Generation Performance</h2>
74
+ <p class="chart-subtitle">Serialization speed across different file sizes</p>
75
+ </div>
76
+ <div class="chart-container">
77
+ <canvas id="chart-generation"></canvas>
78
+ </div>
79
+ </div>
80
+
81
+ <!-- Memory Usage Chart -->
82
+ <div class="chart-card fade-in-up">
83
+ <div class="chart-header">
84
+ <h2 class="chart-title">Memory Usage</h2>
85
+ <p class="chart-subtitle">Memory consumption during operations</p>
86
+ </div>
87
+ <div class="chart-container">
88
+ <canvas id="chart-memory"></canvas>
89
+ </div>
90
+ </div>
91
+
92
+ <!-- Streaming Performance Chart -->
93
+ <div class="chart-card fade-in-up">
94
+ <div class="chart-header">
95
+ <h2 class="chart-title">Streaming Performance</h2>
96
+ <p class="chart-subtitle">Streaming operations performance</p>
97
+ </div>
98
+ <div class="chart-container">
99
+ <canvas id="chart-streaming"></canvas>
100
+ </div>
101
+ </div>
102
+ </section>
103
+
104
+ <!-- Environment Information -->
105
+ <section class="environment-section fade-in-up">
106
+ <h2 class="summary-title">Environment Information</h2>
107
+ <div class="environment-grid" id="environment-info">
108
+ <!-- Environment cards will be populated by JavaScript -->
109
+ </div>
110
+ </section>
111
+
112
+ <!-- Raw Data Downloads -->
113
+ <section class="downloads-section fade-in-up">
114
+ <h2 class="summary-title">Raw Data Downloads</h2>
115
+ <p class="downloads-subtitle">Download benchmark data in YAML format for further analysis</p>
116
+ <div class="downloads-grid" id="downloads-grid">
117
+ <!-- Download links will be populated by JavaScript -->
118
+ </div>
119
+ </section>
120
+
121
+ <!-- Embedded Data -->
122
+ <script>
123
+ window.benchmarkData = {{ data }};
124
+
125
+ // Populate metadata on load
126
+ document.addEventListener('DOMContentLoaded', function() {
127
+ try {
128
+ const data = window.benchmarkData;
129
+
130
+ // Extract metadata
131
+ const timestamps = [];
132
+ const rubyVersions = new Set();
133
+ const platforms = new Set();
134
+
135
+ // Handle both single results (with environments) and resultsets (with results)
136
+ if (data.environments) {
137
+ // Single result format
138
+ Object.values(data.environments).forEach(env => {
139
+ if (env.timestamp) {
140
+ timestamps.push(new Date(env.timestamp));
141
+ }
142
+ if (env.ruby_version) {
143
+ rubyVersions.add(env.ruby_version);
144
+ }
145
+ if (env.os && env.arch) {
146
+ platforms.add(`${env.os} (${env.arch})`);
147
+ }
148
+ });
149
+ } else if (data.results) {
150
+ // Resultset format
151
+ data.results.forEach(result => {
152
+ if (result.metadata && result.metadata.created_at) {
153
+ timestamps.push(new Date(result.metadata.created_at));
154
+ }
155
+ if (result.platform) {
156
+ if (result.platform.ruby_version) {
157
+ rubyVersions.add(result.platform.ruby_version);
158
+ }
159
+ if (result.platform.os) {
160
+ platforms.add(result.platform.os);
161
+ }
162
+ }
163
+ });
164
+ }
165
+
166
+ // Update timestamp
167
+ const timestampEl = document.getElementById('metadata-timestamp');
168
+ if (timestampEl) {
169
+ if (timestamps.length > 0) {
170
+ const latestDate = new Date(Math.max(...timestamps));
171
+ timestampEl.textContent = latestDate.toLocaleString();
172
+ } else if (data.metadata && data.metadata.generated_at) {
173
+ timestampEl.textContent = new Date(data.metadata.generated_at).toLocaleString();
174
+ }
175
+ }
176
+
177
+ // Update Ruby versions
178
+ const versionsEl = document.getElementById('metadata-ruby-versions');
179
+ if (versionsEl && rubyVersions.size > 0) {
180
+ versionsEl.textContent = Array.from(rubyVersions).sort().join(', ');
181
+ }
182
+
183
+ // Update platforms
184
+ const platformsEl = document.getElementById('metadata-platforms');
185
+ if (platformsEl && platforms.size > 0) {
186
+ platformsEl.textContent = Array.from(platforms).sort().join(', ');
187
+ }
188
+ } catch (error) {
189
+ console.error('Error populating metadata:', error);
190
+ }
191
+
192
+ // Populate downloads grid
193
+ try {
194
+ const downloadsGrid = document.getElementById('downloads-grid');
195
+ if (downloadsGrid && data.environments) {
196
+ // Create download cards for individual environment results from environments object
197
+ Object.entries(data.environments).forEach(([envKey, env]) => {
198
+ const filename = `${envKey}.yaml`;
199
+
200
+ const card = document.createElement('div');
201
+ card.className = 'download-card';
202
+ card.innerHTML = `
203
+ <div class="download-card-title">
204
+ ${env.os} ${env.arch}
205
+ </div>
206
+ <div class="download-card-meta">
207
+ Ruby ${env.ruby_version}
208
+ </div>
209
+ <a href="data/${filename}" class="download-link" download>
210
+ <svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
211
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path>
212
+ </svg>
213
+ Download YAML
214
+ </a>
215
+ `;
216
+ downloadsGrid.appendChild(card);
217
+ });
218
+
219
+ // Add complete resultset download
220
+ const resultsetCard = document.createElement('div');
221
+ resultsetCard.className = 'download-card';
222
+ resultsetCard.style.gridColumn = 'span 1';
223
+ resultsetCard.innerHTML = `
224
+ <div class="download-card-title">
225
+ Complete Dataset
226
+ </div>
227
+ <div class="download-card-meta">
228
+ All environments and results
229
+ </div>
230
+ <a href="data/resultset.yaml" class="download-link" download>
231
+ <svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
232
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path>
233
+ </svg>
234
+ Download YAML
235
+ </a>
236
+ `;
237
+ downloadsGrid.appendChild(resultsetCard);
238
+ }
239
+ } catch (error) {
240
+ console.error('Error populating downloads:', error);
241
+ }
242
+ });
243
+ </script>
244
+
245
+ <!-- Additional JavaScript -->
246
+ <script src="assets/js/dashboard.js" defer></script>
247
+
248
+ <!-- Theme CSS (additional to base) -->
249
+ <link rel="stylesheet" href="assets/css/themes.css">
250
+ <link rel="stylesheet" href="assets/css/format_based.css">
251
+
252
+ <!-- Theme Toggle Button -->
253
+ <script>
254
+ // Add theme toggle to navbar
255
+ document.addEventListener('DOMContentLoaded', function() {
256
+ const navbar = document.querySelector('.navbar-header');
257
+ if (navbar) {
258
+ const controls = document.createElement('div');
259
+ controls.className = 'navbar-controls';
260
+ controls.innerHTML = `
261
+ <button class="theme-toggle" title="Toggle theme">
262
+ <svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
263
+ <circle cx="12" cy="12" r="5"></circle>
264
+ <line x1="12" y1="1" x2="12" y2="3"></line>
265
+ <line x1="12" y1="21" x2="12" y2="23"></line>
266
+ <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
267
+ <line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
268
+ <line x1="1" y1="12" x2="3" y2="12"></line>
269
+ <line x1="21" y1="12" x2="23" y2="12"></line>
270
+ <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
271
+ <line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
272
+ </svg>
273
+ </button>
274
+ `;
275
+ navbar.appendChild(controls);
276
+ }
277
+ });
278
+ </script>
279
+
280
+ <!-- Loading indicator -->
281
+ <style>
282
+ .loading-overlay {
283
+ position: fixed;
284
+ top: 0;
285
+ left: 0;
286
+ right: 0;
287
+ bottom: 0;
288
+ background: var(--bg-primary);
289
+ display: flex;
290
+ align-items: center;
291
+ justify-content: center;
292
+ z-index: 9999;
293
+ transition: opacity 0.3s ease;
294
+ }
295
+
296
+ .loading-spinner {
297
+ width: 40px;
298
+ height: 40px;
299
+ border: 3px solid var(--border-primary);
300
+ border-top: 3px solid var(--accent-primary);
301
+ border-radius: 50%;
302
+ animation: spin 1s linear infinite;
303
+ }
304
+
305
+ @keyframes spin {
306
+ 0% { transform: rotate(0deg); }
307
+ 100% { transform: rotate(360deg); }
308
+ }
309
+
310
+ .dashboard-header {
311
+ text-align: center;
312
+ margin-bottom: var(--space-2xl, 2rem);
313
+ padding: var(--space-xl, 1.5rem) 0;
314
+ background: transparent;
315
+ }
316
+
317
+ .dashboard-header h1 {
318
+ font-size: 2.5rem;
319
+ font-weight: 700;
320
+ margin-bottom: var(--space-md, 1rem);
321
+ color: var(--text-primary);
322
+ position: relative;
323
+ z-index: 1;
324
+ }
325
+
326
+ .dashboard-subtitle {
327
+ font-size: 1.125rem;
328
+ color: var(--text-secondary);
329
+ margin-bottom: var(--space-lg, 1.25rem);
330
+ max-width: 600px;
331
+ margin-left: auto;
332
+ margin-right: auto;
333
+ }
334
+
335
+ .dashboard-meta {
336
+ display: flex;
337
+ justify-content: center;
338
+ gap: var(--space-lg, 1.25rem);
339
+ flex-wrap: wrap;
340
+ font-size: 0.875rem;
341
+ color: var(--text-muted);
342
+ }
343
+
344
+ .filter-bar {
345
+ background: var(--bg-secondary, #f9fafb);
346
+ border-bottom: 1px solid var(--border-primary, #e5e7eb);
347
+ padding: 1rem;
348
+ }
349
+
350
+ .filter-container {
351
+ max-width: 1200px;
352
+ margin: 0 auto;
353
+ display: flex;
354
+ gap: 1rem;
355
+ flex-wrap: wrap;
356
+ align-items: center;
357
+ }
358
+
359
+ .filter-group {
360
+ display: flex;
361
+ align-items: center;
362
+ gap: 0.5rem;
363
+ }
364
+
365
+ .filter-label {
366
+ font-size: 0.875rem;
367
+ font-weight: 500;
368
+ color: var(--text-secondary);
369
+ }
370
+
371
+ .custom-select select {
372
+ padding: 0.5rem 2rem 0.5rem 0.75rem;
373
+ border: 1px solid var(--border-primary);
374
+ border-radius: 0.375rem;
375
+ background: var(--bg-card);
376
+ color: var(--text-primary);
377
+ font-size: 0.875rem;
378
+ }
379
+
380
+ .custom-select select option {
381
+ color: var(--text-primary);
382
+ background: var(--bg-card);
383
+ }
384
+
385
+ /* Format tabs styles are in themes.css */
386
+
387
+ /* Downloads Section Styles */
388
+ .downloads-section {
389
+ margin-top: var(--space-2xl, 2rem);
390
+ padding: var(--space-xl, 1.5rem);
391
+ background: var(--bg-card);
392
+ border-radius: 0.75rem;
393
+ border: 1px solid var(--border-primary);
394
+ }
395
+
396
+ .downloads-subtitle {
397
+ text-align: center;
398
+ font-size: 1rem;
399
+ color: var(--text-secondary);
400
+ margin-bottom: var(--space-lg, 1.25rem);
401
+ }
402
+
403
+ .downloads-grid {
404
+ display: grid;
405
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
406
+ gap: var(--space-lg, 1.25rem);
407
+ margin-top: var(--space-lg, 1.25rem);
408
+ }
409
+
410
+ .download-card {
411
+ background: var(--bg-secondary);
412
+ border: 1px solid var(--border-primary);
413
+ border-radius: 0.5rem;
414
+ padding: var(--space-lg, 1.25rem);
415
+ transition: all 0.2s ease;
416
+ display: flex;
417
+ flex-direction: column;
418
+ gap: var(--space-sm, 0.5rem);
419
+ }
420
+
421
+ .download-card:hover {
422
+ border-color: var(--accent-primary);
423
+ transform: translateY(-2px);
424
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
425
+ }
426
+
427
+ .download-card-title {
428
+ font-size: 1rem;
429
+ font-weight: 600;
430
+ color: var(--text-primary);
431
+ margin-bottom: var(--space-xs, 0.25rem);
432
+ }
433
+
434
+ .download-card-meta {
435
+ font-size: 0.875rem;
436
+ color: var(--text-muted);
437
+ margin-bottom: var(--space-sm, 0.5rem);
438
+ }
439
+
440
+ .download-link {
441
+ display: inline-flex;
442
+ align-items: center;
443
+ gap: 0.5rem;
444
+ padding: 0.625rem 1rem;
445
+ background: var(--accent-primary);
446
+ color: white;
447
+ text-decoration: none;
448
+ border-radius: 0.375rem;
449
+ font-size: 0.875rem;
450
+ font-weight: 500;
451
+ transition: background 0.2s ease;
452
+ justify-content: center;
453
+ }
454
+
455
+ .download-link:hover {
456
+ background: var(--accent-dark);
457
+ }
458
+
459
+ .download-link svg {
460
+ width: 16px;
461
+ height: 16px;
462
+ }
463
+
464
+ @media (max-width: 768px) {
465
+ .dashboard-header h1 {
466
+ font-size: 2rem;
467
+ }
468
+
469
+ .dashboard-meta {
470
+ flex-direction: column;
471
+ gap: var(--space-sm, 0.5rem);
472
+ }
473
+
474
+ .filter-container {
475
+ flex-direction: column;
476
+ align-items: stretch;
477
+ }
478
+
479
+ .format-tabs {
480
+ margin-left: 0;
481
+ }
482
+ }
483
+ </style>
484
+
485
+ <div class="loading-overlay" id="loading-overlay">
486
+ <div class="loading-spinner"></div>
487
+ </div>
488
+
489
+ <script>
490
+ // Hide loading overlay once everything is loaded
491
+ window.addEventListener('load', () => {
492
+ const overlay = document.getElementById('loading-overlay');
493
+ if (overlay) {
494
+ overlay.style.opacity = '0';
495
+ setTimeout(() => overlay.remove(), 300);
496
+ }
497
+ });
498
+
499
+ // Fallback to hide loading after 5 seconds
500
+ setTimeout(() => {
501
+ const overlay = document.getElementById('loading-overlay');
502
+ if (overlay) {
503
+ overlay.style.opacity = '0';
504
+ setTimeout(() => overlay.remove(), 300);
505
+ }
506
+ }, 5000);
507
+ </script>