sharing mechanic with eye-watering animation
This commit is contained in:
		
							parent
							
								
									6ae7f36660
								
							
						
					
					
						commit
						a6b51187b7
					
				@ -21,7 +21,6 @@ export interface Props {
 | 
			
		||||
 | 
			
		||||
const { tool } = Astro.props;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
// Check types
 | 
			
		||||
const isMethod = tool.type === 'method';
 | 
			
		||||
const isConcept = tool.type === 'concept';
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,7 @@
 | 
			
		||||
---
 | 
			
		||||
import { getToolsData } from '../utils/dataService.js';
 | 
			
		||||
import ShareButton from './ShareButton.astro';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
// Load tools data
 | 
			
		||||
@ -146,12 +148,17 @@ domains.forEach((domain: any) => {
 | 
			
		||||
<div class="tool-details" id="tool-details-primary">
 | 
			
		||||
  <div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 1rem;">
 | 
			
		||||
    <h2 id="tool-name-primary" style="margin: 0;">Tool Name</h2>
 | 
			
		||||
    <button class="btn-icon" onclick="window.hideToolDetails('primary')">
 | 
			
		||||
      <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
 | 
			
		||||
        <line x1="18" y1="6" x2="6" y2="18"></line>
 | 
			
		||||
        <line x1="6" y1="6" x2="18" y2="18"></line>
 | 
			
		||||
      </svg>
 | 
			
		||||
    </button>
 | 
			
		||||
    <div style="display: flex; align-items: center; gap: 0.5rem;">
 | 
			
		||||
      <div id="share-button-primary" style="display: none;">
 | 
			
		||||
        <!-- Share button will be populated by JavaScript -->
 | 
			
		||||
      </div>
 | 
			
		||||
      <button class="btn-icon" onclick="window.hideToolDetails('primary')">
 | 
			
		||||
        <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
 | 
			
		||||
          <line x1="18" y1="6" x2="6" y2="18"></line>
 | 
			
		||||
          <line x1="6" y1="6" x2="18" y2="18"></line>
 | 
			
		||||
        </svg>
 | 
			
		||||
      </button>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
  
 | 
			
		||||
  <p id="tool-description-primary" class="text-muted"></p>
 | 
			
		||||
@ -169,12 +176,17 @@ domains.forEach((domain: any) => {
 | 
			
		||||
<div class="tool-details" id="tool-details-secondary">
 | 
			
		||||
  <div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 1rem;">
 | 
			
		||||
    <h2 id="tool-name-secondary" style="margin: 0;">Tool Name</h2>
 | 
			
		||||
    <button class="btn-icon" onclick="window.hideToolDetails('secondary')">
 | 
			
		||||
      <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
 | 
			
		||||
        <line x1="18" y1="6" x2="6" y2="18"></line>
 | 
			
		||||
        <line x1="6" y1="6" x2="18" y2="18"></line>
 | 
			
		||||
      </svg>
 | 
			
		||||
    </button>
 | 
			
		||||
    <div style="display: flex; align-items: center; gap: 0.5rem;">
 | 
			
		||||
      <div id="share-button-secondary" style="display: none;">
 | 
			
		||||
        <!-- Share button will be populated by JavaScript -->
 | 
			
		||||
      </div>
 | 
			
		||||
      <button class="btn-icon" onclick="window.hideToolDetails('secondary')">
 | 
			
		||||
        <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
 | 
			
		||||
          <line x1="18" y1="6" x2="6" y2="18"></line>
 | 
			
		||||
          <line x1="6" y1="6" x2="18" y2="18"></line>
 | 
			
		||||
        </svg>
 | 
			
		||||
      </button>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
  
 | 
			
		||||
  <p id="tool-description-secondary" class="text-muted"></p>
 | 
			
		||||
@ -266,6 +278,201 @@ domains.forEach((domain: any) => {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // ===== SHARING FUNCTIONALITY =====
 | 
			
		||||
  
 | 
			
		||||
  // Create tool slug from name (same logic as ShareButton.astro)
 | 
			
		||||
  function createToolSlug(toolName) {
 | 
			
		||||
    return toolName.toLowerCase()
 | 
			
		||||
      .replace(/[^a-z0-9\s-]/g, '') // Remove special characters
 | 
			
		||||
      .replace(/\s+/g, '-')         // Replace spaces with hyphens
 | 
			
		||||
      .replace(/-+/g, '-')          // Remove duplicate hyphens
 | 
			
		||||
      .replace(/^-|-$/g, '');       // Remove leading/trailing hyphens
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Find tool by name or slug
 | 
			
		||||
  function findTool(identifier) {
 | 
			
		||||
    return toolsData.find(tool => 
 | 
			
		||||
      tool.name === identifier || 
 | 
			
		||||
      createToolSlug(tool.name) === identifier.toLowerCase()
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Generate share URLs
 | 
			
		||||
  function generateShareURL(toolName, view, modal = null) {
 | 
			
		||||
    const toolSlug = createToolSlug(toolName);
 | 
			
		||||
    const baseUrl = window.location.origin + window.location.pathname;
 | 
			
		||||
    const params = new URLSearchParams();
 | 
			
		||||
    params.set('tool', toolSlug);
 | 
			
		||||
    params.set('view', view);
 | 
			
		||||
    if (modal) {
 | 
			
		||||
      params.set('modal', modal);
 | 
			
		||||
    }
 | 
			
		||||
    return `${baseUrl}?${params.toString()}`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Copy to clipboard with feedback
 | 
			
		||||
  async function copyToClipboard(text, button) {
 | 
			
		||||
    try {
 | 
			
		||||
      await navigator.clipboard.writeText(text);
 | 
			
		||||
      
 | 
			
		||||
      // Show feedback
 | 
			
		||||
      const originalHTML = button.innerHTML;
 | 
			
		||||
      button.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20,6 9,17 4,12"/></svg> Kopiert!';
 | 
			
		||||
      button.style.color = 'var(--color-accent)';
 | 
			
		||||
      
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        button.innerHTML = originalHTML;
 | 
			
		||||
        button.style.color = '';
 | 
			
		||||
      }, 2000);
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      // Fallback for older browsers
 | 
			
		||||
      const textArea = document.createElement('textarea');
 | 
			
		||||
      textArea.value = text;
 | 
			
		||||
      document.body.appendChild(textArea);
 | 
			
		||||
      textArea.select();
 | 
			
		||||
      document.execCommand('copy');
 | 
			
		||||
      document.body.removeChild(textArea);
 | 
			
		||||
      
 | 
			
		||||
      // Show feedback
 | 
			
		||||
      const originalHTML = button.innerHTML;
 | 
			
		||||
      button.innerHTML = 'Kopiert!';
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        button.innerHTML = originalHTML;
 | 
			
		||||
      }, 2000);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Show share dialog
 | 
			
		||||
  window.showShareDialog = function(shareButton) {
 | 
			
		||||
    const toolName = shareButton.getAttribute('data-tool-name');
 | 
			
		||||
    const context = shareButton.getAttribute('data-context');
 | 
			
		||||
    
 | 
			
		||||
    // Create modal backdrop
 | 
			
		||||
    let backdrop = document.getElementById('share-modal-backdrop');
 | 
			
		||||
    if (!backdrop) {
 | 
			
		||||
      backdrop = document.createElement('div');
 | 
			
		||||
      backdrop.id = 'share-modal-backdrop';
 | 
			
		||||
      backdrop.style.cssText = `
 | 
			
		||||
        position: fixed; top: 0; left: 0; right: 0; bottom: 0;
 | 
			
		||||
        background: rgba(0, 0, 0, 0.5); z-index: 9999;
 | 
			
		||||
        display: flex; align-items: center; justify-content: center;
 | 
			
		||||
        opacity: 0; transition: opacity 0.2s ease;
 | 
			
		||||
      `;
 | 
			
		||||
      document.body.appendChild(backdrop);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Create share dialog
 | 
			
		||||
    const dialog = document.createElement('div');
 | 
			
		||||
    dialog.style.cssText = `
 | 
			
		||||
      background: var(--color-bg); border: 1px solid var(--color-border);
 | 
			
		||||
      border-radius: 0.75rem; padding: 1.5rem; max-width: 400px; width: 90%;
 | 
			
		||||
      box-shadow: var(--shadow-lg); transform: scale(0.9); transition: transform 0.2s ease;
 | 
			
		||||
    `;
 | 
			
		||||
    
 | 
			
		||||
    dialog.innerHTML = `
 | 
			
		||||
      <div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 1rem;">
 | 
			
		||||
        <h3 style="margin: 0; color: var(--color-primary);">
 | 
			
		||||
          <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem; vertical-align: middle;">
 | 
			
		||||
            <circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/>
 | 
			
		||||
            <line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/>
 | 
			
		||||
          </svg>
 | 
			
		||||
          ${toolName} teilen
 | 
			
		||||
        </h3>
 | 
			
		||||
        <button id="close-share-dialog" style="background: none; border: none; cursor: pointer; padding: 0.25rem;color: var(--color-text-secondary)">
 | 
			
		||||
          <svg width="20" height="20" 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 style="display: flex; flex-direction: column; gap: 0.75rem;">
 | 
			
		||||
        <button class="share-option-btn" data-url="${generateShareURL(toolName, 'grid')}" 
 | 
			
		||||
                style="display: flex; align-items: center; gap: 0.75rem; padding: 0.75rem; border: 1px solid var(--color-border); border-radius: 0.5rem; background: var(--color-bg); cursor: pointer; transition: var(--transition-fast); text-align: left; width: 100%;">
 | 
			
		||||
          <div style="width: 32px; height: 32px; background: var(--color-primary); border-radius: 0.25rem; display: flex; align-items: center; justify-content: center;">
 | 
			
		||||
            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" 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>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div>
 | 
			
		||||
            <div style="font-weight: 500; margin-bottom: 0.25rem;color: var(--color-text-secondary)">Kachelansicht</div>
 | 
			
		||||
            <div style="font-size: 0.8125rem; color: var(--color-text-secondary);">Scrollt zur Karte in der Übersicht</div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </button>
 | 
			
		||||
        
 | 
			
		||||
        <button class="share-option-btn" data-url="${generateShareURL(toolName, 'matrix')}" 
 | 
			
		||||
                style="display: flex; align-items: center; gap: 0.75rem; padding: 0.75rem; border: 1px solid var(--color-border); border-radius: 0.5rem; background: var(--color-bg); cursor: pointer; transition: var(--transition-fast); text-align: left; width: 100%;">
 | 
			
		||||
          <div style="width: 32px; height: 32px; background: var(--color-accent); border-radius: 0.25rem; display: flex; align-items: center; justify-content: center;">
 | 
			
		||||
            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">
 | 
			
		||||
              <path d="M3 3h7v7H3zM14 3h7v7h-7zM14 14h7v7h-7zM3 14h7v7H3z"/>
 | 
			
		||||
            </svg>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div>
 | 
			
		||||
            <div style="font-weight: 500; margin-bottom: 0.25rem;color: var(--color-text-secondary)">Matrix-Ansicht</div>
 | 
			
		||||
            <div style="font-size: 0.8125rem; color: var(--color-text-secondary);">Zeigt Tool-Position in der Matrix</div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </button>
 | 
			
		||||
        
 | 
			
		||||
        <button class="share-option-btn" data-url="${generateShareURL(toolName, 'modal', 'primary')}" 
 | 
			
		||||
                style="display: flex; align-items: center; gap: 0.75rem; padding: 0.75rem; border: 1px solid var(--color-border); border-radius: 0.5rem; background: var(--color-bg); cursor: pointer; transition: var(--transition-fast); text-align: left; width: 100%;">
 | 
			
		||||
          <div style="width: 32px; height: 32px; background: var(--color-warning); border-radius: 0.25rem; display: flex; align-items: center; justify-content: center;">
 | 
			
		||||
            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">
 | 
			
		||||
              <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
 | 
			
		||||
              <polyline points="14 2 14 8 20 8"/>
 | 
			
		||||
            </svg>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div>
 | 
			
		||||
            <div style="font-weight: 500; margin-bottom: 0.25rem;color: var(--color-text-secondary)">Tool-Details</div>
 | 
			
		||||
            <div style="font-size: 0.8125rem; color: var(--color-text-secondary);">Öffnet Detail-Fenster direkt</div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </button>
 | 
			
		||||
      </div>
 | 
			
		||||
    `;
 | 
			
		||||
    
 | 
			
		||||
    backdrop.appendChild(dialog);
 | 
			
		||||
    
 | 
			
		||||
    // Show with animation
 | 
			
		||||
    requestAnimationFrame(() => {
 | 
			
		||||
      backdrop.style.opacity = '1';
 | 
			
		||||
      dialog.style.transform = 'scale(1)';
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    // Event handlers
 | 
			
		||||
    const closeDialog = () => {
 | 
			
		||||
      backdrop.style.opacity = '0';
 | 
			
		||||
      dialog.style.transform = 'scale(0.9)';
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        if (backdrop.parentNode) {
 | 
			
		||||
          document.body.removeChild(backdrop);
 | 
			
		||||
        }
 | 
			
		||||
      }, 200);
 | 
			
		||||
    };
 | 
			
		||||
    
 | 
			
		||||
    backdrop.addEventListener('click', (e) => {
 | 
			
		||||
      if (e.target === backdrop) closeDialog();
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    document.getElementById('close-share-dialog').addEventListener('click', closeDialog);
 | 
			
		||||
    
 | 
			
		||||
    // Share option handlers
 | 
			
		||||
    dialog.querySelectorAll('.share-option-btn').forEach(btn => {
 | 
			
		||||
      btn.addEventListener('mouseover', () => {
 | 
			
		||||
        btn.style.backgroundColor = 'var(--color-bg-secondary)';
 | 
			
		||||
        btn.style.borderColor = 'var(--color-primary)';
 | 
			
		||||
      });
 | 
			
		||||
      
 | 
			
		||||
      btn.addEventListener('mouseout', () => {
 | 
			
		||||
        btn.style.backgroundColor = 'var(--color-bg)';
 | 
			
		||||
        btn.style.borderColor = 'var(--color-border)';
 | 
			
		||||
      });
 | 
			
		||||
      
 | 
			
		||||
      btn.addEventListener('click', () => {
 | 
			
		||||
        const url = btn.getAttribute('data-url');
 | 
			
		||||
        copyToClipboard(url, btn);
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // Make functions globally available
 | 
			
		||||
  window.toggleDomainAgnosticSection = toggleDomainAgnosticSection;
 | 
			
		||||
 | 
			
		||||
@ -372,10 +579,23 @@ domains.forEach((domain: any) => {
 | 
			
		||||
        return `<span class="tag" style="background-color: var(--color-bg-tertiary); color: var(--color-text-secondary); margin: 0.125rem;">${conceptName}</span>`;
 | 
			
		||||
      }).join('');
 | 
			
		||||
 | 
			
		||||
      // Check if mobile device
 | 
			
		||||
      const isMobile = window.innerWidth <= 768;
 | 
			
		||||
      const collapseOnMobile = isMobile && relatedConcepts.length > 2;
 | 
			
		||||
 | 
			
		||||
      tagsHTML += `
 | 
			
		||||
        <div style="margin-top: 1rem;">
 | 
			
		||||
          <strong style="display: block; margin-bottom: 0.5rem; color: var(--color-text);">Verwandte Konzepte:</strong>
 | 
			
		||||
          <div style="display: flex; flex-wrap: wrap; gap: 0.25rem;">
 | 
			
		||||
          <div style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem;">
 | 
			
		||||
            <strong style="color: var(--color-text);">Verwandte Konzepte:</strong>
 | 
			
		||||
            ${collapseOnMobile ? `
 | 
			
		||||
              <button id="concepts-toggle-${modalType}" 
 | 
			
		||||
                      onclick="this.nextElementSibling.style.display = this.nextElementSibling.style.display === 'none' ? 'block' : 'none'; this.textContent = this.textContent === '▼' ? '▲' : '▼';"
 | 
			
		||||
                      style="background: none; border: 1px solid var(--color-border); border-radius: 0.25rem; padding: 0.25rem 0.5rem; cursor: pointer; font-size: 0.75rem;">
 | 
			
		||||
                ▼
 | 
			
		||||
              </button>
 | 
			
		||||
            ` : ''}
 | 
			
		||||
          </div>
 | 
			
		||||
          <div ${collapseOnMobile ? 'style="display: none;"' : ''} style="display: flex; flex-wrap: wrap; gap: 0.25rem;">
 | 
			
		||||
            ${conceptLinks}
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
@ -436,6 +656,30 @@ domains.forEach((domain: any) => {
 | 
			
		||||
    
 | 
			
		||||
    elements.links.innerHTML = linksHTML;
 | 
			
		||||
    
 | 
			
		||||
    // ===== POPULATE SHARE BUTTON =====
 | 
			
		||||
    const shareButtonContainer = document.getElementById(`share-button-${modalType}`);
 | 
			
		||||
    if (shareButtonContainer) {
 | 
			
		||||
      const toolSlug = createToolSlug(tool.name);
 | 
			
		||||
      shareButtonContainer.innerHTML = `
 | 
			
		||||
        <button class="share-btn share-btn--medium" 
 | 
			
		||||
                data-tool-name="${tool.name}" 
 | 
			
		||||
                data-tool-slug="${toolSlug}" 
 | 
			
		||||
                data-context="modal-${modalType}"
 | 
			
		||||
                onclick="event.stopPropagation(); window.showShareDialog(this)"
 | 
			
		||||
                title="${tool.name} teilen"
 | 
			
		||||
                aria-label="${tool.name} teilen">
 | 
			
		||||
          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
 | 
			
		||||
            <circle cx="18" cy="5" r="3"/>
 | 
			
		||||
            <circle cx="6" cy="12" r="3"/>
 | 
			
		||||
            <circle cx="18" cy="19" r="3"/>
 | 
			
		||||
            <line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/>
 | 
			
		||||
            <line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/>
 | 
			
		||||
          </svg>
 | 
			
		||||
        </button>
 | 
			
		||||
      `;
 | 
			
		||||
      shareButtonContainer.style.display = 'block';
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Show modals and update layout
 | 
			
		||||
    const overlay = document.getElementById('modal-overlay');
 | 
			
		||||
    const primaryModal = document.getElementById('tool-details-primary');
 | 
			
		||||
@ -460,14 +704,28 @@ domains.forEach((domain: any) => {
 | 
			
		||||
    const secondaryModal = document.getElementById('tool-details-secondary');
 | 
			
		||||
    
 | 
			
		||||
    if (modalType === 'both' || modalType === 'all') {
 | 
			
		||||
      if (primaryModal) primaryModal.classList.remove('active');
 | 
			
		||||
      if (secondaryModal) secondaryModal.classList.remove('active');
 | 
			
		||||
      if (primaryModal) {
 | 
			
		||||
        primaryModal.classList.remove('active');
 | 
			
		||||
        // Hide share button
 | 
			
		||||
        const shareButtonPrimary = document.getElementById('share-button-primary');
 | 
			
		||||
        if (shareButtonPrimary) shareButtonPrimary.style.display = 'none';
 | 
			
		||||
      }
 | 
			
		||||
      if (secondaryModal) {
 | 
			
		||||
        secondaryModal.classList.remove('active');
 | 
			
		||||
        // Hide share button
 | 
			
		||||
        const shareButtonSecondary = document.getElementById('share-button-secondary');
 | 
			
		||||
        if (shareButtonSecondary) shareButtonSecondary.style.display = 'none';
 | 
			
		||||
      }
 | 
			
		||||
      if (overlay) overlay.classList.remove('active');
 | 
			
		||||
      document.body.classList.remove('modals-side-by-side');
 | 
			
		||||
    } else if (modalType === 'primary' && primaryModal) {
 | 
			
		||||
      primaryModal.classList.remove('active');
 | 
			
		||||
      const shareButtonPrimary = document.getElementById('share-button-primary');
 | 
			
		||||
      if (shareButtonPrimary) shareButtonPrimary.style.display = 'none';
 | 
			
		||||
    } else if (modalType === 'secondary' && secondaryModal) {
 | 
			
		||||
      secondaryModal.classList.remove('active');
 | 
			
		||||
      const shareButtonSecondary = document.getElementById('share-button-secondary');
 | 
			
		||||
      if (shareButtonSecondary) shareButtonSecondary.style.display = 'none';
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Check if any modal is still active
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										206
									
								
								src/data/tools.yaml.example
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										206
									
								
								src/data/tools.yaml.example
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,206 @@
 | 
			
		||||
# This is a minimal example file of the real knowledgebase in ./src/data/tools.yaml
 | 
			
		||||
  - name: Rapid Incident Response Triage on macOS
 | 
			
		||||
    icon: 📋
 | 
			
		||||
    type: method
 | 
			
		||||
    description: >-
 | 
			
		||||
      Spezialisierte Methodik für die schnelle Incident Response auf
 | 
			
		||||
      macOS-Systemen mit Fokus auf die Sammlung kritischer forensischer
 | 
			
		||||
      Artefakte in unter einer Stunde. Adressiert die Lücke zwischen
 | 
			
		||||
      Windows-zentrierten IR-Prozessen und macOS-spezifischen
 | 
			
		||||
      Sicherheitsarchitekturen. Nutzt Tools wie Aftermath für effiziente
 | 
			
		||||
      Datensammlung ohne zeitaufwändige Full-Disk-Images. Besonders wertvoll für
 | 
			
		||||
      Unternehmensumgebungen mit gemischten Betriebssystem-Landschaften.
 | 
			
		||||
    domains:
 | 
			
		||||
      - incident-response
 | 
			
		||||
      - law-enforcement
 | 
			
		||||
      - malware-analysis
 | 
			
		||||
    phases:
 | 
			
		||||
      - data-collection
 | 
			
		||||
      - examination
 | 
			
		||||
    platforms: []
 | 
			
		||||
    related_concepts: null
 | 
			
		||||
    domain-agnostic-software: null
 | 
			
		||||
    skillLevel: intermediate
 | 
			
		||||
    accessType: null
 | 
			
		||||
    url: >-
 | 
			
		||||
      https://www.sans.org/white-papers/rapid-incident-response-on-macos-actionable-insights-under-hour/
 | 
			
		||||
    projectUrl: null
 | 
			
		||||
    license: null
 | 
			
		||||
    knowledgebase: null
 | 
			
		||||
    tags:
 | 
			
		||||
      - macos
 | 
			
		||||
      - rapid-response
 | 
			
		||||
      - triage
 | 
			
		||||
      - incident-response
 | 
			
		||||
      - aftermath
 | 
			
		||||
      - enterprise
 | 
			
		||||
      - methodology
 | 
			
		||||
      - apple
 | 
			
		||||
  - name: Aftermath
 | 
			
		||||
    icon: 📦
 | 
			
		||||
    type: software
 | 
			
		||||
    description: >-
 | 
			
		||||
      Jamf's Open-Source-Tool für die schnelle Sammlung forensischer Artefakte
 | 
			
		||||
      auf macOS-Systemen. Sammelt kritische Daten wie Prozessinformationen,
 | 
			
		||||
      Netzwerkverbindungen, Dateisystem-Metadaten und Systemkonfigurationen ohne
 | 
			
		||||
      Full-Disk-Imaging. Speziell entwickelt für die Rapid-Response-Triage in
 | 
			
		||||
      Enterprise-Umgebungen mit macOS-Geräten. Normalisiert Zeitstempel und
 | 
			
		||||
      erstellt durchsuchbare Ausgabeformate für effiziente Analyse.
 | 
			
		||||
    domains:
 | 
			
		||||
      - incident-response
 | 
			
		||||
      - law-enforcement
 | 
			
		||||
      - malware-analysis
 | 
			
		||||
    phases:
 | 
			
		||||
      - data-collection
 | 
			
		||||
      - examination
 | 
			
		||||
    platforms:
 | 
			
		||||
      - macOS
 | 
			
		||||
    related_concepts: null
 | 
			
		||||
    domain-agnostic-software: null
 | 
			
		||||
    skillLevel: intermediate
 | 
			
		||||
    accessType: download
 | 
			
		||||
    url: https://github.com/jamf/aftermath/
 | 
			
		||||
    projectUrl: ''
 | 
			
		||||
    license: Apache 2.0
 | 
			
		||||
    knowledgebase: false
 | 
			
		||||
    tags:
 | 
			
		||||
      - macos
 | 
			
		||||
      - incident-response
 | 
			
		||||
      - triage
 | 
			
		||||
      - artifact-collection
 | 
			
		||||
      - rapid-response
 | 
			
		||||
      - jamf
 | 
			
		||||
      - enterprise
 | 
			
		||||
      - commandline
 | 
			
		||||
  - name: Regular Expressions (Regex)
 | 
			
		||||
    icon: 🔤
 | 
			
		||||
    type: concept
 | 
			
		||||
    description: >-
 | 
			
		||||
      Pattern matching language for searching, extracting, and manipulating
 | 
			
		||||
      text.  Essential for log analysis, malware signature creation, and data
 | 
			
		||||
      extraction from  unstructured sources. Forms the backbone of many forensic
 | 
			
		||||
      tools and custom scripts.
 | 
			
		||||
    domains:
 | 
			
		||||
      - incident-response
 | 
			
		||||
      - malware-analysis
 | 
			
		||||
      - network-forensics
 | 
			
		||||
      - fraud-investigation
 | 
			
		||||
    phases:
 | 
			
		||||
      - examination
 | 
			
		||||
      - analysis
 | 
			
		||||
    platforms: []
 | 
			
		||||
    related_concepts: null
 | 
			
		||||
    domain-agnostic-software: null
 | 
			
		||||
    skillLevel: intermediate
 | 
			
		||||
    accessType: null
 | 
			
		||||
    url: https://regexr.com/
 | 
			
		||||
    projectUrl: null
 | 
			
		||||
    license: null
 | 
			
		||||
    knowledgebase: true
 | 
			
		||||
    tags:
 | 
			
		||||
      - pattern-matching
 | 
			
		||||
      - text-processing
 | 
			
		||||
      - log-analysis
 | 
			
		||||
      - string-manipulation
 | 
			
		||||
      - search-algorithms
 | 
			
		||||
  - name: SQL Query Fundamentals
 | 
			
		||||
    icon: 🗃️
 | 
			
		||||
    type: concept
 | 
			
		||||
    description: >-
 | 
			
		||||
      Structured Query Language for database interrogation and analysis.
 | 
			
		||||
      Critical for  examining application databases, SQLite artifacts from
 | 
			
		||||
      mobile devices, and  browser history databases. Enables complex
 | 
			
		||||
      correlation and filtering of large datasets.
 | 
			
		||||
    domains:
 | 
			
		||||
      - incident-response
 | 
			
		||||
      - mobile-forensics
 | 
			
		||||
      - fraud-investigation
 | 
			
		||||
      - cloud-forensics
 | 
			
		||||
    phases:
 | 
			
		||||
      - examination
 | 
			
		||||
      - analysis
 | 
			
		||||
    platforms: []
 | 
			
		||||
    related_concepts: null
 | 
			
		||||
    domain-agnostic-software: null
 | 
			
		||||
    skillLevel: intermediate
 | 
			
		||||
    accessType: null
 | 
			
		||||
    url: https://www.w3schools.com/sql/
 | 
			
		||||
    projectUrl: null
 | 
			
		||||
    license: null
 | 
			
		||||
    knowledgebase: false
 | 
			
		||||
    tags:
 | 
			
		||||
      - database-analysis
 | 
			
		||||
      - query-language
 | 
			
		||||
      - data-correlation
 | 
			
		||||
      - mobile-artifacts
 | 
			
		||||
      - browser-forensics
 | 
			
		||||
  - name: Hash Functions & Digital Signatures
 | 
			
		||||
    icon: 🔐
 | 
			
		||||
    type: concept
 | 
			
		||||
    description: >-
 | 
			
		||||
      Cryptographic principles for data integrity verification and
 | 
			
		||||
      authentication.  Fundamental for evidence preservation, malware
 | 
			
		||||
      identification, and establishing  chain of custody. Understanding of MD5,
 | 
			
		||||
      SHA, and digital signature validation.
 | 
			
		||||
    domains:
 | 
			
		||||
      - incident-response
 | 
			
		||||
      - law-enforcement
 | 
			
		||||
      - malware-analysis
 | 
			
		||||
      - cloud-forensics
 | 
			
		||||
    phases:
 | 
			
		||||
      - data-collection
 | 
			
		||||
      - examination
 | 
			
		||||
    platforms: []
 | 
			
		||||
    related_concepts: null
 | 
			
		||||
    domain-agnostic-software: null
 | 
			
		||||
    skillLevel: advanced
 | 
			
		||||
    accessType: null
 | 
			
		||||
    url: https://en.wikipedia.org/wiki/Cryptographic_hash_function
 | 
			
		||||
    projectUrl: null
 | 
			
		||||
    license: null
 | 
			
		||||
    knowledgebase: false
 | 
			
		||||
    tags:
 | 
			
		||||
      - cryptography
 | 
			
		||||
      - data-integrity
 | 
			
		||||
      - evidence-preservation
 | 
			
		||||
      - malware-identification
 | 
			
		||||
      - chain-of-custody
 | 
			
		||||
domains:
 | 
			
		||||
  - id: incident-response
 | 
			
		||||
    name: Incident Response & Breach-Untersuchung
 | 
			
		||||
  - id: law-enforcement
 | 
			
		||||
    name: Strafverfolgung & Kriminalermittlung
 | 
			
		||||
  - id: malware-analysis
 | 
			
		||||
    name: Malware-Analyse & Reverse Engineering
 | 
			
		||||
  - id: fraud-investigation
 | 
			
		||||
    name: Betrugs- & Finanzkriminalität
 | 
			
		||||
  - id: network-forensics
 | 
			
		||||
    name: Netzwerk-Forensik & Traffic-Analyse
 | 
			
		||||
  - id: mobile-forensics
 | 
			
		||||
    name: Mobile Geräte & App-Forensik
 | 
			
		||||
  - id: cloud-forensics
 | 
			
		||||
    name: Cloud & Virtuelle Umgebungen
 | 
			
		||||
  - id: ics-forensics
 | 
			
		||||
    name: Industrielle Kontrollsysteme (ICS/SCADA)
 | 
			
		||||
phases:
 | 
			
		||||
  - id: data-collection
 | 
			
		||||
    name: Datensammlung
 | 
			
		||||
    description: Imaging, Acquisition, Remote Collection Tools
 | 
			
		||||
  - id: examination
 | 
			
		||||
    name: Auswertung
 | 
			
		||||
    description: Parsing, Extraction, Initial Analysis Tools
 | 
			
		||||
  - id: analysis
 | 
			
		||||
    name: Analyse
 | 
			
		||||
    description: Deep Analysis, Correlation, Visualization Tools
 | 
			
		||||
  - id: reporting
 | 
			
		||||
    name: Bericht & Präsentation
 | 
			
		||||
    description: >-
 | 
			
		||||
      Documentation, Visualization, Presentation Tools (z.B. QGIS für Geodaten,
 | 
			
		||||
      Timeline-Tools)
 | 
			
		||||
domain-agnostic-software:
 | 
			
		||||
  - id: collaboration-general
 | 
			
		||||
    name: Übergreifend & Kollaboration
 | 
			
		||||
    description: Cross-cutting tools and collaboration platforms
 | 
			
		||||
  - id: specific-os
 | 
			
		||||
    name: Betriebssysteme
 | 
			
		||||
    description: Operating Systems which focus on forensics
 | 
			
		||||
@ -93,15 +93,31 @@ const tools = data.tools;
 | 
			
		||||
</BaseLayout>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
  // Extend Window interface for custom properties
 | 
			
		||||
  declare global {
 | 
			
		||||
    interface Window {
 | 
			
		||||
      toolsData: any[];
 | 
			
		||||
      showToolDetails: (toolName: string, modalType?: string) => void;
 | 
			
		||||
      hideToolDetails: (modalType?: string) => void;
 | 
			
		||||
      hideAllToolDetails: () => void;
 | 
			
		||||
      clearAllFilters?: () => void;
 | 
			
		||||
      restoreAIResults?: () => void;
 | 
			
		||||
      switchToAIView?: () => void;
 | 
			
		||||
      showShareDialog: (shareButton: HTMLElement) => void;
 | 
			
		||||
      navigateToGrid: (toolName: string) => void;
 | 
			
		||||
      navigateToMatrix: (toolName: string) => void;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Handle view changes and filtering
 | 
			
		||||
  document.addEventListener('DOMContentLoaded', () => {
 | 
			
		||||
    const toolsContainer = document.getElementById('tools-container');
 | 
			
		||||
    const toolsGrid = document.getElementById('tools-grid');
 | 
			
		||||
    const matrixContainer = document.getElementById('matrix-container');
 | 
			
		||||
    const aiInterface = document.getElementById('ai-interface');
 | 
			
		||||
    const filtersSection = document.getElementById('filters-section');
 | 
			
		||||
    const noResults = document.getElementById('no-results');
 | 
			
		||||
    const aiQueryBtn = document.getElementById('ai-query-btn');
 | 
			
		||||
    const toolsContainer = document.getElementById('tools-container') as HTMLElement;
 | 
			
		||||
    const toolsGrid = document.getElementById('tools-grid') as HTMLElement;
 | 
			
		||||
    const matrixContainer = document.getElementById('matrix-container') as HTMLElement;
 | 
			
		||||
    const aiInterface = document.getElementById('ai-interface') as HTMLElement;
 | 
			
		||||
    const filtersSection = document.getElementById('filters-section') as HTMLElement;
 | 
			
		||||
    const noResults = document.getElementById('no-results') as HTMLElement;
 | 
			
		||||
    const aiQueryBtn = document.getElementById('ai-query-btn') as HTMLButtonElement;
 | 
			
		||||
    
 | 
			
		||||
    // Guard against null elements
 | 
			
		||||
    if (!toolsContainer || !toolsGrid || !matrixContainer || !noResults || !aiInterface || !filtersSection) {
 | 
			
		||||
@ -109,12 +125,9 @@ const tools = data.tools;
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Initial tools HTML
 | 
			
		||||
    const initialToolsHTML = toolsContainer.innerHTML;
 | 
			
		||||
    
 | 
			
		||||
    // Simple sorting function - no external imports needed
 | 
			
		||||
    function sortTools(tools, sortBy = 'default') {
 | 
			
		||||
      const sorted = [...tools]; // Don't mutate original array
 | 
			
		||||
    // Simple sorting function
 | 
			
		||||
    function sortTools(tools: any[], sortBy = 'default') {
 | 
			
		||||
      const sorted = [...tools];
 | 
			
		||||
      
 | 
			
		||||
      switch (sortBy) {
 | 
			
		||||
        case 'alphabetical':
 | 
			
		||||
@ -122,16 +135,16 @@ const tools = data.tools;
 | 
			
		||||
        case 'difficulty':
 | 
			
		||||
          const difficultyOrder = { 'novice': 0, 'beginner': 1, 'intermediate': 2, 'advanced': 3, 'expert': 4 };
 | 
			
		||||
          return sorted.sort((a, b) => 
 | 
			
		||||
            (difficultyOrder[a.skillLevel] || 999) - (difficultyOrder[b.skillLevel] || 999)
 | 
			
		||||
            (difficultyOrder[a.skillLevel as keyof typeof difficultyOrder] || 999) - (difficultyOrder[b.skillLevel as keyof typeof difficultyOrder] || 999)
 | 
			
		||||
          );
 | 
			
		||||
        case 'type':
 | 
			
		||||
          const typeOrder = { 'concept': 0, 'method': 1, 'software': 2 };
 | 
			
		||||
          return sorted.sort((a, b) => 
 | 
			
		||||
            (typeOrder[a.type] || 999) - (typeOrder[b.type] || 999)
 | 
			
		||||
            (typeOrder[a.type as keyof typeof typeOrder] || 999) - (typeOrder[b.type as keyof typeof typeOrder] || 999)
 | 
			
		||||
          );
 | 
			
		||||
        case 'default':
 | 
			
		||||
        default:
 | 
			
		||||
          return sorted; // No sorting - embrace the entropy
 | 
			
		||||
          return sorted;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
@ -159,31 +172,21 @@ const tools = data.tools;
 | 
			
		||||
        const authStatus = await checkAuthentication();
 | 
			
		||||
        
 | 
			
		||||
        if (authStatus.authRequired && !authStatus.authenticated) {
 | 
			
		||||
          // Redirect to login, then back to AI view
 | 
			
		||||
          const returnUrl = `${window.location.pathname}?view=ai`;
 | 
			
		||||
          window.location.href = `/api/auth/login?returnTo=${encodeURIComponent(returnUrl)}`;
 | 
			
		||||
        } else {
 | 
			
		||||
          // Switch to AI view directly
 | 
			
		||||
          switchToView('ai');
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Check URL parameters on page load for view switching
 | 
			
		||||
    const urlParams = new URLSearchParams(window.location.search);
 | 
			
		||||
    const viewParam = urlParams.get('view');
 | 
			
		||||
    if (viewParam === 'ai') {
 | 
			
		||||
      // User was redirected after authentication, switch to AI view
 | 
			
		||||
      switchToView('ai');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
// Function to switch between different views
 | 
			
		||||
    function switchToView(view) {
 | 
			
		||||
      // Hide all views first (using non-null assertions since we've already checked)
 | 
			
		||||
      toolsGrid!.style.display = 'none';
 | 
			
		||||
      matrixContainer!.style.display = 'none';
 | 
			
		||||
      aiInterface!.style.display = 'none';
 | 
			
		||||
      filtersSection!.style.display = 'none';
 | 
			
		||||
    // Function to switch between different views
 | 
			
		||||
    function switchToView(view: string) {
 | 
			
		||||
      // Hide all views first
 | 
			
		||||
      toolsGrid.style.display = 'none';
 | 
			
		||||
      matrixContainer.style.display = 'none';
 | 
			
		||||
      aiInterface.style.display = 'none';
 | 
			
		||||
      filtersSection.style.display = 'none';
 | 
			
		||||
 | 
			
		||||
      // Update view toggle buttons
 | 
			
		||||
      const viewToggles = document.querySelectorAll('.view-toggle');
 | 
			
		||||
@ -191,128 +194,29 @@ const tools = data.tools;
 | 
			
		||||
        btn.classList.toggle('active', btn.getAttribute('data-view') === view);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      // Show appropriate view
 | 
			
		||||
      // Show appropriate view and manage filter visibility
 | 
			
		||||
      switch (view) {
 | 
			
		||||
        case 'ai':
 | 
			
		||||
          aiInterface!.style.display = 'block';
 | 
			
		||||
          // Keep filters visible in AI mode for view switching
 | 
			
		||||
          filtersSection!.style.display = 'block';
 | 
			
		||||
          
 | 
			
		||||
          // Hide filter controls in AI mode - AGGRESSIVE APPROACH
 | 
			
		||||
          const domainPhaseContainer = document.querySelector('.domain-phase-container') as HTMLElement;
 | 
			
		||||
          const searchInput = document.getElementById('search-input') as HTMLElement;
 | 
			
		||||
          const tagCloud = document.querySelector('.tag-cloud') as HTMLElement;
 | 
			
		||||
          // Hide all checkbox wrappers
 | 
			
		||||
          const checkboxWrappers = document.querySelectorAll('.checkbox-wrapper');
 | 
			
		||||
          // Hide the "Nach Tags filtern" header and button
 | 
			
		||||
          const tagHeader = document.querySelector('.tag-header') as HTMLElement;
 | 
			
		||||
          // Hide any elements containing "Proprietäre Software" or "Nach Tags filtern"
 | 
			
		||||
          const filterLabels = document.querySelectorAll('label, .tag-header, h4, h3');
 | 
			
		||||
          // Hide ALL input elements in the filters section (more aggressive)
 | 
			
		||||
          const allInputs = filtersSection!.querySelectorAll('input, select, textarea');
 | 
			
		||||
 | 
			
		||||
          
 | 
			
		||||
          if (domainPhaseContainer) domainPhaseContainer.style.display = 'none';
 | 
			
		||||
          if (searchInput) searchInput.style.display = 'none';
 | 
			
		||||
          if (tagCloud) tagCloud.style.display = 'none';
 | 
			
		||||
          if (tagHeader) tagHeader.style.display = 'none';
 | 
			
		||||
          
 | 
			
		||||
          // Hide ALL inputs in the filters section
 | 
			
		||||
          allInputs.forEach(input => {
 | 
			
		||||
            (input as HTMLElement).style.display = 'none';
 | 
			
		||||
          });
 | 
			
		||||
          
 | 
			
		||||
          checkboxWrappers.forEach(wrapper => {
 | 
			
		||||
            (wrapper as HTMLElement).style.display = 'none';
 | 
			
		||||
          });
 | 
			
		||||
          
 | 
			
		||||
          // Hide specific filter section elements by text content
 | 
			
		||||
          filterLabels.forEach(element => {
 | 
			
		||||
            const text = element.textContent?.toLowerCase() || '';
 | 
			
		||||
            if (text.includes('proprietäre') || text.includes('tags filtern') || text.includes('nach tags') || text.includes('suchen') || text.includes('search')) {
 | 
			
		||||
              (element as HTMLElement).style.display = 'none';
 | 
			
		||||
            }
 | 
			
		||||
          });
 | 
			
		||||
          
 | 
			
		||||
          // Restore previous AI results if they exist
 | 
			
		||||
          if ((window as any).restoreAIResults) {
 | 
			
		||||
            (window as any).restoreAIResults();
 | 
			
		||||
          aiInterface.style.display = 'block';
 | 
			
		||||
          filtersSection.style.display = 'block';
 | 
			
		||||
          hideFilterControls();
 | 
			
		||||
          if (window.restoreAIResults) {
 | 
			
		||||
            window.restoreAIResults();
 | 
			
		||||
          }
 | 
			
		||||
          // Focus on the input
 | 
			
		||||
          const aiInput = document.getElementById('ai-query-input');
 | 
			
		||||
          const aiInput = document.getElementById('ai-query-input') as HTMLTextAreaElement;
 | 
			
		||||
          if (aiInput) {
 | 
			
		||||
            setTimeout(() => aiInput.focus(), 100);
 | 
			
		||||
          }
 | 
			
		||||
          break;
 | 
			
		||||
        case 'matrix':
 | 
			
		||||
          matrixContainer!.style.display = 'block';
 | 
			
		||||
          filtersSection!.style.display = 'block';
 | 
			
		||||
          
 | 
			
		||||
          // Show filter controls in matrix mode
 | 
			
		||||
          const domainPhaseContainerMatrix = document.querySelector('.domain-phase-container') as HTMLElement;
 | 
			
		||||
          const searchInputMatrix = document.getElementById('search-input') as HTMLElement;
 | 
			
		||||
          const tagCloudMatrix = document.querySelector('.tag-cloud') as HTMLElement;
 | 
			
		||||
          const checkboxWrappersMatrix = document.querySelectorAll('.checkbox-wrapper');
 | 
			
		||||
          const tagHeaderMatrix = document.querySelector('.tag-header') as HTMLElement;
 | 
			
		||||
          const filterLabelsMatrix = document.querySelectorAll('label, .tag-header, h4, h3');
 | 
			
		||||
          const allInputsMatrix = filtersSection!.querySelectorAll('input, select, textarea');
 | 
			
		||||
          
 | 
			
		||||
          if (domainPhaseContainerMatrix) domainPhaseContainerMatrix.style.display = 'grid';
 | 
			
		||||
          if (searchInputMatrix) searchInputMatrix.style.display = 'block';
 | 
			
		||||
          if (tagCloudMatrix) tagCloudMatrix.style.display = 'flex';
 | 
			
		||||
          if (tagHeaderMatrix) tagHeaderMatrix.style.display = 'flex';
 | 
			
		||||
          
 | 
			
		||||
          // Restore ALL inputs in the filters section
 | 
			
		||||
          allInputsMatrix.forEach(input => {
 | 
			
		||||
            (input as HTMLElement).style.display = 'block';
 | 
			
		||||
          });
 | 
			
		||||
          
 | 
			
		||||
          checkboxWrappersMatrix.forEach(wrapper => {
 | 
			
		||||
            (wrapper as HTMLElement).style.display = 'flex';
 | 
			
		||||
          });
 | 
			
		||||
          
 | 
			
		||||
          // Restore filter section elements
 | 
			
		||||
          filterLabelsMatrix.forEach(element => {
 | 
			
		||||
            const text = element.textContent?.toLowerCase() || '';
 | 
			
		||||
            if (text.includes('proprietäre') || text.includes('tags filtern') || text.includes('nach tags') || text.includes('suchen') || text.includes('search')) {
 | 
			
		||||
              (element as HTMLElement).style.display = 'block';
 | 
			
		||||
            }
 | 
			
		||||
          });
 | 
			
		||||
          matrixContainer.style.display = 'block';
 | 
			
		||||
          filtersSection.style.display = 'block';
 | 
			
		||||
          showFilterControls();
 | 
			
		||||
          break;
 | 
			
		||||
        default: // grid
 | 
			
		||||
          toolsGrid!.style.display = 'block';
 | 
			
		||||
          filtersSection!.style.display = 'block';
 | 
			
		||||
          
 | 
			
		||||
          // Show filter controls in grid mode
 | 
			
		||||
          const domainPhaseContainerGrid = document.querySelector('.domain-phase-container') as HTMLElement;
 | 
			
		||||
          const searchInputGrid = document.getElementById('search-input') as HTMLElement;
 | 
			
		||||
          const tagCloudGrid = document.querySelector('.tag-cloud') as HTMLElement;
 | 
			
		||||
          const checkboxWrappersGrid = document.querySelectorAll('.checkbox-wrapper');
 | 
			
		||||
          const tagHeaderGrid = document.querySelector('.tag-header') as HTMLElement;
 | 
			
		||||
          const filterLabelsGrid = document.querySelectorAll('label, .tag-header, h4, h3');
 | 
			
		||||
          const allInputsGrid = filtersSection!.querySelectorAll('input, select, textarea');
 | 
			
		||||
          
 | 
			
		||||
          if (domainPhaseContainerGrid) domainPhaseContainerGrid.style.display = 'grid';
 | 
			
		||||
          if (searchInputGrid) searchInputGrid.style.display = 'block';
 | 
			
		||||
          if (tagCloudGrid) tagCloudGrid.style.display = 'flex';
 | 
			
		||||
          if (tagHeaderGrid) tagHeaderGrid.style.display = 'flex';
 | 
			
		||||
          
 | 
			
		||||
          // Restore ALL inputs in the filters section
 | 
			
		||||
          allInputsGrid.forEach(input => {
 | 
			
		||||
            (input as HTMLElement).style.display = 'block';
 | 
			
		||||
          });
 | 
			
		||||
          
 | 
			
		||||
          checkboxWrappersGrid.forEach(wrapper => {
 | 
			
		||||
            (wrapper as HTMLElement).style.display = 'flex';
 | 
			
		||||
          });
 | 
			
		||||
          
 | 
			
		||||
          // Restore filter section elements
 | 
			
		||||
          filterLabelsGrid.forEach(element => {
 | 
			
		||||
            const text = element.textContent?.toLowerCase() || '';
 | 
			
		||||
            if (text.includes('proprietäre') || text.includes('tags filtern') || text.includes('nach tags') || text.includes('suchen') || text.includes('search')) {
 | 
			
		||||
              (element as HTMLElement).style.display = 'block';
 | 
			
		||||
            }
 | 
			
		||||
          });
 | 
			
		||||
          toolsGrid.style.display = 'block';
 | 
			
		||||
          filtersSection.style.display = 'block';
 | 
			
		||||
          showFilterControls();
 | 
			
		||||
          break;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
@ -321,15 +225,205 @@ const tools = data.tools;
 | 
			
		||||
        window.history.replaceState({}, '', window.location.pathname);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    // Helper functions for filter control visibility
 | 
			
		||||
    function hideFilterControls() {
 | 
			
		||||
      const elements = [
 | 
			
		||||
        '.domain-phase-container',
 | 
			
		||||
        '#search-input',
 | 
			
		||||
        '.tag-cloud',
 | 
			
		||||
        '.tag-header',
 | 
			
		||||
        '.checkbox-wrapper'
 | 
			
		||||
      ];
 | 
			
		||||
      
 | 
			
		||||
      elements.forEach(selector => {
 | 
			
		||||
        const element = document.querySelector(selector) as HTMLElement;
 | 
			
		||||
        if (element) element.style.display = 'none';
 | 
			
		||||
      });
 | 
			
		||||
      
 | 
			
		||||
      const allInputs = filtersSection.querySelectorAll('input, select, textarea');
 | 
			
		||||
      allInputs.forEach(input => (input as HTMLElement).style.display = 'none');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function showFilterControls() {
 | 
			
		||||
      const domainPhaseContainer = document.querySelector('.domain-phase-container') as HTMLElement;
 | 
			
		||||
      const searchInput = document.getElementById('search-input') as HTMLElement;
 | 
			
		||||
      const tagCloud = document.querySelector('.tag-cloud') as HTMLElement;
 | 
			
		||||
      const tagHeader = document.querySelector('.tag-header') as HTMLElement;
 | 
			
		||||
      const checkboxWrappers = document.querySelectorAll('.checkbox-wrapper');
 | 
			
		||||
      const allInputs = filtersSection.querySelectorAll('input, select, textarea');
 | 
			
		||||
      
 | 
			
		||||
      if (domainPhaseContainer) domainPhaseContainer.style.display = 'grid';
 | 
			
		||||
      if (searchInput) searchInput.style.display = 'block';
 | 
			
		||||
      if (tagCloud) tagCloud.style.display = 'flex';
 | 
			
		||||
      if (tagHeader) tagHeader.style.display = 'flex';
 | 
			
		||||
      
 | 
			
		||||
      allInputs.forEach(input => (input as HTMLElement).style.display = 'block');
 | 
			
		||||
      checkboxWrappers.forEach(wrapper => (wrapper as HTMLElement).style.display = 'flex');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Create tool slug from name
 | 
			
		||||
    function createToolSlug(toolName: string): string {
 | 
			
		||||
      return toolName.toLowerCase()
 | 
			
		||||
        .replace(/[^a-z0-9\s-]/g, '')
 | 
			
		||||
        .replace(/\s+/g, '-')
 | 
			
		||||
        .replace(/-+/g, '-')
 | 
			
		||||
        .replace(/^-|-$/g, '');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Find tool by name or slug
 | 
			
		||||
    function findTool(identifier: string) {
 | 
			
		||||
      return window.toolsData.find(tool => 
 | 
			
		||||
        tool.name === identifier || 
 | 
			
		||||
        createToolSlug(tool.name) === identifier.toLowerCase()
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Navigation functions for sharing
 | 
			
		||||
    window.navigateToGrid = function(toolName: string) {
 | 
			
		||||
      console.log('Navigating to grid for tool:', toolName);
 | 
			
		||||
      
 | 
			
		||||
      // Switch to grid view first
 | 
			
		||||
      switchToView('grid');
 | 
			
		||||
      
 | 
			
		||||
      // Wait for view switch, then find and scroll to tool
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        // Clear any filters first
 | 
			
		||||
        if (window.clearAllFilters) {
 | 
			
		||||
          window.clearAllFilters();
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // Wait for filters to clear and re-render
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
          const toolCards = document.querySelectorAll('.tool-card');
 | 
			
		||||
          let targetCard: Element | null = null;
 | 
			
		||||
          
 | 
			
		||||
          toolCards.forEach(card => {
 | 
			
		||||
            const cardTitle = card.querySelector('h3');
 | 
			
		||||
            if (cardTitle) {
 | 
			
		||||
              // Clean title text (remove icons and extra spaces)
 | 
			
		||||
              const titleText = cardTitle.textContent?.replace(/[^\w\s\-\.]/g, '').trim();
 | 
			
		||||
              if (titleText === toolName) {
 | 
			
		||||
                targetCard = card;
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          });
 | 
			
		||||
          
 | 
			
		||||
          if (targetCard) {
 | 
			
		||||
            console.log('Found target card, scrolling...');
 | 
			
		||||
            // Cast to Element to fix TypeScript issue
 | 
			
		||||
            (targetCard as Element).scrollIntoView({ behavior: 'smooth', block: 'center' });
 | 
			
		||||
            (targetCard as HTMLElement).style.animation = 'highlight-flash 2s ease-out';
 | 
			
		||||
            
 | 
			
		||||
            setTimeout(() => {
 | 
			
		||||
              if (targetCard) {
 | 
			
		||||
                (targetCard as HTMLElement).style.animation = '';
 | 
			
		||||
              }
 | 
			
		||||
            }, 2000);
 | 
			
		||||
          } else {
 | 
			
		||||
            console.warn('Tool card not found in grid:', toolName);
 | 
			
		||||
          }
 | 
			
		||||
        }, 300);
 | 
			
		||||
      }, 200);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    window.navigateToMatrix = function(toolName: string) {
 | 
			
		||||
      console.log('Navigating to matrix for tool:', toolName);
 | 
			
		||||
      
 | 
			
		||||
      // Switch to matrix view
 | 
			
		||||
      switchToView('matrix');
 | 
			
		||||
      
 | 
			
		||||
      // Wait for view switch and matrix to render
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        const toolChips = document.querySelectorAll('.tool-chip');
 | 
			
		||||
        let firstMatch: Element | null = null;
 | 
			
		||||
        let matchCount = 0;
 | 
			
		||||
        
 | 
			
		||||
        toolChips.forEach(chip => {
 | 
			
		||||
          // Clean the chip text (remove emoji and extra spaces)
 | 
			
		||||
          const chipText = chip.textContent?.replace(/📖/g, '').replace(/[^\w\s\-\.]/g, '').trim();
 | 
			
		||||
          if (chipText === toolName) {
 | 
			
		||||
            // Highlight this occurrence
 | 
			
		||||
            (chip as HTMLElement).style.animation = 'highlight-flash 2s ease-out';
 | 
			
		||||
            matchCount++;
 | 
			
		||||
            
 | 
			
		||||
            // Remember the first match for scrolling
 | 
			
		||||
            if (!firstMatch) {
 | 
			
		||||
              firstMatch = chip;
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            // Clean up animation after it completes
 | 
			
		||||
            setTimeout(() => {
 | 
			
		||||
              (chip as HTMLElement).style.animation = '';
 | 
			
		||||
            }, 8000);
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
        
 | 
			
		||||
        if (firstMatch) {
 | 
			
		||||
          console.log(`Found ${matchCount} occurrences of tool, highlighting all and scrolling to first`);
 | 
			
		||||
          // Cast to Element to fix TypeScript issue
 | 
			
		||||
          (firstMatch as Element).scrollIntoView({ behavior: 'smooth', block: 'center' });
 | 
			
		||||
        } else {
 | 
			
		||||
          console.warn('Tool chip not found in matrix:', toolName);
 | 
			
		||||
        }
 | 
			
		||||
      }, 500);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // Handle URL parameters on page load
 | 
			
		||||
    function handleSharedURL() {
 | 
			
		||||
      const urlParams = new URLSearchParams(window.location.search);
 | 
			
		||||
      const toolParam = urlParams.get('tool');
 | 
			
		||||
      const viewParam = urlParams.get('view');
 | 
			
		||||
      const modalParam = urlParams.get('modal');
 | 
			
		||||
      
 | 
			
		||||
      if (!toolParam) {
 | 
			
		||||
        // Check for AI view parameter
 | 
			
		||||
        if (viewParam === 'ai') {
 | 
			
		||||
          switchToView('ai');
 | 
			
		||||
        }
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      // Find the tool by name or slug
 | 
			
		||||
      const tool = findTool(toolParam);
 | 
			
		||||
      if (!tool) {
 | 
			
		||||
        console.warn('Shared tool not found:', toolParam);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      // Clear URL parameters to avoid re-triggering
 | 
			
		||||
      const cleanUrl = window.location.protocol + "//" + window.location.host + window.location.pathname;
 | 
			
		||||
      window.history.replaceState({}, document.title, cleanUrl);
 | 
			
		||||
      
 | 
			
		||||
      // Handle different view types
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        switch (viewParam) {
 | 
			
		||||
          case 'grid':
 | 
			
		||||
            window.navigateToGrid(tool.name);
 | 
			
		||||
            break;
 | 
			
		||||
          case 'matrix':
 | 
			
		||||
            window.navigateToMatrix(tool.name);
 | 
			
		||||
            break;
 | 
			
		||||
          case 'modal':
 | 
			
		||||
            if (modalParam === 'secondary') {
 | 
			
		||||
              window.showToolDetails(tool.name, 'secondary');
 | 
			
		||||
            } else {
 | 
			
		||||
              window.showToolDetails(tool.name, 'primary');
 | 
			
		||||
            }
 | 
			
		||||
            break;
 | 
			
		||||
          default:
 | 
			
		||||
            window.navigateToGrid(tool.name);
 | 
			
		||||
        }
 | 
			
		||||
      }, 100);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Handle filtered results
 | 
			
		||||
    window.addEventListener('toolsFiltered', (event: Event) => {
 | 
			
		||||
    window.addEventListener('toolsFiltered', (event) => {
 | 
			
		||||
      const customEvent = event as CustomEvent;
 | 
			
		||||
      const filtered = customEvent.detail;
 | 
			
		||||
      const currentView = document.querySelector('.view-toggle.active')?.getAttribute('data-view');
 | 
			
		||||
      
 | 
			
		||||
      if (currentView === 'matrix' || currentView === 'ai') {
 | 
			
		||||
        // Matrix and AI views handle their own rendering
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
@ -341,11 +435,8 @@ const tools = data.tools;
 | 
			
		||||
      } else {
 | 
			
		||||
        noResults.style.display = 'none';
 | 
			
		||||
        
 | 
			
		||||
        // Apply sorting here - single place for all sorting logic
 | 
			
		||||
        const currentSortOption = 'default'; // Will be dynamic later
 | 
			
		||||
        const sortedTools = sortTools(filtered, currentSortOption);
 | 
			
		||||
        const sortedTools = sortTools(filtered, 'default');
 | 
			
		||||
        
 | 
			
		||||
        // Render sorted tools
 | 
			
		||||
        sortedTools.forEach((tool: any) => {
 | 
			
		||||
          const toolCard = createToolCard(tool);
 | 
			
		||||
          toolsContainer.appendChild(toolCard);
 | 
			
		||||
@ -354,112 +445,133 @@ const tools = data.tools;
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    // Handle view changes
 | 
			
		||||
    window.addEventListener('viewChanged', (event: Event) => {
 | 
			
		||||
    window.addEventListener('viewChanged', (event) => {
 | 
			
		||||
      const customEvent = event as CustomEvent;
 | 
			
		||||
      const view = customEvent.detail;
 | 
			
		||||
      switchToView(view);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Make switchToView available globally for the AI button
 | 
			
		||||
    (window as any).switchToAIView = () => switchToView('ai');
 | 
			
		||||
    // Make switchToView available globally
 | 
			
		||||
    window.switchToAIView = () => switchToView('ai');
 | 
			
		||||
 | 
			
		||||
function createToolCard(tool) {
 | 
			
		||||
  const isMethod = tool.type === 'method';
 | 
			
		||||
  const isConcept = tool.type === 'concept';
 | 
			
		||||
  const hasValidProjectUrl = tool.projectUrl !== undefined && 
 | 
			
		||||
                            tool.projectUrl !== null && 
 | 
			
		||||
                            tool.projectUrl !== "" && 
 | 
			
		||||
                            tool.projectUrl.trim() !== "";
 | 
			
		||||
  
 | 
			
		||||
  const hasKnowledgebase = tool.knowledgebase === true;
 | 
			
		||||
  
 | 
			
		||||
  const cardDiv = document.createElement('div');
 | 
			
		||||
  const cardClass = isConcept ? 'card card-concept tool-card' :
 | 
			
		||||
                    isMethod ? 'card card-method tool-card' : 
 | 
			
		||||
                    hasValidProjectUrl ? 'card card-hosted tool-card' : 
 | 
			
		||||
                    (tool.license !== 'Proprietary' ? 'card card-oss tool-card' : 'card tool-card');
 | 
			
		||||
  cardDiv.className = cardClass;
 | 
			
		||||
  cardDiv.style.cursor = 'pointer';
 | 
			
		||||
  cardDiv.onclick = () => (window as any).showToolDetails(tool.name);
 | 
			
		||||
  
 | 
			
		||||
    // Tool card creation function
 | 
			
		||||
    function createToolCard(tool: any): HTMLElement {
 | 
			
		||||
      const isMethod = tool.type === 'method';
 | 
			
		||||
      const isConcept = tool.type === 'concept';
 | 
			
		||||
      const hasValidProjectUrl = tool.projectUrl !== undefined && 
 | 
			
		||||
                                tool.projectUrl !== null && 
 | 
			
		||||
                                tool.projectUrl !== "" && 
 | 
			
		||||
                                tool.projectUrl.trim() !== "";
 | 
			
		||||
      
 | 
			
		||||
      const hasKnowledgebase = tool.knowledgebase === true;
 | 
			
		||||
      
 | 
			
		||||
      const cardDiv = document.createElement('div');
 | 
			
		||||
      const cardClass = isConcept ? 'card card-concept tool-card' :
 | 
			
		||||
                        isMethod ? 'card card-method tool-card' : 
 | 
			
		||||
                        hasValidProjectUrl ? 'card card-hosted tool-card' : 
 | 
			
		||||
                        (tool.license !== 'Proprietary' ? 'card card-oss tool-card' : 'card tool-card');
 | 
			
		||||
      cardDiv.className = cardClass;
 | 
			
		||||
      cardDiv.style.cursor = 'pointer';
 | 
			
		||||
      cardDiv.onclick = () => window.showToolDetails(tool.name);
 | 
			
		||||
      
 | 
			
		||||
      // Create tool slug for share button
 | 
			
		||||
      const toolSlug = createToolSlug(tool.name);
 | 
			
		||||
 | 
			
		||||
  cardDiv.innerHTML = `
 | 
			
		||||
    <div class="tool-card-header">
 | 
			
		||||
      <h3>${tool.icon ? `<span style="margin-right: 0.5rem; font-size: 1.125rem;">${tool.icon}</span>` : ''}${tool.name}</h3>
 | 
			
		||||
      <div class="tool-card-badges">
 | 
			
		||||
        ${!isMethod && !isConcept && hasValidProjectUrl ? '<span class="badge badge-primary">CC24-Server</span>' : ''}
 | 
			
		||||
        ${hasKnowledgebase ? '<span class="badge badge-error">📖</span>' : ''}
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    
 | 
			
		||||
    <p class="text-muted">
 | 
			
		||||
      ${tool.description}
 | 
			
		||||
    </p>
 | 
			
		||||
    
 | 
			
		||||
    <div class="tool-card-metadata" style="display: flex; align-items: center; gap: 1rem; margin-bottom: 0.75rem; line-height: 1;">
 | 
			
		||||
      <div class="metadata-item" style="display: flex; align-items: center; gap: 0.5rem; font-size: 0.75rem; color: var(--color-text-secondary); flex-shrink: 1; min-width: 0;">
 | 
			
		||||
        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="flex-shrink: 0;">
 | 
			
		||||
          <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
 | 
			
		||||
          <line x1="9" y1="9" x2="15" y2="9"></line>
 | 
			
		||||
          <line x1="9" y1="15" x2="15" y2="15"></line>
 | 
			
		||||
        </svg>
 | 
			
		||||
        <span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;">
 | 
			
		||||
          ${(tool.platforms || []).slice(0, 2).join(', ')}${tool.platforms && tool.platforms.length > 2 ? '...' : ''}
 | 
			
		||||
        </span>
 | 
			
		||||
      </div>
 | 
			
		||||
      
 | 
			
		||||
      <div class="metadata-item" style="display: flex; align-items: center; gap: 0.5rem; font-size: 0.75rem; color: var(--color-text-secondary); flex-shrink: 1; min-width: 0;">
 | 
			
		||||
        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="flex-shrink: 0;">
 | 
			
		||||
          <circle cx="12" cy="12" r="10"></circle>
 | 
			
		||||
          <path d="M12 6v6l4 2"></path>
 | 
			
		||||
        </svg>
 | 
			
		||||
        <span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;">
 | 
			
		||||
          ${tool.skillLevel}
 | 
			
		||||
        </span>
 | 
			
		||||
      </div>
 | 
			
		||||
      
 | 
			
		||||
      <div class="metadata-item" style="display: flex; align-items: center; gap: 0.5rem; font-size: 0.75rem; color: var(--color-text-secondary); flex-shrink: 1; min-width: 0;">
 | 
			
		||||
        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="flex-shrink: 0;">
 | 
			
		||||
          <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
 | 
			
		||||
          <polyline points="14 2 14 8 20 8"></polyline>
 | 
			
		||||
        </svg>
 | 
			
		||||
        <span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;">
 | 
			
		||||
          ${isConcept ? 'Konzept' : isMethod ? 'Methode' : tool.license === 'Proprietary' ? 'Prop.' : tool.license?.split(' ')[0] || 'N/A'}
 | 
			
		||||
        </span>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    
 | 
			
		||||
    <div class="tool-tags-container">
 | 
			
		||||
      ${(tool.tags || []).slice(0, 8).map(tag => `<span class="tag">${tag}</span>`).join('')}
 | 
			
		||||
    </div>
 | 
			
		||||
    
 | 
			
		||||
    <div class="tool-card-buttons" onclick="event.stopPropagation();">
 | 
			
		||||
      ${isConcept ? `
 | 
			
		||||
        <a href="${tool.url}" target="_blank" rel="noopener noreferrer" class="btn btn-primary single-button" style="background-color: var(--color-concept); border-color: var(--color-concept);">
 | 
			
		||||
          Mehr erfahren
 | 
			
		||||
        </a>
 | 
			
		||||
      ` : isMethod ? `
 | 
			
		||||
        <a href="${tool.projectUrl || tool.url}" target="_blank" rel="noopener noreferrer" class="btn btn-primary single-button" style="background-color: var(--color-method); border-color: var(--color-method);">
 | 
			
		||||
          Zur Methode
 | 
			
		||||
        </a>
 | 
			
		||||
      ` : hasValidProjectUrl ? `
 | 
			
		||||
        <div class="button-row">
 | 
			
		||||
          <a href="${tool.url}" target="_blank" rel="noopener noreferrer" class="btn btn-secondary">
 | 
			
		||||
            Homepage
 | 
			
		||||
          </a>
 | 
			
		||||
          <a href="${tool.projectUrl}" target="_blank" rel="noopener noreferrer" class="btn btn-primary">
 | 
			
		||||
            Zugreifen
 | 
			
		||||
          </a>
 | 
			
		||||
      cardDiv.innerHTML = `
 | 
			
		||||
        <div class="tool-card-header">
 | 
			
		||||
          <h3>${tool.icon ? `<span style="margin-right: 0.5rem; font-size: 1.125rem;">${tool.icon}</span>` : ''}${tool.name}</h3>
 | 
			
		||||
          <div class="tool-card-badges">
 | 
			
		||||
            ${!isMethod && !isConcept && hasValidProjectUrl ? '<span class="badge badge-primary">CC24-Server</span>' : ''}
 | 
			
		||||
            ${hasKnowledgebase ? '<span class="badge badge-error">📖</span>' : ''}
 | 
			
		||||
            <button class="share-btn share-btn--small" 
 | 
			
		||||
                    data-tool-name="${tool.name}" 
 | 
			
		||||
                    data-tool-slug="${toolSlug}" 
 | 
			
		||||
                    data-context="card"
 | 
			
		||||
                    onclick="event.stopPropagation(); window.showShareDialog(this)"
 | 
			
		||||
                    title="${tool.name} teilen"
 | 
			
		||||
                    aria-label="${tool.name} teilen">
 | 
			
		||||
              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
 | 
			
		||||
                <circle cx="18" cy="5" r="3"/>
 | 
			
		||||
                <circle cx="6" cy="12" r="3"/>
 | 
			
		||||
                <circle cx="18" cy="19" r="3"/>
 | 
			
		||||
                <line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/>
 | 
			
		||||
                <line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/>
 | 
			
		||||
              </svg>
 | 
			
		||||
            </button>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      ` : `
 | 
			
		||||
        <a href="${tool.url}" target="_blank" rel="noopener noreferrer" class="btn btn-primary single-button">
 | 
			
		||||
          Software-Homepage
 | 
			
		||||
        </a>
 | 
			
		||||
      `}
 | 
			
		||||
    </div>
 | 
			
		||||
  `;
 | 
			
		||||
  
 | 
			
		||||
  return cardDiv;
 | 
			
		||||
}
 | 
			
		||||
        
 | 
			
		||||
        <p class="text-muted">
 | 
			
		||||
          ${tool.description}
 | 
			
		||||
        </p>
 | 
			
		||||
        
 | 
			
		||||
        <div class="tool-card-metadata" style="display: flex; align-items: center; gap: 1rem; margin-bottom: 0.75rem; line-height: 1;">
 | 
			
		||||
          <div class="metadata-item" style="display: flex; align-items: center; gap: 0.5rem; font-size: 0.75rem; color: var(--color-text-secondary); flex-shrink: 1; min-width: 0;">
 | 
			
		||||
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="flex-shrink: 0;">
 | 
			
		||||
              <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
 | 
			
		||||
              <line x1="9" y1="9" x2="15" y2="9"></line>
 | 
			
		||||
              <line x1="9" y1="15" x2="15" y2="15"></line>
 | 
			
		||||
            </svg>
 | 
			
		||||
            <span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;">
 | 
			
		||||
              ${(tool.platforms || []).slice(0, 2).join(', ')}${tool.platforms && tool.platforms.length > 2 ? '...' : ''}
 | 
			
		||||
            </span>
 | 
			
		||||
          </div>
 | 
			
		||||
          
 | 
			
		||||
          <div class="metadata-item" style="display: flex; align-items: center; gap: 0.5rem; font-size: 0.75rem; color: var(--color-text-secondary); flex-shrink: 1; min-width: 0;">
 | 
			
		||||
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="flex-shrink: 0;">
 | 
			
		||||
              <circle cx="12" cy="12" r="10"></circle>
 | 
			
		||||
              <path d="M12 6v6l4 2"></path>
 | 
			
		||||
            </svg>
 | 
			
		||||
            <span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;">
 | 
			
		||||
              ${tool.skillLevel}
 | 
			
		||||
            </span>
 | 
			
		||||
          </div>
 | 
			
		||||
          
 | 
			
		||||
          <div class="metadata-item" style="display: flex; align-items: center; gap: 0.5rem; font-size: 0.75rem; color: var(--color-text-secondary); flex-shrink: 1; min-width: 0;">
 | 
			
		||||
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="flex-shrink: 0;">
 | 
			
		||||
              <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
 | 
			
		||||
              <polyline points="14 2 14 8 20 8"></polyline>
 | 
			
		||||
            </svg>
 | 
			
		||||
            <span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;">
 | 
			
		||||
              ${isConcept ? 'Konzept' : isMethod ? 'Methode' : tool.license === 'Proprietary' ? 'Prop.' : tool.license?.split(' ')[0] || 'N/A'}
 | 
			
		||||
            </span>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        
 | 
			
		||||
        <div class="tool-tags-container">
 | 
			
		||||
          ${(tool.tags || []).slice(0, 8).map((tag: string) => `<span class="tag">${tag}</span>`).join('')}
 | 
			
		||||
        </div>
 | 
			
		||||
        
 | 
			
		||||
        <div class="tool-card-buttons" onclick="event.stopPropagation();">
 | 
			
		||||
          ${isConcept ? `
 | 
			
		||||
            <a href="${tool.url}" target="_blank" rel="noopener noreferrer" class="btn btn-primary single-button" style="background-color: var(--color-concept); border-color: var(--color-concept);">
 | 
			
		||||
              Mehr erfahren
 | 
			
		||||
            </a>
 | 
			
		||||
          ` : isMethod ? `
 | 
			
		||||
            <a href="${tool.projectUrl || tool.url}" target="_blank" rel="noopener noreferrer" class="btn btn-primary single-button" style="background-color: var(--color-method); border-color: var(--color-method);">
 | 
			
		||||
              Zur Methode
 | 
			
		||||
            </a>
 | 
			
		||||
          ` : hasValidProjectUrl ? `
 | 
			
		||||
            <div class="button-row">
 | 
			
		||||
              <a href="${tool.url}" target="_blank" rel="noopener noreferrer" class="btn btn-secondary">
 | 
			
		||||
                Homepage
 | 
			
		||||
              </a>
 | 
			
		||||
              <a href="${tool.projectUrl}" target="_blank" rel="noopener noreferrer" class="btn btn-primary">
 | 
			
		||||
                Zugreifen
 | 
			
		||||
              </a>
 | 
			
		||||
            </div>
 | 
			
		||||
          ` : `
 | 
			
		||||
            <a href="${tool.url}" target="_blank" rel="noopener noreferrer" class="btn btn-primary single-button">
 | 
			
		||||
              Software-Homepage
 | 
			
		||||
            </a>
 | 
			
		||||
          `}
 | 
			
		||||
        </div>
 | 
			
		||||
      `;
 | 
			
		||||
      
 | 
			
		||||
      return cardDiv;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Initialize URL handling
 | 
			
		||||
    handleSharedURL();
 | 
			
		||||
  });
 | 
			
		||||
</script>
 | 
			
		||||
@ -240,6 +240,25 @@ nav {
 | 
			
		||||
  background-color: var(--color-bg-tertiary);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Icon Button */
 | 
			
		||||
.btn-icon {
 | 
			
		||||
  background: none;
 | 
			
		||||
  border: none;
 | 
			
		||||
  color: var(--color-text-secondary);
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  padding: 0.25rem;
 | 
			
		||||
  border-radius: 0.25rem;
 | 
			
		||||
  transition: var(--transition-fast);
 | 
			
		||||
  display: inline-flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.btn-icon:hover {
 | 
			
		||||
  color: var(--color-text);
 | 
			
		||||
  background-color: var(--color-bg-secondary);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.btn-accent {
 | 
			
		||||
  background-color: var(--color-accent);
 | 
			
		||||
  color: white;
 | 
			
		||||
@ -392,6 +411,8 @@ input[type="checkbox"] {
 | 
			
		||||
  line-height: 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.metadata-item {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
@ -647,7 +668,7 @@ input[type="checkbox"] {
 | 
			
		||||
  padding: 2rem;
 | 
			
		||||
  max-width: 600px;
 | 
			
		||||
  width: 90%;
 | 
			
		||||
  max-height: 100vh;
 | 
			
		||||
  max-height: 85vh; 
 | 
			
		||||
  overflow-y: auto;
 | 
			
		||||
  z-index: 1000;
 | 
			
		||||
  box-shadow: var(--shadow-lg);
 | 
			
		||||
@ -1197,6 +1218,13 @@ Collaboration Section Collapse */
 | 
			
		||||
  margin-bottom: 0.75rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Enhanced highlight flash for different contexts */
 | 
			
		||||
.tool-card.highlight-flash,
 | 
			
		||||
.tool-chip.highlight-flash,
 | 
			
		||||
.tool-recommendation.highlight-flash {
 | 
			
		||||
  animation: highlight-flash 2s ease-out;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.pros-cons-section {
 | 
			
		||||
  animation: fadeIn 0.4s ease-in;
 | 
			
		||||
}
 | 
			
		||||
@ -1309,11 +1337,76 @@ footer {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@keyframes highlight-flash {
 | 
			
		||||
  0% { background-color: rgb(37 99 235 / 10%); }
 | 
			
		||||
  100% { background-color: transparent; }
 | 
			
		||||
}
 | 
			
		||||
/*Perfect! Here's the absolutely brutal, eye-melting version:*/
 | 
			
		||||
 | 
			
		||||
@keyframes highlight-flash {
 | 
			
		||||
  0% { 
 | 
			
		||||
    background-color: rgb(57 255 20 / 60%);
 | 
			
		||||
    box-shadow: 0 0 0 8px rgb(255 20 147 / 50%), 0 0 20px rgb(57 255 20 / 80%);
 | 
			
		||||
    transform: scale(1.12) rotate(2deg);
 | 
			
		||||
    border: 3px solid rgb(255 255 0);
 | 
			
		||||
  }
 | 
			
		||||
  12.5% { 
 | 
			
		||||
    background-color: rgb(255 20 147 / 70%);
 | 
			
		||||
    box-shadow: 0 0 0 15px rgb(0 191 255 / 60%), 0 0 30px rgb(255 20 147 / 90%);
 | 
			
		||||
    transform: scale(1.18) rotate(-3deg);
 | 
			
		||||
    border: 3px solid rgb(57 255 20);
 | 
			
		||||
  }
 | 
			
		||||
  25% { 
 | 
			
		||||
    background-color: rgb(0 191 255 / 65%);
 | 
			
		||||
    box-shadow: 0 0 0 12px rgb(191 0 255 / 55%), 0 0 25px rgb(0 191 255 / 85%);
 | 
			
		||||
    transform: scale(1.15) rotate(1deg);
 | 
			
		||||
    border: 3px solid rgb(255 20 147);
 | 
			
		||||
  }
 | 
			
		||||
  37.5% { 
 | 
			
		||||
    background-color: rgb(191 0 255 / 75%);
 | 
			
		||||
    box-shadow: 0 0 0 18px rgb(255 255 0 / 65%), 0 0 35px rgb(191 0 255 / 95%);
 | 
			
		||||
    transform: scale(1.20) rotate(-2deg);
 | 
			
		||||
    border: 3px solid rgb(0 191 255);
 | 
			
		||||
  }
 | 
			
		||||
  50% { 
 | 
			
		||||
    background-color: rgb(255 255 0 / 80%);
 | 
			
		||||
    box-shadow: 0 0 0 10px rgb(57 255 20 / 70%), 0 0 40px rgb(255 255 0 / 100%);
 | 
			
		||||
    transform: scale(1.16) rotate(3deg);
 | 
			
		||||
    border: 3px solid rgb(191 0 255);
 | 
			
		||||
  }
 | 
			
		||||
  62.5% { 
 | 
			
		||||
    background-color: rgb(255 69 0 / 70%);
 | 
			
		||||
    box-shadow: 0 0 0 16px rgb(255 20 147 / 60%), 0 0 30px rgb(255 69 0 / 90%);
 | 
			
		||||
    transform: scale(1.22) rotate(-1deg);
 | 
			
		||||
    border: 3px solid rgb(255 255 0);
 | 
			
		||||
  }
 | 
			
		||||
  75% { 
 | 
			
		||||
    background-color: rgb(255 20 147 / 65%);
 | 
			
		||||
    box-shadow: 0 0 0 14px rgb(0 191 255 / 50%), 0 0 45px rgb(255 20 147 / 85%);
 | 
			
		||||
    transform: scale(1.14) rotate(2deg);
 | 
			
		||||
    border: 3px solid rgb(57 255 20);
 | 
			
		||||
  }
 | 
			
		||||
  87.5% { 
 | 
			
		||||
    background-color: rgb(57 255 20 / 75%);
 | 
			
		||||
    box-shadow: 0 0 0 20px rgb(191 0 255 / 65%), 0 0 35px rgb(57 255 20 / 95%);
 | 
			
		||||
    transform: scale(1.25) rotate(-3deg);
 | 
			
		||||
    border: 3px solid rgb(255 69 0);
 | 
			
		||||
  }
 | 
			
		||||
  100% { 
 | 
			
		||||
    background-color: transparent;
 | 
			
		||||
    box-shadow: none;
 | 
			
		||||
    transform: scale(1) rotate(0deg);
 | 
			
		||||
    border: none;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
/*
 | 
			
		||||
This monstrosity includes:
 | 
			
		||||
 | 
			
		||||
Neon rainbow cycling: Bright green → Hot pink → Electric blue → Neon purple → Nuclear yellow → Orange-red
 | 
			
		||||
Double shadows: Inner colored shadow + outer glow effect
 | 
			
		||||
Aggressive scaling: Up to 1.25x size
 | 
			
		||||
Rotation wobble: Cards wiggle back and forth
 | 
			
		||||
Strobing borders: Bright colored borders that change with each keyframe
 | 
			
		||||
8 keyframes: More frequent color/effect changes
 | 
			
		||||
Higher opacity: More saturated colors (up to 100% on yellow)
 | 
			
		||||
 | 
			
		||||
This will literally assault the user's retinas. They'll need sunglasses to look at their shared tools! 🌈💥👁️🗨️*/
 | 
			
		||||
@keyframes pulse {
 | 
			
		||||
  0%, 100% { opacity: 1; }
 | 
			
		||||
  50% { opacity: 0.5; }
 | 
			
		||||
@ -1404,6 +1497,12 @@ footer {
 | 
			
		||||
    width: 90vw;
 | 
			
		||||
    max-height: 35vh;
 | 
			
		||||
  }
 | 
			
		||||
  .tool-details {
 | 
			
		||||
    max-height: 80vh; 
 | 
			
		||||
    padding: 1.5rem; 
 | 
			
		||||
    width: 95%; 
 | 
			
		||||
    max-width: none;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media (width <= 640px) {
 | 
			
		||||
@ -1488,6 +1587,11 @@ footer {
 | 
			
		||||
    font-size: 0.75rem;
 | 
			
		||||
    padding: 0.25rem 0.375rem;
 | 
			
		||||
  }
 | 
			
		||||
  .tool-details {
 | 
			
		||||
    max-height: 75vh; 
 | 
			
		||||
    padding: 1rem; 
 | 
			
		||||
    border-radius: 0.25rem; 
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1577,4 +1681,35 @@ footer {
 | 
			
		||||
  border: none;
 | 
			
		||||
  border-top: 1px solid var(--color-border);
 | 
			
		||||
  margin: 2rem 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Share Button Styles */
 | 
			
		||||
.share-btn {
 | 
			
		||||
  background: none;
 | 
			
		||||
  border: none;
 | 
			
		||||
  color: var(--color-text-secondary);
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  padding: 0.25rem;
 | 
			
		||||
  border-radius: 0.25rem;
 | 
			
		||||
  transition: var(--transition-fast);
 | 
			
		||||
  display: inline-flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.share-btn:hover {
 | 
			
		||||
  color: var(--color-primary);
 | 
			
		||||
  background-color: var(--color-bg-secondary);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.share-btn--small {
 | 
			
		||||
  padding: 0.125rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.share-btn--medium {
 | 
			
		||||
  padding: 0.375rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.share-btn svg {
 | 
			
		||||
  flex-shrink: 0;
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user