lildocs 0.1.18 → 0.1.20

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/README.md CHANGED
@@ -31,7 +31,11 @@ disk or from any static file host.
31
31
  ## Documentation
32
32
 
33
33
  - [Getting started](https://aleclarson.github.io/lildocs/getting-started.html)
34
- - [Authoring docs](https://aleclarson.github.io/lildocs/guides/authoring.html)
34
+ - [Writing content](https://aleclarson.github.io/lildocs/features/content.html)
35
+ - [Navigation and page structure](https://aleclarson.github.io/lildocs/features/navigation.html)
36
+ - [Local search](https://aleclarson.github.io/lildocs/features/search.html)
37
+ - [Mermaid diagrams](https://aleclarson.github.io/lildocs/features/mermaid.html)
38
+ - [Generated API reference](https://aleclarson.github.io/lildocs/features/api-reference.html)
35
39
  - [GitHub Pages deployment](https://aleclarson.github.io/lildocs/guides/github-pages.html)
36
40
  - [CLI reference](https://aleclarson.github.io/lildocs/reference/cli.html)
37
41
  - [Configuration reference](https://aleclarson.github.io/lildocs/reference/configuration.html)
package/dist/cli.mjs CHANGED
@@ -4,7 +4,7 @@ import { createRequire } from "node:module";
4
4
  import { spawn } from "node:child_process";
5
5
  import { command, flag, option, optional, positional, run, string, subcommands } from "cmd-ts";
6
6
  import { existsSync, readFileSync } from "node:fs";
7
- import { access, copyFile, mkdir, mkdtemp, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
7
+ import { access, appendFile, copyFile, mkdir, mkdtemp, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
8
8
  import path from "node:path";
9
9
  import matter from "gray-matter";
10
10
  import { Lexer, Marked, Parser, Renderer, marked } from "marked";
@@ -516,7 +516,10 @@ async function frontendViteConfig(cwd, mode) {
516
516
  clearScreen: false,
517
517
  logLevel: "warn",
518
518
  plugins: [octaneRuntimeCompatibility(), octane()],
519
- optimizeDeps: { exclude: ["octane"] },
519
+ optimizeDeps: {
520
+ exclude: ["octane"],
521
+ ...mode === "dev" ? { noDiscovery: true } : {}
522
+ },
520
523
  ssr: { noExternal: [/^octane(?:$|\/)/] }
521
524
  };
522
525
  }
@@ -1711,7 +1714,8 @@ async function buildSite(options) {
1711
1714
  favicon: configOptions.favicon
1712
1715
  });
1713
1716
  const mermaid = await createMermaidRenderer({ themeConfig: themeToMermaidConfig(theme, fontResolution.themeFonts) });
1714
- const css = `${resolveThemeFontImports(theme, configOptions.fonts)}${fontResolution.css}${themeToCssVariables(theme, fontResolution.themeFonts, configOptions.link, configOptions.navigation)}\n${mermaid.css}${backgroundResolution.css}${logoResolution.css}${baseCss}`;
1717
+ const configuredCss = `${resolveThemeFontImports(theme, configOptions.fonts)}${fontResolution.css}${themeToCssVariables(theme, fontResolution.themeFonts, configOptions.link, configOptions.navigation)}\n${mermaid.css}${backgroundResolution.css}${logoResolution.css}`;
1718
+ const css = `${configuredCss}${baseCss}`;
1715
1719
  const assets = [
1716
1720
  ...fontResolution.assets,
1717
1721
  ...backgroundResolution.assets,
@@ -1758,7 +1762,7 @@ async function buildSite(options) {
1758
1762
  page,
1759
1763
  nav,
1760
1764
  pageNavigation: pageNavigation.get(page.route),
1761
- css,
1765
+ css: configuredCss,
1762
1766
  searchIndexJson,
1763
1767
  logo: logoResolution.logo,
1764
1768
  favicon: logoResolution.favicon,
@@ -1854,6 +1858,7 @@ async function startDevServer(options) {
1854
1858
  const input = await resolveInput(options.input, options.cwd, { homePagePreference: "readme-first" });
1855
1859
  const outDir = path.resolve(options.cwd, options.outDir);
1856
1860
  validateDevOutDir(options.cwd, input.docsRoot, outDir, options.outDir);
1861
+ await addGitExclude(options.cwd, outDir);
1857
1862
  await mkdir(outDir, { recursive: true });
1858
1863
  const vite = await createFrontendDevServer({
1859
1864
  cwd: options.cwd,
@@ -1948,6 +1953,56 @@ function isInside(parent, child) {
1948
1953
  const relative = path.relative(parent, child);
1949
1954
  return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
1950
1955
  }
1956
+ async function addGitExclude(cwd, outDir) {
1957
+ try {
1958
+ const repository = await findGitRepository(cwd);
1959
+ if (!repository || !isInside(repository.root, outDir)) return;
1960
+ const relativeOutDir = path.relative(repository.root, outDir);
1961
+ if (!relativeOutDir) return;
1962
+ const pattern = `${relativeOutDir.split(path.sep).join("/")}/`;
1963
+ let contents = "";
1964
+ try {
1965
+ contents = await readFile(repository.excludePath, "utf8");
1966
+ } catch (error) {
1967
+ if (error.code !== "ENOENT") throw error;
1968
+ await mkdir(path.dirname(repository.excludePath), { recursive: true });
1969
+ }
1970
+ const entries = contents.split(/\r?\n/).map((entry) => entry.trim());
1971
+ const normalizedPattern = pattern.replace(/^\/|\/$/g, "");
1972
+ if (entries.some((entry) => entry.replace(/^\/|\/$/g, "") === normalizedPattern)) return;
1973
+ const separator = contents.length > 0 && !contents.endsWith("\n") ? "\n" : "";
1974
+ await appendFile(repository.excludePath, `${separator}${pattern}\n`);
1975
+ } catch {}
1976
+ }
1977
+ async function findGitRepository(start) {
1978
+ let current = path.resolve(start);
1979
+ while (true) {
1980
+ const dotGit = path.join(current, ".git");
1981
+ try {
1982
+ const metadata = await stat(dotGit);
1983
+ let gitDir = dotGit;
1984
+ if (!metadata.isDirectory()) {
1985
+ const pointer = await readFile(dotGit, "utf8");
1986
+ const match = /^gitdir:\s*(.+)\s*$/im.exec(pointer);
1987
+ if (!match?.[1]) return;
1988
+ gitDir = path.resolve(current, match[1]);
1989
+ }
1990
+ let commonDir = gitDir;
1991
+ try {
1992
+ const pointer = (await readFile(path.join(gitDir, "commondir"), "utf8")).trim();
1993
+ commonDir = path.resolve(gitDir, pointer);
1994
+ } catch {}
1995
+ return {
1996
+ root: current,
1997
+ excludePath: path.join(commonDir, "info", "exclude")
1998
+ };
1999
+ } catch {
2000
+ const parent = path.dirname(current);
2001
+ if (parent === current) return;
2002
+ current = parent;
2003
+ }
2004
+ }
2005
+ }
1951
2006
  //#endregion
1952
2007
  //#region src/core/deploy.ts
1953
2008
  async function deployGitHubPages(options) {
@@ -34,7 +34,6 @@ export function Layout({
34
34
  clientStylePaths,
35
35
  }: LayoutProps) @{
36
36
  const transition = navigation?.transition ?? "fade";
37
- const repoIconUrl = rootRelativeUrl(page.route, "assets/github-icon.svg");
38
37
  const issueUrl = issueUrlForRepository(repositoryUrl);
39
38
  const repositoryLabel = repositoryLabelForUrl(repositoryUrl);
40
39
 
@@ -82,11 +81,13 @@ export function Layout({
82
81
  type="button"
83
82
  aria-label="Collapse sidebar"
84
83
  aria-controls="lildocs-sidebar"
84
+ aria-expanded="false"
85
85
  >
86
86
  <span
87
- class="ti ti-layout-sidebar-left-collapse"
87
+ class="sidebarCollapseIcon ti ti-layout-sidebar-left-collapse"
88
88
  aria-hidden="true"
89
89
  />
90
+ <span class="sidebarMenuIcon ti ti-menu-2" aria-hidden="true" />
90
91
  </button>
91
92
  </div>
92
93
  <div id="lildocs-search-root" class="searchBox">
@@ -108,6 +109,18 @@ export function Layout({
108
109
  pageRoute={page.route}
109
110
  />
110
111
  </nav>
112
+ @if (repositoryUrl && repositoryLabel) {
113
+ <a
114
+ class="sidebarRepoLink"
115
+ href={repositoryUrl}
116
+ aria-label="View repository on GitHub"
117
+ >
118
+ <span class="repoIcon" aria-hidden="true" />
119
+ <span>
120
+ {repositoryLabel as string}
121
+ </span>
122
+ </a>
123
+ }
111
124
  </div>
112
125
  </aside>
113
126
  <div class="sidebarFloatingControls">
@@ -142,7 +155,6 @@ export function Layout({
142
155
  headings={page.headings}
143
156
  repositoryUrl={repositoryUrl}
144
157
  repositoryLabel={repositoryLabel}
145
- repoIconUrl={repoIconUrl}
146
158
  />
147
159
  </aside>
148
160
  </div>
@@ -313,12 +325,10 @@ function Toc({
313
325
  headings,
314
326
  repositoryUrl,
315
327
  repositoryLabel,
316
- repoIconUrl,
317
328
  }: {
318
329
  headings: Heading[];
319
330
  repositoryUrl?: string;
320
331
  repositoryLabel?: string;
321
- repoIconUrl: string;
322
332
  }) @{
323
333
  const tocHeadings = headings.filter(
324
334
  (heading) => heading.depth > 1 && heading.depth < 4,
@@ -330,16 +340,23 @@ function Toc({
330
340
  <nav aria-label="Table of contents">
331
341
  @if (tocHeadings.length > 0) {
332
342
  <>
333
- <p>On this page</p>
334
- <ul>
335
- @for (const heading of tocHeadings; key heading.id) {
336
- <li class={`tocDepth${heading.depth}`}>
337
- <a href={`#${heading.id}`}>
338
- {heading.text as string}
339
- </a>
340
- </li>
341
- }
342
- </ul>
343
+ <p class="tocTitle">
344
+ <span class="ti ti-align-left" aria-hidden="true" />
345
+ On this page
346
+ </p>
347
+ <div class="tocLinks" data-toc-links>
348
+ <span class="tocRail" aria-hidden="true" />
349
+ <span class="tocVisibility" data-toc-visibility aria-hidden="true" />
350
+ <ul>
351
+ @for (const heading of tocHeadings; key heading.id) {
352
+ <li class={`tocDepth${heading.depth}`}>
353
+ <a href={`#${heading.id}`}>
354
+ {heading.text as string}
355
+ </a>
356
+ </li>
357
+ }
358
+ </ul>
359
+ </div>
343
360
  </>
344
361
  }
345
362
  @if (repositoryUrl && repositoryLabel) {
@@ -348,11 +365,7 @@ function Toc({
348
365
  href={repositoryUrl}
349
366
  aria-label="View repository on GitHub"
350
367
  >
351
- <span
352
- class="repoIcon"
353
- aria-hidden="true"
354
- style={`--ld-repo-icon: url(${repoIconUrl})`}
355
- />
368
+ <span class="repoIcon" aria-hidden="true" />
356
369
  <span>
357
370
  {repositoryLabel as string}
358
371
  </span>
@@ -73,11 +73,16 @@ export function SearchBox({
73
73
  }
74
74
 
75
75
  document.documentElement.classList.remove("sidebar-collapsed");
76
+ document.dispatchEvent(new CustomEvent("lildocs:sidebar-menu-open"));
76
77
  inputRef.current?.focus();
77
78
  setOpen(Boolean(query.trim()));
78
79
  };
79
80
  const handleShortcut = (event: KeyboardEvent) => {
80
- if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") {
81
+ const key = event.key.toLowerCase();
82
+ const hasCommandModifier = event.metaKey || event.ctrlKey;
83
+ if (
84
+ hasCommandModifier && (key === "k" || event.shiftKey && key === "f")
85
+ ) {
81
86
  event.preventDefault();
82
87
  toggleSearch();
83
88
  }
@@ -6,6 +6,7 @@ import { preloadRelativeUrl, SearchBox } from "./Search.tsrx";
6
6
  import { initSectionHighlights } from "./section-highlight.js";
7
7
  import { initSidebar } from "./sidebar.js";
8
8
  import { initTableViewer } from "./table-viewer.js";
9
+ import { initTocVisibility } from "./toc-visibility.js";
9
10
  import type { SearchEntry } from "./Search.tsrx";
10
11
 
11
12
  initCopyCode();
@@ -14,6 +15,7 @@ initNavigation();
14
15
  initSectionHighlights();
15
16
  initSidebar();
16
17
  initTableViewer();
18
+ initTocVisibility();
17
19
 
18
20
  void mountSearch();
19
21
 
@@ -1,7 +1,13 @@
1
1
  const headingSelector =
2
2
  ".content article h1[id], .content article h2[id], .content article h3[id], .content article h4[id], .content article h5[id], .content article h6[id]";
3
+ const headingLinkIcons = {
4
+ link: '<span class="ti ti-link" aria-hidden="true"></span>',
5
+ check: '<span class="ti ti-check" aria-hidden="true"></span>',
6
+ };
3
7
 
4
8
  export function initHeadingLinks() {
9
+ enhanceHeadingLinks();
10
+ document.addEventListener("lildocs:page-view", enhanceHeadingLinks);
5
11
  document.addEventListener("click", (event) => {
6
12
  if (!(event.target instanceof Element) || event.target.closest("a")) {
7
13
  return;
@@ -16,17 +22,46 @@ export function initHeadingLinks() {
16
22
  });
17
23
  }
18
24
 
25
+ function enhanceHeadingLinks() {
26
+ const headings =
27
+ document.querySelectorAll<HTMLHeadingElement>(headingSelector);
28
+
29
+ for (const heading of Array.from(headings)) {
30
+ if (heading.querySelector(".headingLinkIcon")) {
31
+ continue;
32
+ }
33
+
34
+ const icon = document.createElement("button");
35
+ icon.type = "button";
36
+ icon.className = "headingLinkIcon";
37
+ icon.setAttribute("aria-label", "Copy link to heading");
38
+ icon.title = "Copy link to heading";
39
+ icon.innerHTML = headingLinkIcons.link;
40
+ heading.append(icon);
41
+ }
42
+ }
43
+
19
44
  async function copyHeadingLink(heading: HTMLHeadingElement) {
20
45
  const url = new URL(window.location.href);
21
46
  url.hash = heading.tagName === "H1" ? "" : heading.id;
22
47
 
23
48
  try {
24
49
  await writeClipboard(url.href);
25
- heading.classList.add("headingLinkCopied");
26
- window.setTimeout(
27
- () => heading.classList.remove("headingLinkCopied"),
28
- 1600,
29
- );
50
+ const icon = heading.querySelector<HTMLButtonElement>(".headingLinkIcon");
51
+ if (!icon) {
52
+ return;
53
+ }
54
+
55
+ icon.classList.add("headingLinkCopied");
56
+ icon.innerHTML = headingLinkIcons.check;
57
+ icon.setAttribute("aria-label", "Copied");
58
+ icon.title = "Copied";
59
+ window.setTimeout(() => {
60
+ icon.classList.remove("headingLinkCopied");
61
+ icon.innerHTML = headingLinkIcons.link;
62
+ icon.setAttribute("aria-label", "Copy link to heading");
63
+ icon.title = "Copy link to heading";
64
+ }, 1600);
30
65
  } catch {}
31
66
  }
32
67
 
@@ -14,6 +14,17 @@ export function initSidebar() {
14
14
  return;
15
15
  }
16
16
 
17
+ const mobileQuery = window.matchMedia("(max-width: 860px)");
18
+
19
+ const setMenuOpen = (open: boolean) => {
20
+ document.documentElement.classList.toggle("sidebar-menu-open", open);
21
+ collapseButton.setAttribute("aria-expanded", String(open));
22
+ collapseButton.setAttribute(
23
+ "aria-label",
24
+ open ? "Close navigation menu" : "Open navigation menu",
25
+ );
26
+ };
27
+
17
28
  const setCollapsed = (collapsed: boolean) => {
18
29
  document.documentElement.classList.toggle("sidebar-collapsed", collapsed);
19
30
  collapseButton.setAttribute(
@@ -22,10 +33,61 @@ export function initSidebar() {
22
33
  );
23
34
  };
24
35
 
25
- collapseButton.addEventListener("click", () => setCollapsed(true));
36
+ const syncResponsiveState = () => {
37
+ document.documentElement.classList.remove("sidebar-menu-open");
38
+ collapseButton.setAttribute("aria-expanded", "false");
39
+ collapseButton.setAttribute(
40
+ "aria-label",
41
+ mobileQuery.matches ? "Open navigation menu" : "Collapse sidebar",
42
+ );
43
+ };
44
+
45
+ collapseButton.addEventListener("click", () => {
46
+ if (mobileQuery.matches) {
47
+ setMenuOpen(
48
+ !document.documentElement.classList.contains("sidebar-menu-open"),
49
+ );
50
+ return;
51
+ }
52
+ setCollapsed(true);
53
+ });
26
54
  expandButton.addEventListener("click", () => setCollapsed(false));
55
+ document.addEventListener("keydown", (event) => {
56
+ if (event.key === "Escape" && mobileQuery.matches) {
57
+ setMenuOpen(false);
58
+ return;
59
+ }
60
+ if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "b") {
61
+ event.preventDefault();
62
+ if (mobileQuery.matches) {
63
+ setMenuOpen(
64
+ !document.documentElement.classList.contains("sidebar-menu-open"),
65
+ );
66
+ return;
67
+ }
68
+ setCollapsed(
69
+ !document.documentElement.classList.contains("sidebar-collapsed"),
70
+ );
71
+ }
72
+ });
73
+ sidebar.addEventListener("click", (event) => {
74
+ if (
75
+ mobileQuery.matches &&
76
+ event.target instanceof Element &&
77
+ event.target.closest("a[href]")
78
+ ) {
79
+ setMenuOpen(false);
80
+ }
81
+ });
27
82
  searchButton.addEventListener("click", () => {
28
83
  setCollapsed(false);
29
84
  document.dispatchEvent(new CustomEvent("lildocs:search-toggle"));
30
85
  });
86
+ document.addEventListener("lildocs:sidebar-menu-open", () => {
87
+ if (mobileQuery.matches) {
88
+ setMenuOpen(true);
89
+ }
90
+ });
91
+ mobileQuery.addEventListener("change", syncResponsiveState);
92
+ syncResponsiveState();
31
93
  }
@@ -129,6 +129,14 @@ body {
129
129
  outline-offset: 2px;
130
130
  }
131
131
 
132
+ .sidebarMenuIcon {
133
+ display: none;
134
+ }
135
+
136
+ .sidebarRepoLink {
137
+ display: none;
138
+ }
139
+
132
140
  .sidebarFloatingControls {
133
141
  position: fixed;
134
142
  top: 24px;
@@ -202,6 +210,7 @@ html.is-animating .transition-scale {
202
210
  .content h5,
203
211
  .content h6 {
204
212
  font-family: var(--ld-font-heading);
213
+ line-height: 1.2;
205
214
  }
206
215
 
207
216
  .content article :where(h1, h2, h3, h4, h5, h6) {
@@ -210,19 +219,46 @@ html.is-animating .transition-scale {
210
219
  }
211
220
 
212
221
  .content article :where(h1, h2, h3, h4, h5, h6)[id] {
213
- cursor: copy;
222
+ cursor: pointer;
214
223
  }
215
224
 
216
- .content article :where(h1, h2, h3, h4, h5, h6).headingLinkCopied::after {
217
- margin-left: 0.6em;
218
- color: var(--ld-color-accent);
219
- content: "Link copied";
220
- font-family: var(--ld-font-body);
221
- font-size: 0.7rem;
222
- font-weight: 600;
225
+ .headingLinkIcon {
226
+ appearance: none;
227
+ margin-left: 0.35em;
228
+ border: 0;
229
+ padding: 0;
230
+ background: transparent;
231
+ color: currentColor;
232
+ cursor: pointer;
233
+ font-size: 0.7em;
234
+ line-height: 1;
235
+ opacity: 0;
236
+ transition: opacity 150ms ease;
223
237
  vertical-align: middle;
224
238
  }
225
239
 
240
+ .content
241
+ article
242
+ :where(h1, h2, h3, h4, h5, h6)[id]:is(:hover, :focus-within)
243
+ .headingLinkIcon {
244
+ opacity: 0.55;
245
+ }
246
+
247
+ .content
248
+ article
249
+ :where(h1, h2, h3, h4, h5, h6)[id]
250
+ .headingLinkIcon:is(:hover, :focus-visible) {
251
+ opacity: 1;
252
+ }
253
+
254
+ .content
255
+ article
256
+ :where(h1, h2, h3, h4, h5, h6)[id]
257
+ .headingLinkIcon.headingLinkCopied {
258
+ color: var(--ld-color-accent);
259
+ opacity: 1;
260
+ }
261
+
226
262
  .content article :where(h2, h3).sectionHighlight {
227
263
  animation: section-highlight 2.4s ease-out;
228
264
  border-radius: 6px;
@@ -248,6 +284,7 @@ html.is-animating .transition-scale {
248
284
 
249
285
  .content article p {
250
286
  margin-block: 1.25em;
287
+ line-height: 1.9;
251
288
  }
252
289
 
253
290
  .content img {
@@ -261,7 +298,7 @@ html.is-animating .transition-scale {
261
298
  }
262
299
 
263
300
  .groupBreadcrumbs {
264
- margin-block-end: -4px !important;
301
+ margin-block-end: 0px !important;
265
302
  color: var(--ld-color-accent);
266
303
  font-size: 80%;
267
304
  font-weight: 700;
@@ -269,6 +306,7 @@ html.is-animating .transition-scale {
269
306
 
270
307
  .content article li {
271
308
  margin-block: 1em;
309
+ line-height: 1.9;
272
310
  }
273
311
 
274
312
  .content article > div > ul {
@@ -294,7 +332,7 @@ html.is-animating .transition-scale {
294
332
 
295
333
  .content article h1 + blockquote {
296
334
  margin: 0 0 1.5em;
297
- margin-top: -1.1em;
335
+ margin-top: -0.6rem;
298
336
  padding: 0;
299
337
  border-left: 0;
300
338
  border-radius: 0;
@@ -308,6 +346,10 @@ html.is-animating .transition-scale {
308
346
  line-height: 1.45;
309
347
  }
310
348
 
349
+ .content article h1 + blockquote > p {
350
+ line-height: 1.6;
351
+ }
352
+
311
353
  .content blockquote > :first-child {
312
354
  margin-top: 0;
313
355
  }
@@ -365,13 +407,24 @@ html.is-animating .transition-scale {
365
407
 
366
408
  .navList a,
367
409
  .navDisclosure summary {
368
- padding-left: 7px;
369
- border-left: 1px solid
370
- color-mix(in srgb, var(--ld-color-border) 65%, transparent);
410
+ position: relative;
411
+ padding-left: 9px;
371
412
  color: var(--ld-color-muted-text);
372
413
  text-decoration: none;
373
414
  }
374
415
 
416
+ .navList a::before,
417
+ .navDisclosure summary::before {
418
+ position: absolute;
419
+ top: -1px;
420
+ bottom: -1px;
421
+ left: 0;
422
+ width: 2px;
423
+ border-radius: 1px;
424
+ background: color-mix(in srgb, var(--ld-color-border) 65%, transparent);
425
+ content: "";
426
+ }
427
+
375
428
  .navDisclosure summary {
376
429
  display: flex;
377
430
  align-items: center;
@@ -410,11 +463,15 @@ html.is-animating .transition-scale {
410
463
 
411
464
  .navList a.active,
412
465
  .navDisclosure summary.active {
413
- border-left-color: var(--ld-color-accent);
414
466
  color: var(--ld-color-accent);
415
467
  font-weight: 550;
416
468
  }
417
469
 
470
+ .navList a.active::before,
471
+ .navDisclosure summary.active::before {
472
+ background: var(--ld-color-accent);
473
+ }
474
+
418
475
  .content pre {
419
476
  position: relative;
420
477
  overflow: auto;
@@ -741,11 +798,55 @@ html.mermaidFullscreenOpen {
741
798
  font-weight: 700;
742
799
  }
743
800
 
744
- .toc ul {
801
+ .tocTitle {
802
+ display: flex;
803
+ align-items: center;
804
+ gap: 7px;
805
+ }
806
+
807
+ .tocLinks {
808
+ position: relative;
809
+ margin-block: 1em;
810
+ margin-left: 5px;
811
+ }
812
+
813
+ .tocLinks ul {
745
814
  display: grid;
746
815
  gap: 9px;
816
+ margin: 0;
747
817
  list-style: none;
748
- padding-left: 0;
818
+ padding-left: 16px;
819
+ }
820
+
821
+ .tocRail,
822
+ .tocVisibility {
823
+ position: absolute;
824
+ top: 0;
825
+ left: 0;
826
+ width: 2px;
827
+ border-radius: 999px;
828
+ pointer-events: none;
829
+ }
830
+
831
+ .tocRail {
832
+ bottom: 0;
833
+ background: var(--ld-color-border);
834
+ }
835
+
836
+ .tocVisibility {
837
+ top: var(--ld-toc-visibility-top, 0);
838
+ z-index: 1;
839
+ height: var(--ld-toc-visibility-height, 0);
840
+ background: var(--ld-color-accent);
841
+ opacity: 0;
842
+ transition:
843
+ top 180ms var(--ld-navigation-easing),
844
+ height 180ms var(--ld-navigation-easing),
845
+ opacity 120ms ease;
846
+ }
847
+
848
+ .tocVisibility.isVisible {
849
+ opacity: 1;
749
850
  }
750
851
 
751
852
  .toc a {
@@ -762,8 +863,8 @@ html.mermaidFullscreenOpen {
762
863
  width: 20px;
763
864
  height: 20px;
764
865
  background: currentColor;
765
- mask: var(--ld-repo-icon) center / contain no-repeat;
766
- -webkit-mask: var(--ld-repo-icon) center / contain no-repeat;
866
+ mask: url("./github-icon.svg") center / contain no-repeat;
867
+ -webkit-mask: url("./github-icon.svg") center / contain no-repeat;
767
868
  }
768
869
 
769
870
  .searchBox {
@@ -980,19 +1081,102 @@ html.mermaidFullscreenOpen {
980
1081
  }
981
1082
  }
982
1083
 
1084
+ @media (prefers-reduced-motion: reduce) {
1085
+ .tocVisibility {
1086
+ transition: none;
1087
+ }
1088
+ }
1089
+
983
1090
  @media (max-width: 860px) {
1091
+ html.sidebar-menu-open,
1092
+ html.sidebar-menu-open body {
1093
+ overflow: hidden;
1094
+ }
1095
+
984
1096
  .pageShell {
985
1097
  display: block;
986
1098
  }
987
1099
 
1100
+ .content {
1101
+ padding-block-start: 25px;
1102
+ }
1103
+
988
1104
  .sidebar {
989
- position: static;
1105
+ position: sticky;
1106
+ top: 0;
1107
+ z-index: 30;
990
1108
  height: auto;
991
1109
  max-height: none;
1110
+ overflow: visible;
1111
+ padding: 0;
992
1112
  border-bottom: 1px solid var(--ld-color-border);
993
1113
  border-right: 0;
994
1114
  }
995
1115
 
1116
+ .sidebarContents {
1117
+ display: block;
1118
+ }
1119
+
1120
+ .sidebarHeader {
1121
+ min-height: 76px;
1122
+ padding: 20px;
1123
+ background: var(--ld-color-sidebar-background);
1124
+ }
1125
+
1126
+ .sidebarCollapseIcon {
1127
+ display: none;
1128
+ }
1129
+
1130
+ .sidebarMenuIcon {
1131
+ display: block;
1132
+ }
1133
+
1134
+ .sidebarContents > .searchBox,
1135
+ #lildocs-sidebar-navigation {
1136
+ display: none;
1137
+ }
1138
+
1139
+ html.sidebar-menu-open .sidebar {
1140
+ position: fixed;
1141
+ inset: 0;
1142
+ width: 100%;
1143
+ height: 100dvh;
1144
+ max-height: 100dvh;
1145
+ overflow: auto;
1146
+ overscroll-behavior: contain;
1147
+ border-bottom: 0;
1148
+ }
1149
+
1150
+ html.sidebar-menu-open .sidebarHeader {
1151
+ position: sticky;
1152
+ top: 0;
1153
+ z-index: 1;
1154
+ border-bottom: 1px solid var(--ld-color-border);
1155
+ }
1156
+
1157
+ html.sidebar-menu-open .sidebarContents > .searchBox {
1158
+ display: block;
1159
+ margin: 20px;
1160
+ }
1161
+
1162
+ html.sidebar-menu-open #lildocs-sidebar-navigation {
1163
+ display: block;
1164
+ padding: 0 20px 32px;
1165
+ }
1166
+
1167
+ html.sidebar-menu-open .sidebarRepoLink {
1168
+ display: flex;
1169
+ align-items: center;
1170
+ gap: 7px;
1171
+ margin: 0 20px 32px;
1172
+ padding-top: 16px;
1173
+ border-top: 1px solid var(--ld-color-border);
1174
+ color: var(--ld-color-text);
1175
+ font-size: 0.8rem;
1176
+ font-weight: 600;
1177
+ text-decoration: none;
1178
+ }
1179
+
996
1180
  .searchResults {
997
1181
  position: absolute;
998
1182
  top: calc(100% + 8px);
@@ -1009,8 +1193,6 @@ html.mermaidFullscreenOpen {
1009
1193
  }
1010
1194
 
1011
1195
  .toc {
1012
- position: static;
1013
- max-height: none;
1014
- border-bottom: 1px solid var(--ld-color-border);
1196
+ display: none;
1015
1197
  }
1016
1198
  }
@@ -17,6 +17,18 @@
17
17
  --ld-tabler-icon: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20%3E%3Cpath%20stroke%3D%22none%22%20d%3D%22M0%200h24v24H0z%22%20fill%3D%22none%22%20%2F%3E%3Cpath%20d%3D%22M3%2010a7%207%200%201%200%2014%200a7%207%200%201%200%20-14%200%22%20%2F%3E%3Cpath%20d%3D%22M21%2021l-6%20-6%22%20%2F%3E%3C%2Fsvg%3E");
18
18
  }
19
19
 
20
+ .ti-link {
21
+ --ld-tabler-icon: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M9%2015l6%20-6%22%2F%3E%3Cpath%20d%3D%22M11%206l.463%20-.536a5%205%200%200%201%207.071%207.072l-.534%20.464%22%2F%3E%3Cpath%20d%3D%22M13%2018l-.397%20.534a5.068%205.068%200%200%201%20-7.127%200a4.972%204.972%200%200%201%200%20-7.071l.524%20-.463%22%2F%3E%3C%2Fsvg%3E");
22
+ }
23
+
24
+ .ti-check {
25
+ --ld-tabler-icon: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M5%2012l5%205l10%20-10%22%2F%3E%3C%2Fsvg%3E");
26
+ }
27
+
28
+ .ti-align-left {
29
+ --ld-tabler-icon: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M4%206l16%200%22%2F%3E%3Cpath%20d%3D%22M4%2012l10%200%22%2F%3E%3Cpath%20d%3D%22M4%2018l14%200%22%2F%3E%3C%2Fsvg%3E");
30
+ }
31
+
20
32
  .ti-arrows-maximize {
21
33
  --ld-tabler-icon: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M16%204l4%200l0%204%22%2F%3E%3Cpath%20d%3D%22M14%2010l6%20-6%22%2F%3E%3Cpath%20d%3D%22M8%2020l-4%200l0%20-4%22%2F%3E%3Cpath%20d%3D%22M4%2020l6%20-6%22%2F%3E%3Cpath%20d%3D%22M16%2020l4%200l0%20-4%22%2F%3E%3Cpath%20d%3D%22M14%2014l6%206%22%2F%3E%3Cpath%20d%3D%22M8%204l-4%200l0%204%22%2F%3E%3Cpath%20d%3D%22M4%204l6%206%22%2F%3E%3C%2Fsvg%3E");
22
34
  }
@@ -33,6 +45,10 @@
33
45
  --ld-tabler-icon: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20stroke%3D%22none%22%20d%3D%22M0%200h24v24H0z%22%20fill%3D%22none%22%2F%3E%3Cpath%20d%3D%22M4%206a2%202%200%200%201%202%20-2h12a2%202%200%200%201%202%202v12a2%202%200%200%201%20-2%202h-12a2%202%200%200%201%20-2%20-2l0%20-12%22%2F%3E%3Cpath%20d%3D%22M9%204v16%22%2F%3E%3Cpath%20d%3D%22M14%2010l2%202l-2%202%22%2F%3E%3C%2Fsvg%3E");
34
46
  }
35
47
 
48
+ .ti-menu-2 {
49
+ --ld-tabler-icon: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20stroke%3D%22none%22%20d%3D%22M0%200h24v24H0z%22%20fill%3D%22none%22%2F%3E%3Cpath%20d%3D%22M4%206l16%200%22%2F%3E%3Cpath%20d%3D%22M4%2012l16%200%22%2F%3E%3Cpath%20d%3D%22M4%2018l16%200%22%2F%3E%3C%2Fsvg%3E");
50
+ }
51
+
36
52
  .ti-copy {
37
53
  --ld-tabler-icon: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20%3E%3Cpath%20stroke%3D%22none%22%20d%3D%22M0%200h24v24H0z%22%20fill%3D%22none%22%20%2F%3E%3Cpath%20d%3D%22M7%209.667a2.667%202.667%200%200%201%202.667%20-2.667h8.666a2.667%202.667%200%200%201%202.667%202.667v8.666a2.667%202.667%200%200%201%20-2.667%202.667h-8.666a2.667%202.667%200%200%201%20-2.667%20-2.667l0%20-8.666%22%20%2F%3E%3Cpath%20d%3D%22M4.012%2016.737a2.005%202.005%200%200%201%20-1.012%20-1.737v-10c0%20-1.1%20.9%20-2%202%20-2h10c.75%200%201.158%20.385%201.5%201%22%20%2F%3E%3C%2Fsvg%3E");
38
54
  }
@@ -0,0 +1,78 @@
1
+ let animationFrame: number | undefined;
2
+
3
+ export function initTocVisibility() {
4
+ window.addEventListener("scroll", scheduleTocVisibilityUpdate, {
5
+ passive: true,
6
+ });
7
+ window.addEventListener("resize", scheduleTocVisibilityUpdate);
8
+ document.addEventListener("lildocs:page-view", scheduleTocVisibilityUpdate);
9
+ void document.fonts?.ready.then(scheduleTocVisibilityUpdate);
10
+ scheduleTocVisibilityUpdate();
11
+ }
12
+
13
+ function scheduleTocVisibilityUpdate() {
14
+ if (animationFrame !== undefined) {
15
+ return;
16
+ }
17
+
18
+ animationFrame = window.requestAnimationFrame(() => {
19
+ animationFrame = undefined;
20
+ updateTocVisibility();
21
+ });
22
+ }
23
+
24
+ function updateTocVisibility() {
25
+ const container = document.querySelector<HTMLElement>("[data-toc-links]");
26
+ const indicator = container?.querySelector<HTMLElement>(
27
+ "[data-toc-visibility]",
28
+ );
29
+ if (!container || !indicator) {
30
+ return;
31
+ }
32
+
33
+ const items = Array.from(
34
+ container.querySelectorAll<HTMLAnchorElement>('a[href^="#"]'),
35
+ )
36
+ .map((link) => {
37
+ const heading = headingForLink(link);
38
+ return heading ? { heading, link } : undefined;
39
+ })
40
+ .filter((item): item is { heading: HTMLElement; link: HTMLAnchorElement } =>
41
+ Boolean(item),
42
+ );
43
+ const visibleItems = items.filter(({ heading }) => {
44
+ const bounds = heading.getBoundingClientRect();
45
+ return bounds.bottom > 0 && bounds.top < window.innerHeight;
46
+ });
47
+ if (visibleItems.length === 0 || visibleItems.length === items.length) {
48
+ indicator.classList.remove("isVisible");
49
+ return;
50
+ }
51
+
52
+ const containerBounds = container.getBoundingClientRect();
53
+ const firstBounds = visibleItems[0].link.getBoundingClientRect();
54
+ const lastBounds =
55
+ visibleItems.at(-1)?.link.getBoundingClientRect() ?? firstBounds;
56
+ indicator.style.setProperty(
57
+ "--ld-toc-visibility-top",
58
+ `${firstBounds.top - containerBounds.top}px`,
59
+ );
60
+ indicator.style.setProperty(
61
+ "--ld-toc-visibility-height",
62
+ `${lastBounds.bottom - firstBounds.top}px`,
63
+ );
64
+ indicator.classList.add("isVisible");
65
+ }
66
+
67
+ function headingForLink(link: HTMLAnchorElement): HTMLElement | null {
68
+ const id = link.hash.slice(1);
69
+ if (!id) {
70
+ return null;
71
+ }
72
+
73
+ try {
74
+ return document.getElementById(decodeURIComponent(id));
75
+ } catch {
76
+ return document.getElementById(id);
77
+ }
78
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lildocs",
3
- "version": "0.1.18",
3
+ "version": "0.1.20",
4
4
  "description": "A lightweight CLI that turns Markdown docs into a static searchable documentation site.",
5
5
  "homepage": "https://aleclarson.github.io/lildocs/",
6
6
  "repository": {