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" → "Instruction­Persistence­Classifier"
- 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:
TheFlow 2025-10-26 11:03:23 +13:00
parent 0c4c978dcd
commit 63fd753622
4 changed files with 213 additions and 52 deletions

View file

@ -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"/>

View file

@ -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 (&shy;) before capital letters in CamelCase words
// e.g., "InstructionPersistenceClassifier" → "Instruction&shy;Persistence&shy;Classifier"
return text.replace(/([a-z])([A-Z])/g, '$1&shy;$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;
}
});
}
} }
} }

View file

@ -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

View 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();