Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/plain-code-highlighted.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cloudflare/kumo": patch
---

Add a plain CodeHighlighted variant for frameless, padding-free code blocks with a correctly positioned copy button.
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,53 @@ export function CodeHighlightedCopyButtonDemo() {
);
}

/** Frameless code block for embedding in another surface */
export function CodeHighlightedPlainDemo() {
return (
<DemoProvider>
<div className="grid md:grid-cols-2 divide-x divide-kumo-line border border-kumo-line rounded">
<div className="p-2">
<CodeHighlighted
showLineNumbers
code={`type EmbeddedOptions = {
enabled: boolean;
target: string;
};

const options = {
enabled: true,
target: "#preview",
} satisfies EmbeddedOptions;

console.log(options);`}
lang="typescript"
variant="plain"
/>
</div>
<div className="p-2">
<CodeHighlighted
showCopyButton
code={`type EmbeddedOptions = {
enabled: boolean;
target: string;
};

const options = {
enabled: true,
target: "#preview",
} satisfies EmbeddedOptions;

console.log(options);`}
lang="typescript"
variant="plain"
highlightLines={[6, 7, 8, 9]}
/>
</div>
</div>
</DemoProvider>
);
}

/** Full featured example */
export function CodeHighlightedFullFeaturedDemo() {
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@ interface CodeExampleProps {
export function CodeExample({ code }: CodeExampleProps) {
return (
<CodeHighlighted
className="code-block rounded-none border-0 bg-transparent p-2 [&_pre]:text-base!"
className="code-block p-2 [&_pre]:text-base! p-6"
code={code}
lang="tsx"
variant="plain"
/>
);
}
Expand Down
21 changes: 21 additions & 0 deletions packages/kumo-docs-astro/src/pages/components/code-highlighted.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
CodeHighlightedHighlightLinesDemo,
CodeHighlightedLineNumbersDemo,
CodeHighlightedCopyButtonDemo,
CodeHighlightedPlainDemo,
CodeHighlightedFullFeaturedDemo,
CodeHighlightedSharedProviderDemo,
CodeHighlightedCssDemo,
Expand Down Expand Up @@ -173,6 +174,16 @@ export function App() {
<CodeHighlightedCopyButtonDemo client:visible />
</ComponentExample>

### Plain

<p>
Use `variant="plain"` to remove the outer surface, border, and rounded corners
when embedding highlighted code inside another container.
</p>
<ComponentExample demo="CodeHighlightedPlainDemo">
<CodeHighlightedPlainDemo client:visible />
</ComponentExample>

### Full Featured

<p>Combine all features for a complete code display experience.</p>
Expand Down Expand Up @@ -568,6 +579,16 @@ import { ShikiProvider, CodeHighlighted } from "@cloudflare/kumo/code";
Language identifier (must be in provider's languages)
</td>
</tr>
<tr class="border-b border-kumo-hairline/50">
<td class="py-2 pr-4">
<code>variant</code>
</td>
<td class="py-2 pr-4">
<code>"default" | "plain"</code>
</td>
<td class="py-2 pr-4">No</td>
<td class="py-2">Visual style of the code block</td>
</tr>
<tr class="border-b border-kumo-hairline/50">
<td class="py-2 pr-4">
<code>showLineNumbers</code>
Expand Down
46 changes: 39 additions & 7 deletions packages/kumo/src/code/code-highlighted.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import type { CodeHighlightedProps } from "./types";
export function CodeHighlighted({
code,
lang,
variant = "default",
showLineNumbers = false,
highlightLines,
showCopyButton = false,
Expand Down Expand Up @@ -76,17 +77,45 @@ export function CodeHighlighted({
// Container styles - use flex layout for single-line with copy button
// Includes defensive resets (m-0, p-0) to prevent global CSS pollution
const containerClasses = cn(
"group relative m-0 w-full min-w-0 rounded-md border border-kumo-fill bg-kumo-base p-0",
"group relative m-0 w-full min-w-0 p-0",
variant === "plain"
? "rounded-none border-0 bg-transparent"
: "rounded-md border border-kumo-fill bg-kumo-base",
showCopyButton && isSingleLine && "flex items-center",
className,
);

const fallbackCodeClasses = cn(
// Prevent global pre styles from affecting the loading and error states.
"!m-0 min-w-0 flex-1 overflow-x-auto",
"font-mono text-sm leading-relaxed text-kumo-subtle",
variant === "plain" ? "!p-0" : "!p-4",
);

const highlightedCodeClasses = cn(
"kumo-shiki",
// Reset the code element generated by Shiki.
"[&_code]:!m-0 [&_code]:!border-0 [&_code]:!bg-transparent [&_code]:!p-0",
// Let the Kumo container provide the frame and background.
"[&>pre]:!m-0 [&>pre]:!rounded-none [&>pre]:!border-0 [&>pre]:!bg-transparent",
"[&>pre]:font-mono [&>pre]:text-sm [&>pre]:leading-relaxed",
variant === "plain" ? "[&>pre]:!p-0" : "[&>pre]:!p-4",
// Highlighted lines normally extend into the default padding. A plain
// block has no padding, so keep its highlights within the content width.
variant === "plain" &&
"[&_.line-highlighted]:!m-0 [&_.line-highlighted]:!w-full [&_.line-highlighted]:!px-0",
);

// Copy button - inline for single-line, absolute for multi-line
// Hidden until hover (or when showing "Copied!" feedback)
const copyButton = showCopyButton ? (
<div
className={cn(
isSingleLine ? "shrink-0 px-2" : "absolute top-2 right-2",
isSingleLine
? "shrink-0 px-2"
: variant === "plain"
? "absolute top-0 right-0"
: "absolute top-2 right-2",
!copied && "opacity-0 transition-opacity group-hover:opacity-100",
)}
>
Expand All @@ -105,7 +134,10 @@ export function CodeHighlighted({
const lineNumbers =
showLineNumbers && !isSingleLine ? (
<div
className="kumo-line-numbers shrink-0 py-4 pr-4 text-right font-mono text-sm opacity-40 select-none"
className={cn(
"kumo-line-numbers shrink-0 pr-4 text-right font-mono text-sm opacity-40 select-none",
variant === "plain" ? "py-0" : "py-4",
)}
aria-hidden="true"
>
{Array.from({ length: lineCount }, (_, i) => (
Expand Down Expand Up @@ -133,13 +165,13 @@ export function CodeHighlighted({
{lineNumbers && (
<div className="flex">
{lineNumbers}
<pre className="!m-0 min-w-0 flex-1 overflow-x-auto !p-4 font-mono text-sm leading-relaxed text-kumo-subtle">
<pre className={fallbackCodeClasses}>
<code className="!m-0 !p-0">{code}</code>
</pre>
</div>
)}
{!lineNumbers && (
<pre className="!m-0 min-w-0 flex-1 overflow-x-auto !p-4 font-mono text-sm leading-relaxed text-kumo-subtle">
<pre className={fallbackCodeClasses}>
<code className="!m-0 !p-0">{code}</code>
</pre>
)}
Expand All @@ -156,7 +188,7 @@ export function CodeHighlighted({
{lineNumbers}
<div className="min-w-0 flex-1 overflow-x-auto">
<div
className="kumo-shiki [&_code]:!m-0 [&_code]:!border-0 [&_code]:!bg-transparent [&_code]:!p-0 [&>pre]:!m-0 [&>pre]:!rounded-none [&>pre]:!border-0 [&>pre]:!bg-transparent [&>pre]:!p-4 [&>pre]:font-mono [&>pre]:text-sm [&>pre]:leading-relaxed"
className={highlightedCodeClasses}
dangerouslySetInnerHTML={{
__html: processHighlightedHtml(html, highlightLines),
}}
Expand All @@ -167,7 +199,7 @@ export function CodeHighlighted({
{!lineNumbers && (
<div className="overflow-x-auto">
<div
className="kumo-shiki [&_code]:!m-0 [&_code]:!border-0 [&_code]:!bg-transparent [&_code]:!p-0 [&>pre]:!m-0 [&>pre]:!rounded-none [&>pre]:!border-0 [&>pre]:!bg-transparent [&>pre]:!p-4 [&>pre]:font-mono [&>pre]:text-sm [&>pre]:leading-relaxed"
className={highlightedCodeClasses}
dangerouslySetInnerHTML={{
__html: processHighlightedHtml(html, highlightLines),
}}
Expand Down
1 change: 1 addition & 0 deletions packages/kumo/src/code/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export { LANGUAGE_ALIASES } from "./types";
export type {
ShikiProviderProps,
CodeHighlightedProps,
CodeHighlightedVariant,
UseShikiHighlighterResult,
ShikiEngine,
BundledLanguage,
Expand Down
88 changes: 88 additions & 0 deletions packages/kumo/src/code/provider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,94 @@ describe("ShikiProvider", () => {
expect(mockHighlighter.codeToHtml).toHaveBeenCalledTimes(1);
});

it("renders the plain CodeHighlighted variant without a frame", () => {
const { container } = render(
<ShikiProvider engine="javascript" languages={["javascript"]}>
<CodeHighlighted
code={"const x = 1;\nconst y = 2;"}
lang="javascript"
variant="plain"
showCopyButton
/>
</ShikiProvider>,
);

const classList = container.firstElementChild?.classList;
expect(classList?.contains("rounded-none")).toBe(true);
expect(classList?.contains("border-0")).toBe(true);
expect(classList?.contains("bg-transparent")).toBe(true);
expect(classList?.contains("rounded-md")).toBe(false);
expect(classList?.contains("border-kumo-fill")).toBe(false);
expect(classList?.contains("bg-kumo-base")).toBe(false);
expect(container.querySelector("pre")?.classList.contains("!p-0")).toBe(
true,
);

const copyButtonContainer = screen.getByRole("button", {
name: "Copy",
}).parentElement;
expect(copyButtonContainer?.classList.contains("top-0")).toBe(true);
expect(copyButtonContainer?.classList.contains("right-0")).toBe(true);
expect(copyButtonContainer?.classList.contains("top-2")).toBe(false);
expect(copyButtonContainer?.classList.contains("right-2")).toBe(false);
});

it("removes Shiki pre padding from the plain CodeHighlighted variant", async () => {
mockHighlighter.codeToHtml.mockReturnValue(
'<pre><code><span class="line">const x = 1;</span>\n<span class="line">const y = 2;</span></code></pre>',
);

const { container } = render(
<ShikiProvider engine="javascript" languages={["javascript"]}>
<CodeHighlighted
code={"const x = 1;\nconst y = 2;"}
lang="javascript"
variant="plain"
showLineNumbers
/>
</ShikiProvider>,
);

await waitFor(() => {
expect(container.querySelector(".kumo-shiki")).not.toBeNull();
});

const classList = container.querySelector(".kumo-shiki")?.classList;
expect(classList?.contains("[&>pre]:!p-0")).toBe(true);
expect(classList?.contains("[&>pre]:!p-4")).toBe(false);

const lineNumberClasses =
container.querySelector(".kumo-line-numbers")?.classList;
expect(lineNumberClasses?.contains("py-0")).toBe(true);
expect(lineNumberClasses?.contains("py-4")).toBe(false);
});

it("does not extend highlighted lines beyond a plain code block", async () => {
mockHighlighter.codeToHtml.mockReturnValue(
'<pre><code><span class="line">const x = 1;</span></code></pre>',
);

const { container } = render(
<ShikiProvider engine="javascript" languages={["javascript"]}>
<CodeHighlighted
code="const x = 1;"
lang="javascript"
variant="plain"
highlightLines={[1]}
/>
</ShikiProvider>,
);

await waitFor(() => {
expect(container.querySelector(".line-highlighted")).not.toBeNull();
});

const classList = container.querySelector(".kumo-shiki")?.classList;
expect(classList?.contains("[&_.line-highlighted]:!m-0")).toBe(true);
expect(classList?.contains("[&_.line-highlighted]:!w-full")).toBe(true);
expect(classList?.contains("[&_.line-highlighted]:!px-0")).toBe(true);
});

it("exposes normalized languages in context so hook alias resolution works end-to-end", async () => {
// This is the integration test for the bug where the context stores raw
// aliases (e.g., ["js", "ts"]) but the hook normalizes to canonical names
Expand Down
11 changes: 8 additions & 3 deletions packages/kumo/src/code/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ export interface UseShikiHighlighterResult {
/**
* Props for CodeHighlighted component.
*/
export type CodeHighlightedVariant = "default" | "plain";

export interface CodeHighlightedProps {
/** Source code to display */
code: string;
Expand All @@ -155,9 +157,6 @@ export interface CodeHighlightedProps {
*/
highlightLines?: number[];

/** Show copy-to-clipboard button */
showCopyButton?: boolean;

/**
* Override provider labels for this instance.
* @example { copy: "Copy code", copied: "Done!" }
Expand All @@ -166,6 +165,12 @@ export interface CodeHighlightedProps {

/** Additional CSS classes */
className?: string;

/** Code block appearance. @default "default" */
variant?: CodeHighlightedVariant;

/** Show copy-to-clipboard button */
showCopyButton?: boolean;
}

// Re-export for backwards compatibility (deprecated, use SupportedLanguage instead)
Expand Down
5 changes: 1 addition & 4 deletions packages/kumo/src/primitives/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,7 @@ export * from "@base-ui/react/meter";
export * from "@base-ui/react/navigation-menu";
export * from "@base-ui/react/number-field";
export * from "@base-ui/react/otp-field";
export {
OTPField,
OTPField as OTPFieldPreview,
} from "@base-ui/react/otp-field";
export { OTPField, OTPField as OTPFieldPreview } from "@base-ui/react/otp-field";
export * from "@base-ui/react/popover";
export * from "@base-ui/react/preview-card";
export * from "@base-ui/react/progress";
Expand Down
Loading