@eqtylab/docs 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +190 -0
- package/README.md +52 -10
- package/dist/config.js +1 -1
- package/dist/index.js +16 -5
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/dist/runtime/chrome/GlobalSearch.module.css +0 -26
- package/dist/runtime/chrome/GlobalSearch.tsx +0 -422
- package/dist/runtime/chrome/Header.astro +0 -105
- package/dist/runtime/chrome/LinkIcon.astro +0 -35
- package/dist/runtime/chrome/NavDrawer.astro +0 -60
- package/dist/runtime/chrome/NavTree.astro +0 -112
- package/dist/runtime/chrome/NotFoundBody.tsx +0 -12
- package/dist/runtime/chrome/PageFooter.astro +0 -80
- package/dist/runtime/chrome/Prose.astro +0 -169
- package/dist/runtime/chrome/Sidebar.astro +0 -21
- package/dist/runtime/chrome/TableOfContents.astro +0 -86
- package/dist/runtime/chrome/ThemeToggle.tsx +0 -85
- package/dist/runtime/chrome/TocElbow.astro +0 -30
- package/dist/runtime/chrome/TocList.astro +0 -43
- package/dist/runtime/components/AlertBridge.astro +0 -16
- package/dist/runtime/components/CodeFence.astro +0 -38
- package/dist/runtime/components/CodeFenceBridge.astro +0 -20
- package/dist/runtime/components/Link.astro +0 -21
- package/dist/runtime/components/TableBridge.astro +0 -18
- package/dist/runtime/components/index.ts +0 -2
- package/dist/runtime/layouts/DocsPage.astro +0 -50
- package/dist/runtime/layouts/DocsShell.astro +0 -92
- package/dist/runtime/lib/mdx-components.ts +0 -43
- package/dist/runtime/lib/nav-data.ts +0 -74
- package/dist/runtime/lib/summary.ts +0 -57
- package/dist/runtime/lib/theme.ts +0 -84
- package/dist/runtime/routes/docs-md.ts +0 -33
- package/dist/runtime/routes/docs.astro +0 -92
- package/dist/runtime/routes/llms-txt.ts +0 -38
- package/dist/runtime/routes/not-found.astro +0 -16
- package/dist/runtime/scripts/eq-copy.ts +0 -27
- package/dist/runtime/scripts/eq-highlight.ts +0 -24
- package/dist/runtime/scripts/eq-nav-drawer.ts +0 -31
- package/dist/runtime/scripts/eq-nav-group.ts +0 -52
- package/dist/runtime/scripts/eq-toc.ts +0 -166
- package/dist/runtime/styles/chrome.css +0 -30
- package/dist/runtime/styles/prose.css +0 -102
- package/dist/runtime/styles/theme.css +0 -2
- package/dist/runtime/styles/utilities.css +0 -38
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
/**
|
|
3
|
-
* Fenced code through Equality's CodeBlock with no client React. eq-copy drives
|
|
4
|
-
* the copy button and eq-highlight starts the highlight pass CodeBlock's own
|
|
5
|
-
* useEffect never runs here. The highlighter owns language aliasing.
|
|
6
|
-
*/
|
|
7
|
-
import { CodeBlock } from '@eqtylab/equality';
|
|
8
|
-
|
|
9
|
-
interface Props {
|
|
10
|
-
code: string;
|
|
11
|
-
lang?: string;
|
|
12
|
-
title?: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const { code, lang = 'text', title } = Astro.props;
|
|
16
|
-
|
|
17
|
-
const language = lang.toLowerCase();
|
|
18
|
-
---
|
|
19
|
-
|
|
20
|
-
{/*
|
|
21
|
-
`block` is load-bearing: the fence is a custom element, which is inline by default.
|
|
22
|
-
|
|
23
|
-
The descendant rule undoes CodeBlock's max-h-64, which is wrong for a docs sample.
|
|
24
|
-
`!` is load-bearing too: Equality ships unlayered CSS, and an unlayered declaration
|
|
25
|
-
beats anything in `@layer utilities` whatever its specificity, so a plain utility
|
|
26
|
-
loses to `.code-block` and the sample silently goes back to scrolling at 16rem.
|
|
27
|
-
*/}
|
|
28
|
-
<eq-highlight
|
|
29
|
-
class="mb-4 block [&_[class*=code-block]]:max-h-none!"
|
|
30
|
-
data-eq-chrome
|
|
31
|
-
data-eq-copy={code}
|
|
32
|
-
>
|
|
33
|
-
<CodeBlock code={code} language={language} title={title} />
|
|
34
|
-
</eq-highlight>
|
|
35
|
-
<script>
|
|
36
|
-
import '@eqtylab/docs/scripts/eq-copy.ts';
|
|
37
|
-
import '@eqtylab/docs/scripts/eq-highlight.ts';
|
|
38
|
-
</script>
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
/**
|
|
3
|
-
* Renders a fence from the props `rehypeCodeFence` lifted onto the `<pre>`.
|
|
4
|
-
* Do not re-parse the slot: its HTML is escaped in dev and raw in build.
|
|
5
|
-
*/
|
|
6
|
-
import CodeFence from './CodeFence.astro';
|
|
7
|
-
|
|
8
|
-
interface Props {
|
|
9
|
-
'data-code'?: string;
|
|
10
|
-
'data-language'?: string;
|
|
11
|
-
'data-title'?: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const props = Astro.props as Props;
|
|
15
|
-
const code = props['data-code'] ?? '';
|
|
16
|
-
const lang = props['data-language'] ?? 'text';
|
|
17
|
-
const title = props['data-title'];
|
|
18
|
-
---
|
|
19
|
-
|
|
20
|
-
<CodeFence code={code} lang={lang} title={title} />
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
import { isExternalHref } from '@eqtylab/docs/paths';
|
|
3
|
-
|
|
4
|
-
interface Props {
|
|
5
|
-
href?: string;
|
|
6
|
-
[key: string]: unknown;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
const { href = '', ...rest } = Astro.props;
|
|
10
|
-
const external = isExternalHref(href);
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
<a
|
|
14
|
-
href={href}
|
|
15
|
-
class="text-inherit"
|
|
16
|
-
target={external ? '_blank' : undefined}
|
|
17
|
-
rel={external ? 'noopener noreferrer' : undefined}
|
|
18
|
-
{...rest}
|
|
19
|
-
>
|
|
20
|
-
<slot />
|
|
21
|
-
</a>
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
/** Bridges a markdown table to Equality's `Table`; the column count comes from `rehypeTableColumns`. */
|
|
3
|
-
import { TableContainer } from '@eqtylab/equality';
|
|
4
|
-
|
|
5
|
-
interface Props {
|
|
6
|
-
'data-column-count'?: string;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
const count = Number(Astro.props['data-column-count'] ?? 0);
|
|
10
|
-
|
|
11
|
-
// minmax(0, auto): without a zero minimum, one long unbroken cell blows the grid past its container.
|
|
12
|
-
const columns = count > 0 ? `repeat(${count}, minmax(0, auto))` : undefined;
|
|
13
|
-
---
|
|
14
|
-
|
|
15
|
-
{/* Flow spacing only; every visual property comes from Equality's Table. */}
|
|
16
|
-
<TableContainer columns={columns} border className="mb-4" data-eq-chrome>
|
|
17
|
-
<slot />
|
|
18
|
-
</TableContainer>
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
/** Header + sidebar | prose | TOC. The `doc` template. */
|
|
3
|
-
import type { NavNode, TocNode } from '@eqtylab/docs/types';
|
|
4
|
-
import CONFIG from 'virtual:eqty-docs/config';
|
|
5
|
-
import DocsShell from './DocsShell.astro';
|
|
6
|
-
import Header from '../chrome/Header.astro';
|
|
7
|
-
import NavDrawer from '../chrome/NavDrawer.astro';
|
|
8
|
-
import Sidebar from '../chrome/Sidebar.astro';
|
|
9
|
-
import TableOfContents from '../chrome/TableOfContents.astro';
|
|
10
|
-
|
|
11
|
-
interface Props {
|
|
12
|
-
title: string;
|
|
13
|
-
description?: string;
|
|
14
|
-
nav: NavNode[];
|
|
15
|
-
toc: TocNode[];
|
|
16
|
-
showToc?: boolean;
|
|
17
|
-
splash?: boolean;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const { title, description, nav, toc, showToc = true, splash = false } = Astro.props;
|
|
21
|
-
|
|
22
|
-
// A section's own index page if it has one, else its first child. Never the sidebar repeated.
|
|
23
|
-
const suggested = nav
|
|
24
|
-
.map((node) => ({ label: node.label, href: node.href ?? node.children?.[0]?.href }))
|
|
25
|
-
.filter((item): item is { label: string; href: string } => !!item.href)
|
|
26
|
-
.slice(0, 4);
|
|
27
|
-
---
|
|
28
|
-
|
|
29
|
-
<DocsShell title={title} description={description}>
|
|
30
|
-
{/* A prop, not slot fallback: Astro registers a named slot at compile time, so a forwarded
|
|
31
|
-
slot always counts as filled and the fallback never runs. */}
|
|
32
|
-
<Header showSearch={!Astro.slots.has('search')} suggested={suggested}>
|
|
33
|
-
<slot name="search" slot="search" />
|
|
34
|
-
<slot name="versions" slot="versions" />
|
|
35
|
-
</Header>
|
|
36
|
-
<NavDrawer nodes={nav} />
|
|
37
|
-
<div class="flex items-start">
|
|
38
|
-
{!splash && <Sidebar nodes={nav} />}
|
|
39
|
-
{/* 128px of runway, so a page stops deliberately rather than at its last line. */}
|
|
40
|
-
<main
|
|
41
|
-
class="mx-auto flex w-full min-w-0 max-w-[calc(var(--eq-docs-content-max)+var(--eq-docs-toc-width)+8rem)] items-start gap-12 px-6 pb-32 pt-10 data-[splash]:max-w-none xl:gap-16"
|
|
42
|
-
data-splash={splash ? '' : undefined}
|
|
43
|
-
>
|
|
44
|
-
<div class="min-w-0 flex-1 [&>.eq-prose]:max-w-[var(--eq-docs-content-max)]">
|
|
45
|
-
<slot />
|
|
46
|
-
</div>
|
|
47
|
-
{!splash && showToc && toc.length > 0 && <TableOfContents items={toc} />}
|
|
48
|
-
</main>
|
|
49
|
-
</div>
|
|
50
|
-
</DocsShell>
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
/** The <html> document: head, theme bootstrap, and a slot for the page body. */
|
|
3
|
-
import CONFIG from 'virtual:eqty-docs/config';
|
|
4
|
-
import { withBase } from '@eqtylab/docs/paths';
|
|
5
|
-
|
|
6
|
-
import '../styles/docs.css';
|
|
7
|
-
|
|
8
|
-
interface Props {
|
|
9
|
-
title?: string;
|
|
10
|
-
description?: string;
|
|
11
|
-
/** Emit robots noindex. Set for every pinned version build. */
|
|
12
|
-
noIndex?: boolean;
|
|
13
|
-
/** Canonical URL, when it differs from the current page. */
|
|
14
|
-
canonical?: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const { title, description, noIndex, canonical } = Astro.props;
|
|
18
|
-
|
|
19
|
-
const paths = {
|
|
20
|
-
base: import.meta.env.BASE_URL,
|
|
21
|
-
pathPrefix: CONFIG.pathPrefix,
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
const pageTitle = title && title !== CONFIG.title ? `${title} · ${CONFIG.title}` : CONFIG.title;
|
|
25
|
-
const pageDescription = description ?? CONFIG.description;
|
|
26
|
-
|
|
27
|
-
// Pinned version builds are noindex: latest today is not latest tomorrow.
|
|
28
|
-
const robots = noIndex ?? !CONFIG.env.isLatest;
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Blocking inline theme bootstrap, so the attribute is set before first paint.
|
|
32
|
-
* Do not also call Equality's `initializeTheme`: it knows only light and dark
|
|
33
|
-
* and its stored value would override this. Never write storage here, or
|
|
34
|
-
* 'system' becomes a resolved value permanently. Constants must match
|
|
35
|
-
* `runtime/lib/theme.ts`.
|
|
36
|
-
*/
|
|
37
|
-
const themeBootstrap = `
|
|
38
|
-
(() => {
|
|
39
|
-
const KEY = 'eqty-docs-theme';
|
|
40
|
-
const EVENT = 'eqty-docs-theme-change';
|
|
41
|
-
const PERSIST = ${JSON.stringify(CONFIG.theme.persist)};
|
|
42
|
-
const query = window.matchMedia('(prefers-color-scheme: dark)');
|
|
43
|
-
|
|
44
|
-
const read = () => {
|
|
45
|
-
if (!PERSIST) return window.__eqtyDocsTheme ?? null;
|
|
46
|
-
try {
|
|
47
|
-
return window.localStorage.getItem(KEY);
|
|
48
|
-
} catch {
|
|
49
|
-
return null;
|
|
50
|
-
}
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
const apply = () => {
|
|
54
|
-
const stored = read();
|
|
55
|
-
const preference =
|
|
56
|
-
stored === 'dark' || stored === 'light' || stored === 'system' ? stored : 'system';
|
|
57
|
-
const resolved = preference === 'system' ? (query.matches ? 'dark' : 'light') : preference;
|
|
58
|
-
const root = document.documentElement;
|
|
59
|
-
root.setAttribute('data-equality-theme', resolved);
|
|
60
|
-
root.setAttribute('data-eq-theme-pref', preference);
|
|
61
|
-
// Native UI follows this, not the attribute above.
|
|
62
|
-
root.style.colorScheme = resolved;
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
apply();
|
|
66
|
-
query.addEventListener('change', apply);
|
|
67
|
-
window.addEventListener(EVENT, apply);
|
|
68
|
-
// The React subscription never touches the attribute, so other tabs repaint here.
|
|
69
|
-
window.addEventListener('storage', (event) => {
|
|
70
|
-
if (event.key === KEY) apply();
|
|
71
|
-
});
|
|
72
|
-
})();
|
|
73
|
-
`;
|
|
74
|
-
---
|
|
75
|
-
|
|
76
|
-
<!doctype html>
|
|
77
|
-
<html lang="en">
|
|
78
|
-
<head>
|
|
79
|
-
<meta charset="utf-8" />
|
|
80
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
81
|
-
<title>{pageTitle}</title>
|
|
82
|
-
{pageDescription && <meta name="description" content={pageDescription} />}
|
|
83
|
-
{robots && <meta name="robots" content="noindex, follow" />}
|
|
84
|
-
{canonical && <link rel="canonical" href={canonical} />}
|
|
85
|
-
<link rel="icon" href={withBase(CONFIG.favicon, paths)} />
|
|
86
|
-
<script is:inline set:html={themeBootstrap} />
|
|
87
|
-
<slot name="head" />
|
|
88
|
-
</head>
|
|
89
|
-
<body>
|
|
90
|
-
<slot />
|
|
91
|
-
</body>
|
|
92
|
-
</html>
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Route-level MDX component map. Entries here OVERRIDE a page's own
|
|
3
|
-
* `export const components`, so keep it small.
|
|
4
|
-
*/
|
|
5
|
-
import {
|
|
6
|
-
TableBody,
|
|
7
|
-
TableCaption,
|
|
8
|
-
TableCell,
|
|
9
|
-
TableFooter,
|
|
10
|
-
TableHead,
|
|
11
|
-
TableHeader,
|
|
12
|
-
TableRow,
|
|
13
|
-
} from '@eqtylab/equality';
|
|
14
|
-
import CONFIG from 'virtual:eqty-docs/config';
|
|
15
|
-
|
|
16
|
-
import AlertBridge from '../components/AlertBridge.astro';
|
|
17
|
-
import CodeFenceBridge from '../components/CodeFenceBridge.astro';
|
|
18
|
-
import Link from '../components/Link.astro';
|
|
19
|
-
import TableBridge from '../components/TableBridge.astro';
|
|
20
|
-
|
|
21
|
-
const base: Record<string, unknown> = {
|
|
22
|
-
a: Link,
|
|
23
|
-
|
|
24
|
-
// An explicit import of Alert from @eqtylab/equality still wins.
|
|
25
|
-
Alert: AlertBridge,
|
|
26
|
-
|
|
27
|
-
// All hook-free, so they render statically with no client directive.
|
|
28
|
-
table: TableBridge,
|
|
29
|
-
thead: TableHeader,
|
|
30
|
-
tbody: TableBody,
|
|
31
|
-
tfoot: TableFooter,
|
|
32
|
-
tr: TableRow,
|
|
33
|
-
th: TableHead,
|
|
34
|
-
td: TableCell,
|
|
35
|
-
caption: TableCaption,
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
// With 'shiki', Astro has already rendered the block.
|
|
39
|
-
if (CONFIG.code?.highlighter === 'codeblock') {
|
|
40
|
-
base.pre = CodeFenceBridge;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export const mdxComponents = base;
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
/** Bridges the content collections into the pure nav builder. Needs `astro:content`, so it lives in the runtime tree. */
|
|
2
|
-
import { breadcrumbsFor, buildNavTree, buildTocTree, prevNextFor } from '@eqtylab/docs/nav';
|
|
3
|
-
import type { GroupConfig } from '@eqtylab/docs/nav';
|
|
4
|
-
import type { DocsNavEntry, NavNode } from '@eqtylab/docs/types';
|
|
5
|
-
import { getCollection } from 'astro:content';
|
|
6
|
-
import CONFIG from 'virtual:eqty-docs/config';
|
|
7
|
-
|
|
8
|
-
export { buildTocTree, breadcrumbsFor, prevNextFor };
|
|
9
|
-
|
|
10
|
-
/** No `versionPrefix`: the version already lives in `base`, and adding it here doubles the segment. */
|
|
11
|
-
export function pathContext() {
|
|
12
|
-
return {
|
|
13
|
-
base: import.meta.env.BASE_URL,
|
|
14
|
-
pathPrefix: CONFIG.pathPrefix,
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/** All docs entries, with drafts filtered out unless we're in `astro dev`. */
|
|
19
|
-
export async function docsEntries() {
|
|
20
|
-
return getCollection('docs', ({ data }: { data: { draft?: boolean } }) =>
|
|
21
|
-
import.meta.env.DEV ? true : !data.draft
|
|
22
|
-
);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
async function groupMap(): Promise<Map<string, GroupConfig>> {
|
|
26
|
-
const map = new Map<string, GroupConfig>();
|
|
27
|
-
try {
|
|
28
|
-
const groups = await getCollection('docsGroups');
|
|
29
|
-
for (const group of groups) {
|
|
30
|
-
map.set(group.id, group.data as GroupConfig);
|
|
31
|
-
}
|
|
32
|
-
} catch {
|
|
33
|
-
// docsGroups is optional; no _group.yaml means alphabetical ordering.
|
|
34
|
-
}
|
|
35
|
-
return map;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export async function docsNav(currentPath: string): Promise<NavNode[]> {
|
|
39
|
-
const [entries, groups] = await Promise.all([docsEntries(), groupMap()]);
|
|
40
|
-
|
|
41
|
-
const navEntries: DocsNavEntry[] = entries.map((entry) => ({
|
|
42
|
-
id: entry.id,
|
|
43
|
-
filePath: entry.filePath,
|
|
44
|
-
label: entry.data.navLabel ?? entry.data.title,
|
|
45
|
-
icon: entry.data.icon,
|
|
46
|
-
badge: entry.data.badge ?? deprecationBadge(entry.data.deprecated),
|
|
47
|
-
hidden: entry.data.hidden,
|
|
48
|
-
draft: entry.data.draft,
|
|
49
|
-
}));
|
|
50
|
-
|
|
51
|
-
return buildNavTree({
|
|
52
|
-
entries: navEntries,
|
|
53
|
-
groups,
|
|
54
|
-
currentPath,
|
|
55
|
-
paths: pathContext(),
|
|
56
|
-
defaultCollapsed: CONFIG.sidebar.collapsed,
|
|
57
|
-
defaultSort: CONFIG.sidebar.sort,
|
|
58
|
-
extra: (CONFIG.sidebar.extra ?? []) as NavNode[],
|
|
59
|
-
onWarn: warnOnce,
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// The nav is rebuilt per page; report each config problem once per build.
|
|
64
|
-
const warned = new Set<string>();
|
|
65
|
-
function warnOnce(message: string) {
|
|
66
|
-
if (warned.has(message)) return;
|
|
67
|
-
warned.add(message);
|
|
68
|
-
console.warn(`[@eqtylab/docs] ${message}`);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function deprecationBadge(deprecated: unknown) {
|
|
72
|
-
if (!deprecated) return undefined;
|
|
73
|
-
return { text: 'Deprecated', variant: 'warning' as const };
|
|
74
|
-
}
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
/** Fallback page summary for pages with no `description` frontmatter. Build time only. */
|
|
2
|
-
|
|
3
|
-
const MAX_LENGTH = 140;
|
|
4
|
-
|
|
5
|
-
function isSkippable(line: string): boolean {
|
|
6
|
-
return (
|
|
7
|
-
line === '' ||
|
|
8
|
-
line.startsWith('#') ||
|
|
9
|
-
line.startsWith('import ') ||
|
|
10
|
-
line.startsWith('export ') ||
|
|
11
|
-
line.startsWith('<') ||
|
|
12
|
-
line.startsWith('|') ||
|
|
13
|
-
line.startsWith('>') ||
|
|
14
|
-
line.startsWith('- ') ||
|
|
15
|
-
line.startsWith('* ') ||
|
|
16
|
-
/^\d+\.\s/.test(line)
|
|
17
|
-
);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** Enough markdown to make one sentence readable; anything richer is not a summary anyway. */
|
|
21
|
-
function stripMarkdown(text: string): string {
|
|
22
|
-
return text
|
|
23
|
-
.replace(/!\[[^\]]*\]\([^)]*\)/g, '')
|
|
24
|
-
.replace(/\[([^\]]*)\]\([^)]*\)/g, '$1')
|
|
25
|
-
.replace(/[`*_]/g, '')
|
|
26
|
-
.replace(/<[^>]+>/g, '')
|
|
27
|
-
.replace(/\s+/g, ' ')
|
|
28
|
-
.trim();
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function truncate(text: string): string {
|
|
32
|
-
if (text.length <= MAX_LENGTH) return text;
|
|
33
|
-
const cut = text.slice(0, MAX_LENGTH);
|
|
34
|
-
const lastSpace = cut.lastIndexOf(' ');
|
|
35
|
-
return `${cut.slice(0, lastSpace > 0 ? lastSpace : MAX_LENGTH).trimEnd()}…`;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** First paragraph of prose. Frontmatter is already stripped by the content loader. */
|
|
39
|
-
export function firstParagraph(body: string | undefined): string | undefined {
|
|
40
|
-
if (!body) return undefined;
|
|
41
|
-
|
|
42
|
-
let inFence = false;
|
|
43
|
-
for (const raw of body.split('\n')) {
|
|
44
|
-
const line = raw.trim();
|
|
45
|
-
|
|
46
|
-
if (line.startsWith('```')) {
|
|
47
|
-
inFence = !inFence;
|
|
48
|
-
continue;
|
|
49
|
-
}
|
|
50
|
-
if (inFence || isSkippable(line)) continue;
|
|
51
|
-
|
|
52
|
-
const text = stripMarkdown(line);
|
|
53
|
-
// A line that was nothing but a link or a tag strips to nothing; keep looking.
|
|
54
|
-
if (text) return truncate(text);
|
|
55
|
-
}
|
|
56
|
-
return undefined;
|
|
57
|
-
}
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import CONFIG from 'virtual:eqty-docs/config';
|
|
2
|
-
|
|
3
|
-
/** These four are repeated in DocsShell.astro's inline script. Change both. */
|
|
4
|
-
export const STORAGE_KEY = 'eqty-docs-theme';
|
|
5
|
-
export const UPDATE_EVENT = 'eqty-docs-theme-change';
|
|
6
|
-
/** Equality's palette keys on this. */
|
|
7
|
-
export const THEME_ATTRIBUTE = 'data-equality-theme';
|
|
8
|
-
/** ThemeToggle.module.css reads this, so the trigger is correct before React loads. */
|
|
9
|
-
export const PREFERENCE_ATTRIBUTE = 'data-eq-theme-pref';
|
|
10
|
-
|
|
11
|
-
export type ThemePreference = 'light' | 'dark' | 'system';
|
|
12
|
-
|
|
13
|
-
export type ResolvedTheme = 'light' | 'dark';
|
|
14
|
-
|
|
15
|
-
const PREFERENCES: readonly ThemePreference[] = ['dark', 'light', 'system'];
|
|
16
|
-
|
|
17
|
-
function isPreference(value: unknown): value is ThemePreference {
|
|
18
|
-
return typeof value === 'string' && (PREFERENCES as readonly string[]).includes(value);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function readStored(): string | null {
|
|
22
|
-
if (!CONFIG.theme.persist) return window.__eqtyDocsTheme ?? null;
|
|
23
|
-
try {
|
|
24
|
-
return window.localStorage.getItem(STORAGE_KEY);
|
|
25
|
-
} catch {
|
|
26
|
-
return null;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function writeStored(preference: ThemePreference): void {
|
|
31
|
-
if (!CONFIG.theme.persist) {
|
|
32
|
-
window.__eqtyDocsTheme = preference;
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
try {
|
|
36
|
-
window.localStorage.setItem(STORAGE_KEY, preference);
|
|
37
|
-
} catch {
|
|
38
|
-
// Ignore a storage failure; the theme still applies for this page.
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function getSystemTheme(): ResolvedTheme {
|
|
43
|
-
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function getThemePreference(): ThemePreference {
|
|
47
|
-
if (typeof window === 'undefined') return 'system';
|
|
48
|
-
const stored = readStored();
|
|
49
|
-
return isPreference(stored) ? stored : 'system';
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function resolveTheme(preference: ThemePreference): ResolvedTheme {
|
|
53
|
-
return preference === 'system' ? getSystemTheme() : preference;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function applyTheme(preference: ThemePreference): void {
|
|
57
|
-
const resolved = resolveTheme(preference);
|
|
58
|
-
const root = document.documentElement;
|
|
59
|
-
root.setAttribute(THEME_ATTRIBUTE, resolved);
|
|
60
|
-
root.setAttribute(PREFERENCE_ATTRIBUTE, preference);
|
|
61
|
-
// Without this, scrollbars and form controls stay light on a dark page.
|
|
62
|
-
root.style.colorScheme = resolved;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function setThemePreference(preference: ThemePreference): void {
|
|
66
|
-
writeStored(preference);
|
|
67
|
-
applyTheme(preference);
|
|
68
|
-
window.dispatchEvent(new CustomEvent(UPDATE_EVENT, { detail: preference }));
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function subscribeToThemePreference(listener: () => void): () => void {
|
|
72
|
-
if (typeof window === 'undefined') return () => {};
|
|
73
|
-
|
|
74
|
-
const onStorage = (event: StorageEvent) => {
|
|
75
|
-
if (event.key === STORAGE_KEY) listener();
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
window.addEventListener(UPDATE_EVENT, listener);
|
|
79
|
-
window.addEventListener('storage', onStorage);
|
|
80
|
-
return () => {
|
|
81
|
-
window.removeEventListener(UPDATE_EVENT, listener);
|
|
82
|
-
window.removeEventListener('storage', onStorage);
|
|
83
|
-
};
|
|
84
|
-
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
/** Markdown twin of every page, for LLM and agent consumption. */
|
|
2
|
-
import { idToPath } from '@eqtylab/docs/paths';
|
|
3
|
-
import type { APIRoute } from 'astro';
|
|
4
|
-
import { getCollection } from 'astro:content';
|
|
5
|
-
import CONFIG from 'virtual:eqty-docs/config';
|
|
6
|
-
|
|
7
|
-
export async function getStaticPaths() {
|
|
8
|
-
const entries = await getCollection(
|
|
9
|
-
'docs',
|
|
10
|
-
({ data }: { data: { draft?: boolean; noIndex?: boolean } }) => !data.draft && !data.noIndex
|
|
11
|
-
);
|
|
12
|
-
return entries.map((entry) => ({
|
|
13
|
-
// An empty slug would emit a file literally called ".md".
|
|
14
|
-
params: { slug: idToPath(entry.id) || 'index' },
|
|
15
|
-
props: { entry },
|
|
16
|
-
}));
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export const GET: APIRoute = ({ props }) => {
|
|
20
|
-
const { entry } = props as {
|
|
21
|
-
entry: { body?: string; data: { title: string; description?: string } };
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
const frontmatter = ['---', `title: ${JSON.stringify(entry.data.title)}`];
|
|
25
|
-
if (entry.data.description) {
|
|
26
|
-
frontmatter.push(`description: ${JSON.stringify(entry.data.description)}`);
|
|
27
|
-
}
|
|
28
|
-
frontmatter.push(`source: ${JSON.stringify(CONFIG.title)}`, '---', '');
|
|
29
|
-
|
|
30
|
-
return new Response(frontmatter.join('\n') + (entry.body ?? ''), {
|
|
31
|
-
headers: { 'Content-Type': 'text/markdown; charset=utf-8' },
|
|
32
|
-
});
|
|
33
|
-
};
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
import { getCollection, render } from 'astro:content';
|
|
3
|
-
import CONFIG from 'virtual:eqty-docs/config';
|
|
4
|
-
import { docsHref, idToPath, joinPath, normalizePath } from '@eqtylab/docs/paths';
|
|
5
|
-
import DocsPage from '../layouts/DocsPage.astro';
|
|
6
|
-
import { breadcrumbsFor, buildTocTree, docsNav, pathContext, prevNextFor } from '../lib/nav-data.ts';
|
|
7
|
-
import { firstParagraph } from '../lib/summary.ts';
|
|
8
|
-
import { mdxComponents } from '../lib/mdx-components.ts';
|
|
9
|
-
import PageFooter from '../chrome/PageFooter.astro';
|
|
10
|
-
import Prose from '../chrome/Prose.astro';
|
|
11
|
-
|
|
12
|
-
export async function getStaticPaths() {
|
|
13
|
-
const entries = await getCollection('docs', ({ data }: { data: { draft?: boolean } }) =>
|
|
14
|
-
import.meta.env.DEV ? true : !data.draft
|
|
15
|
-
);
|
|
16
|
-
|
|
17
|
-
const owned = new Set((CONFIG.ownedByConsumer ?? []).map(normalizePath));
|
|
18
|
-
|
|
19
|
-
return entries
|
|
20
|
-
// Astro does not de-duplicate injected routes against consumer pages.
|
|
21
|
-
.filter((entry) => !owned.has(normalizePath(docsHref(entry.id, pathContext()))))
|
|
22
|
-
.map((entry) => ({
|
|
23
|
-
// `undefined` (not '') is how a rest param matches the empty path.
|
|
24
|
-
params: { slug: idToPath(entry.id) || undefined },
|
|
25
|
-
props: { entry },
|
|
26
|
-
}));
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const { entry } = Astro.props;
|
|
30
|
-
const { Content, headings } = await render(entry);
|
|
31
|
-
|
|
32
|
-
const tocConfig = entry.data.tableOfContents ?? CONFIG.tableOfContents;
|
|
33
|
-
const showToc = tocConfig !== false;
|
|
34
|
-
const toc = showToc
|
|
35
|
-
? buildTocTree(headings, {
|
|
36
|
-
minLevel: (tocConfig as { minLevel?: number })?.minLevel ?? 2,
|
|
37
|
-
maxLevel: (tocConfig as { maxLevel?: number })?.maxLevel ?? 3,
|
|
38
|
-
})
|
|
39
|
-
: [];
|
|
40
|
-
|
|
41
|
-
const nav = await docsNav(Astro.url.pathname);
|
|
42
|
-
const splash = entry.data.template === 'splash';
|
|
43
|
-
|
|
44
|
-
// Trail is [group, ..., page]; a length of one means the page has no section above it.
|
|
45
|
-
const trail = breadcrumbsFor(nav, Astro.url.pathname);
|
|
46
|
-
const crumbs = trail.length > 1 ? trail.map((node) => node.label).join(' › ') : undefined;
|
|
47
|
-
|
|
48
|
-
// Ancestors only. `crumbs` above keeps the page as well, because a search result row
|
|
49
|
-
// needs the page name to identify itself. The two diverge on purpose.
|
|
50
|
-
const renderedTrail = trail.slice(0, -1).map((node) => ({ label: node.label, href: node.href }));
|
|
51
|
-
|
|
52
|
-
// Indexed only, never rendered: a page with no `description` still needs a result summary.
|
|
53
|
-
const fallbackSummary = entry.data.description ? undefined : firstParagraph(entry.body);
|
|
54
|
-
|
|
55
|
-
const { prev, next } = CONFIG.footer.showPrevNext ? prevNextFor(nav, Astro.url.pathname) : {};
|
|
56
|
-
|
|
57
|
-
// `editUrl` is a base; the content path is appended. The slice is taken at the
|
|
58
|
-
// consumer's own content directory, which is configurable.
|
|
59
|
-
const contentPath = entry.filePath?.split(`${CONFIG.contentDir}/`).pop();
|
|
60
|
-
const editHref =
|
|
61
|
-
CONFIG.footer.editUrl && contentPath
|
|
62
|
-
? `${CONFIG.footer.editUrl.replace(/\/$/, '')}/${contentPath}`
|
|
63
|
-
: undefined;
|
|
64
|
-
|
|
65
|
-
// File-shaped (/x/label.md) beside the directory-shaped page; 'index' must match docs-md.ts.
|
|
66
|
-
const markdownHref = CONFIG.routing.markdownTwins
|
|
67
|
-
? `${joinPath(import.meta.env.BASE_URL, CONFIG.pathPrefix, idToPath(entry.id) || 'index')}.md`
|
|
68
|
-
: undefined;
|
|
69
|
-
---
|
|
70
|
-
|
|
71
|
-
<DocsPage
|
|
72
|
-
title={entry.data.title}
|
|
73
|
-
description={entry.data.description}
|
|
74
|
-
nav={nav}
|
|
75
|
-
toc={toc}
|
|
76
|
-
showToc={showToc}
|
|
77
|
-
splash={splash}
|
|
78
|
-
>
|
|
79
|
-
<Prose
|
|
80
|
-
title={entry.data.title}
|
|
81
|
-
description={entry.data.description}
|
|
82
|
-
deprecated={entry.data.deprecated}
|
|
83
|
-
markdownHref={markdownHref}
|
|
84
|
-
source={entry.body}
|
|
85
|
-
crumbs={crumbs}
|
|
86
|
-
trail={renderedTrail}
|
|
87
|
-
fallbackSummary={fallbackSummary}
|
|
88
|
-
>
|
|
89
|
-
<Content components={mdxComponents} />
|
|
90
|
-
</Prose>
|
|
91
|
-
{!splash && <PageFooter prev={prev} next={next} editHref={editHref} text={CONFIG.footer.text} />}
|
|
92
|
-
</DocsPage>
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
/** An index of every page, in the llms.txt convention. */
|
|
2
|
-
import { docsHref } from '@eqtylab/docs/paths';
|
|
3
|
-
import type { APIRoute } from 'astro';
|
|
4
|
-
import { getCollection } from 'astro:content';
|
|
5
|
-
import CONFIG from 'virtual:eqty-docs/config';
|
|
6
|
-
|
|
7
|
-
export const GET: APIRoute = async (ctx) => {
|
|
8
|
-
const entries = await getCollection(
|
|
9
|
-
'docs',
|
|
10
|
-
({ data }: { data: { draft?: boolean; noIndex?: boolean } }) => !data.draft && !data.noIndex
|
|
11
|
-
);
|
|
12
|
-
|
|
13
|
-
const paths = {
|
|
14
|
-
base: import.meta.env.BASE_URL,
|
|
15
|
-
pathPrefix: CONFIG.pathPrefix,
|
|
16
|
-
};
|
|
17
|
-
const origin = (ctx.site ?? new URL(ctx.request.url)).origin;
|
|
18
|
-
|
|
19
|
-
const sorted = [...entries].sort((a, b) => a.data.title.localeCompare(b.data.title));
|
|
20
|
-
|
|
21
|
-
const lines = [
|
|
22
|
-
`# ${CONFIG.title}`,
|
|
23
|
-
'',
|
|
24
|
-
...(CONFIG.description ? [`> ${CONFIG.description}`, ''] : []),
|
|
25
|
-
'## Pages',
|
|
26
|
-
'',
|
|
27
|
-
...sorted.map((entry) => {
|
|
28
|
-
const href = docsHref(entry.id, paths).replace(/\/$/, '');
|
|
29
|
-
const suffix = entry.data.description ? `: ${entry.data.description}` : '';
|
|
30
|
-
return `- [${entry.data.title}](${origin}${href}.md)${suffix}`;
|
|
31
|
-
}),
|
|
32
|
-
'',
|
|
33
|
-
];
|
|
34
|
-
|
|
35
|
-
return new Response(lines.join('\n'), {
|
|
36
|
-
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
|
|
37
|
-
});
|
|
38
|
-
};
|