@eqtylab/docs 0.3.1 → 0.3.3
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/dist/runtime/chrome/GlobalSearch.module.css +26 -0
- package/dist/runtime/chrome/GlobalSearch.tsx +422 -0
- package/dist/runtime/chrome/Header.astro +105 -0
- package/dist/runtime/chrome/LinkIcon.astro +35 -0
- package/dist/runtime/chrome/NavDrawer.astro +60 -0
- package/dist/runtime/chrome/NavTree.astro +112 -0
- package/dist/runtime/chrome/NotFoundBody.tsx +12 -0
- package/dist/runtime/chrome/PageFooter.astro +80 -0
- package/dist/runtime/chrome/Prose.astro +169 -0
- package/dist/runtime/chrome/Sidebar.astro +21 -0
- package/dist/runtime/chrome/TableOfContents.astro +86 -0
- package/dist/runtime/chrome/ThemeToggle.tsx +85 -0
- package/dist/runtime/chrome/TocElbow.astro +30 -0
- package/dist/runtime/chrome/TocList.astro +43 -0
- package/dist/runtime/components/AlertBridge.astro +16 -0
- package/dist/runtime/components/CodeFence.astro +38 -0
- package/dist/runtime/components/CodeFenceBridge.astro +20 -0
- package/dist/runtime/components/Link.astro +21 -0
- package/dist/runtime/components/TabItem.astro +16 -0
- package/dist/runtime/components/TableBridge.astro +18 -0
- package/dist/runtime/components/Tabs.astro +121 -0
- package/dist/runtime/components/TabsBridge.tsx +41 -0
- package/dist/runtime/components/index.ts +4 -0
- package/dist/runtime/layouts/DocsPage.astro +50 -0
- package/dist/runtime/layouts/DocsShell.astro +92 -0
- package/dist/runtime/lib/mdx-components.ts +49 -0
- package/dist/runtime/lib/nav-data.ts +74 -0
- package/dist/runtime/lib/summary.ts +57 -0
- package/dist/runtime/lib/theme.ts +84 -0
- package/dist/runtime/routes/docs-md.ts +33 -0
- package/dist/runtime/routes/docs.astro +92 -0
- package/dist/runtime/routes/llms-txt.ts +38 -0
- package/dist/runtime/routes/not-found.astro +16 -0
- package/dist/runtime/scripts/eq-copy.ts +27 -0
- package/dist/runtime/scripts/eq-highlight.ts +24 -0
- package/dist/runtime/scripts/eq-nav-drawer.ts +31 -0
- package/dist/runtime/scripts/eq-nav-group.ts +52 -0
- package/dist/runtime/scripts/eq-tabs.ts +114 -0
- package/dist/runtime/scripts/eq-toc.ts +166 -0
- package/dist/runtime/styles/chrome.css +30 -0
- package/dist/runtime/styles/prose.css +102 -0
- package/dist/runtime/styles/theme.css +2 -0
- package/dist/runtime/styles/utilities.css +38 -0
- package/package.json +2 -2
- package/dist/chunk-ATIOPYKE.js +0 -102
- package/dist/chunk-ATIOPYKE.js.map +0 -1
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Recursive sidebar list. Groups are <details>, so they open with no script and
|
|
4
|
+
* nothing remounts on a page load. `eq-nav-group.ts` adds the animation.
|
|
5
|
+
*
|
|
6
|
+
* Every value here comes from Equality: the dropdown item's geometry and hover, and
|
|
7
|
+
* the Separator's colour for the rails.
|
|
8
|
+
*/
|
|
9
|
+
import type { NavNode } from '@eqtylab/docs/types';
|
|
10
|
+
import { Badge, Icon } from '@eqtylab/equality';
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
nodes: NavNode[];
|
|
14
|
+
depth?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const { nodes, depth = 0 } = Astro.props;
|
|
18
|
+
const NavTree = Astro.self;
|
|
19
|
+
|
|
20
|
+
// 25px per level. A Tailwind default, not a token: Equality has no nesting value.
|
|
21
|
+
const list =
|
|
22
|
+
depth === 0 ? 'space-y-1' : 'border-border ml-3 mt-1 space-y-0.5 border-l pl-3';
|
|
23
|
+
|
|
24
|
+
// Equality's dropdown item, minus its `outline-hidden`: a link list needs keyboard
|
|
25
|
+
// focus visible, so `focus-ring` stands in.
|
|
26
|
+
const row =
|
|
27
|
+
'dropdown-item-hover focus-ring flex cursor-pointer select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm no-underline transition-colors';
|
|
28
|
+
|
|
29
|
+
// Equality has no selected-row colour, so the current page borrows the hover one.
|
|
30
|
+
// Search's selected row must keep matching it. The idle colour is left off a current
|
|
31
|
+
// row rather than overridden: two unvariated colour utilities on one element resolve
|
|
32
|
+
// by Tailwind's own order, not by the order they are written.
|
|
33
|
+
const current =
|
|
34
|
+
'bg-lilac-300/50 text-lilac-700 dark:bg-lilac-600/50 dark:text-lilac-100 font-medium shadow-sm';
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
<ul class={list} data-depth={depth}>
|
|
38
|
+
{
|
|
39
|
+
nodes.map((node) => (
|
|
40
|
+
<li class="list-none">
|
|
41
|
+
{node.kind === 'group' && node.children?.length ? (
|
|
42
|
+
<details class="block" data-eq-nav-group open={node.open}>
|
|
43
|
+
{/* pointer-focus undoes Equality's hover firing on plain `:focus`, which
|
|
44
|
+
left a clicked header filled as if it were the current page. */}
|
|
45
|
+
<summary
|
|
46
|
+
class={`${row} text-text-primary justify-between [&::-webkit-details-marker]:hidden pointer-focus:bg-transparent! pointer-focus:text-text-primary! pointer-focus:shadow-none!`}
|
|
47
|
+
>
|
|
48
|
+
<span class="flex min-w-0 flex-1 items-center gap-2 truncate">
|
|
49
|
+
{node.icon && <Icon icon={node.icon} size="sm" />}
|
|
50
|
+
{node.href ? (
|
|
51
|
+
<a
|
|
52
|
+
class="text-inherit no-underline"
|
|
53
|
+
href={node.href}
|
|
54
|
+
aria-current={node.current ? 'page' : undefined}
|
|
55
|
+
>
|
|
56
|
+
{node.label}
|
|
57
|
+
</a>
|
|
58
|
+
) : (
|
|
59
|
+
<span>{node.label}</span>
|
|
60
|
+
)}
|
|
61
|
+
{node.badge && (
|
|
62
|
+
<Badge variant={node.badge.variant} size="sm" className="shrink-0">
|
|
63
|
+
{node.badge.text}
|
|
64
|
+
</Badge>
|
|
65
|
+
)}
|
|
66
|
+
</span>
|
|
67
|
+
{/* The direct-child selector is load-bearing: a plain `group-open`
|
|
68
|
+
would also rotate a closed child's chevron inside an open parent. */}
|
|
69
|
+
<span
|
|
70
|
+
class="text-text-secondary shrink-0 transition-transform duration-200 [details[open]>summary_&]:rotate-90"
|
|
71
|
+
aria-hidden="true"
|
|
72
|
+
>
|
|
73
|
+
<Icon icon="ChevronRight" size="xs" />
|
|
74
|
+
</span>
|
|
75
|
+
</summary>
|
|
76
|
+
<NavTree nodes={node.children} depth={depth + 1} />
|
|
77
|
+
</details>
|
|
78
|
+
) : (
|
|
79
|
+
<a
|
|
80
|
+
class={`${row} ${node.current ? current : 'text-text-secondary'}`}
|
|
81
|
+
href={node.href}
|
|
82
|
+
aria-current={node.current ? 'page' : undefined}
|
|
83
|
+
data-current={node.current ? '' : undefined}
|
|
84
|
+
target={node.external ? '_blank' : undefined}
|
|
85
|
+
rel={node.external ? 'noopener noreferrer' : undefined}
|
|
86
|
+
{...(node.attrs ?? {})}
|
|
87
|
+
>
|
|
88
|
+
{node.icon && <Icon icon={node.icon} size="sm" />}
|
|
89
|
+
<span class="min-w-0 flex-1 truncate">{node.label}</span>
|
|
90
|
+
{node.badge && (
|
|
91
|
+
<Badge variant={node.badge.variant} size="sm" className="shrink-0">
|
|
92
|
+
{node.badge.text}
|
|
93
|
+
</Badge>
|
|
94
|
+
)}
|
|
95
|
+
{node.external && (
|
|
96
|
+
<span class="text-text-secondary shrink-0 opacity-60" aria-hidden="true">
|
|
97
|
+
<Icon icon="ArrowUpRight" size="xs" />
|
|
98
|
+
</span>
|
|
99
|
+
)}
|
|
100
|
+
</a>
|
|
101
|
+
)}
|
|
102
|
+
</li>
|
|
103
|
+
))
|
|
104
|
+
}
|
|
105
|
+
</ul>
|
|
106
|
+
|
|
107
|
+
{
|
|
108
|
+
/** Imported here, so one import covers both the sidebar and the drawer. */
|
|
109
|
+
}
|
|
110
|
+
<script>
|
|
111
|
+
import '@eqtylab/docs/scripts/eq-nav-group.ts';
|
|
112
|
+
</script>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { NotFound } from '@eqtylab/equality';
|
|
2
|
+
|
|
3
|
+
/** `NotFound`'s default onHomeClick ignores `base`, so pass the href explicitly. */
|
|
4
|
+
export default function NotFoundBody({ homeHref }: { homeHref: string }) {
|
|
5
|
+
return (
|
|
6
|
+
<NotFound
|
|
7
|
+
onHomeClick={() => {
|
|
8
|
+
window.location.href = homeHref;
|
|
9
|
+
}}
|
|
10
|
+
/>
|
|
11
|
+
);
|
|
12
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
---
|
|
2
|
+
/** The end of a page: where to go next, and how to fix what you just read. */
|
|
3
|
+
import type { NavNode } from '@eqtylab/docs/types';
|
|
4
|
+
import { Icon } from '@eqtylab/equality';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
prev?: NavNode;
|
|
8
|
+
next?: NavNode;
|
|
9
|
+
editHref?: string;
|
|
10
|
+
text?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const { prev, next, editHref, text } = Astro.props;
|
|
14
|
+
const hasPager = Boolean(prev?.href || next?.href);
|
|
15
|
+
|
|
16
|
+
const page = 'group inline-flex flex-col gap-1 no-underline';
|
|
17
|
+
const direction = 'text-text-tertiary inline-flex items-center gap-1.5 text-sm';
|
|
18
|
+
const pageLabel = 'text-text-secondary group-hover:text-text-primary text-base font-medium';
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
{
|
|
22
|
+
/* 96px, a step above the 64px under the page header, so the footer sits further out
|
|
23
|
+
than anything inside the prose. Capped to the prose width so it does not run under
|
|
24
|
+
the table of contents. */
|
|
25
|
+
}
|
|
26
|
+
{
|
|
27
|
+
(hasPager || editHref || text) && (
|
|
28
|
+
<footer
|
|
29
|
+
class="mt-24 max-w-[var(--eq-docs-content-max)]"
|
|
30
|
+
data-eq-chrome
|
|
31
|
+
data-pagefind-ignore
|
|
32
|
+
>
|
|
33
|
+
{hasPager && (
|
|
34
|
+
<nav class="grid grid-cols-2 gap-4" aria-label="Previous and next page">
|
|
35
|
+
{/* Both cells always, so a page with only a next link keeps it on the right. */}
|
|
36
|
+
<div>
|
|
37
|
+
{prev?.href && (
|
|
38
|
+
<a class={page} href={prev.href} rel="prev">
|
|
39
|
+
<span class={direction}>
|
|
40
|
+
<Icon icon="ArrowLeft" size="xs" />
|
|
41
|
+
Previous
|
|
42
|
+
</span>
|
|
43
|
+
<span class={pageLabel}>{prev.label}</span>
|
|
44
|
+
</a>
|
|
45
|
+
)}
|
|
46
|
+
</div>
|
|
47
|
+
<div class="text-right">
|
|
48
|
+
{next?.href && (
|
|
49
|
+
<a class={page} href={next.href} rel="next">
|
|
50
|
+
<span class={`${direction} justify-end`}>
|
|
51
|
+
Next
|
|
52
|
+
<Icon icon="ArrowRight" size="xs" />
|
|
53
|
+
</span>
|
|
54
|
+
<span class={pageLabel}>{next.label}</span>
|
|
55
|
+
</a>
|
|
56
|
+
)}
|
|
57
|
+
</div>
|
|
58
|
+
</nav>
|
|
59
|
+
)}
|
|
60
|
+
|
|
61
|
+
{/* 32px: inside the footer's own group, so tighter than the 96px around it. */}
|
|
62
|
+
{(editHref || text) && (
|
|
63
|
+
<div class="text-text-tertiary mt-8 flex flex-wrap items-center justify-center gap-x-4 gap-y-1 text-sm">
|
|
64
|
+
{editHref && (
|
|
65
|
+
<a
|
|
66
|
+
class="hover:text-text-primary inline-flex items-center gap-1.5 text-inherit no-underline"
|
|
67
|
+
href={editHref}
|
|
68
|
+
target="_blank"
|
|
69
|
+
rel="noopener noreferrer"
|
|
70
|
+
>
|
|
71
|
+
<Icon icon="Pencil" size="xs" />
|
|
72
|
+
Edit this page
|
|
73
|
+
</a>
|
|
74
|
+
)}
|
|
75
|
+
{text && <p class="m-0">{text}</p>}
|
|
76
|
+
</div>
|
|
77
|
+
)}
|
|
78
|
+
</footer>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
---
|
|
2
|
+
/** Article wrapper. `data-pagefind-body` bounds the search index to content. */
|
|
3
|
+
import { rewriteHtmlBase } from '@eqtylab/docs/internal/rehype-base-url';
|
|
4
|
+
import { Icon } from '@eqtylab/equality';
|
|
5
|
+
|
|
6
|
+
import AlertBridge from '../components/AlertBridge.astro';
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
title: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
deprecated?: boolean | { message?: string; replacedBy?: string };
|
|
12
|
+
/** URL of the page's markdown twin. Absent when `routing.markdownTwins` is off. */
|
|
13
|
+
markdownHref?: string;
|
|
14
|
+
/** The page's authored MDX, inlined for the copy action. */
|
|
15
|
+
source?: string;
|
|
16
|
+
/** Ancestor trail ending in this page, published to the search index. */
|
|
17
|
+
crumbs?: string;
|
|
18
|
+
/** Ancestors only, for the rendered breadcrumb. Deliberately excludes this page. */
|
|
19
|
+
trail?: Array<{ label: string; href?: string }>;
|
|
20
|
+
/** Stands in for `description` in the search index when a page has none. Not rendered. */
|
|
21
|
+
fallbackSummary?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const { title, description, deprecated, markdownHref, source, crumbs, trail, fallbackSummary } =
|
|
25
|
+
Astro.props;
|
|
26
|
+
|
|
27
|
+
const deprecation =
|
|
28
|
+
deprecated === true ? {} : deprecated && typeof deprecated === 'object' ? deprecated : null;
|
|
29
|
+
|
|
30
|
+
// Authored frontmatter HTML bypasses rehype entirely, so base-prefix it here.
|
|
31
|
+
const deprecationMessage = deprecation?.message
|
|
32
|
+
? rewriteHtmlBase(deprecation.message, import.meta.env.BASE_URL)
|
|
33
|
+
: undefined;
|
|
34
|
+
|
|
35
|
+
// Hand-rolled: every Button variant is coloured or filled, and this row is neither.
|
|
36
|
+
const action =
|
|
37
|
+
'text-text-secondary hover:text-text-primary flex cursor-pointer items-center gap-1.5 rounded-md px-2.5 py-1.5 text-sm font-medium no-underline transition-colors';
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
<article
|
|
41
|
+
class="eq-prose"
|
|
42
|
+
data-eq-prose
|
|
43
|
+
data-pagefind-body
|
|
44
|
+
data-pagefind-meta={crumbs ? `crumbs:${crumbs}` : undefined}
|
|
45
|
+
>
|
|
46
|
+
{
|
|
47
|
+
/* Ancestors only: ending the trail in a copy of the h1 repeats the page name in
|
|
48
|
+
two consecutive lines for a screen reader and competes with the only emphasis
|
|
49
|
+
on the page. */
|
|
50
|
+
trail && trail.length > 0 && (
|
|
51
|
+
<nav
|
|
52
|
+
class="text-text-tertiary mb-6 flex items-center text-sm"
|
|
53
|
+
data-eq-crumbs
|
|
54
|
+
data-eq-chrome
|
|
55
|
+
data-pagefind-ignore
|
|
56
|
+
aria-label="Breadcrumb"
|
|
57
|
+
>
|
|
58
|
+
{trail.map((node, index) => (
|
|
59
|
+
<Fragment>
|
|
60
|
+
{index > 0 && (
|
|
61
|
+
<span class="px-1.5" aria-hidden="true">
|
|
62
|
+
/
|
|
63
|
+
</span>
|
|
64
|
+
)}
|
|
65
|
+
{node.href ? (
|
|
66
|
+
<a
|
|
67
|
+
class="hover:text-text-secondary text-inherit no-underline hover:underline"
|
|
68
|
+
href={node.href}
|
|
69
|
+
>
|
|
70
|
+
{node.label}
|
|
71
|
+
</a>
|
|
72
|
+
) : (
|
|
73
|
+
<span>{node.label}</span>
|
|
74
|
+
)}
|
|
75
|
+
</Fragment>
|
|
76
|
+
))}
|
|
77
|
+
</nav>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
{/* 64px, a step above the 48px between sections, so the header reads as further out. */}
|
|
82
|
+
<header
|
|
83
|
+
class="mb-16"
|
|
84
|
+
data-eq-chrome
|
|
85
|
+
data-pagefind-meta={fallbackSummary ? `description:${fallbackSummary}` : undefined}
|
|
86
|
+
>
|
|
87
|
+
{
|
|
88
|
+
/* `w-fit` is load-bearing. The gradient spans the box, so at full width a short
|
|
89
|
+
title sits entirely in the gradient's flat start. */
|
|
90
|
+
}
|
|
91
|
+
<h1
|
|
92
|
+
class="eq-display-gradient w-fit text-4xl font-bold forced-colors:text-text-primary"
|
|
93
|
+
data-pagefind-meta="title"
|
|
94
|
+
>
|
|
95
|
+
{title}
|
|
96
|
+
</h1>
|
|
97
|
+
{description && (
|
|
98
|
+
<p class="text-text-secondary mt-3 text-lg/7" data-pagefind-meta="description">
|
|
99
|
+
{description}
|
|
100
|
+
</p>
|
|
101
|
+
)}
|
|
102
|
+
{
|
|
103
|
+
/**
|
|
104
|
+
* Inside the header, so one margin governs the gap to the content whether or
|
|
105
|
+
* not this row renders. The closing rule is Equality's own SectionHeading pattern.
|
|
106
|
+
*
|
|
107
|
+
* `data-pagefind-ignore` is load-bearing. Without it every page matches "copy".
|
|
108
|
+
*/
|
|
109
|
+
(source || markdownHref) && (
|
|
110
|
+
<div
|
|
111
|
+
class="border-border mt-4 flex items-center border-b pb-2"
|
|
112
|
+
data-eq-actions
|
|
113
|
+
data-eq-chrome
|
|
114
|
+
data-pagefind-ignore
|
|
115
|
+
>
|
|
116
|
+
{markdownHref && (
|
|
117
|
+
<a class={action} href={markdownHref} target="_blank">
|
|
118
|
+
<Icon icon="FileText" size="xs" />
|
|
119
|
+
View as Markdown
|
|
120
|
+
</a>
|
|
121
|
+
)}
|
|
122
|
+
{source && markdownHref && (
|
|
123
|
+
<span class="text-border-raised select-none" data-eq-action-sep aria-hidden="true">
|
|
124
|
+
|
|
|
125
|
+
</span>
|
|
126
|
+
)}
|
|
127
|
+
{source && (
|
|
128
|
+
/* Both labels render and swap on `data-copied` so the button does not resize. */
|
|
129
|
+
<button
|
|
130
|
+
type="button"
|
|
131
|
+
class={`group ${action}`}
|
|
132
|
+
data-eq-copy={source}
|
|
133
|
+
title="Copies the page source as Markdown"
|
|
134
|
+
>
|
|
135
|
+
<Icon icon="Copy" size="xs" />
|
|
136
|
+
<span class="inline-flex group-data-[copied]:hidden" data-eq-copy-idle>
|
|
137
|
+
Copy as Markdown
|
|
138
|
+
</span>
|
|
139
|
+
<span class="hidden group-data-[copied]:inline-flex" data-eq-copy-done>
|
|
140
|
+
Copied
|
|
141
|
+
</span>
|
|
142
|
+
</button>
|
|
143
|
+
)}
|
|
144
|
+
</div>
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
</header>
|
|
148
|
+
|
|
149
|
+
{
|
|
150
|
+
deprecation && (
|
|
151
|
+
<AlertBridge variant="warning" title="Deprecated">
|
|
152
|
+
{deprecationMessage ? (
|
|
153
|
+
<p set:html={deprecationMessage} />
|
|
154
|
+
) : (
|
|
155
|
+
<p>This page documents a deprecated feature.</p>
|
|
156
|
+
)}
|
|
157
|
+
</AlertBridge>
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
<slot />
|
|
162
|
+
</article>
|
|
163
|
+
|
|
164
|
+
{
|
|
165
|
+
/** Imported here too so a page with no fenced code still drives the copy button. */
|
|
166
|
+
}
|
|
167
|
+
<script>
|
|
168
|
+
import '@eqtylab/docs/scripts/eq-copy.ts';
|
|
169
|
+
</script>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { NavNode } from '@eqtylab/docs/types';
|
|
3
|
+
|
|
4
|
+
import NavTree from './NavTree.astro';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
nodes: NavNode[];
|
|
8
|
+
}
|
|
9
|
+
const { nodes } = Astro.props;
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
{/* border-raised pairs with the raised fill, as Gov Studio does. */}
|
|
13
|
+
<aside
|
|
14
|
+
class="bg-background-raised border-border-raised top-[var(--eq-docs-header-height)] hidden w-[var(--eq-docs-sidebar-width)] shrink-0 border-r lg:sticky lg:block lg:h-[calc(100vh-var(--eq-docs-header-height))] lg:overflow-y-auto"
|
|
15
|
+
data-eq-chrome
|
|
16
|
+
data-pagefind-ignore
|
|
17
|
+
>
|
|
18
|
+
<nav class="px-4 py-6" aria-label="Documentation">
|
|
19
|
+
<NavTree nodes={nodes} />
|
|
20
|
+
</nav>
|
|
21
|
+
</aside>
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
---
|
|
2
|
+
/** On-this-page navigation. SSR'd so it works without JS; <eq-toc> only adds scroll-spy. */
|
|
3
|
+
import type { TocNode } from '@eqtylab/docs/types';
|
|
4
|
+
|
|
5
|
+
import TocList from './TocList.astro';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
items: TocNode[];
|
|
9
|
+
label?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const { items, label = 'On this page' } = Astro.props;
|
|
13
|
+
|
|
14
|
+
function flatten(nodes: TocNode[]): TocNode[] {
|
|
15
|
+
return nodes.flatMap((node) => [node, ...flatten(node.children)]);
|
|
16
|
+
}
|
|
17
|
+
const slugs = flatten(items).map((n) => n.slug);
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
{
|
|
21
|
+
/* The elbow reads `--eq-toc-indent` as its width too. Change one and the rail stops
|
|
22
|
+
meeting the level below. */
|
|
23
|
+
}
|
|
24
|
+
{
|
|
25
|
+
items.length > 0 && (
|
|
26
|
+
<eq-toc
|
|
27
|
+
class="[--eq-toc-indent:0.75rem] top-[calc(var(--eq-docs-header-height)+2rem)] hidden w-[var(--eq-docs-toc-width)] shrink-0 xl:sticky xl:block xl:max-h-[calc(100vh-var(--eq-docs-header-height))] xl:overflow-y-auto"
|
|
28
|
+
data-slugs={slugs.join(',')}
|
|
29
|
+
data-eq-chrome
|
|
30
|
+
data-pagefind-ignore
|
|
31
|
+
>
|
|
32
|
+
<nav aria-labelledby="eq-toc-heading">
|
|
33
|
+
<p class="text-text-primary mb-3 text-sm font-semibold" id="eq-toc-heading">
|
|
34
|
+
{label}
|
|
35
|
+
</p>
|
|
36
|
+
<div class="relative" data-eq-toc-rails>
|
|
37
|
+
{
|
|
38
|
+
/* The lit segment. `eq-toc.ts` measures its path from the rendered rows and
|
|
39
|
+
raises the opacity once it has. With no JavaScript it stays empty and the
|
|
40
|
+
rail is simply unlit.
|
|
41
|
+
|
|
42
|
+
Absolute with no z-index paints above the rows, so the segment covers the
|
|
43
|
+
grey border. */
|
|
44
|
+
}
|
|
45
|
+
<svg
|
|
46
|
+
class="pointer-events-none absolute inset-0 h-full w-full overflow-visible [&_path]:opacity-0 [&_path]:[transition:opacity_300ms_ease]"
|
|
47
|
+
aria-hidden="true"
|
|
48
|
+
fill="none"
|
|
49
|
+
>
|
|
50
|
+
{
|
|
51
|
+
/* Keyed to the path's box, so the colour depends on how far down the rail
|
|
52
|
+
the segment has reached. Nothing recomputes as it moves.
|
|
53
|
+
|
|
54
|
+
Each theme needs its own pair: `lilac-*` has no dark values, and one
|
|
55
|
+
pair for both fails contrast. A flat lilac-600 measures 2.42:1 on dark,
|
|
56
|
+
under the 3:1 minimum. */
|
|
57
|
+
}
|
|
58
|
+
<defs>
|
|
59
|
+
<linearGradient id="eq-toc-rail" x1="0" y1="0" x2="0" y2="1">
|
|
60
|
+
<stop
|
|
61
|
+
class="[stop-color:var(--color-lilac-600)] dark:[stop-color:var(--color-lilac-300)]"
|
|
62
|
+
offset="0%"
|
|
63
|
+
/>
|
|
64
|
+
<stop
|
|
65
|
+
class="[stop-color:var(--color-lilac-400)] dark:[stop-color:var(--color-lilac-500)]"
|
|
66
|
+
offset="100%"
|
|
67
|
+
/>
|
|
68
|
+
</linearGradient>
|
|
69
|
+
</defs>
|
|
70
|
+
<path
|
|
71
|
+
data-eq-toc-rail
|
|
72
|
+
stroke="url(#eq-toc-rail)"
|
|
73
|
+
stroke-width="2"
|
|
74
|
+
stroke-linecap="round"
|
|
75
|
+
/>
|
|
76
|
+
</svg>
|
|
77
|
+
<TocList items={items} />
|
|
78
|
+
</div>
|
|
79
|
+
</nav>
|
|
80
|
+
</eq-toc>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
<script>
|
|
85
|
+
import '@eqtylab/docs/scripts/eq-toc.ts';
|
|
86
|
+
</script>
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { useSyncExternalStore } from 'react';
|
|
2
|
+
import { Icon } from '@eqtylab/equality';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
getThemePreference,
|
|
6
|
+
setThemePreference,
|
|
7
|
+
subscribeToThemePreference,
|
|
8
|
+
type ThemePreference,
|
|
9
|
+
} from '../lib/theme.ts';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* One button that cycles System, Light, Dark.
|
|
13
|
+
*
|
|
14
|
+
* Do not reduce this to the current state, or to an `aria-label`. All three render and
|
|
15
|
+
* CSS picks one, which is what keeps the button correct before React hydrates.
|
|
16
|
+
*/
|
|
17
|
+
/** System first, so it is never more than one press away. */
|
|
18
|
+
const CYCLE: ThemePreference[] = ['system', 'light', 'dark'];
|
|
19
|
+
const LABEL: Record<ThemePreference, string> = { system: 'System', light: 'Light', dark: 'Dark' };
|
|
20
|
+
const ICON: Record<ThemePreference, string> = { system: 'Monitor', light: 'Sun', dark: 'Moon' };
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Which state is showing. The root attribute picks one; with no JS it is absent, so
|
|
24
|
+
* System shows. These outrank the `invisible` default on their own, because an
|
|
25
|
+
* ancestor-qualified selector is the more specific of the two.
|
|
26
|
+
*/
|
|
27
|
+
const VISIBLE: Record<ThemePreference, string> = {
|
|
28
|
+
system: '[html:not([data-eq-theme-pref])_&]:visible [html[data-eq-theme-pref=system]_&]:visible',
|
|
29
|
+
light: '[html[data-eq-theme-pref=light]_&]:visible',
|
|
30
|
+
dark: '[html[data-eq-theme-pref=dark]_&]:visible',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const after = (preference: ThemePreference) =>
|
|
34
|
+
CYCLE[(CYCLE.indexOf(preference) + 1) % CYCLE.length];
|
|
35
|
+
|
|
36
|
+
function usePreference() {
|
|
37
|
+
return useSyncExternalStore(
|
|
38
|
+
subscribeToThemePreference,
|
|
39
|
+
getThemePreference,
|
|
40
|
+
() => 'system' as const
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface Props {
|
|
45
|
+
/** The bar's control recipe, passed by Header so this cannot drift from the links. */
|
|
46
|
+
className?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export default function ThemeToggle({ className }: Props) {
|
|
50
|
+
const preference = usePreference();
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
/*
|
|
54
|
+
* All three states share one grid cell, so the button is always as wide as the
|
|
55
|
+
* widest and the bar never shifts. `visibility`, not `display`: a hidden state
|
|
56
|
+
* must still be sized. No room for a label below lg, and hiding it outright is
|
|
57
|
+
* safe because the sentence beside it is the accessible name.
|
|
58
|
+
*/
|
|
59
|
+
<button
|
|
60
|
+
type="button"
|
|
61
|
+
className={[className, 'grid cursor-pointer max-lg:w-10 max-lg:justify-center max-lg:px-0']
|
|
62
|
+
.filter(Boolean)
|
|
63
|
+
.join(' ')}
|
|
64
|
+
onClick={() => setThemePreference(after(preference))}
|
|
65
|
+
>
|
|
66
|
+
{CYCLE.map((value) => (
|
|
67
|
+
<span
|
|
68
|
+
key={value}
|
|
69
|
+
className={`invisible col-start-1 row-start-1 flex items-center gap-2 ${VISIBLE[value]}`}
|
|
70
|
+
data-eq-pref={value}
|
|
71
|
+
>
|
|
72
|
+
<span aria-hidden="true" className="inline-flex">
|
|
73
|
+
<Icon icon={ICON[value]} size="xs" />
|
|
74
|
+
</span>
|
|
75
|
+
<span aria-hidden="true" className="text-sm max-lg:hidden">
|
|
76
|
+
{LABEL[value]}
|
|
77
|
+
</span>
|
|
78
|
+
<span className="sr-only">
|
|
79
|
+
Theme: {LABEL[value]}. Activate to switch to {LABEL[after(value)]}.
|
|
80
|
+
</span>
|
|
81
|
+
</span>
|
|
82
|
+
))}
|
|
83
|
+
</button>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* One corner of the table-of-contents rail. `in` descends a level, `out` returns.
|
|
4
|
+
*
|
|
5
|
+
* An SVG, not a CSS corner. `border-radius` arrives horizontal; the rail has to arrive
|
|
6
|
+
* vertical to meet the child's border. The 1px offset centres it on that border.
|
|
7
|
+
*
|
|
8
|
+
* `vector-effect` is load-bearing: the viewBox is stretched to the indent width, and
|
|
9
|
+
* without it the stroke stretches too.
|
|
10
|
+
*/
|
|
11
|
+
interface Props {
|
|
12
|
+
direction: 'in' | 'out';
|
|
13
|
+
}
|
|
14
|
+
const { direction } = Astro.props;
|
|
15
|
+
const d = direction === 'in' ? 'M 0 0 C 0 0.5, 1 0.5, 1 1' : 'M 1 0 C 1 0.5, 0 0.5, 0 1';
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
<svg
|
|
19
|
+
class="text-border-raised ml-px block h-2 w-[var(--eq-toc-indent)]"
|
|
20
|
+
data-eq-elbow={direction}
|
|
21
|
+
aria-hidden="true"
|
|
22
|
+
viewBox="0 0 1 1"
|
|
23
|
+
preserveAspectRatio="none"
|
|
24
|
+
fill="none"
|
|
25
|
+
stroke="currentColor"
|
|
26
|
+
stroke-width="2"
|
|
27
|
+
overflow="visible"
|
|
28
|
+
>
|
|
29
|
+
<path d={d} vector-effect="non-scaling-stroke"></path>
|
|
30
|
+
</svg>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
/** Split out so recursion repeats only the list, not the <eq-toc>, <nav>, or heading id. */
|
|
3
|
+
import type { TocNode } from '@eqtylab/docs/types';
|
|
4
|
+
|
|
5
|
+
import TocElbow from './TocElbow.astro';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
items: TocNode[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { items } = Astro.props;
|
|
12
|
+
const TocList = Astro.self;
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
{
|
|
16
|
+
/* No gap: each link's left border forms the rail, and a gap breaks it into dashes.
|
|
17
|
+
The nested-list indent is a descendant rule, so one class covers every level.
|
|
18
|
+
The border stays grey on the active row: the lit rail above is what marks it. */
|
|
19
|
+
}
|
|
20
|
+
<ol class="list-none p-0 [&_ol]:ml-[var(--eq-toc-indent)]">
|
|
21
|
+
{
|
|
22
|
+
items.map((item, index) => (
|
|
23
|
+
<li class="list-none">
|
|
24
|
+
<a
|
|
25
|
+
class="text-text-secondary hover:border-text-primary hover:text-text-primary border-border-raised data-[active]:text-text-primary block border-l-2 py-1.5 pl-3 text-sm no-underline transition-all duration-300"
|
|
26
|
+
href={`#${item.slug}`}
|
|
27
|
+
data-slug={item.slug}
|
|
28
|
+
>
|
|
29
|
+
{item.text}
|
|
30
|
+
</a>
|
|
31
|
+
{item.children.length > 0 && (
|
|
32
|
+
<Fragment>
|
|
33
|
+
<TocElbow direction="in" />
|
|
34
|
+
<TocList items={item.children} />
|
|
35
|
+
{/* Only when something follows at this level: otherwise the rail would
|
|
36
|
+
turn back out towards nothing. */}
|
|
37
|
+
{index < items.length - 1 && <TocElbow direction="out" />}
|
|
38
|
+
</Fragment>
|
|
39
|
+
)}
|
|
40
|
+
</li>
|
|
41
|
+
))
|
|
42
|
+
}
|
|
43
|
+
</ol>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Equality's `Alert`, supplied to MDX as `Alert`. Keep `as="aside"`: the default
|
|
4
|
+
* `div` carries role="alert", which screen readers announce on page load.
|
|
5
|
+
*/
|
|
6
|
+
import { Alert } from '@eqtylab/equality';
|
|
7
|
+
|
|
8
|
+
type Props = Record<string, unknown>;
|
|
9
|
+
|
|
10
|
+
const { class: className, ...rest } = Astro.props as Props & { class?: string };
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
{/* Flow spacing only; every visual property comes from Alert. */}
|
|
14
|
+
<Alert as="aside" {...rest} className={['mb-4', className].filter(Boolean).join(' ')} data-eq-chrome>
|
|
15
|
+
<slot />
|
|
16
|
+
</Alert>
|