@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.
Files changed (45) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +52 -10
  3. package/dist/config.js +1 -1
  4. package/dist/index.js +16 -5
  5. package/dist/index.js.map +1 -1
  6. package/package.json +2 -2
  7. package/dist/runtime/chrome/GlobalSearch.module.css +0 -26
  8. package/dist/runtime/chrome/GlobalSearch.tsx +0 -422
  9. package/dist/runtime/chrome/Header.astro +0 -105
  10. package/dist/runtime/chrome/LinkIcon.astro +0 -35
  11. package/dist/runtime/chrome/NavDrawer.astro +0 -60
  12. package/dist/runtime/chrome/NavTree.astro +0 -112
  13. package/dist/runtime/chrome/NotFoundBody.tsx +0 -12
  14. package/dist/runtime/chrome/PageFooter.astro +0 -80
  15. package/dist/runtime/chrome/Prose.astro +0 -169
  16. package/dist/runtime/chrome/Sidebar.astro +0 -21
  17. package/dist/runtime/chrome/TableOfContents.astro +0 -86
  18. package/dist/runtime/chrome/ThemeToggle.tsx +0 -85
  19. package/dist/runtime/chrome/TocElbow.astro +0 -30
  20. package/dist/runtime/chrome/TocList.astro +0 -43
  21. package/dist/runtime/components/AlertBridge.astro +0 -16
  22. package/dist/runtime/components/CodeFence.astro +0 -38
  23. package/dist/runtime/components/CodeFenceBridge.astro +0 -20
  24. package/dist/runtime/components/Link.astro +0 -21
  25. package/dist/runtime/components/TableBridge.astro +0 -18
  26. package/dist/runtime/components/index.ts +0 -2
  27. package/dist/runtime/layouts/DocsPage.astro +0 -50
  28. package/dist/runtime/layouts/DocsShell.astro +0 -92
  29. package/dist/runtime/lib/mdx-components.ts +0 -43
  30. package/dist/runtime/lib/nav-data.ts +0 -74
  31. package/dist/runtime/lib/summary.ts +0 -57
  32. package/dist/runtime/lib/theme.ts +0 -84
  33. package/dist/runtime/routes/docs-md.ts +0 -33
  34. package/dist/runtime/routes/docs.astro +0 -92
  35. package/dist/runtime/routes/llms-txt.ts +0 -38
  36. package/dist/runtime/routes/not-found.astro +0 -16
  37. package/dist/runtime/scripts/eq-copy.ts +0 -27
  38. package/dist/runtime/scripts/eq-highlight.ts +0 -24
  39. package/dist/runtime/scripts/eq-nav-drawer.ts +0 -31
  40. package/dist/runtime/scripts/eq-nav-group.ts +0 -52
  41. package/dist/runtime/scripts/eq-toc.ts +0 -166
  42. package/dist/runtime/styles/chrome.css +0 -30
  43. package/dist/runtime/styles/prose.css +0 -102
  44. package/dist/runtime/styles/theme.css +0 -2
  45. package/dist/runtime/styles/utilities.css +0 -38
