interface improvements

This commit is contained in:
overcuriousity 2025-07-29 22:43:23 +02:00
parent fd65130bac
commit fcab1485f5
4 changed files with 905 additions and 308 deletions

View File

@ -2,10 +2,16 @@
import { getToolsData } from '../utils/dataService.js'; import { getToolsData } from '../utils/dataService.js';
const data = await getToolsData(); const data = await getToolsData();
const domains = data.domains; const domains = data.domains;
const phases = data.phases; const phases = data.phases;
// Extract unique values dynamically - NO HARD-CODING
const skillLevels = [...new Set(data.tools.map(tool => tool.skillLevel))].filter(Boolean).sort();
const platforms = [...new Set(data.tools.flatMap(tool => tool.platforms || []))].filter(Boolean).sort();
const licenses = [...new Set(data.tools.map(tool => tool.license))].filter(Boolean).sort();
const toolTypes = [...new Set(data.tools.map(tool => tool.type))].filter(Boolean).sort();
const accessTypes = [...new Set(data.tools.map(tool => tool.accessType))].filter(Boolean).sort();
const tagFrequency = data.tools.reduce((acc: Record<string, number>, tool: any) => { const tagFrequency = data.tools.reduce((acc: Record<string, number>, tool: any) => {
tool.tags.forEach((tag: string) => { tool.tags.forEach((tag: string) => {
acc[tag] = (acc[tag] || 0) + 1; acc[tag] = (acc[tag] || 0) + 1;
@ -19,97 +25,243 @@ const sortedTags = Object.entries(tagFrequency)
--- ---
<div class="filters-container"> <div class="filters-container">
<div style="margin-bottom: 1.5rem;"> <!-- Search Section -->
<input <div class="filter-section">
type="text" <div class="filter-card-compact">
id="search-input" <div class="filter-header-compact">
placeholder="Suchfeld: Name der Software, Beschreibung oder Tags..." <h3>🔍 Suche</h3>
style="width: 100%;"
/>
</div>
<div class="domain-phase-container" style="margin-bottom: 1.5rem;">
<div class="domain-section">
<label for="domain-select" style="display: block; margin-bottom: 0.5rem; font-weight: 500;">
Forensische Domäne
</label>
<select id="domain-select" style="max-width: 300px;">
<option value="">Alle Domänen</option>
{domains.map((domain: any) => (
<option value={domain.id}>{domain.name}</option>
))}
</select>
</div>
<div class="phase-section">
<label style="display: block; margin-bottom: 0.75rem; font-weight: 500;">
Untersuchungsphase
</label>
<div class="phase-buttons">
{phases.map((phase: any) => (
<button
class="phase-button"
data-phase={phase.id}
type="button"
>
{phase.name}
</button>
))}
</div> </div>
</div> <div class="search-wrapper">
</div> <div class="search-icon">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<div style="margin-bottom: 1.5rem;"> <circle cx="11" cy="11" r="8"/>
<div class="checkbox-wrapper" style="margin-bottom: 1rem;"> <line x1="21" y1="21" x2="16.65" y2="16.65"/>
<input type="checkbox" id="include-proprietary" checked /> </svg>
<label for="include-proprietary">Proprietäre Software mit einschließen</label> </div>
</div> <input
type="text"
<div class="mb-4"> id="search-input"
<div class="tag-header"> placeholder="Software, Beschreibung oder Tags durchsuchen..."
<label style="font-weight: 500;"> class="search-input"
Nach Tags filtern />
</label> <button id="clear-search" class="search-clear hidden" title="Suche löschen">
<button <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
id="tag-cloud-toggle" <line x1="18" y1="6" x2="6" y2="18"/>
class="btn-tag-toggle" <line x1="6" y1="6" x2="18" y2="18"/>
data-expanded="false" </svg>
>
Mehr zeigen
</button> </button>
</div> </div>
<div class="tag-cloud" id="tag-cloud"> </div>
{sortedTags.map((tag, index) => ( </div>
<button
class="tag-cloud-item" <!-- Primary Filters Section - ONLY Domain and Phase -->
data-tag={tag} <div class="filter-section">
data-frequency={tagFrequency[tag]} <div class="filter-card-compact">
data-index={index} <div class="filter-header-compact">
> <h3>🎯 Primäre Filter</h3>
{tag} <button class="filter-reset" id="reset-primary" title="Primäre Filter zurücksetzen">
<span class="tag-frequency">({tagFrequency[tag]})</span> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
</button> <polyline points="1 4 1 10 7 10"/>
))} <path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/>
</svg>
</button>
</div>
<div class="filter-grid-compact">
<div class="filter-group">
<label class="filter-label">Forensische Domäne</label>
<select id="domain-select" class="filter-select">
<option value="">Alle Domänen</option>
{domains.map((domain: any) => (
<option value={domain.id}>{domain.name}</option>
))}
</select>
</div>
<div class="filter-group">
<label class="filter-label">Untersuchungsphase</label>
<select id="phase-select" class="filter-select">
<option value="">Alle Phasen</option>
{phases.map((phase: any) => (
<option value={phase.id}>{phase.name}</option>
))}
</select>
</div>
</div> </div>
</div> </div>
</div> </div>
<div style="display: flex; gap: 1rem; margin-bottom: 1.5rem; align-items: center;"> <!-- Advanced Filters Section -->
<button class="btn btn-secondary view-toggle active h-12" data-view="grid">Kachelansicht</button> <div class="filter-section">
<button class="btn btn-secondary view-toggle h-12" data-view="matrix">Matrix-Ansicht</button> <div class="filter-card-compact">
<div class="filter-header-compact">
<button <h3>⚙️ Erweiterte Filter</h3>
id="ai-view-toggle" <button class="filter-reset" id="reset-advanced" title="Erweiterte Filter zurücksetzen">
class="btn btn-secondary view-toggle" <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
data-view="ai" <polyline points="1 4 1 10 7 10"/>
style="display: none; background-color: var(--color-accent); color: white; border-color: var(--color-accent);" <path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/>
> </svg>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem; height:25px"> </button>
<path d="M9 11H5a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7a2 2 0 0 0-2-2h-4"/> </div>
<path d="M9 11V7a3 3 0 0 1 6 0v4"/>
</svg> <div class="advanced-filters-compact">
Forensic-AI <div class="filter-grid-compact">
</button> <div class="filter-group">
<label class="filter-label">Tool-Typ</label>
<select id="type-select" class="filter-select">
<option value="">Alle Typen</option>
{toolTypes.map((type: string) => (
<option value={type}>{type}</option>
))}
</select>
</div>
<div class="filter-group">
<label class="filter-label">Skill Level</label>
<select id="skill-select" class="filter-select">
<option value="">Alle Level</option>
{skillLevels.map((level: string) => (
<option value={level}>{level}</option>
))}
</select>
</div>
<div class="filter-group">
<label class="filter-label">Plattform</label>
<select id="platform-select" class="filter-select">
<option value="">Alle Plattformen</option>
{platforms.map((platform: string) => (
<option value={platform}>{platform}</option>
))}
</select>
</div>
<div class="filter-group">
<label class="filter-label">Lizenztyp</label>
<select id="license-select" class="filter-select">
<option value="">Alle Lizenzen</option>
{licenses.map((license: string) => (
<option value={license}>{license}</option>
))}
</select>
</div>
<div class="filter-group">
<label class="filter-label">Zugangsart</label>
<select id="access-select" class="filter-select">
<option value="">Alle Zugangsarten</option>
{accessTypes.map((access: string) => (
<option value={access}>{access}</option>
))}
</select>
</div>
</div>
<div class="filter-toggles-compact">
<label class="toggle-wrapper">
<input type="checkbox" id="hosted-only" />
<span class="toggle-label">🟣 Nur CC24-Server Tools</span>
</label>
<label class="toggle-wrapper">
<input type="checkbox" id="knowledgebase-only" />
<span class="toggle-label">📖 Nur Tools mit Knowledgebase</span>
</label>
</div>
</div>
</div>
</div>
<!-- Tag Filters Section -->
<div class="filter-section">
<div class="filter-card-compact">
<div class="filter-header-compact">
<h3>🏷️ Tag-Filter</h3>
<div class="tag-controls">
<button class="filter-reset" id="reset-tags" title="Tags zurücksetzen">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="1 4 1 10 7 10"/>
<path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/>
</svg>
</button>
<button id="tag-cloud-toggle" class="tag-toggle" data-expanded="false">
Mehr zeigen
</button>
</div>
</div>
<div class="tag-section">
<div class="selected-tags" id="selected-tags"></div>
<div class="tag-cloud" id="tag-cloud">
{sortedTags.map((tag, index) => (
<button
class="tag-cloud-item"
data-tag={tag}
data-frequency={tagFrequency[tag]}
data-index={index}
>
{tag}
<span class="tag-frequency">({tagFrequency[tag]})</span>
</button>
))}
</div>
</div>
</div>
</div>
<!-- View Controls Section -->
<div class="filter-section">
<div class="filter-card-compact">
<div class="filter-header-compact">
<h3>👁️ Ansicht</h3>
<div class="results-count">
<span id="results-counter">Alle Tools</span>
</div>
</div>
<div class="view-controls-compact">
<button class="view-toggle active" data-view="grid">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="7" height="7"/>
<rect x="14" y="3" width="7" height="7"/>
<rect x="14" y="14" width="7" height="7"/>
<rect x="3" y="14" width="7" height="7"/>
</svg>
<span>Kacheln</span>
</button>
<button class="view-toggle" data-view="matrix">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="4" height="4"/>
<rect x="10" y="3" width="4" height="4"/>
<rect x="17" y="3" width="4" height="4"/>
<rect x="3" y="10" width="4" height="4"/>
<rect x="10" y="10" width="4" height="4"/>
<rect x="17" y="10" width="4" height="4"/>
<rect x="3" y="17" width="4" height="4"/>
<rect x="10" y="17" width="4" height="4"/>
<rect x="17" y="17" width="4" height="4"/>
</svg>
<span>Matrix</span>
</button>
<button id="ai-view-toggle" class="btn btn-accent view-toggle" data-view="ai" style="display: none;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M9 11H5a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7a2 2 0 0 0-2-2h-4"/>
<path d="M9 11V7a3 3 0 0 1 6 0v4"/>
</svg>
<span>Forensic-AI</span>
</button>
<button class="filter-reset-all" id="reset-all-filters">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="1 4 1 10 7 10"/>
<path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/>
</svg>
<span>Alle Filter zurücksetzen</span>
</button>
</div>
</div>
</div> </div>
</div> </div>
@ -117,31 +269,55 @@ const sortedTags = Object.entries(tagFrequency)
window.toolsData = toolsData; window.toolsData = toolsData;
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
// Cache DOM elements once // Cache DOM elements
const elements = { const elements = {
searchInput: document.getElementById('search-input'), searchInput: document.getElementById('search-input'),
clearSearch: document.getElementById('clear-search'),
domainSelect: document.getElementById('domain-select'), domainSelect: document.getElementById('domain-select'),
phaseButtons: document.querySelectorAll('.phase-button'), phaseSelect: document.getElementById('phase-select'),
proprietaryCheckbox: document.getElementById('include-proprietary'), typeSelect: document.getElementById('type-select'),
skillSelect: document.getElementById('skill-select'),
platformSelect: document.getElementById('platform-select'),
licenseSelect: document.getElementById('license-select'),
accessSelect: document.getElementById('access-select'),
hostedOnly: document.getElementById('hosted-only'),
knowledgebaseOnly: document.getElementById('knowledgebase-only'),
tagCloudItems: document.querySelectorAll('.tag-cloud-item'), tagCloudItems: document.querySelectorAll('.tag-cloud-item'),
tagCloud: document.getElementById('tag-cloud'), tagCloud: document.getElementById('tag-cloud'),
tagCloudToggle: document.getElementById('tag-cloud-toggle'), tagCloudToggle: document.getElementById('tag-cloud-toggle'),
selectedTags: document.getElementById('selected-tags'),
viewToggles: document.querySelectorAll('.view-toggle'), viewToggles: document.querySelectorAll('.view-toggle'),
aiViewToggle: document.getElementById('ai-view-toggle') resultsCounter: document.getElementById('results-counter'),
resetButtons: {
primary: document.getElementById('reset-primary'),
advanced: document.getElementById('reset-advanced'),
tags: document.getElementById('reset-tags'),
all: document.getElementById('reset-all-filters')
}
}; };
// Verify critical elements exist // Verify critical elements exist
if (!elements.searchInput || !elements.domainSelect || !elements.proprietaryCheckbox) { if (!elements.searchInput || !elements.domainSelect) {
console.error('Critical filter elements not found'); console.error('Critical filter elements not found');
return; return;
} }
// State management
let selectedTags = new Set(); let selectedTags = new Set();
let selectedPhase = ''; let selectedPhase = '';
let isTagCloudExpanded = false; let isTagCloudExpanded = false;
// Helper function to check if tool is hosted
function isToolHosted(tool) {
return tool.projectUrl !== undefined &&
tool.projectUrl !== null &&
tool.projectUrl !== "" &&
tool.projectUrl.trim() !== "";
}
// Initialize tag cloud
function initTagCloud() { function initTagCloud() {
const visibleCount = 22; const visibleCount = 20;
elements.tagCloudItems.forEach((item, index) => { elements.tagCloudItems.forEach((item, index) => {
if (index >= visibleCount) { if (index >= visibleCount) {
item.style.display = 'none'; item.style.display = 'none';
@ -149,9 +325,10 @@ const sortedTags = Object.entries(tagFrequency)
}); });
} }
// Toggle tag cloud expansion
function toggleTagCloud() { function toggleTagCloud() {
isTagCloudExpanded = !isTagCloudExpanded; isTagCloudExpanded = !isTagCloudExpanded;
const visibleCount = 22; const visibleCount = 20;
if (isTagCloudExpanded) { if (isTagCloudExpanded) {
elements.tagCloud.classList.add('expanded'); elements.tagCloud.classList.add('expanded');
@ -181,11 +358,12 @@ const sortedTags = Object.entries(tagFrequency)
}); });
} }
} }
// Filter tag cloud based on search
function filterTagCloud() { function filterTagCloud() {
const searchTerm = elements.searchInput.value.toLowerCase(); const searchTerm = elements.searchInput.value.toLowerCase();
let visibleCount = 0; let visibleCount = 0;
const maxVisibleWhenCollapsed = 22; const maxVisibleWhenCollapsed = 20;
elements.tagCloudItems.forEach(item => { elements.tagCloudItems.forEach(item => {
const tagName = item.getAttribute('data-tag').toLowerCase(); const tagName = item.getAttribute('data-tag').toLowerCase();
@ -211,91 +389,132 @@ const sortedTags = Object.entries(tagFrequency)
elements.tagCloudToggle.style.display = hasHiddenTags ? 'block' : 'none'; elements.tagCloudToggle.style.display = hasHiddenTags ? 'block' : 'none';
} }
function isToolHosted(tool) { // Update selected tags display
return tool.projectUrl !== undefined && function updateSelectedTags() {
tool.projectUrl !== null && if (selectedTags.size === 0) {
tool.projectUrl !== "" && elements.selectedTags.style.display = 'none';
tool.projectUrl.trim() !== ""; return;
} }
function isMethod(tool) {
return tool.type === 'method';
}
function updateMatrixHighlighting() {
const currentView = document.querySelector('.view-toggle.active')?.getAttribute('data-view');
if (currentView !== 'matrix') return;
const matrixTable = document.querySelector('.matrix-table'); elements.selectedTags.style.display = 'block';
if (!matrixTable) return; elements.selectedTags.innerHTML = `
<div class="selected-tags-header">Ausgewählte Tags:</div>
<div class="selected-tags-list">
${Array.from(selectedTags).map(tag => `
<span class="selected-tag">
${tag}
<button class="remove-tag" data-tag="${tag}">×</button>
</span>
`).join('')}
</div>
`;
matrixTable.querySelectorAll('.highlight-row, .highlight-column').forEach(el => { // Add event listeners for remove buttons
el.classList.remove('highlight-row', 'highlight-column'); elements.selectedTags.querySelectorAll('.remove-tag').forEach(btn => {
btn.addEventListener('click', () => {
const tag = btn.getAttribute('data-tag');
removeTag(tag);
});
}); });
const selectedDomain = elements.domainSelect.value;
if (selectedDomain) {
const domainRow = matrixTable.querySelector(`tr[data-domain="${selectedDomain}"]`);
if (domainRow) {
domainRow.classList.add('highlight-row');
}
}
if (selectedPhase) {
const phaseHeaders = matrixTable.querySelectorAll('thead th[data-phase]');
const phaseIndex = Array.from(phaseHeaders).findIndex(th =>
th.getAttribute('data-phase') === selectedPhase
);
if (phaseIndex >= 0) {
const columnIndex = phaseIndex + 1;
matrixTable.querySelectorAll(`tr`).forEach(row => {
const cell = row.children[columnIndex];
if (cell) {
cell.classList.add('highlight-column');
}
});
}
}
} }
// Add/remove tags
function addTag(tag) {
selectedTags.add(tag);
document.querySelector(`[data-tag="${tag}"]`).classList.add('active');
updateSelectedTags();
filterTools();
}
function removeTag(tag) {
selectedTags.delete(tag);
const tagElement = document.querySelector(`[data-tag="${tag}"]`);
if (tagElement) tagElement.classList.remove('active');
updateSelectedTags();
filterTools();
}
// Update results counter
function updateResultsCounter(count) {
const total = window.toolsData.length;
elements.resultsCounter.textContent = count === total
? `${total} Tools`
: `${count} von ${total} Tools`;
}
// Main filter function
function filterTools() { function filterTools() {
const searchTerm = elements.searchInput.value.trim(); const searchTerm = elements.searchInput.value.trim().toLowerCase();
const selectedDomain = elements.domainSelect.value; const selectedDomain = elements.domainSelect.value;
const includeProprietary = elements.proprietaryCheckbox.checked; const selectedPhaseFromSelect = elements.phaseSelect.value;
const selectedType = elements.typeSelect.value;
const selectedSkill = elements.skillSelect.value;
const selectedPlatform = elements.platformSelect.value;
const selectedLicense = elements.licenseSelect.value;
const selectedAccess = elements.accessSelect.value;
const hostedOnly = elements.hostedOnly.checked;
const knowledgebaseOnly = elements.knowledgebaseOnly.checked;
// Use phase from either dropdown or button selection
const activePhase = selectedPhaseFromSelect || selectedPhase;
const filtered = window.toolsData.filter(tool => { const filtered = window.toolsData.filter(tool => {
const domains = tool.domains || []; // Search filter
const phases = tool.phases || [];
const tags = tool.tags || [];
// Search filter (keep existing logic)
if (searchTerm && !( if (searchTerm && !(
tool.name.toLowerCase().includes(searchTerm.toLowerCase()) || tool.name.toLowerCase().includes(searchTerm) ||
tool.description.toLowerCase().includes(searchTerm.toLowerCase()) || tool.description.toLowerCase().includes(searchTerm) ||
tags.some(tag => tag.toLowerCase().includes(searchTerm.toLowerCase())) (tool.tags || []).some(tag => tag.toLowerCase().includes(searchTerm))
)) { )) {
return false; return false;
} }
// Domain filter // Domain filter
if (selectedDomain && !domains.includes(selectedDomain)) { if (selectedDomain && !(tool.domains || []).includes(selectedDomain)) {
return false; return false;
} }
// Phase filter // Phase filter
if (selectedPhase && !phases.includes(selectedPhase)) { if (activePhase && !(tool.phases || []).includes(activePhase)) {
return false; return false;
} }
// Proprietary filter // Type filter
if (!includeProprietary && !isMethod(tool) && tool.type !== 'concept' && tool.license === 'Proprietary') { if (selectedType && tool.type !== selectedType) {
return false;
}
// Skill level filter
if (selectedSkill && tool.skillLevel !== selectedSkill) {
return false;
}
// Platform filter
if (selectedPlatform && !(tool.platforms || []).includes(selectedPlatform)) {
return false;
}
// License filter - NO MORE HARD-CODED LOGIC
if (selectedLicense && tool.license !== selectedLicense) {
return false;
}
// Access type filter
if (selectedAccess && tool.accessType !== selectedAccess) {
return false;
}
// Hosted only filter (CC24-Server tools)
if (hostedOnly && !isToolHosted(tool)) {
return false;
}
// Knowledgebase only filter
if (knowledgebaseOnly && !tool.knowledgebase) {
return false; return false;
} }
// Tag filter // Tag filter
if (selectedTags.size > 0 && !Array.from(selectedTags).every(tag => tags.includes(tag))) { if (selectedTags.size > 0 && !Array.from(selectedTags).every(tag => (tool.tags || []).includes(tag))) {
return false; return false;
} }
@ -307,97 +526,118 @@ const sortedTags = Object.entries(tagFrequency)
? window.prioritizeSearchResults(filtered, searchTerm) ? window.prioritizeSearchResults(filtered, searchTerm)
: filtered; : filtered;
updateMatrixHighlighting(); updateResultsCounter(finalResults.length);
// Dispatch event for other components
window.dispatchEvent(new CustomEvent('toolsFiltered', { detail: finalResults })); window.dispatchEvent(new CustomEvent('toolsFiltered', { detail: finalResults }));
} }
function handleTagClick(tagItem) { // Reset functions
const tag = tagItem.getAttribute('data-tag'); function resetPrimaryFilters() {
elements.domainSelect.value = '';
if (selectedTags.has(tag)) { elements.phaseSelect.value = '';
selectedTags.delete(tag); selectedPhase = '';
tagItem.classList.remove('active');
} else {
selectedTags.add(tag);
tagItem.classList.add('active');
}
filterTools(); filterTools();
} }
function handlePhaseClick(button) { function resetAdvancedFilters() {
const phase = button.getAttribute('data-phase'); elements.typeSelect.value = '';
elements.skillSelect.value = '';
if (selectedPhase === phase) { elements.platformSelect.value = '';
selectedPhase = ''; elements.licenseSelect.value = '';
button.classList.remove('active'); elements.accessSelect.value = '';
} else { elements.hostedOnly.checked = false;
elements.phaseButtons.forEach(btn => btn.classList.remove('active')); elements.knowledgebaseOnly.checked = false;
selectedPhase = phase;
button.classList.add('active');
}
filterTools(); filterTools();
} }
function handleViewToggle(view) { function resetTags() {
elements.viewToggles.forEach(btn => {
btn.classList.toggle('active', btn.getAttribute('data-view') === view);
});
window.dispatchEvent(new CustomEvent('viewChanged', { detail: view }));
if (view === 'hosted') {
const hosted = window.toolsData.filter(tool => isToolHosted(tool));
window.dispatchEvent(new CustomEvent('toolsFiltered', { detail: hosted }));
} else {
filterTools();
}
}
function clearTagFilters() {
selectedTags.clear(); selectedTags.clear();
elements.tagCloudItems.forEach(item => item.classList.remove('active')); elements.tagCloudItems.forEach(item => item.classList.remove('active'));
updateSelectedTags();
filterTools(); filterTools();
} }
function clearAllFilters() { function resetAllFilters() {
elements.searchInput.value = ''; elements.searchInput.value = '';
elements.domainSelect.value = ''; elements.clearSearch.classList.add('hidden');
selectedPhase = ''; resetPrimaryFilters();
elements.phaseButtons.forEach(btn => btn.classList.remove('active')); resetAdvancedFilters();
clearTagFilters(); resetTags();
filterTagCloud(); filterTagCloud();
} }
// Event listeners using cached elements // Event listeners
elements.searchInput.addEventListener('input', () => { elements.searchInput.addEventListener('input', (e) => {
const hasValue = e.target.value.length > 0;
elements.clearSearch.classList.toggle('hidden', !hasValue);
filterTagCloud(); filterTagCloud();
filterTools(); filterTools();
}); });
elements.domainSelect.addEventListener('change', filterTools); elements.clearSearch.addEventListener('click', () => {
elements.proprietaryCheckbox.addEventListener('change', filterTools); elements.searchInput.value = '';
elements.clearSearch.classList.add('hidden');
filterTagCloud();
filterTools();
});
[elements.domainSelect, elements.phaseSelect, elements.typeSelect, elements.skillSelect,
elements.platformSelect, elements.licenseSelect, elements.accessSelect].forEach(select => {
select.addEventListener('change', filterTools);
});
[elements.hostedOnly, elements.knowledgebaseOnly].forEach(checkbox => {
checkbox.addEventListener('change', filterTools);
});
elements.tagCloudToggle.addEventListener('click', toggleTagCloud); elements.tagCloudToggle.addEventListener('click', toggleTagCloud);
elements.tagCloudItems.forEach(item => { elements.tagCloudItems.forEach(item => {
item.addEventListener('click', () => handleTagClick(item)); item.addEventListener('click', () => {
}); const tag = item.getAttribute('data-tag');
if (selectedTags.has(tag)) {
elements.phaseButtons.forEach(btn => { removeTag(tag);
btn.addEventListener('click', () => handlePhaseClick(btn)); } else {
addTag(tag);
}
});
}); });
elements.viewToggles.forEach(btn => { elements.viewToggles.forEach(btn => {
btn.addEventListener('click', () => handleViewToggle(btn.getAttribute('data-view'))); btn.addEventListener('click', () => {
const view = btn.getAttribute('data-view');
// Simple toggle like the old version
elements.viewToggles.forEach(b => {
b.classList.toggle('active', b.getAttribute('data-view') === view);
});
window.dispatchEvent(new CustomEvent('viewChanged', { detail: view }));
if (view === 'hosted') {
const hosted = window.toolsData.filter(tool => isToolHosted(tool));
window.dispatchEvent(new CustomEvent('toolsFiltered', { detail: hosted }));
} else {
filterTools();
}
});
}); });
window.clearTagFilters = clearTagFilters; // Reset button listeners
window.clearAllFilters = clearAllFilters; elements.resetButtons.primary.addEventListener('click', resetPrimaryFilters);
elements.resetButtons.advanced.addEventListener('click', resetAdvancedFilters);
elements.resetButtons.tags.addEventListener('click', resetTags);
elements.resetButtons.all.addEventListener('click', resetAllFilters);
// Expose functions globally for backwards compatibility
window.clearTagFilters = resetTags;
window.clearAllFilters = resetAllFilters;
// Initialize
initTagCloud(); initTagCloud();
filterTagCloud(); filterTagCloud();
updateSelectedTags();
setTimeout(() => { setTimeout(() => {
filterTools(); filterTools();

View File

@ -194,8 +194,8 @@ domains.forEach((domain: any) => {
<script define:vars={{ toolsData: tools, domainAgnosticSoftware, domainAgnosticTools }}> <script define:vars={{ toolsData: tools, domainAgnosticSoftware, domainAgnosticTools }}>
function getSelectedPhase() { function getSelectedPhase() {
const activePhaseButton = document.querySelector('.phase-button.active'); const activePhaseChip = document.querySelector('.phase-chip.active');
return activePhaseButton ? activePhaseButton.getAttribute('data-phase') : ''; return activePhaseChip ? activePhaseChip.getAttribute('data-phase') : '';
} }
function getSelectedDomain() { function getSelectedDomain() {

View File

@ -302,35 +302,27 @@ const phases = data.phases;
} }
function hideFilterControls() { function hideFilterControls() {
const elements = [ const filterSections = document.querySelectorAll('.filter-section');
'.domain-phase-container', filterSections.forEach((section, index) => {
'#search-input', if (index < filterSections.length - 1) {
'.tag-cloud', section.style.display = 'none';
'.tag-header', }
'.checkbox-wrapper'
];
elements.forEach(selector => {
const element = document.querySelector(selector);
if (element) element.style.display = 'none';
}); });
const allInputs = filtersSection.querySelectorAll('input, select, textarea');
allInputs.forEach(input => input.style.display = 'none');
} }
function showFilterControls() { function showFilterControls() {
const domainPhaseContainer = document.querySelector('.domain-phase-container'); const filterSections = document.querySelectorAll('.filter-section');
const searchInput = document.getElementById('search-input'); const searchInput = document.getElementById('search-input');
const tagCloud = document.querySelector('.tag-cloud'); const tagCloud = document.querySelector('.tag-cloud');
const tagHeader = document.querySelector('.tag-header'); const tagControls = document.querySelector('.tag-controls');
const checkboxWrappers = document.querySelectorAll('.checkbox-wrapper'); const checkboxWrappers = document.querySelectorAll('.checkbox-wrapper');
const allInputs = filtersSection.querySelectorAll('input, select, textarea'); const allInputs = filtersSection.querySelectorAll('input, select, textarea');
if (domainPhaseContainer) domainPhaseContainer.style.display = 'grid'; filterSections.forEach(section => section.style.display = 'block');
if (searchInput) searchInput.style.display = 'block'; if (searchInput) searchInput.style.display = 'block';
if (tagCloud) tagCloud.style.display = 'flex'; if (tagCloud) tagCloud.style.display = 'flex';
if (tagHeader) tagHeader.style.display = 'flex'; if (tagControls) tagControls.style.display = 'flex';
allInputs.forEach(input => input.style.display = 'block'); allInputs.forEach(input => input.style.display = 'block');
checkboxWrappers.forEach(wrapper => wrapper.style.display = 'flex'); checkboxWrappers.forEach(wrapper => wrapper.style.display = 'flex');

View File

@ -1217,62 +1217,275 @@ input[type="checkbox"] {
} }
/* =================================================================== /* ===================================================================
15. FILTER AND PHASE CONTROLS (CONSOLIDATED) 15. FILTER AND PHASE CONTROLS (CONSOLIDATED & FIXED)
================================================================= */ ================================================================= */
.domain-phase-container { /* Main Filter Container */
display: grid; .filters-container {
grid-template-columns: 1fr 2fr; max-width: 1200px;
gap: 2rem; margin: 0 auto;
align-items: start; padding: 0 1rem;
} }
.phase-buttons { /* Filter Sections */
display: flex; .filter-section {
flex-wrap: wrap; margin-bottom: 1rem;
gap: 0.5rem;
} }
.phase-button { .filter-card-compact {
padding: 0.5rem 1rem; background-color: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: 0.5rem;
padding: 1rem; /* Reduced from 1.5rem */
box-shadow: var(--shadow-sm);
transition: var(--transition-fast);
}
.filter-card-compact:hover {
box-shadow: var(--shadow-md);
}
.filter-header-compact {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.75rem; /* Reduced from 1.25rem */
padding-bottom: 0.5rem; /* Reduced from 0.75rem */
border-bottom: 1px solid var(--color-border);
}
.filter-header-compact h3 {
margin: 0;
font-size: 0.875rem; /* Reduced from 1rem */
font-weight: 600;
display: flex;
align-items: center;
gap: 0.5rem;
}
/* Search Components */
.search-wrapper {
position: relative;
display: flex;
align-items: center;
}
.search-icon {
position: absolute;
left: 0.75rem;
top: 50%;
transform: translateY(-50%);
color: var(--color-text-secondary);
pointer-events: none;
z-index: 1;
}
.search-input {
width: 100%;
padding: 0.75rem 2.5rem 0.75rem 2.5rem;
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
border-radius: 0.375rem; border-radius: 0.375rem;
font-size: 0.875rem;
background-color: var(--color-bg); background-color: var(--color-bg);
color: var(--color-text); color: var(--color-text);
transition: var(--transition-fast);
}
.search-input:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 3px rgb(37 99 235 / 10%);
}
.search-clear {
position: absolute;
right: 0.75rem;
background: none;
border: none;
color: var(--color-text-secondary);
cursor: pointer;
padding: 0.25rem;
border-radius: 0.25rem;
transition: var(--transition-fast);
z-index: 1;
}
.search-clear:hover {
background-color: var(--color-bg-secondary);
color: var(--color-text);
}
/* Filter Grids & Groups */
.filter-grid-compact {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 0.75rem; /* Reduced from 1rem */
}
.filter-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.filter-label {
font-size: 0.875rem;
font-weight: 500;
color: var(--color-text);
}
.filter-select {
padding: 0.625rem 0.75rem;
border: 1px solid var(--color-border);
border-radius: 0.375rem;
font-size: 0.875rem;
cursor: pointer;
transition: var(--transition-fast);
background-color: var(--color-bg);
color: var(--color-text);
}
.filter-select:focus {
border-color: var(--color-primary);
box-shadow: 0 0 0 2px rgb(37 99 235 / 10%);
outline: none;
}
/* Phase Controls */
.phase-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 0.75rem;
}
.phase-chip {
display: flex;
align-items: center;
justify-content: center;
padding: 0.75rem 1rem;
border: 2px solid var(--color-border);
border-radius: 0.5rem;
background-color: var(--color-bg);
color: var(--color-text);
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: var(--transition-fast);
min-height: 3rem;
text-align: center;
}
.phase-chip:hover {
border-color: var(--color-primary);
background-color: var(--color-bg-secondary);
transform: translateY(-1px);
}
.phase-chip.active {
background-color: var(--color-primary);
border-color: var(--color-primary);
color: white;
}
.advanced-filters-compact {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.filter-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
}
.filter-toggles-compact {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
margin-top: 0.5rem;
}
.toggle-wrapper {
display: flex;
align-items: center;
gap: 0.75rem;
cursor: pointer;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
border: 1px solid var(--color-border);
background-color: var(--color-bg-secondary);
transition: var(--transition-fast);
}
.toggle-wrapper:hover {
border-color: var(--color-primary);
}
.toggle-label {
font-size: 0.875rem; font-size: 0.875rem;
font-weight: 500; font-weight: 500;
cursor: pointer; color: var(--color-text);
transition: var(--transition-fast);
user-select: none; user-select: none;
white-space: nowrap;
} }
.phase-button:hover { /* Tag System */
border-color: var(--color-primary); .tag-section {
background-color: var(--color-bg-secondary); display: flex;
transform: translateY(-1px); flex-direction: column;
box-shadow: var(--shadow-sm); gap: 1rem;
} }
.phase-button.active { .selected-tags {
background-color: var(--color-primary); display: none;
border-color: var(--color-primary); padding: 1rem;
color: white; background-color: var(--color-bg-secondary);
border-radius: 0.5rem;
border: 1px solid var(--color-border);
} }
.phase-button.active:hover { .selected-tags-header {
background-color: var(--color-primary-hover); font-size: 0.875rem;
border-color: var(--color-primary-hover); font-weight: 600;
margin-bottom: 0.75rem;
color: var(--color-text);
}
.selected-tags-list {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.selected-tag {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.375rem 0.75rem;
background-color: var(--color-accent);
color: white;
border-radius: 1rem;
font-size: 0.8125rem;
}
.remove-tag {
background: none;
border: none;
color: white;
cursor: pointer;
font-weight: bold;
padding: 0;
width: 16px;
height: 16px;
border-radius: 50%;
transition: var(--transition-fast);
}
.remove-tag:hover {
background-color: rgba(255, 255, 255, 0.2);
} }
/* Tag Cloud */ /* Tag Cloud */
.tag-header {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 0.75rem;
}
.tag-cloud { .tag-cloud {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@ -1346,26 +1559,105 @@ input[type="checkbox"] {
opacity: 0.8; opacity: 0.8;
} }
.btn-tag-toggle { /* Reset Buttons */
padding: 0.25rem 0.75rem; .filter-reset, .filter-reset-all {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.375rem 0.75rem;
background: none;
border: 1px solid var(--color-border);
border-radius: 0.375rem;
color: var(--color-text-secondary);
font-size: 0.8125rem;
cursor: pointer;
transition: var(--transition-fast);
}
.filter-reset:hover {
background-color: var(--color-bg-secondary);
border-color: var(--color-warning);
color: var(--color-warning);
}
.filter-reset-all {
background-color: var(--color-warning);
color: white;
border-color: var(--color-warning);
}
.filter-reset-all:hover {
background-color: var(--color-warning);
opacity: 0.9;
}
/* Tag Controls */
.tag-controls {
display: flex;
align-items: center;
gap: 0.75rem;
}
.tag-toggle {
padding: 0.375rem 0.75rem;
border: 1px solid var(--color-border);
border-radius: 0.375rem;
background-color: var(--color-bg-secondary);
color: var(--color-text-secondary);
font-size: 0.8125rem;
cursor: pointer;
transition: var(--transition-fast);
}
.tag-toggle:hover {
border-color: var(--color-primary);
color: var(--color-text);
}
.view-controls-compact {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
align-items: center;
}
.results-count {
display: flex;
align-items: center;
gap: 0.5rem;
}
#results-counter {
font-size: 0.875rem;
font-weight: 500;
color: var(--color-text-secondary);
padding: 0.375rem 0.75rem;
background-color: var(--color-bg-secondary);
border-radius: 1rem;
border: 1px solid var(--color-border);
}
/* View Toggle Buttons */
.view-toggle {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
border-radius: 0.25rem; border-radius: 0.375rem;
background-color: var(--color-bg-secondary); background-color: var(--color-bg);
color: var(--color-text-secondary); color: var(--color-text);
font-size: 0.75rem; font-size: 0.875rem;
font-weight: 500;
cursor: pointer; cursor: pointer;
transition: var(--transition-fast); transition: var(--transition-fast);
text-decoration: none;
} }
.btn-tag-toggle:hover { .view-toggle:hover {
background-color: var(--color-bg-tertiary); background-color: var(--color-bg-secondary);
color: var(--color-text); border-color: var(--color-primary);
transform: translateY(-1px); text-decoration: none;
}
/* View Toggle */
.view-toggle {
transition: var(--transition-fast);
} }
.view-toggle.active { .view-toggle.active {
@ -1374,6 +1666,29 @@ input[type="checkbox"] {
border-color: var(--color-primary); border-color: var(--color-primary);
} }
.view-toggle.active:hover {
background-color: var(--color-primary-hover);
border-color: var(--color-primary-hover);
}
#ai-view-toggle {
background-color: var(--color-accent) !important;
color: white !important;
border-color: var(--color-accent) !important;
}
#ai-view-toggle:hover {
background-color: var(--color-accent-hover) !important;
border-color: var(--color-accent-hover) !important;
}
#ai-view-toggle.active {
background-color: var(--color-accent) !important;
border-color: var(--color-accent) !important;
}
/* =================================================================== /* ===================================================================
16. AI INTERFACE (CONSOLIDATED) 16. AI INTERFACE (CONSOLIDATED)
================================================================= */ ================================================================= */
@ -2755,24 +3070,22 @@ footer {
grid-template-columns: 1fr; grid-template-columns: 1fr;
gap: 1rem; gap: 1rem;
} }
.phase-buttons { .phase-grid {
gap: 0.375rem; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 0.5rem;
} }
.phase-button { .phase-chip {
padding: 0.375rem 0.75rem; padding: 0.625rem 0.75rem;
font-size: 0.8125rem; font-size: 0.8125rem;
min-height: 2.5rem;
} }
.tag-cloud { .tag-cloud {
max-height: 100px; max-height: 100px;
} }
.tag-header {
gap: 0.75rem;
}
.matrix-table { .matrix-table {
min-width: 600px; min-width: 600px;
} }
@ -2899,19 +3212,58 @@ footer {
.grid-4 { .grid-4 {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.filters-container {
padding: 0 0.5rem;
}
.filter-grid-compact {
grid-template-columns: 1fr;
gap: 0.5rem;
}
.filter-card-compact {
padding: 0.75rem;
}
.filter-header-compact {
margin-bottom: 0.5rem;
padding-bottom: 0.375rem;
}
.filter-header-compact h3 {
font-size: 0.8125rem;
}
.advanced-filters-compact {
gap: 0.5rem;
}
.filter-row {
grid-template-columns: 1fr;
}
.filter-toggles-compact {
flex-direction: column;
gap: 0.5rem;
}
.toggle-wrapper {
padding: 0.625rem 0.875rem;
}
.view-controls-compact {
flex-direction: column;
align-items: stretch;
gap: 0.5rem;
}
.view-toggle {
justify-content: center;
}
} }
@media (width <= 640px) { @media (width <= 640px) {
.phase-buttons {
justify-content: center;
}
.phase-button {
flex: 1;
min-width: 0;
text-align: center;
}
.tag-cloud { .tag-cloud {
max-height: 80px; max-height: 80px;
} }
@ -2970,15 +3322,6 @@ footer {
} }
@media (width <= 480px) { @media (width <= 480px) {
.phase-buttons {
flex-direction: column;
gap: 0.375rem;
}
.phase-button {
width: 100%;
}
.phase-tools { .phase-tools {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
@ -3030,6 +3373,28 @@ footer {
.approach-selector { .approach-selector {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.tag-controls {
width: 100%;
justify-content: space-between;
}
.phase-grid {
grid-template-columns: 1fr;
}
.phase-chip {
padding: 0.75rem;
min-height: 3rem;
}
.filter-grid-compact {
gap: 0.375rem;
}
.filter-card-compact {
padding: 0.5rem;
}
} }