From f1c4849301e4aa7e9f6379b414fc2c1f775c16f5 Mon Sep 17 00:00:00 2001 From: Mohamed Shams El-Deen Date: Thu, 20 Aug 2026 00:00:17 +0300 Subject: [PATCH 1/8] feat(ui): group overloaded functions into tabs --- packages/react/package.json | 1 + packages/react/src/html/constants.mjs | 4 ++ .../html/ui/components/OverloadTabs/index.jsx | 34 +++++++++++ .../components/OverloadTabs/index.module.css | 21 +++++++ packages/react/src/html/ui/index.css | 6 ++ .../react/src/jsx-ast/utils/buildContent.mjs | 58 ++++++++++++++++++- pnpm-lock.yaml | 3 + 7 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 packages/react/src/html/ui/components/OverloadTabs/index.jsx create mode 100644 packages/react/src/html/ui/components/OverloadTabs/index.module.css diff --git a/packages/react/package.json b/packages/react/package.json index 180d18a72..974451561 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -32,6 +32,7 @@ "@fontsource-variable/open-sans": "^5.3.0", "@fontsource/ibm-plex-mono": "^5.3.0", "@heroicons/react": "^2.2.0", + "@radix-ui/react-tabs": "^1.1.0", "@doc-kit/core": "workspace:*", "@node-core/rehype-shiki": "^1.4.3", "@node-core/ui-components": "^1.7.6", diff --git a/packages/react/src/html/constants.mjs b/packages/react/src/html/constants.mjs index 3850a2a38..ae304b32e 100644 --- a/packages/react/src/html/constants.mjs +++ b/packages/react/src/html/constants.mjs @@ -30,6 +30,10 @@ export const JSX_IMPORTS = { name: 'DocumentationIndex', source: resolve(ROOT, './ui/components/DocumentationIndex'), }, + OverloadTabs: { + name: 'OverloadTabs', + source: resolve(ROOT, './ui/components/OverloadTabs'), + }, MDXTooltip: { name: 'MDXTooltip', isDefaultExport: false, diff --git a/packages/react/src/html/ui/components/OverloadTabs/index.jsx b/packages/react/src/html/ui/components/OverloadTabs/index.jsx new file mode 100644 index 000000000..0012e565c --- /dev/null +++ b/packages/react/src/html/ui/components/OverloadTabs/index.jsx @@ -0,0 +1,34 @@ +import Tabs from '@node-core/ui-components/Common/Tabs'; +import * as TabsPrimitive from '@radix-ui/react-tabs'; + +import styles from './index.module.css'; +import withIsland from '../../islands/withIsland.jsx'; + +const OverloadTabs = ({ children }) => { + const tabs = children.map((_, index) => ({ + key: `${index + 1}`, + label: `${index + 1}`, + })); + + return ( + +
+ {children.map((child, index) => ( + + {child} + + ))} +
+
+ ); +}; + +export default withIsland(OverloadTabs, { + name: 'OverloadTabs', + on: { interaction: 'pointerover,focusin,touchstart' }, +}); diff --git a/packages/react/src/html/ui/components/OverloadTabs/index.module.css b/packages/react/src/html/ui/components/OverloadTabs/index.module.css new file mode 100644 index 000000000..80baaf7ef --- /dev/null +++ b/packages/react/src/html/ui/components/OverloadTabs/index.module.css @@ -0,0 +1,21 @@ +.panelContainer { + display: grid; + grid-template-columns: 1fr; + grid-template-rows: 1fr; +} + +.panel { + grid-column: 1; + grid-row: 1; + opacity: 1; + visibility: visible; + pointer-events: auto; + transition: opacity 0.2s ease; + margin-top: calc(var(--spacing, 0.25rem) * 2); +} + +.panel[data-state="inactive"] { + opacity: 0; + visibility: hidden; + pointer-events: none; +} diff --git a/packages/react/src/html/ui/index.css b/packages/react/src/html/ui/index.css index e385a3eb2..7d97506de 100644 --- a/packages/react/src/html/ui/index.css +++ b/packages/react/src/html/ui/index.css @@ -78,6 +78,12 @@ main { } } + .overload-panel { + display: flex; + flex-direction: column; + gap: calc(var(--spacing) * 6); + } + table { td { word-break: break-all; diff --git a/packages/react/src/jsx-ast/utils/buildContent.mjs b/packages/react/src/jsx-ast/utils/buildContent.mjs index 5b29a2f9c..605161aea 100644 --- a/packages/react/src/jsx-ast/utils/buildContent.mjs +++ b/packages/react/src/jsx-ast/utils/buildContent.mjs @@ -317,6 +317,62 @@ export const processEntry = entry => { return entry.content; }; +/** + * Groups consecutive overloaded function API entries into a single OverloadTabs component. + * @param {Array} processedChildren - The processed JSX AST nodes for the API entries + * @param {Array} originalEntries - The original API metadata entries containing the overload flags + * @returns {Array} The final array of layout children with overloads grouped + */ +export const groupOverloadsIntoTabs = (processedChildren, originalEntries) => { + const finalChildren = []; + + /** + * Wraps the AST children of a function entry in a standard panel div. + * @param {import('estree').Node} rootNode - The AST node representing the function content + * @returns {import('estree').Node} A new div JSX element AST node containing the children + */ + const wrapInDiv = rootNode => { + return createJSXElement('div', { + inline: false, + className: 'overload-panel', + children: rootNode.children || [], + }); + }; + + for (const [i, current] of processedChildren.entries()) { + if (originalEntries[i].heading?.data?.isOverload) { + const last = finalChildren.pop(); + + if (last && last.name === JSX_IMPORTS.OverloadTabs.name) { + current.children.shift(); + last.children.push(wrapInDiv(current)); + finalChildren.push(last); + } else { + const firstHeading = last.children.shift(); + current.children.shift(); + + finalChildren.push(firstHeading); + finalChildren.push({ + type: 'heading', + depth: (firstHeading.depth || 2) + 1, + children: [{ type: 'text', value: 'Overloads' }], + }); + + finalChildren.push( + createJSXElement(JSX_IMPORTS.OverloadTabs.name, { + inline: false, + children: [wrapInDiv(last), wrapInDiv(current)], + }) + ); + } + } else { + finalChildren.push(current); + } + } + + return finalChildren; +}; + /** * Builds the overall document layout tree * @param {Array} entries - API documentation metadata entries @@ -336,7 +392,7 @@ export const createDocumentLayout = async (entries, metadata) => { readingTime: showReadingTime ? await readingTime(extractTextContent(entries)) : undefined, - children: entries.map(processEntry), + children: groupOverloadsIntoTabs(entries.map(processEntry), entries), }), ]); }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 569ca86aa..88bd59bf5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -244,6 +244,9 @@ importers: '@orama/ui': specifier: ^1.5.4 version: 1.5.4(@orama/core@1.2.19)(@types/react@19.2.18)(react@19.2.8)(supports-color@7.2.0) + '@radix-ui/react-tabs': + specifier: ^1.1.0 + version: 1.1.21(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) estree-util-to-js: specifier: ^2.0.0 version: 2.0.0 From 47d88f5da42c1159f1491a5c26499ea68d0d7f0c Mon Sep 17 00:00:00 2001 From: Mohamed Shams El-Deen Date: Thu, 20 Aug 2026 00:34:43 +0300 Subject: [PATCH 2/8] test(jsx-ast): add test to increase coverage of handling overloads --- .../html/ui/components/OverloadTabs/index.jsx | 1 + .../components/OverloadTabs/index.module.css | 2 +- .../utils/__tests__/buildContent.test.mjs | 71 ++++++++++++++++++- 3 files changed, 72 insertions(+), 2 deletions(-) diff --git a/packages/react/src/html/ui/components/OverloadTabs/index.jsx b/packages/react/src/html/ui/components/OverloadTabs/index.jsx index 0012e565c..b4449b358 100644 --- a/packages/react/src/html/ui/components/OverloadTabs/index.jsx +++ b/packages/react/src/html/ui/components/OverloadTabs/index.jsx @@ -1,3 +1,4 @@ +/* eslint-disable react-x/no-array-index-key */ import Tabs from '@node-core/ui-components/Common/Tabs'; import * as TabsPrimitive from '@radix-ui/react-tabs'; diff --git a/packages/react/src/html/ui/components/OverloadTabs/index.module.css b/packages/react/src/html/ui/components/OverloadTabs/index.module.css index 80baaf7ef..b23fe518c 100644 --- a/packages/react/src/html/ui/components/OverloadTabs/index.module.css +++ b/packages/react/src/html/ui/components/OverloadTabs/index.module.css @@ -14,7 +14,7 @@ margin-top: calc(var(--spacing, 0.25rem) * 2); } -.panel[data-state="inactive"] { +.panel[data-state='inactive'] { opacity: 0; visibility: hidden; pointer-events: none; diff --git a/packages/react/src/jsx-ast/utils/__tests__/buildContent.test.mjs b/packages/react/src/jsx-ast/utils/__tests__/buildContent.test.mjs index 1be679643..9836a2681 100644 --- a/packages/react/src/jsx-ast/utils/__tests__/buildContent.test.mjs +++ b/packages/react/src/jsx-ast/utils/__tests__/buildContent.test.mjs @@ -3,7 +3,11 @@ import { describe, it } from 'node:test'; import { setConfig } from '@doc-kit/core/utils/configuration/index.mjs'; -import { transformHeadingNode, gatherChangeEntries } from '../buildContent.mjs'; +import { + transformHeadingNode, + gatherChangeEntries, + groupOverloadsIntoTabs, +} from '../buildContent.mjs'; const heading = { type: 'heading', @@ -190,3 +194,68 @@ describe('gatherChangeEntries', () => { assert.equal(result[1].label, 'Added new feature.'); }); }); + +describe('groupOverloadsIntoTabs', () => { + it('groups consecutive overloads into a single OverloadTabs component', () => { + const originalEntries = [ + { heading: { data: { name: 'funcA', isOverload: false } } }, + { heading: { depth: 3, data: { name: 'funcB', isOverload: false } } }, + { heading: { depth: 3, data: { name: 'funcB', isOverload: true } } }, + { heading: { depth: 3, data: { name: 'funcB', isOverload: true } } }, + { heading: { data: { name: 'funcC', isOverload: false } } }, + ]; + + const makeNode = (className, bodyText) => ({ + type: 'element', + tagName: 'div', + properties: { className }, + children: [ + { type: 'element', tagName: 'h3', depth: 3 }, // The heading to be stripped + { type: 'text', value: bodyText }, + ], + }); + + const processedChildren = [ + makeNode('entry-a', 'body a'), + makeNode('entry-b1', 'body b1'), + makeNode('entry-b2', 'body b2'), + makeNode('entry-b3', 'body b3'), + makeNode('entry-c', 'body c'), + ]; + + const result = groupOverloadsIntoTabs(processedChildren, originalEntries); + + // 0: funcA, 1: funcB-heading, 2: Overloads-heading, 3: OverloadTabs(funcB), 4: funcC + assert.equal(result.length, 5); + + // First element is untouched + assert.equal(result[0].properties.className, 'entry-a'); + + // Second element is the extracted heading + assert.equal(result[1].tagName, 'h3'); + + // Third element is the "Overloads" heading + assert.equal(result[2].children[0].value, 'Overloads'); + + // Fourth element is the OverloadTabs component + const tabsComponent = result[3]; + assert.equal(tabsComponent.name, 'OverloadTabs'); + assert.equal(tabsComponent.children.length, 3); // 3 tab panels + + // Check that the h3 was removed from the overloads and they are wrapped in overload-panel + const panel1 = tabsComponent.children[0]; + const classAttr1 = panel1.attributes.find(a => a.name === 'className'); + assert.equal(classAttr1.value, 'overload-panel'); + assert.equal(panel1.children[0].type, 'text'); + assert.equal(panel1.children[0].value, 'body b1'); + + const panel2 = tabsComponent.children[1]; + const classAttr2 = panel2.attributes.find(a => a.name === 'className'); + assert.equal(classAttr2.value, 'overload-panel'); + assert.equal(panel2.children[0].type, 'text'); + assert.equal(panel2.children[0].value, 'body b2'); + + // Fifth element is untouched + assert.equal(result[4].properties.className, 'entry-c'); + }); +}); From 23ae591b7e1e0b4c8f77495997a5f828c17fdedb Mon Sep 17 00:00:00 2001 From: Mohamed Shams El-Deen Date: Sun, 23 Aug 2026 10:22:20 +0300 Subject: [PATCH 3/8] feat(ui): reimplement the tabbed UI for overloaded functions --- .../html/ui/components/OverloadTabs/index.jsx | 2 +- .../utils/__tests__/buildContent.test.mjs | 67 ++++++++---- .../react/src/jsx-ast/utils/buildContent.mjs | 102 ++++++++++++++---- .../react/src/jsx-ast/utils/signature.mjs | 4 +- 4 files changed, 137 insertions(+), 38 deletions(-) diff --git a/packages/react/src/html/ui/components/OverloadTabs/index.jsx b/packages/react/src/html/ui/components/OverloadTabs/index.jsx index b4449b358..dbe70e896 100644 --- a/packages/react/src/html/ui/components/OverloadTabs/index.jsx +++ b/packages/react/src/html/ui/components/OverloadTabs/index.jsx @@ -8,7 +8,7 @@ import withIsland from '../../islands/withIsland.jsx'; const OverloadTabs = ({ children }) => { const tabs = children.map((_, index) => ({ key: `${index + 1}`, - label: `${index + 1}`, + label: `Overload #${index + 1}`, })); return ( diff --git a/packages/react/src/jsx-ast/utils/__tests__/buildContent.test.mjs b/packages/react/src/jsx-ast/utils/__tests__/buildContent.test.mjs index 9836a2681..db80facde 100644 --- a/packages/react/src/jsx-ast/utils/__tests__/buildContent.test.mjs +++ b/packages/react/src/jsx-ast/utils/__tests__/buildContent.test.mjs @@ -205,28 +205,47 @@ describe('groupOverloadsIntoTabs', () => { { heading: { data: { name: 'funcC', isOverload: false } } }, ]; - const makeNode = (className, bodyText) => ({ - type: 'element', - tagName: 'div', - properties: { className }, - children: [ + const getText = node => { + if (node.type === 'text') { + return node.value; + } + return (node.children || []).map(getText).join(''); + }; + + const makeNode = (className, bodyText, sigText = null) => { + const children = [ { type: 'element', tagName: 'h3', depth: 3 }, // The heading to be stripped { type: 'text', value: bodyText }, - ], - }); + ]; + + if (sigText) { + children.push({ + type: 'element', + tagName: 'div', + properties: { class: 'signature', dataSignatureRaw: sigText }, + }); + } + + return { + type: 'element', + tagName: 'div', + properties: { className }, + children, + }; + }; const processedChildren = [ makeNode('entry-a', 'body a'), - makeNode('entry-b1', 'body b1'), - makeNode('entry-b2', 'body b2'), - makeNode('entry-b3', 'body b3'), + makeNode('entry-b1', 'body b1', 'function funcB(arg1);'), + makeNode('entry-b2', 'body b2', 'function funcB(arg1, arg2);'), + makeNode('entry-b3', 'body b3', 'function funcB(arg1, arg2, arg3);'), makeNode('entry-c', 'body c'), ]; const result = groupOverloadsIntoTabs(processedChildren, originalEntries); - // 0: funcA, 1: funcB-heading, 2: Overloads-heading, 3: OverloadTabs(funcB), 4: funcC - assert.equal(result.length, 5); + // 0: funcA, 1: funcB-heading, 2: Overloads-heading, 3: CombinedSignatures, 4: OverloadTabs(funcB), 5: funcC + assert.equal(result.length, 6); // First element is untouched assert.equal(result[0].properties.className, 'entry-a'); @@ -237,8 +256,21 @@ describe('groupOverloadsIntoTabs', () => { // Third element is the "Overloads" heading assert.equal(result[2].children[0].value, 'Overloads'); - // Fourth element is the OverloadTabs component - const tabsComponent = result[3]; + // Fourth element is the combined signatures block + const combinedSigBlock = result[3]; + assert.deepEqual(combinedSigBlock.properties.className, ['signature']); + + // Assert that the combined signatures contain the formatted 'Overload #X' text + const combinedText = getText(combinedSigBlock); + assert.match(combinedText, /Overload #1/); + assert.match(combinedText, /function funcB\(arg1\);/); + assert.match(combinedText, /Overload #2/); + assert.match(combinedText, /function funcB\(arg1, arg2\);/); + assert.match(combinedText, /Overload #3/); + assert.match(combinedText, /function funcB\(arg1, arg2, arg3\);/); + + // Fifth element is the OverloadTabs component + const tabsComponent = result[4]; assert.equal(tabsComponent.name, 'OverloadTabs'); assert.equal(tabsComponent.children.length, 3); // 3 tab panels @@ -246,16 +278,15 @@ describe('groupOverloadsIntoTabs', () => { const panel1 = tabsComponent.children[0]; const classAttr1 = panel1.attributes.find(a => a.name === 'className'); assert.equal(classAttr1.value, 'overload-panel'); - assert.equal(panel1.children[0].type, 'text'); + + // Second panel child should be the text we inserted assert.equal(panel1.children[0].value, 'body b1'); + assert.equal(result[5].properties.className, 'entry-c'); const panel2 = tabsComponent.children[1]; const classAttr2 = panel2.attributes.find(a => a.name === 'className'); assert.equal(classAttr2.value, 'overload-panel'); assert.equal(panel2.children[0].type, 'text'); assert.equal(panel2.children[0].value, 'body b2'); - - // Fifth element is untouched - assert.equal(result[4].properties.className, 'entry-c'); }); }); diff --git a/packages/react/src/jsx-ast/utils/buildContent.mjs b/packages/react/src/jsx-ast/utils/buildContent.mjs index 605161aea..00ff0fcde 100644 --- a/packages/react/src/jsx-ast/utils/buildContent.mjs +++ b/packages/react/src/jsx-ast/utils/buildContent.mjs @@ -6,6 +6,7 @@ import { GITHUB_BLOB_URL, populate, } from '@doc-kit/core/utils/configuration/templates.mjs'; +import { highlighter } from '@doc-kit/core/utils/highlighter.mjs'; import { parseInline } from '@doc-kit/core/utils/inline.mjs'; import { omitKeys } from '@doc-kit/core/utils/misc.mjs'; import { UNIST } from '@doc-kit/core/utils/queries/index.mjs'; @@ -325,11 +326,12 @@ export const processEntry = entry => { */ export const groupOverloadsIntoTabs = (processedChildren, originalEntries) => { const finalChildren = []; + let activeOverloadGroup = null; /** - * Wraps the AST children of a function entry in a standard panel div. - * @param {import('estree').Node} rootNode - The AST node representing the function content - * @returns {import('estree').Node} A new div JSX element AST node containing the children + * Wraps an AST node's children in a styled panel `div` for tab rendering. + * @param {import('estree').Node} rootNode - The root node whose children will be wrapped. + * @returns {import('estree').Node} The new `div` AST node containing the children. */ const wrapInDiv = rootNode => { return createJSXElement('div', { @@ -339,37 +341,101 @@ export const groupOverloadsIntoTabs = (processedChildren, originalEntries) => { }); }; + /** + * Extracts the raw signature string from an API entry node and removes the signature node from its children. + * @param {import('estree').Node} node - The AST node representing the API entry. + * @returns {string|null} The raw TypeScript signature string, or null if not found. + */ + const extractSignature = node => { + const sigIdx = (node.children || []).findIndex( + c => + c.properties?.className?.includes('signature') || + c.properties?.class === 'signature' + ); + if (sigIdx !== -1) { + const sigNode = node.children.splice(sigIdx, 1)[0]; + return sigNode.properties?.dataSignatureRaw; + } + return null; + }; + + /** + * Finalizes the active overload group by generating a combined signatures block + */ + const pushOverloadGroup = () => { + if (!activeOverloadGroup) { + return; + } + + // Build the combined signature raw string + const combinedSigRaw = activeOverloadGroup.signatures + .map((sig, idx) => `// Overload #${idx + 1}\n${sig}`) + .join('\n\n'); + + const highlighted = highlighter.highlightToHast( + combinedSigRaw, + 'typescript' + ); + const combinedSigNode = createElement('div', { class: 'signature' }, [ + highlighted, + ]); + + // Push combined signatures + finalChildren.push(combinedSigNode); + // Push the tabs + finalChildren.push(activeOverloadGroup.tabsNode); + + activeOverloadGroup = null; + }; + + /** + * Processes a single API entry node belonging to an overload group. + * It extracts its signature and pushes its remaining content into a new tab panel. + * @param {import('estree').Node} node - The AST node to process and add to the active group. + */ + const processOverloadNode = node => { + const sigRaw = extractSignature(node); + if (sigRaw) { + activeOverloadGroup.signatures.push(sigRaw); + } + activeOverloadGroup.tabsNode.children.push(wrapInDiv(node)); + }; + for (const [i, current] of processedChildren.entries()) { if (originalEntries[i].heading?.data?.isOverload) { - const last = finalChildren.pop(); - - if (last && last.name === JSX_IMPORTS.OverloadTabs.name) { + if (activeOverloadGroup) { current.children.shift(); - last.children.push(wrapInDiv(current)); - finalChildren.push(last); + processOverloadNode(current); } else { - const firstHeading = last.children.shift(); + const last = finalChildren.pop(); + activeOverloadGroup = { + firstHeading: last.children.shift(), + signatures: [], + tabsNode: createJSXElement(JSX_IMPORTS.OverloadTabs.name, { + inline: false, + children: [], + }), + }; current.children.shift(); - finalChildren.push(firstHeading); + processOverloadNode(last); + processOverloadNode(current); + + finalChildren.push(activeOverloadGroup.firstHeading); finalChildren.push({ type: 'heading', - depth: (firstHeading.depth || 2) + 1, + depth: (activeOverloadGroup.firstHeading.depth || 2) + 1, children: [{ type: 'text', value: 'Overloads' }], }); - - finalChildren.push( - createJSXElement(JSX_IMPORTS.OverloadTabs.name, { - inline: false, - children: [wrapInDiv(last), wrapInDiv(current)], - }) - ); } } else { + pushOverloadGroup(); finalChildren.push(current); } } + pushOverloadGroup(); + return finalChildren; }; diff --git a/packages/react/src/jsx-ast/utils/signature.mjs b/packages/react/src/jsx-ast/utils/signature.mjs index d7032f326..47d5d5808 100644 --- a/packages/react/src/jsx-ast/utils/signature.mjs +++ b/packages/react/src/jsx-ast/utils/signature.mjs @@ -67,7 +67,9 @@ export const createSignatureCodeBlock = (functionName, signature, heading) => { const sig = generateSignature(functionName, signature, heading); const highlighted = highlighter.highlightToHast(sig, 'typescript'); - return createElement('div', { class: 'signature' }, [highlighted]); + return createElement('div', { class: 'signature', dataSignatureRaw: sig }, [ + highlighted, + ]); }; /** From 2f142aebc7522d7a0839bb8d0ecb445d0cba7507 Mon Sep 17 00:00:00 2001 From: Mohamed Shams El-Deen Date: Wed, 26 Aug 2026 22:49:39 +0300 Subject: [PATCH 4/8] feat(react): adopt standard CodeTabs in overload functions --- packages/react/src/html/constants.mjs | 4 --- .../html/ui/components/OverloadTabs/index.jsx | 35 ------------------- .../components/OverloadTabs/index.module.css | 21 ----------- packages/react/src/html/ui/index.css | 23 ++++++++++-- .../utils/__tests__/buildContent.test.mjs | 32 +++++++++-------- .../react/src/jsx-ast/utils/buildContent.mjs | 33 +++++++++++------ 6 files changed, 59 insertions(+), 89 deletions(-) delete mode 100644 packages/react/src/html/ui/components/OverloadTabs/index.jsx delete mode 100644 packages/react/src/html/ui/components/OverloadTabs/index.module.css diff --git a/packages/react/src/html/constants.mjs b/packages/react/src/html/constants.mjs index ae304b32e..3850a2a38 100644 --- a/packages/react/src/html/constants.mjs +++ b/packages/react/src/html/constants.mjs @@ -30,10 +30,6 @@ export const JSX_IMPORTS = { name: 'DocumentationIndex', source: resolve(ROOT, './ui/components/DocumentationIndex'), }, - OverloadTabs: { - name: 'OverloadTabs', - source: resolve(ROOT, './ui/components/OverloadTabs'), - }, MDXTooltip: { name: 'MDXTooltip', isDefaultExport: false, diff --git a/packages/react/src/html/ui/components/OverloadTabs/index.jsx b/packages/react/src/html/ui/components/OverloadTabs/index.jsx deleted file mode 100644 index dbe70e896..000000000 --- a/packages/react/src/html/ui/components/OverloadTabs/index.jsx +++ /dev/null @@ -1,35 +0,0 @@ -/* eslint-disable react-x/no-array-index-key */ -import Tabs from '@node-core/ui-components/Common/Tabs'; -import * as TabsPrimitive from '@radix-ui/react-tabs'; - -import styles from './index.module.css'; -import withIsland from '../../islands/withIsland.jsx'; - -const OverloadTabs = ({ children }) => { - const tabs = children.map((_, index) => ({ - key: `${index + 1}`, - label: `Overload #${index + 1}`, - })); - - return ( - -
- {children.map((child, index) => ( - - {child} - - ))} -
-
- ); -}; - -export default withIsland(OverloadTabs, { - name: 'OverloadTabs', - on: { interaction: 'pointerover,focusin,touchstart' }, -}); diff --git a/packages/react/src/html/ui/components/OverloadTabs/index.module.css b/packages/react/src/html/ui/components/OverloadTabs/index.module.css deleted file mode 100644 index b23fe518c..000000000 --- a/packages/react/src/html/ui/components/OverloadTabs/index.module.css +++ /dev/null @@ -1,21 +0,0 @@ -.panelContainer { - display: grid; - grid-template-columns: 1fr; - grid-template-rows: 1fr; -} - -.panel { - grid-column: 1; - grid-row: 1; - opacity: 1; - visibility: visible; - pointer-events: auto; - transition: opacity 0.2s ease; - margin-top: calc(var(--spacing, 0.25rem) * 2); -} - -.panel[data-state='inactive'] { - opacity: 0; - visibility: hidden; - pointer-events: none; -} diff --git a/packages/react/src/html/ui/index.css b/packages/react/src/html/ui/index.css index 7d97506de..b22d83d7e 100644 --- a/packages/react/src/html/ui/index.css +++ b/packages/react/src/html/ui/index.css @@ -79,9 +79,26 @@ main { } .overload-panel { - display: flex; - flex-direction: column; - gap: calc(var(--spacing) * 6); + background-color: var(--color-neutral-100); + border: 1px solid var(--color-neutral-200); + border-top-left-radius: 0; + border-top-right-radius: 0; + border-bottom-left-radius: 0.25rem; + border-bottom-right-radius: 0.25rem; + padding: calc(var(--spacing) * 4); + padding-bottom: calc(var(--spacing) * 2); + } + + @media (min-width: 48rem) { + .overload-panel { + padding: calc(var(--spacing) * 6); + padding-bottom: calc(var(--spacing) * 3); + } + } + + :where([data-theme='dark'], [data-theme='dark'] *) .overload-panel { + background-color: var(--color-neutral-950); + border-color: var(--color-neutral-900); } table { diff --git a/packages/react/src/jsx-ast/utils/__tests__/buildContent.test.mjs b/packages/react/src/jsx-ast/utils/__tests__/buildContent.test.mjs index db80facde..312eea9d8 100644 --- a/packages/react/src/jsx-ast/utils/__tests__/buildContent.test.mjs +++ b/packages/react/src/jsx-ast/utils/__tests__/buildContent.test.mjs @@ -244,8 +244,8 @@ describe('groupOverloadsIntoTabs', () => { const result = groupOverloadsIntoTabs(processedChildren, originalEntries); - // 0: funcA, 1: funcB-heading, 2: Overloads-heading, 3: CombinedSignatures, 4: OverloadTabs(funcB), 5: funcC - assert.equal(result.length, 6); + // 0: funcA, 1: funcB-heading, 2: CombinedSignatures, 3: CodeTabs(funcB), 4: funcC + assert.equal(result.length, 5); // First element is untouched assert.equal(result[0].properties.className, 'entry-a'); @@ -253,25 +253,27 @@ describe('groupOverloadsIntoTabs', () => { // Second element is the extracted heading assert.equal(result[1].tagName, 'h3'); - // Third element is the "Overloads" heading - assert.equal(result[2].children[0].value, 'Overloads'); - - // Fourth element is the combined signatures block - const combinedSigBlock = result[3]; + // Third element is the combined signatures block + const combinedSigBlock = result[2]; assert.deepEqual(combinedSigBlock.properties.className, ['signature']); - // Assert that the combined signatures contain the formatted 'Overload #X' text + // Assert that the combined signatures contain the raw signatures without comments const combinedText = getText(combinedSigBlock); - assert.match(combinedText, /Overload #1/); assert.match(combinedText, /function funcB\(arg1\);/); - assert.match(combinedText, /Overload #2/); assert.match(combinedText, /function funcB\(arg1, arg2\);/); - assert.match(combinedText, /Overload #3/); assert.match(combinedText, /function funcB\(arg1, arg2, arg3\);/); - // Fifth element is the OverloadTabs component - const tabsComponent = result[4]; - assert.equal(tabsComponent.name, 'OverloadTabs'); + // Fourth element is the CodeTabs component + const tabsComponent = result[3]; + assert.equal(tabsComponent.name, 'CodeTabs'); + const languagesAttr = tabsComponent.attributes.find( + a => a.name === 'languages' + ); + const displayNamesAttr = tabsComponent.attributes.find( + a => a.name === 'displayNames' + ); + assert.equal(languagesAttr.value, 'overload|overload|overload'); + assert.equal(displayNamesAttr.value, 'Overload #1|Overload #2|Overload #3'); assert.equal(tabsComponent.children.length, 3); // 3 tab panels // Check that the h3 was removed from the overloads and they are wrapped in overload-panel @@ -281,7 +283,7 @@ describe('groupOverloadsIntoTabs', () => { // Second panel child should be the text we inserted assert.equal(panel1.children[0].value, 'body b1'); - assert.equal(result[5].properties.className, 'entry-c'); + assert.equal(result[4].properties.className, 'entry-c'); const panel2 = tabsComponent.children[1]; const classAttr2 = panel2.attributes.find(a => a.name === 'className'); diff --git a/packages/react/src/jsx-ast/utils/buildContent.mjs b/packages/react/src/jsx-ast/utils/buildContent.mjs index 00ff0fcde..aa478c60e 100644 --- a/packages/react/src/jsx-ast/utils/buildContent.mjs +++ b/packages/react/src/jsx-ast/utils/buildContent.mjs @@ -16,7 +16,7 @@ import { slice } from 'mdast-util-slice-markdown'; import { u as createTree } from 'unist-builder'; import { SKIP, visit } from 'unist-util-visit'; -import { createJSXElement } from './ast.mjs'; +import { createJSXElement, createAttributeNode } from './ast.mjs'; import { extractHeadings, extractTextContent } from './buildBarProps.mjs'; import { annotateOverloads } from './overloads.mjs'; import { getRemarkRecma as remark } from './remark.mjs'; @@ -367,10 +367,9 @@ export const groupOverloadsIntoTabs = (processedChildren, originalEntries) => { return; } - // Build the combined signature raw string - const combinedSigRaw = activeOverloadGroup.signatures - .map((sig, idx) => `// Overload #${idx + 1}\n${sig}`) - .join('\n\n'); + // Deduplicate signatures and join with a single newline + const uniqueSignatures = [...new Set(activeOverloadGroup.signatures)]; + const combinedSigRaw = uniqueSignatures.join('\n'); const highlighted = highlighter.highlightToHast( combinedSigRaw, @@ -382,6 +381,23 @@ export const groupOverloadsIntoTabs = (processedChildren, originalEntries) => { // Push combined signatures finalChildren.push(combinedSigNode); + + // Inject properties needed by CodeTabs component + const count = activeOverloadGroup.signatures.length; + + const languagesArr = []; + const displayNamesArr = []; + + for (let i = 0; i < count; i++) { + languagesArr.push('overload'); + displayNamesArr.push(`Overload #${i + 1}`); + } + + activeOverloadGroup.tabsNode.attributes.push( + createAttributeNode('languages', languagesArr.join('|')), + createAttributeNode('displayNames', displayNamesArr.join('|')) + ); + // Push the tabs finalChildren.push(activeOverloadGroup.tabsNode); @@ -411,7 +427,7 @@ export const groupOverloadsIntoTabs = (processedChildren, originalEntries) => { activeOverloadGroup = { firstHeading: last.children.shift(), signatures: [], - tabsNode: createJSXElement(JSX_IMPORTS.OverloadTabs.name, { + tabsNode: createJSXElement(JSX_IMPORTS.CodeTabs.name, { inline: false, children: [], }), @@ -422,11 +438,6 @@ export const groupOverloadsIntoTabs = (processedChildren, originalEntries) => { processOverloadNode(current); finalChildren.push(activeOverloadGroup.firstHeading); - finalChildren.push({ - type: 'heading', - depth: (activeOverloadGroup.firstHeading.depth || 2) + 1, - children: [{ type: 'text', value: 'Overloads' }], - }); } } else { pushOverloadGroup(); From e6521be2e26febd890d90fc0c18211080db9cf58 Mon Sep 17 00:00:00 2001 From: Mohamed Shams El-Deen Date: Wed, 26 Aug 2026 23:06:15 +0300 Subject: [PATCH 5/8] fix(react): remove unnecessary dependency --- packages/react/package.json | 1 - pnpm-lock.yaml | 3 --- 2 files changed, 4 deletions(-) diff --git a/packages/react/package.json b/packages/react/package.json index 974451561..180d18a72 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -32,7 +32,6 @@ "@fontsource-variable/open-sans": "^5.3.0", "@fontsource/ibm-plex-mono": "^5.3.0", "@heroicons/react": "^2.2.0", - "@radix-ui/react-tabs": "^1.1.0", "@doc-kit/core": "workspace:*", "@node-core/rehype-shiki": "^1.4.3", "@node-core/ui-components": "^1.7.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 88bd59bf5..569ca86aa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -244,9 +244,6 @@ importers: '@orama/ui': specifier: ^1.5.4 version: 1.5.4(@orama/core@1.2.19)(@types/react@19.2.18)(react@19.2.8)(supports-color@7.2.0) - '@radix-ui/react-tabs': - specifier: ^1.1.0 - version: 1.1.21(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) estree-util-to-js: specifier: ^2.0.0 version: 2.0.0 From 9a26504f83889b93cf1e3450f9c294b5ff552714 Mon Sep 17 00:00:00 2001 From: Mohamed Shams El-Deen Date: Wed, 26 Aug 2026 23:14:08 +0300 Subject: [PATCH 6/8] fixup! --- packages/react/src/html/ui/index.css | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/react/src/html/ui/index.css b/packages/react/src/html/ui/index.css index b22d83d7e..ffea504b7 100644 --- a/packages/react/src/html/ui/index.css +++ b/packages/react/src/html/ui/index.css @@ -79,6 +79,9 @@ main { } .overload-panel { + display: flex; + flex-direction: column; + gap: calc(var(--spacing) * 6); background-color: var(--color-neutral-100); border: 1px solid var(--color-neutral-200); border-top-left-radius: 0; From 739046d4515b9bdcb58b5ba5031c872138df3b3f Mon Sep 17 00:00:00 2001 From: Mohamed Shams El-Deen Date: Wed, 26 Aug 2026 23:29:58 +0300 Subject: [PATCH 7/8] fixup! --- packages/react/src/jsx-ast/utils/buildContent.mjs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/react/src/jsx-ast/utils/buildContent.mjs b/packages/react/src/jsx-ast/utils/buildContent.mjs index aa478c60e..632e4d7e8 100644 --- a/packages/react/src/jsx-ast/utils/buildContent.mjs +++ b/packages/react/src/jsx-ast/utils/buildContent.mjs @@ -425,7 +425,7 @@ export const groupOverloadsIntoTabs = (processedChildren, originalEntries) => { } else { const last = finalChildren.pop(); activeOverloadGroup = { - firstHeading: last.children.shift(), + firstHeading: last?.children?.shift?.(), signatures: [], tabsNode: createJSXElement(JSX_IMPORTS.CodeTabs.name, { inline: false, @@ -437,7 +437,9 @@ export const groupOverloadsIntoTabs = (processedChildren, originalEntries) => { processOverloadNode(last); processOverloadNode(current); - finalChildren.push(activeOverloadGroup.firstHeading); + if (activeOverloadGroup.firstHeading) { + finalChildren.push(activeOverloadGroup.firstHeading); + } } } else { pushOverloadGroup(); From 88170be30aa9cd1b4d8f4cd549d8e7fe14690368 Mon Sep 17 00:00:00 2001 From: Mohamed Shams El-Deen Date: Thu, 27 Aug 2026 00:21:36 +0300 Subject: [PATCH 8/8] chore: trigger CI