Repository: MustardOS/theme
Pull request #54: Improved filter UI
Author: login: fizwidget
Created: 2025-04-08T09:54:40Z | Merged: 2025-04-14T02:44:24Z

The goal here is to support this kind of use case: "I want to see themes for my device that support grid mode". With the current UI this isn't possible, because selecting the grid filter removes the device filter. The UI in this PR supports this by having:
1. A dropdown menu of devices.
2. Checkboxes for the other filter attributes (grid, HDMI, language).

No filters applied:
<img width="1335" alt="Screenshot 2025-04-08 at 7 46 42 pm" src="https://github.com/user-attachments/assets/350bf783-fb8a-4e07-b4b3-5ed8c7ef7062" />
Example filter combination (RGCUBEXX themes that support grid mode and HDMI):
<img width="1335" alt="Screenshot 2025-04-08 at 7 52 55 pm" src="https://github.com/user-attachments/assets/cd9ae5aa-a041-436e-a124-f03312b6537e" />


Diff:
diff --git a/docs/index.html b/docs/index.html
index 8042b4f3f..8e9b3d9f0 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -13,27 +13,39 @@
         <h1>MustardOS Themes</h1>
     </header>
 
-        <a href="https://forms.office.com/r/CiYbDP1SVJ">
-            <button class="top-right-btn">Submit a Theme</button>
-        </a>
-
-    <header>
-        <nav>
-            <ul>
-                <li><button class="filter-btn" data-filter="all">All</button></li>
-                <li><button class="filter-btn" data-filter="RG28XX">RG28XX</button></li>
-                <li><button class="filter-btn" data-filter="RG34XX">RG34XX</button></li>
-                <li><button class="filter-btn" data-filter="RG35XX">RG35XX</button></li>
-                <li><button class="filter-btn" data-filter="RG40XX">RG40XX</button></li>
-                <li><button class="filter-btn" data-filter="RGCUBEXX">RGCUBEXX</button></li>
-                <li><button class="filter-btn" data-filter="TrimUIBrick">TrimUI Brick</button></li>
-                <li><button class="filter-btn" data-filter="TrimUISmartPro">TrimUI Smart Pro</button></li>
-                <li><button class="filter-btn" data-filter="Grid">Grid Mode</button></li>
-                <li><button class="filter-btn" data-filter="HDMI">HDMI</button></li>
-                <li><button class="filter-btn" data-filter="Language">Language</button></li>
-                <li><button class="filter-btn" data-filter="Legacy">Legacy</button></li>
-            </ul>
-        </nav>
+    <a href="https://forms.office.com/r/CiYbDP1SVJ">
+        <button class="top-right-btn">Submit a Theme</button>
+    </a>
+
+    <header id="filters">
+        <label for="device-filter">
+            Device
+            <select id="device-filter" class="filter" name="device-filter">
+                <option value="all">All</option>
+                <option value="RG28XX">RG28XX</option>
+                <option value="RG34XX">RG34XX</option>
+                <option value="RG35XX">RG35XX</option>
+                <option value="RG40XX">RG40XX</option>
+                <option value="RGCUBEXX">RGCUBEXX</option>
+                <option value="TrimUIBrick">TrimUI Brick</option>
+                <option value="TrimUISmartPro">TrimUI Smart Pro</option>
+            </select>
+        </label>
+
+        <label for="grid-filter" title="Supports grid layout">
+            Grid
+            <input type="checkbox" id="grid-filter" class="filter" name="grid" value="Grid">
+        </label>
+
+        <label for="hdmi-filter" title="Adapts well for HDMI output resolutions">
+            HDMI
+            <input type="checkbox" id="hdmi-filter" class="filter" name="hdmi" value="HDMI">
+        </label>
+
+        <label for="language-filter" title="Improved support for multiple languages">
+            Language
+            <input type="checkbox" id="language-filter" class="filter" name="language" value="Language">
+        </label>
     </header>
 
     <!-- Portfolio Grid -->
