fix(docs): card overflow, sequencing, colour legend, and category fixes
Fixed multiple issues with the docs page card-based document view: **Card Overflow Fixed:** - Added overflow-x-hidden to #document-content container - Added w-full max-w-full to card-grid-container - Added w-full to grid itself - Added max-w-full overflow-hidden to individual cards - Cards now stay within container boundaries at all viewport sizes **Long Title Wrapping:** - Added insertSoftHyphens() method to break CamelCase words - Inserts soft hyphens (­) before capitals in compound words - Examples: "InstructionPersistenceClassifier" → "InstructionPersistenceClassifier" - Titles now wrap intelligently without being cut off **Colour Legend (Option C):** - Added toggle button (ℹ️) next to ToC and PDF buttons - Popup shows all 5 colour codes with descriptions - Translated to EN ("Colour Guide"), DE ("Farbcode"), FR ("Guide des couleurs") - Fixed colour square visibility (bg-500 with borders instead of bg-400) - Click outside to close functionality **Card Sequencing:** - Cards now display in original markdown document order - Removed groupByCategory() grouping logic - Removed category header sections - Color coding preserved based on section category **Category Fallback Bug:** - Fixed invalid fallback category 'downloads-resources' → 'resources' - Ensures uncategorized documents go to valid category **Database Migration:** - Added scripts/move-guides-to-resources.js - Moved 3 implementation guides from getting-started to resources - Getting Started now contains only: Introduction, Core Concepts - Resources now contains: Implementation guides **Result:** ✅ Cards respect container width (no overflow) ✅ Long titles wrap with hyphens (no cutoff) ✅ Colour legend accessible and translated ✅ Cards in logical reading order from markdown ✅ Implementation guides in correct category 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
0c4c978dcd
commit
63fd753622
4 changed files with 213 additions and 52 deletions
|
|
@ -587,7 +587,7 @@
|
||||||
<span class="font-medium text-gray-700">Back to Documents</span>
|
<span class="font-medium text-gray-700">Back to Documents</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div id="document-content" class="bg-white rounded-lg shadow-sm border border-gray-200 p-8">
|
<div id="document-content" class="bg-white rounded-lg shadow-sm border border-gray-200 p-8 overflow-x-hidden">
|
||||||
<div class="text-center py-12">
|
<div class="text-center py-12">
|
||||||
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,43 @@ class DocumentCards {
|
||||||
this.container = document.getElementById(containerId);
|
this.container = document.getElementById(containerId);
|
||||||
this.currentDocument = null;
|
this.currentDocument = null;
|
||||||
this.modalViewer = new ModalViewer();
|
this.modalViewer = new ModalViewer();
|
||||||
|
this.legendVisible = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get translations for color legend
|
||||||
|
*/
|
||||||
|
getLegendTranslations() {
|
||||||
|
const lang = (window.I18n && window.I18n.currentLang) || 'en';
|
||||||
|
|
||||||
|
const translations = {
|
||||||
|
en: {
|
||||||
|
title: 'Colour Guide',
|
||||||
|
critical: 'Critical sections',
|
||||||
|
conceptual: 'Conceptual explanations',
|
||||||
|
practical: 'Practical guides',
|
||||||
|
technical: 'Technical details',
|
||||||
|
reference: 'Reference documentation'
|
||||||
|
},
|
||||||
|
de: {
|
||||||
|
title: 'Farbcode',
|
||||||
|
critical: 'Kritische Abschnitte',
|
||||||
|
conceptual: 'Konzeptionelle Erklärungen',
|
||||||
|
practical: 'Praktische Anleitungen',
|
||||||
|
technical: 'Technische Details',
|
||||||
|
reference: 'Referenzdokumentation'
|
||||||
|
},
|
||||||
|
fr: {
|
||||||
|
title: 'Guide des couleurs',
|
||||||
|
critical: 'Sections critiques',
|
||||||
|
conceptual: 'Explications conceptuelles',
|
||||||
|
practical: 'Guides pratiques',
|
||||||
|
technical: 'Détails techniques',
|
||||||
|
reference: 'Documentation de référence'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return translations[lang] || translations.en;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -24,11 +61,8 @@ class DocumentCards {
|
||||||
// Create document header
|
// Create document header
|
||||||
const headerHtml = this.renderHeader(document);
|
const headerHtml = this.renderHeader(document);
|
||||||
|
|
||||||
// Group sections by category
|
// Render cards in original markdown order (no grouping)
|
||||||
const sectionsByCategory = this.groupByCategory(document.sections);
|
const cardsHtml = this.renderCardGrid(document.sections);
|
||||||
|
|
||||||
// Render card grid
|
|
||||||
const cardsHtml = this.renderCardGrid(sectionsByCategory);
|
|
||||||
|
|
||||||
this.container.innerHTML = `
|
this.container.innerHTML = `
|
||||||
${headerHtml}
|
${headerHtml}
|
||||||
|
|
@ -59,6 +93,7 @@ class DocumentCards {
|
||||||
.join(' | ');
|
.join(' | ');
|
||||||
|
|
||||||
const hasToC = document.toc && document.toc.length > 0;
|
const hasToC = document.toc && document.toc.length > 0;
|
||||||
|
const t = this.getLegendTranslations();
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="flex items-center justify-between mb-6 pb-4 border-b border-gray-200">
|
<div class="flex items-center justify-between mb-6 pb-4 border-b border-gray-200">
|
||||||
|
|
@ -67,7 +102,42 @@ class DocumentCards {
|
||||||
${metaText ? `<p class="text-sm text-gray-500">${metaText}</p>` : ''}
|
${metaText ? `<p class="text-sm text-gray-500">${metaText}</p>` : ''}
|
||||||
${document.sections ? `<p class="text-sm text-gray-600 mt-1">${document.sections.length} sections</p>` : ''}
|
${document.sections ? `<p class="text-sm text-gray-600 mt-1">${document.sections.length} sections</p>` : ''}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex items-center gap-2 relative">
|
||||||
|
${document.sections && document.sections.length > 0 ? `
|
||||||
|
<button id="color-legend-button"
|
||||||
|
class="p-2 text-gray-600 hover:text-blue-600 hover:bg-blue-50 rounded transition"
|
||||||
|
title="${t.title}"
|
||||||
|
aria-label="${t.title}">
|
||||||
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div id="color-legend-popup" class="hidden absolute top-12 right-0 bg-white border border-gray-200 rounded-lg shadow-xl p-4 z-50 w-72">
|
||||||
|
<h3 class="text-sm font-bold text-gray-900 mb-3">${t.title}</h3>
|
||||||
|
<div class="space-y-2 text-sm">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="w-4 h-4 bg-red-500 border border-red-600 rounded"></div>
|
||||||
|
<span class="text-gray-700">${t.critical}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="w-4 h-4 bg-blue-500 border border-blue-600 rounded"></div>
|
||||||
|
<span class="text-gray-700">${t.conceptual}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="w-4 h-4 bg-green-500 border border-green-600 rounded"></div>
|
||||||
|
<span class="text-gray-700">${t.practical}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="w-4 h-4 bg-purple-500 border border-purple-600 rounded"></div>
|
||||||
|
<span class="text-gray-700">${t.technical}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="w-4 h-4 bg-gray-500 border border-gray-600 rounded"></div>
|
||||||
|
<span class="text-gray-700">${t.reference}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
${hasToC ? `
|
${hasToC ? `
|
||||||
<button id="toc-button"
|
<button id="toc-button"
|
||||||
class="p-2 text-gray-600 hover:text-blue-600 hover:bg-blue-50 rounded transition"
|
class="p-2 text-gray-600 hover:text-blue-600 hover:bg-blue-50 rounded transition"
|
||||||
|
|
@ -120,49 +190,40 @@ class DocumentCards {
|
||||||
/**
|
/**
|
||||||
* Render card grid
|
* Render card grid
|
||||||
*/
|
*/
|
||||||
renderCardGrid(sectionsByCategory) {
|
renderCardGrid(sections) {
|
||||||
const categoryConfig = {
|
const categoryConfig = {
|
||||||
critical: { icon: '⚠️', label: 'Critical', color: 'red', order: 1 },
|
critical: { color: 'red' },
|
||||||
conceptual: { icon: '📘', label: 'Conceptual', color: 'blue', order: 2 },
|
conceptual: { color: 'blue' },
|
||||||
practical: { icon: '✨', label: 'Practical', color: 'green', order: 3 },
|
practical: { color: 'green' },
|
||||||
technical: { icon: '🔧', label: 'Technical', color: 'purple', order: 4 },
|
technical: { color: 'purple' },
|
||||||
reference: { icon: '📋', label: 'Reference', color: 'gray', order: 5 }
|
reference: { color: 'gray' }
|
||||||
};
|
};
|
||||||
|
|
||||||
let html = '<div class="card-grid-container">';
|
// Render all cards in original markdown order (no grouping)
|
||||||
|
const html = `
|
||||||
// Render categories in priority order (critical first)
|
<div class="card-grid-container w-full max-w-full overflow-hidden">
|
||||||
const orderedCategories = Object.entries(sectionsByCategory)
|
<div class="card-grid grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 w-full">
|
||||||
.filter(([category, sections]) => sections.length > 0)
|
${sections.map(section => {
|
||||||
.sort((a, b) => {
|
const category = section.category || 'conceptual';
|
||||||
const orderA = categoryConfig[a[0]]?.order || 999;
|
const color = categoryConfig[category]?.color || 'blue';
|
||||||
const orderB = categoryConfig[b[0]]?.order || 999;
|
return this.renderCard(section, color);
|
||||||
return orderA - orderB;
|
}).join('')}
|
||||||
});
|
|
||||||
|
|
||||||
// Render each category that has sections
|
|
||||||
for (const [category, sections] of orderedCategories) {
|
|
||||||
|
|
||||||
const config = categoryConfig[category];
|
|
||||||
|
|
||||||
html += `
|
|
||||||
<div class="category-section mb-8">
|
|
||||||
<h2 class="category-header text-lg font-semibold text-gray-700 mb-4 flex items-center">
|
|
||||||
<span class="text-2xl mr-2">${config.icon}</span>
|
|
||||||
${config.label}
|
|
||||||
</h2>
|
|
||||||
<div class="card-grid grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
||||||
${sections.map(section => this.renderCard(section, config.color)).join('')}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
|
||||||
|
|
||||||
html += '</div>';
|
|
||||||
|
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert soft hyphens in CamelCase words for better wrapping
|
||||||
|
*/
|
||||||
|
insertSoftHyphens(text) {
|
||||||
|
// Insert soft hyphens (­) before capital letters in CamelCase words
|
||||||
|
// e.g., "InstructionPersistenceClassifier" → "Instruction­Persistence­Classifier"
|
||||||
|
return text.replace(/([a-z])([A-Z])/g, '$1­$2');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render individual card
|
* Render individual card
|
||||||
*/
|
*/
|
||||||
|
|
@ -176,6 +237,9 @@ class DocumentCards {
|
||||||
const levelIcon = levelIcons[section.technicalLevel] || '○';
|
const levelIcon = levelIcons[section.technicalLevel] || '○';
|
||||||
const levelLabel = section.technicalLevel.charAt(0).toUpperCase() + section.technicalLevel.slice(1);
|
const levelLabel = section.technicalLevel.charAt(0).toUpperCase() + section.technicalLevel.slice(1);
|
||||||
|
|
||||||
|
// Add soft hyphens to long titles for better wrapping
|
||||||
|
const titleWithHyphens = this.insertSoftHyphens(section.title);
|
||||||
|
|
||||||
const borderColor = {
|
const borderColor = {
|
||||||
red: 'border-red-500',
|
red: 'border-red-500',
|
||||||
blue: 'border-blue-400',
|
blue: 'border-blue-400',
|
||||||
|
|
@ -201,13 +265,13 @@ class DocumentCards {
|
||||||
}[color] || 'bg-blue-50';
|
}[color] || 'bg-blue-50';
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="doc-card ${bgColor} border-2 ${borderColor} rounded-lg p-5 cursor-pointer transition-all duration-200 ${hoverColor} hover:shadow-lg"
|
<div class="doc-card ${bgColor} border-2 ${borderColor} rounded-lg p-5 cursor-pointer transition-all duration-200 ${hoverColor} hover:shadow-lg min-w-0 max-w-full overflow-hidden"
|
||||||
data-section-slug="${section.slug}">
|
data-section-slug="${section.slug}">
|
||||||
<h3 class="text-lg font-semibold text-gray-900 mb-3">${section.title}</h3>
|
<h3 class="text-lg font-semibold text-gray-900 mb-3 break-words overflow-wrap-anywhere">${titleWithHyphens}</h3>
|
||||||
<p class="text-sm text-gray-700 mb-4 line-clamp-3">${section.excerpt}</p>
|
<p class="text-sm text-gray-700 mb-4 line-clamp-3 break-words overflow-wrap-anywhere">${section.excerpt}</p>
|
||||||
<div class="flex items-center justify-between text-xs text-gray-600">
|
<div class="flex items-center justify-between text-xs text-gray-600 gap-2">
|
||||||
<span>${section.readingTime} min read</span>
|
<span class="truncate">${section.readingTime} min read</span>
|
||||||
<span title="${levelLabel}">${levelIcon} ${levelLabel}</span>
|
<span title="${levelLabel}" class="flex-shrink-0">${levelIcon} ${levelLabel}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
@ -257,6 +321,28 @@ class DocumentCards {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Attach color legend toggle listener
|
||||||
|
const legendButton = document.getElementById('color-legend-button');
|
||||||
|
const legendPopup = document.getElementById('color-legend-popup');
|
||||||
|
if (legendButton && legendPopup) {
|
||||||
|
legendButton.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
legendPopup.classList.toggle('hidden');
|
||||||
|
this.legendVisible = !this.legendVisible;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close legend when clicking outside
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
if (this.legendVisible &&
|
||||||
|
!legendButton.contains(e.target) &&
|
||||||
|
!legendPopup.contains(e.target)) {
|
||||||
|
legendPopup.classList.add('hidden');
|
||||||
|
this.legendVisible = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -348,16 +348,16 @@ function categorizeDocument(doc) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use category from database
|
// Use category from database
|
||||||
const category = doc.category || 'downloads-resources';
|
const category = doc.category || 'resources';
|
||||||
|
|
||||||
// Validate category exists in CATEGORIES constant
|
// Validate category exists in CATEGORIES constant
|
||||||
if (CATEGORIES[category]) {
|
if (CATEGORIES[category]) {
|
||||||
return category;
|
return category;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to downloads-resources for uncategorized
|
// Fallback to resources for uncategorized
|
||||||
console.warn(`Document "${doc.title}" has invalid category "${category}", using fallback`);
|
console.warn(`Document "${doc.title}" has invalid category "${category}", using fallback to resources`);
|
||||||
return 'downloads-resources';
|
return 'resources';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Group documents by category
|
// Group documents by category
|
||||||
|
|
|
||||||
75
scripts/move-guides-to-resources.js
Normal file
75
scripts/move-guides-to-resources.js
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
const { MongoClient } = require('mongodb');
|
||||||
|
|
||||||
|
const MONGO_URI = 'mongodb://localhost:27017';
|
||||||
|
const DB_NAME = 'tractatus_dev';
|
||||||
|
|
||||||
|
async function moveGuidesToResources() {
|
||||||
|
const client = new MongoClient(MONGO_URI);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.connect();
|
||||||
|
console.log('✓ Connected to MongoDB');
|
||||||
|
|
||||||
|
const db = client.db(DB_NAME);
|
||||||
|
const collection = db.collection('documents');
|
||||||
|
|
||||||
|
// Find implementation guides currently in getting-started
|
||||||
|
const guides = await collection.find({
|
||||||
|
category: 'getting-started',
|
||||||
|
$or: [
|
||||||
|
{ slug: { $regex: /implementation-guide/ } },
|
||||||
|
{ title: { $regex: /Implementation Guide/i } }
|
||||||
|
]
|
||||||
|
}).toArray();
|
||||||
|
|
||||||
|
console.log(`\nFound ${guides.length} implementation guide(s):`);
|
||||||
|
guides.forEach(doc => {
|
||||||
|
console.log(` - ${doc.title} (${doc.slug})`);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (guides.length === 0) {
|
||||||
|
console.log('\n⚠ No guides to move (already in resources?)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update all implementation guides to resources category
|
||||||
|
const result = await collection.updateMany(
|
||||||
|
{
|
||||||
|
category: 'getting-started',
|
||||||
|
$or: [
|
||||||
|
{ slug: { $regex: /implementation-guide/ } },
|
||||||
|
{ title: { $regex: /Implementation Guide/i } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$set: { category: 'resources' }
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`\n✅ Updated ${result.modifiedCount} document(s)`);
|
||||||
|
console.log(' Category changed: getting-started → resources');
|
||||||
|
|
||||||
|
// Verify the change
|
||||||
|
const verifyGuides = await collection.find({
|
||||||
|
category: 'resources',
|
||||||
|
$or: [
|
||||||
|
{ slug: { $regex: /implementation-guide/ } },
|
||||||
|
{ title: { $regex: /Implementation Guide/i } }
|
||||||
|
]
|
||||||
|
}).toArray();
|
||||||
|
|
||||||
|
console.log(`\n✓ Verification: ${verifyGuides.length} guide(s) now in Resources:`);
|
||||||
|
verifyGuides.forEach(doc => {
|
||||||
|
console.log(` - ${doc.title}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
} finally {
|
||||||
|
await client.close();
|
||||||
|
console.log('\n✓ Database connection closed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
moveGuidesToResources();
|
||||||
Loading…
Add table
Reference in a new issue