mirror of
https://github.com/zoriya/v10.git
synced 2026-08-14 18:04:49 +00:00
feat(site): generated multipart component api reference (#468)
This commit is contained in:
@@ -1,63 +0,0 @@
|
||||
---
|
||||
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,133 @@
|
||||
---
|
||||
import { getEntry } from 'astro:content';
|
||||
import { kebabCase } from 'es-toolkit/string';
|
||||
import ContentWidth from '@/components/frames/ContentWidth.astro';
|
||||
import H2 from '@/components/typography/H2Markdown.astro';
|
||||
import H3 from '@/components/typography/H3Markdown.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';
|
||||
import InlineMarkdown from './InlineMarkdown.astro';
|
||||
|
||||
interface Props {
|
||||
component: string;
|
||||
}
|
||||
|
||||
const { component } = Astro.props;
|
||||
const { framework } = Astro.params;
|
||||
if (!framework || !isValidFramework(framework)) {
|
||||
throw new Error(`Invalid or missing framework param.`);
|
||||
}
|
||||
|
||||
const entry = await getEntry('apiReference', kebabCase(component));
|
||||
const apiRef: ComponentApiReference | null = entry?.data ?? null;
|
||||
if (!apiRef) return;
|
||||
|
||||
const hasParts = apiRef.parts && Object.keys(apiRef.parts).length > 0;
|
||||
|
||||
const hasProps = Object.keys(apiRef.props).length > 0;
|
||||
const hasState = Object.keys(apiRef.state).length > 0;
|
||||
const hasDataAttrs = Object.keys(apiRef.dataAttributes).length > 0;
|
||||
---
|
||||
|
||||
{hasParts ? (
|
||||
<ContentWidth>
|
||||
{Object.entries(apiRef.parts!).map(([partKebab, part]) => {
|
||||
const tagName = part.platforms?.html?.tagName;
|
||||
const partHasProps = Object.keys(part.props).length > 0;
|
||||
const partHasState = Object.keys(part.state).length > 0;
|
||||
const partHasDataAttrs = Object.keys(part.dataAttributes).length > 0;
|
||||
const componentName = `${component}.${part.name}`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<H2 id={partKebab}>
|
||||
<FrameworkCase frameworks={["react"]}>
|
||||
<MarkdownCode>{`<${component}.${part.name} />`}</MarkdownCode> reference
|
||||
</FrameworkCase>
|
||||
<FrameworkCase frameworks={["html"]}>
|
||||
<MarkdownCode>{tagName ? `<${tagName}>` : part.name}</MarkdownCode> reference
|
||||
</FrameworkCase>
|
||||
</H2>
|
||||
|
||||
{part.description && (
|
||||
<P><InlineMarkdown content={part.description} /></P>
|
||||
)}
|
||||
|
||||
{partHasProps && (
|
||||
<>
|
||||
<H3 id={`${partKebab}-props`}>Props</H3>
|
||||
<ApiPropsTable props={part.props} componentName={componentName} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{partHasState && (
|
||||
<>
|
||||
<H3 id={`${partKebab}-state`}>State</H3>
|
||||
<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={part.state} componentName={componentName} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{partHasDataAttrs && (
|
||||
<>
|
||||
<H3 id={`${partKebab}-data-attributes`}>Data attributes</H3>
|
||||
<ApiDataAttrsTable dataAttributes={part.dataAttributes} />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</ContentWidth>
|
||||
) : (
|
||||
<ContentWidth>
|
||||
<H2 id="api-reference">API reference</H2>
|
||||
|
||||
{hasProps && (
|
||||
<>
|
||||
<H3 id="props">Props</H3>
|
||||
<ApiPropsTable props={apiRef.props} componentName={component} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{hasState && (
|
||||
<>
|
||||
<H3 id="state">State</H3>
|
||||
<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} componentName={component} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{hasDataAttrs && (
|
||||
<>
|
||||
<H3 id="data-attributes">Data attributes</H3>
|
||||
<ApiDataAttrsTable dataAttributes={apiRef.dataAttributes} />
|
||||
</>
|
||||
)}
|
||||
</ContentWidth>
|
||||
)}
|
||||
@@ -2,21 +2,20 @@
|
||||
/**
|
||||
* 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';
|
||||
import StateRow from './StateRow.astro';
|
||||
|
||||
interface Props {
|
||||
state: Record<string, StateDef>;
|
||||
componentName: string;
|
||||
}
|
||||
|
||||
const { state } = Astro.props;
|
||||
const { state, componentName } = Astro.props;
|
||||
|
||||
const stateEntries = Object.entries(state);
|
||||
---
|
||||
@@ -26,21 +25,19 @@ const stateEntries = Object.entries(state);
|
||||
<Tr>
|
||||
<Th>Property</Th>
|
||||
<Th>Type</Th>
|
||||
<Th>Description</Th>
|
||||
<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>
|
||||
<StateRow
|
||||
name={name}
|
||||
type={def.type}
|
||||
shortType={def.shortType}
|
||||
description={def.description}
|
||||
componentName={componentName}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</Tbody>
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
---
|
||||
/**
|
||||
* Expandable table row used by PropRow and StateRow.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* The default slot provides the summary cells. This component appends the
|
||||
* toggle-button cell and renders the expandable detail panel beneath.
|
||||
*/
|
||||
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 {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
shortType?: string;
|
||||
description?: string;
|
||||
colspan: number;
|
||||
}
|
||||
|
||||
const { id, name, type, shortType, description, colspan } = Astro.props;
|
||||
|
||||
const hasDetail = Boolean(shortType || description);
|
||||
---
|
||||
|
||||
<Tr
|
||||
id={id}
|
||||
data-detail-row
|
||||
class={clsx(
|
||||
hasDetail &&
|
||||
"group cursor-pointer intent:bg-light-60 dark:intent:bg-dark-110 data-expanded:border-transparent dark:data-expanded:border-transparent",
|
||||
)}
|
||||
>
|
||||
<slot />
|
||||
{
|
||||
hasDetail && (
|
||||
<Td class="align-top">
|
||||
<button
|
||||
aria-expanded="false"
|
||||
aria-controls={`${id}-detail`}
|
||||
aria-label={`Details for ${name}`}
|
||||
data-detail-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={String(colspan)} class="p-0">
|
||||
<div class="px-6 py-4">
|
||||
<dl class="grid grid-cols-[auto_1fr] gap-x-6 gap-y-3">
|
||||
{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>
|
||||
)
|
||||
}
|
||||
|
||||
<script>
|
||||
document
|
||||
.querySelectorAll<HTMLButtonElement>("[data-detail-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-detail-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-detail-row]")
|
||||
.forEach((row) => {
|
||||
row.addEventListener("click", (e) => {
|
||||
if ((e.target as Element).closest("a, button")) return;
|
||||
row.querySelector<HTMLButtonElement>(
|
||||
"[data-detail-toggle]",
|
||||
)?.click();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -1,16 +1,7 @@
|
||||
---
|
||||
/**
|
||||
* 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';
|
||||
import DetailRow from './DetailRow.astro';
|
||||
|
||||
interface Props {
|
||||
name: string;
|
||||
@@ -25,18 +16,10 @@ interface Props {
|
||||
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",
|
||||
)}
|
||||
>
|
||||
<DetailRow id={id} name={name} type={type} shortType={shortType} description={description} colspan={4}>
|
||||
<Td class="align-top">
|
||||
<MarkdownCode>
|
||||
{name}
|
||||
@@ -51,90 +34,4 @@ const id = `${componentName}-${name}`;
|
||||
{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>
|
||||
</DetailRow>
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
import MarkdownCode from '@/components/typography/MarkdownCode.astro';
|
||||
import Td from '@/components/typography/Td.astro';
|
||||
import DetailRow from './DetailRow.astro';
|
||||
|
||||
interface Props {
|
||||
name: string;
|
||||
type: string;
|
||||
shortType?: string;
|
||||
description?: string;
|
||||
componentName: string;
|
||||
}
|
||||
|
||||
const { name, type, shortType, description, componentName } = Astro.props;
|
||||
|
||||
const displayType = shortType ?? type;
|
||||
const id = `${componentName}-state-${name}`;
|
||||
---
|
||||
|
||||
<DetailRow id={id} name={name} type={type} shortType={shortType} description={description} colspan={3}>
|
||||
<Td class="align-top">
|
||||
<MarkdownCode>{name}</MarkdownCode>
|
||||
</Td>
|
||||
<Td class="align-top">
|
||||
<MarkdownCode>{displayType}</MarkdownCode>
|
||||
</Td>
|
||||
</DetailRow>
|
||||
Reference in New Issue
Block a user