Files
koboldcpp/tools/ui/src/lib/components/app/content/SyntaxHighlightedCode.svelte
T
Aleksander Grygier bdffafa5df ui: Refactor data-attrs constants, enum for bool strings (#27002)
* refactor: Data-attribute constants + boolean string enum

* refactor: Use CSS class string constants

* refactor: Address review comments
2026-08-13 20:01:12 +02:00

162 lines
4.0 KiB
Svelte

<script lang="ts">
import { browser } from '$app/environment';
import { SYNTAX_CODE_SCROLL_AT_BOTTOM_THRESHOLD_PX, UI_DATA_ATTRS } from '$lib/constants';
import { BooleanString, ColorMode } from '$lib/enums';
import { highlightCode } from '$lib/utils';
import githubLightCss from 'highlight.js/styles/github.css?inline';
import githubDarkCss from 'highlight.js/styles/github-dark.css?inline';
import { mode } from 'mode-watcher';
interface Props {
code: string;
language?: string;
class?: string;
maxHeight?: string;
maxWidth?: string;
/** Auto-scrolls to the bottom of new chunks; pauses on user scroll-up
* until the user returns to the bottom. */
streaming?: boolean;
}
let {
class: className = '',
code,
language = 'text',
maxHeight = '60vh',
maxWidth = '',
streaming = false
}: Props = $props();
const highlightedHtml = $derived(highlightCode(code, language));
let scrollEl = $state<HTMLDivElement>();
let userScrolledUp = $state(false);
let lastScrollTop = 0;
const SCROLL_BOTTOM_THRESHOLD_PX = SYNTAX_CODE_SCROLL_AT_BOTTOM_THRESHOLD_PX;
let pendingFrame: number | null = null;
function loadHighlightTheme(isDark: boolean) {
if (!browser) return;
const existingThemes = document.querySelectorAll(
`style[${UI_DATA_ATTRS.HIGHLIGHT_THEME_PREVIEW}]`
);
existingThemes.forEach((style) => style.remove());
const style = document.createElement('style');
style.setAttribute(UI_DATA_ATTRS.HIGHLIGHT_THEME_PREVIEW, BooleanString.TRUE);
style.textContent = isDark ? githubDarkCss : githubLightCss;
document.head.appendChild(style);
}
function isAtBottom(): boolean {
if (!scrollEl) return false;
return (
scrollEl.scrollHeight - scrollEl.clientHeight - scrollEl.scrollTop <=
SCROLL_BOTTOM_THRESHOLD_PX
);
}
function scrollToBottomOnFrame() {
if (pendingFrame !== null || !scrollEl || userScrolledUp) return;
pendingFrame = requestAnimationFrame(() => {
pendingFrame = null;
// User may scroll between scheduling and paint.
if (scrollEl && !userScrolledUp) {
scrollEl.scrollTop = scrollEl.scrollHeight;
}
});
}
function handleScrollEvent() {
if (!scrollEl) return;
const isScrollingUp = scrollEl.scrollTop < lastScrollTop;
if (isScrollingUp && !isAtBottom()) {
userScrolledUp = true;
} else if (isAtBottom()) {
userScrolledUp = false;
}
lastScrollTop = scrollEl.scrollTop;
}
$effect(() => {
const currentMode = mode.current;
const isDark = currentMode === ColorMode.DARK;
loadHighlightTheme(isDark);
});
// Pin to bottom at the start of each streaming episode.
$effect(() => {
if (streaming) {
userScrolledUp = false;
lastScrollTop = 0;
}
});
$effect(() => {
void code;
if (!streaming || userScrolledUp) return;
scrollToBottomOnFrame();
});
// Layout shifts that don't change `code` (highlight.js re-tokenize, line-wrap reflow).
$effect(() => {
if (!streaming || !scrollEl) return;
const observer = new MutationObserver(() => scrollToBottomOnFrame());
observer.observe(scrollEl, {
characterData: true,
childList: true,
subtree: true
});
return () => observer.disconnect();
});
</script>
<div
bind:this={scrollEl}
onscroll={handleScrollEvent}
class="code-preview-wrapper min-w-0 max-w-full overflow-auto rounded-xl border shadow-[0_1px_2px_0_rgb(0_0_0_/_0.05)] {className}"
style="border-color: color-mix(in oklch, var(--border) 30%, transparent); background: var(--code-background); max-height: {maxHeight}; {maxWidth
? `max-width: ${maxWidth};`
: ''}"
>
<!-- Single line: hljs injection depends on a contiguous source string. -->
<pre class="m-0"><code class="hljs text-sm leading-relaxed">{@html highlightedHtml}</code></pre>
</div>
<style>
.code-preview-wrapper {
overscroll-behavior: contain;
}
.code-preview-wrapper pre {
background: transparent;
padding: 0;
}
.code-preview-wrapper code {
background: transparent;
display: block;
padding: 0.5rem;
}
:global(.dark) .code-preview-wrapper {
border-color: color-mix(in oklch, var(--border) 20%, transparent);
}
</style>