Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 4 additions & 0 deletions packages/react/src/html/constants.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
35 changes: 35 additions & 0 deletions packages/react/src/html/ui/components/OverloadTabs/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/* 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: `${index + 1}`,
}));

return (
<Tabs tabs={tabs} defaultValue="1">
<div className={styles.panelContainer}>
{children.map((child, index) => (
<TabsPrimitive.Content
key={`overload-panel-${index}`}
value={`${index + 1}`}
forceMount={true}
className={styles.panel}
>
{child}
</TabsPrimitive.Content>
))}
</div>
</Tabs>
);
};

export default withIsland(OverloadTabs, {
name: 'OverloadTabs',
on: { interaction: 'pointerover,focusin,touchstart' },
});
Original file line number Diff line number Diff line change
@@ -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;
}
6 changes: 6 additions & 0 deletions packages/react/src/html/ui/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@ main {
}
}

.overload-panel {
display: flex;
flex-direction: column;
gap: calc(var(--spacing) * 6);
}

table {
td {
word-break: break-all;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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');
});
});
58 changes: 57 additions & 1 deletion packages/react/src/jsx-ast/utils/buildContent.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,62 @@ export const processEntry = entry => {
return entry.content;
};

/**
* Groups consecutive overloaded function API entries into a single OverloadTabs component.
* @param {Array<import('estree').Node>} processedChildren - The processed JSX AST nodes for the API entries
* @param {Array<import('@doc-kit/core/generators/metadata/types').MetadataEntry>} originalEntries - The original API metadata entries containing the overload flags
* @returns {Array<import('estree').Node>} 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<import('@doc-kit/core/generators/metadata/types').MetadataEntry>} entries - API documentation metadata entries
Expand All @@ -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),
}),
]);
};
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.