diff --git a/.changeset/odd-snails-rest.md b/.changeset/odd-snails-rest.md
new file mode 100644
index 00000000..bdfa5ebc
--- /dev/null
+++ b/.changeset/odd-snails-rest.md
@@ -0,0 +1,5 @@
+---
+'@doc-kit/generator-react': patch
+---
+
+Redesign the index page
diff --git a/packages/react/src/html/constants.mjs b/packages/react/src/html/constants.mjs
index 3864313f..40756725 100644
--- a/packages/react/src/html/constants.mjs
+++ b/packages/react/src/html/constants.mjs
@@ -65,6 +65,10 @@ export const JSX_IMPORTS = {
name: 'FunctionSignature',
source: '@node-core/ui-components/Containers/FunctionSignature',
},
+ IndexPage: {
+ name: 'IndexPage',
+ source: resolve(ROOT, './ui/components/Layout/IndexPage'),
+ },
ArrowUpRightIcon: {
name: 'ArrowUpRightIcon',
source: '@heroicons/react/24/solid/ArrowUpRightIcon',
diff --git a/packages/react/src/html/ui/components/Layout/IndexPage.jsx b/packages/react/src/html/ui/components/Layout/IndexPage.jsx
new file mode 100644
index 00000000..fde3721f
--- /dev/null
+++ b/packages/react/src/html/ui/components/Layout/IndexPage.jsx
@@ -0,0 +1,25 @@
+import styles from './IndexPage.module.css';
+import StabilityBadge from '../StabilityBadge';
+
+import { project, version } from '#theme/config';
+
+/**
+ * Body of the synthetic `index.html` page
+ */
+export default ({ modules }) => (
+ <>
+
+ The API reference documentation for {project} v{version.version}.
+
+
+
+ {modules.map(({ name, href, stability }) => (
+ -
+ {name}
+
+
+
+ ))}
+
+ >
+);
diff --git a/packages/react/src/html/ui/components/Layout/IndexPage.module.css b/packages/react/src/html/ui/components/Layout/IndexPage.module.css
new file mode 100644
index 00000000..f3720abf
--- /dev/null
+++ b/packages/react/src/html/ui/components/Layout/IndexPage.module.css
@@ -0,0 +1,22 @@
+.moduleIndex {
+ list-style: none;
+ padding: 0;
+
+ li {
+ padding: 0.5rem 0;
+ }
+
+ /* Draw a rule between items */
+ li + li {
+ border-top: 1px solid var(--color-brand-200);
+ }
+}
+
+:where([data-theme='dark'], [data-theme='dark'] *) .moduleIndex li + li {
+ border-color: var(--color-brand-800);
+}
+
+.badge {
+ display: inline-block;
+ margin-left: 0.25rem;
+}
diff --git a/packages/react/src/html/ui/components/MetaBar/index.jsx b/packages/react/src/html/ui/components/MetaBar/index.jsx
index 5f705efd..35c6202e 100644
--- a/packages/react/src/html/ui/components/MetaBar/index.jsx
+++ b/packages/react/src/html/ui/components/MetaBar/index.jsx
@@ -1,9 +1,9 @@
import { CodeBracketIcon, DocumentIcon } from '@heroicons/react/24/outline';
-import Badge from '@node-core/ui-components/Common/Badge';
import MetaBar from '@node-core/ui-components/Containers/MetaBar';
import GitHubIcon from '@node-core/ui-components/Icons/Social/GitHub';
import styles from './index.module.css';
+import StabilityBadge from '../StabilityBadge';
import { editURL } from '#theme/config';
@@ -12,40 +12,17 @@ const iconMap = {
MD: DocumentIcon,
};
-const STABILITY_KINDS = ['error', 'warning', null, 'info'];
-const STABILITY_LABELS = ['D', 'E', null, 'L'];
-const STABILITY_TOOLTIPS = ['Deprecated', 'Experimental', null, 'Legacy'];
-
/**
* Renders a heading value with an optional stability badge
* @param {{ value: string, stability: number }} props
*/
-const HeadingValue = ({ value, stability }) => {
- if (stability === 2) {
- return value;
- }
-
- const ariaLabel = STABILITY_TOOLTIPS[stability]
- ? `Stability: ${STABILITY_TOOLTIPS[stability]}`
- : undefined;
-
- return (
- <>
- {value}
+const HeadingValue = ({ value, stability }) => (
+ <>
+ {value}
-
- {STABILITY_LABELS[stability]}
-
- >
- );
-};
+
+ >
+);
/**
* MetaBar component that displays table of contents and page metadata
diff --git a/packages/react/src/html/ui/components/StabilityBadge.jsx b/packages/react/src/html/ui/components/StabilityBadge.jsx
new file mode 100644
index 00000000..910ecb12
--- /dev/null
+++ b/packages/react/src/html/ui/components/StabilityBadge.jsx
@@ -0,0 +1,29 @@
+import Badge from '@node-core/ui-components/Common/Badge';
+
+const STABILITY_KINDS = ['error', 'warning', null, 'info'];
+const STABILITY_LABELS = ['D', 'E', null, 'L'];
+const STABILITY_TOOLTIPS = ['Deprecated', 'Experimental', null, 'Legacy'];
+
+/**
+ * Compact stability badge used next to API names
+ *
+ * @param {{ stability: number, className?: string }} props
+ */
+export default ({ stability, className }) => {
+ if (!STABILITY_LABELS[stability]) {
+ return null;
+ }
+
+ return (
+
+ {STABILITY_LABELS[stability]}
+
+ );
+};
diff --git a/packages/react/src/jsx-ast/utils/synthetic/__tests__/index.test.mjs b/packages/react/src/jsx-ast/utils/synthetic/__tests__/index.test.mjs
deleted file mode 100644
index f083e905..00000000
--- a/packages/react/src/jsx-ast/utils/synthetic/__tests__/index.test.mjs
+++ /dev/null
@@ -1,114 +0,0 @@
-import assert from 'node:assert/strict';
-import { describe, it } from 'node:test';
-
-import { buildIndexPage, buildStabilityOverview } from '../index.mjs';
-
-const fakeHead = (api, name, stabilityIndex, depth = 1) => ({
- api,
- heading: { depth, data: { name, text: name, slug: api } },
- stability:
- stabilityIndex == null
- ? null
- : {
- data: {
- index: String(stabilityIndex),
- description: `${name} stable. Long-form description.`,
- },
- },
-});
-
-const findChild = (node, tagName) =>
- node.children.find(child => child.tagName === tagName);
-
-describe('buildIndexPage', () => {
- it('returns a synthetic `index` head with an "Index" heading', () => {
- const { head } = buildIndexPage([]);
-
- assert.equal(head.api, 'index');
- assert.equal(head.path, '/index');
- assert.equal(head.basename, 'index');
- assert.equal(head.heading.data.name, 'Index');
- assert.equal(head.synthetic, true);
- });
-
- it('sorts the stability overview rows alphabetically by API name', () => {
- const { entries } = buildIndexPage([
- fakeHead('fs', 'fs', 2),
- fakeHead('assert', 'assert', 2),
- fakeHead('crypto', 'crypto', 2),
- ]);
-
- const table = findChild(entries[0].content, 'table');
- const rows = findChild(table, 'tbody').children;
- const names = rows.map(
- row => row.children[0].children[0].children[0].value
- );
-
- assert.deepEqual(names, ['assert', 'crypto', 'fs']);
- });
-});
-
-describe('buildStabilityOverview', () => {
- it('renders a header row and one body row per entry', () => {
- const table = buildStabilityOverview([
- fakeHead('fs', 'fs', 2),
- fakeHead('crypto', 'crypto', 1),
- ]);
-
- assert.equal(table.tagName, 'table');
- const headerRow = findChild(findChild(table, 'thead'), 'tr');
- assert.deepEqual(
- headerRow.children.map(c => c.children[0].value),
- ['API', 'Stability']
- );
-
- assert.equal(findChild(table, 'tbody').children.length, 2);
- });
-
- it('formats the stability cell with a colored badge and first sentence', () => {
- const table = buildStabilityOverview([fakeHead('fs', 'fs', 1)]);
-
- const row = findChild(table, 'tbody').children[0];
- const stabilityCell = row.children[1];
- const badge = stabilityCell.children[0];
-
- assert.equal(badge.name, 'Badge');
- assert.deepEqual(
- badge.attributes.map(({ name, value }) => [name, value]),
- [
- ['size', 'small'],
- ['kind', 'warning'],
- ['aria-label', 'Stability: 1'],
- ]
- );
- assert.equal(badge.children[0].value, '1');
- assert.equal(stabilityCell.children[1].value, ' fs stable');
- });
-
- it('uses a default badge for stable entries', () => {
- const table = buildStabilityOverview([fakeHead('fs', 'fs', 2)]);
-
- const row = findChild(table, 'tbody').children[0];
- const badge = row.children[1].children[0];
- const kind = badge.attributes.find(attr => attr.name === 'kind');
-
- assert.equal(kind.value, 'default');
- });
-
- it('builds a relative link to the module HTML page', () => {
- const table = buildStabilityOverview([fakeHead('fs', 'fs', 2)]);
-
- const row = findChild(table, 'tbody').children[0];
- const link = row.children[0].children[0];
-
- assert.equal(link.tagName, 'a');
- assert.equal(link.properties.href, 'fs.html');
- assert.equal(link.children[0].value, 'fs');
- });
-
- it('renders an empty body when no entries are passed', () => {
- const table = buildStabilityOverview([]);
-
- assert.equal(findChild(table, 'tbody').children.length, 0);
- });
-});
diff --git a/packages/react/src/jsx-ast/utils/synthetic/index.mjs b/packages/react/src/jsx-ast/utils/synthetic/index.mjs
index a3278202..e0cc94f0 100644
--- a/packages/react/src/jsx-ast/utils/synthetic/index.mjs
+++ b/packages/react/src/jsx-ast/utils/synthetic/index.mjs
@@ -1,65 +1,25 @@
'use strict';
-import { h as createElement } from 'hastscript';
+import getConfig from '@doc-kit/core/utils/configuration/index.mjs';
import { createSyntheticHead, wrapAsEntry } from './synthetic.mjs';
import { JSX_IMPORTS } from '../../../html/constants.mjs';
import { createJSXElement } from '../ast.mjs';
import { getSortedHeadNodes } from '../getSortedHeadNodes.mjs';
-const STABILITY_BADGE_KINDS = [
- 'error',
- 'warning',
- 'default',
- 'info',
- 'neutral',
- 'neutral',
-];
-
-/**
- * Maps a Node.js stability index to a UI badge kind.
- *
- * @param {string} index
- */
-const getStabilityBadgeKind = index =>
- STABILITY_BADGE_KINDS[parseInt(index, 10)] ?? 'neutral';
-
/**
- * Builds the Stability Overview table from module heads that declare a
- * top-level stability index, mirroring the `legacy-html-all` overview.
+ * Maps the sorted module heads to the plain props consumed by the `IndexPage`
+ * component: display name, page href, and numeric stability index (defaulting
+ * to stable, mirroring the ToC).
*
* @param {Array} headEntries
*/
-export const buildStabilityOverview = headEntries =>
- createElement('table', [
- createElement('thead', [
- createElement('tr', [
- createElement('th', 'API'),
- createElement('th', 'Stability'),
- ]),
- ]),
- createElement(
- 'tbody',
- headEntries.map(({ heading, api, stability }) =>
- createElement('tr', [
- createElement(
- 'td',
- createElement('a', { href: `${api}.html` }, heading.data.name)
- ),
- createElement(
- 'td',
- createJSXElement(JSX_IMPORTS.Badge.name, {
- size: 'small',
- kind: getStabilityBadgeKind(stability.data.index),
- 'aria-label': `Stability: ${stability.data.index}`,
- children: stability.data.index,
- }),
- ` ${stability.data.description.split('. ')[0]}`
- ),
- ])
- )
- ),
- ]);
+export const buildModuleProps = headEntries =>
+ headEntries.map(({ heading, api, stability }) => ({
+ name: heading.data.name,
+ href: `${api}.html`,
+ stability: parseInt(stability?.data.index ?? '2', 10),
+ }));
/**
* Builds the page descriptor for `index.html`
@@ -67,14 +27,20 @@ export const buildStabilityOverview = headEntries =>
* @param {Array} entries
*/
export const buildIndexPage = entries => {
- const head = createSyntheticHead('index', 'Index');
- const moduleEntries = getSortedHeadNodes(entries);
+ const config = getConfig('jsx-ast');
+ const head = createSyntheticHead(
+ 'index',
+ `${config.project} Documentation Index`
+ );
return {
head,
entries: [
wrapAsEntry(head, [
- buildStabilityOverview(moduleEntries.filter(entry => entry.stability)),
+ createJSXElement(JSX_IMPORTS.IndexPage.name, {
+ inline: false,
+ modules: buildModuleProps(getSortedHeadNodes(entries)),
+ }),
]),
],
};