diff --git a/packages/components/package.json b/packages/components/package.json
index 48593512..9e72db73 100644
--- a/packages/components/package.json
+++ b/packages/components/package.json
@@ -57,21 +57,27 @@
},
"dependencies": {
"@radix-ui/react-tooltip": "^1.1.4",
+ "@shikijs/transformers": "^3.21.0",
"@sindresorhus/slugify": "^2.1.1",
"clsx": "^2.1.1",
"color": "^4.2.3",
+ "comlink": "^4.4.2",
+ "hast": "^1.0.0",
"lodash": "^4.17.21",
"lucide-react": "^0.453.0",
"mermaid": "^11.12.2",
+ "shiki": "^3.17.0",
"tailwind-merge": "^2.6.0"
},
"devDependencies": {
+ "@shikijs/types": "^3.18.0",
"@storybook/addon-docs": "^10.1.10",
"@storybook/react-vite": "^10.1.10",
"@tailwindcss/postcss": "^4.1.18",
"@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.1.18",
"@types/color": "^4.2.0",
+ "@types/hast": "^3.0.4",
"@types/lodash": "4.17.23",
"@types/node": "^22.10.1",
"@types/react": "^18.3.12",
diff --git a/packages/components/src/code.css b/packages/components/src/code.css
new file mode 100644
index 00000000..5e2b1e40
--- /dev/null
+++ b/packages/components/src/code.css
@@ -0,0 +1,398 @@
+@import "./twoslash.css";
+
+.code-block {
+ --fade-width: 0px;
+ --code-padding-right: 48px;
+}
+
+:not(.code-block) > [data-component-part="code-block-root"] {
+ --code-padding-right: 48px;
+}
+
+.code-block:has([data-floating-buttons] > :nth-child(1):last-child) {
+ --fade-width: 99px;
+ --code-padding-right: 99px;
+}
+
+.code-block:has([data-floating-buttons] > :nth-child(2):last-child) {
+ --fade-width: 131px;
+ --code-padding-right: 131px;
+}
+
+.code-block:has([data-floating-buttons] > :nth-child(3):last-child) {
+ --fade-width: 163px;
+ --code-padding-right: 163px;
+}
+
+.code-block:has([data-component-part="code-block-root"].code-block-wrap) {
+ --code-padding-right: 0px !important;
+}
+
+[data-fade-overlay] {
+ @apply absolute top-0 right-0 h-[44px] pointer-events-none z-[1] rounded-tr-2xl rounded-br-xl;
+
+ width: var(--fade-width, 0px);
+ background: linear-gradient(
+ to right,
+ transparent 0px,
+ color-mix(
+ in srgb,
+ var(--fade-color-light, var(--background-light)) 20%,
+ transparent
+ )
+ 10px,
+ color-mix(
+ in srgb,
+ var(--fade-color-light, var(--background-light)) 50%,
+ transparent
+ )
+ 25px,
+ color-mix(
+ in srgb,
+ var(--fade-color-light, var(--background-light)) 80%,
+ transparent
+ )
+ 35px,
+ var(--fade-color-light, var(--background-light)) 45px
+ );
+}
+
+html.dark [data-fade-overlay] {
+ background: linear-gradient(
+ to right,
+ transparent 0px,
+ color-mix(
+ in srgb,
+ var(--fade-color-dark, var(--background-dark)) 20%,
+ transparent
+ )
+ 10px,
+ color-mix(
+ in srgb,
+ var(--fade-color-dark, var(--background-dark)) 50%,
+ transparent
+ )
+ 25px,
+ color-mix(
+ in srgb,
+ var(--fade-color-dark, var(--background-dark)) 80%,
+ transparent
+ )
+ 35px,
+ var(--fade-color-dark, var(--background-dark)) 45px
+ );
+}
+
+:has([data-floating-buttons])
+ > [data-component-part="code-block-root"]
+ pre
+ > code {
+ padding-right: var(--code-padding-right, 0px) !important;
+}
+
+:has([data-component-part="code-block-header"])
+ > [data-component-part="code-block-root"]
+ pre
+ > code {
+ padding-right: var(--code-padding-right, 48px) !important;
+}
+
+[data-component-part="code-block-root"] pre > code > span.line-highlight,
+[data-component-part="code-block-root"] pre > code > span.line-diff {
+ min-width: calc(100% + var(--code-padding-right, 0px));
+}
+
+html.dark .shiki,
+html.dark .shiki span {
+ position: relative;
+ color: var(--shiki-dark) !important;
+ font-weight: var(--shiki-dark-font-weight) !important;
+ text-decoration: var(--shiki-dark-text-decoration) !important;
+}
+
+pre.shiki {
+ background-color: transparent !important;
+}
+
+html.dark .shiki .line-highlight span {
+ background-color: transparent !important;
+}
+
+html.dark .code-block-background {
+ background-color: var(--shiki-dark-bg) !important;
+}
+
+.code-block:has(.line-diff) [data-fade-overlay] {
+ background: transparent !important;
+}
+
+html.dark,
+html:not(.dark) .codeblock-dark {
+ pre.shiki > code {
+ position: relative;
+ z-index: 1;
+ display: block;
+ width: max-content;
+ min-width: 100%;
+ }
+
+ pre[class^="language-diff-"] {
+ @apply flex px-9;
+ }
+
+ pre[class^="language-diff-"] > code {
+ @apply flex-none min-w-full;
+ }
+}
+
+html:not(.dark) .codeblock-light {
+ pre.shiki > code {
+ position: relative;
+ z-index: 1;
+ display: block;
+ width: max-content;
+ min-width: 100%;
+ }
+
+ pre[class^="language-diff-"] {
+ @apply flex px-9;
+ }
+
+ pre[class^="language-diff-"] > code {
+ @apply flex-none min-w-full;
+ }
+}
+
+/* Lines of code outside code blocks. Generated when you use single ticks: `example` */
+:not(pre) > code {
+ @apply break-words rounded-md px-2 py-0.5 text-gray-600 dark:text-gray-200 dark:border-gray-800 bg-gray-100/50 dark:bg-white/5;
+}
+
+:not(pre) > code:before,
+:not(pre) > code:after {
+ content: none !important;
+}
+
+/* text wrapping for code blocks */
+.code-block-wrap pre.shiki > code {
+ width: 100% !important;
+ padding-right: 0 !important;
+ > .line-highlight {
+ padding-right: 3rem !important;
+ }
+}
+.code-block-wrap pre.shiki .line span {
+ white-space: pre-wrap !important;
+ word-break: break-word;
+}
+
+/* line focus */
+.has-focused pre.shiki > code .line {
+ filter: blur(0.1rem);
+ transition:
+ filter 0.35s,
+ opacity 0.35s;
+
+ &:hover {
+ filter: blur(0);
+ }
+}
+
+.has-focused pre.shiki:hover > code .line {
+ filter: blur(0);
+}
+
+.has-focused pre.shiki > code .line-focus {
+ filter: blur(0);
+}
+
+/* line highlighting (without line numbers) */
+[data-component-part="code-block-root"]:not(.has-line-numbers) {
+ .line-highlight {
+ background: color-mix(
+ in srgb,
+ var(--color-primary-light) 20%,
+ transparent
+ ) !important;
+ width: 100%;
+ display: inline-block;
+ position: relative;
+ z-index: 0;
+
+ &::before,
+ &::after {
+ content: "";
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ width: 1rem;
+ background: color-mix(
+ in srgb,
+ var(--color-primary-light) 20%,
+ transparent
+ ) !important;
+ }
+
+ &::before {
+ left: -1rem;
+ border-left: 1px solid var(--color-primary-light);
+ }
+
+ &::after {
+ right: -1rem;
+ }
+ }
+
+ .line-diff {
+ width: 100%;
+ display: inline-block;
+ position: relative;
+ z-index: 0;
+
+ &::before {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: -1rem;
+ width: 1rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ &::after {
+ content: "";
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ right: -1rem;
+ width: 1rem;
+ }
+
+ &.line-add {
+ background: rgb(34 197 94 / 0.15) !important;
+
+ &::before {
+ content: "+";
+ background: rgb(34 197 94 / 0.15) !important;
+ border-left: 1px solid rgb(34 197 94 / 0.8);
+ color: rgb(34 197 94);
+ }
+
+ &::after {
+ background: rgb(34 197 94 / 0.15) !important;
+ }
+ }
+
+ &.line-remove {
+ background: rgb(239 68 68 / 0.15) !important;
+
+ &::before {
+ content: "-";
+ background: rgb(239 68 68 / 0.15) !important;
+ border-left: 1px solid rgb(239 68 68 / 0.8);
+ color: rgb(239 68 68);
+ }
+
+ &::after {
+ background: rgb(239 68 68 / 0.15) !important;
+ }
+ }
+ }
+}
+
+/* line numbers */
+.has-line-numbers pre.shiki > code {
+ counter-reset: step;
+ counter-increment: step 0;
+}
+
+.has-line-numbers pre.shiki > code > .line::before {
+ content: counter(step);
+ counter-increment: step;
+ width: 0.6rem;
+ margin-right: 1.1rem;
+ display: inline-block;
+ text-align: right;
+ user-select: none;
+ .dark & {
+ color: var(--shiki-dark) !important;
+ }
+ :not(.dark) & {
+ color: var(--shiki-light) !important;
+ opacity: 0.4 !important;
+ }
+}
+
+/* line numbers + highlighting */
+.has-line-numbers .line-highlight {
+ width: 100%;
+ display: inline-block;
+ position: relative;
+
+ &::before {
+ display: none;
+ }
+
+ &::after {
+ content: "";
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: -1rem;
+ right: -1rem;
+ background: color-mix(
+ in srgb,
+ var(--color-primary-light) 20%,
+ transparent
+ ) !important;
+ border-left: 1px solid var(--color-primary-light);
+ z-index: -1;
+ }
+}
+
+.has-line-numbers .line-diff {
+ width: 100%;
+ display: inline-block;
+ position: relative;
+
+ &.line-add::before {
+ content: "+" !important;
+ }
+
+ &.line-remove::before {
+ content: "-" !important;
+ }
+
+ &.line-add::after {
+ content: "";
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: -1rem;
+ right: -1rem;
+ background: rgb(34 197 94 / 0.15) !important;
+ border-left: 1px solid rgb(34 197 94 / 0.8);
+ z-index: -1;
+ }
+
+ &.line-remove::after {
+ content: "";
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: -1rem;
+ right: -1rem;
+ background: rgb(239 68 68 / 0.15) !important;
+ border-left: 1px solid rgb(239 68 68 / 0.8);
+ z-index: -1;
+ }
+}
+
+.nct-inline-flex {
+ z-index: -9999;
+}
+
+code[data-symbols="true"] {
+ @apply font-[inherit];
+}
diff --git a/packages/components/src/common/get-code-styling.ts b/packages/components/src/common/get-code-styling.ts
new file mode 100644
index 00000000..78f4e0b3
--- /dev/null
+++ b/packages/components/src/common/get-code-styling.ts
@@ -0,0 +1,48 @@
+import {
+ DEFAULT_DARK_THEME,
+ DEFAULT_LIGHT_THEME,
+ type ShikiThemeType,
+} from "@/utils/shiki/constants";
+import type { CodeStyling } from "@/validation";
+
+const codeStylingToThemeOrThemes = (
+ codeStylingFromConfig: CodeStyling
+): { themes: { light: ShikiThemeType; dark: ShikiThemeType } } => {
+ if (
+ typeof codeStylingFromConfig === "object" &&
+ "theme" in codeStylingFromConfig &&
+ typeof codeStylingFromConfig.theme === "object"
+ ) {
+ return {
+ themes: codeStylingFromConfig.theme,
+ };
+ }
+ if (
+ typeof codeStylingFromConfig === "object" &&
+ "theme" in codeStylingFromConfig &&
+ typeof codeStylingFromConfig.theme === "string"
+ ) {
+ return {
+ themes: {
+ light: codeStylingFromConfig.theme,
+ dark: codeStylingFromConfig.theme,
+ },
+ };
+ }
+ if (codeStylingFromConfig === "dark") {
+ return {
+ themes: {
+ light: DEFAULT_DARK_THEME,
+ dark: DEFAULT_DARK_THEME,
+ },
+ };
+ }
+ return {
+ themes: {
+ light: DEFAULT_LIGHT_THEME,
+ dark: DEFAULT_DARK_THEME,
+ },
+ };
+};
+
+export { codeStylingToThemeOrThemes };
diff --git a/packages/components/src/components/code-block/base-code-block.tsx b/packages/components/src/components/code-block/base-code-block.tsx
new file mode 100644
index 00000000..6aa50dbf
--- /dev/null
+++ b/packages/components/src/components/code-block/base-code-block.tsx
@@ -0,0 +1,211 @@
+import type React from "react";
+
+import {
+ DEFAULT_EXPANDABLE_CODE_BLOCK_HEIGHT,
+ SMALL_EXPANDABLE_CODE_BLOCK_HEIGHT,
+ SMALL_EXPANDABLE_NUMBER_OF_LINES,
+ useExpandable,
+} from "@/hooks/use-expandable";
+import { useGetShikiHighlightedHtml } from "@/hooks/use-get-shiki-highlighted-html";
+import { cn } from "@/utils/cn";
+import { getShikiBackgroundColors } from "@/utils/shiki/get-shiki-background-colors";
+import { getCodeString, useCalculateCodeLines } from "@/utils/shiki/lib";
+
+import type { CodeBlockProps } from "./code-block";
+import { CodeFooter } from "./code-footer";
+
+interface BaseCodeBlockProps extends CodeBlockProps {
+ isParentCodeGroup?: boolean;
+ shouldHighlight?: boolean;
+ // pass isLivePreview to forceExtract to force code string theme changes
+ forceExtract?: boolean;
+}
+
+const BaseCodeBlock = ({
+ expandable = false,
+ shouldHighlight = true,
+ children,
+ isParentCodeGroup,
+ isSmallText,
+ numberOfLines,
+ forceExtract = false,
+ codeBlockTheme = "system",
+ codeBlockThemeObject = "system",
+ ...props
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: TODO
+}: BaseCodeBlockProps) => {
+ const { focus, highlight, lang, language, className, wrap, lines } = props;
+
+ const codeString = getCodeString(children, props.className, forceExtract);
+
+ const opts: {
+ highlightedLines?: number[];
+ focusedLines?: number[];
+ } = {};
+
+ let focusLines: number[] = [];
+ let highlightLines: number[] = [];
+
+ if (focus) {
+ try {
+ const parsed = JSON.parse(focus);
+ focusLines = Array.isArray(parsed) ? parsed : [];
+ } catch {
+ focusLines = [];
+ }
+ }
+
+ if (highlight) {
+ try {
+ const parsed = JSON.parse(highlight);
+ highlightLines = Array.isArray(parsed) ? parsed : [];
+ } catch {
+ highlightLines = [];
+ }
+ }
+
+ if (highlightLines.length > 0) {
+ opts.highlightedLines = highlightLines;
+ }
+ if (focusLines.length > 0) {
+ opts.focusedLines = focusLines;
+ }
+
+ const selectedLang = lang || language;
+
+ const html = useGetShikiHighlightedHtml(
+ selectedLang
+ ? {
+ codeString,
+ codeBlockTheme: codeBlockThemeObject,
+ language: shouldHighlight ? selectedLang : "text",
+ opts,
+ }
+ : {
+ codeString,
+ codeBlockTheme: codeBlockThemeObject,
+ className: shouldHighlight ? className : "lang-text",
+ opts,
+ }
+ );
+
+ numberOfLines = useCalculateCodeLines(html, numberOfLines);
+ const { isExpanded, calculatedHeight, contentRef, toggleExpanded } =
+ useExpandable(expandable, numberOfLines);
+ const shikiBackgroundColors = getShikiBackgroundColors(
+ codeBlockTheme,
+ html,
+ children
+ );
+
+ const isFocusEnabled = focusLines.length > 0;
+ const isHighlightEnabled = highlightLines.length > 0;
+ const getHeight = () => {
+ if (expandable) {
+ if (isExpanded) {
+ return `${calculatedHeight}px`;
+ }
+ if (numberOfLines && numberOfLines < SMALL_EXPANDABLE_NUMBER_OF_LINES) {
+ return `${SMALL_EXPANDABLE_CODE_BLOCK_HEIGHT}px`;
+ }
+ return `${DEFAULT_EXPANDABLE_CODE_BLOCK_HEIGHT}px`;
+ }
+ if (isParentCodeGroup) {
+ return "100%";
+ }
+ return "auto";
+ };
+ const height = getHeight();
+
+ return (
+ <>
+
+ {html ? (
+
+ ) : (
+
+ {children}
+
+ )}
+
+ {!(wrap || isHighlightEnabled || expandable) &&
+ focusLines.length === 0 && (
+
+ )}
+ {expandable && (
+
+ )}
+ >
+ );
+};
+
+export { type BaseCodeBlockProps, BaseCodeBlock };
diff --git a/packages/components/src/components/code-block/code-block.stories.tsx b/packages/components/src/components/code-block/code-block.stories.tsx
new file mode 100644
index 00000000..7f98bc3b
--- /dev/null
+++ b/packages/components/src/components/code-block/code-block.stories.tsx
@@ -0,0 +1,466 @@
+import type { Meta, StoryObj } from "@storybook/react-vite";
+import { CodeBlock } from "./code-block";
+import {
+ complexDiffCode,
+ diffCodeWithRemove,
+ longExpandableCode,
+ longSingleLineCode,
+ mediumCode,
+ simpleDiffCode,
+ singleLineCode,
+} from "./story-utils";
+
+const meta: Meta = {
+ title: "Components/CodeBlock",
+ component: CodeBlock,
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {
+ render: () => (
+
+ {mediumCode}
+
+ ),
+};
+
+export const SingleLineCode: Story = {
+ render: () => {singleLineCode},
+};
+
+export const LongSingleLineCode: Story = {
+ render: () => {longSingleLineCode},
+};
+
+export const SystemTheme: Story = {
+ render: (_, { globals }) => (
+
+ {mediumCode}
+
+ ),
+};
+
+export const DarkTheme: Story = {
+ render: () => (
+
+ {mediumCode}
+
+ ),
+};
+
+export const WithShikiTheme: Story = {
+ render: () => (
+
+ {mediumCode}
+
+ ),
+};
+
+export const WithLightDarkShikiThemes: Story = {
+ render: () => (
+
+ {mediumCode}
+
+ ),
+};
+
+export const WithFilename: Story = {
+ render: () => (
+
+ {mediumCode}
+
+ ),
+};
+
+export const WithIcon: Story = {
+ render: () => (
+
+ {mediumCode}
+
+ ),
+};
+
+export const WithSpacesInFilename: Story = {
+ render: () => (
+
+ {mediumCode}
+
+ ),
+};
+
+export const WithLineNumbers: Story = {
+ render: () => (
+
+ {mediumCode}
+
+ ),
+};
+
+export const WithLineNumbersDark: Story = {
+ render: () => (
+
+ {mediumCode}
+
+ ),
+};
+
+export const WithWrap: Story = {
+ render: () => (
+
+ {mediumCode}
+
+ ),
+};
+
+export const WithWrapAndLineNumbers: Story = {
+ render: () => (
+
+ {mediumCode}
+
+ ),
+};
+
+export const WrapMultiLine: Story = {
+ render: () => {longSingleLineCode},
+};
+
+export const WithHighlight: Story = {
+ render: () => (
+
+ {mediumCode}
+
+ ),
+};
+
+export const WithHighlightAndLineNumbers: Story = {
+ render: () => (
+
+ {mediumCode}
+
+ ),
+};
+
+export const HighlightRange: Story = {
+ render: () => (
+
+ {mediumCode}
+
+ ),
+};
+
+export const HighlightMixed: Story = {
+ render: () => (
+
+ {mediumCode}
+
+ ),
+};
+
+export const HighlightWithShikiTheme: Story = {
+ render: () => (
+
+ {mediumCode}
+
+ ),
+};
+
+export const WithFocus: Story = {
+ render: () => (
+
+ {mediumCode}
+
+ ),
+};
+
+export const WithFocusAndLineNumbers: Story = {
+ render: () => (
+
+ {mediumCode}
+
+ ),
+};
+
+export const FocusRange: Story = {
+ render: () => (
+
+ {mediumCode}
+
+ ),
+};
+
+export const FocusMixed: Story = {
+ render: () => (
+
+ {mediumCode}
+
+ ),
+};
+
+export const HighlightAndFocus: Story = {
+ render: () => (
+
+ {mediumCode}
+
+ ),
+};
+
+export const HighlightAndFocusAndLineNumbers: Story = {
+ render: () => (
+
+ {mediumCode}
+
+ ),
+};
+
+export const HighlightFocusOverlap: Story = {
+ render: () => (
+
+ {mediumCode}
+
+ ),
+};
+
+export const Expandable: Story = {
+ render: () => (
+
+ {longExpandableCode}
+
+ ),
+};
+
+export const ExpandableAndLineNumbers: Story = {
+ render: () => (
+
+ {longExpandableCode}
+
+ ),
+};
+
+export const ExpandableWithHighlight: Story = {
+ render: () => (
+
+ {longExpandableCode}
+
+ ),
+};
+
+export const ExpandableWithFocus: Story = {
+ render: () => (
+
+ {longExpandableCode}
+
+ ),
+};
+
+export const ExpandableAllFeatures: Story = {
+ render: () => (
+
+ {longExpandableCode}
+
+ ),
+};
+
+export const EmptyCode: Story = {
+ render: () => (
+
+ {""}
+
+ ),
+};
+
+export const SpecialCharacters: Story = {
+ render: () => (
+
+ {`// Special characters test
+const symbols = "!@#$%^&*()_+-=[]{}|;':",./<>?";
+const unicode = "Hello 世界 🌍 café naïve résumé";
+const regex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/;`}
+
+ ),
+};
+
+export const DiffBasic: Story = {
+ render: () => (
+
+ {simpleDiffCode}
+
+ ),
+};
+
+export const DiffWithShikiTheme: Story = {
+ render: () => (
+
+ {simpleDiffCode}
+
+ ),
+};
+
+export const DiffWithLineNumbers: Story = {
+ render: () => (
+
+ {simpleDiffCode}
+
+ ),
+};
+
+export const DiffFunction: Story = {
+ render: () => (
+
+ {diffCodeWithRemove}
+
+ ),
+};
+
+export const DiffFunctionWithLineNumbers: Story = {
+ render: () => (
+
+ {diffCodeWithRemove}
+
+ ),
+};
+
+export const DiffReactExample: Story = {
+ render: () => (
+
+ {complexDiffCode}
+
+ ),
+};
+
+export const DiffExpandable: Story = {
+ render: () => (
+
+ {complexDiffCode}
+
+ ),
+};
+
+export const WithCustomClassName: Story = {
+ render: () => (
+
+ {mediumCode}
+
+ ),
+};
diff --git a/packages/components/src/components/code-block/code-block.tsx b/packages/components/src/components/code-block/code-block.tsx
new file mode 100644
index 00000000..00b87e21
--- /dev/null
+++ b/packages/components/src/components/code-block/code-block.tsx
@@ -0,0 +1,153 @@
+import type { ReactNode, RefObject } from "react";
+
+import { Classes } from "@/lib/local/selectors";
+import { cn } from "@/utils/cn";
+import type { CopyToClipboardResult } from "@/utils/copy-to-clipboard";
+import { getNodeText } from "@/utils/get-node-text";
+import type { CodeStyling } from "@/validation";
+
+import { BaseCodeBlock } from "./base-code-block";
+import { CodeHeader } from "./code-header";
+import { CopyToClipboardButton } from "./copy-button";
+
+type CodeBlockProps = {
+ language?: string;
+ filename?: string;
+ icon?: string;
+ lang?: string;
+ className?: string;
+ /**
+ * Whether to show line numbers.
+ */
+ lines?: boolean;
+ /**
+ * Whether to wrap the code block.
+ */
+ wrap?: boolean;
+ /**
+ * Whether to expand the code block.
+ */
+ expandable?: boolean;
+ /**
+ * The lines to highlight. it's a stringified array of numbers.
+ * Example: "[1,3,4,5]"
+ */
+ highlight?: string;
+ /**
+ * The lines to focus on. it's a stringified array of numbers.
+ * Example: "[1,3,4,5]"
+ */
+ focus?: string;
+ /**
+ * Internal prop to set the number of lines in the code block.
+ * Users should not set this prop.
+ */
+ numberOfLines?: number;
+ /**
+ * Prop to hide the ask ai button
+ */
+ hideAskAiButton?: boolean;
+ /**
+ * Prop to set the small text size
+ */
+ isSmallText?: boolean;
+ /**
+ * Pass in from CodeSnippetFeedbackProvider
+ */
+ feedbackModalOpen?: boolean;
+ /**
+ * Pass in from CodeSnippetFeedbackProvider
+ */
+ anchorRef?: RefObject;
+ /**
+ * Prop to set the code block theme (code block UI theme)
+ */
+ codeBlockTheme?: "dark" | "system";
+ /**
+ * Prop to set the code block theme object (syntax highlighting theme)
+ */
+ codeBlockThemeObject?: CodeStyling;
+ /**
+ * Pass in AskAiCodeBlockButton component
+ */
+ askAiButton?: ReactNode;
+ /**
+ * Pass in CodeSnippetFeedbackButton component
+ */
+ feedbackButton?: ReactNode;
+ /**
+ * The callback function when a user clicks on the copied to clipboard button
+ */
+ onCopied?: (result: CopyToClipboardResult, textToCopy?: string) => void;
+ children?: ReactNode;
+};
+
+const CodeBlock = function CodeBlock(params: CodeBlockProps) {
+ const {
+ filename,
+ onCopied,
+ children,
+ className,
+ icon,
+ isSmallText,
+ hideAskAiButton,
+ feedbackModalOpen,
+ anchorRef,
+ codeBlockTheme = "system",
+ codeBlockThemeObject,
+ askAiButton,
+ feedbackButton,
+ } = params;
+
+ const codeString = getNodeText(children);
+ const hasGrayBackgroundContainer = !!filename || !!icon;
+
+ return (
+
+ {hasGrayBackgroundContainer ? (
+
+ {feedbackButton && feedbackButton}
+
+ {askAiButton && askAiButton}
+
+ ) : (
+
+ {feedbackButton && feedbackButton}
+
+ {askAiButton && askAiButton}
+
+ )}
+
+
+ );
+};
+
+export { type CodeBlockProps, CodeBlock };
diff --git a/packages/components/src/components/code-block/code-footer.tsx b/packages/components/src/components/code-block/code-footer.tsx
new file mode 100644
index 00000000..3dc43ac0
--- /dev/null
+++ b/packages/components/src/components/code-block/code-footer.tsx
@@ -0,0 +1,38 @@
+import { EllipsisIcon } from "lucide-react";
+
+type CodeFooterProps = {
+ numberOfLines: number | undefined;
+ isExpanded: boolean;
+ toggleExpanded: () => void;
+};
+
+const CodeFooter = ({
+ numberOfLines,
+ isExpanded,
+ toggleExpanded,
+}: CodeFooterProps) => {
+ if (!numberOfLines) {
+ return null;
+ }
+
+ return (
+
+
+
+ );
+};
+
+export { type CodeFooterProps, CodeFooter };
diff --git a/packages/components/src/components/code-block/code-header.tsx b/packages/components/src/components/code-block/code-header.tsx
new file mode 100644
index 00000000..cc3e6941
--- /dev/null
+++ b/packages/components/src/components/code-block/code-header.tsx
@@ -0,0 +1,52 @@
+import type { ReactNode } from "react";
+
+import { Icon as ComponentIcon } from "@/components/icon";
+import { Classes } from "@/lib/local/selectors";
+import { cn } from "@/utils/cn";
+
+type CodeHeaderProps = {
+ filename?: string;
+ icon?: string;
+ codeBlockTheme?: "dark" | "system";
+ children?: ReactNode;
+};
+
+const CodeHeader = ({
+ filename,
+ icon,
+ codeBlockTheme = "system",
+ children,
+}: CodeHeaderProps) => {
+ return (
+
+
+ {icon && (
+
+ )}
+ {filename}
+
+
+ {children}
+
+
+ );
+};
+
+export { type CodeHeaderProps, CodeHeader };
diff --git a/packages/components/src/components/code-block/copy-button.tsx b/packages/components/src/components/code-block/copy-button.tsx
new file mode 100644
index 00000000..9f71605e
--- /dev/null
+++ b/packages/components/src/components/code-block/copy-button.tsx
@@ -0,0 +1,99 @@
+import { type ComponentPropsWithoutRef, useEffect, useState } from "react";
+import { ActiveCopyButtonIcon, CopyButtonIcon } from "@/icons";
+import { cn } from "@/utils/cn";
+import {
+ type CopyToClipboardResult,
+ copyToClipboard,
+} from "@/utils/copy-to-clipboard";
+
+const DEFAULT_COPY_BUTTON_ARIA_LABEL = "Copy the contents from the code block";
+const DEFAULT_TOOLTIP_COPY_TEXT = "Copy";
+const DEFAULT_TOOLTIP_COPIED_TEXT = "Copied!";
+
+type CopyToClipboardButtonProps = {
+ textToCopy: string;
+ onCopied?: (result: CopyToClipboardResult, textToCopy?: string) => void;
+ className?: string;
+ showTooltip?: boolean;
+ codeBlockTheme?: "system" | "dark";
+ copyButtonAriaLabel?: string;
+ tooltipCopyText?: string;
+ tooltipCopiedText?: string;
+};
+
+const CopyToClipboardButton = ({
+ textToCopy,
+ onCopied,
+ className,
+ showTooltip = true,
+ codeBlockTheme,
+ copyButtonAriaLabel = DEFAULT_COPY_BUTTON_ARIA_LABEL,
+ tooltipCopyText = DEFAULT_TOOLTIP_COPY_TEXT,
+ tooltipCopiedText = DEFAULT_TOOLTIP_COPIED_TEXT,
+ ...buttonProps
+}: CopyToClipboardButtonProps & ComponentPropsWithoutRef<"button">) => {
+ const trimmedTextToCopy = textToCopy.trim();
+ const [isCopiedActive, setIsCopiedActive] = useState(false);
+ const [isDisabled, setIsDisabled] = useState(false);
+
+ useEffect(() => {
+ // Hide copy button if the browser does not support it
+ if (typeof window !== "undefined" && !navigator.clipboard) {
+ console.warn(
+ "The browser's Clipboard API is unavailable. The Clipboard API is only available on HTTPS."
+ );
+ setIsDisabled(true);
+ } else {
+ setIsDisabled(false);
+ }
+ }, []);
+
+ // Hide copy button if you would copy an empty string
+ if (!trimmedTextToCopy || isDisabled) {
+ return null;
+ }
+
+ const onCopy = async () => {
+ const result = await copyToClipboard(trimmedTextToCopy);
+ if (onCopied) {
+ onCopied(result, trimmedTextToCopy);
+ }
+ if (result === "success") {
+ setIsCopiedActive(true);
+ setTimeout(() => {
+ setIsCopiedActive(false);
+ }, 2000);
+ }
+ };
+
+ return (
+
+
+ {showTooltip && (
+
+ {isCopiedActive ? tooltipCopiedText : tooltipCopyText}
+
+ )}
+
+ );
+};
+
+export { type CopyToClipboardButtonProps, CopyToClipboardButton };
diff --git a/packages/components/src/components/code-block/index.tsx b/packages/components/src/components/code-block/index.tsx
new file mode 100644
index 00000000..a9e6c6a4
--- /dev/null
+++ b/packages/components/src/components/code-block/index.tsx
@@ -0,0 +1,3 @@
+// biome-ignore lint/performance/noBarrelFile: TODO
+export { BaseCodeBlock, type BaseCodeBlockProps } from "./base-code-block";
+export { CodeBlock, type CodeBlockProps } from "./code-block";
diff --git a/packages/components/src/components/code-block/story-utils.ts b/packages/components/src/components/code-block/story-utils.ts
new file mode 100644
index 00000000..597356eb
--- /dev/null
+++ b/packages/components/src/components/code-block/story-utils.ts
@@ -0,0 +1,143 @@
+const mediumCode = `class HelloWorld {
+ private String message;
+
+ public HelloWorld(String message) {
+ this.message = message;
+ }
+
+ public void greet() {
+ System.out.println("Hello, World!");
+ System.out.println(this.message);
+ }
+
+ public static void main(String[] args) {
+ HelloWorld hw = new HelloWorld("Welcome to Java!");
+ hw.greet();
+ }
+}`;
+
+const longExpandableCode = `from datetime import datetime, timedelta
+from typing import Dict, List, Optional
+from dataclasses import dataclass
+
+@dataclass
+class Book:
+ title: str
+ author: str
+ isbn: str
+ checked_out: bool = False
+ due_date: Optional[datetime] = None
+
+class Library:
+ def __init__(self):
+ self.books: Dict[str, Book] = {}
+ self.checkouts: Dict[str, List[str]] = {} # patron -> list of ISBNs
+
+ def add_book(self, book: Book) -> None:
+ if book.isbn in self.books:
+ raise ValueError(f"Book with ISBN {book.isbn} already exists")
+ self.books[book.isbn] = book
+
+ def checkout_book(self, isbn: str, patron: str, days: int = 14) -> None:
+ if patron not in self.checkouts:
+ self.checkouts[patron] = []
+
+ book = self.books.get(isbn)
+ if not book:
+ raise ValueError("Book not found")
+
+ if book.checked_out:
+ raise ValueError("Book is already checked out")
+
+ if len(self.checkouts[patron]) >= 3:
+ raise ValueError("Patron has reached checkout limit")
+
+ book.checked_out = True
+ book.due_date = datetime.now() + timedelta(days=days)
+ self.checkouts[patron].append(isbn)
+
+ def return_book(self, isbn: str) -> float:
+ book = self.books.get(isbn)
+ if not book or not book.checked_out:
+ raise ValueError("Book not found or not checked out")
+
+ late_fee = 0.0
+ if datetime.now() > book.due_date:
+ days_late = (datetime.now() - book.due_date).days
+ late_fee = days_late * 0.50
+
+ book.checked_out = False
+ book.due_date = None
+
+ # Remove from patron's checkouts
+ for patron, books in self.checkouts.items():
+ if isbn in books:
+ books.remove(isbn)
+ break
+
+ return late_fee`;
+
+const longSingleLineCode =
+ "To denote a `word` or `phrase` as code, enclose it in backticks (`). The backtick ` is a typographical mark used mainly in computing. It is also known as backquote, grave, or grave accent. This line is intentionally very long to test wrapping behavior and horizontal scrolling.";
+
+const singleLineCode =
+ "To denote a `word` or `phrase` as code, enclose it in backticks (`)";
+
+const diffCodeWithRemove = `function calculateTotal(items) {
+ let total = 0;
+ for (let i = 0; i < items.length; i++) { // [!code --]
+ total += items[i].price; // [!code --]
+ for (const item of items) { // [!code ++]
+ total += item.price; // [!code ++]
+ total += item.tax; // [!code ++]
+ }
+ return total;
+}
+
+function processOrder(order) {
+ const total = calculateTotal(order.items);
+ const discount = calculateDiscount(order); // [!code ++]
+ order.total = total - discount; // [!code ++]
+ order.total = total; // [!code --]
+ return order;
+}`;
+
+const simpleDiffCode = `console.log("Hello World");
+console.log("Old message"); // [!code --]
+console.log("New message"); // [!code ++]
+console.log("End");`;
+
+const complexDiffCode = `import React from 'react';
+import { useState } from 'react'; // [!code --]
+import { useState, useEffect } from 'react'; // [!code ++]
+
+function MyComponent() {
+ const [count, setCount] = useState(0);
+ const [isLoading, setIsLoading] = useState(false); // [!code ++]
+
+ useEffect(() => { // [!code ++]
+ setIsLoading(true); // [!code ++]
+ // Simulate async operation // [!code ++]
+ setTimeout(() => setIsLoading(false), 1000); // [!code ++]
+ }, []); // [!code ++]
+
+ return (
+
+
Count: {count}
// [!code --]
+ {isLoading ? 'Loading...' : \`Count: \${count}\`}
// [!code ++]
+
+
+ );
+}`;
+
+export {
+ mediumCode,
+ longExpandableCode,
+ longSingleLineCode,
+ singleLineCode,
+ diffCodeWithRemove,
+ simpleDiffCode,
+ complexDiffCode,
+};
diff --git a/packages/components/src/components/index.ts b/packages/components/src/components/index.ts
index c0fbbfba..1ba93437 100644
--- a/packages/components/src/components/index.ts
+++ b/packages/components/src/components/index.ts
@@ -3,6 +3,7 @@ export * from "./accordion";
export * from "./badge";
export * from "./callout";
export * from "./card";
+export * from "./code-block";
export * from "./code-group";
export * from "./color";
export * from "./columns";
diff --git a/packages/components/src/constants/index.ts b/packages/components/src/constants/index.ts
index 890ef254..07221844 100644
--- a/packages/components/src/constants/index.ts
+++ b/packages/components/src/constants/index.ts
@@ -1,4 +1,5 @@
const MINTLIFY_ICONS_CDN_URL = "https://d3gk2c5xim1je2.cloudfront.net";
const MAX_DEFAULT_VALUE_LENGTH = 50;
+const MAX_PREVIEW_BYTES = 50_000; // 0.05 mb / 50 kb
-export { MINTLIFY_ICONS_CDN_URL, MAX_DEFAULT_VALUE_LENGTH };
+export { MINTLIFY_ICONS_CDN_URL, MAX_DEFAULT_VALUE_LENGTH, MAX_PREVIEW_BYTES };
diff --git a/packages/components/src/hooks/use-expandable.ts b/packages/components/src/hooks/use-expandable.ts
new file mode 100644
index 00000000..a22aa6c2
--- /dev/null
+++ b/packages/components/src/hooks/use-expandable.ts
@@ -0,0 +1,47 @@
+import { useEffect, useRef, useState } from "react";
+
+const SMALL_EXPANDABLE_NUMBER_OF_LINES = 7;
+const SMALL_EXPANDED_HEIGHT_OFFSET = 30;
+const EXPANDED_HEIGHT_OFFSET = 40;
+const SMALL_EXPANDABLE_CODE_BLOCK_HEIGHT = 45;
+const DEFAULT_EXPANDABLE_CODE_BLOCK_HEIGHT = 190;
+
+const useExpandable = (enable: boolean, numberOfLines?: number) => {
+ const [isExpanded, setIsExpanded] = useState(false);
+ const [calculatedHeight, setCalculatedHeight] = useState(0);
+ const contentRef = useRef(null);
+
+ useEffect(() => {
+ let rafId: number | undefined;
+ if (contentRef.current != null && enable) {
+ rafId = requestAnimationFrame(() => {
+ if (contentRef.current != null) {
+ setCalculatedHeight(
+ contentRef.current.scrollHeight +
+ (numberOfLines && numberOfLines < SMALL_EXPANDABLE_NUMBER_OF_LINES
+ ? SMALL_EXPANDED_HEIGHT_OFFSET
+ : EXPANDED_HEIGHT_OFFSET)
+ );
+ }
+ });
+ }
+ return () => {
+ if (rafId !== undefined) {
+ cancelAnimationFrame(rafId);
+ }
+ };
+ }, [enable, numberOfLines]);
+
+ const toggleExpanded = () => setIsExpanded((prev) => !prev);
+
+ return { isExpanded, calculatedHeight, contentRef, toggleExpanded };
+};
+
+export {
+ SMALL_EXPANDABLE_NUMBER_OF_LINES,
+ SMALL_EXPANDED_HEIGHT_OFFSET,
+ EXPANDED_HEIGHT_OFFSET,
+ SMALL_EXPANDABLE_CODE_BLOCK_HEIGHT,
+ DEFAULT_EXPANDABLE_CODE_BLOCK_HEIGHT,
+ useExpandable,
+};
diff --git a/packages/components/src/hooks/use-get-shiki-highlighted-html.ts b/packages/components/src/hooks/use-get-shiki-highlighted-html.ts
new file mode 100644
index 00000000..99cae206
--- /dev/null
+++ b/packages/components/src/hooks/use-get-shiki-highlighted-html.ts
@@ -0,0 +1,56 @@
+import { useState } from "react";
+
+import { useIsomorphicLayoutEffect } from "@/hooks/use-isomorphic-layout-effect";
+import {
+ getShikiHighlightedHtml,
+ type ShikiHighlightedHtmlArgs,
+} from "@/utils/shiki";
+
+const useGetShikiHighlightedHtml = (
+ props: ShikiHighlightedHtmlArgs
+): string | undefined => {
+ const htmlOrPromise = getShikiHighlightedHtml(props);
+
+ const [html, setHtml] = useState(
+ htmlOrPromise instanceof Promise ? undefined : htmlOrPromise
+ );
+
+ useIsomorphicLayoutEffect(() => {
+ if (!(htmlOrPromise instanceof Promise)) {
+ setHtml(htmlOrPromise);
+ return;
+ }
+
+ let cancelled = false;
+
+ async function getHtml() {
+ if (!(htmlOrPromise instanceof Promise)) {
+ setHtml(htmlOrPromise);
+ return;
+ }
+
+ try {
+ const result = await htmlOrPromise;
+ if (!cancelled) {
+ setHtml(result);
+ }
+ } catch (err) {
+ console.error(err);
+ if (!cancelled) {
+ setHtml(undefined);
+ }
+ }
+ }
+
+ // biome-ignore lint/complexity/noVoid: TODO
+ void getHtml();
+
+ return () => {
+ cancelled = true;
+ };
+ }, [props, htmlOrPromise]);
+
+ return html;
+};
+
+export { useGetShikiHighlightedHtml };
diff --git a/packages/components/src/hooks/use-isomorphic-layout-effect.ts b/packages/components/src/hooks/use-isomorphic-layout-effect.ts
new file mode 100644
index 00000000..9bd32554
--- /dev/null
+++ b/packages/components/src/hooks/use-isomorphic-layout-effect.ts
@@ -0,0 +1,6 @@
+import { useEffect, useLayoutEffect } from "react";
+
+const useIsomorphicLayoutEffect =
+ typeof window !== "undefined" ? useLayoutEffect : useEffect;
+
+export { useIsomorphicLayoutEffect };
diff --git a/packages/components/src/icons/index.tsx b/packages/components/src/icons/index.tsx
index 9a0f540b..21beef74 100644
--- a/packages/components/src/icons/index.tsx
+++ b/packages/components/src/icons/index.tsx
@@ -269,7 +269,72 @@ const LinkIcon = (props: SVGProps) => {
);
};
+const ActiveCopyButtonIcon = ({
+ codeBlockTheme = "system",
+}: {
+ codeBlockTheme?: "system" | "dark";
+}) => {
+ return (
+
+ );
+};
+
+const CopyButtonIcon = ({
+ codeBlockTheme = "system",
+}: {
+ codeBlockTheme?: "system" | "dark";
+}) => {
+ return (
+
+ );
+};
+
export {
+ ActiveCopyButtonIcon,
+ CopyButtonIcon,
LinkIcon,
InfoIcon,
Folder2OpenIcon,
diff --git a/packages/components/src/mdx/index.ts b/packages/components/src/mdx/index.ts
new file mode 100644
index 00000000..da6d8bee
--- /dev/null
+++ b/packages/components/src/mdx/index.ts
@@ -0,0 +1,66 @@
+const SHIKI_THEMES = [
+ "andromeeda",
+ "aurora-x",
+ "ayu-dark",
+ "catppuccin-frappe",
+ "catppuccin-latte",
+ "catppuccin-macchiato",
+ "catppuccin-mocha",
+ "dark-plus",
+ "dracula",
+ "dracula-soft",
+ "everforest-dark",
+ "everforest-light",
+ "github-dark",
+ "github-dark-default",
+ "github-dark-dimmed",
+ "github-dark-high-contrast",
+ "github-light",
+ "github-light-default",
+ "github-light-high-contrast",
+ "gruvbox-dark-hard",
+ "gruvbox-dark-medium",
+ "gruvbox-dark-soft",
+ "gruvbox-light-hard",
+ "gruvbox-light-medium",
+ "gruvbox-light-soft",
+ "houston",
+ "kanagawa-dragon",
+ "kanagawa-lotus",
+ "kanagawa-wave",
+ "laserwave",
+ "light-plus",
+ "material-theme",
+ "material-theme-darker",
+ "material-theme-lighter",
+ "material-theme-ocean",
+ "material-theme-palenight",
+ "min-dark",
+ "min-light",
+ "monokai",
+ "night-owl",
+ "nord",
+ "one-dark-pro",
+ "one-light",
+ "plastic",
+ "poimandres",
+ "red",
+ "rose-pine",
+ "rose-pine-dawn",
+ "rose-pine-moon",
+ "slack-dark",
+ "slack-ochin",
+ "snazzy-light",
+ "solarized-dark",
+ "solarized-light",
+ "synthwave-84",
+ "tokyo-night",
+ "vesper",
+ "vitesse-black",
+ "vitesse-dark",
+ "vitesse-light",
+
+ "css-variables", // for users who want to use custom CSS to style their code blocks
+] as const;
+
+export { SHIKI_THEMES };
diff --git a/packages/components/src/plugins/scrollbar.ts b/packages/components/src/plugins/scrollbar.ts
new file mode 100644
index 00000000..0889b06e
--- /dev/null
+++ b/packages/components/src/plugins/scrollbar.ts
@@ -0,0 +1,86 @@
+import plugin from "tailwindcss/plugin";
+
+export default plugin(({ addUtilities }) => {
+ const scrollbarBase = {
+ "scrollbar-width": "thin",
+ "&::-webkit-scrollbar": {
+ width: "8px",
+ height: "8px",
+ },
+ "&::-webkit-scrollbar-track": {
+ background: "transparent",
+ },
+ "&::-webkit-scrollbar-thumb": {
+ borderRadius: "9999px",
+ },
+ };
+
+ addUtilities({
+ ".scrollbar-code-dark": {
+ ...scrollbarBase,
+ "scrollbar-color": "rgb(255 255 255 / 0.2) transparent",
+ "&::-webkit-scrollbar-thumb": {
+ borderRadius: "9999px",
+ backgroundColor: "rgb(255 255 255 / 0.2)",
+ },
+ "&:hover::-webkit-scrollbar-thumb": {
+ backgroundColor: "rgb(255 255 255 / 0.25)",
+ },
+ "&:active::-webkit-scrollbar-thumb": {
+ backgroundColor: "rgb(255 255 255 / 0.25)",
+ },
+ },
+ ".scrollbar-code-system": {
+ ...scrollbarBase,
+ "scrollbar-color": "rgb(0 0 0 / 0.15) transparent",
+ "&::-webkit-scrollbar-thumb": {
+ borderRadius: "9999px",
+ backgroundColor: "rgb(0 0 0 / 0.15)",
+ },
+ "&:hover::-webkit-scrollbar-thumb": {
+ backgroundColor: "rgb(0 0 0 / 0.2)",
+ },
+ "&:active::-webkit-scrollbar-thumb": {
+ backgroundColor: "rgb(0 0 0 / 0.2)",
+ },
+ },
+ ".dark .scrollbar-code-system": {
+ "scrollbar-color": "rgb(255 255 255 / 0.2) transparent",
+ "&::-webkit-scrollbar-thumb": {
+ backgroundColor: "rgb(255 255 255 / 0.2)",
+ },
+ "&:hover::-webkit-scrollbar-thumb": {
+ backgroundColor: "rgb(255 255 255 / 0.25)",
+ },
+ "&:active::-webkit-scrollbar-thumb": {
+ backgroundColor: "rgb(255 255 255 / 0.25)",
+ },
+ },
+ ".scrollbar-common": {
+ ...scrollbarBase,
+ "scrollbar-color": "rgb(0 0 0 / 0.2) transparent",
+ "&::-webkit-scrollbar-thumb": {
+ borderRadius: "9999px",
+ backgroundColor: "rgb(0 0 0 / 0.2)",
+ },
+ "&:hover::-webkit-scrollbar-thumb": {
+ backgroundColor: "rgb(0 0 0 / 0.25)",
+ },
+ "&:active::-webkit-scrollbar-thumb": {
+ backgroundColor: "rgb(0 0 0 / 0.25)",
+ },
+ },
+ ".dark .scrollbar-common": {
+ "scrollbar-color": "rgb(255 255 255 / 0.2) transparent",
+ "&::-webkit-scrollbar-thumb": {
+ backgroundColor: "rgb(255 255 255 / 0.2)",
+ },
+ "&:hover::-webkit-scrollbar-thumb": {
+ backgroundColor: "rgb(255 255 255 / 0.25)",
+ },
+ "&:active::-webkit-scrollbar-thumb": {
+ backgroundColor: "rgb(255 255 255 / 0.25)",
+ },
+ },
+ });
+});
diff --git a/packages/components/src/styles.css b/packages/components/src/styles.css
index 471da8e7..ac73fb9e 100644
--- a/packages/components/src/styles.css
+++ b/packages/components/src/styles.css
@@ -1,7 +1,9 @@
@import "tailwindcss";
+@import "./code.css";
@plugin "@tailwindcss/typography";
@plugin "./plugins/bg-grid.ts";
+@plugin "./plugins/scrollbar.ts";
@source "./**/*.{js,jsx,ts,tsx}";
diff --git a/packages/components/src/twoslash.css b/packages/components/src/twoslash.css
new file mode 100644
index 00000000..1da89c14
--- /dev/null
+++ b/packages/components/src/twoslash.css
@@ -0,0 +1,320 @@
+/* modified from https://github.com/shikijs/shiki/blob/main/packages/twoslash/style-rich.css */
+/* ===== Basic ===== */
+:root {
+ --twoslash-border-color: #dbdfde;
+ --twoslash-underline-color: currentColor;
+ --twoslash-popup-bg: #f3f7f6;
+ --twoslash-popup-color: inherit;
+ --twoslash-popup-shadow: rgba(0, 0, 0, 0.08) 0px 1px 4px;
+ --twoslash-code-font: inherit;
+ --twoslash-code-font-size: 1em;
+ --twoslash-matched-color: inherit;
+
+ --twoslash-highlighted-border: #c37d0d50;
+ --twoslash-highlighted-bg: #c37d0d20;
+ --twoslash-unmatched-color: #888;
+ --twoslash-cursor-color: #8888;
+ --twoslash-error-color: #d45656;
+ --twoslash-error-bg: #d4565620;
+ --twoslash-warn-color: #c37d0d;
+ --twoslash-warn-bg: #c37d0d20;
+ --twoslash-tag-color: #3772cf;
+ --twoslash-tag-bg: #3772cf20;
+ --twoslash-tag-warn-color: var(--twoslash-warn-color);
+ --twoslash-tag-warn-bg: var(--twoslash-warn-bg);
+ --twoslash-tag-annotate-color: #1ba673;
+ --twoslash-tag-annotate-bg: #1ba67320;
+ --twoslash-text-size: 0.8rem;
+ --twoslash-docs-tag-style: italic;
+}
+
+:root.twoslash-dark,
+html.dark div.twoslash-dark,
+html.dark div.dark\:twoslash-dark {
+ --twoslash-border-color: #222526;
+ --twoslash-popup-bg: #151819;
+ --twoslash-highlighted-border: #ffa50080;
+ --twoslash-highlighted-bg: #ffa50030;
+ --twoslash-unmatched-color: #aaa;
+ --twoslash-cursor-color: #bbbb;
+ --twoslash-error-color: #ff6b6b;
+ --twoslash-error-bg: #ff6b6b30;
+ --twoslash-warn-color: #ffa500;
+ --twoslash-warn-bg: #ffa50030;
+ --twoslash-tag-color: #6bb6ff;
+ --twoslash-tag-bg: #6bb6ff30;
+ --twoslash-tag-warn-color: var(--twoslash-warn-color);
+ --twoslash-tag-warn-bg: var(--twoslash-warn-bg);
+ --twoslash-tag-annotate-color: #4ade80;
+ --twoslash-tag-annotate-bg: #4ade8030;
+}
+
+/* Respect people's wishes to not have animations */
+@media (prefers-reduced-motion: reduce) {
+ .twoslash * {
+ transition: none !important;
+ }
+}
+
+/* ===== Hover Info ===== */
+.twoslash:hover .twoslash-hover {
+ border-color: var(--twoslash-underline-color);
+}
+
+.twoslash .twoslash-hover {
+ border-bottom: 1px dotted transparent;
+ transition: border-color 0.3s;
+ transition-timing-function: ease;
+ position: relative;
+}
+
+.twoslash a span.twoslash-hover {
+ border-bottom: 1px solid var(--twoslash-underline-color);
+ position: relative;
+}
+
+.twoslash a span.twoslash-hover:hover {
+ opacity: 0.75;
+}
+
+.twoslash-popup-container .code-block {
+ width: 100%;
+ margin-top: 8px !important;
+ margin-bottom: 8px !important;
+}
+
+.mint-twoslash-popover div[data-component-part="code-block-root"] {
+ width: 100%;
+}
+
+/* ===== Popup Override ===== */
+[data-radix-popper-content-wrapper] {
+ z-index: 9999 !important;
+}
+
+.mint-twoslash-popover {
+ background: var(--twoslash-popup-bg);
+ color: var(--twoslash-popup-color);
+ border: 1px solid var(--twoslash-border-color);
+ border-radius: 10px;
+ font-size: var(--twoslash-text-size) !important;
+ pointer-events: auto;
+ text-align: left;
+ box-shadow: var(--twoslash-popup-shadow);
+ display: inline-flex;
+ flex-direction: column;
+ padding: 6px;
+ max-width: 50vw;
+}
+
+.mint-twoslash-popover-pre {
+ width: 100%;
+ display: flex;
+ font-size: var(--twoslash-text-size);
+ font-family: var(--twoslash-code-font);
+ font-weight: 400;
+}
+
+.mint-twoslash-popover code {
+ padding: 0 !important;
+ background: transparent !important;
+}
+
+code.twoslash-popup-code.shiki {
+ padding: 8px !important;
+}
+
+.mint-twoslash-popover:hover {
+ user-select: auto;
+}
+
+.twoslash .twoslash-popup-arrow {
+ display: none;
+}
+
+.twoslash-popup-code,
+.twoslash-popup-error,
+.twoslash-popup-docs {
+ padding: 6px 8px !important;
+}
+
+.mint-twoslash-popover .twoslash-popup-docs {
+ max-width: unset;
+}
+
+.mint-twoslash-popover .twoslash-popup-error {
+ color: var(--twoslash-error-color);
+ background-color: var(--twoslash-error-bg);
+}
+
+.mint-twoslash-popover .twoslash-popup-docs-tags {
+ display: flex;
+ flex-direction: column;
+}
+
+.mint-twoslash-popover .twoslash-popup-docs-tag-name {
+ margin-right: 0.5em;
+ font-style: var(--twoslash-docs-tag-style);
+}
+
+.mint-twoslash-popover .twoslash-popup-docs-tag-name {
+ font-family: var(--twoslash-code-font);
+}
+
+/* ===== Query Line ===== */
+.mint-twoslash-popover .twoslash-query-line .twoslash-popup-container {
+ position: relative;
+ margin-bottom: 1.4em;
+ transform: translateY(0.6em);
+}
+
+div.twoslash-meta-line.twoslash-query-line {
+ display: contents;
+}
+
+/* ===== Error Line ===== */
+.twoslash-error-line {
+ position: relative;
+ background-color: var(--twoslash-error-bg);
+ border-left: 3px solid var(--twoslash-error-color);
+ color: var(--twoslash-error-color);
+ padding: 6px 12px;
+ margin: 0.2em 0;
+ min-width: 100%;
+ width: max-content;
+}
+
+.twoslash-error-line.twoslash-error-level-warning {
+ background-color: var(--twoslash-warn-bg);
+ border-left: 3px solid var(--twoslash-warn-color);
+ color: var(--twoslash-warn-color);
+}
+
+.mint-twoslash-popover .twoslash-error {
+ background: url("data:image/svg+xml,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%200%206%203'%20enable-background%3D'new%200%200%206%203'%20height%3D'3'%20width%3D'6'%3E%3Cg%20fill%3D'%23c94824'%3E%3Cpolygon%20points%3D'5.5%2C0%202.5%2C3%201.1%2C3%204.1%2C0'%2F%3E%3Cpolygon%20points%3D'4%2C0%206%2C2%206%2C0.6%205.4%2C0'%2F%3E%3Cpolygon%20points%3D'0%2C2%201%2C3%202.4%2C3%200%2C0.6'%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E")
+ repeat-x bottom left;
+ padding-bottom: 2px;
+}
+
+.mint-twoslash-popover .twoslash-error.twoslash-error-level-warning {
+ background: url("data:image/svg+xml,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%200%206%203'%20enable-background%3D'new%200%200%206%203'%20height%3D'3'%20width%3D'6'%3E%3Cg%20fill%3D'%23c37d0d'%3E%3Cpolygon%20points%3D'5.5%2C0%202.5%2C3%201.1%2C3%204.1%2C0'%2F%3E%3Cpolygon%20points%3D'4%2C0%206%2C2%206%2C0.6%205.4%2C0'%2F%3E%3Cpolygon%20points%3D'0%2C2%201%2C3%202.4%2C3%200%2C0.6'%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E")
+ repeat-x bottom left;
+ padding-bottom: 2px;
+}
+
+/* ===== Completeions ===== */
+.mint-twoslash-popover .twoslash-completion-cursor {
+ position: relative;
+}
+
+.mint-twoslash-popover .twoslash-completion-cursor .twoslash-completion-list {
+ user-select: none;
+ position: absolute;
+ top: 0;
+ left: 0;
+ transform: translate(0, 1.2em);
+ margin: 3px 0 0 -1px;
+ display: inline-block;
+ z-index: 8;
+ box-shadow: var(--twoslash-popup-shadow);
+ background: var(--twoslash-popup-bg);
+ border: 1px solid var(--twoslash-border-color);
+}
+
+.twoslash-completion-list {
+ width: 240px;
+ font-size: var(--twoslash-text-size);
+ padding: 4px;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.twoslash-completion-list:hover {
+ user-select: auto;
+}
+
+.twoslash-completion-list::before {
+ background-color: var(--twoslash-cursor-color);
+ width: 2px;
+ position: absolute;
+ top: -1.6em;
+ height: 1.4em;
+ left: -1px;
+ content: " ";
+}
+
+.twoslash-completion-list li {
+ overflow: hidden;
+ display: flex;
+ align-items: center;
+ gap: 0.25em;
+ line-height: 1em;
+}
+
+.twoslash-completion-list li span.twoslash-completions-unmatched {
+ color: var(--twoslash-unmatched-color);
+}
+
+.twoslash-completion-list .deprecated {
+ text-decoration: line-through;
+ opacity: 0.5;
+}
+
+.twoslash-completion-list li span.twoslash-completions-matched {
+ color: var(--twoslash-matched-color);
+}
+
+/* Highlights */
+.twoslash-highlighted {
+ background-color: var(--twoslash-highlighted-bg);
+ border: 1px solid var(--twoslash-highlighted-border);
+ padding: 1px 2px;
+ margin: -1px -3px;
+ border-radius: 4px;
+}
+
+/* Icons */
+.twoslash-completion-list .twoslash-completions-icon {
+ color: var(--twoslash-unmatched-color);
+ width: 1em;
+ flex: none;
+}
+
+/* Custom Tags */
+.mint-twoslash-popover .twoslash-tag-line {
+ position: relative;
+ background-color: var(--twoslash-tag-bg);
+ border-left: 3px solid var(--twoslash-tag-color);
+ color: var(--twoslash-tag-color);
+ padding: 6px 10px;
+ margin: 0.2em 0;
+ display: flex;
+ align-items: center;
+ gap: 0.3em;
+ min-width: 100%;
+ width: max-content;
+}
+
+.mint-twoslash-popover .twoslash-tag-line .twoslash-tag-icon {
+ width: 1.1em;
+ color: inherit;
+}
+
+.twoslash-tag-line.twoslash-tag-error-line {
+ background-color: var(--twoslash-error-bg);
+ border-left: 3px solid var(--twoslash-error-color);
+ color: var(--twoslash-error-color);
+}
+
+.mint-twoslash-popover .twoslash-tag-line.twoslash-tag-warn-line {
+ background-color: var(--twoslash-tag-warn-bg);
+ border-left: 3px solid var(--twoslash-tag-warn-color);
+ color: var(--twoslash-tag-warn-color);
+}
+
+.mint-twoslash-popover .twoslash-tag-line.twoslash-tag-annotate-line {
+ background-color: var(--twoslash-tag-annotate-bg);
+ border-left: 3px solid var(--twoslash-tag-annotate-color);
+ color: var(--twoslash-tag-annotate-color);
+}
diff --git a/packages/components/src/utils/get-node-text.ts b/packages/components/src/utils/get-node-text.ts
new file mode 100644
index 00000000..aa9729f7
--- /dev/null
+++ b/packages/components/src/utils/get-node-text.ts
@@ -0,0 +1,26 @@
+// Gets the text from a component as if you selected it with a mouse and copied it.
+const getNodeText = (node: unknown): string => {
+ if (typeof node === "string" || typeof node === "number") {
+ // Convert number into string
+ return node.toString();
+ }
+
+ if (Array.isArray(node)) {
+ return node.map(getNodeText).join("");
+ }
+
+ if (
+ node &&
+ typeof node === "object" &&
+ "props" in node &&
+ node.props &&
+ typeof node.props === "object" &&
+ "children" in node.props
+ ) {
+ return getNodeText(node.props.children);
+ }
+
+ return "";
+};
+
+export { getNodeText };
diff --git a/packages/components/src/utils/icon-utils.ts b/packages/components/src/utils/icon-utils.ts
index b50dc496..abf707c3 100644
--- a/packages/components/src/utils/icon-utils.ts
+++ b/packages/components/src/utils/icon-utils.ts
@@ -1,6 +1,7 @@
import { MINTLIFY_ICONS_CDN_URL } from "@/constants";
import { FONT_AWESOME_BRANDS } from "@/constants/icons/font-awesome/v7.1.0/brands";
import type { IconLibrary, IconType } from "@/models";
+import { getIconKey } from "@/utils/shiki/snippet-presets";
const getIconUrl = (
icon: string,
@@ -11,6 +12,11 @@ const getIconUrl = (
return `${MINTLIFY_ICONS_CDN_URL}/lucide/v0.545.0/${icon}.svg`;
}
+ const languageIconUrl = getLanguageIconUrl(icon);
+ if (languageIconUrl) {
+ return languageIconUrl;
+ }
+
if (isBrandsIcon(icon)) {
return `${MINTLIFY_ICONS_CDN_URL}/v7.1.0/brands/${icon}.svg`;
}
@@ -28,4 +34,12 @@ const isBrandsIcon = (icon?: string): boolean => {
return FONT_AWESOME_BRANDS.includes(icon.toLowerCase());
};
-export { getIconUrl, isBrandsIcon };
+const getLanguageIconUrl = (language: string): string | null => {
+ const iconKey = getIconKey(language);
+ if (!iconKey) {
+ return null;
+ }
+ return `${MINTLIFY_ICONS_CDN_URL}/devicon/${iconKey}.svg`;
+};
+
+export { getIconUrl, isBrandsIcon, getLanguageIconUrl };
diff --git a/packages/components/src/utils/shiki/constants.ts b/packages/components/src/utils/shiki/constants.ts
new file mode 100644
index 00000000..f4880104
--- /dev/null
+++ b/packages/components/src/utils/shiki/constants.ts
@@ -0,0 +1,173 @@
+import type { BuiltinTheme } from "shiki/types";
+import { SHIKI_THEMES } from "@/mdx";
+
+const DEFAULT_DARK_THEME = "dark-plus";
+const DEFAULT_LIGHT_THEME = "github-light-default";
+type ShikiThemeType = (typeof SHIKI_THEMES)[number];
+
+const LINE_HIGHLIGHT_CLASS_NAME = "line-highlight";
+const LINE_FOCUS_CLASS_NAME = "line-focus";
+const LINE_DIFF_ADD_CLASS_NAME = "line-diff line-add";
+const LINE_DIFF_REMOVE_CLASS_NAME = "line-diff line-remove";
+
+const SHIKI_THEMES_EXCLUDING_CSS_VARIABLES_AND_DEFAULTS = SHIKI_THEMES.filter(
+ (theme) =>
+ theme !== "css-variables" &&
+ theme !== DEFAULT_DARK_THEME &&
+ theme !== DEFAULT_LIGHT_THEME
+);
+
+const DEFAULT_DARK_BG = "#0B0C0E";
+const DEFAULT_LIGHT_BG = "#FFFFFF";
+
+const THEMES: [BuiltinTheme, BuiltinTheme, ...BuiltinTheme[]] = [
+ DEFAULT_DARK_THEME,
+ DEFAULT_LIGHT_THEME,
+ ...SHIKI_THEMES_EXCLUDING_CSS_VARIABLES_AND_DEFAULTS,
+] as const;
+
+const LANGS = [
+ "bash" as const,
+ "c" as const,
+ "c++" as const,
+ "dart" as const,
+ "go" as const,
+ "java" as const,
+ "javascript" as const,
+ "json" as const,
+ "kotlin" as const,
+ "php" as const,
+ "python" as const,
+ "ruby" as const,
+ "rust" as const,
+ "swift" as const,
+ "csharp" as const,
+ "typescript" as const,
+ "tsx" as const,
+ "yaml" as const,
+];
+
+type ShikiTheme = BuiltinTheme;
+type ShikiLanguage = (typeof LANGS)[number];
+
+const SHIKI_COLOR_REPLACEMENTS: Record<
+ string,
+ string | Record
+> = {
+ [THEMES[0]]: {
+ "#1e1e1e": DEFAULT_DARK_BG,
+ },
+};
+
+const SHIKI_THEME_MAP: Record<"dark" | "light", ShikiTheme> = {
+ dark: THEMES[0],
+ light: THEMES[1],
+} as const;
+
+const SHIKI_LANG_MAP: Record = {
+ curl: "bash",
+ bash: "bash",
+ sh: "bash",
+ shell: "bash",
+ zsh: "bash",
+ shellscript: "bash",
+
+ c: "c",
+ csharp: "csharp",
+
+ "c++": "c++",
+ cpp: "c++",
+ cc: "c++",
+
+ go: "go",
+ golang: "go",
+
+ java: "java",
+
+ javascript: "javascript",
+ js: "javascript",
+ node: "javascript",
+ nodejs: "javascript",
+
+ json: "json",
+ jsonc: "json",
+ json5: "json",
+
+ php: "php",
+
+ python: "python",
+ py: "python",
+
+ typescript: "typescript",
+ ts: "typescript",
+
+ tsx: "tsx",
+ react: "tsx",
+ reactts: "tsx",
+ "react-ts": "tsx",
+ jsx: "tsx",
+
+ ruby: "ruby",
+ rb: "ruby",
+
+ rust: "rust",
+ rs: "rust",
+ rustc: "rust",
+
+ swift: "swift",
+ kotlin: "kotlin",
+ kt: "kotlin",
+ dart: "dart",
+ flutter: "dart",
+
+ yaml: "yaml",
+ yml: "yaml",
+ toml: "yaml",
+} as const;
+
+const SHIKI_DISPLAY_LANG_MAP: Record = {
+ bash: "cURL",
+ c: "C",
+ "c++": "C++",
+ "c#": "C#",
+ ".NET": ".NET",
+ dart: "Dart",
+ go: "Go",
+ java: "Java",
+ javascript: "Javascript",
+ json: "JSON",
+ kotlin: "Kotlin",
+ node: "Node.js",
+ php: "PHP",
+ python: "Python",
+ ruby: "Ruby",
+ rust: "Rust",
+ swift: "Swift",
+ typescript: "Typescript",
+ tsx: "TSX",
+ yaml: "YAML",
+} as const;
+
+const SHIKI_CLASSNAME = "shiki shiki-themes";
+
+export {
+ SHIKI_CLASSNAME,
+ SHIKI_COLOR_REPLACEMENTS,
+ SHIKI_DISPLAY_LANG_MAP,
+ SHIKI_LANG_MAP,
+ SHIKI_THEME_MAP,
+ type ShikiTheme,
+ type ShikiLanguage,
+ THEMES,
+ LANGS,
+ DEFAULT_DARK_BG,
+ DEFAULT_LIGHT_BG,
+ LINE_HIGHLIGHT_CLASS_NAME,
+ LINE_FOCUS_CLASS_NAME,
+ LINE_DIFF_ADD_CLASS_NAME,
+ LINE_DIFF_REMOVE_CLASS_NAME,
+ SHIKI_THEMES_EXCLUDING_CSS_VARIABLES_AND_DEFAULTS,
+ DEFAULT_DARK_THEME,
+ DEFAULT_LIGHT_THEME,
+ type ShikiThemeType,
+};
diff --git a/packages/components/src/utils/shiki/get-shiki-background-colors.ts b/packages/components/src/utils/shiki/get-shiki-background-colors.ts
new file mode 100644
index 00000000..7ec748a8
--- /dev/null
+++ b/packages/components/src/utils/shiki/get-shiki-background-colors.ts
@@ -0,0 +1,83 @@
+import { DEFAULT_DARK_BG, DEFAULT_LIGHT_BG } from "@/utils/shiki/constants";
+
+const getShikiBackgroundColors = (
+ codeBlockTheme: "system" | "dark",
+ html: string | undefined,
+ children: unknown
+): { light: string | undefined; dark: string | undefined } | undefined => {
+ const defaultBgColors =
+ codeBlockTheme === "dark"
+ ? { light: DEFAULT_DARK_BG, dark: DEFAULT_DARK_BG }
+ : { light: DEFAULT_LIGHT_BG, dark: DEFAULT_DARK_BG };
+ if (html) {
+ return getBackgroundColorsFromHtml(html, defaultBgColors);
+ }
+ if (children) {
+ return getBackgroundColorsFromChildren(children, defaultBgColors);
+ }
+ return defaultBgColors;
+};
+
+const isTransparentOrUndefined = (color: string | undefined) =>
+ color === "transparent" || color === undefined;
+
+const getBackgroundColorsFromHtml = (
+ html: string,
+ defaultBgColors: { light: string; dark: string }
+): { light: string | undefined; dark: string | undefined } => {
+ const styles = html.split('style="');
+ if (styles.length < 2) {
+ return defaultBgColors;
+ }
+ const backgroundColor = styles[1]
+ ?.split("background-color:")[1]
+ ?.split(";")[0];
+ const shikiDarkBgValue = styles[1]
+ ?.split("--shiki-dark-bg:")[1]
+ ?.split(";")[0];
+ return {
+ light: isTransparentOrUndefined(backgroundColor)
+ ? defaultBgColors.light
+ : backgroundColor,
+ dark: isTransparentOrUndefined(shikiDarkBgValue)
+ ? defaultBgColors.dark
+ : shikiDarkBgValue,
+ };
+};
+
+const getBackgroundColorsFromChildren = (
+ children: unknown,
+ defaultBgColors: { light: string; dark: string }
+): { light: string | undefined; dark: string | undefined } => {
+ if (
+ children &&
+ typeof children === "object" &&
+ "props" in children &&
+ children.props &&
+ typeof children.props === "object" &&
+ "style" in children.props &&
+ typeof children.props.style === "object" &&
+ children.props.style &&
+ typeof children.props.style === "object" &&
+ "backgroundColor" in children.props.style &&
+ typeof children.props.style.backgroundColor === "string" &&
+ "--shiki-dark-bg" in children.props.style &&
+ typeof children.props.style["--shiki-dark-bg"] === "string"
+ ) {
+ return {
+ light: isTransparentOrUndefined(children.props.style.backgroundColor)
+ ? defaultBgColors.light
+ : children.props.style.backgroundColor,
+ dark: isTransparentOrUndefined(children.props.style["--shiki-dark-bg"])
+ ? defaultBgColors.dark
+ : children.props.style["--shiki-dark-bg"],
+ };
+ }
+ return defaultBgColors;
+};
+
+export {
+ getShikiBackgroundColors,
+ getBackgroundColorsFromHtml,
+ getBackgroundColorsFromChildren,
+};
diff --git a/packages/components/src/utils/shiki/index.ts b/packages/components/src/utils/shiki/index.ts
new file mode 100644
index 00000000..9be790cd
--- /dev/null
+++ b/packages/components/src/utils/shiki/index.ts
@@ -0,0 +1,317 @@
+import {
+ transformerMetaHighlight,
+ transformerNotationDiff,
+ transformerNotationFocus,
+ transformerNotationHighlight,
+} from "@shikijs/transformers";
+import type { ShikiTransformer } from "@shikijs/types";
+import type { Element, Root } from "hast";
+import { createHighlighter, hastToHtml } from "shiki";
+import { createJavaScriptRegexEngine } from "shiki/engine/javascript";
+import type {
+ BundledLanguage,
+ BundledTheme,
+ CodeToHastOptions,
+ HighlighterGeneric,
+} from "shiki/types";
+import { codeStylingToThemeOrThemes } from "@/common/get-code-styling";
+import { MAX_PREVIEW_BYTES } from "@/constants";
+import { getShikiLanguageFromPreset } from "@/utils/shiki/snippet-presets";
+import type { CodeStyling } from "@/validation";
+
+import {
+ LANGS,
+ LINE_DIFF_ADD_CLASS_NAME,
+ LINE_DIFF_REMOVE_CLASS_NAME,
+ LINE_FOCUS_CLASS_NAME,
+ LINE_HIGHLIGHT_CLASS_NAME,
+ SHIKI_COLOR_REPLACEMENTS,
+ SHIKI_LANG_MAP,
+ type ShikiLanguage,
+} from "./constants";
+import { getShikiWorker } from "./worker-client";
+
+const matchAlgorithm = {
+ matchAlgorithm: "v3",
+} as const;
+
+const SHIKI_TRANSFORMERS: ShikiTransformer[] = [
+ transformerMetaHighlight({
+ className: LINE_HIGHLIGHT_CLASS_NAME,
+ }),
+ transformerNotationHighlight({
+ ...matchAlgorithm,
+ classActiveLine: LINE_HIGHLIGHT_CLASS_NAME,
+ }),
+ transformerNotationFocus({
+ ...matchAlgorithm,
+ classActiveLine: LINE_FOCUS_CLASS_NAME,
+ }),
+ transformerNotationDiff({
+ ...matchAlgorithm,
+ classLineAdd: LINE_DIFF_ADD_CLASS_NAME,
+ classLineRemove: LINE_DIFF_REMOVE_CLASS_NAME,
+ }),
+];
+
+const jsEngine = createJavaScriptRegexEngine({
+ forgiving: true,
+ cache: new Map(),
+});
+
+let highlighter: HighlighterGeneric | undefined;
+let highlighterError: Error | undefined;
+let highlighterResolved = false;
+const highlighterPromise = createHighlighter({
+ themes: [],
+ langs: LANGS,
+ engine: jsEngine,
+})
+ .then((hl) => {
+ highlighter = hl;
+ })
+ .catch((err) => {
+ console.error(err);
+ highlighterError =
+ err ?? new Error("Unknown error occurred initializing highlighter");
+ })
+ .finally(() => {
+ highlighterResolved = true;
+ });
+
+function getShikiLanguage(lang: string | undefined): ShikiLanguage {
+ const text = "text" as ShikiLanguage;
+ if (lang === undefined) {
+ return text;
+ }
+
+ // Handle HTTP status codes as JSON
+ const possibleStatusCode = Number(lang);
+ if (
+ !Number.isNaN(possibleStatusCode) &&
+ possibleStatusCode > 99 &&
+ possibleStatusCode < 600
+ ) {
+ return "json";
+ }
+
+ const lower = lang.toLowerCase();
+
+ // Check snippet preset system first (source of truth for API playground languages)
+ // e.g., "dotnet" → "csharp", "node" → "javascript"
+ const presetLang = getShikiLanguageFromPreset(lower);
+
+ // Then check shiki lang map (handles general aliases like "py" → "python")
+ return SHIKI_LANG_MAP[presetLang] ?? SHIKI_LANG_MAP[lower] ?? text;
+}
+
+function getLanguageFromClassName(
+ className: string | undefined,
+ fallback?: string
+): string {
+ // biome-ignore lint/performance/useTopLevelRegex: TODO
+ const match = /language-(\w+)/.exec(className ?? "");
+ return match ? (match[1] ?? "text") : (fallback ?? "text");
+}
+
+function getThemesForCodeStyling(codeblocks?: CodeStyling): string[] {
+ if (typeof codeblocks === "string" || !codeblocks) {
+ return codeblocks === "system" || !codeblocks
+ ? ["dark-plus", "github-light-default"]
+ : ["dark-plus"];
+ }
+ const { theme } = codeblocks;
+ if (typeof theme === "string") {
+ return [theme];
+ }
+ if (typeof theme === "object") {
+ return [theme.dark, theme.light];
+ }
+ return ["dark-plus", "github-light-default"];
+}
+
+function areThemesLoaded(codeblocks?: CodeStyling): boolean {
+ if (!highlighter) {
+ return false;
+ }
+ const loaded = highlighter.getLoadedThemes();
+ const needed = getThemesForCodeStyling(codeblocks);
+ return needed.every((t) => t === "css-variables" || loaded.includes(t));
+}
+
+async function loadShikiThemes(codeblocks?: CodeStyling) {
+ await highlighterPromise;
+ if (!highlighter) {
+ return;
+ }
+
+ const loaded = highlighter.getLoadedThemes();
+ const toLoad = getThemesForCodeStyling(codeblocks).filter(
+ (t) => t !== "css-variables" && !loaded.includes(t)
+ ) as [BundledTheme, ...BundledTheme[]];
+
+ if (toLoad.length > 0) {
+ await highlighter.loadTheme(...toLoad);
+ }
+}
+
+type ShikiHighlightedHtmlArgs = {
+ codeString: string;
+ codeBlockTheme: CodeStyling;
+ opts?: Partial & {
+ highlightedLines?: number[];
+ focusedLines?: number[];
+ noAsync?: boolean;
+ };
+} & (
+ | {
+ className: string | undefined;
+ fileName?: string;
+ }
+ | {
+ language: string;
+ }
+);
+
+function getShikiHighlightedHtml(
+ props: ShikiHighlightedHtmlArgs & {
+ opts: ShikiHighlightedHtmlArgs["opts"] & { noAsync: true };
+ }
+): string | undefined;
+
+function getShikiHighlightedHtml(
+ props: ShikiHighlightedHtmlArgs
+): string | undefined | Promise;
+
+// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: TODO
+function getShikiHighlightedHtml(
+ props: ShikiHighlightedHtmlArgs
+): string | undefined | Promise {
+ if (!props.codeString || highlighterError !== undefined) {
+ return undefined;
+ }
+
+ if (highlighter === undefined) {
+ if (props.opts?.noAsync || highlighterResolved) {
+ return undefined;
+ }
+ return highlighterPromise
+ .then(() => getShikiHighlightedHtml(props))
+ .catch(() => undefined);
+ }
+
+ if (!areThemesLoaded(props.codeBlockTheme)) {
+ if (props.opts?.noAsync) {
+ return undefined;
+ }
+ return loadShikiThemes(props.codeBlockTheme)
+ .then(() => getShikiHighlightedHtml(props))
+ .catch(() => undefined);
+ }
+
+ let lang: string;
+ if ("language" in props) {
+ if (props.language === "text") {
+ return undefined;
+ }
+ lang = props.language;
+ } else {
+ if (props.className === "lang-text") {
+ return undefined;
+ }
+ lang = getLanguageFromClassName(props.className, props.fileName);
+ }
+
+ if (props.codeString.length > MAX_PREVIEW_BYTES * 5) {
+ return undefined;
+ }
+
+ if (props.codeString.length > MAX_PREVIEW_BYTES) {
+ if (props.opts?.noAsync === true) {
+ return undefined;
+ }
+
+ try {
+ const worker = getShikiWorker();
+ return worker === undefined ? undefined : worker.highlight(props);
+ } catch {
+ return undefined;
+ }
+ }
+
+ let html: string | Root | undefined;
+ if (lang) {
+ try {
+ const serializer =
+ !!props.opts?.highlightedLines?.length ||
+ !!props.opts?.focusedLines?.length
+ ? "codeToHast"
+ : "codeToHtml";
+ const code = props.codeString.trim();
+ const themeOrThemes = codeStylingToThemeOrThemes(props.codeBlockTheme);
+
+ html = highlighter[serializer](code, {
+ lang: getShikiLanguage(lang),
+ ...themeOrThemes,
+ colorReplacements: { ...SHIKI_COLOR_REPLACEMENTS },
+ transformers: SHIKI_TRANSFORMERS,
+ tabindex: false,
+ ...props.opts,
+ });
+ } catch (error) {
+ console.error("error getting shiki highlighted html", error);
+ }
+ }
+
+ if (typeof html !== "object") {
+ return html;
+ }
+
+ const firstChild = html.children[0];
+ if (!firstChild) {
+ return undefined;
+ }
+ if (firstChild.type === "element" && firstChild.tagName === "pre") {
+ const codeElement = firstChild.children[0] as Element | undefined;
+ if (!codeElement?.children) {
+ return hastToHtml(html);
+ }
+ const spanElements = codeElement.children.filter(
+ (child) => child.type === "element" && child.tagName === "span"
+ ) as Element[];
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: TODO
+ spanElements.forEach((child, index) => {
+ // we used 1-based index in the meta string, so we need to increment it by 1
+ const actualIndex = index + 1;
+ if (typeof child.properties.class === "string") {
+ if (props.opts?.highlightedLines?.includes(actualIndex)) {
+ child.properties.class += ` ${LINE_HIGHLIGHT_CLASS_NAME}`;
+ }
+
+ if (props.opts?.focusedLines?.includes(actualIndex)) {
+ child.properties.class += ` ${LINE_FOCUS_CLASS_NAME}`;
+ }
+ } else if (Array.isArray(child.properties.class)) {
+ if (props.opts?.highlightedLines?.includes(actualIndex)) {
+ child.properties.class.push(LINE_HIGHLIGHT_CLASS_NAME);
+ }
+
+ if (props.opts?.focusedLines?.includes(actualIndex)) {
+ child.properties.class.push(LINE_FOCUS_CLASS_NAME);
+ }
+ }
+ });
+ }
+
+ return hastToHtml(html);
+}
+
+export {
+ SHIKI_TRANSFORMERS,
+ highlighterPromise,
+ getShikiLanguage,
+ getLanguageFromClassName,
+ loadShikiThemes,
+ type ShikiHighlightedHtmlArgs,
+ getShikiHighlightedHtml,
+};
diff --git a/packages/components/src/utils/shiki/lib.ts b/packages/components/src/utils/shiki/lib.ts
new file mode 100644
index 00000000..99c9bab0
--- /dev/null
+++ b/packages/components/src/utils/shiki/lib.ts
@@ -0,0 +1,79 @@
+import { type ReactNode, useMemo } from "react";
+
+import { getNodeText } from "@/utils/get-node-text";
+import { SHIKI_CLASSNAME } from "@/utils/shiki/constants";
+
+function findShikiClassName(children: unknown): boolean {
+ if (!children || typeof children !== "object") {
+ return false;
+ }
+
+ if (Array.isArray(children)) {
+ for (const child of children) {
+ if (findShikiClassName(child)) {
+ return true;
+ }
+ }
+ }
+
+ if (
+ "props" in children &&
+ children.props &&
+ typeof children.props === "object"
+ ) {
+ if (
+ "className" in children.props &&
+ typeof children.props.className === "string" &&
+ children.props.className.includes(SHIKI_CLASSNAME)
+ ) {
+ return true;
+ }
+
+ if ("children" in children.props) {
+ return findShikiClassName(children.props.children);
+ }
+ }
+
+ return false;
+}
+
+function getCodeString(
+ children: ReactNode,
+ className?: string,
+ forceExtract = false
+): string {
+ const isShiki =
+ className?.includes(SHIKI_CLASSNAME) || findShikiClassName(children);
+ if (isShiki && !forceExtract) {
+ return "";
+ }
+
+ const codeString = getNodeText(children);
+
+ return codeString;
+}
+
+function calculateCodeLinesFromHtml(html: string | undefined): number {
+ if (!html || typeof html !== "string") {
+ return 0;
+ }
+
+ const lineMatches = html.match(/();
+for (const preset of SNIPPET_PRESETS) {
+ presetLookup.set(preset.key.toLowerCase(), preset);
+ for (const alias of preset.aliases ?? []) {
+ presetLookup.set(alias.toLowerCase(), preset);
+ }
+}
+
+const getPreset = (lang: string): SnippetPreset | undefined =>
+ presetLookup.get(lang.toLowerCase());
+
+const getDisplayName = (lang: string): string =>
+ getPreset(lang)?.displayName ?? lang;
+
+const getIconKey = (lang: string): string | undefined => {
+ const preset = getPreset(lang);
+ return preset ? (preset.iconKey ?? preset.key) : undefined;
+};
+
+const getShikiLanguageFromPreset = (lang: string): string =>
+ getPreset(lang)?.shikiLanguage ?? lang;
+
+export {
+ type SnippetPreset,
+ SNIPPET_PRESETS,
+ getPreset,
+ getDisplayName,
+ getIconKey,
+ getShikiLanguageFromPreset,
+};
diff --git a/packages/components/src/utils/shiki/worker-client.ts b/packages/components/src/utils/shiki/worker-client.ts
new file mode 100644
index 00000000..969905c2
--- /dev/null
+++ b/packages/components/src/utils/shiki/worker-client.ts
@@ -0,0 +1,28 @@
+import { type Remote, wrap } from "comlink";
+
+import type { ShikiHighlightedHtmlArgs } from "../shiki";
+
+type HighlightFn = (
+ props: ShikiHighlightedHtmlArgs
+) => string | undefined | Promise;
+type ShikiWorkerInstance =
+ | Remote<{ highlight: HighlightFn; ready: () => Promise }>
+ | undefined;
+
+let instance: ShikiWorkerInstance;
+
+function getShikiWorker(): ShikiWorkerInstance {
+ if (typeof Worker === "undefined") {
+ return undefined;
+ }
+ if (instance) {
+ return instance;
+ }
+ const worker = new Worker(new URL("./worker.js", import.meta.url), {
+ type: "module",
+ });
+ instance = wrap(worker);
+ return instance;
+}
+
+export { getShikiWorker };
diff --git a/packages/components/src/utils/shiki/worker.js b/packages/components/src/utils/shiki/worker.js
new file mode 100644
index 00000000..ce23c143
--- /dev/null
+++ b/packages/components/src/utils/shiki/worker.js
@@ -0,0 +1,170 @@
+import { expose } from "comlink";
+
+let replacements, langMap;
+let transformers;
+let highlighter;
+let hastToHtmlFn;
+let codeStylingToThemeOrThemesFn;
+let getShikiLanguageFromPresetFn;
+let lineHighlightClassName, lineFocusClassName;
+
+const ready = (async () => {
+ const [
+ { createHighlighter, hastToHtml },
+ { createJavaScriptRegexEngine },
+ {
+ transformerNotationHighlight,
+ transformerNotationFocus,
+ transformerMetaHighlight,
+ transformerNotationDiff,
+ },
+ {
+ THEMES,
+ LANGS,
+ SHIKI_COLOR_REPLACEMENTS,
+ SHIKI_LANG_MAP,
+ LINE_HIGHLIGHT_CLASS_NAME,
+ LINE_FOCUS_CLASS_NAME,
+ LINE_DIFF_ADD_CLASS_NAME,
+ LINE_DIFF_REMOVE_CLASS_NAME,
+ },
+ { codeStylingToThemeOrThemes },
+ { getShikiLanguageFromPreset },
+ ] = await Promise.all([
+ import("shiki"),
+ import("shiki/engine/javascript"),
+ import("@shikijs/transformers"),
+ import("./constants.ts"),
+ import("@/common/get-code-styling"),
+ import("@/utils/shiki/snippet-presets"),
+ ]);
+
+ replacements = SHIKI_COLOR_REPLACEMENTS;
+ langMap = SHIKI_LANG_MAP;
+ hastToHtmlFn = hastToHtml;
+ lineHighlightClassName = LINE_HIGHLIGHT_CLASS_NAME;
+ lineFocusClassName = LINE_FOCUS_CLASS_NAME;
+ codeStylingToThemeOrThemesFn = codeStylingToThemeOrThemes;
+ getShikiLanguageFromPresetFn = getShikiLanguageFromPreset;
+ const engine = createJavaScriptRegexEngine({
+ forgiving: true,
+ cache: new Map(),
+ });
+ highlighter = await createHighlighter({
+ themes: THEMES,
+ langs: LANGS,
+ engine,
+ });
+
+ const matchAlgorithm = {
+ matchAlgorithm: "v3",
+ };
+
+ transformers = [
+ transformerMetaHighlight({
+ className: LINE_HIGHLIGHT_CLASS_NAME,
+ }),
+ transformerNotationHighlight({
+ ...matchAlgorithm,
+ classActiveLine: LINE_HIGHLIGHT_CLASS_NAME,
+ }),
+ transformerNotationFocus({
+ ...matchAlgorithm,
+ classActiveLine: LINE_FOCUS_CLASS_NAME,
+ }),
+ transformerNotationDiff({
+ ...matchAlgorithm,
+ classLineAdd: LINE_DIFF_ADD_CLASS_NAME,
+ classLineRemove: LINE_DIFF_REMOVE_CLASS_NAME,
+ }),
+ ];
+})();
+
+function getShikiLanguage(lang) {
+ const text = "text";
+ if (!lang) {
+ return text;
+ }
+
+ const n = Number(lang);
+ const isStatus = !Number.isNaN(n) && Number.isFinite(n) && n > 99 && n < 600;
+ if (isStatus) {
+ return "json";
+ }
+
+ const lower = lang.toLowerCase();
+ const presetLang = getShikiLanguageFromPresetFn(lower);
+
+ return langMap[presetLang] ?? langMap[lower] ?? text;
+}
+
+function getLanguageFromClassName(className, fallback) {
+ // biome-ignore lint/performance/useTopLevelRegex: TODO
+ const m = /language-(\w+)/.exec(className ?? "");
+ return m ? (m[1] ?? "text") : (fallback ?? "text");
+}
+
+function highlightSync(opts) {
+ const lang =
+ "language" in opts
+ ? opts.language
+ : getLanguageFromClassName(opts.className, opts.fileName);
+ let html;
+ if (lang) {
+ const serializer =
+ !!opts.opts?.highlightedLines?.length || !!opts.opts?.focusedLines?.length
+ ? "codeToHast"
+ : "codeToHtml";
+ const code = opts.codeString.trim();
+ const themeOrThemes = codeStylingToThemeOrThemesFn(opts.codeBlockTheme);
+ html = highlighter[serializer](code, {
+ lang: getShikiLanguage(lang),
+ ...themeOrThemes,
+ colorReplacements: { ...replacements },
+ transformers,
+ tabindex: false,
+ ...opts.opts,
+ });
+ }
+ if (typeof html !== "object") {
+ return html;
+ }
+ const pre = html.children[0];
+ if (pre?.type === "element" && pre.tagName === "pre") {
+ const codeElement = pre.children[0];
+ if (!codeElement?.children) {
+ return hastToHtmlFn(html);
+ }
+ const spanElements = codeElement.children.filter(
+ (child) => child.type === "element" && child.tagName === "span"
+ );
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: TODO
+ spanElements.forEach((c, i) => {
+ if (typeof c.properties.class === "string") {
+ if (opts.opts?.highlightedLines?.includes(i + 1)) {
+ c.properties.class += ` ${lineHighlightClassName}`;
+ }
+
+ if (opts.opts?.focusedLines?.includes(i + 1)) {
+ c.properties.class += ` ${lineFocusClassName}`;
+ }
+ } else if (Array.isArray(c.properties.class)) {
+ if (opts.opts?.highlightedLines?.includes(i + 1)) {
+ c.properties.class.push(lineHighlightClassName);
+ }
+
+ if (opts.opts?.focusedLines?.includes(i + 1)) {
+ c.properties.class.push(lineFocusClassName);
+ }
+ }
+ });
+ }
+ return hastToHtmlFn(html);
+}
+
+async function highlight(props) {
+ await ready;
+ return highlightSync(props);
+}
+
+expose({ highlight, ready });
diff --git a/packages/components/src/validation/index.ts b/packages/components/src/validation/index.ts
new file mode 100644
index 00000000..ccb3c6a6
--- /dev/null
+++ b/packages/components/src/validation/index.ts
@@ -0,0 +1,202 @@
+type CodeStyling =
+ | "dark"
+ | "system"
+ | {
+ languages?:
+ | {
+ custom?: string[] | undefined;
+ }
+ | undefined;
+ theme?:
+ | "andromeeda"
+ | "aurora-x"
+ | "ayu-dark"
+ | "catppuccin-frappe"
+ | "catppuccin-latte"
+ | "catppuccin-macchiato"
+ | "catppuccin-mocha"
+ | "dark-plus"
+ | "dracula"
+ | "dracula-soft"
+ | "everforest-dark"
+ | "everforest-light"
+ | "github-dark"
+ | "github-dark-default"
+ | "github-dark-dimmed"
+ | "github-dark-high-contrast"
+ | "github-light"
+ | "github-light-default"
+ | "github-light-high-contrast"
+ | "gruvbox-dark-hard"
+ | "gruvbox-dark-medium"
+ | "gruvbox-dark-soft"
+ | "gruvbox-light-hard"
+ | "gruvbox-light-medium"
+ | "gruvbox-light-soft"
+ | "houston"
+ | "kanagawa-dragon"
+ | "kanagawa-lotus"
+ | "kanagawa-wave"
+ | "laserwave"
+ | "light-plus"
+ | "material-theme"
+ | "material-theme-darker"
+ | "material-theme-lighter"
+ | "material-theme-ocean"
+ | "material-theme-palenight"
+ | "min-dark"
+ | "min-light"
+ | "monokai"
+ | "night-owl"
+ | "nord"
+ | "one-dark-pro"
+ | "one-light"
+ | "plastic"
+ | "poimandres"
+ | "red"
+ | "rose-pine"
+ | "rose-pine-dawn"
+ | "rose-pine-moon"
+ | "slack-dark"
+ | "slack-ochin"
+ | "snazzy-light"
+ | "solarized-dark"
+ | "solarized-light"
+ | "synthwave-84"
+ | "tokyo-night"
+ | "vesper"
+ | "vitesse-black"
+ | "vitesse-dark"
+ | "vitesse-light"
+ | "css-variables"
+ | {
+ light:
+ | "andromeeda"
+ | "aurora-x"
+ | "ayu-dark"
+ | "catppuccin-frappe"
+ | "catppuccin-latte"
+ | "catppuccin-macchiato"
+ | "catppuccin-mocha"
+ | "dark-plus"
+ | "dracula"
+ | "dracula-soft"
+ | "everforest-dark"
+ | "everforest-light"
+ | "github-dark"
+ | "github-dark-default"
+ | "github-dark-dimmed"
+ | "github-dark-high-contrast"
+ | "github-light"
+ | "github-light-default"
+ | "github-light-high-contrast"
+ | "gruvbox-dark-hard"
+ | "gruvbox-dark-medium"
+ | "gruvbox-dark-soft"
+ | "gruvbox-light-hard"
+ | "gruvbox-light-medium"
+ | "gruvbox-light-soft"
+ | "houston"
+ | "kanagawa-dragon"
+ | "kanagawa-lotus"
+ | "kanagawa-wave"
+ | "laserwave"
+ | "light-plus"
+ | "material-theme"
+ | "material-theme-darker"
+ | "material-theme-lighter"
+ | "material-theme-ocean"
+ | "material-theme-palenight"
+ | "min-dark"
+ | "min-light"
+ | "monokai"
+ | "night-owl"
+ | "nord"
+ | "one-dark-pro"
+ | "one-light"
+ | "plastic"
+ | "poimandres"
+ | "red"
+ | "rose-pine"
+ | "rose-pine-dawn"
+ | "rose-pine-moon"
+ | "slack-dark"
+ | "slack-ochin"
+ | "snazzy-light"
+ | "solarized-dark"
+ | "solarized-light"
+ | "synthwave-84"
+ | "tokyo-night"
+ | "vesper"
+ | "vitesse-black"
+ | "vitesse-dark"
+ | "vitesse-light"
+ | "css-variables";
+ dark:
+ | "andromeeda"
+ | "aurora-x"
+ | "ayu-dark"
+ | "catppuccin-frappe"
+ | "catppuccin-latte"
+ | "catppuccin-macchiato"
+ | "catppuccin-mocha"
+ | "dark-plus"
+ | "dracula"
+ | "dracula-soft"
+ | "everforest-dark"
+ | "everforest-light"
+ | "github-dark"
+ | "github-dark-default"
+ | "github-dark-dimmed"
+ | "github-dark-high-contrast"
+ | "github-light"
+ | "github-light-default"
+ | "github-light-high-contrast"
+ | "gruvbox-dark-hard"
+ | "gruvbox-dark-medium"
+ | "gruvbox-dark-soft"
+ | "gruvbox-light-hard"
+ | "gruvbox-light-medium"
+ | "gruvbox-light-soft"
+ | "houston"
+ | "kanagawa-dragon"
+ | "kanagawa-lotus"
+ | "kanagawa-wave"
+ | "laserwave"
+ | "light-plus"
+ | "material-theme"
+ | "material-theme-darker"
+ | "material-theme-lighter"
+ | "material-theme-ocean"
+ | "material-theme-palenight"
+ | "min-dark"
+ | "min-light"
+ | "monokai"
+ | "night-owl"
+ | "nord"
+ | "one-dark-pro"
+ | "one-light"
+ | "plastic"
+ | "poimandres"
+ | "red"
+ | "rose-pine"
+ | "rose-pine-dawn"
+ | "rose-pine-moon"
+ | "slack-dark"
+ | "slack-ochin"
+ | "snazzy-light"
+ | "solarized-dark"
+ | "solarized-light"
+ | "synthwave-84"
+ | "tokyo-night"
+ | "vesper"
+ | "vitesse-black"
+ | "vitesse-dark"
+ | "vitesse-light"
+ | "css-variables";
+ }
+ | undefined;
+ }
+ | undefined;
+
+export type { CodeStyling };
diff --git a/packages/components/vite.config.ts b/packages/components/vite.config.ts
index 1e8dd6e7..ed06afea 100644
--- a/packages/components/vite.config.ts
+++ b/packages/components/vite.config.ts
@@ -24,6 +24,9 @@ export default defineConfig({
"@": resolve(__dirname, "./src"),
},
},
+ worker: {
+ format: "es",
+ },
build: {
lib: {
entry: resolve(__dirname, "src/index.ts"),
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index f9593007..fa4fcf4f 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -20,6 +20,9 @@ importers:
'@radix-ui/react-tooltip':
specifier: ^1.1.4
version: 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@shikijs/transformers':
+ specifier: ^3.21.0
+ version: 3.21.0
'@sindresorhus/slugify':
specifier: ^2.1.1
version: 2.2.1
@@ -29,6 +32,12 @@ importers:
color:
specifier: ^4.2.3
version: 4.2.3
+ comlink:
+ specifier: ^4.4.2
+ version: 4.4.2
+ hast:
+ specifier: ^1.0.0
+ version: 1.0.0
lodash:
specifier: ^4.17.21
version: 4.17.21
@@ -38,10 +47,16 @@ importers:
mermaid:
specifier: ^11.12.2
version: 11.12.2
+ shiki:
+ specifier: ^3.17.0
+ version: 3.21.0
tailwind-merge:
specifier: ^2.6.0
version: 2.6.0
devDependencies:
+ '@shikijs/types':
+ specifier: ^3.18.0
+ version: 3.21.0
'@storybook/addon-docs':
specifier: ^10.1.10
version: 10.1.11(@types/react@18.3.27)(esbuild@0.27.2)(rollup@4.53.3)(storybook@10.1.10(@testing-library/dom@10.4.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@6.4.1(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6))
@@ -60,6 +75,9 @@ importers:
'@types/color':
specifier: ^4.2.0
version: 4.2.0
+ '@types/hast':
+ specifier: ^3.0.4
+ version: 3.0.4
'@types/lodash':
specifier: 4.17.23
version: 4.17.23
@@ -1117,6 +1135,30 @@ packages:
'@rushstack/ts-command-line@5.1.3':
resolution: {integrity: sha512-Kdv0k/BnnxIYFlMVC1IxrIS0oGQd4T4b7vKfx52Y2+wk2WZSDFIvedr7JrhenzSlm3ou5KwtoTGTGd5nbODRug==}
+ '@shikijs/core@3.21.0':
+ resolution: {integrity: sha512-AXSQu/2n1UIQekY8euBJlvFYZIw0PHY63jUzGbrOma4wPxzznJXTXkri+QcHeBNaFxiiOljKxxJkVSoB3PjbyA==}
+
+ '@shikijs/engine-javascript@3.21.0':
+ resolution: {integrity: sha512-ATwv86xlbmfD9n9gKRiwuPpWgPENAWCLwYCGz9ugTJlsO2kOzhOkvoyV/UD+tJ0uT7YRyD530x6ugNSffmvIiQ==}
+
+ '@shikijs/engine-oniguruma@3.21.0':
+ resolution: {integrity: sha512-OYknTCct6qiwpQDqDdf3iedRdzj6hFlOPv5hMvI+hkWfCKs5mlJ4TXziBG9nyabLwGulrUjHiCq3xCspSzErYQ==}
+
+ '@shikijs/langs@3.21.0':
+ resolution: {integrity: sha512-g6mn5m+Y6GBJ4wxmBYqalK9Sp0CFkUqfNzUy2pJglUginz6ZpWbaWjDB4fbQ/8SHzFjYbtU6Ddlp1pc+PPNDVA==}
+
+ '@shikijs/themes@3.21.0':
+ resolution: {integrity: sha512-BAE4cr9EDiZyYzwIHEk7JTBJ9CzlPuM4PchfcA5ao1dWXb25nv6hYsoDiBq2aZK9E3dlt3WB78uI96UESD+8Mw==}
+
+ '@shikijs/transformers@3.21.0':
+ resolution: {integrity: sha512-CZwvCWWIiRRiFk9/JKzdEooakAP8mQDtBOQ1TKiCaS2E1bYtyBCOkUzS8akO34/7ufICQ29oeSfkb3tT5KtrhA==}
+
+ '@shikijs/types@3.21.0':
+ resolution: {integrity: sha512-zGrWOxZ0/+0ovPY7PvBU2gIS9tmhSUUt30jAcNV0Bq0gb2S98gwfjIs1vxlmH5zM7/4YxLamT6ChlqqAJmPPjA==}
+
+ '@shikijs/vscode-textmate@10.0.2':
+ resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==}
+
'@sindresorhus/slugify@2.2.1':
resolution: {integrity: sha512-MkngSCRZ8JdSOCHRaYd+D01XhvU3Hjy6MGl06zhOk614hp9EOAp5gIkBeQg7wtmxpitU6eAL4kdiRMcJa2dlrw==}
engines: {node: '>=12'}
@@ -1441,12 +1483,18 @@ packages:
'@types/geojson@7946.0.16':
resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==}
+ '@types/hast@3.0.4':
+ resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
+
'@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
'@types/lodash@4.17.23':
resolution: {integrity: sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==}
+ '@types/mdast@4.0.4':
+ resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==}
+
'@types/mdx@2.0.13':
resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==}
@@ -1470,6 +1518,9 @@ packages:
'@types/trusted-types@2.0.7':
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
+ '@types/unist@3.0.3':
+ resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
+
'@typescript-eslint/eslint-plugin@8.47.0':
resolution: {integrity: sha512-fe0rz9WJQ5t2iaLfdbDc9T80GJy0AeO453q8C3YCilnGozvOyCG5t+EZtg7j7D88+c3FipfP/x+wzGnh1xp8ZA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -1529,6 +1580,9 @@ packages:
resolution: {integrity: sha512-SIV3/6eftCy1bNzCQoPmbWsRLujS8t5iDIZ4spZOBHqrM+yfX2ogg8Tt3PDTAVKw3sSCiUgg30uOAvK2r9zGjQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ '@ungap/structured-clone@1.3.0':
+ resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
+
'@vitejs/plugin-react@4.7.0':
resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==}
engines: {node: ^14.18.0 || >=16.0.0}
@@ -1699,6 +1753,9 @@ packages:
caniuse-lite@1.0.30001756:
resolution: {integrity: sha512-4HnCNKbMLkLdhJz3TToeVWHSnfJvPaq6vu/eRP0Ahub/07n484XHhBF5AJoSGHdVrS8tKFauUQz8Bp9P7LVx7A==}
+ ccount@2.0.1:
+ resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
+
chai@5.3.3:
resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==}
engines: {node: '>=18'}
@@ -1707,6 +1764,12 @@ packages:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'}
+ character-entities-html4@2.1.0:
+ resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==}
+
+ character-entities-legacy@3.0.0:
+ resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==}
+
check-error@2.1.3:
resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==}
engines: {node: '>= 16'}
@@ -1740,6 +1803,11 @@ packages:
resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
engines: {node: '>=12.5.0'}
+ comlink@4.4.2:
+ resolution: {integrity: sha512-OxGdvBmJuNKSCMO4NTl1L47VRp6xn2wG4F/2hYzB6tiCb709otOxtEYCSvK80PtjODfXXZu8ds+Nw5kVCjqd2g==}
+
+ comma-separated-tokens@2.0.3:
+ resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
commander@14.0.2:
resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==}
engines: {node: '>=20'}
@@ -1993,6 +2061,9 @@ packages:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
+ devlop@1.1.0:
+ resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
+
diff@8.0.2:
resolution: {integrity: sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==}
engines: {node: '>=0.3.1'}
@@ -2221,10 +2292,23 @@ packages:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
+ hast-util-to-html@9.0.5:
+ resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==}
+
+ hast-util-whitespace@3.0.0:
+ resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==}
+
+ hast@1.0.0:
+ resolution: {integrity: sha512-vFUqlRV5C+xqP76Wwq2SrM0kipnmpxJm7OfvVXpB35Fp+Fn4MV+ozr+JZr5qFvyR1q/U+Foim2x+3P+x9S1PLA==}
+ deprecated: Renamed to rehype
+
he@1.2.0:
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
hasBin: true
+ html-void-elements@3.0.0:
+ resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
+
iconv-lite@0.6.3:
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
engines: {node: '>=0.10.0'}
@@ -2498,6 +2582,13 @@ packages:
engines: {node: '>= 20'}
hasBin: true
+ math-intrinsics@1.1.0:
+ resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
+ engines: {node: '>= 0.4'}
+
+ mdast-util-to-hast@13.2.1:
+ resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==}
+
merge2@1.4.1:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
@@ -2505,6 +2596,21 @@ packages:
mermaid@11.12.2:
resolution: {integrity: sha512-n34QPDPEKmaeCG4WDMGy0OT6PSyxKCfy2pJgShP+Qow2KLrvWjclwbc3yXfSIf4BanqWEhQEpngWwNp/XhZt6w==}
+ micromark-util-character@2.1.1:
+ resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==}
+
+ micromark-util-encode@2.0.1:
+ resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==}
+
+ micromark-util-sanitize-uri@2.0.1:
+ resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==}
+
+ micromark-util-symbol@2.0.1:
+ resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==}
+
+ micromark-util-types@2.0.2:
+ resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==}
+
micromatch@4.0.8:
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
engines: {node: '>=8.6'}
@@ -2570,6 +2676,12 @@ packages:
engines: {node: '>=18'}
hasBin: true
+ 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@10.2.0:
resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==}
engines: {node: '>=18'}
@@ -2663,6 +2775,9 @@ packages:
resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
+ property-information@7.1.0:
+ resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==}
+
punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
@@ -2706,6 +2821,23 @@ packages:
resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==}
engines: {node: '>=8'}
+ reflect.getprototypeof@1.0.10:
+ 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'}
+
require-from-string@2.0.2:
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
engines: {node: '>=0.10.0'}
@@ -2775,6 +2907,25 @@ packages:
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
engines: {node: '>=8'}
+ shiki@3.21.0:
+ resolution: {integrity: sha512-N65B/3bqL/TI2crrXr+4UivctrAGEjmsib5rPMMPpFp1xAx/w03v8WZ9RDDFYteXoEgY7qZ4HGgl5KBIu1153w==}
+
+ side-channel-list@1.0.0:
+ resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==}
+ engines: {node: '>= 0.4'}
+
+ side-channel-map@1.0.1:
+ resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==}
+ engines: {node: '>= 0.4'}
+
+ side-channel-weakmap@1.0.2:
+ resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==}
+ engines: {node: '>= 0.4'}
+
+ side-channel@1.1.0:
+ resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
+ engines: {node: '>= 0.4'}
+
signal-exit@4.1.0:
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
engines: {node: '>=14'}
@@ -2793,6 +2944,9 @@ packages:
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
engines: {node: '>=0.10.0'}
+ space-separated-tokens@2.0.2:
+ resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
+
sprintf-js@1.0.3:
resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
@@ -2817,6 +2971,21 @@ packages:
resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
engines: {node: '>=12'}
+ string.prototype.trim@1.2.10:
+ resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==}
+ engines: {node: '>= 0.4'}
+
+ string.prototype.trimend@1.0.9:
+ resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==}
+ engines: {node: '>= 0.4'}
+
+ string.prototype.trimstart@1.0.8:
+ resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==}
+ engines: {node: '>= 0.4'}
+
+ stringify-entities@4.0.4:
+ resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==}
+
strip-ansi@6.0.1:
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
engines: {node: '>=8'}
@@ -2889,6 +3058,8 @@ packages:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
+ trim-lines@3.0.1:
+ resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
trpc-cli@0.12.2:
resolution: {integrity: sha512-kGNCiyOimGlfcZFImbWzFF2Nn3TMnenwUdyuckiN5SEaceJbIac7+Iau3WsVHjQpoNgugFruZMDOKf8GNQNtJw==}
engines: {node: '>=18'}
@@ -2965,6 +3136,21 @@ packages:
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
+ unist-util-is@6.0.1:
+ resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==}
+
+ unist-util-position@5.0.0:
+ resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==}
+
+ unist-util-stringify-position@4.0.0:
+ resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==}
+
+ unist-util-visit-parents@6.0.2:
+ resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==}
+
+ unist-util-visit@5.0.0:
+ resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==}
+
universalify@2.0.1:
resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
engines: {node: '>= 10.0.0'}
@@ -2994,6 +3180,12 @@ packages:
resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
hasBin: true
+ vfile-message@4.0.3:
+ resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==}
+
+ vfile@6.0.3:
+ resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
+
vite-plugin-dts@4.5.4:
resolution: {integrity: sha512-d4sOM8M/8z7vRXHHq/ebbblfaxENjogAAekcfcDCCwAyvGqnPrc7f4NZbvItS+g4WTgerW0xDwSz5qz11JT3vg==}
peerDependencies:
@@ -3112,6 +3304,8 @@ packages:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
+ zwitch@2.0.4:
+ resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
zod@4.3.5:
resolution: {integrity: sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==}
@@ -3940,6 +4134,44 @@ snapshots:
transitivePeerDependencies:
- '@types/node'
+ '@shikijs/core@3.21.0':
+ dependencies:
+ '@shikijs/types': 3.21.0
+ '@shikijs/vscode-textmate': 10.0.2
+ '@types/hast': 3.0.4
+ hast-util-to-html: 9.0.5
+
+ '@shikijs/engine-javascript@3.21.0':
+ dependencies:
+ '@shikijs/types': 3.21.0
+ '@shikijs/vscode-textmate': 10.0.2
+ oniguruma-to-es: 4.3.4
+
+ '@shikijs/engine-oniguruma@3.21.0':
+ dependencies:
+ '@shikijs/types': 3.21.0
+ '@shikijs/vscode-textmate': 10.0.2
+
+ '@shikijs/langs@3.21.0':
+ dependencies:
+ '@shikijs/types': 3.21.0
+
+ '@shikijs/themes@3.21.0':
+ dependencies:
+ '@shikijs/types': 3.21.0
+
+ '@shikijs/transformers@3.21.0':
+ dependencies:
+ '@shikijs/core': 3.21.0
+ '@shikijs/types': 3.21.0
+
+ '@shikijs/types@3.21.0':
+ dependencies:
+ '@shikijs/vscode-textmate': 10.0.2
+ '@types/hast': 3.0.4
+
+ '@shikijs/vscode-textmate@10.0.2': {}
+
'@sindresorhus/slugify@2.2.1':
dependencies:
'@sindresorhus/transliterate': 1.6.0
@@ -4311,10 +4543,18 @@ snapshots:
'@types/geojson@7946.0.16': {}
+ '@types/hast@3.0.4':
+ dependencies:
+ '@types/unist': 3.0.3
+
'@types/json-schema@7.0.15': {}
'@types/lodash@4.17.23': {}
+ '@types/mdast@4.0.4':
+ dependencies:
+ '@types/unist': 3.0.3
+
'@types/mdx@2.0.13': {}
'@types/node@22.19.1':
@@ -4337,6 +4577,8 @@ snapshots:
'@types/trusted-types@2.0.7':
optional: true
+ '@types/unist@3.0.3': {}
+
'@typescript-eslint/eslint-plugin@8.47.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)':
dependencies:
'@eslint-community/regexpp': 4.12.2
@@ -4430,6 +4672,8 @@ snapshots:
'@typescript-eslint/types': 8.47.0
eslint-visitor-keys: 4.2.1
+ '@ungap/structured-clone@1.3.0': {}
+
'@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6))':
dependencies:
'@babel/core': 7.28.5
@@ -4617,6 +4861,8 @@ snapshots:
caniuse-lite@1.0.30001756: {}
+ ccount@2.0.1: {}
+
chai@5.3.3:
dependencies:
assertion-error: 2.0.1
@@ -4630,6 +4876,10 @@ snapshots:
ansi-styles: 4.3.0
supports-color: 7.2.0
+ character-entities-html4@2.1.0: {}
+
+ character-entities-legacy@3.0.0: {}
+
check-error@2.1.3: {}
chevrotain-allstar@0.3.1(chevrotain@11.0.3):
@@ -4666,6 +4916,9 @@ snapshots:
color-convert: 2.0.1
color-string: 1.9.1
+ comlink@4.4.2: {}
+
+ comma-separated-tokens@2.0.3: {}
commander@14.0.2: {}
commander@7.2.0: {}
@@ -4917,6 +5170,10 @@ snapshots:
detect-libc@2.1.2: {}
+ devlop@1.1.0:
+ dependencies:
+ dequal: 2.0.3
+
diff@8.0.2: {}
doctrine@3.0.0:
@@ -5194,8 +5451,30 @@ snapshots:
dependencies:
function-bind: 1.1.2
+ 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-whitespace@3.0.0:
+ dependencies:
+ '@types/hast': 3.0.4
+
+ hast@1.0.0: {}
+
he@1.2.0: {}
+ html-void-elements@3.0.0: {}
+
iconv-lite@0.6.3:
dependencies:
safer-buffer: 2.1.2
@@ -5405,6 +5684,20 @@ snapshots:
marked@16.4.2: {}
+ math-intrinsics@1.1.0: {}
+
+ mdast-util-to-hast@13.2.1:
+ dependencies:
+ '@types/hast': 3.0.4
+ '@types/mdast': 4.0.4
+ '@ungap/structured-clone': 1.3.0
+ devlop: 1.1.0
+ micromark-util-sanitize-uri: 2.0.1
+ trim-lines: 3.0.1
+ unist-util-position: 5.0.0
+ unist-util-visit: 5.0.0
+ vfile: 6.0.3
+
merge2@1.4.1: {}
mermaid@11.12.2:
@@ -5430,6 +5723,23 @@ snapshots:
ts-dedent: 2.2.0
uuid: 11.1.0
+ micromark-util-character@2.1.1:
+ dependencies:
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-util-encode@2.0.1: {}
+
+ micromark-util-sanitize-uri@2.0.1:
+ dependencies:
+ micromark-util-character: 2.1.1
+ micromark-util-encode: 2.0.1
+ micromark-util-symbol: 2.0.1
+
+ micromark-util-symbol@2.0.1: {}
+
+ micromark-util-types@2.0.2: {}
+
micromatch@4.0.8:
dependencies:
braces: 3.0.3
@@ -5487,6 +5797,14 @@ snapshots:
pathe: 2.0.3
tinyexec: 1.0.2
+ 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@10.2.0:
dependencies:
default-browser: 5.4.0
@@ -5582,6 +5900,8 @@ snapshots:
ansi-styles: 5.2.0
react-is: 17.0.2
+ property-information@7.1.0: {}
+
punycode@2.3.1: {}
quansync@0.2.11: {}
@@ -5634,6 +5954,36 @@ snapshots:
indent-string: 4.0.0
strip-indent: 3.0.0
+ reflect.getprototypeof@1.0.10:
+ dependencies:
+ call-bind: 1.0.8
+ define-properties: 1.2.1
+ es-abstract: 1.24.0
+ es-errors: 1.3.0
+ es-object-atoms: 1.1.1
+ get-intrinsic: 1.3.0
+ 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
+ define-properties: 1.2.1
+ es-errors: 1.3.0
+ get-proto: 1.0.1
+ gopd: 1.2.0
+ set-function-name: 2.0.2
+
require-from-string@2.0.2: {}
resolve-from@4.0.0: {}
@@ -5714,6 +6064,45 @@ snapshots:
shebang-regex@3.0.0: {}
+ shiki@3.21.0:
+ dependencies:
+ '@shikijs/core': 3.21.0
+ '@shikijs/engine-javascript': 3.21.0
+ '@shikijs/engine-oniguruma': 3.21.0
+ '@shikijs/langs': 3.21.0
+ '@shikijs/themes': 3.21.0
+ '@shikijs/types': 3.21.0
+ '@shikijs/vscode-textmate': 10.0.2
+ '@types/hast': 3.0.4
+
+ side-channel-list@1.0.0:
+ dependencies:
+ es-errors: 1.3.0
+ object-inspect: 1.13.4
+
+ side-channel-map@1.0.1:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ get-intrinsic: 1.3.0
+ object-inspect: 1.13.4
+
+ side-channel-weakmap@1.0.2:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ get-intrinsic: 1.3.0
+ object-inspect: 1.13.4
+ side-channel-map: 1.0.1
+
+ side-channel@1.1.0:
+ dependencies:
+ es-errors: 1.3.0
+ object-inspect: 1.13.4
+ side-channel-list: 1.0.0
+ side-channel-map: 1.0.1
+ side-channel-weakmap: 1.0.2
+
signal-exit@4.1.0: {}
simple-swizzle@0.2.4:
@@ -5726,6 +6115,8 @@ snapshots:
source-map@0.6.1: {}
+ space-separated-tokens@2.0.2: {}
+
sprintf-js@1.0.3: {}
storybook@10.1.10(@testing-library/dom@10.4.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
@@ -5763,6 +6154,34 @@ snapshots:
emoji-regex: 9.2.2
strip-ansi: 7.1.2
+ string.prototype.trim@1.2.10:
+ dependencies:
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ define-data-property: 1.1.4
+ define-properties: 1.2.1
+ es-abstract: 1.24.0
+ es-object-atoms: 1.1.1
+ has-property-descriptors: 1.0.2
+
+ string.prototype.trimend@1.0.9:
+ dependencies:
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ define-properties: 1.2.1
+ es-object-atoms: 1.1.1
+
+ string.prototype.trimstart@1.0.8:
+ dependencies:
+ call-bind: 1.0.8
+ define-properties: 1.2.1
+ es-object-atoms: 1.1.1
+
+ stringify-entities@4.0.4:
+ dependencies:
+ character-entities-html4: 2.1.0
+ character-entities-legacy: 3.0.0
+
strip-ansi@6.0.1:
dependencies:
ansi-regex: 5.0.1
@@ -5816,6 +6235,7 @@ snapshots:
dependencies:
is-number: 7.0.0
+ trim-lines@3.0.1: {}
trpc-cli@0.12.2(@trpc/server@11.8.1(typescript@5.9.3))(zod@4.3.5):
dependencies:
commander: 14.0.2
@@ -5874,6 +6294,29 @@ snapshots:
undici-types@6.21.0: {}
+ unist-util-is@6.0.1:
+ dependencies:
+ '@types/unist': 3.0.3
+
+ unist-util-position@5.0.0:
+ dependencies:
+ '@types/unist': 3.0.3
+
+ unist-util-stringify-position@4.0.0:
+ dependencies:
+ '@types/unist': 3.0.3
+
+ unist-util-visit-parents@6.0.2:
+ dependencies:
+ '@types/unist': 3.0.3
+ unist-util-is: 6.0.1
+
+ unist-util-visit@5.0.0:
+ dependencies:
+ '@types/unist': 3.0.3
+ unist-util-is: 6.0.1
+ unist-util-visit-parents: 6.0.2
+
universalify@2.0.1: {}
unplugin@2.3.10:
@@ -5901,6 +6344,16 @@ snapshots:
uuid@11.1.0: {}
+ vfile-message@4.0.3:
+ dependencies:
+ '@types/unist': 3.0.3
+ unist-util-stringify-position: 4.0.0
+
+ vfile@6.0.3:
+ dependencies:
+ '@types/unist': 3.0.3
+ vfile-message: 4.0.3
+
vite-plugin-dts@4.5.4(@types/node@22.19.1)(rollup@4.53.3)(typescript@5.9.3)(vite@6.4.1(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)):
dependencies:
'@microsoft/api-extractor': 7.55.0(@types/node@22.19.1)
@@ -5986,4 +6439,5 @@ snapshots:
yocto-queue@0.1.0: {}
+ zwitch@2.0.4: {}
zod@4.3.5: {}