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';
const data = await getToolsData();
const domains = data.domains;
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) => {
tool.tags.forEach((tag: string) => {
acc[tag] = (acc[tag] || 0) + 1;
@ -19,21 +25,52 @@ const sortedTags = Object.entries(tagFrequency)
---
<div class="filters-container">
<div style="margin-bottom: 1.5rem;">
<!-- Search Section -->
<div class="filter-section">
<div class="filter-card-compact">
<div class="filter-header-compact">
<h3>🔍 Suche</h3>
</div>
<div class="search-wrapper">
<div class="search-icon">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8"/>
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
</svg>
</div>
<input
type="text"
id="search-input"
placeholder="Suchfeld: Name der Software, Beschreibung oder Tags..."
style="width: 100%;"
placeholder="Software, Beschreibung oder Tags durchsuchen..."
class="search-input"
/>
<button id="clear-search" class="search-clear hidden" title="Suche löschen">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"/>
<line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
</div>
</div>
</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;">
<!-- Primary Filters Section - ONLY Domain and Phase -->
<div class="filter-section">
<div class="filter-card-compact">
<div class="filter-header-compact">
<h3>🎯 Primäre Filter</h3>
<button class="filter-reset" id="reset-primary" title="Primäre Filter 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>
</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>
@ -41,43 +78,120 @@ const sortedTags = Object.entries(tagFrequency)
</select>
</div>
<div class="phase-section">
<label style="display: block; margin-bottom: 0.75rem; font-weight: 500;">
Untersuchungsphase
</label>
<div class="phase-buttons">
<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) => (
<button
class="phase-button"
data-phase={phase.id}
type="button"
>
{phase.name}
</button>
<option value={phase.id}>{phase.name}</option>
))}
</select>
</div>
</div>
</div>
</div>
<div style="margin-bottom: 1.5rem;">
<div class="checkbox-wrapper" style="margin-bottom: 1rem;">
<input type="checkbox" id="include-proprietary" checked />
<label for="include-proprietary">Proprietäre Software mit einschließen</label>
<!-- Advanced Filters Section -->
<div class="filter-section">
<div class="filter-card-compact">
<div class="filter-header-compact">
<h3>⚙️ Erweiterte Filter</h3>
<button class="filter-reset" id="reset-advanced" title="Erweiterte Filter 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>
</div>
<div class="mb-4">
<div class="tag-header">
<label style="font-weight: 500;">
Nach Tags filtern
<div class="advanced-filters-compact">
<div class="filter-grid-compact">
<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>
<button
id="tag-cloud-toggle"
class="btn-tag-toggle"
data-expanded="false"
>
<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
@ -93,23 +207,61 @@ const sortedTags = Object.entries(tagFrequency)
</div>
</div>
</div>
</div>
<div style="display: flex; gap: 1rem; margin-bottom: 1.5rem; align-items: center;">
<button class="btn btn-secondary view-toggle active h-12" data-view="grid">Kachelansicht</button>
<button class="btn btn-secondary view-toggle h-12" data-view="matrix">Matrix-Ansicht</button>
<!-- 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>
<button
id="ai-view-toggle"
class="btn btn-secondary view-toggle"
data-view="ai"
style="display: none; background-color: var(--color-accent); color: white; border-color: var(--color-accent);"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem; height:25px">
<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>
Forensic-AI
<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>
@ -117,31 +269,55 @@ const sortedTags = Object.entries(tagFrequency)
window.toolsData = toolsData;
document.addEventListener('DOMContentLoaded', () => {
// Cache DOM elements once
// Cache DOM elements
const elements = {
searchInput: document.getElementById('search-input'),
clearSearch: document.getElementById('clear-search'),
domainSelect: document.getElementById('domain-select'),
phaseButtons: document.querySelectorAll('.phase-button'),
proprietaryCheckbox: document.getElementById('include-proprietary'),
phaseSelect: document.getElementById('phase-select'),
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'),
tagCloud: document.getElementById('tag-cloud'),
tagCloudToggle: document.getElementById('tag-cloud-toggle'),
selectedTags: document.getElementById('selected-tags'),
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
if (!elements.searchInput || !elements.domainSelect || !elements.proprietaryCheckbox) {
if (!elements.searchInput || !elements.domainSelect) {
console.error('Critical filter elements not found');
return;
}
// State management
let selectedTags = new Set();
let selectedPhase = '';
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() {
const visibleCount = 22;
const visibleCount = 20;
elements.tagCloudItems.forEach((item, index) => {
if (index >= visibleCount) {
item.style.display = 'none';
@ -149,9 +325,10 @@ const sortedTags = Object.entries(tagFrequency)
});
}
// Toggle tag cloud expansion
function toggleTagCloud() {
isTagCloudExpanded = !isTagCloudExpanded;
const visibleCount = 22;
const visibleCount = 20;
if (isTagCloudExpanded) {
elements.tagCloud.classList.add('expanded');
@ -182,10 +359,11 @@ const sortedTags = Object.entries(tagFrequency)
}
}
// Filter tag cloud based on search
function filterTagCloud() {
const searchTerm = elements.searchInput.value.toLowerCase();
let visibleCount = 0;
const maxVisibleWhenCollapsed = 22;
const maxVisibleWhenCollapsed = 20;
elements.tagCloudItems.forEach(item => {
const tagName = item.getAttribute('data-tag').toLowerCase();
@ -211,91 +389,132 @@ const sortedTags = Object.entries(tagFrequency)
elements.tagCloudToggle.style.display = hasHiddenTags ? 'block' : 'none';
}
function isToolHosted(tool) {
return tool.projectUrl !== undefined &&
tool.projectUrl !== null &&
tool.projectUrl !== "" &&
tool.projectUrl.trim() !== "";
// Update selected tags display
function updateSelectedTags() {
if (selectedTags.size === 0) {
elements.selectedTags.style.display = 'none';
return;
}
function isMethod(tool) {
return tool.type === 'method';
}
elements.selectedTags.style.display = 'block';
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>
`;
function updateMatrixHighlighting() {
const currentView = document.querySelector('.view-toggle.active')?.getAttribute('data-view');
if (currentView !== 'matrix') return;
const matrixTable = document.querySelector('.matrix-table');
if (!matrixTable) return;
matrixTable.querySelectorAll('.highlight-row, .highlight-column').forEach(el => {
el.classList.remove('highlight-row', 'highlight-column');
// Add event listeners for remove buttons
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() {
const searchTerm = elements.searchInput.value.trim();
const searchTerm = elements.searchInput.value.trim().toLowerCase();
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 domains = tool.domains || [];
const phases = tool.phases || [];
const tags = tool.tags || [];
// Search filter (keep existing logic)
// Search filter
if (searchTerm && !(
tool.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
tool.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
tags.some(tag => tag.toLowerCase().includes(searchTerm.toLowerCase()))
tool.name.toLowerCase().includes(searchTerm) ||
tool.description.toLowerCase().includes(searchTerm) ||
(tool.tags || []).some(tag => tag.toLowerCase().includes(searchTerm))
)) {
return false;
}
// Domain filter
if (selectedDomain && !domains.includes(selectedDomain)) {
if (selectedDomain && !(tool.domains || []).includes(selectedDomain)) {
return false;
}
// Phase filter
if (selectedPhase && !phases.includes(selectedPhase)) {
if (activePhase && !(tool.phases || []).includes(activePhase)) {
return false;
}
// Proprietary filter
if (!includeProprietary && !isMethod(tool) && tool.type !== 'concept' && tool.license === 'Proprietary') {
// Type filter
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;
}
// 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;
}
@ -307,43 +526,91 @@ const sortedTags = Object.entries(tagFrequency)
? window.prioritizeSearchResults(filtered, searchTerm)
: filtered;
updateMatrixHighlighting();
updateResultsCounter(finalResults.length);
// Dispatch event for other components
window.dispatchEvent(new CustomEvent('toolsFiltered', { detail: finalResults }));
}
function handleTagClick(tagItem) {
const tag = tagItem.getAttribute('data-tag');
if (selectedTags.has(tag)) {
selectedTags.delete(tag);
tagItem.classList.remove('active');
} else {
selectedTags.add(tag);
tagItem.classList.add('active');
}
filterTools();
}
function handlePhaseClick(button) {
const phase = button.getAttribute('data-phase');
if (selectedPhase === phase) {
// Reset functions
function resetPrimaryFilters() {
elements.domainSelect.value = '';
elements.phaseSelect.value = '';
selectedPhase = '';
button.classList.remove('active');
} else {
elements.phaseButtons.forEach(btn => btn.classList.remove('active'));
selectedPhase = phase;
button.classList.add('active');
}
filterTools();
}
function handleViewToggle(view) {
function resetAdvancedFilters() {
elements.typeSelect.value = '';
elements.skillSelect.value = '';
elements.platformSelect.value = '';
elements.licenseSelect.value = '';
elements.accessSelect.value = '';
elements.hostedOnly.checked = false;
elements.knowledgebaseOnly.checked = false;
filterTools();
}
function resetTags() {
selectedTags.clear();
elements.tagCloudItems.forEach(item => item.classList.remove('active'));
updateSelectedTags();
filterTools();
}
function resetAllFilters() {
elements.searchInput.value = '';
elements.clearSearch.classList.add('hidden');
resetPrimaryFilters();
resetAdvancedFilters();
resetTags();
filterTagCloud();
}
// Event listeners
elements.searchInput.addEventListener('input', (e) => {
const hasValue = e.target.value.length > 0;
elements.clearSearch.classList.toggle('hidden', !hasValue);
filterTagCloud();
filterTools();
});
elements.clearSearch.addEventListener('click', () => {
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.tagCloudItems.forEach(item => {
item.addEventListener('click', () => {
const tag = item.getAttribute('data-tag');
if (selectedTags.has(tag)) {
removeTag(tag);
} else {
addTag(tag);
}
});
});
elements.viewToggles.forEach(btn => {
btn.classList.toggle('active', btn.getAttribute('data-view') === 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 }));
@ -354,50 +621,23 @@ const sortedTags = Object.entries(tagFrequency)
} else {
filterTools();
}
}
function clearTagFilters() {
selectedTags.clear();
elements.tagCloudItems.forEach(item => item.classList.remove('active'));
filterTools();
}
function clearAllFilters() {
elements.searchInput.value = '';
elements.domainSelect.value = '';
selectedPhase = '';
elements.phaseButtons.forEach(btn => btn.classList.remove('active'));
clearTagFilters();
filterTagCloud();
}
// Event listeners using cached elements
elements.searchInput.addEventListener('input', () => {
filterTagCloud();
filterTools();
});
});
elements.domainSelect.addEventListener('change', filterTools);
elements.proprietaryCheckbox.addEventListener('change', filterTools);
elements.tagCloudToggle.addEventListener('click', toggleTagCloud);
// Reset button listeners
elements.resetButtons.primary.addEventListener('click', resetPrimaryFilters);
elements.resetButtons.advanced.addEventListener('click', resetAdvancedFilters);
elements.resetButtons.tags.addEventListener('click', resetTags);
elements.resetButtons.all.addEventListener('click', resetAllFilters);
elements.tagCloudItems.forEach(item => {
item.addEventListener('click', () => handleTagClick(item));
});
elements.phaseButtons.forEach(btn => {
btn.addEventListener('click', () => handlePhaseClick(btn));
});
elements.viewToggles.forEach(btn => {
btn.addEventListener('click', () => handleViewToggle(btn.getAttribute('data-view')));
});
window.clearTagFilters = clearTagFilters;
window.clearAllFilters = clearAllFilters;
// Expose functions globally for backwards compatibility
window.clearTagFilters = resetTags;
window.clearAllFilters = resetAllFilters;
// Initialize
initTagCloud();
filterTagCloud();
updateSelectedTags();
setTimeout(() => {
filterTools();

View File

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

View File

@ -302,35 +302,27 @@ const phases = data.phases;
}
function hideFilterControls() {
const elements = [
'.domain-phase-container',
'#search-input',
'.tag-cloud',
'.tag-header',
'.checkbox-wrapper'
];
elements.forEach(selector => {
const element = document.querySelector(selector);
if (element) element.style.display = 'none';
const filterSections = document.querySelectorAll('.filter-section');
filterSections.forEach((section, index) => {
if (index < filterSections.length - 1) {
section.style.display = 'none';
}
});
const allInputs = filtersSection.querySelectorAll('input, select, textarea');
allInputs.forEach(input => input.style.display = 'none');
}
function showFilterControls() {
const domainPhaseContainer = document.querySelector('.domain-phase-container');
const filterSections = document.querySelectorAll('.filter-section');
const searchInput = document.getElementById('search-input');
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 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 (tagCloud) tagCloud.style.display = 'flex';
if (tagHeader) tagHeader.style.display = 'flex';
if (tagControls) tagControls.style.display = 'flex';
allInputs.forEach(input => input.style.display = 'block');
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 {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 2rem;
align-items: start;
/* Main Filter Container */
.filters-container {
max-width: 1200px;
margin: 0 auto;
padding: 0 1rem;
}
.phase-buttons {
/* Filter Sections */
.filter-section {
margin-bottom: 1rem;
}
.filter-card-compact {
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;
flex-wrap: wrap;
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;
}
.phase-button {
padding: 0.5rem 1rem;
/* 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-radius: 0.375rem;
font-size: 0.875rem;
background-color: var(--color-bg);
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);
user-select: none;
white-space: nowrap;
min-height: 3rem;
text-align: center;
}
.phase-button:hover {
.phase-chip:hover {
border-color: var(--color-primary);
background-color: var(--color-bg-secondary);
transform: translateY(-1px);
box-shadow: var(--shadow-sm);
}
.phase-button.active {
.phase-chip.active {
background-color: var(--color-primary);
border-color: var(--color-primary);
color: white;
}
.phase-button.active:hover {
background-color: var(--color-primary-hover);
border-color: var(--color-primary-hover);
.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-weight: 500;
color: var(--color-text);
user-select: none;
}
/* Tag System */
.tag-section {
display: flex;
flex-direction: column;
gap: 1rem;
}
.selected-tags {
display: none;
padding: 1rem;
background-color: var(--color-bg-secondary);
border-radius: 0.5rem;
border: 1px solid var(--color-border);
}
.selected-tags-header {
font-size: 0.875rem;
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-header {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 0.75rem;
}
.tag-cloud {
display: flex;
flex-wrap: wrap;
@ -1346,34 +1559,136 @@ input[type="checkbox"] {
opacity: 0.8;
}
.btn-tag-toggle {
padding: 0.25rem 0.75rem;
/* Reset Buttons */
.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.25rem;
background-color: var(--color-bg-secondary);
border-radius: 0.375rem;
color: var(--color-text-secondary);
font-size: 0.75rem;
font-size: 0.8125rem;
cursor: pointer;
transition: var(--transition-fast);
}
.btn-tag-toggle:hover {
background-color: var(--color-bg-tertiary);
color: var(--color-text);
transform: translateY(-1px);
.filter-reset:hover {
background-color: var(--color-bg-secondary);
border-color: var(--color-warning);
color: var(--color-warning);
}
/* View Toggle */
.view-toggle {
.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-radius: 0.375rem;
background-color: var(--color-bg);
color: var(--color-text);
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: var(--transition-fast);
text-decoration: none;
}
.view-toggle:hover {
background-color: var(--color-bg-secondary);
border-color: var(--color-primary);
text-decoration: none;
}
.view-toggle.active {
background-color: var(--color-primary);
color: white;
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)
================================================================= */
@ -2756,23 +3071,21 @@ footer {
gap: 1rem;
}
.phase-buttons {
gap: 0.375rem;
.phase-grid {
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 0.5rem;
}
.phase-button {
padding: 0.375rem 0.75rem;
.phase-chip {
padding: 0.625rem 0.75rem;
font-size: 0.8125rem;
min-height: 2.5rem;
}
.tag-cloud {
max-height: 100px;
}
.tag-header {
gap: 0.75rem;
}
.matrix-table {
min-width: 600px;
}
@ -2899,19 +3212,58 @@ footer {
.grid-4 {
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) {
.phase-buttons {
justify-content: center;
}
.phase-button {
flex: 1;
min-width: 0;
text-align: center;
}
.tag-cloud {
max-height: 80px;
}
@ -2970,15 +3322,6 @@ footer {
}
@media (width <= 480px) {
.phase-buttons {
flex-direction: column;
gap: 0.375rem;
}
.phase-button {
width: 100%;
}
.phase-tools {
grid-template-columns: 1fr;
}
@ -3030,6 +3373,28 @@ footer {
.approach-selector {
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;
}
}