From 48a8e0c4d8a70ffd0e5a6d5b90a048ee92567174 Mon Sep 17 00:00:00 2001 From: Mohamed Shams El-Deen Date: Thu, 20 Aug 2026 00:00:17 +0300 Subject: [PATCH 1/2] 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 1e7d63ff8..2ac85a1fa 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.4", diff --git a/packages/react/src/html/constants.mjs b/packages/react/src/html/constants.mjs index 4ca3e783a..e3fe3561e 100644 --- a/packages/react/src/html/constants.mjs +++ b/packages/react/src/html/constants.mjs @@ -26,6 +26,10 @@ export const JSX_IMPORTS = { name: 'CodeTabs', source: resolve(ROOT, './ui/components/CodeTabs'), }, + 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 7b9a7dda4..ac1cd1451 100644 --- a/packages/react/src/jsx-ast/utils/buildContent.mjs +++ b/packages/react/src/jsx-ast/utils/buildContent.mjs @@ -310,6 +310,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 @@ -325,7 +381,7 @@ export const createDocumentLayout = (entries, metadata) => { metadata, headings: extractHeadings(entries), readingTime: readingTime(extractTextContent(entries)).text, - children: entries.map(processEntry), + children: groupOverloadsIntoTabs(entries.map(processEntry), entries), }), ]); }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3c6191fc3..8b2e1d525 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 537808f63beb3ef8003642e2ced404468cd4e379 Mon Sep 17 00:00:00 2001 From: Mohamed Shams El-Deen Date: Thu, 20 Aug 2026 00:34:43 +0300 Subject: [PATCH 2/2] 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 225f8b857..2a5ac9da5 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', @@ -142,3 +146,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'); + }); +});