feat(site): extract api reference from components (#464)

This commit is contained in:
Darius Cepulis
2026-02-05 19:57:54 -06:00
committed by GitHub
parent 48364f17fd
commit 0991a899b2
80 changed files with 3425 additions and 1562 deletions
@@ -0,0 +1,43 @@
---
/**
* Renders the data attributes table for API reference.
*/
import MarkdownCode from '@/components/typography/MarkdownCode.astro';
import Table from '@/components/typography/Table.astro';
import Tbody from '@/components/typography/Tbody.astro';
import Td from '@/components/typography/Td.astro';
import Th from '@/components/typography/Th.astro';
import Thead from '@/components/typography/Thead.astro';
import Tr from '@/components/typography/Tr.astro';
import type { DataAttrDef } from '@/types/api-reference';
import InlineMarkdown from './InlineMarkdown.astro';
interface Props {
dataAttributes: Record<string, DataAttrDef>;
}
const { dataAttributes } = Astro.props;
const attrs = Object.entries(dataAttributes);
---
<Table maxWidth={false} outerClass="my-6">
<Thead>
<Tr>
<Th>Attribute</Th>
<Th>Description</Th>
</Tr>
</Thead>
<Tbody>
{
attrs.map(([name, def]) => (
<Tr>
<Td class="align-top">
<MarkdownCode>{name}</MarkdownCode>
</Td>
<Td><InlineMarkdown content={def.description} /></Td>
</Tr>
))
}
</Tbody>
</Table>
@@ -0,0 +1,47 @@
---
/**
* Renders the props table for API reference.
*
* Props are sorted: required first, then alphabetical.
*/
import Table from '@/components/typography/Table.astro';
import Tbody from '@/components/typography/Tbody.astro';
import Th from '@/components/typography/Th.astro';
import Thead from '@/components/typography/Thead.astro';
import Tr from '@/components/typography/Tr.astro';
import type { PropDef } from '@/types/api-reference';
import PropRow from './PropRow.astro';
interface Props {
props: Record<string, PropDef>;
componentName: string;
}
const { props, componentName } = Astro.props;
---
<Table maxWidth={false} outerClass="my-6">
<Thead>
<Tr>
<Th>Prop</Th>
<Th>Type</Th>
<Th>Default</Th>
<Th />
</Tr>
</Thead>
<Tbody>
{
Object.entries(props).map(([name, prop]) => (
<PropRow
name={name}
type={prop.type}
shortType={prop.shortType}
description={prop.description}
defaultValue={prop.default}
required={prop.required}
componentName={componentName}
/>
))
}
</Tbody>
</Table>
@@ -0,0 +1,63 @@
---
import { getEntry } from 'astro:content';
import { kebabCase } from 'es-toolkit/string';
import ContentWidth from '@/components/frames/ContentWidth.astro';
import MarkdownCode from '@/components/typography/MarkdownCode.astro';
import P from '@/components/typography/P.astro';
import type { ComponentApiReference } from '@/types/api-reference';
import { isValidFramework } from '@/types/docs';
import FrameworkCase from '../FrameworkCase.astro';
import ApiDataAttrsTable from './ApiDataAttrsTable.astro';
import ApiPropsTable from './ApiPropsTable.astro';
import ApiStateTable from './ApiStateTable.astro';
interface Props {
component: string;
section: 'props' | 'state' | 'dataAttributes';
}
const { component, section } = Astro.props;
const { framework } = Astro.params;
if (!framework || !isValidFramework(framework)) {
throw new Error(`Invalid or missing framework param "${framework ?? 'undefined'}".`);
}
const entry = await getEntry('apiReference', kebabCase(component));
const apiRef: ComponentApiReference | null = entry?.data ?? null;
const hasData = apiRef && Object.keys(apiRef[section]).length > 0;
---
<ContentWidth>
{
hasData && section === "props" && (
<ApiPropsTable props={apiRef.props} componentName={component} />
)
}
{
hasData && section === "state" && (
<>
<P>
<FrameworkCase frameworks={["react"]}>
State is accessible via the{" "}
<MarkdownCode>render</MarkdownCode>,{" "}
<MarkdownCode>className</MarkdownCode>, and{" "}
<MarkdownCode>style</MarkdownCode> props.
</FrameworkCase>
<FrameworkCase frameworks={["html"]}>
State is reflected as data attributes for CSS styling.
</FrameworkCase>
</P>
<ApiStateTable state={apiRef.state} />
</>
)
}
{
hasData && section === "dataAttributes" && (
<ApiDataAttrsTable dataAttributes={apiRef.dataAttributes} />
)
}
</ContentWidth>
@@ -0,0 +1,47 @@
---
/**
* Renders the state interface table for API reference.
*/
import MarkdownCode from '@/components/typography/MarkdownCode.astro';
import Table from '@/components/typography/Table.astro';
import Tbody from '@/components/typography/Tbody.astro';
import Td from '@/components/typography/Td.astro';
import Th from '@/components/typography/Th.astro';
import Thead from '@/components/typography/Thead.astro';
import Tr from '@/components/typography/Tr.astro';
import type { StateDef } from '@/types/api-reference';
import InlineMarkdown from './InlineMarkdown.astro';
interface Props {
state: Record<string, StateDef>;
}
const { state } = Astro.props;
const stateEntries = Object.entries(state);
---
<Table maxWidth={false} outerClass="my-6">
<Thead>
<Tr>
<Th>Property</Th>
<Th>Type</Th>
<Th>Description</Th>
</Tr>
</Thead>
<Tbody>
{
stateEntries.map(([name, def]) => (
<Tr>
<Td class="align-top">
<MarkdownCode>{name}</MarkdownCode>
</Td>
<Td class="align-top">
<MarkdownCode>{def.type}</MarkdownCode>
</Td>
<Td><InlineMarkdown content={def.description} /></Td>
</Tr>
))
}
</Tbody>
</Table>
@@ -0,0 +1,14 @@
---
import { renderInlineMarkdown } from '@/utils/docs/renderInlineMarkdown';
interface Props {
content?: string;
fallback?: string;
}
const { content, fallback = '-' } = Astro.props;
const html = content ? renderInlineMarkdown(content) : fallback;
---
<Fragment set:html={html} />
@@ -0,0 +1,140 @@
---
/**
* Single row in the props table with expandable details.
*
* Uses button disclosure pattern: the summary row has real `<td>` cells,
* the detail row is a separate `<tr>` toggled via `aria-controls` + `hidden`.
* The entire summary row is clickable, delegating to the toggle button.
*/
import clsx from 'clsx';
import MarkdownCode from '@/components/typography/MarkdownCode.astro';
import Td from '@/components/typography/Td.astro';
import Tr from '@/components/typography/Tr.astro';
import InlineMarkdown from './InlineMarkdown.astro';
interface Props {
name: string;
type: string;
shortType?: string;
description?: string;
defaultValue?: string;
required?: boolean;
componentName: string;
}
const { name, type, shortType, description, defaultValue, required, componentName } = Astro.props;
const displayType = shortType ?? type;
const hasDetail = Boolean(shortType || description);
const id = `${componentName}-${name}`;
---
<Tr
id={id}
data-prop-row
class={clsx(
hasDetail &&
"group cursor-pointer intent:bg-light-60 dark:intent:bg-dark-110",
)}
>
<Td class="align-top">
<MarkdownCode>
{name}
{required && <span class="text-orange">*</span>}
</MarkdownCode>
</Td>
<Td class="align-top">
<MarkdownCode>{displayType}</MarkdownCode>
</Td>
<Td class="align-top">
<MarkdownCode>
{defaultValue ?? "—"}
</MarkdownCode>
</Td>
{
hasDetail && (
<Td class="align-top">
<button
aria-expanded="false"
aria-controls={`${id}-detail`}
aria-label={`Details for ${name}`}
data-prop-toggle
>
<span
aria-hidden="true"
class="inline-block group-data-expanded:rotate-90"
>
</span>
</button>
</Td>
)
}
</Tr>
{
hasDetail && (
<Tr id={`${id}-detail`} hidden>
<Td colspan="4" class="p-0">
<div class="px-6 py-4">
<dl class="grid grid-cols-[auto_1fr] gap-x-6 gap-y-3 text-sm">
{description && (
<>
<dt class="text-dark-40 dark:text-light-40">
Description
</dt>
<dd><InlineMarkdown content={description} /></dd>
</>
)}
{(type || shortType) && (
<>
<dt class="text-dark-40 dark:text-light-40">
Type
</dt>
<dd>
<MarkdownCode>
{type || shortType}
</MarkdownCode>
</dd>
</>
)}
</dl>
</div>
</Td>
</Tr>
)
}
{/* Astro deduplicates script tags, so this runs once per page. */}
<script>
document
.querySelectorAll<HTMLButtonElement>("[data-prop-toggle]")
.forEach((button) => {
button.addEventListener("click", () => {
const expanded =
button.getAttribute("aria-expanded") === "true";
button.setAttribute("aria-expanded", String(!expanded));
const row =
button.closest<HTMLTableRowElement>("[data-prop-row]");
if (row) row.toggleAttribute("data-expanded", !expanded);
const detail = document.getElementById(
button.getAttribute("aria-controls")!,
);
if (detail) detail.hidden = expanded;
});
});
document
.querySelectorAll<HTMLTableRowElement>("[data-prop-row]")
.forEach((row) => {
row.addEventListener("click", (e) => {
if ((e.target as Element).closest("a, button")) return;
row.querySelector<HTMLButtonElement>(
"[data-prop-toggle]",
)?.click();
});
});
</script>
+4 -3
View File
@@ -1,8 +1,9 @@
---
import type { HTMLTag, Polymorphic } from 'astro/types';
import { twMerge } from 'tailwind-merge';
import { shared } from './styles';
type Props<Tag extends HTMLTag = 'a'> = Polymorphic<{ as: Tag }> & {
class?: string;
};
@@ -10,6 +11,6 @@ type Props<Tag extends HTMLTag = 'a'> = Polymorphic<{ as: Tag }> & {
const { as: Tag = 'a', class: className, ...props } = Astro.props;
---
<Tag class={twMerge('underline intent:no-underline', className)} {...props}>
<slot />
<Tag class={twMerge(shared.a, className)} {...props}>
<slot />
</Tag>
+4 -3
View File
@@ -1,8 +1,9 @@
---
import type { HTMLTag, Polymorphic } from 'astro/types';
import { twMerge } from 'tailwind-merge';
import { shared } from './styles';
type Props<Tag extends HTMLTag = 'em'> = Polymorphic<{ as: Tag }> & {
class?: string;
};
@@ -10,6 +11,6 @@ type Props<Tag extends HTMLTag = 'em'> = Polymorphic<{ as: Tag }> & {
const { as: Tag = 'em', class: className, ...props } = Astro.props;
---
<Tag class={twMerge('font-medium', className)} {...props}>
<slot />
<Tag class={twMerge(shared.em, className)} {...props}>
<slot />
</Tag>
+4 -3
View File
@@ -1,8 +1,9 @@
---
import type { HTMLTag, Polymorphic } from 'astro/types';
import { twMerge } from 'tailwind-merge';
import { shared } from './styles';
type Props<Tag extends HTMLTag = 'li'> = Polymorphic<{ as: Tag }> & {
class?: string;
};
@@ -10,6 +11,6 @@ type Props<Tag extends HTMLTag = 'li'> = Polymorphic<{ as: Tag }> & {
const { as: Tag = 'li', class: className, ...props } = Astro.props;
---
<Tag class={twMerge('text-base', className)} {...props}>
<slot />
<Tag class={twMerge(shared.li, className)} {...props}>
<slot />
</Tag>
@@ -1,8 +1,9 @@
---
import type { HTMLTag, Polymorphic } from 'astro/types';
import { twMerge } from 'tailwind-merge';
import { shared } from './styles';
type Props<Tag extends HTMLTag = 'code'> = Polymorphic<{ as: Tag }> & {
class?: string;
codeBlock?: string;
@@ -16,19 +17,13 @@ const isCodeBlock = codeBlock === 'true';
---
{
isCodeBlock ? (
<Tag class={twMerge('font-mono text-code', className)} {...props}>
<slot />
</Tag>
) : (
<Tag
class={twMerge(
'bg-light-100 dark:bg-dark-110 dark:text-light-100 border border-light-40 dark:border-dark-80 px-1 rounded font-mono text-code',
className,
)}
{...props}
>
<slot />
</Tag>
)
isCodeBlock ? (
<Tag class={twMerge(shared.codeBlock, className)} {...props}>
<slot />
</Tag>
) : (
<Tag class={twMerge(shared.code, className)} {...props}>
<slot />
</Tag>
)
}
+9 -4
View File
@@ -1,9 +1,10 @@
---
import type { HTMLTag, Polymorphic } from 'astro/types';
import { clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
import { shared } from './styles';
type Props<Tag extends HTMLTag = 'ol'> = Polymorphic<{ as: Tag }> & {
maxWidth?: boolean;
class?: string;
@@ -13,8 +14,12 @@ const { as: Tag = 'ol', maxWidth = true, class: className, ...props } = Astro.pr
---
<Tag
class={twMerge(clsx('list-decimal list-outside pl-6 mb-6 space-y-1', maxWidth && 'max-w-3xl mx-auto'), className)}
{...props}
class={twMerge(
shared.ol,
clsx("mb-6", maxWidth && "max-w-3xl mx-auto"),
className,
)}
{...props}
>
<slot />
<slot />
</Tag>
+4 -3
View File
@@ -1,8 +1,9 @@
---
import type { HTMLTag, Polymorphic } from 'astro/types';
import { twMerge } from 'tailwind-merge';
import { shared } from './styles';
type Props<Tag extends HTMLTag = 'strong'> = Polymorphic<{ as: Tag }> & {
class?: string;
};
@@ -10,6 +11,6 @@ type Props<Tag extends HTMLTag = 'strong'> = Polymorphic<{ as: Tag }> & {
const { as: Tag = 'strong', class: className, ...props } = Astro.props;
---
<Tag class={twMerge('font-semibold', className)} {...props}>
<slot />
<Tag class={twMerge(shared.strong, className)} {...props}>
<slot />
</Tag>
+3 -2
View File
@@ -5,13 +5,14 @@ import { twMerge } from 'tailwind-merge';
type Props<Tag extends HTMLTag = 'table'> = Polymorphic<{ as: Tag }> & {
maxWidth?: boolean;
outerClass?: string;
class?: string;
};
const { as: Tag = 'table', maxWidth = true, class: className, ...props } = Astro.props;
const { as: Tag = 'table', maxWidth = true, outerClass, class: className, ...props } = Astro.props;
---
<div class={clsx("my-12", maxWidth && "mx-auto max-w-3xl")}>
<div class={twMerge(clsx("my-12", maxWidth && "mx-auto max-w-3xl"), outerClass)}>
<div class="overflow-x-auto flex -mx-6">
<div
class="sticky left-0 z-10 shrink-0 basis-6 bg-linear-to-r from-light-80 dark:from-dark-100 to-transparent pointer-events-none"
+1 -1
View File
@@ -10,6 +10,6 @@ type Props<Tag extends HTMLTag = 'thead'> = Polymorphic<{ as: Tag }> & {
const { as: Tag = 'thead', class: className, ...props } = Astro.props;
---
<Tag class={twMerge('border-b-2 border-light-40', className)} {...props}>
<Tag class={twMerge('border-b-2 border-light-40 dark:border-dark-80', className)} {...props}>
<slot />
</Tag>
+1 -1
View File
@@ -10,6 +10,6 @@ type Props<Tag extends HTMLTag = 'tr'> = Polymorphic<{ as: Tag }> & {
const { as: Tag = 'tr', class: className, ...props } = Astro.props;
---
<Tag class={twMerge('border-b border-light-40', className)} {...props}>
<Tag class={twMerge('border-b border-light-40 dark:border-dark-80', className)} {...props}>
<slot />
</Tag>
+9 -4
View File
@@ -1,9 +1,10 @@
---
import type { HTMLTag, Polymorphic } from 'astro/types';
import { clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
import { shared } from './styles';
type Props<Tag extends HTMLTag = 'ul'> = Polymorphic<{ as: Tag }> & {
maxWidth?: boolean;
class?: string;
@@ -13,8 +14,12 @@ const { as: Tag = 'ul', maxWidth = true, class: className, ...props } = Astro.pr
---
<Tag
class={twMerge(clsx('list-disc list-outside pl-6 mb-6 space-y-1', maxWidth && 'max-w-3xl mx-auto'), className)}
{...props}
class={twMerge(
shared.ul,
clsx("mb-6", maxWidth && "max-w-3xl mx-auto"),
className,
)}
{...props}
>
<slot />
<slot />
</Tag>
+17
View File
@@ -0,0 +1,17 @@
/**
* Shared Tailwind class strings for typography elements.
*
* Used by both the Astro typography components and `renderInlineMarkdown`
* so styling stays in sync across server-rendered MDX and programmatic
* HTML generation.
*/
export const shared = {
a: 'underline intent:no-underline',
code: 'bg-light-100 dark:bg-dark-110 dark:text-light-100 border border-light-40 dark:border-dark-80 px-1 rounded font-mono text-code',
codeBlock: 'font-mono text-code',
em: 'font-medium',
li: 'text-base',
ol: 'list-decimal list-outside pl-6 space-y-1',
strong: 'font-semibold',
ul: 'list-disc list-outside pl-6 space-y-1',
} as const;