From 0f7aec8e13aa7776b7342e2679c5c45daa1acc95 Mon Sep 17 00:00:00 2001 From: Mohamed Shams El-Deen Date: Sun, 13 Sep 2026 20:01:11 +0300 Subject: [PATCH 1/8] feat(ui): implement deep linking in CodeTabs via URL hash --- .../src/Common/CodeTabs/index.tsx | 2 +- packages/ui-components/src/MDX/CodeTabs.tsx | 50 +++++++++++++++++-- 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/packages/ui-components/src/Common/CodeTabs/index.tsx b/packages/ui-components/src/Common/CodeTabs/index.tsx index 12ff05973037e..910fda8abfc58 100644 --- a/packages/ui-components/src/Common/CodeTabs/index.tsx +++ b/packages/ui-components/src/Common/CodeTabs/index.tsx @@ -6,7 +6,7 @@ import styles from './index.module.css'; type CodeTabsProps = Pick< ComponentProps, - 'tabs' | 'defaultValue' | 'children' | 'addons' + 'tabs' | 'defaultValue' | 'value' | 'onValueChange' | 'children' | 'addons' >; const CodeTabs: FC = ({ ...props }) => ( diff --git a/packages/ui-components/src/MDX/CodeTabs.tsx b/packages/ui-components/src/MDX/CodeTabs.tsx index a4092ae6ece73..ccb1bacade6f8 100644 --- a/packages/ui-components/src/MDX/CodeTabs.tsx +++ b/packages/ui-components/src/MDX/CodeTabs.tsx @@ -1,5 +1,7 @@ +'use client'; + import * as TabsPrimitive from '@radix-ui/react-tabs'; -import { useMemo } from 'react'; +import { useEffect, useId, useMemo, useState } from 'react'; import CodeTabs from '#ui/Common/CodeTabs'; @@ -10,6 +12,7 @@ type MDXCodeTabsProps = { languages: string; displayNames?: string; defaultTab?: string; + groupId?: string; }; const NAME_OVERRIDES: Record = { @@ -21,8 +24,12 @@ const MDXCodeTabs: FC = ({ displayNames: rawDisplayNames, children: codes, defaultTab = '0', + groupId, ...props }) => { + const id = useId(); + const prefix = groupId ? `tab-${groupId}` : `tab-${id}`; + const { tabs, languages } = useMemo(() => { const occurrences: Record = {}; @@ -43,17 +50,54 @@ const MDXCodeTabs: FC = ({ return { key: `${language}-${index}`, + anchorId: `${prefix}-${language}-${index}`.replace( + /[^a-zA-Z0-9-_]/g, + '-' + ), label, }; }); return { tabs, languages }; - }, [rawLanguages, rawDisplayNames]); + }, [rawLanguages, rawDisplayNames, prefix]); + + const [activeTab, setActiveTab] = useState(() => { + if (typeof window !== 'undefined' && window.location.hash) { + const hash = window.location.hash.slice(1); + const matched = tabs.find(t => t.anchorId === hash); + if (matched) { + return matched.key; + } + } + return tabs[Number(defaultTab)]?.key ?? tabs[0].key; + }); + + useEffect(() => { + const handleHashChange = () => { + const hash = window.location.hash.slice(1); + const matched = tabs.find(t => t.anchorId === hash); + if (matched) { + setActiveTab(matched.key); + } + }; + + window.addEventListener('hashchange', handleHashChange); + return () => window.removeEventListener('hashchange', handleHashChange); + }, [tabs]); + + const handleValueChange = (value: string) => { + setActiveTab(value); + const matched = tabs.find(t => t.key === value); + if (matched) { + window.history.replaceState(null, '', `#${matched.anchorId}`); + } + }; return ( {languages.map((_, index) => ( From 97b39de079c36d4c29a4277fbec2eab07ab08b93 Mon Sep 17 00:00:00 2001 From: Mohamed Shams El-Deen Date: Tue, 15 Sep 2026 18:52:44 +0300 Subject: [PATCH 2/8] feat(site,ui): add HashContext and HashProvider for global URL state --- apps/site/app/[locale]/layout.tsx | 9 ++-- apps/site/hooks/useHash.ts | 44 +++++++++++++++++++ apps/site/providers/hashProvider.tsx | 13 ++++++ .../src/contexts/HashContext.tsx | 15 +++++++ 4 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 apps/site/hooks/useHash.ts create mode 100644 apps/site/providers/hashProvider.tsx create mode 100644 packages/ui-components/src/contexts/HashContext.tsx diff --git a/apps/site/app/[locale]/layout.tsx b/apps/site/app/[locale]/layout.tsx index 3e1c358b1103f..6f46973abc806 100644 --- a/apps/site/app/[locale]/layout.tsx +++ b/apps/site/app/[locale]/layout.tsx @@ -6,6 +6,7 @@ import { getLocale } from 'next-intl/server'; import BaseLayout from '#site/layouts/Base'; import { IBM_PLEX_MONO, OPEN_SANS } from '#site/next.fonts'; +import { HashProvider } from '#site/providers/hashProvider'; import { ThemeProvider } from '#site/providers/themeProvider'; import type { FC, PropsWithChildren } from 'react'; @@ -29,9 +30,11 @@ const RootLayout: FC = async ({ children }) => { > - - {children} - + + + {children} + + void>(); +let isListening = false; + +const onHashChange = () => { + listeners.forEach(rerender => rerender()); +}; + +const subscribe = (callback: () => void) => { + listeners.add(callback); + if (!isListening && typeof window !== 'undefined') { + window.addEventListener('hashchange', onHashChange); + isListening = true; + } + return () => { + listeners.delete(callback); + if (listeners.size === 0 && typeof window !== 'undefined') { + window.removeEventListener('hashchange', onHashChange); + isListening = false; + } + }; +}; + +const getSnapshot = () => + typeof window !== 'undefined' ? window.location.hash.slice(1) : ''; +const getServerSnapshot = () => ''; + +const useHash = () => { + const hash = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); + + const setHash = (newHash: string) => { + if (typeof window !== 'undefined') { + window.history.replaceState(null, '', `#${newHash}`); + onHashChange(); + } + }; + + return [hash, setHash] as const; +}; + +export default useHash; diff --git a/apps/site/providers/hashProvider.tsx b/apps/site/providers/hashProvider.tsx new file mode 100644 index 0000000000000..ad4439dfb8d01 --- /dev/null +++ b/apps/site/providers/hashProvider.tsx @@ -0,0 +1,13 @@ +'use client'; + +import { HashContext } from '@node-core/ui-components/contexts/HashContext'; + +import useHash from '#site/hooks/useHash'; + +import type { FC, PropsWithChildren } from 'react'; + +export const HashProvider: FC = ({ children }) => { + const [hash, setHash] = useHash(); + + return {children}; +}; diff --git a/packages/ui-components/src/contexts/HashContext.tsx b/packages/ui-components/src/contexts/HashContext.tsx new file mode 100644 index 0000000000000..7a738fd53eea7 --- /dev/null +++ b/packages/ui-components/src/contexts/HashContext.tsx @@ -0,0 +1,15 @@ +'use client'; + +import { createContext, use } from 'react'; + +type HashContextType = { + hash: string; + setHash: (newHash: string) => void; +}; + +export const HashContext = createContext({ + hash: '', + setHash: () => {}, +}); + +export const useHashContext = () => use(HashContext); From 07251e622a599e8f8f306602fe9ce57b032aeeed Mon Sep 17 00:00:00 2001 From: Mohamed Shams El-Deen Date: Tue, 15 Sep 2026 18:53:57 +0300 Subject: [PATCH 3/8] refactor(ui): isolate window hash logic from CodeTabs --- .../src/MDX/CodeTabs/CodeTabsWithHash.tsx | 52 +++++++++++++++++++ .../MDX/{CodeTabs.tsx => CodeTabs/index.tsx} | 48 +++-------------- 2 files changed, 60 insertions(+), 40 deletions(-) create mode 100644 packages/ui-components/src/MDX/CodeTabs/CodeTabsWithHash.tsx rename packages/ui-components/src/MDX/{CodeTabs.tsx => CodeTabs/index.tsx} (58%) diff --git a/packages/ui-components/src/MDX/CodeTabs/CodeTabsWithHash.tsx b/packages/ui-components/src/MDX/CodeTabs/CodeTabsWithHash.tsx new file mode 100644 index 0000000000000..a4f7838745727 --- /dev/null +++ b/packages/ui-components/src/MDX/CodeTabs/CodeTabsWithHash.tsx @@ -0,0 +1,52 @@ +'use client'; + +import { useCallback, useState } from 'react'; + +import CodeTabs from '#ui/Common/CodeTabs'; +import { useHashContext } from '#ui/contexts/HashContext'; + +import type { ComponentProps, FC } from 'react'; + +type CodeTabsWithHashProps = Omit, 'tabs'> & { + tabs: Array<{ key: string; label: string; anchorId: string }>; +}; + +const CodeTabsWithHash: FC = ({ + tabs, + defaultValue, + ...props +}) => { + const { hash, setHash } = useHashContext(); + const [activeTab, setActiveTab] = useState(defaultValue); + const [prevHash, setPrevHash] = useState(hash); + + if (hash !== prevHash) { + setPrevHash(hash); + const matched = tabs.find(t => t.anchorId === hash); + if (matched && matched.key !== activeTab) { + setActiveTab(matched.key); + } + } + + const handleValueChange = useCallback( + (value: string) => { + setActiveTab(value); + const matched = tabs.find(t => t.key === value); + if (matched) { + setHash(matched.anchorId); + } + }, + [tabs, setHash] + ); + + return ( + + ); +}; + +export default CodeTabsWithHash; diff --git a/packages/ui-components/src/MDX/CodeTabs.tsx b/packages/ui-components/src/MDX/CodeTabs/index.tsx similarity index 58% rename from packages/ui-components/src/MDX/CodeTabs.tsx rename to packages/ui-components/src/MDX/CodeTabs/index.tsx index ccb1bacade6f8..b2af7bb20ef0d 100644 --- a/packages/ui-components/src/MDX/CodeTabs.tsx +++ b/packages/ui-components/src/MDX/CodeTabs/index.tsx @@ -1,12 +1,12 @@ -'use client'; - import * as TabsPrimitive from '@radix-ui/react-tabs'; -import { useEffect, useId, useMemo, useState } from 'react'; +import { useMemo } from 'react'; import CodeTabs from '#ui/Common/CodeTabs'; import type { FC, ReactElement } from 'react'; +import CodeTabsWithHash from './CodeTabsWithHash'; + type MDXCodeTabsProps = { children: Array>; languages: string; @@ -27,8 +27,7 @@ const MDXCodeTabs: FC = ({ groupId, ...props }) => { - const id = useId(); - const prefix = groupId ? `tab-${groupId}` : `tab-${id}`; + const prefix = groupId ?? 'tab'; const { tabs, languages } = useMemo(() => { const occurrences: Record = {}; @@ -61,43 +60,12 @@ const MDXCodeTabs: FC = ({ return { tabs, languages }; }, [rawLanguages, rawDisplayNames, prefix]); - const [activeTab, setActiveTab] = useState(() => { - if (typeof window !== 'undefined' && window.location.hash) { - const hash = window.location.hash.slice(1); - const matched = tabs.find(t => t.anchorId === hash); - if (matched) { - return matched.key; - } - } - return tabs[Number(defaultTab)]?.key ?? tabs[0].key; - }); - - useEffect(() => { - const handleHashChange = () => { - const hash = window.location.hash.slice(1); - const matched = tabs.find(t => t.anchorId === hash); - if (matched) { - setActiveTab(matched.key); - } - }; - - window.addEventListener('hashchange', handleHashChange); - return () => window.removeEventListener('hashchange', handleHashChange); - }, [tabs]); - - const handleValueChange = (value: string) => { - setActiveTab(value); - const matched = tabs.find(t => t.key === value); - if (matched) { - window.history.replaceState(null, '', `#${matched.anchorId}`); - } - }; + const Component = groupId ? CodeTabsWithHash : CodeTabs; return ( - {languages.map((_, index) => ( @@ -109,7 +77,7 @@ const MDXCodeTabs: FC = ({ {codes[index]} ))} - + ); }; From d4ed69883fd4c6da49c8b6a7b772f643f4eddf5e Mon Sep 17 00:00:00 2001 From: Mohamed Shams El-Deen Date: Tue, 15 Sep 2026 23:27:25 +0300 Subject: [PATCH 4/8] chore: improve code documentation for useHash hook --- apps/site/hooks/useHash.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/site/hooks/useHash.ts b/apps/site/hooks/useHash.ts index d771455f09b11..ff6b852b20140 100644 --- a/apps/site/hooks/useHash.ts +++ b/apps/site/hooks/useHash.ts @@ -29,6 +29,7 @@ const getSnapshot = () => const getServerSnapshot = () => ''; const useHash = () => { + // Works in both client and server environments, provides the initial value for the store during server rendering and hydration avoiding hydration mismatches const hash = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); const setHash = (newHash: string) => { From c79ec058e73b7d4608e5a60bb7dbb263e88640c2 Mon Sep 17 00:00:00 2001 From: Mohamed Shams El-Deen Date: Wed, 16 Sep 2026 00:10:33 +0300 Subject: [PATCH 5/8] feat(ui): use native DOM id for CodeTabs scrolling --- packages/ui-components/src/Common/Tabs/index.tsx | 2 ++ .../src/MDX/CodeTabs/CodeTabsWithHash.tsx | 8 ++++---- packages/ui-components/src/MDX/CodeTabs/index.tsx | 11 ++++------- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/packages/ui-components/src/Common/Tabs/index.tsx b/packages/ui-components/src/Common/Tabs/index.tsx index bd2f769b596a6..c20e40eeaccf2 100644 --- a/packages/ui-components/src/Common/Tabs/index.tsx +++ b/packages/ui-components/src/Common/Tabs/index.tsx @@ -7,6 +7,7 @@ import styles from './index.module.css'; type Tab = { key: string; + id?: string; label: string; secondaryLabel?: string; value?: string; @@ -34,6 +35,7 @@ const Tabs: FC> = ({ {tabs.map(tab => ( diff --git a/packages/ui-components/src/MDX/CodeTabs/CodeTabsWithHash.tsx b/packages/ui-components/src/MDX/CodeTabs/CodeTabsWithHash.tsx index a4f7838745727..0794ded34a742 100644 --- a/packages/ui-components/src/MDX/CodeTabs/CodeTabsWithHash.tsx +++ b/packages/ui-components/src/MDX/CodeTabs/CodeTabsWithHash.tsx @@ -8,7 +8,7 @@ import { useHashContext } from '#ui/contexts/HashContext'; import type { ComponentProps, FC } from 'react'; type CodeTabsWithHashProps = Omit, 'tabs'> & { - tabs: Array<{ key: string; label: string; anchorId: string }>; + tabs: Array<{ key: string; label: string; id?: string }>; }; const CodeTabsWithHash: FC = ({ @@ -22,7 +22,7 @@ const CodeTabsWithHash: FC = ({ if (hash !== prevHash) { setPrevHash(hash); - const matched = tabs.find(t => t.anchorId === hash); + const matched = tabs.find(t => t.id === hash); if (matched && matched.key !== activeTab) { setActiveTab(matched.key); } @@ -32,8 +32,8 @@ const CodeTabsWithHash: FC = ({ (value: string) => { setActiveTab(value); const matched = tabs.find(t => t.key === value); - if (matched) { - setHash(matched.anchorId); + if (matched?.id) { + setHash(matched.id); } }, [tabs, setHash] diff --git a/packages/ui-components/src/MDX/CodeTabs/index.tsx b/packages/ui-components/src/MDX/CodeTabs/index.tsx index b2af7bb20ef0d..1042bcaff57ed 100644 --- a/packages/ui-components/src/MDX/CodeTabs/index.tsx +++ b/packages/ui-components/src/MDX/CodeTabs/index.tsx @@ -27,8 +27,6 @@ const MDXCodeTabs: FC = ({ groupId, ...props }) => { - const prefix = groupId ?? 'tab'; - const { tabs, languages } = useMemo(() => { const occurrences: Record = {}; @@ -49,16 +47,15 @@ const MDXCodeTabs: FC = ({ return { key: `${language}-${index}`, - anchorId: `${prefix}-${language}-${index}`.replace( - /[^a-zA-Z0-9-_]/g, - '-' - ), label, + id: + groupId && + `${groupId}-${language}-${index}`.replace(/[^a-zA-Z0-9-_]/g, '-'), }; }); return { tabs, languages }; - }, [rawLanguages, rawDisplayNames, prefix]); + }, [rawLanguages, rawDisplayNames, groupId]); const Component = groupId ? CodeTabsWithHash : CodeTabs; From 7bc608a0f1aae96cc740f1d8ccf005d73e34313d Mon Sep 17 00:00:00 2001 From: Mohamed Shams El-Deen Date: Thu, 1 Oct 2026 23:37:53 +0300 Subject: [PATCH 6/8] fix(site): dispatch global hashchange event on hash update --- apps/site/hooks/useHash.ts | 38 +++++++++----------------------------- 1 file changed, 9 insertions(+), 29 deletions(-) diff --git a/apps/site/hooks/useHash.ts b/apps/site/hooks/useHash.ts index ff6b852b20140..daddf8e23e56a 100644 --- a/apps/site/hooks/useHash.ts +++ b/apps/site/hooks/useHash.ts @@ -1,43 +1,23 @@ 'use client'; -import { useSyncExternalStore } from 'react'; - -const listeners = new Set<() => void>(); -let isListening = false; - -const onHashChange = () => { - listeners.forEach(rerender => rerender()); -}; +import { useSyncExternalStore, useCallback } from 'react'; const subscribe = (callback: () => void) => { - listeners.add(callback); - if (!isListening && typeof window !== 'undefined') { - window.addEventListener('hashchange', onHashChange); - isListening = true; - } - return () => { - listeners.delete(callback); - if (listeners.size === 0 && typeof window !== 'undefined') { - window.removeEventListener('hashchange', onHashChange); - isListening = false; - } - }; + window.addEventListener('hashchange', callback); + return () => window.removeEventListener('hashchange', callback); }; -const getSnapshot = () => - typeof window !== 'undefined' ? window.location.hash.slice(1) : ''; +const getSnapshot = () => window.location.hash.slice(1); const getServerSnapshot = () => ''; const useHash = () => { - // Works in both client and server environments, provides the initial value for the store during server rendering and hydration avoiding hydration mismatches + // Works in both client and server environments, provides the initial value for the store during server rendering and hydration, avoiding hydration mismatches const hash = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); - const setHash = (newHash: string) => { - if (typeof window !== 'undefined') { - window.history.replaceState(null, '', `#${newHash}`); - onHashChange(); - } - }; + const setHash = useCallback((newHash: string) => { + window.history.replaceState(null, '', `#${newHash}`); + window.dispatchEvent(new Event('hashchange')); + }, []); return [hash, setHash] as const; }; From de1d09c7429afb6e9521f6696b2f958ff0c437cd Mon Sep 17 00:00:00 2001 From: Mohamed Shams El-Deen Date: Thu, 1 Oct 2026 23:49:02 +0300 Subject: [PATCH 7/8] refactor(ui-components): revert CodeTabs and use derived state in CodeTabsWithHash --- .../src/MDX/CodeTabs/CodeTabsWithHash.tsx | 73 ++++++++++++++----- .../ui-components/src/MDX/CodeTabs/index.tsx | 15 +--- 2 files changed, 58 insertions(+), 30 deletions(-) diff --git a/packages/ui-components/src/MDX/CodeTabs/CodeTabsWithHash.tsx b/packages/ui-components/src/MDX/CodeTabs/CodeTabsWithHash.tsx index 0794ded34a742..9bf663270739e 100644 --- a/packages/ui-components/src/MDX/CodeTabs/CodeTabsWithHash.tsx +++ b/packages/ui-components/src/MDX/CodeTabs/CodeTabsWithHash.tsx @@ -1,36 +1,64 @@ 'use client'; -import { useCallback, useState } from 'react'; +import * as TabsPrimitive from '@radix-ui/react-tabs'; +import { useCallback, useMemo } from 'react'; import CodeTabs from '#ui/Common/CodeTabs'; import { useHashContext } from '#ui/contexts/HashContext'; import type { ComponentProps, FC } from 'react'; -type CodeTabsWithHashProps = Omit, 'tabs'> & { - tabs: Array<{ key: string; label: string; id?: string }>; +import MDXCodeTabs from './index'; + +type CodeTabsWithHashProps = ComponentProps & { + groupId: string; +}; + +const NAME_OVERRIDES: Record = { + mjs: 'ESM', }; const CodeTabsWithHash: FC = ({ - tabs, - defaultValue, + languages: rawLanguages, + displayNames: rawDisplayNames, + children: codes, + defaultTab = '0', + groupId, ...props }) => { const { hash, setHash } = useHashContext(); - const [activeTab, setActiveTab] = useState(defaultValue); - const [prevHash, setPrevHash] = useState(hash); - if (hash !== prevHash) { - setPrevHash(hash); - const matched = tabs.find(t => t.id === hash); - if (matched && matched.key !== activeTab) { - setActiveTab(matched.key); - } - } + const { tabs, languages } = useMemo(() => { + const occurrences: Record = {}; + const languages = rawLanguages.split('|'); + const displayNames = rawDisplayNames?.split('|') ?? []; + + const tabs = languages.map((language, index) => { + const base = + displayNames[index]?.trim() || + NAME_OVERRIDES[language] || + language.toUpperCase(); + + const count = occurrences[base] ?? 0; + occurrences[base] = count + 1; + + const label = count > 0 ? `${base} (${count + 1})` : base; + + return { + key: `${language}-${index}`, + label, + id: `${groupId}-${language}-${index}`.replace(/[^a-zA-Z0-9-_]/g, '-'), + }; + }); + + return { tabs, languages }; + }, [rawLanguages, rawDisplayNames, groupId]); + + const activeTab = + tabs.find(t => t.id === hash)?.key ?? tabs[Number(defaultTab)].key; const handleValueChange = useCallback( (value: string) => { - setActiveTab(value); const matched = tabs.find(t => t.key === value); if (matched?.id) { setHash(matched.id); @@ -41,12 +69,21 @@ const CodeTabsWithHash: FC = ({ return ( + {...props} + > + {languages.map((_, index) => ( + + {codes[index]} + + ))} + ); }; - export default CodeTabsWithHash; diff --git a/packages/ui-components/src/MDX/CodeTabs/index.tsx b/packages/ui-components/src/MDX/CodeTabs/index.tsx index 1042bcaff57ed..a4092ae6ece73 100644 --- a/packages/ui-components/src/MDX/CodeTabs/index.tsx +++ b/packages/ui-components/src/MDX/CodeTabs/index.tsx @@ -5,14 +5,11 @@ import CodeTabs from '#ui/Common/CodeTabs'; import type { FC, ReactElement } from 'react'; -import CodeTabsWithHash from './CodeTabsWithHash'; - type MDXCodeTabsProps = { children: Array>; languages: string; displayNames?: string; defaultTab?: string; - groupId?: string; }; const NAME_OVERRIDES: Record = { @@ -24,7 +21,6 @@ const MDXCodeTabs: FC = ({ displayNames: rawDisplayNames, children: codes, defaultTab = '0', - groupId, ...props }) => { const { tabs, languages } = useMemo(() => { @@ -48,19 +44,14 @@ const MDXCodeTabs: FC = ({ return { key: `${language}-${index}`, label, - id: - groupId && - `${groupId}-${language}-${index}`.replace(/[^a-zA-Z0-9-_]/g, '-'), }; }); return { tabs, languages }; - }, [rawLanguages, rawDisplayNames, groupId]); - - const Component = groupId ? CodeTabsWithHash : CodeTabs; + }, [rawLanguages, rawDisplayNames]); return ( - = ({ {codes[index]} ))} - + ); }; From e9021060cc0d98b109b250463e41df52894fb1a7 Mon Sep 17 00:00:00 2001 From: Mohamed Shams El-Deen Date: Thu, 1 Oct 2026 23:53:15 +0300 Subject: [PATCH 8/8] feat(rehype-shiki): route to CodeTabsWithHash if groupId exists --- apps/site/mdx/components.mjs | 2 ++ packages/rehype-shiki/src/plugin.mjs | 9 ++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/apps/site/mdx/components.mjs b/apps/site/mdx/components.mjs index 46db7dc2afe11..7ae15c06dba08 100644 --- a/apps/site/mdx/components.mjs +++ b/apps/site/mdx/components.mjs @@ -4,6 +4,7 @@ import AlertBox from '@node-core/ui-components/Common/AlertBox'; import BadgeGroup from '@node-core/ui-components/Common/BadgeGroup'; import Blockquote from '@node-core/ui-components/Common/Blockquote'; import MDXCodeTabs from '@node-core/ui-components/MDX/CodeTabs'; +import CodeTabsWithHash from '@node-core/ui-components/MDX/CodeTabs/CodeTabsWithHash'; import { MDXTooltip, MDXTooltipContent, @@ -58,6 +59,7 @@ export default { AlertBox, // Renders MDX CodeTabs CodeTabs: MDXCodeTabs, + CodeTabsWithHash, // Renders Tooltips MDXTooltip, MDXTooltipContent, diff --git a/packages/rehype-shiki/src/plugin.mjs b/packages/rehype-shiki/src/plugin.mjs index 6f08603fd7293..384ecbfb07b0f 100644 --- a/packages/rehype-shiki/src/plugin.mjs +++ b/packages/rehype-shiki/src/plugin.mjs @@ -67,6 +67,7 @@ export default async function rehypeShikiji(options) { const codeTabsChildren = []; let defaultTab = '0'; + let groupId; let currentIndex = index; while (isCodeBlock(parent?.children[currentIndex])) { @@ -84,6 +85,11 @@ export default async function rehypeShikiji(options) { // Map the display names of each variant for the CodeTab displayNames.push(meta.displayName?.replaceAll('|', '') ?? ''); + // Extract groupId if it exists + if (meta.groupId) { + groupId = meta.groupId; + } + codeTabsChildren.push(parent?.children[currentIndex]); // If `active="true"` is provided in a CodeBox @@ -102,12 +108,13 @@ export default async function rehypeShikiji(options) { if (codeTabsChildren.length >= 2) { const codeTabElement = { type: 'element', - tagName: 'CodeTabs', + tagName: groupId ? 'CodeTabsWithHash' : 'CodeTabs', children: codeTabsChildren, properties: { languages: languages.join('|'), displayNames: displayNames.join('|'), defaultTab, + groupId, }, };