dbviewer 0.7.10 → 0.7.11

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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -0
  3. data/app/assets/images/dbviewer/emoji-favicon.txt +1 -0
  4. data/app/assets/images/dbviewer/favicon.ico +4 -0
  5. data/app/assets/images/dbviewer/favicon.png +4 -0
  6. data/app/assets/images/dbviewer/favicon.svg +10 -0
  7. data/app/assets/javascripts/dbviewer/entity_relationship_diagram.js +38 -42
  8. data/app/assets/javascripts/dbviewer/error_handler.js +58 -0
  9. data/app/assets/javascripts/dbviewer/home.js +25 -34
  10. data/app/assets/javascripts/dbviewer/layout.js +100 -129
  11. data/app/assets/javascripts/dbviewer/query.js +309 -246
  12. data/app/assets/javascripts/dbviewer/sidebar.js +170 -183
  13. data/app/assets/javascripts/dbviewer/utility.js +124 -0
  14. data/app/assets/stylesheets/dbviewer/application.css +8 -146
  15. data/app/assets/stylesheets/dbviewer/entity_relationship_diagram.css +0 -34
  16. data/app/assets/stylesheets/dbviewer/logs.css +0 -11
  17. data/app/assets/stylesheets/dbviewer/query.css +21 -9
  18. data/app/assets/stylesheets/dbviewer/table.css +49 -131
  19. data/app/controllers/concerns/dbviewer/database_operations/connection_management.rb +90 -0
  20. data/app/controllers/concerns/dbviewer/database_operations/data_export.rb +31 -0
  21. data/app/controllers/concerns/dbviewer/database_operations/database_information.rb +54 -0
  22. data/app/controllers/concerns/dbviewer/database_operations/datatable_operations.rb +37 -0
  23. data/app/controllers/concerns/dbviewer/database_operations/query_operations.rb +37 -0
  24. data/app/controllers/concerns/dbviewer/database_operations/relationship_management.rb +175 -0
  25. data/app/controllers/concerns/dbviewer/database_operations/table_operations.rb +46 -0
  26. data/app/controllers/concerns/dbviewer/database_operations.rb +4 -9
  27. data/app/controllers/dbviewer/api/tables_controller.rb +12 -0
  28. data/app/controllers/dbviewer/entity_relationship_diagrams_controller.rb +0 -15
  29. data/app/controllers/dbviewer/tables_controller.rb +4 -33
  30. data/app/views/dbviewer/entity_relationship_diagrams/index.html.erb +1 -1
  31. data/app/views/dbviewer/tables/query.html.erb +21 -6
  32. data/app/views/dbviewer/tables/show.html.erb +2 -2
  33. data/app/views/layouts/dbviewer/application.html.erb +12 -3
  34. data/config/routes.rb +2 -2
  35. data/lib/dbviewer/database/manager.rb +2 -2
  36. data/lib/dbviewer/datatable/query_operations.rb +1 -17
  37. data/lib/dbviewer/engine.rb +29 -0
  38. data/lib/dbviewer/version.rb +1 -1
  39. metadata +15 -10
  40. data/app/controllers/concerns/dbviewer/connection_management.rb +0 -88
  41. data/app/controllers/concerns/dbviewer/data_export.rb +0 -32
  42. data/app/controllers/concerns/dbviewer/database_information.rb +0 -62
  43. data/app/controllers/concerns/dbviewer/datatable_support.rb +0 -47
  44. data/app/controllers/concerns/dbviewer/pagination_concern.rb +0 -34
  45. data/app/controllers/concerns/dbviewer/query_operations.rb +0 -28
  46. data/app/controllers/concerns/dbviewer/relationship_management.rb +0 -173
  47. data/app/controllers/concerns/dbviewer/table_operations.rb +0 -56
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c8875d81748ca0f9c40b3d13c8f2ed9ac47ab2aba58c68a3da51f7ce403ee56c
4
- data.tar.gz: b670e95150a1843357373c5c892ee3c08e78243a0b0453731212177a077f12a6
3
+ metadata.gz: b357ba9437acaa807f1b31562bb7a1b76bfaefc7c3e2e6ccb3be4cfdbd627a0a
4
+ data.tar.gz: 2adbc0f217154bad12cb3c7bfd711a7daa7dcbfa5b01538563ea49010cd6d2f3
5
5
  SHA512:
