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;
|
const { tool } = Astro.props;
|
||||||
|
|
||||||
|
|
||||||
// Check types
|
// Check types
|
||||||
const isMethod = tool.type === 'method';
|
const isMethod = tool.type === 'method';
|
||||||
const isConcept = tool.type === 'concept';
|
const isConcept = tool.type === 'concept';
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
---
|
---
|
||||||
import { getToolsData } from '../utils/dataService.js';
|
import { getToolsData } from '../utils/dataService.js';
|
||||||
|
import ShareButton from './ShareButton.astro';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Load tools data
|
// Load tools data
|
||||||
@ -146,6 +148,10 @@ domains.forEach((domain: any) => {
|
|||||||
<div class="tool-details" id="tool-details-primary">
|
<div class="tool-details" id="tool-details-primary">
|
||||||
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 1rem;">
|
<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>
|
<h2 id="tool-name-primary" style="margin: 0;">Tool Name</h2>
|
||||||
|
<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')">
|
<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">
|
<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="18" y1="6" x2="6" y2="18"></line>
|
||||||
@ -153,6 +159,7 @@ domains.forEach((domain: any) => {
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<p id="tool-description-primary" class="text-muted"></p>
|
<p id="tool-description-primary" class="text-muted"></p>
|
||||||
|
|
||||||
@ -169,6 +176,10 @@ domains.forEach((domain: any) => {
|
|||||||
<div class="tool-details" id="tool-details-secondary">
|
<div class="tool-details" id="tool-details-secondary">
|
||||||
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 1rem;">
|
<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>
|
<h2 id="tool-name-secondary" style="margin: 0;">Tool Name</h2>
|
||||||
|
<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')">
|
<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">
|
<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="18" y1="6" x2="6" y2="18"></line>
|
||||||
@ -176,6 +187,7 @@ domains.forEach((domain: any) => {
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<p id="tool-description-secondary" class="text-muted"></p>
|
<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
|
// Make functions globally available
|
||||||
window.toggleDomainAgnosticSection = toggleDomainAgnosticSection;
|
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>`;
|
return `<span class="tag" style="background-color: var(--color-bg-tertiary); color: var(--color-text-secondary); margin: 0.125rem;">${conceptName}</span>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
|
// Check if mobile device
|
||||||
|
const isMobile = window.innerWidth <= 768;
|
||||||
|
const collapseOnMobile = isMobile && relatedConcepts.length > 2;
|
||||||
|
|
||||||
tagsHTML += `
|
tagsHTML += `
|
||||||
<div style="margin-top: 1rem;">
|
<div style="margin-top: 1rem;">
|
||||||
<strong style="display: block; margin-bottom: 0.5rem; color: var(--color-text);">Verwandte Konzepte:</strong>
|
<div style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem;">
|
||||||
<div style="display: flex; flex-wrap: wrap; gap: 0.25rem;">
|
<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}
|
${conceptLinks}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -436,6 +656,30 @@ domains.forEach((domain: any) => {
|
|||||||
|
|
||||||
elements.links.innerHTML = linksHTML;
|
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
|
// Show modals and update layout
|
||||||
const overlay = document.getElementById('modal-overlay');
|
const overlay = document.getElementById('modal-overlay');
|
||||||
const primaryModal = document.getElementById('tool-details-primary');
|
const primaryModal = document.getElementById('tool-details-primary');
|
||||||
@ -460,14 +704,28 @@ domains.forEach((domain: any) => {
|
|||||||
const secondaryModal = document.getElementById('tool-details-secondary');
|
const secondaryModal = document.getElementById('tool-details-secondary');
|
||||||
|
|
||||||
if (modalType === 'both' || modalType === 'all') {
|
if (modalType === 'both' || modalType === 'all') {
|
||||||
if (primaryModal) primaryModal.classList.remove('active');
|
if (primaryModal) {
|
||||||
if (secondaryModal) secondaryModal.classList.remove('active');
|
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');
|
if (overlay) overlay.classList.remove('active');
|
||||||
document.body.classList.remove('modals-side-by-side');
|
document.body.classList.remove('modals-side-by-side');
|
||||||
} else if (modalType === 'primary' && primaryModal) {
|
} else if (modalType === 'primary' && primaryModal) {
|
||||||
primaryModal.classList.remove('active');
|
primaryModal.classList.remove('active');
|
||||||
|
const shareButtonPrimary = document.getElementById('share-button-primary');
|
||||||
|
if (shareButtonPrimary) shareButtonPrimary.style.display = 'none';
|
||||||
} else if (modalType === 'secondary' && secondaryModal) {
|
} else if (modalType === 'secondary' && secondaryModal) {
|
||||||
secondaryModal.classList.remove('active');
|
secondaryModal.classList.remove('active');
|
||||||
|
const shareButtonSecondary = document.getElementById('share-button-secondary');
|
||||||
|
if (shareButtonSecondary) shareButtonSecondary.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if any modal is still active
|
// 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>
|
</BaseLayout>
|
||||||
|
|
||||||
<script>
|
<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
|
// Handle view changes and filtering
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const toolsContainer = document.getElementById('tools-container');
|
const toolsContainer = document.getElementById('tools-container') as HTMLElement;
|
||||||
const toolsGrid = document.getElementById('tools-grid');
|
const toolsGrid = document.getElementById('tools-grid') as HTMLElement;
|
||||||
const matrixContainer = document.getElementById('matrix-container');
|
const matrixContainer = document.getElementById('matrix-container') as HTMLElement;
|
||||||
const aiInterface = document.getElementById('ai-interface');
|
const aiInterface = document.getElementById('ai-interface') as HTMLElement;
|
||||||
const filtersSection = document.getElementById('filters-section');
|
const filtersSection = document.getElementById('filters-section') as HTMLElement;
|
||||||
const noResults = document.getElementById('no-results');
|
const noResults = document.getElementById('no-results') as HTMLElement;
|
||||||
const aiQueryBtn = document.getElementById('ai-query-btn');
|
const aiQueryBtn = document.getElementById('ai-query-btn') as HTMLButtonElement;
|
||||||
|
|
||||||
// Guard against null elements
|
// Guard against null elements
|
||||||
if (!toolsContainer || !toolsGrid || !matrixContainer || !noResults || !aiInterface || !filtersSection) {
|
if (!toolsContainer || !toolsGrid || !matrixContainer || !noResults || !aiInterface || !filtersSection) {
|
||||||
@ -109,12 +125,9 @@ const tools = data.tools;
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initial tools HTML
|
// Simple sorting function
|
||||||
const initialToolsHTML = toolsContainer.innerHTML;
|
function sortTools(tools: any[], sortBy = 'default') {
|
||||||
|
const sorted = [...tools];
|
||||||
// Simple sorting function - no external imports needed
|
|
||||||
function sortTools(tools, sortBy = 'default') {
|
|
||||||
const sorted = [...tools]; // Don't mutate original array
|
|
||||||
|
|
||||||
switch (sortBy) {
|
switch (sortBy) {
|
||||||
case 'alphabetical':
|
case 'alphabetical':
|
||||||
@ -122,16 +135,16 @@ const tools = data.tools;
|
|||||||
case 'difficulty':
|
case 'difficulty':
|
||||||
const difficultyOrder = { 'novice': 0, 'beginner': 1, 'intermediate': 2, 'advanced': 3, 'expert': 4 };
|
const difficultyOrder = { 'novice': 0, 'beginner': 1, 'intermediate': 2, 'advanced': 3, 'expert': 4 };
|
||||||
return sorted.sort((a, b) =>
|
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':
|
case 'type':
|
||||||
const typeOrder = { 'concept': 0, 'method': 1, 'software': 2 };
|
const typeOrder = { 'concept': 0, 'method': 1, 'software': 2 };
|
||||||
return sorted.sort((a, b) =>
|
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':
|
case 'default':
|
||||||
default:
|
default:
|
||||||
return sorted; // No sorting - embrace the entropy
|
return sorted;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,31 +172,21 @@ const tools = data.tools;
|
|||||||
const authStatus = await checkAuthentication();
|
const authStatus = await checkAuthentication();
|
||||||
|
|
||||||
if (authStatus.authRequired && !authStatus.authenticated) {
|
if (authStatus.authRequired && !authStatus.authenticated) {
|
||||||
// Redirect to login, then back to AI view
|
|
||||||
const returnUrl = `${window.location.pathname}?view=ai`;
|
const returnUrl = `${window.location.pathname}?view=ai`;
|
||||||
window.location.href = `/api/auth/login?returnTo=${encodeURIComponent(returnUrl)}`;
|
window.location.href = `/api/auth/login?returnTo=${encodeURIComponent(returnUrl)}`;
|
||||||
} else {
|
} else {
|
||||||
// Switch to AI view directly
|
|
||||||
switchToView('ai');
|
switchToView('ai');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check URL parameters on page load for view switching
|
// Function to switch between different views
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
function switchToView(view: string) {
|
||||||
const viewParam = urlParams.get('view');
|
// Hide all views first
|
||||||
if (viewParam === 'ai') {
|
toolsGrid.style.display = 'none';
|
||||||
// User was redirected after authentication, switch to AI view
|
matrixContainer.style.display = 'none';
|
||||||
switchToView('ai');
|
aiInterface.style.display = 'none';
|
||||||
}
|
filtersSection.style.display = 'none';
|
||||||
|
|
||||||
// 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';
|
|
||||||
|
|
||||||
// Update view toggle buttons
|
// Update view toggle buttons
|
||||||
const viewToggles = document.querySelectorAll('.view-toggle');
|
const viewToggles = document.querySelectorAll('.view-toggle');
|
||||||
@ -191,128 +194,29 @@ const tools = data.tools;
|
|||||||
btn.classList.toggle('active', btn.getAttribute('data-view') === view);
|
btn.classList.toggle('active', btn.getAttribute('data-view') === view);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Show appropriate view
|
// Show appropriate view and manage filter visibility
|
||||||
switch (view) {
|
switch (view) {
|
||||||
case 'ai':
|
case 'ai':
|
||||||
aiInterface!.style.display = 'block';
|
aiInterface.style.display = 'block';
|
||||||
// Keep filters visible in AI mode for view switching
|
filtersSection.style.display = 'block';
|
||||||
filtersSection!.style.display = 'block';
|
hideFilterControls();
|
||||||
|
if (window.restoreAIResults) {
|
||||||
// Hide filter controls in AI mode - AGGRESSIVE APPROACH
|
window.restoreAIResults();
|
||||||
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';
|
|
||||||
}
|
}
|
||||||
});
|
const aiInput = document.getElementById('ai-query-input') as HTMLTextAreaElement;
|
||||||
|
|
||||||
// Restore previous AI results if they exist
|
|
||||||
if ((window as any).restoreAIResults) {
|
|
||||||
(window as any).restoreAIResults();
|
|
||||||
}
|
|
||||||
// Focus on the input
|
|
||||||
const aiInput = document.getElementById('ai-query-input');
|
|
||||||
if (aiInput) {
|
if (aiInput) {
|
||||||
setTimeout(() => aiInput.focus(), 100);
|
setTimeout(() => aiInput.focus(), 100);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'matrix':
|
case 'matrix':
|
||||||
matrixContainer!.style.display = 'block';
|
matrixContainer.style.display = 'block';
|
||||||
filtersSection!.style.display = 'block';
|
filtersSection.style.display = 'block';
|
||||||
|
showFilterControls();
|
||||||
// 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';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
default: // grid
|
default: // grid
|
||||||
toolsGrid!.style.display = 'block';
|
toolsGrid.style.display = 'block';
|
||||||
filtersSection!.style.display = 'block';
|
filtersSection.style.display = 'block';
|
||||||
|
showFilterControls();
|
||||||
// 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';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -322,14 +226,204 @@ const tools = data.tools;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
// Handle filtered results
|
||||||
window.addEventListener('toolsFiltered', (event: Event) => {
|
window.addEventListener('toolsFiltered', (event) => {
|
||||||
const customEvent = event as CustomEvent;
|
const customEvent = event as CustomEvent;
|
||||||
const filtered = customEvent.detail;
|
const filtered = customEvent.detail;
|
||||||
const currentView = document.querySelector('.view-toggle.active')?.getAttribute('data-view');
|
const currentView = document.querySelector('.view-toggle.active')?.getAttribute('data-view');
|
||||||
|
|
||||||
if (currentView === 'matrix' || currentView === 'ai') {
|
if (currentView === 'matrix' || currentView === 'ai') {
|
||||||
// Matrix and AI views handle their own rendering
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -341,11 +435,8 @@ const tools = data.tools;
|
|||||||
} else {
|
} else {
|
||||||
noResults.style.display = 'none';
|
noResults.style.display = 'none';
|
||||||
|
|
||||||
// Apply sorting here - single place for all sorting logic
|
const sortedTools = sortTools(filtered, 'default');
|
||||||
const currentSortOption = 'default'; // Will be dynamic later
|
|
||||||
const sortedTools = sortTools(filtered, currentSortOption);
|
|
||||||
|
|
||||||
// Render sorted tools
|
|
||||||
sortedTools.forEach((tool: any) => {
|
sortedTools.forEach((tool: any) => {
|
||||||
const toolCard = createToolCard(tool);
|
const toolCard = createToolCard(tool);
|
||||||
toolsContainer.appendChild(toolCard);
|
toolsContainer.appendChild(toolCard);
|
||||||
@ -354,16 +445,17 @@ const tools = data.tools;
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Handle view changes
|
// Handle view changes
|
||||||
window.addEventListener('viewChanged', (event: Event) => {
|
window.addEventListener('viewChanged', (event) => {
|
||||||
const customEvent = event as CustomEvent;
|
const customEvent = event as CustomEvent;
|
||||||
const view = customEvent.detail;
|
const view = customEvent.detail;
|
||||||
switchToView(view);
|
switchToView(view);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Make switchToView available globally for the AI button
|
// Make switchToView available globally
|
||||||
(window as any).switchToAIView = () => switchToView('ai');
|
window.switchToAIView = () => switchToView('ai');
|
||||||
|
|
||||||
function createToolCard(tool) {
|
// Tool card creation function
|
||||||
|
function createToolCard(tool: any): HTMLElement {
|
||||||
const isMethod = tool.type === 'method';
|
const isMethod = tool.type === 'method';
|
||||||
const isConcept = tool.type === 'concept';
|
const isConcept = tool.type === 'concept';
|
||||||
const hasValidProjectUrl = tool.projectUrl !== undefined &&
|
const hasValidProjectUrl = tool.projectUrl !== undefined &&
|
||||||
@ -380,8 +472,10 @@ function createToolCard(tool) {
|
|||||||
(tool.license !== 'Proprietary' ? 'card card-oss tool-card' : 'card tool-card');
|
(tool.license !== 'Proprietary' ? 'card card-oss tool-card' : 'card tool-card');
|
||||||
cardDiv.className = cardClass;
|
cardDiv.className = cardClass;
|
||||||
cardDiv.style.cursor = 'pointer';
|
cardDiv.style.cursor = 'pointer';
|
||||||
cardDiv.onclick = () => (window as any).showToolDetails(tool.name);
|
cardDiv.onclick = () => window.showToolDetails(tool.name);
|
||||||
|
|
||||||
|
// Create tool slug for share button
|
||||||
|
const toolSlug = createToolSlug(tool.name);
|
||||||
|
|
||||||
cardDiv.innerHTML = `
|
cardDiv.innerHTML = `
|
||||||
<div class="tool-card-header">
|
<div class="tool-card-header">
|
||||||
@ -389,6 +483,21 @@ function createToolCard(tool) {
|
|||||||
<div class="tool-card-badges">
|
<div class="tool-card-badges">
|
||||||
${!isMethod && !isConcept && hasValidProjectUrl ? '<span class="badge badge-primary">CC24-Server</span>' : ''}
|
${!isMethod && !isConcept && hasValidProjectUrl ? '<span class="badge badge-primary">CC24-Server</span>' : ''}
|
||||||
${hasKnowledgebase ? '<span class="badge badge-error">📖</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -430,7 +539,7 @@ function createToolCard(tool) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tool-tags-container">
|
<div class="tool-tags-container">
|
||||||
${(tool.tags || []).slice(0, 8).map(tag => `<span class="tag">${tag}</span>`).join('')}
|
${(tool.tags || []).slice(0, 8).map((tag: string) => `<span class="tag">${tag}</span>`).join('')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tool-card-buttons" onclick="event.stopPropagation();">
|
<div class="tool-card-buttons" onclick="event.stopPropagation();">
|
||||||
@ -460,6 +569,9 @@ function createToolCard(tool) {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
return cardDiv;
|
return cardDiv;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize URL handling
|
||||||
|
handleSharedURL();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
@ -240,6 +240,25 @@ nav {
|
|||||||
background-color: var(--color-bg-tertiary);
|
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 {
|
.btn-accent {
|
||||||
background-color: var(--color-accent);
|
background-color: var(--color-accent);
|
||||||
color: white;
|
color: white;
|
||||||
@ -392,6 +411,8 @@ input[type="checkbox"] {
|
|||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.metadata-item {
|
.metadata-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -647,7 +668,7 @@ input[type="checkbox"] {
|
|||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
width: 90%;
|
width: 90%;
|
||||||
max-height: 100vh;
|
max-height: 85vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
box-shadow: var(--shadow-lg);
|
box-shadow: var(--shadow-lg);
|
||||||
@ -1197,6 +1218,13 @@ Collaboration Section Collapse */
|
|||||||
margin-bottom: 0.75rem;
|
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 {
|
.pros-cons-section {
|
||||||
animation: fadeIn 0.4s ease-in;
|
animation: fadeIn 0.4s ease-in;
|
||||||
}
|
}
|
||||||
@ -1309,11 +1337,76 @@ footer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes highlight-flash {
|
/*Perfect! Here's the absolutely brutal, eye-melting version:*/
|
||||||
0% { background-color: rgb(37 99 235 / 10%); }
|
|
||||||
100% { background-color: transparent; }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
@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 {
|
@keyframes pulse {
|
||||||
0%, 100% { opacity: 1; }
|
0%, 100% { opacity: 1; }
|
||||||
50% { opacity: 0.5; }
|
50% { opacity: 0.5; }
|
||||||
@ -1404,6 +1497,12 @@ footer {
|
|||||||
width: 90vw;
|
width: 90vw;
|
||||||
max-height: 35vh;
|
max-height: 35vh;
|
||||||
}
|
}
|
||||||
|
.tool-details {
|
||||||
|
max-height: 80vh;
|
||||||
|
padding: 1.5rem;
|
||||||
|
width: 95%;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (width <= 640px) {
|
@media (width <= 640px) {
|
||||||
@ -1488,6 +1587,11 @@ footer {
|
|||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
padding: 0.25rem 0.375rem;
|
padding: 0.25rem 0.375rem;
|
||||||
}
|
}
|
||||||
|
.tool-details {
|
||||||
|
max-height: 75vh;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -1578,3 +1682,34 @@ footer {
|
|||||||
border-top: 1px solid var(--color-border);
|
border-top: 1px solid var(--color-border);
|
||||||
margin: 2rem 0;
|
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