buggy state

This commit is contained in:
overcuriousity 2025-08-11 14:37:01 +02:00
parent 6918df9348
commit 6d2e345db1
9 changed files with 1174 additions and 400 deletions

View File

@ -215,7 +215,6 @@ const domainAgnosticSoftware = data['domain-agnostic-software'] || [];
</section> </section>
<script type="module" define:vars={{ tools, phases, domainAgnosticSoftware }}> <script type="module" define:vars={{ tools, phases, domainAgnosticSoftware }}>
const Utils = { const Utils = {
phaseConfig: { phaseConfig: {
'initialization': { icon: '🚀', displayName: 'Initialisierung' }, 'initialization': { icon: '🚀', displayName: 'Initialisierung' },
@ -255,73 +254,6 @@ const Utils = {
if (confidence >= 60) return 'var(--color-warning)'; if (confidence >= 60) return 'var(--color-warning)';
return 'var(--color-error)'; return 'var(--color-error)';
}, },
escapeHtml(text) {
if (typeof text !== 'string') return String(text);
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
},
truncateText(text, maxLength) {
if (!text || text.length <= maxLength) return text;
return text.slice(0, maxLength) + '...';
},
sanitizeText(text) {
if (typeof text !== 'string') return '';
return text
.replace(/^#{1,6}\s+/gm, '')
.replace(/^\s*[-*+]\s+/gm, '')
.replace(/^\s*\d+\.\s+/gm, '')
.replace(/\*\*(.+?)\*\*/g, '$1')
.replace(/\*(.+?)\*/g, '$1')
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
.replace(/```[\s\S]*?```/g, '[CODE BLOCK]')
.replace(/`([^`]+)`/g, '$1')
.replace(/<[^>]+>/g, '')
.replace(/\n\s*\n\s*\n/g, '\n\n')
.trim();
},
summarizeData(data) {
if (data === null || data === undefined) return 'null';
if (typeof data === 'string') {
return data.length > 100 ? data.slice(0, 100) + '...' : data;
}
if (typeof data === 'number' || typeof data === 'boolean') {
return data.toString();
}
if (Array.isArray(data)) {
if (data.length === 0) return '[]';
if (data.length <= 3) return JSON.stringify(data);
return `[${data.slice(0, 3).map(i => typeof i === 'string' ? i : JSON.stringify(i)).join(', ')}, ...+${data.length - 3}]`;
}
if (typeof data === 'object') {
const keys = Object.keys(data);
if (keys.length === 0) return '{}';
if (keys.length <= 3) {
return '{' + keys.map(k => `${k}: ${typeof data[k] === 'string' ? data[k].slice(0, 20) + (data[k].length > 20 ? '...' : '') : JSON.stringify(data[k])}`).join(', ') + '}';
}
return `{${keys.slice(0, 3).join(', ')}, ...+${keys.length - 3} keys}`;
}
return String(data);
},
showElement(element) {
if (element) {
element.style.display = 'block';
element.classList.remove('hidden');
}
},
hideElement(element) {
if (element) {
element.style.display = 'none';
element.classList.add('hidden');
}
}
}; };
class AuditTrailProcessor { class AuditTrailProcessor {

View File

@ -432,13 +432,6 @@ const sortedTags = Object.entries(tagFrequency)
} }
} }
function isToolHosted(tool) {
return tool.projectUrl !== undefined &&
tool.projectUrl !== null &&
tool.projectUrl !== "" &&
tool.projectUrl.trim() !== "";
}
function initTagCloud() { function initTagCloud() {
const visibleCount = 20; const visibleCount = 20;
elements.tagCloudItems.forEach((item, index) => { elements.tagCloudItems.forEach((item, index) => {

View File

@ -267,44 +267,6 @@ domains.forEach((domain: any) => {
} }
} }
function generateShareURL(toolName, view, modal = null) {
const toolSlug = window.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()}`;
}
async function copyToClipboard(text, button) {
try {
await navigator.clipboard.writeText(text);
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) {
const textArea = document.createElement('textarea');
textArea.value = text;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
const originalHTML = button.innerHTML;
button.innerHTML = 'Kopiert!';
setTimeout(() => {
button.innerHTML = originalHTML;
}, 2000);
}
}
function showShareDialog(shareButton) { function showShareDialog(shareButton) {
const toolName = shareButton.getAttribute('data-tool-name'); const toolName = shareButton.getAttribute('data-tool-name');
const context = shareButton.getAttribute('data-context'); const context = shareButton.getAttribute('data-context');

View File

@ -26,180 +26,155 @@ const { title, description = 'ForensicPathways - A comprehensive directory of di
<script> <script>
async function loadUtilityFunctions() { async function loadUtilityFunctions() {
try { // Define utility functions directly instead of importing
const { createToolSlug, findToolByIdentifier, isToolHosted } = await import('../utils/clientUtils.js'); (window as any).createToolSlug = (toolName: string) => {
if (!toolName || typeof toolName !== 'string') return '';
return toolName.toLowerCase().replace(/[^a-z0-9\s-]/g, '').replace(/\s+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
};
(window as any).createToolSlug = createToolSlug; (window as any).findToolByIdentifier = (tools: any[], identifier: string) => {
(window as any).findToolByIdentifier = findToolByIdentifier; if (!identifier || !Array.isArray(tools)) return undefined;
(window as any).isToolHosted = isToolHosted; return tools.find((tool: any) =>
tool.name === identifier ||
(window as any).createToolSlug(tool.name) === identifier.toLowerCase()
);
};
console.log('[UTILS] Utility functions loaded successfully'); (window as any).isToolHosted = (tool: any) => {
} catch (error) { return tool.projectUrl !== undefined &&
console.error('Failed to load utility functions:', error); tool.projectUrl !== null &&
tool.projectUrl !== "" &&
tool.projectUrl.trim() !== "";
};
// Provide fallback implementations // Scroll utilities
(window as any).createToolSlug = (toolName: string) => { (window as any).scrollToElement = (element: Element | null, options: ScrollIntoViewOptions = {}) => {
if (!toolName || typeof toolName !== 'string') return ''; if (!element) return;
return toolName.toLowerCase().replace(/[^a-z0-9\s-]/g, '').replace(/\s+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, ''); setTimeout(() => {
}; const headerHeight = document.querySelector('nav')?.offsetHeight || 80;
const elementRect = element.getBoundingClientRect();
const absoluteElementTop = elementRect.top + window.pageYOffset;
const targetPosition = absoluteElementTop - headerHeight - 20;
window.scrollTo({
top: targetPosition,
behavior: 'smooth',
...options
});
}, 100);
};
(window as any).findToolByIdentifier = (tools: any[], identifier: string) => { (window as any).scrollToElementById = (elementId: string, options: ScrollIntoViewOptions = {}) => {
if (!identifier || !Array.isArray(tools)) return undefined; const element = document.getElementById(elementId);
return tools.find((tool: any) => if (element) (window as any).scrollToElement(element, options);
tool.name === identifier || };
(window as any).createToolSlug(tool.name) === identifier.toLowerCase()
);
};
(window as any).isToolHosted = (tool: any) => { (window as any).scrollToElementBySelector = (selector: string, options: ScrollIntoViewOptions = {}) => {
return tool.projectUrl !== undefined && const element = document.querySelector(selector);
tool.projectUrl !== null && if (element) (window as any).scrollToElement(element, options);
tool.projectUrl !== "" && };
tool.projectUrl.trim() !== "";
};
console.log('[UTILS] Fallback utility functions registered'); // Search utilities
} (window as any).prioritizeSearchResults = (tools: any[], searchTerm: string) => {
} if (!searchTerm || !searchTerm.trim()) return tools;
const lowerSearchTerm = searchTerm.toLowerCase().trim();
function scrollToElement(element: Element | null, options = {}) { return tools.sort((a: any, b: any) => {
if (!element) return; const aTagsLower = (a.tags || []).map((tag: string) => tag.toLowerCase());
const bTagsLower = (b.tags || []).map((tag: string) => tag.toLowerCase());
setTimeout(() => { const aExactTag = aTagsLower.includes(lowerSearchTerm);
const headerHeight = document.querySelector('nav')?.offsetHeight || 80; const bExactTag = bTagsLower.includes(lowerSearchTerm);
const elementRect = element.getBoundingClientRect(); if (aExactTag && !bExactTag) return -1;
const absoluteElementTop = elementRect.top + window.pageYOffset; if (!aExactTag && bExactTag) return 1;
const targetPosition = absoluteElementTop - headerHeight - 20; return a.name.localeCompare(b.name);
window.scrollTo({
top: targetPosition,
behavior: 'smooth'
}); });
}, 100); };
// Share utilities
(window as any).shareArticle = async (button: HTMLElement, url: string, title: string) => {
const fullUrl = window.location.origin + url;
await (window as any).copyToClipboard(fullUrl, button);
};
(window as any).shareCurrentArticle = async (button: HTMLElement) => {
await (window as any).copyToClipboard(window.location.href, button);
};
// Clipboard utility
(window as any).copyToClipboard = async (text: string, button: HTMLElement) => {
try {
await navigator.clipboard.writeText(text);
const originalHTML = button.innerHTML;
button.innerHTML = 'Kopiert!';
button.style.color = 'var(--color-accent)';
setTimeout(() => {
button.innerHTML = originalHTML;
button.style.color = '';
}, 2000);
} catch (err) {
const textArea = document.createElement('textarea');
textArea.value = text;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
const originalHTML = button.innerHTML;
button.innerHTML = 'Kopiert!';
setTimeout(() => { button.innerHTML = originalHTML; }, 2000);
}
};
// Theme utilities
(window as any).themeUtils = (() => {
const THEME_KEY = 'dfir-theme';
function getSystemTheme(): string {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
function getStoredTheme(): string {
return localStorage.getItem(THEME_KEY) || 'auto';
}
function applyTheme(theme: string): void {
const effectiveTheme = theme === 'auto' ? getSystemTheme() : theme;
document.documentElement.setAttribute('data-theme', effectiveTheme);
}
function initTheme(): void {
const storedTheme = getStoredTheme();
applyTheme(storedTheme);
}
return { initTheme, getStoredTheme };
})();
console.log('[UTILS] Essential utility functions loaded directly');
} }
function scrollToElementById(elementId: string, options = {}) {
const element = document.getElementById(elementId);
if (element) {
scrollToElement(element, options);
}
}
function scrollToElementBySelector(selector: string, options = {}) {
const element = document.querySelector(selector);
if (element) {
scrollToElement(element, options);
}
}
function prioritizeSearchResults(tools: any[], searchTerm: string) {
if (!searchTerm || !searchTerm.trim()) {
return tools;
}
const lowerSearchTerm = searchTerm.toLowerCase().trim();
return tools.sort((a, b) => {
const aTagsLower = (a.tags || []).map((tag: string) => tag.toLowerCase());
const bTagsLower = (b.tags || []).map((tag: string) => tag.toLowerCase());
const aExactTag = aTagsLower.includes(lowerSearchTerm);
const bExactTag = bTagsLower.includes(lowerSearchTerm);
if (aExactTag && !bExactTag) return -1;
if (!aExactTag && bExactTag) return 1;
return a.name.localeCompare(b.name);
});
}
(window as any).scrollToElement = scrollToElement;
(window as any).scrollToElementById = scrollToElementById;
(window as any).scrollToElementBySelector = scrollToElementBySelector;
(window as any).prioritizeSearchResults = prioritizeSearchResults;
document.addEventListener('DOMContentLoaded', async () => { document.addEventListener('DOMContentLoaded', async () => {
// CRITICAL: Load utility functions FIRST before any URL handling // CRITICAL: Load utility functions FIRST before any URL handling
await loadUtilityFunctions(); await loadUtilityFunctions();
const THEME_KEY = 'dfir-theme'; // Modal delegation functions - using async import for DOM helpers
(window as any).showToolDetails = function(toolName: string, modalType: string = 'primary') {
let attempts = 0;
const maxAttempts = 50;
function getSystemTheme() { const tryDelegate = () => {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; const matrixShowToolDetails = (window as any).matrixShowToolDetails;
}
function getStoredTheme() { if (matrixShowToolDetails && typeof matrixShowToolDetails === 'function') {
return localStorage.getItem(THEME_KEY) || 'auto'; return matrixShowToolDetails(toolName, modalType);
} }
function applyTheme(theme: string) { const directShowToolDetails = (window as any).directShowToolDetails;
const effectiveTheme = theme === 'auto' ? getSystemTheme() : theme; if (directShowToolDetails && typeof directShowToolDetails === 'function') {
document.documentElement.setAttribute('data-theme', effectiveTheme); return directShowToolDetails(toolName, modalType);
} }
function updateThemeToggle(theme: string) { attempts++;
document.querySelectorAll('[data-theme-toggle]').forEach(button => { if (attempts < maxAttempts) {
button.setAttribute('data-current-theme', theme); setTimeout(tryDelegate, 100);
}); }
} };
function initTheme() { tryDelegate();
const storedTheme = getStoredTheme();
applyTheme(storedTheme);
updateThemeToggle(storedTheme);
}
function toggleTheme() {
const current = getStoredTheme();
const themes = ['light', 'dark', 'auto'];
const currentIndex = themes.indexOf(current);
const nextIndex = (currentIndex + 1) % themes.length;
const nextTheme = themes[nextIndex];
localStorage.setItem(THEME_KEY, nextTheme);
applyTheme(nextTheme);
updateThemeToggle(nextTheme);
}
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
if (getStoredTheme() === 'auto') {
applyTheme('auto');
}
});
(window as any).themeUtils = {
initTheme,
toggleTheme,
getStoredTheme
}; };
(window as any).showToolDetails = function(toolName: string, modalType: string = 'primary') {
let attempts = 0;
const maxAttempts = 50;
const tryDelegate = () => {
const matrixShowToolDetails = (window as any).matrixShowToolDetails;
if (matrixShowToolDetails && typeof matrixShowToolDetails === 'function') {
return matrixShowToolDetails(toolName, modalType);
}
const directShowToolDetails = (window as any).directShowToolDetails;
if (directShowToolDetails && typeof directShowToolDetails === 'function') {
return directShowToolDetails(toolName, modalType);
}
attempts++;
if (attempts < maxAttempts) {
setTimeout(tryDelegate, 100);
} else {
}
};
tryDelegate();
};
(window as any).hideToolDetails = function(modalType: string = 'both') { (window as any).hideToolDetails = function(modalType: string = 'both') {
const matrixHideToolDetails = (window as any).matrixHideToolDetails; const matrixHideToolDetails = (window as any).matrixHideToolDetails;
if (matrixHideToolDetails && typeof matrixHideToolDetails === 'function') { if (matrixHideToolDetails && typeof matrixHideToolDetails === 'function') {
@ -211,6 +186,7 @@ const { title, description = 'ForensicPathways - A comprehensive directory of di
(window as any).hideToolDetails('both'); (window as any).hideToolDetails('both');
}; };
// Auth functions
async function checkClientAuth(context = 'general') { async function checkClientAuth(context = 'general') {
try { try {
const response = await fetch('/api/auth/status'); const response = await fetch('/api/auth/status');
@ -294,52 +270,10 @@ const { title, description = 'ForensicPathways - A comprehensive directory of di
(window as any).showIfAuthenticated = showIfAuthenticated; (window as any).showIfAuthenticated = showIfAuthenticated;
(window as any).setupAuthButtons = setupAuthButtons; (window as any).setupAuthButtons = setupAuthButtons;
async function copyUrlToClipboard(url: string, button: HTMLElement) { // Theme and auth initialization
try { if ((window as any).themeUtils) {
await navigator.clipboard.writeText(url); (window as any).themeUtils.initTheme();
const originalHTML = button.innerHTML;
button.innerHTML = `
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.375rem;">
<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) {
const textArea = document.createElement('textarea');
textArea.value = url;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
const originalHTML = button.innerHTML;
button.innerHTML = 'Kopiert!';
setTimeout(() => {
button.innerHTML = originalHTML;
}, 2000);
}
} }
async function shareArticle(button: HTMLElement, url: string, title: string) {
const fullUrl = window.location.origin + url;
await copyUrlToClipboard(fullUrl, button);
}
async function shareCurrentArticle(button: HTMLElement) {
await copyUrlToClipboard(window.location.href, button);
}
(window as any).shareArticle = shareArticle;
(window as any).shareCurrentArticle = shareCurrentArticle;
initTheme();
setupAuthButtons('[data-contribute-button]'); setupAuthButtons('[data-contribute-button]');
const initAIButton = async () => { const initAIButton = async () => {

View File

@ -1,36 +1,18 @@
// src/utils/clientUtils.ts // src/utils/clientUtils.ts - Simplified by using consolidated utilities
// Client-side utilities that mirror server-side toolHelpers.ts // This file now only contains AutocompleteManager functionality that wasn't moved to domHelpers
export function createToolSlug(toolName: string): string { import {
if (!toolName || typeof toolName !== 'string') { createToolSlug,
console.warn('[toolHelpers] Invalid toolName provided to createToolSlug:', toolName); findToolByIdentifier,
return ''; isToolHosted,
} escapeHtml,
Tool
} from './uiHelpers.js';
return toolName.toLowerCase() // Re-export the consolidated utilities for backward compatibility
.replace(/[^a-z0-9\s-]/g, '') // Remove special characters export { createToolSlug, findToolByIdentifier, isToolHosted };
.replace(/\s+/g, '-') // Replace spaces with hyphens
.replace(/-+/g, '-') // Remove duplicate hyphens
.replace(/^-|-$/g, ''); // Remove leading/trailing hyphens
}
export function findToolByIdentifier(tools: any[], identifier: string): any | undefined { // AutocompleteManager - keeping this here as it's a complex component-specific utility
if (!identifier || !Array.isArray(tools)) return undefined;
return tools.find((tool: any) =>
tool.name === identifier ||
createToolSlug(tool.name) === identifier.toLowerCase()
);
}
export function isToolHosted(tool: any): boolean {
return tool.projectUrl !== undefined &&
tool.projectUrl !== null &&
tool.projectUrl !== "" &&
tool.projectUrl.trim() !== "";
}
// Consolidated Autocomplete Functionality
interface AutocompleteOptions { interface AutocompleteOptions {
minLength?: number; minLength?: number;
maxResults?: number; maxResults?: number;
@ -205,7 +187,7 @@ export class AutocompleteManager {
defaultRender(item: any): string { defaultRender(item: any): string {
const text = typeof item === 'string' ? item : item.name || item.label || item.toString(); const text = typeof item === 'string' ? item : item.name || item.label || item.toString();
return `<div class="autocomplete-item">${this.escapeHtml(text)}</div>`; return `<div class="autocomplete-item">${escapeHtml(text)}</div>`;
} }
renderDropdown(): void { renderDropdown(): void {
@ -289,8 +271,8 @@ export class AutocompleteManager {
align-items: center; align-items: center;
gap: 0.25rem; gap: 0.25rem;
"> ">
${this.escapeHtml(item)} ${escapeHtml(item)}
<button type="button" class="autocomplete-remove" data-item="${this.escapeHtml(item)}" style=" <button type="button" class="autocomplete-remove" data-item="${escapeHtml(item)}" style="
background: none; background: none;
border: none; border: none;
color: white; color: white;
@ -333,12 +315,6 @@ export class AutocompleteManager {
this.selectedIndex = -1; this.selectedIndex = -1;
} }
escapeHtml(text: string): string {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
setDataSource(newDataSource: any[]): void { setDataSource(newDataSource: any[]): void {
this.dataSource = newDataSource; this.dataSource = newDataSource;
} }

337
src/utils/domHelpers.ts Normal file
View File

@ -0,0 +1,337 @@
// src/utils/domHelpers.ts - Consolidated DOM manipulation utilities
// ELEMENT VISIBILITY UTILITIES (consolidates AIQueryInterface.astro patterns)
export function showElement(element: HTMLElement | null): void {
if (element) {
element.style.display = 'block';
element.classList.remove('hidden');
}
}
export function hideElement(element: HTMLElement | null): void {
if (element) {
element.style.display = 'none';
element.classList.add('hidden');
}
}
export function toggleElement(element: HTMLElement | null, show?: boolean): void {
if (!element) return;
const shouldShow = show !== undefined ? show : element.classList.contains('hidden');
if (shouldShow) {
showElement(element);
} else {
hideElement(element);
}
}
// MODAL MANAGEMENT UTILITIES (consolidates ToolMatrix.astro modal logic)
export interface ModalElements {
overlay: HTMLElement | null;
primaryModal: HTMLElement | null;
secondaryModal: HTMLElement | null;
}
export function getModalElements(): ModalElements {
return {
overlay: document.getElementById('modal-overlay'),
primaryModal: document.getElementById('tool-details-primary'),
secondaryModal: document.getElementById('tool-details-secondary')
};
}
export function showModal(modalType: 'primary' | 'secondary' = 'primary'): void {
const { overlay, primaryModal, secondaryModal } = getModalElements();
if (overlay) overlay.classList.add('active');
if (modalType === 'primary' && primaryModal) primaryModal.classList.add('active');
if (modalType === 'secondary' && secondaryModal) secondaryModal.classList.add('active');
// Handle side-by-side modals
const primaryActive = primaryModal && primaryModal.classList.contains('active');
const secondaryActive = secondaryModal && secondaryModal.classList.contains('active');
if (primaryActive && secondaryActive) {
document.body.classList.add('modals-side-by-side');
}
}
export function hideModal(modalType: 'primary' | 'secondary' | 'both' = 'both'): void {
if ((window as any).modalHideInProgress) return;
(window as any).modalHideInProgress = true;
setTimeout(() => {
(window as any).modalHideInProgress = false;
}, 100);
const { overlay, primaryModal, secondaryModal } = getModalElements();
if (modalType === 'both' || modalType === 'primary') {
if (primaryModal) {
primaryModal.classList.remove('active');
hideModalButtons('primary');
}
}
if (modalType === 'both' || modalType === 'secondary') {
if (secondaryModal) {
secondaryModal.classList.remove('active');
hideModalButtons('secondary');
}
}
// Update overlay and body classes
const primaryActive = primaryModal && primaryModal.classList.contains('active');
const secondaryActive = secondaryModal && secondaryModal.classList.contains('active');
if (!primaryActive && !secondaryActive) {
if (overlay) overlay.classList.remove('active');
document.body.classList.remove('modals-side-by-side');
} else if (primaryActive && secondaryActive) {
document.body.classList.add('modals-side-by-side');
} else {
document.body.classList.remove('modals-side-by-side');
}
}
function hideModalButtons(modalType: 'primary' | 'secondary'): void {
const shareButton = document.getElementById(`share-button-${modalType}`);
const contributeButton = document.getElementById(`contribute-button-${modalType}`);
if (shareButton) shareButton.style.display = 'none';
if (contributeButton) contributeButton.style.display = 'none';
}
// COLLAPSIBLE SECTION UTILITIES (consolidates ToolFilters.astro + ToolMatrix.astro patterns)
export function toggleCollapsible(
toggleBtn: HTMLElement,
content: HTMLElement,
storageKey?: string
): void {
const isCollapsed = toggleBtn.getAttribute('data-collapsed') === 'true';
const newState = !isCollapsed;
toggleBtn.setAttribute('data-collapsed', newState.toString());
if (newState) {
content.classList.add('hidden');
toggleBtn.style.transform = 'rotate(0deg)';
} else {
content.classList.remove('hidden');
toggleBtn.style.transform = 'rotate(180deg)';
}
if (storageKey) {
sessionStorage.setItem(storageKey, newState.toString());
}
}
export function initializeCollapsible(
toggleBtn: HTMLElement,
content: HTMLElement,
storageKey: string
): void {
const collapsed = sessionStorage.getItem(storageKey) !== 'false';
toggleBtn.setAttribute('data-collapsed', collapsed.toString());
if (collapsed) {
content.classList.add('hidden');
toggleBtn.style.transform = 'rotate(0deg)';
} else {
content.classList.remove('hidden');
toggleBtn.style.transform = 'rotate(180deg)';
}
}
// FORM UTILITIES (consolidates form handling patterns)
export function updateElementContent(
elementId: string,
content: string,
contentType: 'text' | 'html' = 'text'
): void {
const element = document.getElementById(elementId);
if (element) {
if (contentType === 'html') {
element.innerHTML = content;
} else {
element.textContent = content;
}
}
}
export function updateElementAttribute(
elementId: string,
attribute: string,
value: string
): void {
const element = document.getElementById(elementId);
if (element) {
element.setAttribute(attribute, value);
}
}
// EVENT DELEGATION UTILITIES (consolidates event handling patterns)
export function delegateClick(
containerSelector: string,
targetSelector: string,
handler: (target: Element, event: Event) => void
): void {
document.addEventListener('click', (e) => {
const container = document.querySelector(containerSelector);
if (!container || !e.target) return;
const target = (e.target as Element).closest(targetSelector);
if (target && container.contains(target)) {
handler(target, e);
}
});
}
// LOADING STATE UTILITIES (consolidates AIQueryInterface.astro loading patterns)
export interface LoadingElements {
button: HTMLElement | null;
buttonText: HTMLElement | null;
loading: HTMLElement | null;
error: HTMLElement | null;
results: HTMLElement | null;
}
export function setLoadingState(
elements: LoadingElements,
isLoading: boolean,
loadingText?: string
): void {
if (elements.button) {
(elements.button as HTMLButtonElement).disabled = isLoading;
}
if (elements.buttonText && loadingText) {
elements.buttonText.textContent = isLoading ? loadingText : elements.buttonText.getAttribute('data-original-text') || '';
}
if (elements.loading) {
toggleElement(elements.loading, isLoading);
}
if (elements.error) {
hideElement(elements.error);
}
if (elements.results && isLoading) {
hideElement(elements.results);
}
}
// ANIMATION UTILITIES
export function addHighlightAnimation(element: HTMLElement, duration: number = 2000): void {
element.style.animation = `highlight-flash ${duration}ms ease-out`;
setTimeout(() => {
element.style.animation = '';
}, duration);
}
// VIEW SWITCHING UTILITIES (consolidates index.astro view switching)
export function switchView(
view: string,
viewElements: Record<string, HTMLElement | null>
): void {
// Hide all view elements
Object.values(viewElements).forEach(element => {
if (element) {
element.style.display = 'none';
element.classList.add('hidden');
}
});
// Show target view
const targetElement = viewElements[view];
if (targetElement) {
targetElement.style.display = 'block';
targetElement.classList.remove('hidden');
}
// Update view toggle buttons
document.querySelectorAll('.view-toggle').forEach(btn => {
btn.classList.toggle('active', btn.getAttribute('data-view') === view);
});
}
// THEME UTILITIES (consolidates BaseLayout.astro theme functions)
export interface ThemeUtils {
initTheme: () => void;
toggleTheme: () => void;
getStoredTheme: () => string;
}
export function createThemeUtils(): ThemeUtils {
const THEME_KEY = 'dfir-theme';
function getSystemTheme(): string {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
function getStoredTheme(): string {
return localStorage.getItem(THEME_KEY) || 'auto';
}
function applyTheme(theme: string): void {
const effectiveTheme = theme === 'auto' ? getSystemTheme() : theme;
document.documentElement.setAttribute('data-theme', effectiveTheme);
}
function updateThemeToggle(theme: string): void {
document.querySelectorAll('[data-theme-toggle]').forEach(button => {
button.setAttribute('data-current-theme', theme);
});
}
function initTheme(): void {
const storedTheme = getStoredTheme();
applyTheme(storedTheme);
updateThemeToggle(storedTheme);
}
function toggleTheme(): void {
const current = getStoredTheme();
const themes = ['light', 'dark', 'auto'];
const currentIndex = themes.indexOf(current);
const nextIndex = (currentIndex + 1) % themes.length;
const nextTheme = themes[nextIndex];
localStorage.setItem(THEME_KEY, nextTheme);
applyTheme(nextTheme);
updateThemeToggle(nextTheme);
}
// Listen for system theme changes
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
if (getStoredTheme() === 'auto') {
applyTheme('auto');
}
});
return { initTheme, toggleTheme, getStoredTheme };
}
// AUTOCOMPLETE UTILITIES (consolidates clientUtils.ts AutocompleteManager functionality)
export interface AutocompleteOptions {
minLength?: number;
maxResults?: number;
placeholder?: string;
allowMultiple?: boolean;
separator?: string;
filterFunction?: (query: string) => any[];
renderFunction?: (item: any) => string;
hiddenInput?: HTMLInputElement;
}
export function createAutocomplete(
inputElement: HTMLInputElement,
dataSource: any[],
options: AutocompleteOptions = {}
): void {
// This is a simplified version - the full AutocompleteManager from clientUtils.ts
// can be refactored to use these DOM utilities
console.log('[DOM Helpers] Autocomplete setup for:', inputElement.id);
}

View File

@ -1,43 +1,9 @@
export interface Tool { // src/utils/toolHelpers.ts - Simplified to re-export consolidated utilities
name: string; // This file now just re-exports the consolidated utilities for backward compatibility
type?: 'software' | 'method' | 'concept';
projectUrl?: string | null;
license?: string;
knowledgebase?: boolean;
domains?: string[];
phases?: string[];
platforms?: string[];
skillLevel?: string;
description?: string;
tags?: string[];
related_concepts?: string[];
}
export function createToolSlug(toolName: string): string { export {
if (!toolName || typeof toolName !== 'string') { Tool,
console.warn('[toolHelpers] Invalid toolName provided to createToolSlug:', toolName); createToolSlug,
return ''; findToolByIdentifier,
} isToolHosted
} from './uiHelpers.js';
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
}
export function findToolByIdentifier(tools: Tool[], identifier: string): Tool | undefined {
if (!identifier || !Array.isArray(tools)) return undefined;
return tools.find(tool =>
tool.name === identifier ||
createToolSlug(tool.name) === identifier.toLowerCase()
);
}
export function isToolHosted(tool: Tool): boolean {
return tool.projectUrl !== undefined &&
tool.projectUrl !== null &&
tool.projectUrl !== "" &&
tool.projectUrl.trim() !== "";
}

411
src/utils/toolRendering.ts Normal file
View File

@ -0,0 +1,411 @@
// src/utils/toolRendering.ts - Consolidated tool rendering utilities
import { Tool, escapeHtml, sanitizeText, truncateText, getConfidenceColor } from './uiHelpers.js';
// TOOL CLASSIFICATION UTILITIES (consolidates repeated logic across components)
export interface ToolClassification {
isMethod: boolean;
isConcept: boolean;
isHosted: boolean;
isOpenSource: boolean;
hasKnowledgebase: boolean;
}
export function classifyTool(tool: Tool): ToolClassification {
const isMethod = tool.type === 'method';
const isConcept = tool.type === 'concept';
const isHosted = tool.projectUrl !== undefined &&
tool.projectUrl !== null &&
tool.projectUrl !== "" &&
tool.projectUrl.trim() !== "";
const isOpenSource = tool.license !== 'Proprietary';
const hasKnowledgebase = tool.knowledgebase === true;
return { isMethod, isConcept, isHosted, isOpenSource, hasKnowledgebase };
}
export function getToolCardClass(tool: Tool): string {
const { isMethod, isConcept, isHosted, isOpenSource } = classifyTool(tool);
if (isConcept) return 'card card-concept tool-card cursor-pointer';
if (isMethod) return 'card card-method tool-card cursor-pointer';
if (isHosted) return 'card card-hosted tool-card cursor-pointer';
if (isOpenSource) return 'card card-oss tool-card cursor-pointer';
return 'card tool-card cursor-pointer';
}
export function getToolChipClass(tool: Tool): string {
const { isMethod, isHosted, isOpenSource } = classifyTool(tool);
if (isMethod) return 'tool-chip tool-chip-method';
if (isHosted) return 'tool-chip tool-chip-hosted';
if (isOpenSource) return 'tool-chip tool-chip-oss';
return 'tool-chip';
}
export function getRecommendationClass(tool: Tool): string {
const { isMethod, isHosted, isOpenSource } = classifyTool(tool);
if (isMethod) return 'method';
if (isHosted) return 'hosted';
if (isOpenSource) return 'oss';
return '';
}
// BADGE RENDERING UTILITIES (consolidates badge logic across components)
export function renderToolBadges(tool: Tool): string {
const { isMethod, isConcept, isHosted, hasKnowledgebase } = classifyTool(tool);
let badges = '';
if (isConcept) {
badges += '<span class="badge" style="background-color: var(--color-concept); color: white;">Konzept</span>';
} else if (isMethod) {
badges += '<span class="badge" style="background-color: var(--color-method); color: white;">Methode</span>';
} else if (isHosted) {
badges += '<span class="badge badge-primary">CC24-Server</span>';
}
if (hasKnowledgebase) {
badges += '<span class="badge badge-error">📖</span>';
}
return badges;
}
export function renderInlineBadges(tool: Tool): string {
const { isMethod, isHosted, hasKnowledgebase } = classifyTool(tool);
let badges = '';
if (isMethod) {
badges += '<span class="badge" style="background-color: var(--color-method); color: white;">Methode</span>';
} else if (isHosted) {
badges += '<span class="badge badge-primary">CC24-Server</span>';
}
if (hasKnowledgebase) {
badges += '<span class="badge badge-error">📖</span>';
}
return badges;
}
// METADATA RENDERING UTILITIES (consolidates metadata display logic)
export function renderToolMetadata(tool: Tool, compact: boolean = false): string {
const { isMethod, isConcept } = classifyTool(tool);
const domains = tool.domains || [];
const phases = tool.phases || [];
const domainsText = domains.length > 0 ? domains.join(', ') : 'Domain-agnostic';
const phasesText = phases.join(', ');
if (compact) {
return `
<div class="flex gap-3 text-xs text-secondary">
<span>${isMethod ? 'Methode' : isConcept ? 'Konzept' : tool.platforms?.slice(0, 2).join(', ')}</span>
<span></span>
<span>${tool.skillLevel}</span>
</div>
`;
}
let metadataHTML = `<div class="grid gap-2">`;
if (!isConcept) {
metadataHTML += `
<div><strong>Betriebssystem:</strong> ${(tool.platforms || []).join(', ')}</div>
<div><strong>Skill Level:</strong> ${tool.skillLevel}</div>
<div><strong>Lizenzmodell:</strong> ${tool.license}</div>
<div><strong>Deployment:</strong> ${tool.accessType}</div>
`;
} else {
metadataHTML += `<div><strong>Skill Level:</strong> ${tool.skillLevel}</div>`;
}
metadataHTML += `
<div><strong>Einsatzgebiete:</strong> ${domainsText}</div>
<div><strong>Ermittlungsphasen:</strong> ${phasesText}</div>
</div>`;
return metadataHTML;
}
// BUTTON RENDERING UTILITIES (consolidates button logic across components)
export function renderToolButtons(tool: Tool, stopPropagation: boolean = true): string {
const { isMethod, isConcept, isHosted } = classifyTool(tool);
const onClickAttr = stopPropagation ? 'onclick="event.stopPropagation();"' : '';
if (isConcept) {
return `
<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);"
${onClickAttr}>
Mehr erfahren
</a>
`;
}
if (isMethod) {
return `
<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);"
${onClickAttr}>
Zur Methode
</a>
`;
}
if (isHosted) {
return `
<div class="button-row" ${onClickAttr}>
<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>
`;
}
return `
<a href="${tool.url}" target="_blank" rel="noopener noreferrer"
class="btn btn-primary single-button" ${onClickAttr}>
Software-Homepage
</a>
`;
}
export function renderExtendedToolLinks(tool: Tool): string {
const { hasKnowledgebase } = classifyTool(tool);
let linksHTML = renderToolButtons(tool, false);
if (hasKnowledgebase) {
const kbId = tool.name.toLowerCase()
.replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '');
linksHTML += `
<a href="/knowledgebase#kb-${kbId}" class="btn btn-secondary w-full mt-2">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="mr-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"/>
<line x1="16" y1="13" x2="8" y2="13"/>
<line x1="16" y1="17" x2="8" y2="17"/>
<polyline points="10 9 9 9 8 9"/>
</svg>
Knowledgebase anzeigen
</a>
`;
}
return linksHTML;
}
// TAG RENDERING UTILITIES (consolidates tag display logic)
export function renderTagCloud(tool: Tool, maxTags: number = 8): string {
const tags = tool.tags || [];
return tags.slice(0, maxTags).map(tag =>
`<span class="tag">${escapeHtml(tag)}</span>`
).join('');
}
export function renderRelatedItems(
tool: Tool,
allTools: Tool[],
modalType: string = 'primary'
): string {
const relatedConcepts = tool.related_concepts || [];
const relatedSoftware = tool.related_software || [];
let html = '';
if (relatedConcepts.length > 0 && modalType === 'primary') {
const conceptLinks = relatedConcepts.map(conceptName => {
const concept = allTools.find(t => t.name === conceptName && t.type === 'concept');
if (concept) {
return `
<button class="tag cursor-pointer"
style="background-color: var(--color-concept-bg); border: 1px solid var(--color-concept); color: var(--color-concept); transition: var(--transition-fast);"
onclick="event.stopPropagation(); window.showToolDetails('${conceptName}', 'secondary')"
onmouseover="this.style.backgroundColor='var(--color-concept)'; this.style.color='white';"
onmouseout="this.style.backgroundColor='var(--color-concept-bg)'; this.style.color='var(--color-concept)';">
${conceptName}
</button>
`;
}
return `<span class="tag" style="background-color: var(--color-bg-tertiary); color: var(--color-text-secondary);">${conceptName}</span>`;
}).join('');
const isMobile = window.innerWidth <= 768;
const collapseOnMobile = isMobile && relatedConcepts.length > 2;
html += `
<div class="mt-4">
<div class="flex items-center gap-2 mb-2">
<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 === '▼' ? '▲' : '▼';"
class="btn-icon text-xs">
</button>
` : ''}
</div>
<div ${collapseOnMobile ? 'class="hidden"' : ''} class="flex flex-wrap gap-1">
${conceptLinks}
</div>
</div>
`;
}
if (relatedSoftware.length > 0 && modalType === 'primary') {
const softwareLinks = relatedSoftware.map(softwareName => {
const software = allTools.find(t => t.name === softwareName && (t.type === 'software' || t.type === 'method'));
if (software) {
const { isHosted, isMethod } = classifyTool(software);
const bgColor = isMethod ? 'var(--color-method-bg)' :
isHosted ? 'var(--color-hosted-bg)' : 'var(--color-oss-bg)';
const borderColor = isMethod ? 'var(--color-method)' :
isHosted ? 'var(--color-hosted)' : 'var(--color-oss)';
return `
<button class="tag cursor-pointer"
style="background-color: ${bgColor}; border: 1px solid ${borderColor}; color: ${borderColor}; transition: var(--transition-fast);"
onclick="event.stopPropagation(); window.showToolDetails('${softwareName}', 'secondary')"
onmouseover="this.style.backgroundColor='${borderColor}'; this.style.color='white';"
onmouseout="this.style.backgroundColor='${bgColor}'; this.style.color='${borderColor}';">
${softwareName}
</button>
`;
}
return `<span class="tag" style="background-color: var(--color-bg-tertiary); color: var(--color-text-secondary);">${softwareName}</span>`;
}).join('');
const isMobile = window.innerWidth <= 768;
const collapseOnMobile = isMobile && relatedSoftware.length > 2;
html += `
<div class="mt-4">
<div class="flex items-center gap-2 mb-2">
<strong style="color: var(--color-text);">Verwandte Software:</strong>
${collapseOnMobile ? `
<button id="software-toggle-${modalType}"
onclick="this.nextElementSibling.style.display = this.nextElementSibling.style.display === 'none' ? 'block' : 'none'; this.textContent = this.textContent === '▼' ? '▲' : '▼';"
class="btn-icon text-xs">
</button>
` : ''}
</div>
<div ${collapseOnMobile ? 'class="hidden"' : ''} class="flex flex-wrap gap-1">
${softwareLinks}
</div>
</div>
`;
}
return html;
}
// CONFIDENCE RENDERING UTILITIES (consolidates AI confidence display logic)
export interface ConfidenceData {
overall: number;
semanticRelevance: number;
taskSuitability: number;
uncertaintyFactors: string[];
strengthIndicators: string[];
}
export function renderConfidenceTooltip(confidence: ConfidenceData): string {
const confidenceColor = getConfidenceColor(confidence.overall);
const tooltipId = `tooltip-${Math.random().toString(36).substr(2, 9)}`;
return `
<span class="confidence-tooltip-trigger"
style="display: inline-flex; align-items: center; gap: 0.125rem; cursor: help; margin-left: 0.25rem; position: relative;"
onmouseenter="this.querySelector('.confidence-tooltip').style.display = 'block'"
onmouseleave="this.querySelector('.confidence-tooltip').style.display = 'none'"
onclick="event.stopPropagation();">
<div style="width: 6px; height: 6px; border-radius: 50%; background-color: ${confidenceColor}; flex-shrink: 0;"></div>
<span style="font-size: 0.625rem; color: white; font-weight: 600; text-shadow: 0 1px 2px rgba(0,0,0,0.5);">${confidence.overall}%</span>
<div class="confidence-tooltip"
style="display: none; position: absolute; top: 100%; right: 0; z-index: 1001; background: var(--color-bg); border: 1px solid var(--color-border); border-radius: 0.5rem; padding: 1rem; min-width: 320px; max-width: 400px; box-shadow: var(--shadow-lg); font-size: 0.75rem; color: var(--color-text); margin-top: 0.5rem;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem;">
<strong style="font-size: 0.875rem; color: var(--color-primary);">KI-Vertrauenswertung</strong>
<span style="background-color: ${confidenceColor}; color: white; font-weight: 600; padding: 0.125rem 0.375rem; border-radius: 0.25rem; font-size: 0.625rem;">${confidence.overall}%</span>
</div>
<div style="display: grid; grid-template-columns: 1fr; gap: 0.625rem; margin-bottom: 0.75rem;">
<div style="background: var(--color-bg-secondary); padding: 0.5rem; border-radius: 0.375rem; border-left: 3px solid var(--color-accent);">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.25rem;">
<span style="font-weight: 600; font-size: 0.6875rem; color: var(--color-text);">🔍 Semantische Relevanz</span>
<strong style="color: var(--color-accent);">${confidence.semanticRelevance}%</strong>
</div>
<div style="font-size: 0.625rem; color: var(--color-text-secondary); line-height: 1.3;">
Wie gut die Tool-Beschreibung semantisch zu Ihrer Anfrage passt
</div>
</div>
<div style="background: var(--color-bg-secondary); padding: 0.5rem; border-radius: 0.375rem; border-left: 3px solid var(--color-primary);">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.25rem;">
<span style="font-weight: 600; font-size: 0.6875rem; color: var(--color-text);">🎯 Aufgaben-Eignung</span>
<strong style="color: var(--color-primary);">${confidence.taskSuitability}%</strong>
</div>
<div style="font-size: 0.625rem; color: var(--color-text-secondary); line-height: 1.3;">
KI-bewertete Eignung für Ihre spezifische Aufgabenstellung
</div>
</div>
</div>
${confidence.strengthIndicators && confidence.strengthIndicators.length > 0 ? `
<div style="margin-bottom: 0.75rem; padding: 0.5rem; background: var(--color-oss-bg); border-radius: 0.375rem; border-left: 3px solid var(--color-accent);">
<strong style="color: var(--color-accent); font-size: 0.6875rem; display: flex; align-items: center; gap: 0.25rem; margin-bottom: 0.375rem;">
<span></span> Stärken dieser Empfehlung:
</strong>
<ul style="margin: 0; padding-left: 1rem; font-size: 0.625rem; line-height: 1.4; color: var(--color-text);">
${confidence.strengthIndicators.slice(0, 3).map(s => `<li style="margin-bottom: 0.25rem;">${sanitizeText(s)}</li>`).join('')}
</ul>
</div>
` : ''}
${confidence.uncertaintyFactors && confidence.uncertaintyFactors.length > 0 ? `
<div style="padding: 0.5rem; background: var(--color-hosted-bg); border-radius: 0.375rem; border-left: 3px solid var(--color-warning);">
<strong style="color: var(--color-warning); font-size: 0.6875rem; display: flex; align-items: center; gap: 0.25rem; margin-bottom: 0.375rem;">
<span></span> Mögliche Einschränkungen:
</strong>
<ul style="margin: 0; padding-left: 1rem; font-size: 0.625rem; line-height: 1.4; color: var(--color-text);">
${confidence.uncertaintyFactors.slice(0, 3).map(f => `<li style="margin-bottom: 0.25rem;">${sanitizeText(f)}</li>`).join('')}
</ul>
</div>
` : ''}
<div style="margin-top: 0.75rem; padding-top: 0.75rem; border-top: 1px solid var(--color-border); font-size: 0.625rem; color: var(--color-text-secondary); text-align: center;">
Forensisch fundierte KI-Analyse
</div>
</div>
</span>
`;
}
// SUITABILITY UTILITIES (consolidates suitability display logic)
export function getSuitabilityText(score: string): string {
const texts = {
high: 'GUT GEEIGNET',
medium: 'GEEIGNET',
low: 'VIELLEICHT GEEIGNET'
};
return texts[score] || 'GEEIGNET';
}
export function getSuitabilityColor(score: string): string {
const colors = {
high: 'var(--color-accent)',
medium: 'var(--color-warning)',
low: 'var(--color-text-secondary)'
};
return colors[score] || 'var(--color-warning)';
}

263
src/utils/uiHelpers.ts Normal file
View File

@ -0,0 +1,263 @@
// src/utils/uiHelpers.ts - Consolidated UI utilities
// This file consolidates all scattered utility functions
declare global {
interface Window {
createToolSlug: (toolName: string) => string;
findToolByIdentifier: (tools: Tool[], identifier: string) => Tool | undefined;
isToolHosted: (tool: Tool) => boolean;
scrollToElement: (element: Element | null, options?: ScrollIntoViewOptions) => void;
scrollToElementById: (elementId: string, options?: ScrollIntoViewOptions) => void;
scrollToElementBySelector: (selector: string, options?: ScrollIntoViewOptions) => void;
prioritizeSearchResults: (tools: Tool[], searchTerm: string) => Tool[];
shareArticle: (button: HTMLElement, url: string, title: string) => Promise<void>;
shareCurrentArticle: (button: HTMLElement) => Promise<void>;
}
}
export interface Tool {
name: string;
type?: 'software' | 'method' | 'concept';
projectUrl?: string | null;
license?: string;
knowledgebase?: boolean;
domains?: string[];
phases?: string[];
platforms?: string[];
skillLevel?: string;
description?: string;
tags?: string[];
related_concepts?: string[];
related_software?: string[];
url?: string; // Add missing property
accessType?: string; // Add missing property
}
// CORE TOOL UTILITIES (consolidates clientUtils.ts + toolHelpers.ts)
export function createToolSlug(toolName: string): string {
if (!toolName || typeof toolName !== 'string') {
console.warn('[uiHelpers] Invalid toolName provided to createToolSlug:', toolName);
return '';
}
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
}
export function findToolByIdentifier(tools: Tool[], identifier: string): Tool | undefined {
if (!identifier || !Array.isArray(tools)) return undefined;
return tools.find(tool =>
tool.name === identifier ||
createToolSlug(tool.name) === identifier.toLowerCase()
);
}
export function isToolHosted(tool: Tool): boolean {
return tool.projectUrl !== undefined &&
tool.projectUrl !== null &&
tool.projectUrl !== "" &&
tool.projectUrl.trim() !== "";
}
// TEXT UTILITIES (consolidates scattered text functions)
export function escapeHtml(text: string): string {
if (typeof text !== 'string') return String(text);
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
export function truncateText(text: string, maxLength: number): string {
if (!text || text.length <= maxLength) return text;
return text.slice(0, maxLength) + '...';
}
export function sanitizeText(text: string): string {
if (typeof text !== 'string') return '';
return text
.replace(/^#{1,6}\s+/gm, '')
.replace(/^\s*[-*+]\s+/gm, '')
.replace(/^\s*\d+\.\s+/gm, '')
.replace(/\*\*(.+?)\*\*/g, '$1')
.replace(/\*(.+?)\*/g, '$1')
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
.replace(/```[\s\S]*?```/g, '[CODE BLOCK]')
.replace(/`([^`]+)`/g, '$1')
.replace(/<[^>]+>/g, '')
.replace(/\n\s*\n\s*\n/g, '\n\n')
.trim();
}
export function summarizeData(data: any): string {
if (data === null || data === undefined) return 'null';
if (typeof data === 'string') {
return data.length > 100 ? data.slice(0, 100) + '...' : data;
}
if (typeof data === 'number' || typeof data === 'boolean') {
return data.toString();
}
if (Array.isArray(data)) {
if (data.length === 0) return '[]';
if (data.length <= 3) return JSON.stringify(data);
return `[${data.slice(0, 3).map(i => typeof i === 'string' ? i : JSON.stringify(i)).join(', ')}, ...+${data.length - 3}]`;
}
if (typeof data === 'object') {
const keys = Object.keys(data);
if (keys.length === 0) return '{}';
if (keys.length <= 3) {
return '{' + keys.map(k => `${k}: ${typeof data[k] === 'string' ? data[k].slice(0, 20) + (data[k].length > 20 ? '...' : '') : JSON.stringify(data[k])}`).join(', ') + '}';
}
return `{${keys.slice(0, 3).join(', ')}, ...+${keys.length - 3} keys}`;
}
return String(data);
}
// SCROLL UTILITIES (consolidates BaseLayout.astro scroll functions)
export function scrollToElement(element: Element | null, options: ScrollIntoViewOptions = {}): void {
if (!element) return;
setTimeout(() => {
const headerHeight = document.querySelector('nav')?.offsetHeight || 80;
const elementRect = element.getBoundingClientRect();
const absoluteElementTop = elementRect.top + window.pageYOffset;
const targetPosition = absoluteElementTop - headerHeight - 20;
window.scrollTo({
top: targetPosition,
behavior: 'smooth',
...options
});
}, 100);
}
export function scrollToElementById(elementId: string, options: ScrollIntoViewOptions = {}): void {
const element = document.getElementById(elementId);
if (element) {
scrollToElement(element, options);
}
}
export function scrollToElementBySelector(selector: string, options: ScrollIntoViewOptions = {}): void {
const element = document.querySelector(selector);
if (element) {
scrollToElement(element, options);
}
}
// SEARCH UTILITIES (consolidates BaseLayout.astro search functions)
export function prioritizeSearchResults(tools: Tool[], searchTerm: string): Tool[] {
if (!searchTerm || !searchTerm.trim()) {
return tools;
}
const lowerSearchTerm = searchTerm.toLowerCase().trim();
return tools.sort((a, b) => {
const aTagsLower = (a.tags || []).map((tag: string) => tag.toLowerCase());
const bTagsLower = (b.tags || []).map((tag: string) => tag.toLowerCase());
const aExactTag = aTagsLower.includes(lowerSearchTerm);
const bExactTag = bTagsLower.includes(lowerSearchTerm);
if (aExactTag && !bExactTag) return -1;
if (!aExactTag && bExactTag) return 1;
return a.name.localeCompare(b.name);
});
}
// URL UTILITIES (consolidates ToolMatrix.astro share functions)
export function generateShareURL(toolName: string, view: string, modal?: string): string {
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()}`;
}
// CLIPBOARD UTILITIES (consolidates ToolMatrix.astro + BaseLayout.astro clipboard functions)
export async function copyToClipboard(text: string, button: HTMLElement): Promise<void> {
try {
await navigator.clipboard.writeText(text);
const originalHTML = button.innerHTML;
button.innerHTML = `
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.375rem;">
<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);
const originalHTML = button.innerHTML;
button.innerHTML = 'Kopiert!';
setTimeout(() => {
button.innerHTML = originalHTML;
}, 2000);
}
}
// TIME UTILITIES (consolidates audit trail time formatting)
export function formatDuration(ms: number): string {
if (ms < 1000) return '< 1s';
if (ms < 60000) return `${Math.ceil(ms / 1000)}s`;
const minutes = Math.floor(ms / 60000);
const seconds = Math.ceil((ms % 60000) / 1000);
return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;
}
// CONFIDENCE UTILITIES (consolidates AI confidence color logic)
export function getConfidenceColor(confidence: number): string {
if (confidence >= 80) return 'var(--color-accent)';
if (confidence >= 60) return 'var(--color-warning)';
return 'var(--color-error)';
}
// Make all functions available globally for backward compatibility
export function registerGlobalUtilities(): void {
if (typeof window !== 'undefined') {
// Tool utilities
window.createToolSlug = createToolSlug;
window.findToolByIdentifier = findToolByIdentifier;
window.isToolHosted = isToolHosted;
// Scroll utilities
window.scrollToElement = scrollToElement;
window.scrollToElementById = scrollToElementById;
window.scrollToElementBySelector = scrollToElementBySelector;
// Search utilities
window.prioritizeSearchResults = prioritizeSearchResults;
// Share utilities
window.shareArticle = async (button: HTMLElement, url: string, title: string) => {
const fullUrl = window.location.origin + url;
await copyToClipboard(fullUrl, button);
};
window.shareCurrentArticle = async (button: HTMLElement) => {
await copyToClipboard(window.location.href, button);
};
console.log('[UI Helpers] Global utilities registered');
}
}