6
- metadata.gz: ab5ff0acfabc15d711f61aa3e8811c40af5655c70b033eacb63f1ba2d508f08cf6c151f5da2a46ffd21e04cb4b170c14c03d87eea7c9d4b789232796d1087c88
7
- data.tar.gz: cb1e8197c523567d1dc29e96b98a1690f4056dff85ccf4cf4adcca5ab816cb0da6baf1adf84e9875059b400931e344923f08c579057531a829f1f1cd126d25bc
6
+ metadata.gz: c24e471a8d6d3248ef88040cff072a7868b8e936bdb535888882497b9ac64396017bb42f7c7f100b68240c8a927e45b573f679e47bd6573c832fcd2e6adc837a
7
+ data.tar.gz: a7be084a91cb9d090544d7a770ba3f1cf18e512a56a216b431a58672c09c4fd9b682cb6df768708c5fcc1bf85494a9738643e42f488103abe1aefc945ef2eda6
data/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  ![dbviewer](https://github.com/user-attachments/assets/665c1a65-aab3-4a7e-aa54-b42e871cb3d0)
2
2
 
3
3
  # 👁️ DBViewer
4
+ > **The fastest way to visualize and explore your database**
4
5
 
5
6
  DBViewer is a powerful Rails engine that provides a comprehensive interface to view and explore database tables, records, and schema.
6
7
  It's designed for development, debugging, and database analysis, offering a clean and intuitive way to interact with your application's database.
@@ -0,0 +1 @@
1
+ data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>👁️</text></svg>
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
2
+ <circle fill="#0d6efd" cx="8" cy="8" r="8"/>
3
+ <text x="8" y="11" font-family="sans-serif" font-size="8" text-anchor="middle" fill="white">👁️</text>
4
+ </svg>
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
2
+ <circle fill="#0d6efd" cx="16" cy="16" r="16"/>
3
+ <text x="16" y="22" font-family="sans-serif" font-size="16" text-anchor="middle" fill="white">👁️</text>
4
+ </svg>
@@ -0,0 +1,10 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg width="100px" height="100px" viewBox="0 0 100 100" version="1.1"
3
+ xmlns="http://www.w3.org/2000/svg">
4
+ <title>DB Viewer Favicon</title>
5
+ <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
6
+ <circle fill="#0d6efd" cx="50" cy="50" r="50" />
7
+ <text x="50" y="70" font-family="sans-serif" font-size="60" text-anchor="middle"
8
+ fill="white">👁️</text>
9
+ </g>
10
+ </svg>
@@ -1,30 +1,30 @@
1
1
  document.addEventListener("DOMContentLoaded", function () {
2
+ // Validate that required utility scripts have loaded
3
+ if (!window.DBViewer || !DBViewer.Utility || !DBViewer.ErrorHandler) {
4
+ console.error(
5
+ "Required DBViewer scripts not loaded. Please check utility.js and error_handler.js."
6
+ );
7
+ return;
8
+ }
9
+
10
+ // Destructure the needed functions for easier access
11
+ const { debounce, ThemeManager } = DBViewer.Utility;
12
+ const { displayError } = DBViewer.ErrorHandler;
2
13
  // Check if mermaid is loaded first
3
14
  if (typeof mermaid === "undefined") {
4
15
  console.error("Mermaid library not loaded!");
5
- showError(
6
- "Mermaid library not loaded",
16
+ displayError(
17
+ "erd-container",
18
+ "Mermaid Library Not Loaded",
7
19
  "The diagram library could not be loaded. Please check your internet connection and try again."
8
20
  );
9
21
  return;
10
22
  }
11
23
 
12
- // Helper function to debounce rapid function calls
13
- function debounce(func, wait) {
14
- let timeout;
15
- return function (...args) {
16
- clearTimeout(timeout);
17
- timeout = setTimeout(() => func.apply(this, args), wait);
18
- };
19
- }
20
-
21
24
  // Initialize mermaid with theme detection like mini ERD
22
25
  mermaid.initialize({
23
26
  startOnLoad: true,
24
- theme:
25
- document.documentElement.getAttribute("data-bs-theme") === "dark"
26
- ? "dark"
27
- : "default",
27
+ theme: ThemeManager.getCurrentTheme() === "dark" ? "dark" : "default",
28
28
  securityLevel: "loose",
29
29
  er: {
30
30
  diagramPadding: 20,
@@ -38,7 +38,7 @@ document.addEventListener("DOMContentLoaded", function () {
38
38
  },
39
39
  });
40
40
 
41
- // Function to show error messages
41
+ // Function to show error messages - using our custom error handler with specific UI adjustments
42
42
  function showError(title, message, details = "") {
43
43
  const errorContainer = document.getElementById("erd-error");
44
44
  const errorMessage = document.getElementById("erd-error-message");
@@ -656,34 +656,30 @@ document.addEventListener("DOMContentLoaded", function () {
656
656
 
657
657
  // Add theme observer to update diagram when theme changes
658
658
  function setupThemeObserver() {
659
- const observer = new MutationObserver((mutations) => {
660
- mutations.forEach((mutation) => {
661
- if (mutation.attributeName === "data-bs-theme") {
662
- const newTheme =
663
- document.documentElement.getAttribute("data-bs-theme");
664
- mermaid.initialize({
665
- theme: newTheme === "dark" ? "dark" : "default",
666
- // Keep other settings
667
- securityLevel: "loose",
668
- er: {
669
- diagramPadding: 20,
670
- layoutDirection: "TB",
671
- minEntityWidth: 100,
672
- minEntityHeight: 75,
673
- entityPadding: 15,
674
- stroke: "gray",
675
- fill: "honeydew",
676
- fontSize: 20,
677
- },
678
- });
679
- // Trigger redraw if diagram is already displayed
680
- if (diagramReady) {
681
- updateDiagramWithFullData();
682
- }
683
- }
659
+ // Listen for our custom theme change event
660
+ document.addEventListener("dbviewerThemeChanged", (event) => {
661
+ const newTheme = event.detail.theme;
662
+ mermaid.initialize({
663
+ theme: newTheme === "dark" ? "dark" : "default",
664
+ // Keep other settings
665
+ securityLevel: "loose",
666
+ er: {
667
+ diagramPadding: 20,
668
+ layoutDirection: "TB",
669
+ minEntityWidth: 100,
670
+ minEntityHeight: 75,
671
+ entityPadding: 15,
672
+ stroke: "gray",
673
+ fill: "honeydew",
674
+ fontSize: 20,
675
+ },
684
676
  });
677
+
678
+ // Trigger redraw if diagram is already displayed
679
+ if (diagramReady) {
680
+ updateDiagramWithFullData();
681
+ }
685
682
  });
686
- observer.observe(document.documentElement, { attributes: true });
687
683
  }
688
684
 
689
685
  setupThemeObserver();
@@ -0,0 +1,58 @@
1
+ /**
2
+ * DBViewer Error Handler
3
+ * Provides consistent error handling across the application
4
+ */
5
+
6
+ // Create a global namespace for DBViewer if it doesn't exist yet
7
+ window.DBViewer = window.DBViewer || {};
8
+
9
+ /**
10
+ * Display an error message in a container
11
+ * @param {string} containerId - The ID of the container to show the error in
12
+ * @param {string} title - Error title
13
+ * @param {string} message - Error message
14
+ * @param {string} details - Optional details about the error
15
+ */
16
+ function displayError(containerId, title, message, details = "") {
17
+ const container = document.getElementById(containerId);
18
+ if (!container) {
19
+ console.error(`Error container ${containerId} not found`);
20
+ return;
21
+ }
22
+
23
+ let detailsHtml = "";
24
+ if (details) {
25
+ detailsHtml = `<small class="text-muted">${details}</small>`;
26
+ }
27
+
28
+ container.innerHTML = `
29
+ <div class="text-center my-4 text-danger">
30
+ <i class="bi bi-exclamation-triangle fs-2 d-block mb-2"></i>
31
+ <p class="mb-1">${title}</p>
32
+ <div>${message}</div>
33
+ ${detailsHtml}
34
+ </div>
35
+ `;
36
+ }
37
+
38
+ /**
39
+ * Handle API fetch errors with consistent error handling and logging
40
+ * @param {string} endpoint - The API endpoint being accessed
41
+ * @param {Error} error - The error that occurred
42
+ * @param {Function} errorCallback - Callback function to handle UI updates on error
43
+ */
44
+ async function handleApiError(endpoint, error, errorCallback) {
45
+ console.error(`Error fetching from ${endpoint}:`, error);
46
+
47
+ if (typeof errorCallback === "function") {
48
+ errorCallback(error);
49
+ }
50
+
51
+ // You could add additional error tracking here, like sending to a monitoring service
52
+ }
53
+
54
+ // Expose error handling functions globally
55
+ DBViewer.ErrorHandler = {
56
+ displayError,
57
+ handleApiError,
58
+ };
@@ -1,21 +1,16 @@
1
+ // Use DBViewer namespace for utility functions and error handling
1
2
  document.addEventListener("DOMContentLoaded", async function () {
2
- // Helper function to format numbers with commas
3
- function numberWithDelimiter(number) {
4
- return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
5
- }
6
-
7
- // Helper function to format file sizes
8
- function numberToHumanSize(bytes) {
9
- if (bytes === null || bytes === undefined) return "N/A";
10
- if (bytes === 0) return "0 Bytes";
11
-
12
- const k = 1024;
13
- const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
14
- const i = Math.floor(Math.log(bytes) / Math.log(k));
15
-
16
- return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
3
+ // Validate that required utility scripts have loaded
4
+ if (!window.DBViewer || !DBViewer.Utility || !DBViewer.ErrorHandler) {
5
+ console.error(
6
+ "Required DBViewer scripts not loaded. Please check utility.js and error_handler.js."
7
+ );
8
+ return;
17
9
  }
18
10
 
11
+ // Destructure the needed functions for easier access
12
+ const { numberWithDelimiter, numberToHumanSize } = DBViewer.Utility;
13
+ const { displayError, handleApiError } = DBViewer.ErrorHandler;
19
14
  // Function to update analytics cards
20
15
  function updateTablesCount(data) {
21
16
  document.getElementById("tables-loading").classList.add("d-none");
@@ -167,17 +162,7 @@ document.addEventListener("DOMContentLoaded", async function () {
167
162
  }
168
163
  }
169
164
 
170
- // Function to show error state
171
- function showError(containerId, message) {
172
- const container = document.getElementById(containerId);
173
- container.innerHTML = `
174
- <div class="text-center my-4 text-danger">
175
- <i class="bi bi-exclamation-triangle fs-2 d-block mb-2"></i>
176
- <p>Error loading data</p>
177
- <small>${message}</small>
178
- </div>
179
- `;
180
- }
165
+ // Using error handler from imported module
181
166
 
182
167
  async function fetchTableCount() {
183
168
  try {
@@ -196,12 +181,13 @@ document.addEventListener("DOMContentLoaded", async function () {
196
181
  const data = await response.json();
197
182
  updateTablesCount(data);
198
183
  } catch (error) {
199
- console.error("Error loading tables count:", error);
200
- const loading = document.getElementById("tables-loading");
201
- const count = document.getElementById("tables-count");
202
- loading.classList.add("d-none");
203
- count.classList.remove("d-none");
204
- count.innerHTML = '<span class="text-danger">Error</span>';
184
+ await handleApiError("tables count", error, () => {
185
+ const loading = document.getElementById("tables-loading");
186
+ const count = document.getElementById("tables-count");
187
+ loading.classList.add("d-none");
188
+ count.classList.remove("d-none");
189
+ count.innerHTML = '<span class="text-danger">Error</span>';
190
+ });
205
191
  }
206
192
  }
207
193
 
@@ -274,8 +260,13 @@ document.addEventListener("DOMContentLoaded", async function () {
274
260
  const data = await response.json();
275
261
  updateRecentQueries(data);
276
262
  } catch (error) {
277
- console.error("Error loading recent queries:", error);
278
- showError("recent-queries-container", error.message);
263
+ await handleApiError("recent queries", error, () => {
264
+ displayError(
265
+ "recent-queries-container",
266
+ "Error Loading Queries",
267
+ error.message
268
+ );
269
+ });
279
270
  }
280
271
  }
281
272
  // Load database size data (Loading records first to prevent race condition on dbviewer model constant creation)
@@ -1,52 +1,28 @@
1
1
  document.addEventListener("DOMContentLoaded", function () {
2
+ // Validate that required utility scripts have loaded
3
+ if (!window.DBViewer || !DBViewer.Utility) {
4
+ console.error(
5
+ "Required DBViewer utility scripts not loaded. Please check utility.js."
6
+ );
7
+ return;
8
+ }
9
+
10
+ // Get ThemeManager from the global namespace
11
+ const { ThemeManager } = DBViewer.Utility;
12
+
2
13
  // Theme toggle functionality
3
14
  const themeToggleBtn = document.querySelector(".theme-toggle");
4
- const htmlElement = document.documentElement;
5
-
6
- // Check for saved theme preference or respect OS preference
7
- const prefersDarkMode = window.matchMedia(
8
- "(prefers-color-scheme: dark)"
9
- ).matches;
10
- const savedTheme = localStorage.getItem("dbviewerTheme");
11
-
12
- // Set initial theme
13
- if (savedTheme) {
14
- htmlElement.setAttribute("data-bs-theme", savedTheme);
15
- } else if (prefersDarkMode) {
16
- htmlElement.setAttribute("data-bs-theme", "dark");
17
- localStorage.setItem("dbviewerTheme", "dark");
18
- }
15
+
16
+ // Initialize theme based on saved preference or OS preference
17
+ ThemeManager.initialize();
19
18
 
20
19
  // Toggle theme when button is clicked
21
20
  if (themeToggleBtn) {
22
21
  themeToggleBtn.addEventListener("click", function () {
23
- const currentTheme = htmlElement.getAttribute("data-bs-theme");
24
- const newTheme = currentTheme === "dark" ? "light" : "dark";
25
-
26
- // Update theme
27
- htmlElement.setAttribute("data-bs-theme", newTheme);
28
- localStorage.setItem("dbviewerTheme", newTheme);
29
-
30
- // Dispatch event for other components to respond to theme change (Monaco editor)
31
- const themeChangeEvent = new CustomEvent("dbviewerThemeChanged", {
32
- detail: { theme: newTheme },
33
- });
34
- document.dispatchEvent(themeChangeEvent);
22
+ ThemeManager.toggleTheme();
35
23
  });
36
24
  }
37
25
 
38
- // Check if styles are loaded properly
39
- const styleCheck = getComputedStyle(
40
- document.documentElement
41
- ).getPropertyValue("--dbviewer-styles-loaded");
42
- if (!styleCheck) {
43
- console.log(
44
- "DBViewer: Using fallback inline styles (asset pipeline may not be available)"
45
- );
46
- } else {
47
- console.log("DBViewer: External CSS loaded successfully");
48
- }
49
-
50
26
  const toggleBtn = document.querySelector(".dbviewer-sidebar-toggle");
51
27
  const closeBtn = document.querySelector(".dbviewer-sidebar-close");
52
28
  const sidebar = document.querySelector(".dbviewer-sidebar");
@@ -72,26 +48,22 @@ document.addEventListener("DOMContentLoaded", function () {
72
48
  }, 300);
73
49
  }
74
50
 
75
- if (toggleBtn) {
76
- toggleBtn.addEventListener("click", function () {
77
- if (sidebar.classList.contains("active")) {
78
- hideSidebar();
79
- } else {
80
- showSidebar();
81
- // Focus the search input when sidebar becomes visible
82
- setTimeout(() => {
83
- const searchInput = document.getElementById("tableSearch");
84
- if (searchInput) searchInput.focus();
85
- }, 300); // Small delay to allow for animation
86
- }
87
- });
88
- }
89
-
90
- if (closeBtn) {
91
- closeBtn.addEventListener("click", function () {
51
+ toggleBtn.addEventListener("click", function () {
52
+ if (sidebar.classList.contains("active")) {
92
53
  hideSidebar();
93
- });
94
- }
54
+ } else {
55
+ showSidebar();
56
+ // Focus the search input when sidebar becomes visible
57
+ setTimeout(() => {
58
+ const searchInput = document.getElementById("tableSearch");
59
+ if (searchInput) searchInput.focus();
60
+ }, 300); // Small delay to allow for animation
61
+ }
62
+ });
63
+
64
+ closeBtn.addEventListener("click", function () {
65
+ hideSidebar();
66
+ });
95
67
 
96
68
  overlay.addEventListener("click", function () {
97
69
  hideSidebar();
@@ -110,85 +82,84 @@ document.addEventListener("DOMContentLoaded", function () {
110
82
 
111
83
  // Offcanvas enhancement for theme synchronization
112
84
  const offcanvasElement = document.getElementById("navbarOffcanvas");
113
- if (offcanvasElement) {
114
- // Get all theme toggles
115
- const allThemeToggles = document.querySelectorAll(".theme-toggle");
116
-
117
- // Handle theme change from any toggle button
118
- allThemeToggles.forEach((toggleBtn) => {
119
- toggleBtn.addEventListener("click", function () {
120
- const currentTheme =
121
- document.documentElement.getAttribute("data-bs-theme") || "light";
122
-
123
- // Update all theme toggle buttons to maintain consistency
124
- allThemeToggles.forEach((btn) => {
125
- // Update icon in all theme toggle buttons
126
- if (currentTheme === "dark") {
127
- btn.querySelector("span").innerHTML =
128
- '<i class="bi bi-sun-fill"></i>';
129
- btn.setAttribute("aria-label", "Switch to light mode");
130
- } else {
131
- btn.querySelector("span").innerHTML =
132
- '<i class="bi bi-moon-fill"></i>';
133
- btn.setAttribute("aria-label", "Switch to dark mode");
134
- }
135
- });
136
- });
137
- });
138
85
 
139
- // Function to sync offcanvas colors with current theme
140
- function syncOffcanvasWithTheme() {
86
+ // Get all theme toggles
87
+ const allThemeToggles = document.querySelectorAll(".theme-toggle");
88
+
89
+ // Handle theme change from any toggle button
90
+ allThemeToggles.forEach((toggleBtn) => {
91
+ toggleBtn.addEventListener("click", function () {
141
92
  const currentTheme =
142
93
  document.documentElement.getAttribute("data-bs-theme") || "light";
143
- if (currentTheme === "dark") {
144
- offcanvasElement
145
- .querySelector(".offcanvas-header")
146
- .classList.remove("bg-light-subtle");
147
- offcanvasElement
148
- .querySelector(".offcanvas-header")
149
- .classList.add("bg-dark-subtle");
150
- } else {
151
- offcanvasElement
152
- .querySelector(".offcanvas-header")
153
- .classList.remove("bg-dark-subtle");
154
- offcanvasElement
155
- .querySelector(".offcanvas-header")
156
- .classList.add("bg-light-subtle");
157
- }
158
- }
159
-
160
- // Sync on page load
161
- document.addEventListener("DOMContentLoaded", syncOffcanvasWithTheme);
162
94
 
163
- // Listen for theme changes
164
- document.addEventListener("dbviewerThemeChanged", syncOffcanvasWithTheme);
165
-
166
- // Handle link click in offcanvas (auto-close on mobile)
167
- const offcanvasLinks = offcanvasElement.querySelectorAll(
168
- ".nav-link:not(.dropdown-toggle)"
169
- );
170
- offcanvasLinks.forEach((link) => {
171
- link.addEventListener("click", function () {
172
- if (window.innerWidth < 992) {
173
- bootstrap.Offcanvas.getInstance(offcanvasElement).hide();
95
+ // Update all theme toggle buttons to maintain consistency
96
+ allThemeToggles.forEach((btn) => {
97
+ // Update icon in all theme toggle buttons
98
+ if (currentTheme === "dark") {
99
+ btn.querySelector("span").innerHTML =
100
+ '<i class="bi bi-sun-fill"></i>';
101
+ btn.setAttribute("aria-label", "Switch to light mode");
102
+ } else {
103
+ btn.querySelector("span").innerHTML =
104
+ '<i class="bi bi-moon-fill"></i>';
105
+ btn.setAttribute("aria-label", "Switch to dark mode");
174
106
  }
175
107
  });
176
108
  });
109
+ });
177
110
 
178
- // Fix offcanvas backdrop on desktop
179
- window.addEventListener("resize", function () {
180
- if (window.innerWidth >= 992) {
181
- const offcanvasInstance =
182
- bootstrap.Offcanvas.getInstance(offcanvasElement);
183
- if (offcanvasInstance) {
184
- offcanvasInstance.hide();
185
- }
186
- // Also remove any backdrop
187
- const backdrop = document.querySelector(".offcanvas-backdrop");
188
- if (backdrop) {
189
- backdrop.remove();
190
- }
111
+ // Function to sync offcanvas colors with current theme
112
+ function syncOffcanvasWithTheme() {
113
+ const currentTheme =
114
+ document.documentElement.getAttribute("data-bs-theme") || "light";
115
+ if (currentTheme === "dark") {
116
+ offcanvasElement
117
+ .querySelector(".offcanvas-header")
118
+ .classList.remove("bg-light-subtle");
119
+ offcanvasElement
120
+ .querySelector(".offcanvas-header")
121
+ .classList.add("bg-dark-subtle");
122
+ } else {
123
+ offcanvasElement
124
+ .querySelector(".offcanvas-header")
125
+ .classList.remove("bg-dark-subtle");
126
+ offcanvasElement
127
+ .querySelector(".offcanvas-header")
128
+ .classList.add("bg-light-subtle");
129
+ }
130
+ }
131
+
132
+ // Sync on page load
133
+ document.addEventListener("DOMContentLoaded", syncOffcanvasWithTheme);
134
+
135
+ // Listen for theme changes
136
+ document.addEventListener("dbviewerThemeChanged", syncOffcanvasWithTheme);
137
+
138
+ // Handle link click in offcanvas (auto-close on mobile)
139
+ const offcanvasLinks = offcanvasElement.querySelectorAll(
140
+ ".nav-link:not(.dropdown-toggle)"
141
+ );
142
+ offcanvasLinks.forEach((link) => {
143
+ link.addEventListener("click", function () {
144
+ if (window.innerWidth < 992) {
145
+ bootstrap.Offcanvas.getInstance(offcanvasElement).hide();
191
146
  }
192
147
  });
193
- }
148
+ });
149
+
150
+ // Fix offcanvas backdrop on desktop
151
+ window.addEventListener("resize", function () {
152
+ if (window.innerWidth >= 992) {
153
+ const offcanvasInstance =
154
+ bootstrap.Offcanvas.getInstance(offcanvasElement);
155
+ if (offcanvasInstance) {
156
+ offcanvasInstance.hide();
157
+ }
158
+ // Also remove any backdrop
159
+ const backdrop = document.querySelector(".offcanvas-backdrop");
160
+ if (backdrop) {
161
+ backdrop.remove();
162
+ }
163
+ }
164
+ });
194
165
  });