search/filter update for transforms, node version bump

This commit is contained in:
ph1r3754r73r
2026-06-24 10:48:03 -07:00
parent d3b8821090
commit 5abc5eb7b4
5 changed files with 372 additions and 82 deletions
+1 -1
View File
@@ -8,7 +8,7 @@ This workflow automatically builds and deploys the project to GitHub Pages whene
1. **Build Stage:**
- Checks out the repository
- Sets up Node.js (v20) with npm cache enabled
- Sets up Node.js (v24) with npm cache enabled
- Installs dependencies with `npm ci`
- Runs test suite with `npm run test:all`
- Runs `npm run build` which:
+5 -5
View File
@@ -24,12 +24,12 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version: '20'
node-version: '24'
cache: 'npm'
- name: Install dependencies
@@ -52,7 +52,7 @@ jobs:
echo "✅ All critical build files present"
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
uses: actions/upload-pages-artifact@v4
with:
path: 'dist/'
retention-days: 7
@@ -67,4 +67,4 @@ jobs:
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
uses: actions/deploy-pages@v5
+204 -7
View File
@@ -2448,7 +2448,184 @@ body.theme-light .mobile-tool-dropdown {
border: 1px solid var(--input-border);
}
/* Transform category legend styling */
/* Transform filter bar (search + category filters) */
.transform-filter-bar {
display: flex;
flex-direction: column;
gap: 14px;
margin-bottom: 16px;
padding: 14px 16px;
border-radius: 10px;
border: 1px solid var(--input-border);
background: color-mix(in srgb, var(--button-bg) 65%, var(--main-bg-color));
}
.transform-search-wrap {
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
padding: 0 14px;
min-height: 42px;
border-radius: 8px;
border: 1px solid var(--input-border);
background: var(--input-bg);
box-sizing: border-box;
}
.transform-search-icon {
color: var(--text-color);
opacity: 0.45;
flex-shrink: 0;
font-size: 0.9rem;
}
.transform-search-wrap input[type="search"] {
flex: 1;
min-width: 0;
height: 40px;
padding: 0;
border: none;
background: transparent;
color: var(--text-color);
font: inherit;
font-size: 0.92rem;
outline: none;
line-height: 1.4;
}
.transform-search-wrap input[type="search"]::placeholder {
color: var(--text-color);
opacity: 0.4;
}
.transform-search-wrap input[type="search"]::-webkit-search-cancel-button {
-webkit-appearance: none;
}
.transform-filter-clear {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border: none;
background: var(--button-bg);
color: var(--text-color);
opacity: 0.7;
cursor: pointer;
border-radius: 6px;
flex-shrink: 0;
}
.transform-filter-clear:hover {
opacity: 1;
background: var(--button-hover-bg);
}
.transform-category-filters {
display: flex;
flex-direction: column;
gap: 8px;
}
.transform-filter-label {
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--text-color);
opacity: 0.5;
}
.transform-category-chips {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.category-chip {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
font-size: 0.8rem;
font-weight: 500;
line-height: 1.2;
border-radius: 999px;
border: 1px solid var(--input-border);
background: var(--main-bg-color);
color: var(--text-color);
cursor: pointer;
transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease, box-shadow 0.15s ease;
text-transform: capitalize;
box-shadow: none;
}
.category-chip::before {
content: '';
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--chip-accent, var(--accent-color));
flex-shrink: 0;
opacity: 0.9;
}
.category-chip.category-chip-all::before {
background: var(--accent-color);
}
.category-chip:hover {
border-color: color-mix(in srgb, var(--chip-accent, var(--accent-color)) 45%, var(--input-border));
background: var(--button-hover-bg);
}
.category-chip.active-filter {
background: var(--chip-accent, var(--accent-color));
border-color: var(--chip-accent, var(--accent-color));
color: var(--text-on-accent);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15);
}
.category-chip.active-filter::before {
background: var(--text-on-accent);
opacity: 0.9;
}
.category-chip.transform-category-case { --chip-accent: var(--case-color); }
.category-chip.transform-category-cipher { --chip-accent: var(--cipher-color); }
.category-chip.transform-category-concealment { --chip-accent: var(--concealment-color); }
.category-chip.transform-category-encoding { --chip-accent: var(--encoding-color); }
.category-chip.transform-category-format { --chip-accent: var(--format-color); }
.category-chip.transform-category-signwriting { --chip-accent: var(--signwriting-color); }
.category-chip.transform-category-special { --chip-accent: var(--special-color); }
.category-chip.transform-category-symbol { --chip-accent: var(--symbol-color); }
.category-chip.transform-category-technical { --chip-accent: var(--technical-color); }
.category-chip.transform-category-unicode { --chip-accent: var(--unicode-color); }
.category-chip.transform-category-visual { --chip-accent: var(--visual-color); }
.category-chip.transform-category-randomizer { --chip-accent: var(--randomizer-color); }
.category-chip.transform-category-custom_spelling { --chip-accent: var(--custom-spelling-color); }
.transform-filter-empty {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
padding: 24px 16px;
text-align: center;
color: var(--text-color);
opacity: 0.8;
border: 1px dashed var(--input-border);
border-radius: 8px;
}
.transform-filter-empty i {
font-size: 1.25rem;
opacity: 0.6;
}
/* Legacy alias — category filter chips reuse legend-item styles */
.transform-category-legend {
display: flex;
flex-wrap: wrap;
@@ -2491,6 +2668,7 @@ body.theme-light .mobile-tool-dropdown {
background-color: var(--button-hover-bg);
}
.legend-item.active-filter,
.legend-item.active-category {
background-color: var(--accent-color);
color: var(--text-on-accent);
@@ -6887,16 +7065,35 @@ html {
font-size: 0.85rem;
}
/* Legend items wrap better */
/* Category filter bar */
.transform-filter-bar {
padding: 12px;
gap: 12px;
}
.transform-search-wrap {
min-height: 38px;
padding: 0 12px;
}
.transform-search-wrap input[type="search"] {
height: 36px;
font-size: 0.88rem;
}
.transform-category-chips {
gap: 5px;
}
.category-chip {
font-size: 0.75rem;
padding: 5px 10px;
}
.transform-category-legend {
gap: var(--spacing-xs);
}
.legend-item {
font-size: 0.75rem;
padding: 3px var(--spacing-xs);
}
/* Transform chain items stack vertically */
.transform-chain-item .transform-select {
min-width: 100%;
+101 -50
View File
@@ -54,7 +54,9 @@ class TransformTool extends Tool {
transformOptionPrefs: this.loadTransformOptionPrefs(),
transformOptionsModalOpen: false,
transformOptionsModalTransform: null,
transformOptionsDraft: {}
transformOptionsDraft: {},
transformSearchQuery: '',
transformCategoryFilter: ''
};
}
@@ -395,6 +397,104 @@ class TransformTool extends Tool {
!this.favorites.some(f => typeof f === 'string' && f === t.name)
);
},
formatCategoryLabel: function(category) {
return String(category || '').replace(/_/g, ' ');
},
toggleCategoryFilter: function(category) {
this.transformCategoryFilter = this.transformCategoryFilter === category ? '' : category;
},
clearTransformFilters: function() {
this.transformSearchQuery = '';
this.transformCategoryFilter = '';
},
transformSearchActive: function() {
return String(this.transformSearchQuery || '').trim().length > 0;
},
transformMatchesSearchText: function(text) {
const query = String(this.transformSearchQuery || '').trim().toLowerCase();
if (!query) {
return true;
}
return String(text || '').toLowerCase().indexOf(query) !== -1;
},
transformMatchesSearch: function(transform) {
return this.transformMatchesSearchText(transform && transform.name);
},
getFilteredTransformsByCategory: function(category) {
const list = this.getTransformsByCategory(category);
if (!this.transformSearchActive()) {
return list;
}
return list.filter(t => this.transformMatchesSearch(t));
},
categorySectionVisible: function(category) {
if (this.transformCategoryFilter && this.transformCategoryFilter !== category) {
return false;
}
return this.getFilteredTransformsByCategory(category).length > 0;
},
displayItemMatchesFilters: function(item) {
if (!item) {
return false;
}
if (this.transformCategoryFilter) {
if (item.type === 'translate') {
return false;
}
if (item.type === 'transform' && item.transform) {
const category = item.transform.category || this.getDisplayCategory(item.transform.name);
if (category !== this.transformCategoryFilter) {
return false;
}
}
}
if (!this.transformSearchActive()) {
return true;
}
if (item.type === 'translate') {
return this.transformMatchesSearchText(item.langName);
}
if (item.type === 'transform' && item.transform) {
return this.transformMatchesSearch(item.transform);
}
return true;
},
getFilteredFavoriteDisplayItems: function() {
return this.getFavoriteDisplayItems().filter(item => this.displayItemMatchesFilters(item));
},
getFilteredLastUsedDisplayItems: function() {
return this.getLastUsedDisplayItems().filter(item => this.displayItemMatchesFilters(item));
},
favoritesSectionVisible: function() {
return this.showFavorites && this.getFilteredFavoriteDisplayItems().length > 0;
},
lastUsedSectionVisible: function() {
return this.showLastUsed && this.getFilteredLastUsedDisplayItems().length > 0;
},
translateSectionVisible: function() {
if (this.transformCategoryFilter) {
return false;
}
if (!this.transformSearchActive()) {
return true;
}
const langs = (this.translateMainLangs || [])
.concat(this.translateExoticLangs || [])
.concat(this.translateCustomLangs || []);
return langs.some(lang => this.transformMatchesSearchText(lang.name));
},
translateLangVisible: function(langName) {
return this.transformMatchesSearchText(langName);
},
transformListHasNoMatches: function() {
if (!this.transformSearchActive() && !this.transformCategoryFilter) {
return false;
}
if (this.favoritesSectionVisible() || this.lastUsedSectionVisible() || this.translateSectionVisible()) {
return false;
}
return !this.categories.some(category => this.categorySectionVisible(category));
},
isSpecialCategory: function(category) {
return category === 'randomizer';
},
@@ -667,52 +767,7 @@ class TransformTool extends Tool {
if (nextCustomCount !== previousCustomCount) {
this.saveCategoryOrder(this.categories);
}
this.$nextTick(() => {
if (typeof this.initializeCategoryNavigation === 'function') {
this.initializeCategoryNavigation();
}
});
},
initializeCategoryNavigation: function() {
this.$nextTick(() => {
const legendItems = document.querySelectorAll('.transform-category-legend .legend-item');
legendItems.forEach(item => {
const newItem = item.cloneNode(true);
item.parentNode.replaceChild(newItem, item);
});
document.querySelectorAll('.transform-category-legend .legend-item').forEach(item => {
item.addEventListener('click', () => {
const targetId = item.getAttribute('data-target');
if (targetId) {
const targetElement = document.getElementById(targetId);
if (targetElement) {
document.querySelectorAll('.transform-category-legend .legend-item').forEach(li => {
li.classList.remove('active-category');
});
item.classList.add('active-category');
const inputSection = document.querySelector('.input-section');
const inputSectionHeight = inputSection.offsetHeight;
const elementPosition = targetElement.getBoundingClientRect().top + window.pageYOffset;
const offsetPosition = elementPosition - inputSectionHeight - 10;
window.scrollTo({
top: offsetPosition,
behavior: 'smooth'
});
targetElement.classList.add('highlight-section');
setTimeout(() => {
targetElement.classList.remove('highlight-section');
}, 1000);
}
}
});
});
});
}
};
}
@@ -741,7 +796,6 @@ class TransformTool extends Tool {
if (typeof this.refreshCustomSpellingTransforms === 'function') {
this.refreshCustomSpellingTransforms();
}
this.initializeCategoryNavigation();
// Save initial category order to localStorage if it doesn't exist
// This ensures consistent state for category reordering operations
@@ -761,9 +815,6 @@ class TransformTool extends Tool {
if (typeof vueInstance.refreshCustomSpellingTransforms === 'function') {
vueInstance.refreshCustomSpellingTransforms();
}
vueInstance.$nextTick(() => {
vueInstance.initializeCategoryNavigation();
});
}
}
+61 -19
View File
@@ -57,23 +57,56 @@
</div>
<div class="transform-section">
<div class="transform-category-legend">
<div class="legend-title">Categories:</div>
<div>
<div
v-for="category in legendCategories"
:key="category"
:class="'legend-item transform-category-' + category"
:data-target="'category-' + category"
<div class="transform-filter-bar">
<div class="transform-search-wrap">
<i class="fas fa-search transform-search-icon" aria-hidden="true"></i>
<input
type="search"
v-model="transformSearchQuery"
placeholder="Search transforms..."
aria-label="Search transforms"
autocomplete="off"
spellcheck="false"
/>
<button
v-if="transformSearchActive() || transformCategoryFilter"
type="button"
class="transform-filter-clear"
@click="clearTransformFilters"
title="Clear filters"
aria-label="Clear filters"
>
{{ category }}
<i class="fas fa-times"></i>
</button>
</div>
<div class="transform-category-filters">
<span class="transform-filter-label">Category</span>
<div class="transform-category-chips" role="group" aria-label="Filter by category">
<button
type="button"
class="category-chip category-chip-all"
:class="{ 'active-filter': !transformCategoryFilter }"
@click="transformCategoryFilter = ''"
>
All
</button>
<button
v-for="category in legendCategories"
:key="category"
type="button"
:class="'category-chip transform-category-' + category"
:class="{ 'active-filter': transformCategoryFilter === category }"
@click="toggleCategoryFilter(category)"
>
{{ formatCategoryLabel(category) }}
</button>
</div>
</div>
</div>
<div class="transform-categories">
<!-- Favorites Section -->
<div
v-if="showFavorites && getFavoriteDisplayItems().length > 0"
v-if="favoritesSectionVisible()"
id="category-favorites"
class="transform-category-section favorites-section"
>
@@ -81,7 +114,7 @@
<i class="fas fa-star"></i> Favorites
</h4>
<div class="transform-buttons">
<template v-for="item in getFavoriteDisplayItems()" :key="item.key">
<template v-for="item in getFilteredFavoriteDisplayItems()" :key="item.key">
<div v-if="item.type === 'transform'" class="transform-button-group">
<button
@click="applyTransform(item.transform, $event)"
@@ -160,7 +193,7 @@
<!-- Last Used Section -->
<div
v-if="showLastUsed && getLastUsedDisplayItems().length > 0"
v-if="lastUsedSectionVisible()"
id="category-last-used"
class="transform-category-section last-used-section"
>
@@ -168,7 +201,7 @@
<i class="fas fa-clock"></i> Last Used
</h4>
<div class="transform-buttons">
<template v-for="item in getLastUsedDisplayItems()" :key="item.key">
<template v-for="item in getFilteredLastUsedDisplayItems()" :key="item.key">
<div v-if="item.type === 'transform'" class="transform-button-group">
<button
@click="applyTransform(item.transform, $event)"
@@ -246,7 +279,7 @@
</div>
<!-- AI Translation — pinned after Favorites / Last Used, before transform categories -->
<div id="category-translate" class="transform-category-section translate-inline-section">
<div v-if="translateSectionVisible()" id="category-translate" class="transform-category-section translate-inline-section">
<h4 class="category-title transform-category-translate">
<i class="fas fa-language"></i> Translation (AI)
<small class="translate-powered-by">TranslateGemma</small>
@@ -267,6 +300,7 @@
<div class="translate-lang-grid translate-lang-grid-inline">
<div
v-for="lang in translateMainLangs"
v-if="translateLangVisible(lang.name)"
:key="lang.code"
class="translate-lang-card"
>
@@ -304,6 +338,7 @@
<div class="translate-lang-grid translate-lang-grid-inline">
<div
v-for="lang in translateExoticLangs"
v-if="translateLangVisible(lang.name)"
:key="lang.code"
class="translate-lang-card"
>
@@ -363,6 +398,7 @@
<div class="translate-lang-grid translate-lang-grid-inline" v-if="translateCustomLangs.length">
<div
v-for="(lang, idx) in translateCustomLangs"
v-if="translateLangVisible(lang.name)"
:key="'custom-'+idx"
class="translate-lang-card"
>
@@ -406,7 +442,7 @@
<template v-for="(category, categoryIndex) in categories" :key="category">
<div
v-if="isSpecialCategory(category)"
v-if="isSpecialCategory(category) && categorySectionVisible(category)"
:id="'category-' + category"
:class="'transform-category-section randomizer-special'"
>
@@ -416,7 +452,7 @@
</p>
<div class="chaos-overlay" aria-hidden="true"></div>
<div class="transform-buttons">
<div v-for="transform in getTransformsByCategory(category)" :key="getTransformKey(transform)" class="transform-button-group">
<div v-for="transform in getFilteredTransformsByCategory(category)" :key="getTransformKey(transform)" class="transform-button-group">
<button
@click="applyTransform(transform, $event)"
:class="'transform-button transform-category-' + category + ' randomizer-button'"
@@ -460,12 +496,12 @@
<!-- Standard category -->
<div
v-else
v-else-if="categorySectionVisible(category)"
:id="'category-' + category"
:class="'transform-category-section'"
>
<h4 :class="'category-title transform-category-' + category">
{{ category }}
{{ formatCategoryLabel(category) }}
<span class="category-order-controls">
<button
v-if="categoryIndex > 0"
@@ -486,7 +522,7 @@
</span>
</h4>
<div class="transform-buttons">
<div v-for="transform in getTransformsByCategory(category)" :key="getTransformKey(transform)" class="transform-button-group">
<div v-for="transform in getFilteredTransformsByCategory(category)" :key="getTransformKey(transform)" class="transform-button-group">
<button
@click="applyTransform(transform, $event)"
:class="'transform-button transform-category-' + category"
@@ -531,6 +567,12 @@
</div>
</div>
</template>
<div v-if="transformListHasNoMatches()" class="transform-filter-empty">
<i class="fas fa-search" aria-hidden="true"></i>
<p>No transforms match your filters.</p>
<button type="button" class="btn btn-secondary" @click="clearTransformFilters">Clear filters</button>
</div>
</div>
</div>