Skip to content
Merged
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
27,673 changes: 24,140 additions & 3,533 deletions src/pages/docs/api-snapshot.ts

Large diffs are not rendered by default.

12 changes: 11 additions & 1 deletion src/pages/docs/catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ type PageInput = {
keywords?: readonly string[];
aliases?: readonly string[];
packages?: readonly PackageReference[];
propTypes?: readonly string[];
status?: DocsStatus;
loader?: DocsPageDefinition['loader'];
};
Expand Down Expand Up @@ -150,6 +151,7 @@ function definePage(
navSection: section,
status: input.status ?? 'stable',
packages: input.packages ?? [packageReference('askr')],
propTypes: input.propTypes,
headings,
keywords: input.keywords ?? [],
aliases: input.aliases,
Expand Down Expand Up @@ -1093,7 +1095,15 @@ const dataLayout = componentPages('Data and layout', [
ui: [],
themes: ['typography'],
},
{ title: 'Application Layout', ui: [], themes: [] },
{
title: 'Layout Primitives',
path: 'application-layout',
description:
'Start here for layout: use Block as the general-purpose primitive, then compose Container and Text with exact published prop values.',
ui: [],
themes: ['components'],
propTypes: ['BlockOwnProps', 'ContainerProps', 'TextProps'],
},
{
title: 'Advanced Layout',
// SidebarScope is a pure styling wrapper — it carries no open/closed/rail
Expand Down
85 changes: 85 additions & 0 deletions src/pages/docs/component-props.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { apiManifest } from './api-manifest';
import {
apiSymbolSets,
type ApiMemberDefinition,
type ApiSymbolDefinition,
} from './api-snapshot';
import type { DocsPageDefinition } from './types';

export interface ComponentPropReference extends ApiSymbolDefinition {
readonly importName: string;
readonly members: readonly ApiMemberDefinition[];
}

function propSymbolsFor(importName: string): readonly ComponentPropReference[] {
const entrypoint = apiManifest.find(
(candidate) => candidate.importName === importName
);
if (!entrypoint) return [];
return (apiSymbolSets[entrypoint.symbolSet] ?? []).flatMap((symbol) =>
symbol.name.endsWith('Props') && symbol.members?.length
? [{ ...symbol, importName, members: symbol.members }]
: []
);
}

/**
* Resolve component prop contracts from the declarations captured from the
* installed package. Explicit page selections retain their authored order.
*/
export function componentPropReferences(
page: DocsPageDefinition
): readonly ComponentPropReference[] {
const available = page.packages.flatMap((item) =>
item.importPath &&
(page.propTypes?.length || item.importPath.split('/').length > 2)
? propSymbolsFor(item.importPath)
: []
);
const byName = new Map(available.map((symbol) => [symbol.name, symbol]));

// ContainerProps is an Omit<BlockDivProps, ...> composition. The compiler's
// mapped type exposes its local `size` member separately, so present the
// inherited public layout surface alongside it.
const container = byName.get('ContainerProps');
const block = byName.get('BlockOwnProps');
if (container && block) {
const members = new Map(
[...block.members, ...container.members].map((member) => [
member.name,
member,
])
);
members.delete('maxWidth');
byName.set('ContainerProps', {
...container,
members: [...members.values()].sort((left, right) =>
left.name.localeCompare(right.name)
),
});
}

const text = byName.get('TextProps');
if (text) {
byName.set('TextProps', {
...text,
members: text.members.map((member) =>
member.name === 'as'
? {
...member,
signature: member.signature.replace('TElement', 'TextElement'),
}
: member
),
});
}

const selected = page.propTypes ?? [];
if (selected.length > 0) {
return selected.flatMap((name) => {
const symbol = byName.get(name);
return symbol ? [symbol] : [];
});
}
return [...byName.values()];
}
10 changes: 5 additions & 5 deletions src/pages/docs/content-overrides.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,9 +234,9 @@ export const headingOverrides: Readonly<
'live-examples':
'A standard admin layout is `SidebarScope` wrapping a `Sidebar` (with `SidebarHeader`, `SidebarContent` full of `SidebarGroup`/`SidebarMenu` entries, and `SidebarFooter`) alongside a `SidebarInset` holding the routed page content, with `SidebarTrigger` somewhere in the header to toggle it. Set `collapsible="icon"` on `Sidebar` when you want it to shrink to an icon rail instead of disappearing entirely on collapse.',
purpose:
"This page extends Application Layout into more involved shells — persistent sidebars, nested content regions, and multi-panel apps — using the same `Block`-based primitives plus `Sidebar` and its parts. As with Application Layout, there's no single component to reach for; it's a set of composable pieces and a recommended way to combine them.",
"This page extends Layout Primitives into more involved shells — persistent sidebars, nested content regions, and multi-panel apps — using the same `Block`-based primitives plus `Sidebar` and its parts. As with Layout Primitives, there's no single component to reach for; it's a set of composable pieces and a recommended way to combine them.",
'related-pages':
"Application Layout covers the simpler header-and-main shell this page builds on top of. NavigationMenu and Navbar are worth comparing for top-nav-driven apps that don't need a persistent side panel. Item and ItemGroup are a reasonable alternative to `SidebarMenu` when you need list rows that aren't specifically navigation.",
"Layout Primitives covers the simpler header-and-main shell this page builds on top of. NavigationMenu and Navbar are worth comparing for top-nav-driven apps that don't need a persistent side panel. Item and ItemGroup are a reasonable alternative to `SidebarMenu` when you need list rows that aren't specifically navigation.",
'state-model':
"`SidebarScope` is a pure styling/CSS-boundary wrapper — it renders a plain part with no state of its own. It does **not** coordinate open/closed or rail-expanded state across `Sidebar` and `SidebarTrigger`; there's no built-in mechanism tracking that. You own collapse state yourself (a `state()` cell, driven by clicking `SidebarTrigger`, controlling whatever prop or class toggles the collapsed styling) the same way you would with any other composable primitive here — don't assume wrapping things in `SidebarScope` gets you that behavior for free. Individual `SidebarMenuButton`s and `SidebarMenuAction`s take an `active?: boolean` prop for marking the current route, which is a value you compute from your router, not something the component derives on its own.",
'styling-and-tokens':
Expand Down Expand Up @@ -297,7 +297,7 @@ export const headingOverrides: Readonly<
purpose:
"`@askrjs/themes` and `@askrjs/ui` don't ship an `application-chrome` component, so treat this page as a walkthrough of how to assemble a full app shell from pieces that already exist. The outer frame comes from `Shell`, `ShellNav`, and `ShellMain`; the major regions are built with `Header`, `Main`, `Aside`, and `Footer` (plus its `FooterSection`, `FooterLinks`, and `FooterLink` parts). Fill the navigation areas inside that frame with `Navbar`, `Sidebar`, `Breadcrumb`, and `Pagination`.",
'related-pages':
'See Sidebar, Menubar, and Navbar and Navigation Menu for the navigation pieces that typically live inside this shell, and Application Layout for the broader page-layout primitives (`Page`, `PageHeader`, `Section`, `Container`) that chrome usually wraps.',
'See Sidebar, Menubar, and Navbar and Navigation Menu for the navigation pieces that typically live inside this shell, and Layout Primitives for the broader page-layout primitives (`Block`, `Container`, `Text`) that chrome usually wraps.',
'state-model':
"These are pure layout components; none of them own state. Anything dynamic in your chrome — a collapsed sidebar, an open mobile nav, the active breadcrumb trail — is state you manage in your own app and pass down as props or conditional children, the same way you would with `Navbar`'s `collapseAt` or `Sidebar`'s open/closed props on those individual component pages.",
'styling-and-tokens':
Expand Down Expand Up @@ -1003,7 +1003,7 @@ export const headingOverrides: Readonly<
purpose:
"Sidebar covers the collapsible side-navigation panel pattern common in dashboards and admin tools, and it's styled entirely at the theme layer — @askrjs/ui doesn't define a headless version to build on. What you get is a family of layout parts (header, content, footer, groups, menu items) alongside a trigger button, all presentational rather than stateful. Use it for persistent app chrome; for anything transient or overlay-based, Dialog, Sheet, or Popover are the better fit.",
'related-pages':
'Application Chrome and Application Layout cover how Sidebar fits into a full page shell alongside a Navbar or Header. For the toggle button pattern itself independent of sidebar-specific styling, Button covers the underlying variants SidebarTrigger builds on. If what you actually need is in-page section switching rather than persistent app navigation, look at Tabs or Accordion and Collapsible instead.',
'Application Chrome and Layout Primitives cover how Sidebar fits into a full page shell alongside a Navbar or Header. For the toggle button pattern itself independent of sidebar-specific styling, Button covers the underlying variants SidebarTrigger builds on. If what you actually need is in-page section switching rather than persistent app navigation, look at Tabs or Accordion and Collapsible instead.',
'state-model':
"None of this is stateful — Sidebar has no internal open/collapsed flag; `collapsible`, `side`, and `variant` are plain props you pass in based on state you own elsewhere (a boolean in your app, a cookie, whatever fits). SidebarTrigger and SidebarRail render as buttons with no click behavior baked in, so wiring their onClick to actually toggle your sidebar state is on you. This is a deliberate consequence of Sidebar being themes-only: there's no headless controller underneath managing collapse for you.",
'styling-and-tokens':
Expand Down Expand Up @@ -1228,7 +1228,7 @@ export const headingOverrides: Readonly<
purpose:
"This page covers the `Typography` family in `@askrjs/themes/typography` — a set of styled text components (`TypographyH1` through `TypographyH4`, `TypographyP`, `TypographyBlockquote`, `TypographyList`, `TypographyLead`, `TypographyMuted`, and a generic `Typography` wrapper) that give you consistent type scale and rhythm without writing raw heading and paragraph markup by hand everywhere. There's no `@askrjs/ui` equivalent because text rendering doesn't need behavior — this is pure presentation, so it lives entirely in the themes package.",
'related-pages':
"Card's `CardTitle` and `CardDescription` cover heading and text needs specifically inside a card and don't need to be paired with `Typography` separately. Field's label and description parts handle typography inside forms with their own conventions. For page-level structure that typography sits inside, see Application Layout and Advanced Layout.",
"Card's `CardTitle` and `CardDescription` cover heading and text needs specifically inside a card and don't need to be paired with `Typography` separately. Field's label and description parts handle typography inside forms with their own conventions. For page-level structure that typography sits inside, see Layout Primitives and Advanced Layout.",
'state-model':
"Rendering is all these components do — there's nothing stateful to track once you land on typography and display primitives. Every one of them shares the same generic prop shape, `CatalogComponentProps`, giving you `as` to swap which element gets rendered and `asChild` to merge props onto a child rather than wrap it. That's how, say, `TypographyH2` can render as an `h3` when you need the visual weight of an H2 without breaking your document's heading hierarchy.",
'styling-and-tokens':
Expand Down
33 changes: 33 additions & 0 deletions src/pages/docs/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { apiManifest } from './api-manifest';
import { apiSymbolSets } from './api-snapshot';
import { docsCatalog, docsSections } from './catalog';
import { cliSnapshot } from './cli-snapshot';
import { componentPropReferences } from './component-props';
import { upgradeGuidance } from './release-notes';
import type { DocsPageDefinition } from './types';
import { buildUsageGuide, routeExampleFor } from './usage-guide';
Expand Down Expand Up @@ -62,6 +63,36 @@ function pageHeader(page: DocsPageDefinition): string[] {
`Source: [${canonical}](${canonical})`,
'',
`Status: ${page.status}. Packages: ${packages}.`,
'',
'**Published packages are authoritative.** Examples may lag behind a published contract. When guidance differs, verify the exports and TypeScript declarations in your installed package, then file an issue.',
];
}

function componentPropsMarkdown(page: DocsPageDefinition): string[] {
const references = componentPropReferences(page);
if (references.length === 0) return [];
return [
'',
'## Published props',
'',
'Generated from the TypeScript declarations shipped by the installed package.',
...references.flatMap((reference) => [
'',
`### \`${reference.name}\``,
'',
`Import from \`${reference.importName}\`.`,
'',
...reference.members.map((member) => {
const description = member.summary
? ` — ${markdownProse(member.summary)}`
: '';
const defaultValue = member.tags?.default?.join(' ');
const defaultText = defaultValue
? ` Default: \`${defaultValue}\`.`
: '';
return `- \`${member.signature}\`${description}${defaultText}`;
}),
]),
];
}

Expand Down Expand Up @@ -205,6 +236,8 @@ export function renderDocsPageMarkdown(page: DocsPageDefinition): string {
lines.push(codeFence(guide.code, exampleLanguage(guide.code)));
}

lines.push(...componentPropsMarkdown(page));

for (const heading of page.headings) {
lines.push(
'',
Expand Down
64 changes: 64 additions & 0 deletions src/pages/docs/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ArrowLeftIcon, ArrowRightIcon, CopyIcon } from '@askrjs/lucide';
import { docsByRoute, docsSections, resolveDocsRoute } from './catalog';
import { cliSnapshot } from './cli-snapshot';
import { componentDemoFor } from './component-demos';
import { componentPropReferences } from './component-props';
import { upgradeGuidance } from './release-notes';
import type { DocsHeadingDefinition, DocsPageDefinition } from './types';
import { buildUsageGuide, routeExampleFor } from './usage-guide';
Expand Down Expand Up @@ -131,6 +132,60 @@ function ComponentDemo({ page }: { page: DocsPageDefinition }) {
);
}

function ComponentPropsReference({ page }: { page: DocsPageDefinition }) {
const references = componentPropReferences(page);
if (references.length === 0) return null;
return (
<section aria-labelledby="published-props">
<h2 id="published-props" class="anchored-heading">
<a href="#published-props">Published props</a>
</h2>
<p>
Generated from the TypeScript declarations shipped by the installed
package. Named types in the Type column define the accepted values.
</p>
{references.map((reference) => (
<div class="component-props" key={reference.name}>
<h3>
<code>{reference.name}</code>
</h3>
<p>
Import from <code>{reference.importName}</code>.
</p>
<div class="api-table-wrap">
<table class="api-table">
<thead>
<tr>
<th>Prop</th>
<th>Type</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
{reference.members.map((member) => (
<tr key={member.name}>
<td>
<code>{member.name}</code>
</td>
<td>
<code>{member.signature}</code>
</td>
<td>
<code>{member.tags?.default?.join(' ') || '—'}</code>
</td>
<td>{member.summary || '—'}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
))}
</section>
);
}

function HeadingContent({
item,
page,
Expand Down Expand Up @@ -223,6 +278,14 @@ export default function DocsPage() {
<h1>{page.title}</h1>
<p>{page.description}</p>
</header>
<aside class="docs-callout" data-tone="authority">
<strong>Published packages are authoritative</strong>
<p>
Examples may lag behind a published contract. When guidance differs,
verify the exports and TypeScript declarations in your installed
package, then file an issue so the example can be corrected.
</p>
</aside>
{page.status !== 'stable' && (
<aside class="docs-callout" data-tone="warning">
<strong>
Expand All @@ -239,6 +302,7 @@ export default function DocsPage() {
)}
<UsageGuide page={page} />
<ComponentDemo page={page} />
<ComponentPropsReference page={page} />
<HeadingContentList headings={page.headings} page={page} />
{page.route === '/docs' && <DocsLandingDetails />}
{page.route === '/docs/tooling/cli-overview' && <CliReferenceSection />}
Expand Down
3 changes: 3 additions & 0 deletions src/pages/docs/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ export type DocsPageDefinition = {
navSection?: string;
status: DocsStatus;
packages: readonly PackageReference[];
/** Generated published prop types to show on this page. When omitted, all
* exported `*Props` types from the listed component entrypoints are shown. */
propTypes?: readonly string[];
headings: readonly DocsHeadingDefinition[];
keywords: readonly string[];
aliases?: readonly string[];
Expand Down
17 changes: 10 additions & 7 deletions src/pages/docs/usage-guide.ts
Original file line number Diff line number Diff line change
Expand Up @@ -615,13 +615,16 @@ const columns: readonly VirtualTableColumn<Project>[] = [
</>`,
],
[
/^Application Layout$/,
`import { Container, Main, Page, PageHeader, Section } from '@askrjs/themes/components';

<Page>
<PageHeader title="Projects" description="Active work in this workspace." />
<Main><Container><Section><ProjectList /></Section></Container></Main>
</Page>`,
/^Layout Primitives$/,
`import { Block, Container, Text } from '@askrjs/themes/components';

<Container paddingY="lg">
<Block direction="column" gap="md">
<Text as="strong" size="lg">Projects</Text>
<Text tone="muted">Active work in this workspace.</Text>
<ProjectList />
</Block>
</Container>`,
],
[
/^Advanced Layout$/,
Expand Down
Loading