Skip to content
Draft
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
26 changes: 3 additions & 23 deletions apps/docs/app/components/ascii/Frame.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,18 @@
import type { ReactNode } from 'react';

interface FrameProps {
/** Sits in the top rule, the way a filename sits in a box-drawn panel. */
/** Names the section. */
label?: ReactNode;
/** Sits at the right of the top rule. Use it for counts and short notes. */
/** Adds a count or short note to the section heading. */
note?: ReactNode;
className?: string;
children: ReactNode;
}

/**
* A panel drawn as a box.
*
* The four sides are ordinary one-pixel borders so they stay crisp at any zoom
* and any width. Only the corners are real box-drawing characters, sitting on
* top of the border. A panel built entirely from characters comes apart the
* moment the container is resized; this does not.
*/
/** Groups a named section of a page. */
export function Frame({ label, note, className, children }: FrameProps) {
return (
<section className={['fb-frame', className].filter(Boolean).join(' ')}>
<span aria-hidden className="fb-frame__corner fb-frame__corner--tl">
</span>
<span aria-hidden className="fb-frame__corner fb-frame__corner--tr">
</span>
<span aria-hidden className="fb-frame__corner fb-frame__corner--bl">
</span>
<span aria-hidden className="fb-frame__corner fb-frame__corner--br">
</span>

{label ? <h2 className="fb-frame__label">{label}</h2> : null}
{note ? <p className="fb-frame__note">{note}</p> : null}

Expand Down
16 changes: 4 additions & 12 deletions apps/docs/app/components/ascii/Rule.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,13 @@
interface RuleProps {
char?: string;
className?: string;
}

/**
* A horizontal rule made of real characters.
*
* The string is longer than any sensible screen and the container clips it, so
* the rule fills its width without any measuring.
*/
export function Rule({ char = '─', className }: RuleProps) {
/** Separates the main regions of the page. */
export function Rule({ className }: RuleProps) {
return (
<div
<hr
aria-hidden
className={['fb-rule', className].filter(Boolean).join(' ')}
>
{char.repeat(400)}
</div>
/>
);
}
8 changes: 4 additions & 4 deletions apps/docs/app/components/chrome/ThemeToggle.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ describe('ThemeToggle', () => {
document.documentElement.dataset.theme = 'light';
await renderToggle();

expect(toggleButton().textContent).toBe('[◑ light]');
expect(toggleButton().textContent).toBe('Theme: light');
expect(toggleButton().getAttribute('aria-label')).toBe(
'Switch to the dark theme'
);
Expand All @@ -43,7 +43,7 @@ describe('ThemeToggle', () => {

expect(document.documentElement.dataset.theme).toBe('dark');
expect(localStorage.getItem(THEME_KEY)).toBe('dark');
expect(toggleButton().textContent).toBe('[◐ dark]');
expect(toggleButton().textContent).toBe('Theme: dark');
expect(toggleButton().getAttribute('aria-label')).toBe(
'Switch to the light theme'
);
Expand All @@ -52,7 +52,7 @@ describe('ThemeToggle', () => {

expect(document.documentElement.dataset.theme).toBe('light');
expect(localStorage.getItem(THEME_KEY)).toBe('light');
expect(toggleButton().textContent).toBe('[◑ light]');
expect(toggleButton().textContent).toBe('Theme: light');
});

it('still updates the document and control when storage rejects the write', async () => {
Expand All @@ -68,7 +68,7 @@ describe('ThemeToggle', () => {

expect(setItem).toHaveBeenCalledWith(THEME_KEY, 'dark');
expect(document.documentElement.dataset.theme).toBe('dark');
expect(toggleButton().textContent).toBe('[◐ dark]');
expect(toggleButton().textContent).toBe('Theme: dark');
});
});

Expand Down
2 changes: 1 addition & 1 deletion apps/docs/app/components/chrome/ThemeToggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export function ThemeToggle() {
className="fb-button"
aria-label={`Switch to the ${theme === 'dark' ? 'light' : 'dark'} theme`}
>
{theme === 'dark' ? '[◐ dark]' : '[◑ light]'}
Theme: {theme}
</button>
);
}
202 changes: 202 additions & 0 deletions apps/docs/app/components/manual/ManualContentDiagram.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import Link from 'next/link';

import type { DocSummary, PackageSummary, Section } from '../../../lib/content';
import { CONTENT_PARTS } from './content-system';
import type { ContentPartKey } from './content-system';

interface ManualContentDiagramProps {
sections: Section[];
docs: DocSummary[];
packages: PackageSummary[];
}

interface AssemblyPart {
ref: string;
name: string;
count: number;
source: string;
purpose: string;
items: Array<{ id: string; label: string; href?: string }>;
}

/**
* A semantic, CSS-drawn exploded view of the content that builds this site.
*
* The native details element deliberately supplies the one interaction: it is
* keyboard and touch accessible without JavaScript, and CSS may animate its
* `[open]` state only when reduced motion is not requested.
*/
export function ManualContentDiagram({
sections,
docs,
packages,
}: ManualContentDiagramProps) {
const counts: Record<ContentPartKey, number> = {
sections: sections.length,
docs: docs.length,
packages: packages.length,
};
const items: Record<ContentPartKey, AssemblyPart['items']> = {
sections: sections.map((section) => ({
id: section.id,
label: `${number(section.order)} ${section.title}`,
})),
docs: docs.map((doc) => ({
id: doc.id,
label: doc.title,
href: `/docs/${doc.id}/`,
})),
packages: packages.map((entry) => ({
id: entry.id,
label: entry.id,
href: `/reference/${entry.id}/`,
})),
};
const parts: AssemblyPart[] = CONTENT_PARTS.map((part) => ({
ref: part.ref,
name: part.name,
count: counts[part.key],
source: part.sourcePattern,
purpose: part.purpose,
items: items[part.key],
}));
const total = parts.reduce((sum, part) => sum + part.count, 0);

return (
<section
className="fb-manual-assembly"
aria-labelledby="content-system-title"
>
<div className="fb-manual-section-heading">
<p className="fb-manual-section-heading__ref">FIG. 00–1</p>
<h2 id="content-system-title">Content system, exploded</h2>
<p className="fb-manual-section-heading__note">
{total} source records assembled into this documentation site.
</p>
</div>

<figure className="fb-manual-diagram">
<div className="fb-manual-diagram__stage">
<div className="fb-manual-diagram__assembly">
<span className="fb-manual-diagram__assembly-label">
Rendered documentation
</span>
<strong>flatbread docs</strong>
<span className="fb-manual-diagram__assembly-count">
{total} source records
</span>
</div>

<ol
className="fb-manual-diagram__parts"
aria-label="Content-system parts"
>
{parts.map((part) => (
<li
key={part.ref}
className="fb-manual-diagram__part"
data-ref={part.ref}
>
<span className="fb-manual-diagram__part-ref">{part.ref}</span>
<span className="fb-manual-diagram__part-name">
{part.name}
</span>
<span className="fb-manual-diagram__part-count">
{part.count} records
</span>
<span
aria-hidden="true"
className="fb-manual-diagram__leader"
/>
</li>
))}
</ol>

<ol
className="fb-manual-diagram__relations"
aria-label="Content-system relations"
>
<li
className="fb-manual-diagram__relation"
data-from="01"
data-to="02"
>
<span className="fb-manual-diagram__relation-ref">01 → 02</span>
Navigation sections group guide records.
</li>
<li
className="fb-manual-diagram__relation"
data-from="02"
data-to="00"
>
<span className="fb-manual-diagram__relation-ref">02 → 00</span>
Guide records render as documentation pages.
</li>
<li
className="fb-manual-diagram__relation"
data-from="03"
data-to="00"
>
<span className="fb-manual-diagram__relation-ref">03 → 00</span>
Package references render alongside guides.
</li>
</ol>
</div>

<figcaption className="fb-manual-diagram__caption">
<span>CONTENT ASSEMBLY</span>
<span>
Direct labels identify the source collection and its role.
</span>
</figcaption>
</figure>

<details className="fb-manual-inspection" data-motion="inspection">
<summary className="fb-manual-inspection__summary">
<span>Inspect assembled records</span>
<span className="fb-manual-inspection__summary-meta">
{total} records
</span>
</summary>

<div className="fb-manual-inspection__panel">
<p className="fb-manual-inspection__intro">
Each part remains a repository file or README; the site reads it at
build time.
</p>

<ol className="fb-manual-inspection__parts">
{parts.map((part) => (
<li key={part.ref} className="fb-manual-inspection__part">
<div className="fb-manual-inspection__part-heading">
<span className="fb-manual-inspection__ref">
REF. {part.ref}
</span>
<h3>{part.name}</h3>
<span>{part.count} records</span>
</div>
<p>{part.purpose}</p>
<code>{part.source}</code>
<ul>
{part.items.map((item) => (
<li key={item.id}>
{item.href ? (
<Link href={item.href}>{item.label}</Link>
) : (
item.label
)}
</li>
))}
</ul>
</li>
))}
</ol>
</div>
</details>
</section>
);
}

function number(value: number): string {
return String(value).padStart(2, '0');
}
15 changes: 15 additions & 0 deletions apps/docs/app/components/manual/content-system.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { describe, expect, it } from 'vitest';

import { CONTENT_PARTS } from './content-system';

describe('content-system references', () => {
it('keeps one ordered reference for each source collection', () => {
expect(
CONTENT_PARTS.map(({ key, ref, name }) => ({ key, ref, name }))
).toEqual([
{ key: 'sections', ref: '01', name: 'Navigation sections' },
{ key: 'docs', ref: '02', name: 'Guide records' },
{ key: 'packages', ref: '03', name: 'Package references' },
]);
});
});
42 changes: 42 additions & 0 deletions apps/docs/app/components/manual/content-system.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
export type ContentPartKey = 'sections' | 'docs' | 'packages';

interface ContentPartDefinition {
key: ContentPartKey;
ref: string;
name: string;
sourcePattern: string;
manifestPath: string;
manifestNote: string;
purpose: string;
}

/** One reference system for both the exploded view and its parts manifest. */
export const CONTENT_PARTS: readonly ContentPartDefinition[] = [
{
key: 'sections',
ref: '01',
name: 'Navigation sections',
sourcePattern: 'apps/docs/content/nav/*.yaml',
manifestPath: 'apps/docs/content/nav/[id].yaml',
manifestNote: 'Names and orders the guide index.',
purpose: 'Groups the guide index.',
},
{
key: 'docs',
ref: '02',
name: 'Guide records',
sourcePattern: 'apps/docs/content/docs/*.md',
manifestPath: 'apps/docs/content/docs/[id].md',
manifestNote: 'Rendered as guide pages and grouped by section.',
purpose: 'Supplies the guide pages and local contents.',
},
{
key: 'packages',
ref: '03',
name: 'Package references',
sourcePattern: 'apps/docs/content/reference/*.md',
manifestPath: 'apps/docs/content/reference/[id].md',
manifestNote: 'Published README content presented as reference pages.',
purpose: 'Supplies the package README reference pages.',
},
];
5 changes: 1 addition & 4 deletions apps/docs/app/components/nav/NavDisclosure.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,7 @@ export function NavDisclosure({
open={wide || open}
onToggle={(event) => setOpen(event.currentTarget.open)}
>
<summary className="fb-nav__summary">
<span aria-hidden>{open ? '▾' : '▸'} </span>
{label}
</summary>
<summary className="fb-nav__summary">{label}</summary>
{children}
</details>
);
Expand Down
Loading
Loading