WIP: Rewrite export-html with tree sidebar, client-side rendering

- Add tree sidebar with search and filter (Default/All/Labels)
- Client-side markdown/syntax highlighting via vendored marked.js + highlight.js
- Base64 encode session data to avoid escaping issues
- Reuse theme.ts color tokens via getResolvedThemeColors()
- Sticky sidebar, responsive mobile layout with overlay
- Click tree node to scroll to message
- Keyboard shortcuts: Esc to reset, Ctrl/Cmd+F to search
This commit is contained in:
Mario Zechner 2026-01-01 03:36:47 +01:00
parent a073477555
commit 256fa575fb
11 changed files with 3195 additions and 1446 deletions

View file

@ -652,6 +652,91 @@ export function stopThemeWatcher(): void {
}
}
// ============================================================================
// HTML Export Helpers
// ============================================================================
/**
* Convert a 256-color index to hex string.
* Indices 0-15: basic colors (approximate)
* Indices 16-231: 6x6x6 color cube
* Indices 232-255: grayscale ramp
*/
function ansi256ToHex(index: number): string {
// Basic colors (0-15) - approximate common terminal values
const basicColors = [
"#000000",
"#800000",
"#008000",
"#808000",
"#000080",
"#800080",
"#008080",
"#c0c0c0",
"#808080",
"#ff0000",
"#00ff00",
"#ffff00",
"#0000ff",
"#ff00ff",
"#00ffff",
"#ffffff",
];
if (index < 16) {
return basicColors[index];
}
// Color cube (16-231): 6x6x6 = 216 colors
if (index < 232) {
const cubeIndex = index - 16;
const r = Math.floor(cubeIndex / 36);
const g = Math.floor((cubeIndex % 36) / 6);
const b = cubeIndex % 6;
const toHex = (n: number) => (n === 0 ? 0 : 55 + n * 40).toString(16).padStart(2, "0");
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
}
// Grayscale (232-255): 24 shades
const gray = 8 + (index - 232) * 10;
const grayHex = gray.toString(16).padStart(2, "0");
return `#${grayHex}${grayHex}${grayHex}`;
}
/**
* Get resolved theme colors as CSS-compatible hex strings.
* Used by HTML export to generate CSS custom properties.
*/
export function getResolvedThemeColors(themeName?: string): Record<string, string> {
const name = themeName ?? getDefaultTheme();
const isLight = name === "light";
const themeJson = loadThemeJson(name);
const resolved = resolveThemeColors(themeJson.colors, themeJson.vars);
// Default text color for empty values (terminal uses default fg color)
const defaultText = isLight ? "#000000" : "#e5e5e7";
const cssColors: Record<string, string> = {};
for (const [key, value] of Object.entries(resolved)) {
if (typeof value === "number") {
cssColors[key] = ansi256ToHex(value);
} else if (value === "") {
// Empty means default terminal color - use sensible fallback for HTML
cssColors[key] = defaultText;
} else {
cssColors[key] = value;
}
}
return cssColors;
}
/**
* Check if a theme is a "light" theme (for CSS that needs light/dark variants).
*/
export function isLightTheme(themeName?: string): boolean {
// Currently just check the name - could be extended to analyze colors
return themeName === "light";
}
// ============================================================================
// TUI Helpers
// ============================================================================