diff --git a/tools/server/public/index.html.gz b/tools/server/public/index.html.gz
index a81bae04d1983..501fa455a2416 100644
Binary files a/tools/server/public/index.html.gz and b/tools/server/public/index.html.gz differ
diff --git a/tools/server/webui/src/lib/components/app/misc/CodePreviewDialog.svelte b/tools/server/webui/src/lib/components/app/misc/CodePreviewDialog.svelte
new file mode 100644
index 0000000000000..702519f9ff971
--- /dev/null
+++ b/tools/server/webui/src/lib/components/app/misc/CodePreviewDialog.svelte
@@ -0,0 +1,93 @@
+
+
+
+
+
+
+
+
+
+
+
+ Close preview
+
+
+
+
+
+
diff --git a/tools/server/webui/src/lib/components/app/misc/MarkdownContent.svelte b/tools/server/webui/src/lib/components/app/misc/MarkdownContent.svelte
index 1f4caa9003bce..1c069db58d830 100644
--- a/tools/server/webui/src/lib/components/app/misc/MarkdownContent.svelte
+++ b/tools/server/webui/src/lib/components/app/misc/MarkdownContent.svelte
@@ -15,6 +15,7 @@
import githubLightCss from 'highlight.js/styles/github.css?inline';
import { mode } from 'mode-watcher';
import { remarkLiteralHtml } from '$lib/markdown/literal-html';
+ import CodePreviewDialog from './CodePreviewDialog.svelte';
interface Props {
content: string;
@@ -25,6 +26,9 @@
let containerRef = $state();
let processedHtml = $state('');
+ let previewDialogOpen = $state(false);
+ let previewCode = $state('');
+ let previewLanguage = $state('text');
function loadHighlightTheme(isDark: boolean) {
if (!browser) return;
@@ -117,7 +121,6 @@
const rawCode = codeElement.textContent || '';
const codeId = `code-${Date.now()}-${index}`;
-
codeElement.setAttribute('data-code-id', codeId);
codeElement.setAttribute('data-raw-code', rawCode);
@@ -138,11 +141,30 @@
copyButton.setAttribute('type', 'button');
copyButton.innerHTML = `
-
- `;
+
+ `;
+
+ const actions = document.createElement('div');
+ actions.className = 'code-block-actions';
+
+ actions.appendChild(copyButton);
+
+ if (language.toLowerCase() === 'html') {
+ const previewButton = document.createElement('button');
+ previewButton.className = 'preview-code-btn';
+ previewButton.setAttribute('data-code-id', codeId);
+ previewButton.setAttribute('title', 'Preview code');
+ previewButton.setAttribute('type', 'button');
+
+ previewButton.innerHTML = `
+
+ `;
+
+ actions.appendChild(previewButton);
+ }
header.appendChild(languageLabel);
- header.appendChild(copyButton);
+ header.appendChild(actions);
wrapper.appendChild(header);
const clonedPre = pre.cloneNode(true) as HTMLElement;
@@ -180,49 +202,105 @@
}
}
- function setupCopyButtons() {
- if (!containerRef) return;
+ function getCodeInfoFromTarget(target: HTMLElement) {
+ const wrapper = target.closest('.code-block-wrapper');
- const copyButtons = containerRef.querySelectorAll('.copy-code-btn');
+ if (!wrapper) {
+ console.error('No wrapper found');
+ return null;
+ }
- for (const button of copyButtons) {
- button.addEventListener('click', async (e) => {
- e.preventDefault();
- e.stopPropagation();
+ const codeElement = wrapper.querySelector('code[data-code-id]');
- const target = e.currentTarget as HTMLButtonElement;
- const codeId = target.getAttribute('data-code-id');
+ if (!codeElement) {
+ console.error('No code element found in wrapper');
+ return null;
+ }
- if (!codeId) {
- console.error('No code ID found on button');
- return;
- }
+ const rawCode = codeElement.getAttribute('data-raw-code');
- // Find the code element within the same wrapper
- const wrapper = target.closest('.code-block-wrapper');
- if (!wrapper) {
- console.error('No wrapper found');
- return;
- }
+ if (rawCode === null) {
+ console.error('No raw code found');
+ return null;
+ }
- const codeElement = wrapper.querySelector('code[data-code-id]');
- if (!codeElement) {
- console.error('No code element found in wrapper');
- return;
- }
+ const languageLabel = wrapper.querySelector('.code-language');
+ const language = languageLabel?.textContent?.trim() || 'text';
- const rawCode = codeElement.getAttribute('data-raw-code');
- if (!rawCode) {
- console.error('No raw code found');
- return;
- }
+ return { rawCode, language };
+ }
- try {
- await copyCodeToClipboard(rawCode);
- } catch (error) {
- console.error('Failed to copy code:', error);
- }
- });
+ async function handleCopyClick(event: Event) {
+ event.preventDefault();
+ event.stopPropagation();
+
+ const target = event.currentTarget as HTMLButtonElement | null;
+
+ if (!target) {
+ return;
+ }
+
+ const info = getCodeInfoFromTarget(target);
+
+ if (!info) {
+ return;
+ }
+
+ try {
+ await copyCodeToClipboard(info.rawCode);
+ } catch (error) {
+ console.error('Failed to copy code:', error);
+ }
+ }
+
+ function handlePreviewClick(event: Event) {
+ event.preventDefault();
+ event.stopPropagation();
+
+ const target = event.currentTarget as HTMLButtonElement | null;
+
+ if (!target) {
+ return;
+ }
+
+ const info = getCodeInfoFromTarget(target);
+
+ if (!info) {
+ return;
+ }
+
+ previewCode = info.rawCode;
+ previewLanguage = info.language;
+ previewDialogOpen = true;
+ }
+
+ function setupCodeBlockActions() {
+ if (!containerRef) return;
+
+ const wrappers = containerRef.querySelectorAll('.code-block-wrapper');
+
+ for (const wrapper of wrappers) {
+ const copyButton = wrapper.querySelector('.copy-code-btn');
+ const previewButton = wrapper.querySelector('.preview-code-btn');
+
+ if (copyButton && copyButton.dataset.listenerBound !== 'true') {
+ copyButton.dataset.listenerBound = 'true';
+ copyButton.addEventListener('click', handleCopyClick);
+ }
+
+ if (previewButton && previewButton.dataset.listenerBound !== 'true') {
+ previewButton.dataset.listenerBound = 'true';
+ previewButton.addEventListener('click', handlePreviewClick);
+ }
+ }
+ }
+
+ function handlePreviewDialogOpenChange(open: boolean) {
+ previewDialogOpen = open;
+
+ if (!open) {
+ previewCode = '';
+ previewLanguage = 'text';
}
}
@@ -243,7 +321,7 @@
$effect(() => {
if (containerRef && processedHtml) {
- setupCopyButtons();
+ setupCodeBlockActions();
}
});
@@ -253,6 +331,13 @@
{@html processedHtml}
+
+