Code Block
Syntax-highlighted code with header, copy, language select and line numbers. The canonical code block for the kit.
const w = { NVDAx: 0.32, MSFTx: 0.28 }import { rebalance } from "@/lib/keeper"
export async function run(index: string) { // only when drift is above the limit const drift = await getDrift(index) if (drift < 0.02) return return rebalance(index, { slippage: 0.01 })}const w = { NVDAx: 0.32, MSFTx: 0.28 }import { rebalance } from "@/lib/keeper"
export async function run(index: string) { // only when drift is above the limit const drift = await getDrift(index) if (drift < 0.02) return return rebalance(index, { slippage: 0.01 })}const w = { NVDAx: 0.32, MSFTx: 0.28 }import { CodeBlock, CodeBlockActions, CodeBlockCopyButton, CodeBlockFilename, CodeBlockHeader, CodeBlockLanguageSelector, CodeBlockLanguageSelectorContent, CodeBlockLanguageSelectorItem, CodeBlockLanguageSelectorTrigger, CodeBlockLanguageSelectorValue, CodeBlockTitle,} from "@/components/ai/code-block";import { useState } from "react";import { IconPlaceholder } from "@/edmi/icon-placeholder";
const code = `import { rebalance } from "@/lib/keeper"
export async function run(index: string) { // only when drift is above the limit const drift = await getDrift(index) if (drift < 0.02) return return rebalance(index, { slippage: 0.01 })}`;
const languages = [ { value: "typescript", label: "TypeScript" }, { value: "javascript", label: "JavaScript" }, { value: "json", label: "JSON" },];
export default function Demo() { const [language, setLanguage] = useState("typescript");
return ( <div className="flex w-full max-w-xl flex-col gap-6"> <div className="flex flex-col gap-2"> <span className="font-mono text-[10.5px] tracking-wide text-muted-foreground uppercase"> With header + line numbers </span> <CodeBlock code={code} language={language as "typescript"} showLineNumbers > <CodeBlockHeader> <CodeBlockTitle> <IconPlaceholder lucide="FileCodeIcon" tabler="IconFileCode" hugeicons="File01Icon" phosphor="FileCodeIcon" remixicon="RiFileCodeLine" className="size-3.5" /> <CodeBlockFilename>keeper.ts</CodeBlockFilename> </CodeBlockTitle> <CodeBlockActions> <CodeBlockLanguageSelector items={languages} onValueChange={(v) => v && setLanguage(String(v))} value={language} > <CodeBlockLanguageSelectorTrigger> <CodeBlockLanguageSelectorValue /> </CodeBlockLanguageSelectorTrigger> <CodeBlockLanguageSelectorContent> {languages.map((l) => ( <CodeBlockLanguageSelectorItem key={l.value} value={l.value} > {l.label} </CodeBlockLanguageSelectorItem> ))} </CodeBlockLanguageSelectorContent> </CodeBlockLanguageSelector> <CodeBlockCopyButton /> </CodeBlockActions> </CodeBlockHeader> </CodeBlock> </div> <div className="flex flex-col gap-2"> <span className="font-mono text-[10.5px] tracking-wide text-muted-foreground uppercase"> No header </span> <CodeBlock className="max-w-md" code="const w = { NVDAx: 0.32, MSFTx: 0.28 }" language="typescript" /> </div> </div> );}<script setup lang="ts">import { FileCodeIcon } from "@lucide/vue";import { ref } from "vue";import { CodeBlock, CodeBlockActions, CodeBlockCopyButton, CodeBlockFilename, CodeBlockHeader, CodeBlockLanguageSelector, CodeBlockLanguageSelectorContent, CodeBlockLanguageSelectorItem, CodeBlockLanguageSelectorTrigger, CodeBlockLanguageSelectorValue, CodeBlockTitle,} from "@/components/ai/code-block";
const code = `import { rebalance } from "@/lib/keeper"
export async function run(index: string) { // only when drift is above the limit const drift = await getDrift(index) if (drift < 0.02) return return rebalance(index, { slippage: 0.01 })}`;
const languages = [ { value: "typescript", label: "TypeScript" }, { value: "javascript", label: "JavaScript" }, { value: "json", label: "JSON" },] as const;
const language = ref<"typescript" | "javascript" | "json">("typescript");</script>
<template> <div class="flex w-full max-w-xl flex-col gap-6"> <div class="flex flex-col gap-2"> <span class="font-mono text-[10.5px] tracking-wide text-muted-foreground uppercase">With header + line numbers</span> <CodeBlock :code="code" :language="language" show-line-numbers> <CodeBlockHeader> <CodeBlockTitle> <FileCodeIcon class="size-3.5" /> <CodeBlockFilename>keeper.ts</CodeBlockFilename> </CodeBlockTitle> <CodeBlockActions> <CodeBlockLanguageSelector v-model="language"> <CodeBlockLanguageSelectorTrigger> <CodeBlockLanguageSelectorValue /> </CodeBlockLanguageSelectorTrigger> <CodeBlockLanguageSelectorContent> <CodeBlockLanguageSelectorItem v-for="l in languages" :key="l.value" :value="l.value"> {{ l.label }} </CodeBlockLanguageSelectorItem> </CodeBlockLanguageSelectorContent> </CodeBlockLanguageSelector> <CodeBlockCopyButton /> </CodeBlockActions> </CodeBlockHeader> </CodeBlock> </div> <div class="flex flex-col gap-2"> <span class="font-mono text-[10.5px] tracking-wide text-muted-foreground uppercase">No header</span> <CodeBlock class="max-w-md" code="const w = { NVDAx: 0.32, MSFTx: 0.28 }" language="typescript" /> </div> </div></template><script lang="ts"> import IconPlaceholder from "$lib/components/icon-placeholder/icon-placeholder.svelte"; import { CodeBlock, CodeBlockActions, CodeBlockCopyButton, CodeBlockFilename, CodeBlockHeader, CodeBlockLanguageSelector, CodeBlockLanguageSelectorContent, CodeBlockLanguageSelectorItem, CodeBlockLanguageSelectorTrigger, CodeBlockLanguageSelectorValue, CodeBlockTitle, } from "$lib/components/ai/code-block";
const code = `import { rebalance } from "@/lib/keeper"
export async function run(index: string) { // only when drift is above the limit const drift = await getDrift(index) if (drift < 0.02) return return rebalance(index, { slippage: 0.01 })}`;
const languages = [ { value: "typescript", label: "TypeScript" }, { value: "javascript", label: "JavaScript" }, { value: "json", label: "JSON" }, ] as const;
let language = $state<"typescript" | "javascript" | "json">("typescript"); const label = $derived(languages.find((l) => l.value === language)?.label);</script>
<div class="flex w-full max-w-xl flex-col gap-6"> <div class="flex flex-col gap-2"> <span class="font-mono text-[10.5px] tracking-wide text-muted-foreground uppercase" >With header + line numbers</span > <CodeBlock {code} {language} showLineNumbers> <CodeBlockHeader> <CodeBlockTitle> <IconPlaceholder lucide="FileCodeIcon" tabler="IconFileCode" hugeicons="File01Icon" phosphor="FileCodeIcon" remixicon="RiFileCodeLine" class="size-3.5" /> <CodeBlockFilename>keeper.ts</CodeBlockFilename> </CodeBlockTitle> <CodeBlockActions> <CodeBlockLanguageSelector type="single" bind:value={language}> <CodeBlockLanguageSelectorTrigger> <CodeBlockLanguageSelectorValue>{label}</CodeBlockLanguageSelectorValue> </CodeBlockLanguageSelectorTrigger> <CodeBlockLanguageSelectorContent align="end"> {#each languages as l (l.value)} <CodeBlockLanguageSelectorItem value={l.value} label={l.label} >{l.label}</CodeBlockLanguageSelectorItem > {/each} </CodeBlockLanguageSelectorContent> </CodeBlockLanguageSelector> <CodeBlockCopyButton /> </CodeBlockActions> </CodeBlockHeader> </CodeBlock> </div> <div class="flex flex-col gap-2"> <span class="font-mono text-[10.5px] tracking-wide text-muted-foreground uppercase">No header</span> <CodeBlock class="max-w-md" code="const w = { NVDAx: 0.32, MSFTx: 0.28 }" language="typescript" /> </div></div>The canonical code block of the kit: the body sits on card, the header on muted. Built on button and select; highlighting uses Shiki’s CSS-variables theme mapped onto chart tokens (keyword chart-2, string success-text, number chart-3, function chart-4, comment muted-foreground), so it follows mode, base and theme.
Installation
Section titled “Installation”npx shadcn@latest add @edmi-ui/ai-code-blockpnpm dlx shadcn@latest add @edmi-ui/ai-code-blockyarn dlx shadcn@latest add @edmi-ui/ai-code-blockbunx shadcn@latest add @edmi-ui/ai-code-blockPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install shikiTerminal window pnpm add shikiTerminal window yarn add shikiTerminal window bun add shikiAdd the Edmi components this one builds on:
button,select.Copy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-vue@latest add @edmi-ui/ai-code-blockpnpm dlx shadcn-vue@latest add @edmi-ui/ai-code-blockyarn dlx shadcn-vue@latest add @edmi-ui/ai-code-blockbunx shadcn-vue@latest add @edmi-ui/ai-code-blockPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install @lucide/vue @vueuse/core reka-ui shikiTerminal window pnpm add @lucide/vue @vueuse/core reka-ui shikiTerminal window yarn add @lucide/vue @vueuse/core reka-ui shikiTerminal window bun add @lucide/vue @vueuse/core reka-ui shikiAdd the Edmi components this one builds on:
button,select.Copy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-code-block.jsonpnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-code-block.jsonyarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-code-block.jsonbunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-code-block.jsonPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Add the Edmi components this one builds on:
button,select.Copy the code from Component source into your project and update the import paths to match your aliases.
import { CodeBlock } from "@/components/ai/code-block";
<div className="flex w-full max-w-xl flex-col gap-6"> <div className="flex flex-col gap-2"> <span className="font-mono text-[10.5px] tracking-wide text-muted-foreground uppercase"> With header + line numbers </span> {/* … */} </div> <div className="flex flex-col gap-2"> <span className="font-mono text-[10.5px] tracking-wide text-muted-foreground uppercase"> No header </span> <CodeBlock className="max-w-md" code="const w = { NVDAx: 0.32, MSFTx: 0.28 }" language="typescript" /> </div></div><script setup lang="ts">import { CodeBlock, CodeBlockActions, CodeBlockCopyButton, CodeBlockFilename, CodeBlockHeader, CodeBlockLanguageSelector, CodeBlockLanguageSelectorContent, CodeBlockLanguageSelectorItem, CodeBlockLanguageSelectorTrigger, CodeBlockLanguageSelectorValue, CodeBlockTitle,} from "@/components/ai/code-block";</script>
<template> <div class="flex w-full max-w-xl flex-col gap-6"> <div class="flex flex-col gap-2"> <span class="font-mono text-[10.5px] tracking-wide text-muted-foreground uppercase">With header + line numbers</span> <CodeBlock show-line-numbers> <CodeBlockHeader> <CodeBlockTitle> <FileCodeIcon class="size-3.5" /> <CodeBlockFilename>keeper.ts</CodeBlockFilename> </CodeBlockTitle> <CodeBlockActions> <CodeBlockLanguageSelector> <CodeBlockLanguageSelectorTrigger> <CodeBlockLanguageSelectorValue /> </CodeBlockLanguageSelectorTrigger> <CodeBlockLanguageSelectorContent> <CodeBlockLanguageSelectorItem :key="l.value" :value="l.value"> {{ l.label }} </CodeBlockLanguageSelectorItem> </CodeBlockLanguageSelectorContent> </CodeBlockLanguageSelector> <CodeBlockCopyButton /> </CodeBlockActions> </CodeBlockHeader> </CodeBlock> </div> <!-- … --> </div></template><script lang="ts"> import { CodeBlock } from "$lib/components/ai/code-block";</script>
<div class="flex w-full max-w-xl flex-col gap-6"> <div class="flex flex-col gap-2"> <span class="font-mono text-[10.5px] tracking-wide text-muted-foreground uppercase" >With header + line numbers</span > <!-- … --> </div> <div class="flex flex-col gap-2"> <span class="font-mono text-[10.5px] tracking-wide text-muted-foreground uppercase">No header</span> <CodeBlock class="max-w-md" code="const w = { NVDAx: 0.32, MSFTx: 0.28 }" language="typescript" /> </div></div>Component source
Section titled “Component source”"use client";
import { cn } from "cn";import type { ComponentProps, CSSProperties, HTMLAttributes } from "react";import { createContext, memo, useCallback, useContext, useEffect, useMemo, useRef, useState,} from "react";import type { BundledLanguage, HighlighterGeneric, ThemedToken, ThemeRegistration,} from "shiki";import { createCssVariablesTheme, createHighlighter } from "shiki";import { CheckIcon, CopyIcon } from "@phosphor-icons/react";import { Button } from "@/registry/edmi/ui/button";import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from "@/registry/edmi/ui/select";
// The canonical code block of the kit (DESIGN §5b): body on --card, header on --muted,// highlight colours come from chart tokens so they follow mode, base and theme.// Shiki's `css-variables` theme emits `var(--shiki-token-*)`; the container maps those onto tokens.
const SHIKI_TOKEN_VARS = cn( "[--shiki-foreground:var(--foreground)] [--shiki-background:transparent]", "[--shiki-token-keyword:var(--chart-2)] [--shiki-token-string:var(--success-text)]", "[--shiki-token-string-expression:var(--success-text)] [--shiki-token-constant:var(--chart-3)]", "[--shiki-token-function:var(--chart-4)] [--shiki-token-comment:var(--muted-foreground)]", "[--shiki-token-parameter:var(--foreground)] [--shiki-token-punctuation:var(--foreground)]", "[--shiki-token-link:var(--info-text)]",);
const THEME_NAME = "edmi-css-variables";const edmiTheme: ThemeRegistration = createCssVariablesTheme({ name: THEME_NAME, variablePrefix: "--shiki-", fontStyle: true,});
// Shiki font styles are bitflags: 1 = italic, 2 = bold, 4 = underline.const isItalic = (fontStyle: number | undefined) => fontStyle && fontStyle & 1;const isBold = (fontStyle: number | undefined) => fontStyle && fontStyle & 2;const isUnderline = (fontStyle: number | undefined) => fontStyle && fontStyle & 4;
interface KeyedToken { token: ThemedToken; key: string;}interface KeyedLine { tokens: KeyedToken[]; key: string;}
const addKeysToTokens = (lines: ThemedToken[][]): KeyedLine[] => lines.map((line, lineIdx) => ({ key: `line-${lineIdx}`, tokens: line.map((token, tokenIdx) => ({ key: `line-${lineIdx}-${tokenIdx}`, token, })), }));
const TokenSpan = ({ token }: { token: ThemedToken }) => ( <span style={ { color: token.color, fontStyle: isItalic(token.fontStyle) ? "italic" : undefined, fontWeight: isBold(token.fontStyle) ? "bold" : undefined, textDecoration: isUnderline(token.fontStyle) ? "underline" : undefined, ...token.htmlStyle, } as CSSProperties } > {token.content} </span>);
// Line numbers use CSS counters: 38px gutter, number right-aligned 14px from the code (board AI 05).const LINE_NUMBER_CLASSES = cn( "block", "before:inline-block before:w-[38px] before:pr-3.5 before:text-right", "before:content-[counter(line)] before:[counter-increment:line]", "before:font-mono before:text-muted-foreground-2 before:select-none",);
const LineSpan = ({ keyedLine, showLineNumbers,}: { keyedLine: KeyedLine; showLineNumbers: boolean;}) => ( <span className={showLineNumbers ? LINE_NUMBER_CLASSES : "block"}> {keyedLine.tokens.length === 0 ? "\n" : keyedLine.tokens.map(({ token, key }) => ( <TokenSpan key={key} token={token} /> ))} </span>);
type CodeBlockProps = HTMLAttributes<HTMLDivElement> & { code: string; language: BundledLanguage; showLineNumbers?: boolean;};
interface TokenizedCode { tokens: ThemedToken[][]; fg: string; bg: string;}
interface CodeBlockContextType { code: string;}
const CodeBlockContext = createContext<CodeBlockContextType>({ code: "" });
// Highlighter cache (singleton per language), token cache and async subscribers.const highlighterCache = new Map< string, Promise<HighlighterGeneric<BundledLanguage, string>>>();const tokensCache = new Map<string, TokenizedCode>();const subscribers = new Map<string, Set<(result: TokenizedCode) => void>>();
const getTokensCacheKey = (code: string, language: BundledLanguage) => { const start = code.slice(0, 100); const end = code.length > 100 ? code.slice(-100) : ""; return `${language}:${code.length}:${start}:${end}`;};
const getHighlighter = (language: BundledLanguage) => { const cached = highlighterCache.get(language); if (cached) { return cached; } const promise = createHighlighter({ langs: [language], themes: [edmiTheme], }) as Promise<HighlighterGeneric<BundledLanguage, string>>; highlighterCache.set(language, promise); return promise;};
// Plain tokens for immediate display while highlighting loads.const createRawTokens = (code: string): TokenizedCode => ({ bg: "transparent", fg: "inherit", tokens: code.split("\n").map((line) => line === "" ? [] : [ { color: "inherit", content: line, } as ThemedToken, ], ),});
/** Tokenizes `code`; returns the cached result synchronously or `null` and calls `callback` once ready. */export const highlightCode = ( code: string, language: BundledLanguage, callback?: (result: TokenizedCode) => void,): TokenizedCode | null => { const key = getTokensCacheKey(code, language); const cached = tokensCache.get(key); if (cached) { return cached; }
if (callback) { if (!subscribers.has(key)) { subscribers.set(key, new Set()); } subscribers.get(key)?.add(callback); }
getHighlighter(language) .then((highlighter) => { const loaded = highlighter.getLoadedLanguages(); const lang = loaded.includes(language) ? language : "text"; const result = highlighter.codeToTokens(code, { lang, theme: THEME_NAME, }); const tokenized: TokenizedCode = { bg: result.bg ?? "transparent", fg: result.fg ?? "inherit", tokens: result.tokens, }; tokensCache.set(key, tokenized); const subs = subscribers.get(key); if (subs) { for (const sub of subs) { sub(tokenized); } subscribers.delete(key); } }) .catch((error) => { console.error("Failed to highlight code:", error); subscribers.delete(key); });
return null;};
const CodeBlockBody = memo( ({ tokenized, showLineNumbers, className, }: { tokenized: TokenizedCode; showLineNumbers: boolean; className?: string; }) => { const keyedLines = useMemo( () => addKeysToTokens(tokenized.tokens), [tokenized.tokens], );
return ( <pre className={cn( "m-0 py-2.5 font-mono text-[12.5px] leading-[1.7]", showLineNumbers ? "pr-4" : "px-4", className, )} > <code className={cn( "font-mono", showLineNumbers && "[counter-increment:line_0] [counter-reset:line]", )} > {keyedLines.map((keyedLine) => ( <LineSpan key={keyedLine.key} keyedLine={keyedLine} showLineNumbers={showLineNumbers} /> ))} </code> </pre> ); }, (prev, next) => prev.tokenized === next.tokenized && prev.showLineNumbers === next.showLineNumbers && prev.className === next.className,);
CodeBlockBody.displayName = "CodeBlockBody";
export const CodeBlockContainer = ({ className, language, style, ...props}: HTMLAttributes<HTMLDivElement> & { language: string }) => ( <div data-slot="ai-code-block" className={cn( "group relative w-full overflow-hidden rounded-xl border border-border bg-card text-foreground", SHIKI_TOKEN_VARS, className, )} data-language={language} style={{ containIntrinsicSize: "auto 200px", contentVisibility: "auto", ...style, }} {...props} />);
export const CodeBlockHeader = ({ children, className, ...props}: HTMLAttributes<HTMLDivElement>) => ( <div data-slot="ai-code-block-header" className={cn( "flex h-[38px] items-center justify-between gap-2 border-b border-border bg-muted pr-1.5 pl-3 text-[12.5px] text-muted-foreground", className, )} {...props} > {children} </div>);
export const CodeBlockTitle = ({ children, className, ...props}: HTMLAttributes<HTMLDivElement>) => ( <div className={cn("flex items-center gap-2", className)} {...props}> {children} </div>);
export const CodeBlockFilename = ({ children, className, ...props}: HTMLAttributes<HTMLSpanElement>) => ( <span className={cn("font-mono text-xs", className)} {...props}> {children} </span>);
export const CodeBlockActions = ({ children, className, ...props}: HTMLAttributes<HTMLDivElement>) => ( <div className={cn("flex items-center gap-1", className)} {...props}> {children} </div>);
export const CodeBlockContent = ({ code, language, showLineNumbers = false,}: { code: string; language: BundledLanguage; showLineNumbers?: boolean;}) => { const rawTokens = useMemo(() => createRawTokens(code), [code]); const syncTokens = useMemo( () => highlightCode(code, language) ?? rawTokens, [code, language, rawTokens], );
const [asyncTokens, setAsyncTokens] = useState<TokenizedCode | null>(null); const asyncKeyRef = useRef({ code, language });
// Invalidate stale async tokens synchronously during render. if ( asyncKeyRef.current.code !== code || asyncKeyRef.current.language !== language ) { asyncKeyRef.current = { code, language }; setAsyncTokens(null); }
useEffect(() => { let cancelled = false; highlightCode(code, language, (result) => { if (!cancelled) { setAsyncTokens(result); } }); return () => { cancelled = true; }; }, [code, language]);
// Server and hydration render the plain tokens; the (possibly cached) highlighted ones only after mount. const [mounted, setMounted] = useState(false); useEffect(() => setMounted(true), []); const tokenized = asyncTokens ?? (mounted ? syncTokens : rawTokens);
return ( <div className="relative overflow-auto"> <CodeBlockBody showLineNumbers={showLineNumbers} tokenized={tokenized} /> </div> );};
export const CodeBlock = ({ code, language, showLineNumbers = false, className, children, ...props}: CodeBlockProps) => { const contextValue = useMemo(() => ({ code }), [code]);
return ( <CodeBlockContext.Provider value={contextValue}> <CodeBlockContainer className={className} language={language} {...props}> {children} <CodeBlockContent code={code} language={language} showLineNumbers={showLineNumbers} /> </CodeBlockContainer> </CodeBlockContext.Provider> );};
export type CodeBlockCopyButtonProps = ComponentProps<typeof Button> & { onCopy?: () => void; onError?: (error: Error) => void; timeout?: number;};
export const CodeBlockCopyButton = ({ onCopy, onError, timeout = 2000, children, className, ...props}: CodeBlockCopyButtonProps) => { const [isCopied, setIsCopied] = useState(false); const timeoutRef = useRef<number>(0); const { code } = useContext(CodeBlockContext);
const copyToClipboard = useCallback(async () => { if (typeof window === "undefined" || !navigator?.clipboard?.writeText) { onError?.(new Error("Clipboard API not available")); return; }
try { if (!isCopied) { await navigator.clipboard.writeText(code); setIsCopied(true); onCopy?.(); timeoutRef.current = window.setTimeout( () => setIsCopied(false), timeout, ); } } catch (error) { onError?.(error as Error); } }, [code, onCopy, onError, timeout, isCopied]);
useEffect( () => () => { window.clearTimeout(timeoutRef.current); }, [], );
return ( <Button aria-label="Copy code" className={cn("shrink-0", className)} onClick={copyToClipboard} size="icon-xs" variant="ghost" {...props} > {children ?? (isCopied ? ( <CheckIcon /> ) : ( <CopyIcon /> ))} </Button> );};
export type CodeBlockLanguageSelectorProps = ComponentProps<typeof Select>;
export const CodeBlockLanguageSelector = ( props: CodeBlockLanguageSelectorProps,) => <Select {...props} />;
export type CodeBlockLanguageSelectorTriggerProps = ComponentProps< typeof SelectTrigger>;
/** Ghost trigger (board: a ghost xs button with a chevron). */export const CodeBlockLanguageSelectorTrigger = ({ className, ...props}: CodeBlockLanguageSelectorTriggerProps) => ( <SelectTrigger className={cn( "h-6 gap-1 rounded-md border-transparent data-[size=sm]:h-6 data-[size=sm]:rounded-md bg-transparent px-2 text-xs font-normal text-foreground shadow-none hover:bg-accent", className, )} size="sm" {...props} />);
export type CodeBlockLanguageSelectorValueProps = ComponentProps< typeof SelectValue>;
export const CodeBlockLanguageSelectorValue = ( props: CodeBlockLanguageSelectorValueProps,) => <SelectValue {...props} />;
export type CodeBlockLanguageSelectorContentProps = ComponentProps< typeof SelectContent>;
export const CodeBlockLanguageSelectorContent = ({ align = "end", ...props}: CodeBlockLanguageSelectorContentProps) => ( <SelectContent align={align} {...props} />);
export type CodeBlockLanguageSelectorItemProps = ComponentProps< typeof SelectItem>;
export const CodeBlockLanguageSelectorItem = ( props: CodeBlockLanguageSelectorItemProps,) => <SelectItem {...props} />;<script setup lang="ts">import type { BundledLanguage } from "shiki"import type { HTMLAttributes } from "vue"import { computed, provide } from "vue"import CodeBlockContainer from "./CodeBlockContainer.vue"import CodeBlockContent from "./CodeBlockContent.vue"import { CodeBlockKey } from "./context"
const props = withDefaults( defineProps<{ code: string language: BundledLanguage showLineNumbers?: boolean class?: HTMLAttributes["class"] }>(), { showLineNumbers: false, },)
provide(CodeBlockKey, { code: computed(() => props.code),})</script>
<template> <CodeBlockContainer :class="props.class" :language="props.language"> <slot /> <CodeBlockContent :code="props.code" :language="props.language" :show-line-numbers="props.showLineNumbers" /> </CodeBlockContainer></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()</script>
<template> <div :class="cn('flex items-center gap-1', props.class)"> <slot /> </div></template><script setup lang="ts">import type { CSSProperties, HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"
// The canonical code block of the kit (DESIGN 5b): body on --card, header on --muted.// Highlight colours come from chart tokens through Shiki's css-variables theme.const SHIKI_TOKEN_VARS = cn( "[--shiki-foreground:var(--foreground)] [--shiki-background:transparent]", "[--shiki-token-keyword:var(--chart-2)] [--shiki-token-string:var(--success-text)]", "[--shiki-token-string-expression:var(--success-text)] [--shiki-token-constant:var(--chart-3)]", "[--shiki-token-function:var(--chart-4)] [--shiki-token-comment:var(--muted-foreground)]", "[--shiki-token-parameter:var(--foreground)] [--shiki-token-punctuation:var(--foreground)]", "[--shiki-token-link:var(--info-text)]",)
const props = defineProps<{ class?: HTMLAttributes["class"] language: string}>()
const style: CSSProperties = { contentVisibility: "auto", containIntrinsicSize: "auto 200px",}</script>
<template> <div data-slot="ai-code-block" :class="cn( 'group relative w-full overflow-hidden rounded-xl border border-border bg-card text-foreground', SHIKI_TOKEN_VARS, props.class, )" :data-language="props.language" :style="style" > <slot /> </div></template><script setup lang="ts">import type { BundledLanguage } from "shiki"import type { TokenizedCode } from "./utils"import { computed, ref, watch } from "vue"import { cn } from "@/registry/edmi/lib/utils"import { createRawTokens, highlightCode, isBold, isItalic, isUnderline } from "./utils"
const props = withDefaults( defineProps<{ code: string language: BundledLanguage showLineNumbers?: boolean }>(), { showLineNumbers: false, },)
const rawTokens = computed(() => createRawTokens(props.code))// Cached result when there is one, raw tokens until highlighting finishes.const tokenized = ref<TokenizedCode>(highlightCode(props.code, props.language) ?? rawTokens.value)
watch( () => [props.code, props.language], () => { tokenized.value = highlightCode(props.code, props.language) ?? rawTokens.value highlightCode(props.code, props.language, (result) => { tokenized.value = result }) }, { immediate: true },)
const keyedLines = computed(() => tokenized.value.tokens.map((line, lineIdx) => ({ key: `line-${lineIdx}`, tokens: line.map((token, tokenIdx) => ({ token, key: `line-${lineIdx}-${tokenIdx}`, })), })),)
// Line numbers use CSS counters: 38px gutter, number right-aligned 14px from the code (board AI 05).const lineNumberClasses = cn( "block", "before:inline-block before:w-[38px] before:pr-3.5 before:text-right", "before:content-[counter(line)] before:[counter-increment:line]", "before:font-mono before:text-muted-foreground-2 before:select-none",)</script>
<template> <div class="relative overflow-auto"> <pre :class="cn('m-0 py-2.5 font-mono text-[12.5px] leading-[1.7]', showLineNumbers ? 'pr-4' : 'px-4')" ><code :class="cn( 'font-mono', showLineNumbers && '[counter-increment:line_0] [counter-reset:line]', )" ><template v-for="line in keyedLines" :key="line.key"><span :class="showLineNumbers ? lineNumberClasses : 'block'"><template v-if="line.tokens.length === 0">{{ "\n" }}</template><template v-else><span v-for="tokenObj in line.tokens" :key="tokenObj.key" :style="{ color: tokenObj.token.color, ...tokenObj.token.htmlStyle, fontStyle: isItalic(tokenObj.token.fontStyle) ? 'italic' : undefined, fontWeight: isBold(tokenObj.token.fontStyle) ? 'bold' : undefined, textDecoration: isUnderline(tokenObj.token.fontStyle) ? 'underline' : undefined, }" >{{ tokenObj.token.content }}</span></template></span></template></code></pre> </div></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { CheckIcon, CopyIcon } from "@lucide/vue"import { onBeforeUnmount, ref } from "vue"import { cn } from "@/registry/edmi/lib/utils"import { Button } from "@/registry/edmi/ui/button"import { useCodeBlockContext } from "./context"
const props = withDefaults(defineProps<{ timeout?: number class?: HTMLAttributes["class"]}>(), { timeout: 2000,})
const emit = defineEmits<{ (event: "copy"): void (event: "error", error: Error): void}>()
const { code } = useCodeBlockContext()
const isCopied = ref(false)let resetTimer: ReturnType<typeof setTimeout> | undefined
async function copyToClipboard() { if (typeof window === "undefined" || !navigator?.clipboard?.writeText) { emit("error", new Error("Clipboard API not available")) return }
try { await navigator.clipboard.writeText(code.value) isCopied.value = true emit("copy")
if (resetTimer) { clearTimeout(resetTimer) } resetTimer = setTimeout(() => { isCopied.value = false }, props.timeout) } catch (error) { emit("error", error instanceof Error ? error : new Error("Copy failed")) }}
onBeforeUnmount(() => { if (resetTimer) { clearTimeout(resetTimer) }})</script>
<template> <Button data-slot="ai-code-block-copy" aria-label="Copy code" :class="cn('shrink-0', props.class)" size="icon-xs" type="button" variant="ghost" @click="copyToClipboard" > <slot> <CheckIcon v-if="isCopied" /> <CopyIcon v-else /> </slot> </Button></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()</script>
<template> <span :class="cn('font-mono text-xs', props.class)"> <slot /> </span></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()</script>
<template> <div data-slot="ai-code-block-header" :class="cn( 'flex h-[38px] items-center justify-between gap-2 border-b border-border bg-muted pr-1.5 pl-3 text-[12.5px] text-muted-foreground', props.class, )" > <slot /> </div></template><script setup lang="ts">import type { SelectRootEmits, SelectRootProps } from "reka-ui"import { useForwardPropsEmits } from "reka-ui"import { Select } from "@/registry/edmi/ui/select"
const props = defineProps<SelectRootProps>()const emits = defineEmits<SelectRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)</script>
<template> <Select v-bind="forwarded"> <slot /> </Select></template><script setup lang="ts">import type { SelectContentEmits, SelectContentProps } from "reka-ui"import type { HTMLAttributes } from "vue"import { reactiveOmit } from "@vueuse/core"import { useForwardPropsEmits } from "reka-ui"import { SelectContent } from "@/registry/edmi/ui/select"
const props = withDefaults( defineProps<SelectContentProps & { class?: HTMLAttributes["class"] }>(), { align: "end" },)const emits = defineEmits<SelectContentEmits>()
const delegatedProps = reactiveOmit(props, "class")const forwarded = useForwardPropsEmits(delegatedProps, emits)</script>
<template> <SelectContent v-bind="forwarded" :class="props.class"> <slot /> </SelectContent></template><script setup lang="ts">import type { SelectItemProps } from "reka-ui"import type { HTMLAttributes } from "vue"import { SelectItem } from "@/registry/edmi/ui/select"
const props = defineProps<SelectItemProps & { class?: HTMLAttributes["class"] }>()</script>
<template> <SelectItem v-bind="props"> <slot /> </SelectItem></template><script setup lang="ts">import type { SelectTriggerProps } from "reka-ui"import type { HTMLAttributes } from "vue"import { reactiveOmit } from "@vueuse/core"import { useForwardProps } from "reka-ui"import { cn } from "@/registry/edmi/lib/utils"import { SelectTrigger } from "@/registry/edmi/ui/select"
// Ghost trigger (board: a ghost xs button with a chevron).const props = defineProps<SelectTriggerProps & { class?: HTMLAttributes["class"] }>()const delegatedProps = reactiveOmit(props, "class")const forwardedProps = useForwardProps(delegatedProps)</script>
<template> <SelectTrigger v-bind="forwardedProps" :class="cn( 'h-6 gap-1 rounded-md border-transparent bg-transparent px-2 text-xs font-normal text-foreground shadow-none hover:bg-accent data-[size=sm]:h-6 data-[size=sm]:rounded-md', props.class, )" size="sm" > <slot /> </SelectTrigger></template><script setup lang="ts">import type { SelectValueProps } from "reka-ui"import type { HTMLAttributes } from "vue"import { SelectValue } from "@/registry/edmi/ui/select"
const props = defineProps<SelectValueProps & { class?: HTMLAttributes["class"] }>()</script>
<template> <SelectValue v-bind="props"> <slot /> </SelectValue></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()</script>
<template> <div :class="cn('flex items-center gap-2', props.class)"> <slot /> </div></template>import type { ComputedRef, InjectionKey } from "vue";import { inject } from "vue";
export interface CodeBlockContext { code: ComputedRef<string>;}
export const CodeBlockKey: InjectionKey<CodeBlockContext> = Symbol("CodeBlock");
export function useCodeBlockContext() { const context = inject(CodeBlockKey);
if (!context) throw new Error("CodeBlockCopyButton must be used within a <CodeBlock />");
return context;}import type { BundledLanguage, HighlighterGeneric, ThemedToken } from "shiki";import { createCssVariablesTheme, createHighlighter } from "shiki";
// Shiki's `css-variables` theme emits `var(--shiki-token-*)`; CodeBlockContainer maps those onto// chart tokens, so highlighting follows mode, base and theme (DESIGN 5b).export const THEME_NAME = "edmi-css-variables";const edmiTheme = createCssVariablesTheme({ name: THEME_NAME, variablePrefix: "--shiki-", fontStyle: true,});
// Shiki font styles are bitflags: 1 = italic, 2 = bold, 4 = underline.export const isItalic = (fontStyle: number | undefined) => fontStyle && fontStyle & 1;export const isBold = (fontStyle: number | undefined) => fontStyle && fontStyle & 2;export function isUnderline(fontStyle: number | undefined) { return fontStyle && fontStyle & 4;}
export interface TokenizedCode { tokens: ThemedToken[][]; fg: string; bg: string;}
// Highlighter cache (singleton per language), token cache and async subscribers.const highlighterCache = new Map< string, Promise<HighlighterGeneric<BundledLanguage, string>>>();const tokensCache = new Map<string, TokenizedCode>();const subscribers = new Map<string, Set<(result: TokenizedCode) => void>>();
function getTokensCacheKey(code: string, language: BundledLanguage) { const start = code.slice(0, 100); const end = code.length > 100 ? code.slice(-100) : ""; return `${language}:${code.length}:${start}:${end}`;}
function getHighlighter(language: BundledLanguage) { const cached = highlighterCache.get(language); if (cached) { return cached; } const promise = createHighlighter({ themes: [edmiTheme], langs: [language], }) as Promise<HighlighterGeneric<BundledLanguage, string>>; highlighterCache.set(language, promise); return promise;}
// Plain tokens for immediate display while highlighting loads.export function createRawTokens(code: string): TokenizedCode { return { tokens: code.split("\n").map((line) => line === "" ? [] : [ { content: line, color: "inherit", } as ThemedToken, ], ), fg: "inherit", bg: "transparent", };}
/** Tokenizes `code`; returns the cached result synchronously or `null` and calls `callback` once ready. */export function highlightCode( code: string, language: BundledLanguage, callback?: (result: TokenizedCode) => void,): TokenizedCode | null { const key = getTokensCacheKey(code, language);
const cached = tokensCache.get(key); if (cached) { return cached; }
if (callback) { if (!subscribers.has(key)) { subscribers.set(key, new Set()); } subscribers.get(key)?.add(callback); }
getHighlighter(language) .then((highlighter) => { const loaded = highlighter.getLoadedLanguages(); const lang = loaded.includes(language) ? language : "text"; const result = highlighter.codeToTokens(code, { lang, theme: THEME_NAME, }); const tokenized: TokenizedCode = { tokens: result.tokens, fg: result.fg ?? "inherit", bg: result.bg ?? "transparent", }; tokensCache.set(key, tokenized); const subs = subscribers.get(key); if (subs) { for (const sub of subs) { sub(tokenized); } subscribers.delete(key); } }) .catch((error) => { console.error("Failed to highlight code:", error); subscribers.delete(key); });
return null;}export { default as CodeBlock } from "./CodeBlock.vue";export { default as CodeBlockActions } from "./CodeBlockActions.vue";export { default as CodeBlockContainer } from "./CodeBlockContainer.vue";export { default as CodeBlockContent } from "./CodeBlockContent.vue";export { default as CodeBlockCopyButton } from "./CodeBlockCopyButton.vue";export { default as CodeBlockFilename } from "./CodeBlockFilename.vue";export { default as CodeBlockHeader } from "./CodeBlockHeader.vue";export { default as CodeBlockLanguageSelector } from "./CodeBlockLanguageSelector.vue";export { default as CodeBlockLanguageSelectorContent } from "./CodeBlockLanguageSelectorContent.vue";export { default as CodeBlockLanguageSelectorItem } from "./CodeBlockLanguageSelectorItem.vue";export { default as CodeBlockLanguageSelectorTrigger } from "./CodeBlockLanguageSelectorTrigger.vue";export { default as CodeBlockLanguageSelectorValue } from "./CodeBlockLanguageSelectorValue.vue";export { default as CodeBlockTitle } from "./CodeBlockTitle.vue";export * from "./context";export { highlightCode } from "./utils";<script lang="ts"> import { cn } from "$lib/utils.js"; import type { Snippet } from "svelte"; import type { HTMLAttributes } from "svelte/elements";
let { class: className, children, ...restProps }: HTMLAttributes<HTMLDivElement> & { children?: Snippet } = $props();</script>
<div class={cn("flex items-center gap-1", className)} {...restProps}> {@render children?.()}</div><script lang="ts"> import { cn } from "$lib/utils.js"; import type { BundledLanguage } from "shiki"; import { createRawTokens, highlightCode, isBold, isItalic, isUnderline, type TokenizedCode, } from "./utils.js";
let { code, language, showLineNumbers = false, }: { code: string; language: BundledLanguage; showLineNumbers?: boolean } = $props();
// Line numbers use CSS counters: 38px gutter, number right-aligned 14px from the code (board AI 05). const LINE_NUMBER_CLASSES = cn( "block", "before:inline-block before:w-[38px] before:pr-3.5 before:text-right", "before:content-[counter(line)] before:[counter-increment:line]", "before:font-mono before:text-muted-foreground-2 before:select-none" );
let highlighted = $state<TokenizedCode | null>(null); const tokenized = $derived( highlighted ?? highlightCode(code, language) ?? createRawTokens(code) );
$effect(() => { const current = { code, language }; highlighted = null; let cancelled = false; highlightCode(current.code, current.language, (result) => { if (!cancelled) highlighted = result; }); return () => { cancelled = true; }; });</script>
<div class="relative overflow-auto"> <pre class={cn("m-0 py-2.5 font-mono text-[12.5px] leading-[1.7]", showLineNumbers ? "pr-4" : "px-4")}><code class={cn("font-mono", showLineNumbers && "[counter-increment:line_0] [counter-reset:line]")} >{#each tokenized.tokens as line, i (i)}<span class={showLineNumbers ? LINE_NUMBER_CLASSES : "block"} >{#if line.length === 0}{"\n"}{:else}{#each line as token, j (j)}<span style:color={token.color} style:font-style={isItalic(token.fontStyle) ? "italic" : undefined} style:font-weight={isBold(token.fontStyle) ? "bold" : undefined} style:text-decoration={isUnderline(token.fontStyle) ? "underline" : undefined} >{token.content}</span >{/each}{/if}</span >{/each}</code ></pre></div><script lang="ts"> import CheckIcon from "phosphor-svelte/lib/Check"; import CopyIcon from "phosphor-svelte/lib/Copy"; import { Button, type ButtonProps } from "$lib/registry/ui/button/index.js"; import { cn } from "$lib/utils.js"; import { onDestroy } from "svelte"; import { useCodeBlockContext } from "./use-code-block.svelte.js";
let { onCopy, onError, timeout = 2000, class: className, children, ...restProps }: Omit<ButtonProps, "onclick" | "href"> & { onCopy?: () => void; onError?: (error: Error) => void; timeout?: number; } = $props();
const block = useCodeBlockContext(); let isCopied = $state(false); let timer: ReturnType<typeof setTimeout> | undefined;
async function copy() { if (typeof window === "undefined" || !navigator?.clipboard?.writeText) { onError?.(new Error("Clipboard API not available")); return; } try { await navigator.clipboard.writeText(block.code); isCopied = true; onCopy?.(); clearTimeout(timer); timer = setTimeout(() => (isCopied = false), timeout); } catch (error) { onError?.(error as Error); } }
onDestroy(() => clearTimeout(timer));</script>
<Button data-slot="ai-code-block-copy" aria-label="Copy code" class={cn("shrink-0", className)} onclick={copy} size="icon-xs" type="button" variant="ghost" {...restProps}> {#if children} {@render children()} {:else if isCopied} <CheckIcon /> {:else} <CopyIcon /> {/if}</Button><script lang="ts"> import { cn } from "$lib/utils.js"; import type { Snippet } from "svelte"; import type { HTMLAttributes } from "svelte/elements";
let { class: className, children, ...restProps }: HTMLAttributes<HTMLSpanElement> & { children?: Snippet } = $props();</script>
<span class={cn("font-mono text-xs", className)} {...restProps}> {@render children?.()}</span><script lang="ts"> import { cn } from "$lib/utils.js"; import type { Snippet } from "svelte"; import type { HTMLAttributes } from "svelte/elements";
let { class: className, children, ...restProps }: HTMLAttributes<HTMLDivElement> & { children?: Snippet } = $props();</script>
<div data-slot="ai-code-block-header" class={cn( "flex h-[38px] items-center justify-between gap-2 border-b border-border bg-muted pr-1.5 pl-3 text-[12.5px] text-muted-foreground", className )} {...restProps}> {@render children?.()}</div><script lang="ts"> import { SelectTrigger } from "$lib/registry/ui/select/index.js"; import { cn } from "$lib/utils.js"; import type { ComponentProps } from "svelte";
// Ghost trigger (board: a ghost xs button with a chevron). let { class: className, children, ...restProps }: ComponentProps<typeof SelectTrigger> = $props();</script>
<SelectTrigger size="sm" class={cn( "h-6 gap-1 rounded-md border-transparent bg-transparent px-2 text-xs font-normal text-foreground shadow-none hover:bg-accent data-[size=sm]:h-6 data-[size=sm]:rounded-md", className )} {...restProps}> {@render children?.()}</SelectTrigger><script lang="ts"> import { cn } from "$lib/utils.js"; import type { Snippet } from "svelte"; import type { HTMLAttributes } from "svelte/elements";
let { class: className, children, ...restProps }: HTMLAttributes<HTMLDivElement> & { children?: Snippet } = $props();</script>
<div class={cn("flex items-center gap-2", className)} {...restProps}> {@render children?.()}</div><script lang="ts"> import { cn } from "$lib/utils.js"; import type { Snippet } from "svelte"; import type { HTMLAttributes } from "svelte/elements"; import type { BundledLanguage } from "shiki"; import CodeBlockContent from "./code-block-content.svelte"; import { setCodeBlockContext } from "./use-code-block.svelte.js";
// The canonical code block of the kit (DESIGN 5b): body on --card, header on --muted. Highlight // colours come from chart tokens through Shiki's css-variables theme. const SHIKI_TOKEN_VARS = cn( "[--shiki-foreground:var(--foreground)] [--shiki-background:transparent]", "[--shiki-token-keyword:var(--chart-2)] [--shiki-token-string:var(--success-text)]", "[--shiki-token-string-expression:var(--success-text)] [--shiki-token-constant:var(--chart-3)]", "[--shiki-token-function:var(--chart-4)] [--shiki-token-comment:var(--muted-foreground)]", "[--shiki-token-parameter:var(--foreground)] [--shiki-token-punctuation:var(--foreground)]", "[--shiki-token-link:var(--info-text)]" );
let { code, language, showLineNumbers = false, class: className, children, ...restProps }: HTMLAttributes<HTMLDivElement> & { code: string; language: BundledLanguage; showLineNumbers?: boolean; children?: Snippet; } = $props();
setCodeBlockContext({ get code() { return code; }, });</script>
<div data-slot="ai-code-block" data-language={language} class={cn( "group relative w-full overflow-hidden rounded-xl border border-border bg-card text-foreground", SHIKI_TOKEN_VARS, className )} style="content-visibility:auto;contain-intrinsic-size:auto 200px" {...restProps}> {@render children?.()} <CodeBlockContent {code} {language} {showLineNumbers} /></div>import { getContext, setContext } from "svelte";
/** Getter object keeps the context reactive. */export interface CodeBlockContextValue { readonly code: string;}
const KEY = Symbol("ai-code-block");
export function setCodeBlockContext(value: CodeBlockContextValue) { return setContext(KEY, value);}
export function useCodeBlockContext(): CodeBlockContextValue { const ctx = getContext<CodeBlockContextValue | undefined>(KEY); if (!ctx) { throw new Error("CodeBlockCopyButton must be used within <CodeBlock>"); } return ctx;}import type { BundledLanguage, HighlighterGeneric, ThemedToken } from "shiki";import { createCssVariablesTheme, createHighlighter } from "shiki";
// Shiki's `css-variables` theme emits `var(--shiki-token-*)`; the container maps those onto chart// tokens, so highlighting follows mode, base and theme (DESIGN 5b).export const THEME_NAME = "edmi-css-variables";const edmiTheme = createCssVariablesTheme({ name: THEME_NAME, variablePrefix: "--shiki-", fontStyle: true,});
// Shiki font styles are bitflags: 1 = italic, 2 = bold, 4 = underline.export const isItalic = (fontStyle: number | undefined) => Boolean(fontStyle && fontStyle & 1);export const isBold = (fontStyle: number | undefined) => Boolean(fontStyle && fontStyle & 2);export const isUnderline = (fontStyle: number | undefined) => Boolean(fontStyle && fontStyle & 4);
export interface TokenizedCode { tokens: ThemedToken[][]; fg: string; bg: string;}
const highlighterCache = new Map< string, Promise<HighlighterGeneric<BundledLanguage, string>>>();const tokensCache = new Map<string, TokenizedCode>();const subscribers = new Map<string, Set<(result: TokenizedCode) => void>>();
const getKey = (code: string, language: BundledLanguage) => { const start = code.slice(0, 100); const end = code.length > 100 ? code.slice(-100) : ""; return `${language}:${code.length}:${start}:${end}`;};
const getHighlighter = (language: BundledLanguage) => { const cached = highlighterCache.get(language); if (cached) { return cached; } const promise = createHighlighter({ langs: [language], themes: [edmiTheme], }) as Promise<HighlighterGeneric<BundledLanguage, string>>; highlighterCache.set(language, promise); return promise;};
/** Plain tokens for immediate display while highlighting loads. */export const createRawTokens = (code: string): TokenizedCode => ({ bg: "transparent", fg: "inherit", tokens: code .split("\n") .map((line) => line === "" ? [] : [{ color: "inherit", content: line } as ThemedToken], ),});
/** Tokenizes `code`; returns the cached result synchronously or `null` and calls `callback` once ready. */export function highlightCode( code: string, language: BundledLanguage, callback?: (result: TokenizedCode) => void,): TokenizedCode | null { const key = getKey(code, language); const cached = tokensCache.get(key); if (cached) { return cached; }
if (callback) { if (!subscribers.has(key)) { subscribers.set(key, new Set()); } subscribers.get(key)?.add(callback); }
getHighlighter(language) .then((highlighter) => { const lang = highlighter.getLoadedLanguages().includes(language) ? language : "text"; const result = highlighter.codeToTokens(code, { lang, theme: THEME_NAME, }); const tokenized: TokenizedCode = { bg: result.bg ?? "transparent", fg: result.fg ?? "inherit", tokens: result.tokens, }; tokensCache.set(key, tokenized); for (const sub of subscribers.get(key) ?? []) { sub(tokenized); } subscribers.delete(key); }) .catch((error) => { console.error("Failed to highlight code:", error); subscribers.delete(key); });
return null;}import { Root as Select, Content as SelectContent, Item as SelectItem, Value as SelectValue,} from "$lib/registry/ui/select/index.js";import Root from "./code-block.svelte";import Actions from "./code-block-actions.svelte";import Content from "./code-block-content.svelte";import CopyButton from "./code-block-copy-button.svelte";import Filename from "./code-block-filename.svelte";import Header from "./code-block-header.svelte";import LanguageSelectorTrigger from "./code-block-language-selector-trigger.svelte";import Title from "./code-block-title.svelte";
export { useCodeBlockContext } from "./use-code-block.svelte.js";export { highlightCode } from "./utils.js";
export { Actions as CodeBlockActions, Content as CodeBlockContent, CopyButton as CodeBlockCopyButton, Filename as CodeBlockFilename, Header as CodeBlockHeader, LanguageSelectorTrigger as CodeBlockLanguageSelectorTrigger, Root as CodeBlock, Select as CodeBlockLanguageSelector, SelectContent as CodeBlockLanguageSelectorContent, SelectItem as CodeBlockLanguageSelectorItem, SelectValue as CodeBlockLanguageSelectorValue, Title as CodeBlockTitle,};| Part | Prop | Type | Default |
|---|---|---|---|
| CodeBlock | code, language, showLineNumbers |
string, BundledLanguage, boolean |
false |
| CodeBlockCopyButton | onCopy, onError, timeout |
2000 |
|
| CodeBlockLanguageSelector | Select props (items, value, onValueChange) |
✦ Edmi additions
Section titled “✦ Edmi additions”- Highlight colours come from chart tokens instead of a fixed Shiki theme.