feat(docs): add brand book and page copy functionality

- Add comprehensive brand book with color system, typography, components
- Add page copy dropdown with markdown copy/view functionality
- Update mkdocs.yml with new assets and branding navigation
- Use terminal-style ASCII icons and condensed menu design
This commit is contained in:
unclecode
2025-09-30 18:28:05 +08:00
parent 3fe49a766c
commit 0d8d043109
4 changed files with 2111 additions and 3 deletions

View File

@@ -0,0 +1,388 @@
/* ==== File: assets/page_actions.css ==== */
/* Page Actions Dropdown - Terminal Style */
/* Wrapper - positioned in content area */
.page-actions-wrapper {
position: absolute;
top: 1.3rem;
right: 1rem;
z-index: 1000;
}
/* Floating Action Button */
.page-actions-button {
position: relative;
display: inline-flex;
align-items: center;
gap: 0.5rem;
background: #3f3f44;
border: 1px solid #50ffff;
color: #e8e9ed;
padding: 0.75rem 1rem;
border-radius: 6px;
font-family: 'Dank Mono', Monaco, monospace;
font-size: 0.875rem;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.page-actions-button:hover {
background: #50ffff;
color: #070708;
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(80, 255, 255, 0.3);
}
.page-actions-button::before {
content: '▤';
font-size: 1.2rem;
line-height: 1;
}
.page-actions-button::after {
content: '▼';
font-size: 0.6rem;
transition: transform 0.2s ease;
}
.page-actions-button.active::after {
transform: rotate(180deg);
}
/* Dropdown Menu */
.page-actions-dropdown {
position: absolute;
top: 3.5rem;
right: 0;
z-index: 1001;
background: #1a1a1a;
border: 1px solid #3f3f44;
border-radius: 8px;
min-width: 280px;
opacity: 0;
visibility: hidden;
transform: translateY(-10px);
transition: all 0.2s ease;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
overflow: hidden;
}
.page-actions-dropdown.active {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.page-actions-dropdown::before {
content: '';
position: absolute;
top: -8px;
right: 1.5rem;
width: 0;
height: 0;
border-left: 8px solid transparent;
border-right: 8px solid transparent;
border-bottom: 8px solid #3f3f44;
}
/* Menu Header */
.page-actions-header {
background: #3f3f44;
padding: 0.5rem 0.75rem;
border-bottom: 1px solid #50ffff;
font-family: 'Dank Mono', Monaco, monospace;
font-size: 0.7rem;
color: #a3abba;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.page-actions-header::before {
content: '┌─';
margin-right: 0.5rem;
color: #50ffff;
}
/* Menu Items */
.page-actions-menu {
list-style: none;
margin: 0;
padding: 0.25rem 0;
}
.page-action-item {
display: block;
padding: 0;
}
ul>li.page-action-item::after{
content: '';
}
.page-action-link {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
color: #e8e9ed;
text-decoration: none !important;
font-family: 'Dank Mono', Monaco, monospace;
font-size: 0.8rem;
transition: all 0.15s ease;
cursor: pointer;
border-left: 3px solid transparent;
}
.page-action-link:hover:not(.disabled) {
background: #3f3f44;
border-left-color: #50ffff;
color: #50ffff;
text-decoration: none;
}
.page-action-link.disabled {
opacity: 0.5;
cursor: not-allowed;
}
.page-action-link.disabled:hover {
background: transparent;
color: #e8e9ed;
text-decoration: none;
}
/* Icons using ASCII/Terminal characters */
.page-action-icon {
font-size: 1rem;
width: 1.5rem;
text-align: center;
font-weight: bold;
color: #50ffff;
}
.page-action-link:hover:not(.disabled) .page-action-icon {
color: #50ffff;
}
.page-action-link.disabled .page-action-icon {
color: #666;
}
/* Specific icons */
.icon-copy::before {
content: '⎘'; /* Copy/duplicate symbol */
}
.icon-view::before {
content: '⎙'; /* Document symbol */
}
.icon-ai::before {
content: '⚡'; /* Lightning/AI symbol */
}
/* Action Text */
.page-action-text {
flex: 1;
}
.page-action-label {
display: block;
font-weight: 600;
margin-bottom: 0.05rem;
line-height: 1.3;
}
.page-action-description {
display: block;
font-size: 0.7rem;
color: #a3abba;
line-height: 1.2;
}
/* Badge */
.page-action-badge {
display: inline-block;
background: #f59e0b;
color: #070708;
padding: 0.125rem 0.5rem;
border-radius: 12px;
font-size: 0.65rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* External link indicator */
.page-action-external::after {
content: '→';
margin-left: 0.25rem;
font-size: 0.75rem;
}
/* Divider */
.page-actions-divider {
height: 1px;
background: #3f3f44;
margin: 0.25rem 0;
}
/* Success/Copy feedback */
.page-action-copied {
background: #50ff50 !important;
color: #070708 !important;
border-left-color: #50ff50 !important;
}
.page-action-copied .page-action-icon {
color: #070708 !important;
}
.page-action-copied .page-action-icon::before {
content: '✓';
}
/* Mobile Responsive */
@media (max-width: 768px) {
.page-actions-wrapper {
top: 0.5rem;
right: 0.5rem;
}
.page-actions-button {
padding: 0.6rem 0.8rem;
font-size: 0.8rem;
}
.page-actions-dropdown {
min-width: 260px;
max-width: calc(100vw - 2rem);
right: -0.5rem;
}
.page-action-link {
padding: 0.6rem 0.8rem;
font-size: 0.8rem;
}
.page-action-description {
font-size: 0.7rem;
}
}
/* Animation for tooltip/notification */
@keyframes slideInFromTop {
from {
transform: translateY(-20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.page-actions-notification {
position: fixed;
top: calc(var(--header-height) + 0.5rem);
right: 50%;
transform: translateX(50%);
z-index: 1100;
background: #50ff50;
color: #070708;
padding: 0.75rem 1.5rem;
border-radius: 6px;
font-family: 'Dank Mono', Monaco, monospace;
font-size: 0.875rem;
font-weight: 600;
box-shadow: 0 4px 12px rgba(80, 255, 80, 0.4);
animation: slideInFromTop 0.3s ease;
pointer-events: none;
}
.page-actions-notification::before {
content: '✓ ';
margin-right: 0.5rem;
}
/* Hide on print */
@media print {
.page-actions-button,
.page-actions-dropdown {
display: none !important;
}
}
/* Overlay for mobile */
.page-actions-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 998;
opacity: 0;
transition: opacity 0.2s ease;
}
.page-actions-overlay.active {
display: block;
opacity: 1;
}
@media (max-width: 768px) {
.page-actions-overlay {
display: block;
}
}
/* Keyboard focus styles */
.page-action-link:focus {
outline: 2px solid #50ffff;
outline-offset: -2px;
}
.page-actions-button:focus {
outline: 2px solid #50ffff;
outline-offset: 2px;
}
/* Loading state */
.page-action-link.loading {
pointer-events: none;
opacity: 0.7;
}
.page-action-link.loading .page-action-icon::before {
content: '⟳';
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* Terminal-style border effect on hover */
.page-actions-dropdown:hover {
border-color: #50ffff;
}
/* Footer info */
.page-actions-footer {
background: #070708;
padding: 0.4rem 0.75rem;
border-top: 1px solid #3f3f44;
font-size: 0.65rem;
color: #666;
text-align: center;
font-family: 'Dank Mono', Monaco, monospace;
}
.page-actions-footer::before {
content: '└─';
margin-right: 0.5rem;
color: #3f3f44;
}

View File

@@ -0,0 +1,346 @@
// ==== File: assets/page_actions.js ====
// Page Actions - Copy/View Markdown functionality
document.addEventListener('DOMContentLoaded', () => {
// Configuration
const config = {
githubRepo: 'unclecode/crawl4ai',
githubBranch: 'main',
docsPath: 'docs/md_v2',
excludePaths: ['/apps/c4a-script/', '/apps/llmtxt/', '/apps/crawl4ai-assistant/'], // Don't show on app pages
};
// Check if we should show the button on this page
function shouldShowButton() {
const currentPath = window.location.pathname;
// Don't show on homepage
if (currentPath === '/' || currentPath === '/index.html') {
return false;
}
// Don't show on excluded paths (apps)
for (const excludePath of config.excludePaths) {
if (currentPath.includes(excludePath)) {
return false;
}
}
// Only show on documentation pages
return true;
}
if (!shouldShowButton()) {
return;
}
// Get current page markdown path
function getCurrentMarkdownPath() {
let path = window.location.pathname;
// Remove leading/trailing slashes
path = path.replace(/^\/|\/$/g, '');
// Remove .html extension if present
path = path.replace(/\.html$/, '');
// Handle root/index
if (!path || path === 'index') {
return 'index.md';
}
// Add .md extension
return `${path}.md`;
}
// Get GitHub raw URL for current page
function getGithubRawUrl() {
const mdPath = getCurrentMarkdownPath();
return `https://raw.githubusercontent.com/${config.githubRepo}/${config.githubBranch}/${config.docsPath}/${mdPath}`;
}
// Get GitHub file URL for current page (for viewing)
function getGithubFileUrl() {
const mdPath = getCurrentMarkdownPath();
return `https://github.com/${config.githubRepo}/blob/${config.githubBranch}/${config.docsPath}/${mdPath}`;
}
// Create the UI
function createPageActionsUI() {
// Find the main content area
const mainContent = document.getElementById('terminal-mkdocs-main-content');
if (!mainContent) {
console.warn('Page Actions: Could not find #terminal-mkdocs-main-content');
return null;
}
// Create button
const button = document.createElement('button');
button.className = 'page-actions-button';
button.setAttribute('aria-label', 'Page copy');
button.setAttribute('aria-expanded', 'false');
button.innerHTML = '<span>Page Copy</span>';
// Create overlay for mobile
const overlay = document.createElement('div');
overlay.className = 'page-actions-overlay';
// Create dropdown
const dropdown = document.createElement('div');
dropdown.className = 'page-actions-dropdown';
dropdown.setAttribute('role', 'menu');
dropdown.innerHTML = `
<div class="page-actions-header">Page Copy</div>
<ul class="page-actions-menu">
<li class="page-action-item">
<a href="#" class="page-action-link" id="action-copy-markdown" role="menuitem">
<span class="page-action-icon icon-copy"></span>
<span class="page-action-text">
<span class="page-action-label">Copy as Markdown</span>
<span class="page-action-description">Copy page for LLMs</span>
</span>
</a>
</li>
<li class="page-action-item">
<a href="#" class="page-action-link page-action-external" id="action-view-markdown" target="_blank" role="menuitem">
<span class="page-action-icon icon-view"></span>
<span class="page-action-text">
<span class="page-action-label">View as Markdown</span>
<span class="page-action-description">Open raw source</span>
</span>
</a>
</li>
<div class="page-actions-divider"></div>
<li class="page-action-item">
<a href="#" class="page-action-link disabled" id="action-ask-ai" role="menuitem">
<span class="page-action-icon icon-ai"></span>
<span class="page-action-text">
<span class="page-action-label">Ask AI about page</span>
<span class="page-action-description">
<span class="page-action-badge">Coming Soon</span>
</span>
</span>
</a>
</li>
</ul>
<div class="page-actions-footer">ESC to close</div>
`;
// Create a wrapper for button and dropdown
const wrapper = document.createElement('div');
wrapper.className = 'page-actions-wrapper';
wrapper.appendChild(button);
wrapper.appendChild(dropdown);
// Inject into main content area
mainContent.appendChild(wrapper);
// Append overlay to body
document.body.appendChild(overlay);
return { button, dropdown, overlay, wrapper };
}
// Toggle dropdown
function toggleDropdown(button, dropdown, overlay) {
const isActive = dropdown.classList.contains('active');
if (isActive) {
closeDropdown(button, dropdown, overlay);
} else {
openDropdown(button, dropdown, overlay);
}
}
function openDropdown(button, dropdown, overlay) {
dropdown.classList.add('active');
// Don't activate overlay - not needed
button.classList.add('active');
button.setAttribute('aria-expanded', 'true');
}
function closeDropdown(button, dropdown, overlay) {
dropdown.classList.remove('active');
// Don't deactivate overlay - not needed
button.classList.remove('active');
button.setAttribute('aria-expanded', 'false');
}
// Show notification
function showNotification(message, duration = 2000) {
const notification = document.createElement('div');
notification.className = 'page-actions-notification';
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
notification.remove();
}, duration);
}
// Copy markdown to clipboard
async function copyMarkdownToClipboard(link) {
const rawUrl = getGithubRawUrl();
// Add loading state
link.classList.add('loading');
try {
const response = await fetch(rawUrl);
if (!response.ok) {
throw new Error(`Failed to fetch markdown: ${response.status}`);
}
const markdown = await response.text();
// Copy to clipboard
await navigator.clipboard.writeText(markdown);
// Visual feedback
link.classList.remove('loading');
link.classList.add('page-action-copied');
showNotification('Markdown copied to clipboard!');
// Reset after delay
setTimeout(() => {
link.classList.remove('page-action-copied');
}, 2000);
} catch (error) {
console.error('Error copying markdown:', error);
link.classList.remove('loading');
showNotification('Error: Could not copy markdown');
}
}
// View markdown in new tab
function viewMarkdown() {
const githubUrl = getGithubFileUrl();
window.open(githubUrl, '_blank', 'noopener,noreferrer');
}
// Initialize
const { button, dropdown, overlay } = createPageActionsUI();
// Event listeners
button.addEventListener('click', (e) => {
e.stopPropagation();
toggleDropdown(button, dropdown, overlay);
});
overlay.addEventListener('click', () => {
closeDropdown(button, dropdown, overlay);
});
// Copy markdown action
document.getElementById('action-copy-markdown').addEventListener('click', async (e) => {
e.preventDefault();
e.stopPropagation();
await copyMarkdownToClipboard(e.currentTarget);
});
// View markdown action
document.getElementById('action-view-markdown').addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
viewMarkdown();
closeDropdown(button, dropdown, overlay);
});
// Ask AI action (disabled for now)
document.getElementById('action-ask-ai').addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
// Future: Integrate with Ask AI feature
// For now, do nothing (disabled state)
});
// Close on ESC key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && dropdown.classList.contains('active')) {
closeDropdown(button, dropdown, overlay);
}
});
// Close when clicking outside
document.addEventListener('click', (e) => {
if (!dropdown.contains(e.target) && !button.contains(e.target)) {
closeDropdown(button, dropdown, overlay);
}
});
// Prevent dropdown from closing when clicking inside
dropdown.addEventListener('click', (e) => {
// Only stop propagation if not clicking on a link
if (!e.target.closest('.page-action-link')) {
e.stopPropagation();
}
});
// Close dropdown on link click (except for copy which handles itself)
dropdown.querySelectorAll('.page-action-link:not(#action-copy-markdown)').forEach(link => {
link.addEventListener('click', () => {
if (!link.classList.contains('disabled')) {
setTimeout(() => {
closeDropdown(button, dropdown, overlay);
}, 100);
}
});
});
// Handle window resize
let resizeTimer;
window.addEventListener('resize', () => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
// Close dropdown on resize to prevent positioning issues
if (dropdown.classList.contains('active')) {
closeDropdown(button, dropdown, overlay);
}
}, 250);
});
// Accessibility: Focus management
button.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
toggleDropdown(button, dropdown, overlay);
// Focus first menu item when opening
if (dropdown.classList.contains('active')) {
const firstLink = dropdown.querySelector('.page-action-link:not(.disabled)');
if (firstLink) {
setTimeout(() => firstLink.focus(), 100);
}
}
}
});
// Arrow key navigation within menu
dropdown.addEventListener('keydown', (e) => {
if (!dropdown.classList.contains('active')) return;
const links = Array.from(dropdown.querySelectorAll('.page-action-link:not(.disabled)'));
const currentIndex = links.indexOf(document.activeElement);
if (e.key === 'ArrowDown') {
e.preventDefault();
const nextIndex = (currentIndex + 1) % links.length;
links[nextIndex].focus();
} else if (e.key === 'ArrowUp') {
e.preventDefault();
const prevIndex = (currentIndex - 1 + links.length) % links.length;
links[prevIndex].focus();
} else if (e.key === 'Home') {
e.preventDefault();
links[0].focus();
} else if (e.key === 'End') {
e.preventDefault();
links[links.length - 1].focus();
}
});
console.log('Page Actions initialized for:', getCurrentMarkdownPath());
});

1371
docs/md_v2/branding/index.md Normal file

File diff suppressed because it is too large Load Diff