diff --git a/docs/scripts.js b/docs/scripts.js
index 00ad49c53..430fc6854 100644
--- a/docs/scripts.js
+++ b/docs/scripts.js
@@ -1,55 +1,100 @@
 document.addEventListener('DOMContentLoaded', function() {
-    const filterButtons = document.querySelectorAll('.filter-btn');
+    // UI elements
+    const deviceFilterMenu = document.getElementById('device-filter');
+    const hdmiFilterCheckbox = document.getElementById('hdmi-filter');
+    const gridFilterCheckbox = document.getElementById('grid-filter');
+    const languageFilterCheckbox = document.getElementById('language-filter');
     const portfolioItems = document.querySelectorAll('.portfolio-item');
-    
-    // Primary colors corresponding to each filter category
-    const filterColors = {
-        'all': '#c7af26',
-        'RG28XX': '#ba5a31',
-        'RG34XX': '#7cafc4',
-        'RG35XX': '#243e36',
-        'RG40XX': '#9e778f',
-        'RGCUBEXX': '#870058',
-        'TrimUIBrick': '#F25F5C',
-        'TrimUISmartPro': '#243e36',
-        'Legacy': '#c7af26'
-    };
-
-    // Updates the UI to reflect the filter category set in the URL
+
+    // Updates the UI to reflect the URL query params
     function updateUI() {
-        // Get filter category from URL
-        const urlParams = new URLSearchParams(window.location.search);
-        const filterCategory = urlParams.get('filter') ?? 'all';
+        // Get filters from query params
+        const deviceFilter = getQueryParm(queryParams.device) ?? 'all';
+        const isHdmiFilterEnabled = getQueryParm(queryParams.hdmi) === 'true';
+        const isGridFilterEnabled = getQueryParm(queryParams.grid) === 'true';
+        const isLanguageFilterEnabled = getQueryParm(queryParams.language) === 'true';
+
+        // Update filter UI
+        deviceFilterMenu.value = deviceFilter;
+        hdmiFilterCheckbox.checked = isHdmiFilterEnabled;
+        gridFilterCheckbox.checked = isGridFilterEnabled;
+        languageFilterCheckbox.checked = isLanguageFilterEnabled;
 
-        // Reset theme colors
-        const color = filterColors[filterCategory];
+        // Update page color scheme
+        const color = primaryColors[deviceFilter];
         document.documentElement.style.setProperty('--primary-color', color);
 
         // Show or hide portfolio items
         portfolioItems.forEach(item => {
-            const categories = item.getAttribute('data-category').split(' '); // Get the categories as an array
-            const parentItem = item.closest('a'); // Find the parent <a> element
-            const isVisible = filterCategory === 'all' || categories.includes(filterCategory);
-            parentItem.style.display = isVisible ? 'block' : 'none';
-        });
-
-        // Update filter button styles
-        filterButtons.forEach(button => {
-            const buttonFilter = button.getAttribute('data-filter');
-            button.classList.toggle('applied', buttonFilter === filterCategory);
+            // E.g. "RG35XX RG40XX HDMI Grid"
+            const categories = item.getAttribute('data-category').split(' ');
+            
+            const isMatchingDeviceFilter = deviceFilter === 'all' || categories.includes(deviceFilter);
+            const isMatchingHdmiFilter = !isHdmiFilterEnabled || categories.includes('HDMI');
+            const isMatchingGridFilter = !isGridFilterEnabled || categories.includes('Grid');
+            const isMatchingLanguageFilter = !isLanguageFilterEnabled || categories.includes('Language');
+            
+            const isMatchingAllFilters =
+                isMatchingDeviceFilter &&
+                isMatchingHdmiFilter &&
+                isMatchingGridFilter &&
+                isMatchingLanguageFilter;
+            
+            // Only show items that match all filters
+            const parentItem = item.closest('a');
+            parentItem.style.display = isMatchingAllFilters ? 'block' : 'none';
         });
     }
 
-    // Update URL + UI when button is clicked
-    filterButtons.forEach(button => {
-        button.addEventListener('click', function() {
-            const url = new URL(window.location);
-            url.searchParams.set("filter", this.getAttribute('data-filter'));
-            window.history.replaceState(null, '', url);
-            updateUI();
-        });
+    // Update URL & UI when filters are changed
+    deviceFilterMenu.addEventListener('change', function() {
+        setQueryParam(queryParams.device, this.value);
+        updateUI();
+    });
+    hdmiFilterCheckbox.addEventListener('change', function() {
+        setQueryParam(queryParams.hdmi, this.checked);
+        updateUI();
+    });
+    gridFilterCheckbox.addEventListener('change', function() {
+        setQueryParam(queryParams.grid, this.checked);
+        updateUI();
     });
-    
+    languageFilterCheckbox.addEventListener('change', function() {
+        setQueryParam(queryParams.language, this.checked);
+        updateUI();
+    });
+
     // Update UI on initial page load
     updateUI();
 });
+
+const getQueryParm = (key) =>
+    new URLSearchParams(window.location.search).get(key);
+
+const setQueryParam = (key, value) => {
+    const params = new URLSearchParams(window.location.search);
+    if (value) {
+        params.set(key, value);
+    } else {
+        params.delete(key);
+    }
+    window.history.replaceState({}, '', `${window.location.pathname}?${params}`);
+}
+
+const queryParams = {
+    device: 'device',
+    hdmi: 'hdmi',
+    grid: 'grid',
+    language: 'language'
+}
+
+const primaryColors = {
+    all: '#c7af26',
+    RG28XX: '#ba5a31',
+    RG34XX: '#7cafc4',
+    RG35XX: '#243e36',
+    RG40XX: '#9e778f',
+    RGCUBEXX: '#870058',
+    TrimUIBrick: '#F25F5C',
+    TrimUISmartPro: '#243e36',
+};
diff --git a/docs/styles.css b/docs/styles.css
index 3187ed4fa..db49e22dc 100644
--- a/docs/styles.css
+++ b/docs/styles.css
@@ -2,8 +2,8 @@
     --primary-color: #c7af26; /* Set default primary color */
     --background-color: #1f1f1f;
     --text-color: #ffffff;
-    --filter-button-hover: #6a6a6a;
-    --filter-button-applied: #4a4a4a;
+    --filter-control-background: color-mix(in srgb, var(--primary-color), black 20%);
+    --filter-control-hover: color-mix(in srgb, var(--primary-color), black 40%);
 }
 
 body {
@@ -28,41 +28,72 @@ body {
     font-weight: bold;
 }
 
+h5 {
+    margin-bottom: 0;
+}
+
 header {
     background-color: var(--primary-color);
     padding: 20px;
     text-align: center;
 }
 
-h5 {
-    margin-bottom: 0;
+header#filters {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    flex-wrap: wrap;
+    gap: 20px;
 }
 
-nav ul {
-    list-style: none;
-    padding: 0;
+.filter {
+    background-color: var(--filter-control-background);
+    transition: background-color 0.3s ease;
 }
 
-nav ul li {
-    display: inline;
-    margin: 0 15px;
+.filter:hover {
+    background-color: var(--filter-control-hover);
 }
 
-button.filter-btn {
-    background-color: var(--primary-color);
-    color: #fff;
+select#device-filter {
+    appearance: none;
+    color: var(--text-color);
     border: none;
-    padding: 10px 20px;
+    padding: 4px 32px 4px 16px;
+    border-radius: 6px;
+    font-size: 16px;
     cursor: pointer;
+    background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23ffffff" width="24px" height="24px"><path d="M7 10l5 5 5-5z" stroke="%23ffffff" stroke-width="2" stroke-linejoin="round" stroke-linecap="round"/></svg>');
+    background-repeat: no-repeat;
+    background-position: right 10px center;
+}
+
+label {
+    display: inline-flex;
+    align-items: center;
     font-size: 16px;
+    gap: 6px;
+    color: var(--text-color);
+    cursor: pointer;
 }
 
-button.filter-btn:hover {
-    background-color: var(--filter-button-hover);
+input[type="checkbox"] {
+    appearance: none;
+    width: 20px;
+    height: 20px;
+    border-radius: 6px;
+    cursor: pointer;
+    position: relative;
 }
 
-button.filter-btn.applied {
-    background-color: var(--filter-button-applied);
+input[type="checkbox"]:checked::after {
+    content: '\2713';
+    color: var(--text-color);
+    font-size: 16px;
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
 }
 
 .portfolio {
@@ -103,21 +134,21 @@ button.filter-btn.applied {
 }
 
 .top-right-btn {
-            position: absolute;
-            top: 10px;
-            right: 10px;
-            padding: 10px 20px;
-            background-color: #4a4a4a;
-            color: white;
-            border: none;
-            border-radius: 5px;
-            font-size: 16px;
-            cursor: pointer;
-        }
-
-        .top-right-btn:hover {
-            background-color: #45a049;
-        }
+    position: absolute;
+    top: 10px;
+    right: 10px;
+    padding: 10px 20px;
+    background-color: #4a4a4a;
+    color: white;
+    border: none;
+    border-radius: 5px;
+    font-size: 16px;
+    cursor: pointer;
+}
+
+.top-right-btn:hover {
+    background-color: #45a049;
+}
 
 a {
     text-decoration: none; 


Reviews:
user: login: fizwidget
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T09:55:44Z
user: login: fizwidget
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T09:56:17Z
user: login: fizwidget
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T09:58:20Z

Review comments:
diff_hunk: @@ -1,55 +1,100 @@
 document.addEventListener('DOMContentLoaded', function() {
-    const filterButtons = document.querySelectorAll('.filter-btn');
+    // UI elements
+    const deviceFilterMenu = document.getElementById('device-filter');
+    const hdmiFilterCheckbox = document.getElementById('hdmi-filter');
+    const gridFilterCheckbox = document.getElementById('grid-filter');
+    const languageFilterCheckbox = document.getElementById('language-filter');
     const portfolioItems = document.querySelectorAll('.portfolio-item');
-    
-    // Primary colors corresponding to each filter category
-    const filterColors = {
-        'all': '#c7af26',
-        'RG28XX': '#ba5a31',
-        'RG34XX': '#7cafc4',
-        'RG35XX': '#243e36',
-        'RG40XX': '#9e778f',
-        'RGCUBEXX': '#870058',
-        'TrimUIBrick': '#F25F5C',
-        'TrimUISmartPro': '#243e36',
-        'Legacy': '#c7af26'
-    };
path: docs/scripts.js
user: login: fizwidget
body: Moved to the bottom of the file to reduce clutter in this function.
created_at: 2025-04-08T09:55:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,55 +1,100 @@
 document.addEventListener('DOMContentLoaded', function() {
-    const filterButtons = document.querySelectorAll('.filter-btn');
+    // UI elements
+    const deviceFilterMenu = document.getElementById('device-filter');
+    const hdmiFilterCheckbox = document.getElementById('hdmi-filter');
+    const gridFilterCheckbox = document.getElementById('grid-filter');
+    const languageFilterCheckbox = document.getElementById('language-filter');
     const portfolioItems = document.querySelectorAll('.portfolio-item');
-    
-    // Primary colors corresponding to each filter category
-    const filterColors = {
-        'all': '#c7af26',
-        'RG28XX': '#ba5a31',
-        'RG34XX': '#7cafc4',
-        'RG35XX': '#243e36',
-        'RG40XX': '#9e778f',
-        'RGCUBEXX': '#870058',
-        'TrimUIBrick': '#F25F5C',
-        'TrimUISmartPro': '#243e36',
-        'Legacy': '#c7af26'
-    };
-
-    // Updates the UI to reflect the filter category set in the URL
+
+    // Updates the UI to reflect the URL query params
     function updateUI() {
-        // Get filter category from URL
-        const urlParams = new URLSearchParams(window.location.search);
-        const filterCategory = urlParams.get('filter') ?? 'all';
+        // Get filters from query params
+        const deviceFilter = getQueryParm(queryParams.device) ?? 'all';
+        const isHdmiFilterEnabled = getQueryParm(queryParams.hdmi) === 'true';
+        const isGridFilterEnabled = getQueryParm(queryParams.grid) === 'true';
+        const isLanguageFilterEnabled = getQueryParm(queryParams.language) === 'true';
+
path: docs/scripts.js
user: login: fizwidget
body: Example query params would be:
```
?device=RGCUBEXX&grid=true&hdmi=true
```
created_at: 2025-04-08T09:56:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -103,21 +133,21 @@ button.filter-btn.applied {
 }
 
 .top-right-btn {
path: docs/styles.css
user: login: fizwidget
body: The changes below are just making indentation more consistent.
created_at: 2025-04-08T09:58:20Z
author_association: CONTRIBUTOR

PR comments:
user: login: antiKk
created_at: 2025-04-08T09:58:50Z
body: I love these changes

author_association: COLLABORATOR
user: login: GrumpyGopher
created_at: 2025-04-10T03:44:37Z
body: looks great!
author_association: COLLABORATOR