@@ -1,422 +0,0 @@
1
- import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
- import {
3
- Button,
4
- Command,
5
- CommandGroup,
6
- CommandInput,
7
- CommandItem,
8
- CommandList,
9
- Dialog,
10
- DialogContainer,
11
- DialogDescription,
12
- DialogTitle,
13
- EmptyTableState,
14
- Icon,
15
- } from '@eqtylab/equality';
16
-
17
- import styles from './GlobalSearch.module.css';
18
-
19
- /** The header sits outside `Command`: moving the query there costs a hidden input and key forwarding. */
20
-
21
- /**
22
- * Equality ships unlayered CSS, and an unlayered declaration beats anything in
23
- * `@layer utilities` whatever its specificity. So any utility here that has to
24
- * overrule a value Equality already sets on the same element needs `!` — the list's
25
- * own `max-height: 300px` is one, and without the `!` the results pane silently
26
- * shrinks. Utilities setting a property Equality leaves alone need nothing.
27
- */
28
-
29
- /** Equality styles the hover but not cmdk's selected row. */
30
- const ITEM =
31
- 'cursor-pointer data-[selected=true]:bg-lilac-300/50 dark:data-[selected=true]:bg-lilac-600/50';
32
-
33
- /**
34
- * Equality has no highlight token, so these name steps off the option scale directly. Not
35
- * `brand-primary`: it is the selected row's colour, so a mark painted with it vanishes exactly
36
- * when a row is selected. Each theme names its own step because `lilac-*` carries no dark values.
37
- */
38
- const MARK = 'rounded bg-lilac-200 px-0.5 text-lilac-800 dark:bg-lilac-700 dark:text-lilac-200';
39
-
40
- /** The same treatment, for the <mark>s inside Pagefind's own excerpt markup. */
41
- const HIT_SUMMARY =
42
- 'text-text-secondary line-clamp-1 text-xs [&_mark]:rounded [&_mark]:bg-lilac-200 [&_mark]:px-0.5 [&_mark]:text-lilac-800 dark:[&_mark]:bg-lilac-700 dark:[&_mark]:text-lilac-200';
43
-
44
- const MAX_RESULTS = 20;
45
- const PAGE_SIZE = 5;
46
- const RECENT_KEY = 'eq-docs-recent-pages';
47
- const MAX_RECENT = 5;
48
-
49
- /** Hand-written: Pagefind is fetched at runtime, so no package supplies types. */
50
- interface PagefindDoc {
51
- url: string;
52
- excerpt: string;
53
- /** Stamped by Prose.astro; `crumbs` is the `_group.yaml` trail, not the URL. */
54
- meta?: { title?: string; crumbs?: string; description?: string };
55
- }
56
-
57
- interface PagefindRawResult {
58
- id: string;
59
- data: () => Promise<PagefindDoc>;
60
- }
61
-
62
- interface PagefindApi {
63
- options: (opts: Record<string, unknown>) => Promise<void>;
64
- init: () => Promise<void>;
65
- debouncedSearch: (query: string) => Promise<{ results: PagefindRawResult[] } | null>;
66
- }
67
-
68
- interface Props {
69
- suggested?: Array<{ label: string; href: string }>;
70
- }
71
-
72
- function toRecent(item: { label: string; href: string }): RecentPage {
73
- return { url: item.href, title: item.label };
74
- }
75
-
76
- interface Hit {
77
- id: string;
78
- url: string;
79
- title: string;
80
- summary: string;
81
- isExcerpt: boolean;
82
- crumbs?: string;
83
- }
84
-
85
- const BASE = import.meta.env.BASE_URL;
86
- const BUNDLE_PATH = `${BASE.replace(/\/+$/, '')}/pagefind/`;
87
-
88
- let apiPromise: Promise<PagefindApi> | null = null;
89
-
90
- /** Drop `@vite-ignore` and the build fails: this path exists only after the build runs. */
91
- function loadPagefind(): Promise<PagefindApi> {
92
- if (!apiPromise) {
93
- apiPromise = import(/* @vite-ignore */ `${BUNDLE_PATH}pagefind.js`).then(async (module) => {
94
- const api = module as unknown as PagefindApi;
95
- // Without `baseUrl` every result href misses the base on a versioned deploy.
96
- await api.options({ baseUrl: BASE, bundlePath: BUNDLE_PATH });
97
- await api.init();
98
- return api;
99
- });
100
- }
101
- return apiPromise;
102
- }
103
-
104
- interface RecentPage {
105
- url: string;
106
- title: string;
107
- crumbs?: string;
108
- }
109
-
110
- function getRecentPages(): RecentPage[] {
111
- try {
112
- const stored = localStorage.getItem(RECENT_KEY);
113
- return stored ? (JSON.parse(stored) as RecentPage[]) : [];
114
- } catch {
115
- return [];
116
- }
117
- }
118
-
119
- /** Reads the page it is on from the same attributes the index is built from. */
120
- function recordCurrentPage(): RecentPage[] {
121
- const heading = document.querySelector('h1[data-pagefind-meta="title"]');
122
- const article = document.querySelector('article[data-pagefind-body]');
123
- const title = heading?.textContent?.trim();
124
- if (!title) return getRecentPages();
125
-
126
- const meta = article?.getAttribute('data-pagefind-meta') ?? '';
127
- const crumbs = meta.startsWith('crumbs:') ? meta.slice('crumbs:'.length) : undefined;
128
- const entry: RecentPage = { url: window.location.pathname, title, crumbs };
129
-
130
- try {
131
- const kept = getRecentPages().filter((page) => page.url !== entry.url);
132
- const updated = [entry, ...kept].slice(0, MAX_RECENT);
133
- localStorage.setItem(RECENT_KEY, JSON.stringify(updated));
134
- return updated;
135
- } catch {
136
- return getRecentPages();
137
- }
138
- }
139
-
140
- function isTypingTarget(): boolean {
141
- const el = document.activeElement;
142
- if (!el) return false;
143
- const tag = el.tagName.toLowerCase();
144
- return tag === 'input' || tag === 'textarea' || (el as HTMLElement).isContentEditable;
145
- }
146
-
147
- function HighlightMatch({ text, query }: { text: string; query: string }) {
148
- if (!query) return <>{text}</>;
149
- const escaped = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
150
- const parts = text.split(new RegExp(`(${escaped})`, 'gi'));
151
- return (
152
- <>
153
- {parts.map((part, i) =>
154
- part.toLowerCase() === query.toLowerCase() ? (
155
- <mark key={i} className={MARK}>
156
- {part}
157
- </mark>
158
- ) : (
159
- part
160
- )
161
- )}
162
- </>
163
- );
164
- }
165
-
166
- export default function GlobalSearch({ suggested = [] }: Props) {
167
- const [isOpen, setIsOpen] = useState(false);
168
- const [query, setQuery] = useState('');
169
- const [hits, setHits] = useState<Hit[]>([]);
170
- const [status, setStatus] = useState<'idle' | 'loading' | 'ready' | 'error'>('idle');
171
- const [expanded, setExpanded] = useState(false);
172
- // Lazy initializer, safe only because this island is client:only and never prerenders.
173
- const [recentPages] = useState<RecentPage[]>(recordCurrentPage);
174
- const triggerRef = useRef<HTMLButtonElement>(null);
175
- const requestId = useRef(0);
176
-
177
- const open = useCallback(() => setIsOpen(true), []);
178
-
179
- const close = useCallback(() => {
180
- setIsOpen(false);
181
- setQuery('');
182
- setHits([]);
183
- setStatus('idle');
184
- setExpanded(false);
185
- requestId.current++;
186
- // DialogContainer preventDefaults onCloseAutoFocus; drop this and a keyboard user lands on <body>.
187
- requestAnimationFrame(() => triggerRef.current?.focus());
188
- }, [setExpanded]);
189
-
190
- useEffect(() => {
191
- function handler(event: KeyboardEvent) {
192
- if ((event.metaKey || event.ctrlKey) && event.key === 'k') {
193
- event.preventDefault();
194
- if (isOpen) close();
195
- else open();
196
- }
197
- // Escape is handled by Radix once the dialog is open.
198
- if (event.key === '/' && !isOpen && !isTypingTarget()) {
199
- event.preventDefault();
200
- open();
201
- }
202
- }
203
- window.addEventListener('keydown', handler);
204
- return () => window.removeEventListener('keydown', handler);
205
- }, [isOpen, open, close]);
206
-
207
- const runSearch = useCallback(async (value: string) => {
208
- const id = ++requestId.current;
209
- if (!value.trim()) {
210
- setHits([]);
211
- setStatus('idle');
212
- return;
213
- }
214
- setStatus('loading');
215
- try {
216
- const api = await loadPagefind();
217
- const search = await api.debouncedSearch(value);
218
- // Null means a newer keystroke superseded this call.
219
- if (search === null || id !== requestId.current) return;
220
- const docs = await Promise.all(search.results.slice(0, MAX_RESULTS).map((r) => r.data()));
221
- if (id !== requestId.current) return;
222
- setHits(
223
- docs.map((doc, index) => ({
224
- id: search.results[index].id,
225
- url: doc.url,
226
- title: doc.meta?.title ?? doc.url,
227
- summary: doc.meta?.description ?? doc.excerpt,
228
- isExcerpt: !doc.meta?.description,
229
- crumbs: doc.meta?.crumbs,
230
- }))
231
- );
232
- setStatus('ready');
233
- } catch {
234
- if (id !== requestId.current) return;
235
- // No index before the first build; say so rather than showing nothing.
236
- setStatus('error');
237
- setHits([]);
238
- }
239
- }, []);
240
-
241
- const visible = useMemo(() => (expanded ? hits : hits.slice(0, PAGE_SIZE)), [hits, expanded]);
242
-
243
- function onQueryChange(value: string) {
244
- setQuery(value);
245
- setExpanded(false);
246
- void runSearch(value);
247
- }
248
-
249
- function handleSelect(hit: Hit) {
250
- setIsOpen(false);
251
- // assign(), not `location.href =`: the React compiler lint rejects the assignment form.
252
- window.location.assign(hit.url);
253
- }
254
-
255
- // Excluding the current page is what makes a first visit fall through to `suggested`.
256
- const elsewhere = recentPages.filter((page) => page.url !== window.location.pathname);
257
- const landing = elsewhere.length > 0 ? elsewhere : suggested.map(toRecent);
258
- const landingHeading = elsewhere.length > 0 ? 'Recently viewed' : 'Start here';
259
- const showLanding = !query && landing.length > 0;
260
- const showEmpty = status !== 'error' && hits.length === 0 && !showLanding;
261
-
262
- return (
263
- <>
264
- {/* A button, not an Input: Enter, Space and the accessible name come free. */}
265
- <div role="search" className="w-full min-w-0 max-sm:w-auto">
266
- {/* Tokens, not a copy of Input: a theme change carries, an Input restyle does not. */}
267
- <button
268
- ref={triggerRef}
269
- type="button"
270
- className="border-border bg-background text-text-secondary focus-ring flex h-10 w-full min-w-0 cursor-pointer items-center gap-2 rounded-md border p-2 text-sm max-sm:w-10 max-sm:justify-center max-sm:border-transparent max-sm:bg-transparent max-sm:p-0"
271
- aria-haspopup="dialog"
272
- aria-label="Search documentation"
273
- onClick={open}
274
- >
275
- <Icon icon="Search" size="xs" />
276
- {/* min-w-0 so a long label truncates instead of pushing the shortcut out. */}
277
- <span className="min-w-0 flex-1 truncate text-left max-sm:sr-only">
278
- Search documentation...
279
- </span>
280
- {/* No room for it on a narrow header, and the shortcut still works. */}
281
- <kbd
282
- aria-hidden="true"
283
- className="bg-background-raised text-text-secondary pointer-events-none hidden rounded border px-1.5 py-0.5 font-mono text-xs sm:inline-block"
284
- >
285
- ⌘K
286
- </kbd>
287
- </button>
288
- </div>
289
-
290
- <Dialog open={isOpen} onOpenChange={(next: boolean) => (next ? open() : close())}>
291
- <DialogContainer className={styles.dialog} aria-describedby={undefined}>
292
- <div className="sr-only">
293
- <DialogTitle>Search documentation</DialogTitle>
294
- <DialogDescription>
295
- Type to search every page. Arrow keys move, Enter opens, Escape closes.
296
- </DialogDescription>
297
- </div>
298
-
299
- {/* Pagefind has already ranked these. Let cmdk filter and it discards the good ones. */}
300
- <Command shouldFilter={false} className="w-full border-none">
301
- <CommandInput
302
- value={query}
303
- onValueChange={onQueryChange}
304
- placeholder="Search documentation..."
305
- />
306
-
307
- <CommandList className="max-h-[min(60vh,28rem)]! overflow-y-auto pb-2">
308
- {status === 'error' && (
309
- <div className="flex flex-col items-center gap-3 px-4 py-10">
310
- <EmptyTableState icon="SearchX" title="Search needs a build to run first" />
311
- </div>
312
- )}
313
-
314
- {/* Not CommandEmpty: its filter count never updates while shouldFilter is false. */}
315
- {showEmpty && (
316
- <div className="flex flex-col items-center gap-3 px-4 py-10">
317
- <EmptyTableState
318
- icon={query ? 'SearchX' : 'Search'}
319
- title={
320
- query
321
- ? status === 'loading'
322
- ? 'Searching...'
323
- : `No results for "${query}"`
324
- : 'Start typing to search'
325
- }
326
- />
327
- {query && status !== 'loading' && (
328
- <Button variant="tertiary" size="sm" onClick={() => onQueryChange('')}>
329
- Clear search
330
- </Button>
331
- )}
332
- </div>
333
- )}
334
-
335
- {showLanding && (
336
- <CommandGroup className="px-1 [&>*+*]:mt-1" heading={landingHeading}>
337
- {landing.map((page) => (
338
- <CommandItem
339
- key={page.url}
340
- value={page.url}
341
- onSelect={() => window.location.assign(page.url)}
342
- className={ITEM}
343
- >
344
- <Icon icon="FileText" size="sm" />
345
- <span className="flex min-w-0 flex-1 flex-col gap-0.5">
346
- <span className="text-text-primary truncate text-sm font-medium">
347
- {page.title}
348
- </span>
349
- {page.crumbs && (
350
- // Location, not identity: separated from the pair above it.
351
- <span className="text-text-tertiary mt-1 truncate text-xs">
352
- {page.crumbs}
353
- </span>
354
- )}
355
- </span>
356
- </CommandItem>
357
- ))}
358
- </CommandGroup>
359
- )}
360
-
361
- {/* Flat and in Pagefind's order: grouping by section reordered the ranking. */}
362
- {hits.length > 0 && (
363
- <CommandGroup className="px-1 [&>*+*]:mt-1" heading="Results">
364
- {visible.map((hit) => (
365
- <CommandItem
366
- key={hit.id}
367
- value={hit.id}
368
- data-search-id={hit.id}
369
- onSelect={() => handleSelect(hit)}
370
- className={ITEM}
371
- >
372
- <Icon icon="FileText" size="sm" />
373
- <span className="flex min-w-0 flex-1 flex-col gap-0.5">
374
- <span className="text-text-primary truncate text-sm font-medium">
375
- <HighlightMatch text={hit.title} query={query} />
376
- </span>
377
- {hit.isExcerpt ? (
378
- // Pagefind's markup, built from our own content.
379
- <span
380
- className={HIT_SUMMARY}
381
- dangerouslySetInnerHTML={{ __html: hit.summary }}
382
- />
383
- ) : (
384
- <span className={HIT_SUMMARY}>
385
- <HighlightMatch text={hit.summary} query={query} />
386
- </span>
387
- )}
388
- {hit.crumbs && (
389
- // Location, not identity: separated from the pair above it.
390
- <span className="text-text-tertiary mt-1 truncate text-xs">
391
- {hit.crumbs}
392
- </span>
393
- )}
394
- </span>
395
- </CommandItem>
396
- ))}
397
-
398
- {hits.length > visible.length && (
399
- <CommandItem
400
- value="view-more"
401
- onSelect={() => setExpanded(true)}
402
- className="text-brand-primary cursor-pointer justify-center text-sm"
403
- >
404
- View more results
405
- </CommandItem>
406
- )}
407
- </CommandGroup>
408
- )}
409
- </CommandList>
410
-
411
- {/* Outside the list: inside it the note scrolls out of sight. */}
412
- {import.meta.env.DEV && status !== 'error' && (
413
- <p className="text-text-secondary border-border border-t px-4 py-2 text-xs">
414
- Dev mode: results come from the last build.
415
- </p>
416
- )}
417
- </Command>
418
- </DialogContainer>
419
- </Dialog>
420
- </>
421
- );
422
- }
@@ -1,105 +0,0 @@
1
- ---
2
- import CONFIG from 'virtual:eqty-docs/config';
3
- import { withBase } from '@eqtylab/docs/paths';
4
- import { Icon } from '@eqtylab/equality';
5
- import GlobalSearch from './GlobalSearch.tsx';
6
- import LinkIcon from './LinkIcon.astro';
7
- import ThemeToggle from './ThemeToggle.tsx';
8
-
9
- interface Props {
10
- /** First page of each top-level section. Shown before a reader has any history. */
11
- suggested?: Array<{ label: string; href: string }>;
12
- /** False when the page forwards a consumer-provided `search` slot. */
13
- showSearch?: boolean;
14
- }
15
-
16
- const { showSearch = true, suggested = [] } = Astro.props;
17
-
18
- const paths = { base: import.meta.env.BASE_URL, pathPrefix: CONFIG.pathPrefix };
19
- const homeHref = withBase(CONFIG.pathPrefix ? `/${CONFIG.pathPrefix}/` : '/', {
20
- base: import.meta.env.BASE_URL,
21
- });
22
-
23
- const navLink = 'eq-nav-control flex';
24
- ---
25
-
26
- <header
27
- class="bg-background-raised border-border-raised sticky top-0 z-50 h-[var(--eq-docs-header-height)] border-b"
28
- data-eq-chrome
29
- data-pagefind-ignore
30
- >
31
- {
32
- /* Below sm the search becomes an icon in the control cluster. `order` moves it
33
- among these three children, so it only works while all three stay siblings. */
34
- }
35
- <div class="flex h-full items-center justify-between gap-4 px-4 max-sm:gap-2">
36
- {/* min-h-10: the mark alone leaves this link under the 24px minimum tap target. */}
37
- <a
38
- class="text-text-primary flex min-h-10 shrink-0 items-center gap-3 no-underline"
39
- href={homeHref}
40
- >
41
- {
42
- /* Only the height is set. An <img> keeps its own aspect ratio, so any logo
43
- fits. The mark ships white, so light mode inverts it; invert only works for
44
- a one-colour logo, and a coloured one needs a second file. */
45
- }
46
- {
47
- CONFIG.logo ? (
48
- <img
49
- class="light:invert block h-[22px] w-auto shrink-0"
50
- src={withBase(CONFIG.logo.src, paths)}
51
- alt={CONFIG.logo.alt}
52
- height="24"
53
- />
54
- ) : (
55
- <span class="whitespace-nowrap text-xl font-bold">{CONFIG.title}</span>
56
- )
57
- }
58
- <span class="eq-display-gradient whitespace-nowrap text-xl font-bold forced-colors:text-inherit">
59
- {CONFIG.title}
60
- </span>
61
- </a>
62
-
63
- {/* min-w-0: without it the search pushes the theme control off a narrow header. */}
64
- <div class="flex min-w-0 max-w-[28rem] flex-1 justify-center max-sm:order-2 max-sm:ml-auto max-sm:max-w-none max-sm:flex-none">
65
- <slot name="search" />
66
- {
67
- /* client:only: with no JS there is no search, and client:load would leave a dead control. */
68
- showSearch && CONFIG.search.provider !== 'none' && <GlobalSearch suggested={suggested} client:only="react" />
69
- }
70
- </div>
71
-
72
- <div class="flex shrink-0 items-center gap-2 max-sm:order-3">
73
- <slot name="versions" />
74
- <div class="hidden items-center gap-1 lg:flex">
75
- {CONFIG.header.links.map((link) => (
76
- <a
77
- class={navLink}
78
- href={link.href}
79
- target={link.external ? '_blank' : undefined}
80
- rel={link.external ? 'noopener noreferrer' : undefined}
81
- >
82
- {link.icon && <LinkIcon icon={link.icon} />}
83
- {/* Always visible. Hiding it shipped a nameless link, because `icon` is
84
- optional and Lucide has no brand marks, so a GitHub link had nothing
85
- left to render. */}
86
- <span class="inline">{link.label}</span>
87
- </a>
88
- ))}
89
- </div>
90
- {CONFIG.header.showThemeToggle && <ThemeToggle client:load className="eq-nav-control" />}
91
- {
92
- /* popovertarget: the browser handles opening, closing, Escape, a click
93
- outside, and returning focus here. */
94
- }
95
- <button
96
- class="text-text-secondary hover:text-text-primary flex size-10 items-center justify-center rounded-md lg:hidden"
97
- type="button"
98
- popovertarget="eq-nav-drawer"
99
- aria-label="Open navigation"
100
- >
101
- <Icon icon="Menu" size="sm" />
102
- </button>
103
- </div>
104
- </div>
105
- </header>
@@ -1,35 +0,0 @@
1
- ---
2
- /**
3
- * The icon on a configured header link: a Lucide name, or a path to an SVG.
4
- *
5
- * The SVG branch exists because Lucide ships no brand marks, so a GitHub link has no
6
- * name it could use.
7
- */
8
- import CONFIG from 'virtual:eqty-docs/config';
9
- import { withBase } from '@eqtylab/docs/paths';
10
- import { Icon } from '@eqtylab/equality';
11
-
12
- interface Props {
13
- icon: string;
14
- }
15
- const { icon } = Astro.props;
16
- const isFile = icon.endsWith('.svg');
17
- const paths = { base: import.meta.env.BASE_URL, pathPrefix: CONFIG.pathPrefix };
18
- ---
19
-
20
- {
21
- /* size-4 matches Equality's Icon at `sm`, which the Lucide branch renders.
22
- The file ships white, so light mode is the theme that inverts it. */
23
- }
24
- {
25
- isFile ? (
26
- <img
27
- class="light:invert block size-4 shrink-0"
28
- src={withBase(icon, paths)}
29
- alt=""
30
- aria-hidden="true"
31
- />
32
- ) : (
33
- <Icon icon={icon} size="sm" />
34
- )
35
- }
@@ -1,60 +0,0 @@
1
- ---
2
- /**
3
- * Mobile navigation, below `lg` where the sidebar is hidden.
4
- *
5
- * The Popover API, not a custom element. With no JavaScript it opens and closes,
6
- * shuts on Escape or a click outside, returns focus to the button, and paints above
7
- * everything else. It also avoids `DialogContainer`'s focus-restoration bug.
8
- */
9
- import CONFIG from 'virtual:eqty-docs/config';
10
- import type { NavNode } from '@eqtylab/docs/types';
11
- import { Icon } from '@eqtylab/equality';
12
-
13
- import LinkIcon from './LinkIcon.astro';
14
- import NavTree from './NavTree.astro';
15
-
16
- interface Props {
17
- nodes: NavNode[];
18
- }
19
- const { nodes } = Astro.props;
20
- ---
21
-
22
- {
23
- /*
24
- A popover is `display:none` until opened, so no `hidden` is needed, and above lg
25
- the sidebar is back so it must never open. Resetting the UA's margin, border and
26
- padding is required: a popover is not a plain box.
27
-
28
- The `::backdrop` pair is the scrim. It comes with the popover, so there is no extra
29
- element and no scrim selector reaching into another package.
30
- */
31
- }
32
- <div
33
- id="eq-nav-drawer"
34
- popover
35
- class="bg-background-raised border-border-raised fixed inset-y-0 left-0 m-0 h-full w-[min(20rem,84vw)] overflow-y-auto border-r p-4 lg:hidden [&::backdrop]:bg-background/50 [&::backdrop]:backdrop-blur-[3px]"
36
- data-eq-chrome
37
- data-pagefind-ignore
38
- >
39
- <nav class="block" aria-label="Documentation">
40
- {
41
- CONFIG.header.links.map((link) => (
42
- <a
43
- class="text-text-secondary hover:text-text-primary mb-1 flex max-h-10 items-center gap-2 rounded-md px-2 py-1.5 text-sm font-medium no-underline"
44
- href={link.href}
45
- target={link.external ? '_blank' : undefined}
46
- rel={link.external ? 'noopener noreferrer' : undefined}
47
- >
48
- {link.icon && <LinkIcon icon={link.icon} />}
49
- <span>{link.label}</span>
50
- {link.external && <Icon icon="ArrowUpRight" size="xs" />}
51
- </a>
52
- ))
53
- }
54
- <NavTree nodes={nodes} />
55
- </nav>
56
- </div>
57
-
58
- <script>
59
- import '@eqtylab/docs/scripts/eq-nav-drawer.ts';
60
- </script>