Files
windmill/frontend/src/lib/components/MarkdownCodeBlock.svelte
T
GuilhemandClaude Opus 4.8 a00ee5196b feat: shared tab system, universal markdown code blocks, subtle scrollbars (#10003)
* feat: universal styled markdown code blocks with copy button and subtle scrollbar

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(sessions): use the shared DraggableTabs for the preview tab strip

The session preview tabs were bespoke markup; converge them onto the same
DraggableTabs component the raw-app editor uses, gaining drag-reorder and
keyboard nav. The active tab keeps its breadcrumb/router picker via a new
tabAccessory snippet, and tabs persist their new order.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(sessions): keep the new-tab + button right after the last tab

Add an afterTabs snippet to DraggableTabs that renders inside the scroll row
after the tabs (unlike trailing, which stays pinned outside it), and move the
session preview "+" there so it sits next to the last tab.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(tabs): use the subtle ScrollableX scrollbar for Tabs/TabsV2 headers

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* style(sessions): use bg-surface for the preview tab strip

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* style(tabs): add subtle shadow-sm to the selected DraggableTabs tab

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* style(tabs): drop selected-tab shadow; session strip bg-surface-secondary/50

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* style(sessions): drop persistent bg on preview bar buttons, hover-only

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(scrollbar): share a .scrollbar-subtle utility across tabs and chat

Extract ScrollableX's hover-revealed scrollbar styling into a global
.scrollbar-subtle utility (both axes, size via --wm-scrollbar-size), have
ScrollableX consume it, and apply it to the AI chat message list so the chat
scrollbar matches the tabs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: address review — scope HighlightCode copy button, plaintext unknown fences, Tailwind ScrollableX

- HighlightCode: keep the subtle CopyButton + surface chip behind buttonsOnHover
  so the ~20 non-markdown callers keep the original light copy Button.
- MarkdownCodeBlock: unlabeled/unknown fences render as plaintext instead of
  being mis-colored as TypeScript; added common language aliases (ts/js/py/...)
  so real languages still highlight.
- ScrollableX: replace the custom <style> block with Tailwind overflow classes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* style: make chat and session-sidebar typing dots slightly smaller

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: address auto-review — powershell fence to plaintext + reorder tests

- MarkdownCodeBlock: drop 'powershell' from the sql group so it renders
  plaintext instead of SQL-colored (no powershell highlighter in the map).
- sessionPreviewTabs.test.ts: cover reorder (reorders+persists, ignores
  unknown ids / keeps omitted at end, no-op when unchanged).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: keep scrollbar-hidden on Tabs row as TroubleshootFlowTutorial selector hook

codex-review: removing scrollbar-hidden broke the tutorial's '.border-b.flex
.flex-row.whitespace-nowrap.scrollbar-hidden.mx-auto' selector. The class is
inert on the non-scrolling row (ScrollableX owns the scroll) but is kept as the
tutorial's stable hook.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: preserve raw <pre> content in MarkdownCodeBlock (codex-review)

As the universal pre renderer, MarkdownCodeBlock also handles sanitized raw
HTML <pre>text</pre> from rehypeRaw, where the text is a direct child of <pre>
(no <code>). Fall back to that text child so raw pre content isn't dropped to
an empty block. Kitchen-sink sample gains a raw <pre> case.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 12:01:05 +00:00

150 lines
3.7 KiB
Svelte

<script lang="ts">
import { getAstNode } from 'svelte-exmarkdown'
import HighlightCode from './HighlightCode.svelte'
import {
csharp,
go,
graphql,
javascript,
php,
plaintext,
python,
rust,
shell,
sql,
typescript,
yaml
} from 'svelte-highlight/languages'
import type { ClipboardCopy } from 'lucide-svelte'
import MermaidDisplay from './copilot/chat/script/MermaidDisplay.svelte'
// Shared `pre` renderer for fenced code blocks in svelte-exmarkdown markdown.
// Chat-agnostic: gives every markdown surface the highlighted panel + subtle
// hover copy button + custom horizontal scrollbar. The AI chat wraps this to
// add its Apply button (via the apply props) and mermaid diagrams.
let {
showApplyButton = false,
onApplyCode = undefined,
applyButtonIcon = undefined,
// Render ```mermaid blocks as diagrams. Off by default so untrusted
// markdown surfaces don't execute mermaid — the chat opts in.
renderMermaid = false
}: {
showApplyButton?: boolean
onApplyCode?: () => void
applyButtonIcon?: typeof ClipboardCopy
renderMermaid?: boolean
} = $props()
const astNode = getAstNode()
// Map a fence language to a known highlighter key, or undefined when the fence
// is unlabeled/unrecognized — those render as plaintext rather than being
// mis-colored as TypeScript (this renderer runs on all user markdown).
function getSmartLang(lang: string | undefined) {
switch (lang) {
case 'python':
case 'python3':
case 'py':
return 'python'
case 'deno':
case 'nativets':
case 'bun':
case 'bunnative':
case 'typescript':
case 'ts':
case 'tsx':
return 'typescript'
case 'go':
case 'golang':
return 'go'
case 'shell':
case 'bash':
case 'sh':
case 'zsh':
case 'console':
return 'shell'
case 'frontend':
case 'javascript':
case 'js':
case 'jsx':
case 'mjs':
case 'cjs':
return 'javascript'
case 'graphql':
return 'graphql'
case 'mysql':
case 'snowflake':
case 'bigquery':
case 'oracledb':
case 'postgresql':
case 'postgres':
case 'sql':
return 'sql'
case 'php':
return 'php'
case 'rust':
case 'rs':
return 'rust'
case 'csharp':
case 'cs':
return 'csharp'
case 'ansible':
case 'yaml':
case 'yml':
return 'yaml'
default:
return undefined
}
}
const SMART_LANG_TO_HIGHLIGHT_LANG = {
python: python,
typescript: typescript,
go: go,
shell: shell,
javascript: javascript,
graphql: graphql,
sql: sql,
php: php,
rust: rust,
csharp: csharp,
yaml: yaml
}
// Fenced code is `<pre><code>text</code></pre>` (text nested under <code>).
// Sanitized raw HTML through rehypeRaw is `<pre>text</pre>` — the text is a
// direct child of <pre> with no <code>, so fall back to it or the block would
// render empty now that this renderer handles every <pre>.
let code = $derived(
astNode.current.children?.[0]?.children?.[0]?.value ?? astNode.current.children?.[0]?.value
)
let language = $derived(
(astNode.current.children?.[0]?.properties?.class as string | undefined)?.split('-')[1]
)
let smartLang = $derived(getSmartLang(language))
let highlightLanguage = $derived(smartLang ? SMART_LANG_TO_HIGHLIGHT_LANG[smartLang] : plaintext)
</script>
<div class="flex flex-col gap-0.5 rounded-md relative not-prose !text-xs">
<div class="relative w-full bg-surface-secondary rounded-md overflow-hidden">
{#if renderMermaid && language === 'mermaid'}
<MermaidDisplay code={code ?? ''} />
{:else}
<HighlightCode
className="px-3 py-2.5 !text-xs"
code={code ?? ''}
{highlightLanguage}
language={undefined}
{onApplyCode}
{showApplyButton}
{applyButtonIcon}
buttonsOnHover
customScrollbarX
/>
{/if}
</div>
</div>