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>
|
||||
</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">
|
||||
<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"/>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,43 @@ class DocumentCards {
|
|||
this.container = document.getElementById(containerId);
|
||||
this.currentDocument = null;
|
||||
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
|
||||
const headerHtml = this.renderHeader(document);
|
||||
|
||||
// Group sections by category
|
||||
const sectionsByCategory = this.groupByCategory(document.sections);
|
||||
|
||||
// Render card grid
|
||||
const cardsHtml = this.renderCardGrid(sectionsByCategory);
|
||||
// Render cards in original markdown order (no grouping)
|
||||
const cardsHtml = this.renderCardGrid(document.sections);
|
||||
|
||||
this.container.innerHTML = `
|
||||
${headerHtml}
|
||||
|
|
@ -59,6 +93,7 @@ class DocumentCards {
|
|||
.join(' | ');
|
||||
|
||||
const hasToC = document.toc && document.toc.length > 0;
|
||||
const t = this.getLegendTranslations();
|
||||
|
||||
return `
|
||||
<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>` : ''}
|
||||
${document.sections ? `<p class="text-sm text-gray-600 mt-1">${document.sections.length} sections</p>` : ''}
|
||||
</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="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 ? `
|
||||
<button id="toc-button"
|
||||
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
|
||||
*/
|
||||
renderCardGrid(sectionsByCategory) {
|
||||
renderCardGrid(sections) {
|
||||
const categoryConfig = {
|
||||
critical: { icon: '⚠️', label: 'Critical', color: 'red', order: 1 },
|
||||
conceptual: { icon: '📘', label: 'Conceptual', color: 'blue', order: 2 },
|
||||
practical: { icon: '✨', label: 'Practical', color: 'green', order: 3 },
|
||||
technical: { icon: '🔧', label: 'Technical', color: 'purple', order: 4 },
|
||||
reference: { icon: '📋', label: 'Reference', color: 'gray', order: 5 }
|
||||
critical: { color: 'red' },
|
||||
conceptual: { color: 'blue' },
|
||||
practical: { color: 'green' },
|
||||
technical: { color: 'purple' },
|
||||
reference: { color: 'gray' }
|
||||
};
|
||||
|
||||
let html = '<div class="card-grid-container">';
|
||||
|
||||
// Render categories in priority order (critical first)
|
||||
const orderedCategories = Object.entries(sectionsByCategory)
|
||||
.filter(([category, sections]) => sections.length > 0)
|
||||
.sort((a, b) => {
|
||||
const orderA = categoryConfig[a[0]]?.order || 999;
|
||||
const orderB = categoryConfig[b[0]]?.order || 999;
|
||||
return orderA - orderB;
|
||||
});
|
||||
|
||||
// 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('')}
|
||||
// Render all cards in original markdown order (no grouping)
|
||||
const html = `
|
||||
<div class="card-grid-container w-full max-w-full overflow-hidden">
|
||||
<div class="card-grid grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 w-full">
|
||||
${sections.map(section => {
|
||||
const category = section.category || 'conceptual';
|
||||
const color = categoryConfig[category]?.color || 'blue';
|
||||
return this.renderCard(section, color);
|
||||
}).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
|
||||
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
|
||||
*/
|
||||
|
|
@ -176,6 +237,9 @@ class DocumentCards {
|
|||
const levelIcon = levelIcons[section.technicalLevel] || '○';
|
||||
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 = {
|
||||
red: 'border-red-500',
|
||||
blue: 'border-blue-400',
|
||||
|
|
@ -201,13 +265,13 @@ class DocumentCards {
|
|||
}[color] || 'bg-blue-50';
|
||||
|
||||
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}">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-3">${section.title}</h3>
|
||||
<p class="text-sm text-gray-700 mb-4 line-clamp-3">${section.excerpt}</p>
|
||||
<div class="flex items-center justify-between text-xs text-gray-600">
|
||||
<span>${section.readingTime} min read</span>
|
||||
<span title="${levelLabel}">${levelIcon} ${levelLabel}</span>
|
||||
<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 break-words overflow-wrap-anywhere">${section.excerpt}</p>
|
||||
<div class="flex items-center justify-between text-xs text-gray-600 gap-2">
|
||||
<span class="truncate">${section.readingTime} min read</span>
|
||||
<span title="${levelLabel}" class="flex-shrink-0">${levelIcon} ${levelLabel}</span>
|
||||
</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
|
||||
const category = doc.category || 'downloads-resources';
|
||||
const category = doc.category || 'resources';
|
||||
|
||||
// Validate category exists in CATEGORIES constant
|
||||
if (CATEGORIES[category]) {
|
||||
return category;
|
||||
}
|
||||
|
||||
// Fallback to downloads-resources for uncategorized
|
||||
console.warn(`Document "${doc.title}" has invalid category "${category}", using fallback`);
|
||||
return 'downloads-resources';
|
||||
// Fallback to resources for uncategorized
|
||||
console.warn(`Document "${doc.title}" has invalid category "${category}", using fallback to resources`);
|
||||
return 'resources';
|
||||
}
|
||||
|
||||
// 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