From 56d0d6597162e9791355201ed3537de8a1f0ae9c Mon Sep 17 00:00:00 2001 From: Vladimir Eremin Date: Fri, 6 Mar 2026 23:06:30 +0300 Subject: [PATCH] feat: Add codehighlight for the Code' --- packages/ui/package.json | 1 + .../ui/src/components/Code/Code.stories.tsx | 18 +++ packages/ui/src/components/Code/Code.test.tsx | 26 ++- packages/ui/src/components/Code/Code.tsx | 64 +++++++- .../components/Code/CodeBlock/CodeBlock.tsx | 10 +- .../Code/contexts/CodeContext/CodeContext.ts | 4 + .../Code/lib/highlighter/highlighter.ts | 113 +++++++++++++ .../Markdown/MarkdownCode/MarkdownCode.tsx | 17 +- .../Markdown/MarkdownPre/MarkdownPre.tsx | 26 ++- packages/ui/src/static/styles.css | 4 + pnpm-lock.yaml | 153 +++++++++++++++++- 11 files changed, 422 insertions(+), 14 deletions(-) create mode 100644 packages/ui/src/components/Code/lib/highlighter/highlighter.ts diff --git a/packages/ui/package.json b/packages/ui/package.json index 8e10ea4..be4319e 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -47,6 +47,7 @@ "react-aria": "^3.45.0", "react-icons": "^5.5.0", "react-stately": "^3.44.0", + "shiki": "^4.0.1", "tw-animate-css": "^1.2.8" }, "devDependencies": { diff --git a/packages/ui/src/components/Code/Code.stories.tsx b/packages/ui/src/components/Code/Code.stories.tsx index 2522e0f..5a2ff34 100644 --- a/packages/ui/src/components/Code/Code.stories.tsx +++ b/packages/ui/src/components/Code/Code.stories.tsx @@ -90,6 +90,24 @@ export const LongWithScroll: StoryObj = { }, }; +export const ShikiHighlight: StoryObj = { + render: (args) => ( + + + {`import React from 'react'; + +export const MyComponent = () => { + return
Hello World
; +};`} +
+
+ ), + args: { + label: 'ShikiHighlight.tsx', + language: 'tsx', + }, +}; + export const PlainText: StoryObj = { render: (args) => ( diff --git a/packages/ui/src/components/Code/Code.test.tsx b/packages/ui/src/components/Code/Code.test.tsx index 8ea1558..80650e0 100644 --- a/packages/ui/src/components/Code/Code.test.tsx +++ b/packages/ui/src/components/Code/Code.test.tsx @@ -1,9 +1,17 @@ import { render, screen, fireEvent, act } from '@testing-library/react'; import React from 'react'; +import { vi, describe, it, expect, beforeEach } from 'vitest'; import { Code } from './Code'; import { CodeBlock } from './CodeBlock/CodeBlock'; +// Mock shiki +vi.mock('shiki', () => ({ + createHighlighter: vi.fn().mockResolvedValue({ + codeToHtml: vi.fn().mockReturnValue('
highlighted code
'), + }), +})); + // Mock clipboard API const mockWriteText = vi.fn().mockImplementation(() => Promise.resolve()); Object.assign(navigator, { @@ -15,6 +23,7 @@ Object.assign(navigator, { describe('Code Component', () => { beforeEach(() => { mockWriteText.mockClear(); + vi.clearAllMocks(); }); it('renders inline code correctly', () => { @@ -46,7 +55,6 @@ describe('Code Component', () => { await act(async () => { fireEvent.click(codeElement); - await Promise.resolve(); }); expect(mockWriteText).toHaveBeenCalledWith('copied!'); @@ -64,7 +72,6 @@ describe('Code Component', () => { const copyButton = screen.getByLabelText('Copy code'); await act(async () => { fireEvent.click(copyButton); - await Promise.resolve(); }); expect(mockWriteText).toHaveBeenCalledWith('block copy'); @@ -76,9 +83,22 @@ describe('Code Component', () => { const codeElement = screen.getByText('extracted'); await act(async () => { fireEvent.click(codeElement); - await Promise.resolve(); }); expect(mockWriteText).toHaveBeenCalledWith('extracted'); }); + + it('uses shiki for highlighting when language is provided in block mode', async () => { + await act(async () => { + render( + + const x: number = 1; + + ); + }); + + // Wait for shiki async highlighting + const highlighted = await screen.findByText('highlighted code'); + expect(highlighted).toBeInTheDocument(); + }); }); diff --git a/packages/ui/src/components/Code/Code.tsx b/packages/ui/src/components/Code/Code.tsx index b7a6d78..30e874d 100644 --- a/packages/ui/src/components/Code/Code.tsx +++ b/packages/ui/src/components/Code/Code.tsx @@ -3,8 +3,12 @@ import React, { memo, useCallback, useEffect, useRef, useState } from 'react'; import { usePress } from 'react-aria'; import { useCodeContext } from './contexts/CodeContext/CodeContext'; +import { codeHighlighter } from './lib/highlighter/highlighter'; import type { ClassNameProps, TestIdProps } from '@ermnvldmr/stl'; +import type { CodeLanguage } from './lib/highlighter/highlighter'; + +export type { CodeLanguage }; /** * Props for the Code component. @@ -18,6 +22,12 @@ export interface CodeProps extends ClassNameProps, TestIdProps { * If omitted in inline mode, the component will attempt to extract text from `children`. */ copyValue?: string; + + /** + * The language of the code. Only used when rendered inside a CodeBlock. + * If provided, shiki will be used for syntax highlighting. + */ + language?: CodeLanguage; } /** @@ -30,12 +40,16 @@ export interface CodeProps extends ClassNameProps, TestIdProps { export const Code = memo(function Code({ children, copyValue, + language: propsLanguage, className, 'data-testid': testId, }: CodeProps) { const context = useCodeContext(); const [isCopied, setIsCopied] = useState(false); const codeRef = useRef(null); + const [highlightedHtml, setHighlightedHtml] = useState(null); + + const language = propsLanguage ?? context?.language; const handleCopy = useCallback(async () => { try { @@ -67,6 +81,22 @@ export const Code = memo(function Code({ } }, [isCopied]); + useEffect(() => { + if (context?.isInCodeBlock && language && typeof children === 'string') { + const highlight = async () => { + try { + const html = await codeHighlighter.highlight(children, language); + setHighlightedHtml(html); + } catch (err) { + console.error('Failed to highlight code:', err); + } + }; + void highlight(); + } else { + setHighlightedHtml(null); + } + }, [children, language, context?.isInCodeBlock]); + const { pressProps } = usePress({ onPress: onCopyPress, }); @@ -74,7 +104,39 @@ export const Code = memo(function Code({ if (context?.isInCodeBlock) { const { showLineNumbers } = context; - let content = children; + let content: React.ReactNode = children; + + if (highlightedHtml) { + return ( +
pre]:!bg-transparent [&>pre]:!p-0 [&>pre]:!m-0', + showLineNumbers && [ + '[&>pre>code]:[counter-reset:line]', + '[&>pre>code>.line]:relative [&>pre>code>.line]:pl-10', + '[&>pre>code>.line]:[counter-increment:line]', + '[&>pre>code>.line]:before:content-[counter(line)]', + '[&>pre>code>.line]:before:absolute [&>pre>code>.line]:before:left-0 [&>pre>code>.line]:before:top-0', + '[&>pre>code>.line]:before:inline-block', + '[&>pre>code>.line]:before:w-8', + '[&>pre>code>.line]:before:text-right', + '[&>pre>code>.line]:before:text-[var(--rb-muted-text)]/50', + '[&>pre>code>.line]:before:select-none', + '[&>pre>code>.line]:before:border-r', + '[&>pre>code>.line]:before:border-[var(--rb-outline)]/10', + '[&>pre>code>.line]:before:pr-3', + '[&>pre>code>.line]:before:font-mono [&>pre>code>.line]:before:text-[10px]', + ], + className + )} + dangerouslySetInnerHTML={{ __html: highlightedHtml }} + data-testid={testId} + /> + ); + } + if (typeof children === 'string') { content = children .trimEnd() diff --git a/packages/ui/src/components/Code/CodeBlock/CodeBlock.tsx b/packages/ui/src/components/Code/CodeBlock/CodeBlock.tsx index c40bcbf..7b0c440 100644 --- a/packages/ui/src/components/Code/CodeBlock/CodeBlock.tsx +++ b/packages/ui/src/components/Code/CodeBlock/CodeBlock.tsx @@ -7,6 +7,7 @@ import { Container } from '../../Container/Container'; import { CodeContext, type CodeContextValue } from '../contexts/CodeContext/CodeContext'; import type { ClassNameProps, TestIdProps } from '@ermnvldmr/stl'; +import type { CodeLanguage } from '../lib/highlighter/highlighter'; /** * Props for the CodeBlock component. @@ -21,6 +22,11 @@ export interface CodeBlockProps extends ClassNameProps, TestIdProps { */ label?: string; + /** + * The language of the code block. + */ + language?: CodeLanguage; + /** * Whether to render line number gutters. * Requires children (Code) to be rendered as block-level elements. @@ -49,6 +55,7 @@ export interface CodeBlockProps extends ClassNameProps, TestIdProps { export const CodeBlock = memo(function CodeBlock({ children, label, + language, showLineNumbers = true, copyValue, maxHeight, @@ -92,8 +99,9 @@ export const CodeBlock = memo(function CodeBlock({ () => ({ isInCodeBlock: true, showLineNumbers, + language, }), - [showLineNumbers] + [showLineNumbers, language] ); return ( diff --git a/packages/ui/src/components/Code/contexts/CodeContext/CodeContext.ts b/packages/ui/src/components/Code/contexts/CodeContext/CodeContext.ts index de16cb4..c5a4fdc 100644 --- a/packages/ui/src/components/Code/contexts/CodeContext/CodeContext.ts +++ b/packages/ui/src/components/Code/contexts/CodeContext/CodeContext.ts @@ -1,5 +1,7 @@ import { createContext, useContext } from 'react'; +import type { CodeLanguage } from '../../lib/highlighter/highlighter'; + /** * Value for the Code context. */ @@ -8,6 +10,8 @@ export interface CodeContextValue { isInCodeBlock: boolean; /** Whether to show line numbers in the block. */ showLineNumbers?: boolean; + /** The language of the code in the block. */ + language?: CodeLanguage; } /** diff --git a/packages/ui/src/components/Code/lib/highlighter/highlighter.ts b/packages/ui/src/components/Code/lib/highlighter/highlighter.ts new file mode 100644 index 0000000..418a7ca --- /dev/null +++ b/packages/ui/src/components/Code/lib/highlighter/highlighter.ts @@ -0,0 +1,113 @@ +import { createHighlighter } from 'shiki'; + +import type { Highlighter } from 'shiki'; + +/** + * Supported languages for syntax highlighting. + */ +export type CodeLanguage = + | 'typescript' + | 'javascript' + | 'tsx' + | 'jsx' + | 'bash' + | 'shell' + | 'json' + | 'css' + | 'html' + | 'markdown' + | 'yaml' + | 'dockerfile' + | 'toml' + | 'rust' + | 'go' + | 'python'; + +export const SUPPORTED_LANGUAGES: CodeLanguage[] = [ + 'typescript', + 'javascript', + 'tsx', + 'jsx', + 'bash', + 'shell', + 'json', + 'css', + 'html', + 'markdown', + 'yaml', + 'dockerfile', + 'toml', + 'rust', + 'go', + 'python', +]; + +/** + * A singleton highlighter service for code blocks. + */ +class CodeHighlighter { + private static instance: CodeHighlighter; + private highlighter: Highlighter | null = null; + private initializingPromise: Promise | null = null; + + private constructor() {} + + /** + * Gets the singleton instance of the highlighter. + */ + public static getInstance(): CodeHighlighter { + if (!CodeHighlighter.instance) { + CodeHighlighter.instance = new CodeHighlighter(); + } + return CodeHighlighter.instance; + } + + /** + * Initializes the highlighter if it hasn't been already. + */ + private async getHighlighter(): Promise { + if (this.highlighter) { + return this.highlighter; + } + + if (this.initializingPromise) { + return this.initializingPromise; + } + + this.initializingPromise = createHighlighter({ + themes: ['github-dark', 'github-light'], + langs: SUPPORTED_LANGUAGES, + }).then((highlighter) => { + this.highlighter = highlighter; + this.initializingPromise = null; + return highlighter; + }); + + return this.initializingPromise; + } + + /** + * Highlights the given code. + * + * @param code - The raw source code to highlight + * @param lang - The language of the code + * @returns A promise resolving to the highlighted HTML + */ + public async highlight(code: string, lang: CodeLanguage): Promise { + try { + const highlighter = await this.getHighlighter(); + return highlighter.codeToHtml(code, { + lang, + themes: { + light: 'github-light', + dark: 'github-dark', + }, + }); + } catch (err) { + console.error('Failed to highlight code:', err); + throw err; + } + } +} + +export const codeHighlighter = CodeHighlighter.getInstance(); diff --git a/packages/ui/src/components/Markdown/MarkdownCode/MarkdownCode.tsx b/packages/ui/src/components/Markdown/MarkdownCode/MarkdownCode.tsx index 206097e..14d4359 100644 --- a/packages/ui/src/components/Markdown/MarkdownCode/MarkdownCode.tsx +++ b/packages/ui/src/components/Markdown/MarkdownCode/MarkdownCode.tsx @@ -1,12 +1,25 @@ -import React, { memo } from 'react'; +import React, { memo, useMemo } from 'react'; import { Code } from '../../Code/Code'; import type { CodeProps } from '../../Code/Code'; +import type { CodeLanguage } from '../../Code/lib/highlighter/highlighter'; /** * A Markdown code component for inline snippets. */ export const MarkdownCode = memo(function MarkdownCode(props: CodeProps) { - return ; + const { className, ...rest } = props; + + const language = useMemo(() => { + if (className) { + const match = /language-(\w+)/.exec(className); + if (match) { + return match[1] as CodeLanguage; + } + } + return undefined; + }, [className]); + + return ; }); diff --git a/packages/ui/src/components/Markdown/MarkdownPre/MarkdownPre.tsx b/packages/ui/src/components/Markdown/MarkdownPre/MarkdownPre.tsx index fe22516..1752daa 100644 --- a/packages/ui/src/components/Markdown/MarkdownPre/MarkdownPre.tsx +++ b/packages/ui/src/components/Markdown/MarkdownPre/MarkdownPre.tsx @@ -1,8 +1,9 @@ -import React, { memo } from 'react'; +import React, { memo, useMemo } from 'react'; import { CodeBlock } from '../../Code/CodeBlock/CodeBlock'; import type { CodeBlockProps } from '../../Code/CodeBlock/CodeBlock'; +import type { CodeLanguage } from '../../Code/lib/highlighter/highlighter'; /** * A Markdown pre component for code blocks. @@ -10,5 +11,26 @@ import type { CodeBlockProps } from '../../Code/CodeBlock/CodeBlock'; * Wraps content in the design system CodeBlock. */ export const MarkdownPre = memo(function MarkdownPre(props: CodeBlockProps) { - return ; + const { children, ...rest } = props; + + // MDX often passes a element as the only child of
.
+  // That  element usually has the language class (e.g., "language-typescript").
+  const language = useMemo(() => {
+    if (React.isValidElement<{ className?: string }>(children) && children.props.className) {
+      const className = children.props.className;
+      const match = /language-(\w+)/.exec(className);
+      if (match) {
+        // We cast here because we know it comes from markdown,
+        // shiki will handle it or fallback to plaintext if unknown at runtime.
+        return match[1] as CodeLanguage;
+      }
+    }
+    return undefined;
+  }, [children]);
+
+  return (
+    
+      {children}
+    
+  );
 });
diff --git a/packages/ui/src/static/styles.css b/packages/ui/src/static/styles.css
index f7be542..a5d4630 100644
--- a/packages/ui/src/static/styles.css
+++ b/packages/ui/src/static/styles.css
@@ -285,4 +285,8 @@
     );
     background-size: 200% 100%;
   }
+
+  .dark .shiki span {
+    color: var(--shiki-dark) !important;
+  }
 }
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 25ff5d7..ad6ebf5 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -198,7 +198,7 @@ importers:
         version: 7.0.1
       storybook-react-rsbuild:
         specifier: ^1.0.3
-        version: 1.0.3(@rsbuild/core@1.7.3)(@storybook/test@8.6.15(storybook@8.6.15(prettier@3.5.3)))(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.56.0)(storybook@8.6.15(prettier@3.5.3))(typescript@5.8.3)(webpack@5.105.1)
+        version: 1.0.3(@rsbuild/core@1.7.3)(@storybook/test@8.6.15(storybook@8.6.15(prettier@3.5.3)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.56.0)(storybook@8.6.15(prettier@3.5.3))(typescript@5.8.3)(webpack@5.105.1)
       typescript:
         specifier: ^5.8.3
         version: 5.8.3
@@ -244,6 +244,9 @@ importers:
       react-stately:
         specifier: ^3.44.0
         version: 3.44.0(react@19.2.3)
+      shiki:
+        specifier: ^4.0.1
+        version: 4.0.1
       tailwindcss:
         specifier: ^4.1.4
         version: 4.1.18
@@ -295,7 +298,7 @@ importers:
         version: 4.1.18
       '@tailwindcss/typography':
         specifier: ^0.5.19
-        version: 0.5.19(tailwindcss@4.1.18)
+        version: 0.5.19
       '@testing-library/jest-dom':
         specifier: ^6.9.1
         version: 6.9.1
@@ -3057,6 +3060,37 @@ packages:
   '@rushstack/ts-command-line@5.1.5':
     resolution: {integrity: sha512-YmrFTFUdHXblYSa+Xc9OO9FsL/XFcckZy0ycQ6q7VSBsVs5P0uD9vcges5Q9vctGlVdu27w+Ct6IuJ458V0cTQ==}
 
+  '@shikijs/core@4.0.1':
+    resolution: {integrity: sha512-vWvqi9JNgz1dRL9Nvog5wtx7RuNkf7MEPl2mU/cyUUxJeH1CAr3t+81h8zO8zs7DK6cKLMoU9TvukWIDjP4Lzg==}
+    engines: {node: '>=20'}
+
+  '@shikijs/engine-javascript@4.0.1':
+    resolution: {integrity: sha512-DJK9NiwtGYqMuKCRO4Ip0FKNDQpmaiS+K5bFjJ7DWFn4zHueDWgaUG8kAofkrnXF6zPPYYQY7J5FYVW9MbZyBg==}
+    engines: {node: '>=20'}
+
+  '@shikijs/engine-oniguruma@4.0.1':
+    resolution: {integrity: sha512-oCWdCTDch3J8Kc0OZJ98KuUPC02O1VqIE3W/e2uvrHqTxYRR21RGEJMtchrgrxhsoJJCzmIciKsqG+q/yD+Cxg==}
+    engines: {node: '>=20'}
+
+  '@shikijs/langs@4.0.1':
+    resolution: {integrity: sha512-v/mluaybWdnGJR4GqAR6zh8qAZohW9k+cGYT28Y7M8+jLbC0l4yG085O1A+WkseHTn+awd+P3UBymb2+MXFc8w==}
+    engines: {node: '>=20'}
+
+  '@shikijs/primitive@4.0.1':
+    resolution: {integrity: sha512-ns0hHZc5eWZuvuIEJz2pTx3Qecz0aRVYumVQJ8JgWY2tq/dH8WxdcVM49Fc2NsHEILNIT6vfdW9MF26RANWiTA==}
+    engines: {node: '>=20'}
+
+  '@shikijs/themes@4.0.1':
+    resolution: {integrity: sha512-FW41C/D6j/yKQkzVdjrRPiJCtgeDaYRJFEyCKFCINuRJRj9WcmubhP4KQHPZ4+9eT87jruSrYPyoblNRyDFzvA==}
+    engines: {node: '>=20'}
+
+  '@shikijs/types@4.0.1':
+    resolution: {integrity: sha512-EaygPEn57+jJ76mw+nTLvIpJMAcMPokFbrF8lufsZP7Ukk+ToJYEcswN1G0e49nUZAq7aCQtoeW219A8HK1ZOw==}
+    engines: {node: '>=20'}
+
+  '@shikijs/vscode-textmate@10.0.2':
+    resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==}
+
   '@sideway/address@4.1.5':
     resolution: {integrity: sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==}
 
@@ -5121,6 +5155,9 @@ packages:
   hast-util-to-estree@3.1.3:
     resolution: {integrity: sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==}
 
+  hast-util-to-html@9.0.5:
+    resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==}
+
   hast-util-to-jsx-runtime@2.3.6:
     resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==}
 
@@ -5154,6 +5191,9 @@ packages:
     engines: {node: ^14.13.1 || >=16.0.0}
     hasBin: true
 
+  html-void-elements@3.0.0:
+    resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
+
   htmlparser2@3.10.1:
     resolution: {integrity: sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==}
 
@@ -6166,6 +6206,12 @@ packages:
     resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
     engines: {node: '>=6'}
 
+  oniguruma-parser@0.12.1:
+    resolution: {integrity: sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==}
+
+  oniguruma-to-es@4.3.4:
+    resolution: {integrity: sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA==}
+
   open@8.4.2:
     resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==}
     engines: {node: '>=12'}
@@ -6569,6 +6615,15 @@ packages:
     resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
     engines: {node: '>= 0.4'}
 
+  regex-recursion@6.0.2:
+    resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==}
+
+  regex-utilities@2.3.0:
+    resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==}
+
+  regex@6.1.0:
+    resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==}
+
   regexp.prototype.flags@1.5.4:
     resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==}
     engines: {node: '>= 0.4'}
@@ -6779,6 +6834,10 @@ packages:
     resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==}
     engines: {node: '>= 0.4'}
 
+  shiki@4.0.1:
+    resolution: {integrity: sha512-EkAEhDTN5WhpoQFXFw79OHIrSAfHhlImeCdSyg4u4XvrpxKEmdo/9x/HWSowujAnUrFsGOwWiE58a6GVentMnQ==}
+    engines: {node: '>=20'}
+
   side-channel-list@1.0.0:
     resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==}
     engines: {node: '>= 0.4'}
@@ -10716,6 +10775,46 @@ snapshots:
       - '@types/node'
     optional: true
 
+  '@shikijs/core@4.0.1':
+    dependencies:
+      '@shikijs/primitive': 4.0.1
+      '@shikijs/types': 4.0.1
+      '@shikijs/vscode-textmate': 10.0.2
+      '@types/hast': 3.0.4
+      hast-util-to-html: 9.0.5
+
+  '@shikijs/engine-javascript@4.0.1':
+    dependencies:
+      '@shikijs/types': 4.0.1
+      '@shikijs/vscode-textmate': 10.0.2
+      oniguruma-to-es: 4.3.4
+
+  '@shikijs/engine-oniguruma@4.0.1':
+    dependencies:
+      '@shikijs/types': 4.0.1
+      '@shikijs/vscode-textmate': 10.0.2
+
+  '@shikijs/langs@4.0.1':
+    dependencies:
+      '@shikijs/types': 4.0.1
+
+  '@shikijs/primitive@4.0.1':
+    dependencies:
+      '@shikijs/types': 4.0.1
+      '@shikijs/vscode-textmate': 10.0.2
+      '@types/hast': 3.0.4
+
+  '@shikijs/themes@4.0.1':
+    dependencies:
+      '@shikijs/types': 4.0.1
+
+  '@shikijs/types@4.0.1':
+    dependencies:
+      '@shikijs/vscode-textmate': 10.0.2
+      '@types/hast': 3.0.4
+
+  '@shikijs/vscode-textmate@10.0.2': {}
+
   '@sideway/address@4.1.5':
     dependencies:
       '@hapi/hoek': 9.3.0
@@ -11206,10 +11305,9 @@ snapshots:
       postcss: 8.5.6
       tailwindcss: 4.1.18
 
-  '@tailwindcss/typography@0.5.19(tailwindcss@4.1.18)':
+  '@tailwindcss/typography@0.5.19':
     dependencies:
       postcss-selector-parser: 6.0.10
-      tailwindcss: 4.1.18
 
   '@testing-library/dom@10.4.0':
     dependencies:
@@ -13226,6 +13324,20 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
+  hast-util-to-html@9.0.5:
+    dependencies:
+      '@types/hast': 3.0.4
+      '@types/unist': 3.0.3
+      ccount: 2.0.1
+      comma-separated-tokens: 2.0.3
+      hast-util-whitespace: 3.0.0
+      html-void-elements: 3.0.0
+      mdast-util-to-hast: 13.2.1
+      property-information: 7.1.0
+      space-separated-tokens: 2.0.2
+      stringify-entities: 4.0.4
+      zwitch: 2.0.4
+
   hast-util-to-jsx-runtime@2.3.6:
     dependencies:
       '@types/estree': 1.0.8
@@ -13280,6 +13392,8 @@ snapshots:
       relateurl: 0.2.7
       terser: 5.46.0
 
+  html-void-elements@3.0.0: {}
+
   htmlparser2@3.10.1:
     dependencies:
       domelementtype: 1.3.1
@@ -14753,6 +14867,14 @@ snapshots:
     dependencies:
       mimic-fn: 2.1.0
 
+  oniguruma-parser@0.12.1: {}
+
+  oniguruma-to-es@4.3.4:
+    dependencies:
+      oniguruma-parser: 0.12.1
+      regex: 6.1.0
+      regex-recursion: 6.0.2
+
   open@8.4.2:
     dependencies:
       define-lazy-prop: 2.0.0
@@ -15197,6 +15319,16 @@ snapshots:
       get-proto: 1.0.1
       which-builtin-type: 1.2.1
 
+  regex-recursion@6.0.2:
+    dependencies:
+      regex-utilities: 2.3.0
+
+  regex-utilities@2.3.0: {}
+
+  regex@6.1.0:
+    dependencies:
+      regex-utilities: 2.3.0
+
   regexp.prototype.flags@1.5.4:
     dependencies:
       call-bind: 1.0.8
@@ -15458,6 +15590,17 @@ snapshots:
 
   shell-quote@1.8.3: {}
 
+  shiki@4.0.1:
+    dependencies:
+      '@shikijs/core': 4.0.1
+      '@shikijs/engine-javascript': 4.0.1
+      '@shikijs/engine-oniguruma': 4.0.1
+      '@shikijs/langs': 4.0.1
+      '@shikijs/themes': 4.0.1
+      '@shikijs/types': 4.0.1
+      '@shikijs/vscode-textmate': 10.0.2
+      '@types/hast': 3.0.4
+
   side-channel-list@1.0.0:
     dependencies:
       es-errors: 1.3.0
@@ -15653,7 +15796,7 @@ snapshots:
       - tslib
       - webpack
 
-  storybook-react-rsbuild@1.0.3(@rsbuild/core@1.7.3)(@storybook/test@8.6.15(storybook@8.6.15(prettier@3.5.3)))(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.56.0)(storybook@8.6.15(prettier@3.5.3))(typescript@5.8.3)(webpack@5.105.1):
+  storybook-react-rsbuild@1.0.3(@rsbuild/core@1.7.3)(@storybook/test@8.6.15(storybook@8.6.15(prettier@3.5.3)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.56.0)(storybook@8.6.15(prettier@3.5.3))(typescript@5.8.3)(webpack@5.105.1):
     dependencies:
       '@rollup/pluginutils': 5.3.0(rollup@4.56.0)
       '@rsbuild/core': 1.7.3