Add initial implementation of the Docsify shell template for interactive software manuals

This commit is contained in:
catlog22
2025-12-28 22:46:43 +08:00
parent 6ffac8810b
commit 32cea006b9
5 changed files with 2188 additions and 13 deletions

View File

@@ -91,6 +91,71 @@
color: hsl(var(--warning-foreground, white));
}
/* ===== Search Box ===== */
.cli-stream-search {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
background: hsl(var(--background));
border: 1px solid hsl(var(--border));
border-radius: 6px;
transition: all 0.2s;
}
.cli-stream-search:focus-within {
border-color: hsl(var(--primary));
box-shadow: 0 0 0 2px hsl(var(--primary) / 0.1);
}
.cli-stream-search-input {
width: 140px;
padding: 2px 4px;
background: transparent;
border: none;
outline: none;
font-size: 0.75rem;
color: hsl(var(--foreground));
}
.cli-stream-search-input::placeholder {
color: hsl(var(--muted-foreground));
}
.cli-stream-search-icon {
width: 14px;
height: 14px;
color: hsl(var(--muted-foreground));
flex-shrink: 0;
}
.cli-stream-search-clear {
display: flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
padding: 0;
background: transparent;
border: none;
border-radius: 50%;
font-size: 0.75rem;
color: hsl(var(--muted-foreground));
cursor: pointer;
opacity: 0;
transition: all 0.15s;
}
.cli-stream-search:focus-within .cli-stream-search-clear,
.cli-stream-search-input:not(:placeholder-shown) + .cli-stream-search-clear {
opacity: 1;
}
.cli-stream-search-clear:hover {
background: hsl(var(--muted));
color: hsl(var(--foreground));
}
.cli-stream-actions {
display: flex;
align-items: center;
@@ -334,6 +399,26 @@
color: hsl(200 80% 70%);
}
/* Search highlight */
.cli-stream-highlight {
background: hsl(50 100% 50% / 0.4);
color: inherit;
padding: 0 2px;
border-radius: 2px;
}
/* Filter result info */
.cli-stream-filter-info {
display: inline-block;
padding: 4px 10px;
margin-bottom: 8px;
background: hsl(var(--primary) / 0.15);
color: hsl(var(--primary));
border-radius: 4px;
font-size: 0.6875rem;
font-weight: 500;
}
/* Auto-scroll indicator */
.cli-stream-scroll-btn {
position: sticky;

View File

@@ -8,6 +8,7 @@ let cliStreamExecutions = {}; // { executionId: { tool, mode, output, status, s
let activeStreamTab = null;
let autoScrollEnabled = true;
let isCliStreamViewerOpen = false;
let searchFilter = ''; // Search filter for output content
const MAX_OUTPUT_LINES = 5000; // Prevent memory issues
@@ -230,9 +231,9 @@ function renderStreamTabs() {
function renderStreamContent(executionId) {
const contentContainer = document.getElementById('cliStreamContent');
if (!contentContainer) return;
const exec = executionId ? cliStreamExecutions[executionId] : null;
if (!exec) {
// Show empty state
contentContainer.innerHTML = `
@@ -245,20 +246,43 @@ function renderStreamContent(executionId) {
if (typeof lucide !== 'undefined') lucide.createIcons();
return;
}
// Check if should auto-scroll
const wasAtBottom = contentContainer.scrollHeight - contentContainer.scrollTop <= contentContainer.clientHeight + 50;
// Render output lines
contentContainer.innerHTML = exec.output.map(line =>
`<div class="cli-stream-line ${line.type}">${escapeHtml(line.content)}</div>`
).join('');
// Filter output lines based on search
let filteredOutput = exec.output;
if (searchFilter.trim()) {
const searchLower = searchFilter.toLowerCase();
filteredOutput = exec.output.filter(line =>
line.content.toLowerCase().includes(searchLower)
);
}
// Render output lines with search highlighting
contentContainer.innerHTML = filteredOutput.map(line => {
let content = escapeHtml(line.content);
// Highlight search matches
if (searchFilter.trim()) {
const searchRegex = new RegExp(`(${escapeRegex(searchFilter)})`, 'gi');
content = content.replace(searchRegex, '<mark class="cli-stream-highlight">$1</mark>');
}
return `<div class="cli-stream-line ${line.type}">${content}</div>`;
}).join('');
// Show filter result count if filtering
if (searchFilter.trim() && filteredOutput.length !== exec.output.length) {
const filterInfo = document.createElement('div');
filterInfo.className = 'cli-stream-filter-info';
filterInfo.textContent = `${filteredOutput.length} / ${exec.output.length} lines`;
contentContainer.insertBefore(filterInfo, contentContainer.firstChild);
}
// Auto-scroll if enabled and was at bottom
if (autoScrollEnabled && wasAtBottom) {
contentContainer.scrollTop = contentContainer.scrollHeight;
}
// Update status bar
renderStreamStatus(executionId);
}
@@ -408,14 +432,14 @@ function handleStreamContentScroll() {
// ===== Helper Functions =====
function formatDuration(ms) {
if (ms < 1000) return `${ms}ms`;
const seconds = Math.floor(ms / 1000);
if (seconds < 60) return `${seconds}s`;
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
if (minutes < 60) return `${minutes}m ${remainingSeconds}s`;
const hours = Math.floor(minutes / 60);
const remainingMinutes = minutes % 60;
return `${hours}h ${remainingMinutes}m`;
@@ -428,6 +452,25 @@ function escapeHtml(text) {
return div.innerHTML;
}
function escapeRegex(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
// ===== Search Functions =====
function handleSearchInput(event) {
searchFilter = event.target.value;
renderStreamContent(activeStreamTab);
}
function clearSearch() {
searchFilter = '';
const searchInput = document.getElementById('cliStreamSearchInput');
if (searchInput) {
searchInput.value = '';
}
renderStreamContent(activeStreamTab);
}
// Translation helper with fallback (uses global t from i18n.js)
function _streamT(key) {
// First try global t() from i18n.js
@@ -459,3 +502,16 @@ if (document.readyState === 'loading') {
} else {
initCliStreamViewer();
}
// ===== Global Exposure =====
window.toggleCliStreamViewer = toggleCliStreamViewer;
window.handleCliStreamStarted = handleCliStreamStarted;
window.handleCliStreamOutput = handleCliStreamOutput;
window.handleCliStreamCompleted = handleCliStreamCompleted;
window.handleCliStreamError = handleCliStreamError;
window.switchStreamTab = switchStreamTab;
window.closeStream = closeStream;
window.clearCompletedStreams = clearCompletedStreams;
window.toggleAutoScroll = toggleAutoScroll;
window.handleSearchInput = handleSearchInput;
window.clearSearch = clearSearch;