pagefind integration and search bar
This commit is contained in:
		
							parent
							
								
									a787497feb
								
							
						
					
					
						commit
						cd1226e333
					
				
					 19 changed files with 565 additions and 240 deletions
				
			
		| 
						 | 
					@ -4,3 +4,12 @@ Documentation for [quickshell](https://git.outfoxxed.me/outfoxxed/quickshell)
 | 
				
			||||||
Hosted version at [quickshell.outfoxxed.me](https://quickshell.outfoxxed.me)
 | 
					Hosted version at [quickshell.outfoxxed.me](https://quickshell.outfoxxed.me)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Frontend rewritten by [Xanazf](https://github.com/Xanazf)
 | 
					Frontend rewritten by [Xanazf](https://github.com/Xanazf)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Notes for future updates
 | 
				
			||||||
 | 
					- improve Head
 | 
				
			||||||
 | 
					- improve light theme
 | 
				
			||||||
 | 
					- QtQML docs search
 | 
				
			||||||
 | 
					- page metadata:
 | 
				
			||||||
 | 
					  - lastUpdatedAt
 | 
				
			||||||
 | 
					  - prevUpdate?
 | 
				
			||||||
 | 
					  - editURL
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,7 +4,7 @@ import { remarkAlert } from "remark-github-blockquote-alert";
 | 
				
			||||||
import sectionize from "@hbsnow/rehype-sectionize";
 | 
					import sectionize from "@hbsnow/rehype-sectionize";
 | 
				
			||||||
import mdx from "@astrojs/mdx";
 | 
					import mdx from "@astrojs/mdx";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import pagefind from "astro-pagefind";
 | 
					import pagefind from "./pagefind";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// https://astro.build/config
 | 
					// https://astro.build/config
 | 
				
			||||||
export default defineConfig({
 | 
					export default defineConfig({
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,10 +14,10 @@
 | 
				
			||||||
    "@astrojs/check": "^0.9.3",
 | 
					    "@astrojs/check": "^0.9.3",
 | 
				
			||||||
    "@astrojs/mdx": "^3.1.7",
 | 
					    "@astrojs/mdx": "^3.1.7",
 | 
				
			||||||
    "@astrojs/solid-js": "^4.4.2",
 | 
					    "@astrojs/solid-js": "^4.4.2",
 | 
				
			||||||
 | 
					    "@pagefind/default-ui": "^1.1.1",
 | 
				
			||||||
    "@types/node": "^20.14.11",
 | 
					    "@types/node": "^20.14.11",
 | 
				
			||||||
    "astro": "^4.15.9",
 | 
					    "astro": "^4.15.9",
 | 
				
			||||||
    "astro-breadcrumbs": "^2.3.1",
 | 
					    "astro-breadcrumbs": "^2.3.1",
 | 
				
			||||||
    "astro-pagefind": "^1.6.0",
 | 
					 | 
				
			||||||
    "marked": "^14.1.0",
 | 
					    "marked": "^14.1.0",
 | 
				
			||||||
    "marked-alert": "^2.0.2",
 | 
					    "marked-alert": "^2.0.2",
 | 
				
			||||||
    "node": "npm:22.7.0",
 | 
					    "node": "npm:22.7.0",
 | 
				
			||||||
| 
						 | 
					@ -31,6 +31,7 @@
 | 
				
			||||||
    "@astrojs/ts-plugin": "^1.10.2",
 | 
					    "@astrojs/ts-plugin": "^1.10.2",
 | 
				
			||||||
    "@biomejs/biome": "^1.8.3",
 | 
					    "@biomejs/biome": "^1.8.3",
 | 
				
			||||||
    "@hbsnow/rehype-sectionize": "^1.0.7",
 | 
					    "@hbsnow/rehype-sectionize": "^1.0.7",
 | 
				
			||||||
 | 
					    "pagefind": "^1.1.1",
 | 
				
			||||||
    "shiki": "^1.11.0"
 | 
					    "shiki": "^1.11.0"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "packageManager": "yarn@4.5.0"
 | 
					  "packageManager": "yarn@4.5.0"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										28
									
								
								pagefind.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								pagefind.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,28 @@
 | 
				
			||||||
 | 
					import type { AstroIntegration } from "astro";
 | 
				
			||||||
 | 
					import { fileURLToPath } from "node:url";
 | 
				
			||||||
 | 
					import { spawn } from "node:child_process";
 | 
				
			||||||
 | 
					import { dirname, relative } from "node:path";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function pagefind(): AstroIntegration {
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    name: "pagefind",
 | 
				
			||||||
 | 
					    hooks: {
 | 
				
			||||||
 | 
					      "astro:build:done": ({ dir }) => {
 | 
				
			||||||
 | 
					        const targetDir = fileURLToPath(dir);
 | 
				
			||||||
 | 
					        const cwd = dirname(fileURLToPath(import.meta.url));
 | 
				
			||||||
 | 
					        const relativeDir = relative(cwd, targetDir);
 | 
				
			||||||
 | 
					        return new Promise<void>(resolve => {
 | 
				
			||||||
 | 
					          spawn(
 | 
				
			||||||
 | 
					            "yarn dlx",
 | 
				
			||||||
 | 
					            ["-q", "pagefind", "--site", relativeDir],
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					              stdio: "inherit",
 | 
				
			||||||
 | 
					              shell: true,
 | 
				
			||||||
 | 
					              cwd,
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          ).on("close", () => resolve());
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										12
									
								
								src/components/iconsModule.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/components/iconsModule.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,12 @@
 | 
				
			||||||
 | 
					const magnifier =
 | 
				
			||||||
 | 
					  "M232.49 215.51L185 168a92.12 92.12 0 1 0-17 17l47.53 47.54a12 12 0 0 0 17-17ZM44 112a68 68 0 1 1 68 68a68.07 68.07 0 0 1-68-68";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function getHTMLIcon(name: string): string {
 | 
				
			||||||
 | 
					  const hashmap = {
 | 
				
			||||||
 | 
					    magnifier: () => magnifier,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return hashmap[name as keyof typeof hashmap]();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export { getHTMLIcon };
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,154 @@
 | 
				
			||||||
---
 | 
					---
 | 
				
			||||||
import SearchComponent from "./search";
 | 
					import "@pagefind/default-ui/css/ui.css";
 | 
				
			||||||
 | 
					import { getHTMLIcon } from "../iconsModule";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const magnifier = getHTMLIcon("magnifier");
 | 
				
			||||||
---
 | 
					---
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div class="search-wrapper">
 | 
					<site-search class="search-wrapper">
 | 
				
			||||||
  <SearchComponent client:only={"solid-js"}/>
 | 
					  <button
 | 
				
			||||||
</div>
 | 
					    data-open-modal
 | 
				
			||||||
 | 
					    disabled
 | 
				
			||||||
 | 
					    aria-label="Search"
 | 
				
			||||||
 | 
					    aria-keyshortcuts="Control+K"
 | 
				
			||||||
 | 
					    class="search-button"
 | 
				
			||||||
 | 
					  >
 | 
				
			||||||
 | 
					    <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256"><path fill="currentColor" d={magnifier}/></svg>
 | 
				
			||||||
 | 
					    <span class="search-label" aria-hidden="true">Search</span>
 | 
				
			||||||
 | 
					    <kbd class="search-kbd">
 | 
				
			||||||
 | 
					      <kbd>Ctrl</kbd><kbd>K</kbd>
 | 
				
			||||||
 | 
					    </kbd>
 | 
				
			||||||
 | 
					  </button>
 | 
				
			||||||
 | 
					  <dialog aria-label="Search" class="search-dialog">
 | 
				
			||||||
 | 
					    <div class="dialog-frame">
 | 
				
			||||||
 | 
					      <button data-close-modal class="search-cancel">
 | 
				
			||||||
 | 
					        Cancel
 | 
				
			||||||
 | 
					      </button>
 | 
				
			||||||
 | 
					      <div class="search-container">
 | 
				
			||||||
 | 
					        <div id="qs_search"/>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </dialog>
 | 
				
			||||||
 | 
					</site-search>
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * NOTE: YOINKED FROM STARLIGHT
 | 
				
			||||||
 | 
						 *  This is intentionally inlined to avoid briefly showing an invalid shortcut.
 | 
				
			||||||
 | 
						 *  Purposely using the deprecated `navigator.platform` property to detect Apple devices, as the
 | 
				
			||||||
 | 
						 *  user agent is spoofed by some browsers when opening the devtools.
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					<script is:inline>
 | 
				
			||||||
 | 
						(() => {
 | 
				
			||||||
 | 
							const openBtn = document.querySelector('button[data-open-modal]');
 | 
				
			||||||
 | 
							const shortcut = openBtn?.querySelector('kbd');
 | 
				
			||||||
 | 
							if (!openBtn || !(shortcut instanceof HTMLElement)) return;
 | 
				
			||||||
 | 
							const platformKey = shortcut.querySelector('kbd');
 | 
				
			||||||
 | 
							if (platformKey && /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform)) {
 | 
				
			||||||
 | 
								platformKey.textContent = '⌘';
 | 
				
			||||||
 | 
								openBtn.setAttribute('aria-keyshortcuts', 'Meta+K');
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							shortcut.style.display = '';
 | 
				
			||||||
 | 
						})();
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script>
 | 
				
			||||||
 | 
					import { getQMLTypeLinkObject, getQMLTypeLink, getIconForLink } from '@src/config/io/helpers';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						class SiteSearch extends HTMLElement {
 | 
				
			||||||
 | 
							constructor() {
 | 
				
			||||||
 | 
								super();
 | 
				
			||||||
 | 
								const openBtn = this.querySelector<HTMLButtonElement>('button[data-open-modal]')!;
 | 
				
			||||||
 | 
								const closeBtn = this.querySelector<HTMLButtonElement>('button[data-close-modal]')!;
 | 
				
			||||||
 | 
								const dialog = this.querySelector('dialog')!;
 | 
				
			||||||
 | 
								const dialogFrame = this.querySelector('.dialog-frame')!;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								/** Close the modal if a user clicks on a link or outside of the modal. */
 | 
				
			||||||
 | 
								const onClick = (event: MouseEvent) => {
 | 
				
			||||||
 | 
									const isLink = 'href' in (event.target || {});
 | 
				
			||||||
 | 
									if (
 | 
				
			||||||
 | 
										isLink ||
 | 
				
			||||||
 | 
										(document.body.contains(event.target as Node) &&
 | 
				
			||||||
 | 
											!dialogFrame.contains(event.target as Node))
 | 
				
			||||||
 | 
									) {
 | 
				
			||||||
 | 
										closeModal();
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								const openModal = (event?: MouseEvent) => {
 | 
				
			||||||
 | 
									dialog.showModal();
 | 
				
			||||||
 | 
									document.body.toggleAttribute('data-search-modal-open', true);
 | 
				
			||||||
 | 
									this.querySelector('input')?.focus();
 | 
				
			||||||
 | 
									event?.stopPropagation();
 | 
				
			||||||
 | 
									window.addEventListener('click', onClick);
 | 
				
			||||||
 | 
								};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								const closeModal = () => dialog.close();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								openBtn.addEventListener('click', openModal);
 | 
				
			||||||
 | 
								openBtn.disabled = false;
 | 
				
			||||||
 | 
								closeBtn.addEventListener('click', closeModal);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								dialog.addEventListener('close', () => {
 | 
				
			||||||
 | 
									document.body.toggleAttribute('data-search-modal-open', false);
 | 
				
			||||||
 | 
									window.removeEventListener('click', onClick);
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Listen for `ctrl + k` and `cmd + k` keyboard shortcuts.
 | 
				
			||||||
 | 
								window.addEventListener('keydown', (e) => {
 | 
				
			||||||
 | 
									if ((e.metaKey === true || e.ctrlKey === true) && e.key === 'k') {
 | 
				
			||||||
 | 
										dialog.open ? closeModal() : openModal();
 | 
				
			||||||
 | 
										e.preventDefault();
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      const processExcerpt = (sub_resultExcerpt:string):string => {
 | 
				
			||||||
 | 
					        const linkRegex = /TYPE99(\w+.)99TYPE/g;
 | 
				
			||||||
 | 
					        let excerpt = sub_resultExcerpt;
 | 
				
			||||||
 | 
					        const match = [...excerpt.matchAll(linkRegex)];
 | 
				
			||||||
 | 
					        if (match.length > 0){
 | 
				
			||||||
 | 
					          for (const matching of match) {
 | 
				
			||||||
 | 
					            const linkObject = getQMLTypeLinkObject(matching[1]);
 | 
				
			||||||
 | 
					            const link = getQMLTypeLink(linkObject);
 | 
				
			||||||
 | 
					            const icon = linkObject.mtype ? getIconForLink(linkObject.mtype, false) : null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // for signal
 | 
				
			||||||
 | 
					            const bracketString = getIconForLink("func", false)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const newLink = `<span class="type${linkObject.mtype}-link typedata-link">${icon ? icon : ""}<a href=${link}>${linkObject.mname || linkObject.name}</a>${linkObject.mtype === "signal" ? bracketString : ""}</span>`;
 | 
				
			||||||
 | 
					            excerpt = excerpt.replace(matching[0], newLink)
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return excerpt
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								const formatURL = (path: string) => path;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								window.addEventListener('DOMContentLoaded', () => {
 | 
				
			||||||
 | 
									const onIdle = window.requestIdleCallback || ((cb) => setTimeout(cb, 1));
 | 
				
			||||||
 | 
									onIdle(async () => {
 | 
				
			||||||
 | 
										// @ts-expect-error — Missing types for @pagefind/default-ui package.
 | 
				
			||||||
 | 
										const { PagefindUI } = await import('@pagefind/default-ui');
 | 
				
			||||||
 | 
										new PagefindUI({
 | 
				
			||||||
 | 
											element: '#qs_search',
 | 
				
			||||||
 | 
											baseUrl: import.meta.env.BASE_URL,
 | 
				
			||||||
 | 
											bundlePath: import.meta.env.BASE_URL.replace(/\/$/, '') + '/pagefind/',
 | 
				
			||||||
 | 
											showImages: false,
 | 
				
			||||||
 | 
											showSubResults: true,
 | 
				
			||||||
 | 
					            processResult: (result: { url: string; excerpt:string; sub_results: Array<{ url: string, excerpt:string }> }) => {
 | 
				
			||||||
 | 
												result.url = formatURL(result.url);
 | 
				
			||||||
 | 
												result.sub_results = result.sub_results.map((sub_result) => {
 | 
				
			||||||
 | 
													sub_result.url = formatURL(sub_result.url);
 | 
				
			||||||
 | 
					                sub_result.excerpt = processExcerpt(sub_result.excerpt)
 | 
				
			||||||
 | 
													return sub_result;
 | 
				
			||||||
 | 
												});
 | 
				
			||||||
 | 
					              result.excerpt = processExcerpt(result.excerpt)
 | 
				
			||||||
 | 
											},
 | 
				
			||||||
 | 
										});
 | 
				
			||||||
 | 
									});
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						customElements.define('site-search', SiteSearch);
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,53 +0,0 @@
 | 
				
			||||||
import { For, type Component } from "solid-js";
 | 
					 | 
				
			||||||
import type { SearchResult } from "./types";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  getIconForLink,
 | 
					 | 
				
			||||||
  getQMLTypeLink,
 | 
					 | 
				
			||||||
  getQMLTypeLinkObject,
 | 
					 | 
				
			||||||
} from "@src/config/io/helpers";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const SearchModal: Component<{
 | 
					 | 
				
			||||||
  results: SearchResult[];
 | 
					 | 
				
			||||||
}> = props => {
 | 
					 | 
				
			||||||
  const { results } = props;
 | 
					 | 
				
			||||||
  const linkRegex = /TYPE99(\w+.)99TYPE/g;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <div
 | 
					 | 
				
			||||||
      id="search-modal"
 | 
					 | 
				
			||||||
      class="search-output"
 | 
					 | 
				
			||||||
    >
 | 
					 | 
				
			||||||
      <For each={results}>
 | 
					 | 
				
			||||||
        {result => {
 | 
					 | 
				
			||||||
          let excerpt = result.excerpt;
 | 
					 | 
				
			||||||
          const linkMatch = [...excerpt.matchAll(linkRegex)];
 | 
					 | 
				
			||||||
          for (const match of linkMatch) {
 | 
					 | 
				
			||||||
            const unparsed = match[1];
 | 
					 | 
				
			||||||
            const linkObject = getQMLTypeLinkObject(unparsed);
 | 
					 | 
				
			||||||
            const linkParsed = getQMLTypeLink(linkObject);
 | 
					 | 
				
			||||||
            const icon = linkObject.mtype
 | 
					 | 
				
			||||||
              ? getIconForLink(linkObject.mtype, false)
 | 
					 | 
				
			||||||
              : "";
 | 
					 | 
				
			||||||
            const bracketString = getIconForLink("func", false);
 | 
					 | 
				
			||||||
            const newString = `<span class="type${linkObject.mtype}-link typedata-link">${icon}<a href=${linkParsed}>${linkObject.mname || linkObject.name}</a>${linkObject.mtype === "signal" ? bracketString : ""}</span>`;
 | 
					 | 
				
			||||||
            excerpt = excerpt.replace(match[0], newString);
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
          excerpt = `${excerpt}...`;
 | 
					 | 
				
			||||||
          return (
 | 
					 | 
				
			||||||
            <div class="search-output_item">
 | 
					 | 
				
			||||||
              <h3 class="search-output_heading">
 | 
					 | 
				
			||||||
                <a href={result.url}>{result.meta.title}</a>
 | 
					 | 
				
			||||||
              </h3>
 | 
					 | 
				
			||||||
              <section
 | 
					 | 
				
			||||||
                class="search-output_excerpt"
 | 
					 | 
				
			||||||
                innerHTML={excerpt}
 | 
					 | 
				
			||||||
              />
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
        }}
 | 
					 | 
				
			||||||
      </For>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default SearchModal;
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,61 +0,0 @@
 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  createResource,
 | 
					 | 
				
			||||||
  createSignal,
 | 
					 | 
				
			||||||
  type Component,
 | 
					 | 
				
			||||||
} from "solid-js";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import type { SearchResult } from "./types";
 | 
					 | 
				
			||||||
import SearchModal from "./SearchModal";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
//const pagefindPath = "@dist/pagefind/pagefind.js";
 | 
					 | 
				
			||||||
//const pagefind = await import(pagefindPath);
 | 
					 | 
				
			||||||
const pagefind = await import("@dist/pagefind/pagefind.js");
 | 
					 | 
				
			||||||
pagefind.init();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function PagefindSearch(query: string) {
 | 
					 | 
				
			||||||
  const search = await pagefind.search(query);
 | 
					 | 
				
			||||||
  const resultdata: SearchResult[] = [];
 | 
					 | 
				
			||||||
  for (const result of search.results) {
 | 
					 | 
				
			||||||
    const data = await result.data();
 | 
					 | 
				
			||||||
    resultdata.push(data);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  return resultdata;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const SearchComponent: Component = () => {
 | 
					 | 
				
			||||||
  let modal!: HTMLElement;
 | 
					 | 
				
			||||||
  const [query, setQuery] = createSignal("");
 | 
					 | 
				
			||||||
  const [results, { refetch }] = createResource(
 | 
					 | 
				
			||||||
    query,
 | 
					 | 
				
			||||||
    PagefindSearch
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  function handleSearch(value: string) {
 | 
					 | 
				
			||||||
    setQuery(value);
 | 
					 | 
				
			||||||
    refetch();
 | 
					 | 
				
			||||||
    console.log(results());
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <div class="search">
 | 
					 | 
				
			||||||
      <input
 | 
					 | 
				
			||||||
        id="search-input"
 | 
					 | 
				
			||||||
        type="text"
 | 
					 | 
				
			||||||
        role="searchbox"
 | 
					 | 
				
			||||||
        incremental
 | 
					 | 
				
			||||||
        value={query()}
 | 
					 | 
				
			||||||
        placeholder="Search"
 | 
					 | 
				
			||||||
        onChange={e => handleSearch(e.target.value)}
 | 
					 | 
				
			||||||
        //onfocusout={() => setQuery("")}
 | 
					 | 
				
			||||||
      />{" "}
 | 
					 | 
				
			||||||
      {!results.loading && results() && results()!.length > 0 ? (
 | 
					 | 
				
			||||||
        <SearchModal
 | 
					 | 
				
			||||||
          results={results()!}
 | 
					 | 
				
			||||||
          ref={modal}
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
      ) : null}
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default SearchComponent;
 | 
					 | 
				
			||||||
							
								
								
									
										15
									
								
								src/components/navigation/search/types.d.ts
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										15
									
								
								src/components/navigation/search/types.d.ts
									
										
									
									
										vendored
									
									
								
							| 
						 | 
					@ -1,15 +0,0 @@
 | 
				
			||||||
interface SearchResult {
 | 
					 | 
				
			||||||
  url: string;
 | 
					 | 
				
			||||||
  excerpt: string;
 | 
					 | 
				
			||||||
  meta: {
 | 
					 | 
				
			||||||
    title: string;
 | 
					 | 
				
			||||||
    image?: string;
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
  sub_results: {
 | 
					 | 
				
			||||||
    title: string;
 | 
					 | 
				
			||||||
    url: string;
 | 
					 | 
				
			||||||
    excerpt: string;
 | 
					 | 
				
			||||||
  }[];
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export type { SearchResult }
 | 
					 | 
				
			||||||
| 
						 | 
					@ -37,7 +37,10 @@ const NavComponent: Component<NavProps> = props => {
 | 
				
			||||||
          <XToMenu class="nav-icon" />
 | 
					          <XToMenu class="nav-icon" />
 | 
				
			||||||
        )}
 | 
					        )}
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <div class={`nav-items ${open() ? "shown" : ""}`}>
 | 
					      <div
 | 
				
			||||||
 | 
					        id={open() ? "#qs_search" : ""}
 | 
				
			||||||
 | 
					        class={`nav-items ${open() ? "shown" : ""}`}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
        <Tree
 | 
					        <Tree
 | 
				
			||||||
          currentRoute={tree.currentRoute}
 | 
					          currentRoute={tree.currentRoute}
 | 
				
			||||||
          currentModule={tree.currentModule}
 | 
					          currentModule={tree.currentModule}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +0,0 @@
 | 
				
			||||||
export const options = {
 | 
					 | 
				
			||||||
  includeMatches: true,
 | 
					 | 
				
			||||||
  minMatchCharLength: 2,
 | 
					 | 
				
			||||||
  threshold: 0.5,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,41 +0,0 @@
 | 
				
			||||||
.search-output {
 | 
					 | 
				
			||||||
  position: fixed;
 | 
					 | 
				
			||||||
  inset: 0 25svw;
 | 
					 | 
				
			||||||
  top: 3.6rem;
 | 
					 | 
				
			||||||
  z-index: 33;
 | 
					 | 
				
			||||||
  max-height: 100svw;
 | 
					 | 
				
			||||||
  max-width: 50svw;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  overflow: scroll;
 | 
					 | 
				
			||||||
  overflow-x: show;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  display: grid;
 | 
					 | 
				
			||||||
  grid-template-columns: 1fr;
 | 
					 | 
				
			||||||
  grid-auto-rows: max-content;
 | 
					 | 
				
			||||||
  row-gap: 0.618rem;
 | 
					 | 
				
			||||||
  padding: 1rem;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.search-output::before {
 | 
					 | 
				
			||||||
  content: "";
 | 
					 | 
				
			||||||
  position: fixed;
 | 
					 | 
				
			||||||
  inset: -4rem;
 | 
					 | 
				
			||||||
  backdrop-filter: blur(4px);
 | 
					 | 
				
			||||||
  overflow: hidden;
 | 
					 | 
				
			||||||
  background-color: hsla(0 0 0 / 0.3);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.search-output_item {
 | 
					 | 
				
			||||||
  height: max-content;
 | 
					 | 
				
			||||||
  padding: 1rem;
 | 
					 | 
				
			||||||
  border: 2px solid rgba(200, 200, 200, 0.23);
 | 
					 | 
				
			||||||
  border-radius: 12px;
 | 
					 | 
				
			||||||
  background-color: hsl(0 0 0);
 | 
					 | 
				
			||||||
  overflow: hidden;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  & mark {
 | 
					 | 
				
			||||||
    all: unset;
 | 
					 | 
				
			||||||
    background: hsla(53 800 34 / 0.5);
 | 
					 | 
				
			||||||
    padding-inline: 3px;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1 +0,0 @@
 | 
				
			||||||
@import "./modal.css";
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,5 @@
 | 
				
			||||||
@import "./nav-tree.css";
 | 
					@import "./nav-tree.css";
 | 
				
			||||||
 | 
					@import "./search.css";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.nav-wrapper {
 | 
					.nav-wrapper {
 | 
				
			||||||
  display: none;
 | 
					  display: none;
 | 
				
			||||||
| 
						 | 
					@ -44,8 +45,7 @@
 | 
				
			||||||
      display: none;
 | 
					      display: none;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    transition:
 | 
					    transition: width 0.3s ease,
 | 
				
			||||||
      width 0.3s ease,
 | 
					 | 
				
			||||||
    height 0.3s ease,
 | 
					    height 0.3s ease,
 | 
				
			||||||
    background-color 0.3s ease,
 | 
					    background-color 0.3s ease,
 | 
				
			||||||
    backdrop-filter 0.3s ease,
 | 
					    backdrop-filter 0.3s ease,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										338
									
								
								src/styles/docs/nav/search.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										338
									
								
								src/styles/docs/nav/search.css
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,338 @@
 | 
				
			||||||
 | 
					/* is:global */
 | 
				
			||||||
 | 
					[data-search-modal-open] {
 | 
				
			||||||
 | 
					  overflow: hidden;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#qs_search {
 | 
				
			||||||
 | 
					  --search-result-spacing: calc(1.25rem * var(--pagefind-ui-scale));
 | 
				
			||||||
 | 
					  --search-result-pad-inline-start: calc(3.75rem * var(--pagefind-ui-scale));
 | 
				
			||||||
 | 
					  --search-result-pad-inline-end: calc(1.25rem * var(--pagefind-ui-scale));
 | 
				
			||||||
 | 
					  --search-result-pad-block: calc(0.9375rem * var(--pagefind-ui-scale));
 | 
				
			||||||
 | 
					  --search-result-nested-pad-block: calc(0.625rem * var(--pagefind-ui-scale));
 | 
				
			||||||
 | 
					  --search-corners: calc(0.3125rem * var(--pagefind-ui-scale));
 | 
				
			||||||
 | 
					  --search-page-icon-size: calc(1.875rem * var(--pagefind-ui-scale));
 | 
				
			||||||
 | 
					  --search-page-icon-inline-start: calc((var(--search-result-pad-inline-start) - var(--search-page-icon-size)) / 2);
 | 
				
			||||||
 | 
					  --search-tree-diagram-size: calc(2.5rem * var(--pagefind-ui-scale));
 | 
				
			||||||
 | 
					  --search-tree-diagram-inline-start: calc((var(--search-result-pad-inline-start) - var(--search-tree-diagram-size)) / 2);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#qs_search .pagefind-ui__form::before {
 | 
				
			||||||
 | 
					  --pagefind-ui-text: hsl(0 0 65);
 | 
				
			||||||
 | 
					  opacity: 1;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#qs_search .pagefind-ui__search-input {
 | 
				
			||||||
 | 
					  color: hsl(0 0 100);
 | 
				
			||||||
 | 
					  font-weight: 400;
 | 
				
			||||||
 | 
					  width: calc(100% - var(--search-cancel-space));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#qs_search input:focus {
 | 
				
			||||||
 | 
					  --pagefind-ui-border: hsla(var(--accent-500));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#qs_search .pagefind-ui__search-clear {
 | 
				
			||||||
 | 
					  inset-inline-end: var(--search-cancel-space);
 | 
				
			||||||
 | 
					  width: calc(60px * var(--pagefind-ui-scale));
 | 
				
			||||||
 | 
					  padding: 0;
 | 
				
			||||||
 | 
					  background-color: transparent;
 | 
				
			||||||
 | 
					  overflow: hidden;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#qs_search .pagefind-ui__search-clear:focus {
 | 
				
			||||||
 | 
					  outline: 1px solid hsla(var(--accent-600));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#qs_search .pagefind-ui__search-clear::before {
 | 
				
			||||||
 | 
					  content: "";
 | 
				
			||||||
 | 
					  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='m13.41 12 6.3-6.29a1 1 0 1 0-1.42-1.42L12 10.59l-6.29-6.3a1 1 0 0 0-1.42 1.42l6.3 6.29-6.3 6.29a1 1 0 0 0 .33 1.64 1 1 0 0 0 1.09-.22l6.29-6.3 6.29 6.3a1 1 0 0 0 1.64-.33 1 1 0 0 0-.22-1.09L13.41 12Z'/%3E%3C/svg%3E") center / 50% no-repeat;
 | 
				
			||||||
 | 
					  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='m13.41 12 6.3-6.29a1 1 0 1 0-1.42-1.42L12 10.59l-6.29-6.3a1 1 0 0 0-1.42 1.42l6.3 6.29-6.3 6.29a1 1 0 0 0 .33 1.64 1 1 0 0 0 1.09-.22l6.29-6.3 6.29 6.3a1 1 0 0 0 1.64-.33 1 1 0 0 0-.22-1.09L13.41 12Z'/%3E%3C/svg%3E") center / 50% no-repeat;
 | 
				
			||||||
 | 
					  background-color: hsl(var(--accent-600));
 | 
				
			||||||
 | 
					  display: block;
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  height: 100%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#qs_search .pagefind-ui__results>*+* {
 | 
				
			||||||
 | 
					  margin-top: var(--search-result-spacing);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#qs_search .pagefind-ui__result {
 | 
				
			||||||
 | 
					  border: 0;
 | 
				
			||||||
 | 
					  padding: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#qs_search .pagefind-ui__result-nested {
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					  padding: var(--search-result-nested-pad-block) var(--search-result-pad-inline-end);
 | 
				
			||||||
 | 
					  padding-inline-start: var(--search-result-pad-inline-start);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#qs_search .pagefind-ui__result-title:not( :where(.pagefind-ui__result-nested *)),
 | 
				
			||||||
 | 
					#qs_search .pagefind-ui__result-nested {
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					  background-color: hsl(0 0 10);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#qs_search .pagefind-ui__result-title:not( :where(.pagefind-ui__result-nested *)):hover,
 | 
				
			||||||
 | 
					#qs_search .pagefind-ui__result-title:not( :where(.pagefind-ui__result-nested *)):focus-within,
 | 
				
			||||||
 | 
					#qs_search .pagefind-ui__result-nested:hover,
 | 
				
			||||||
 | 
					#qs_search .pagefind-ui__result-nested:focus-within {
 | 
				
			||||||
 | 
					  outline: 1px solid hsl(var(--accent-600));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#qs_search .pagefind-ui__result-title:not( :where(.pagefind-ui__result-nested *)):focus-within,
 | 
				
			||||||
 | 
					#qs_search .pagefind-ui__result-nested:focus-within {
 | 
				
			||||||
 | 
					  background-color: hsl(var(--accent-400));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#qs_search .pagefind-ui__result-thumb,
 | 
				
			||||||
 | 
					#qs_search .pagefind-ui__result-inner {
 | 
				
			||||||
 | 
					  margin-top: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#qs_search .pagefind-ui__result-inner> :first-child {
 | 
				
			||||||
 | 
					  border-radius: var(--search-corners) var(--search-corners) 0 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#qs_search .pagefind-ui__result-inner> :last-child {
 | 
				
			||||||
 | 
					  border-radius: 0 0 var(--search-corners) var(--search-corners);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#qs_search .pagefind-ui__result-inner>.pagefind-ui__result-title {
 | 
				
			||||||
 | 
					  padding: var(--search-result-pad-block) var(--search-result-pad-inline-end);
 | 
				
			||||||
 | 
					  padding-inline-start: var(--search-result-pad-inline-start);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#qs_search .pagefind-ui__result-inner>.pagefind-ui__result-title::before {
 | 
				
			||||||
 | 
					  content: "";
 | 
				
			||||||
 | 
					  position: absolute;
 | 
				
			||||||
 | 
					  inset-block: 0;
 | 
				
			||||||
 | 
					  inset-inline-start: var(--search-page-icon-inline-start);
 | 
				
			||||||
 | 
					  width: var(--search-page-icon-size);
 | 
				
			||||||
 | 
					  background: hsl(var(--blue) 15 33);
 | 
				
			||||||
 | 
					  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='currentColor' viewBox='0 0 24 24'%3E%3Cpath d='M9 10h1a1 1 0 1 0 0-2H9a1 1 0 0 0 0 2Zm0 2a1 1 0 0 0 0 2h6a1 1 0 0 0 0-2H9Zm11-3V8l-6-6a1 1 0 0 0-1 0H7a3 3 0 0 0-3 3v14a3 3 0 0 0 3 3h10a3 3 0 0 0 3-3V9Zm-6-4 3 3h-2a1 1 0 0 1-1-1V5Zm4 14a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h5v3a3 3 0 0 0 3 3h3v9Zm-3-3H9a1 1 0 0 0 0 2h6a1 1 0 0 0 0-2Z'/%3E%3C/svg%3E") center no-repeat;
 | 
				
			||||||
 | 
					  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='currentColor' viewBox='0 0 24 24'%3E%3Cpath d='M9 10h1a1 1 0 1 0 0-2H9a1 1 0 0 0 0 2Zm0 2a1 1 0 0 0 0 2h6a1 1 0 0 0 0-2H9Zm11-3V8l-6-6a1 1 0 0 0-1 0H7a3 3 0 0 0-3 3v14a3 3 0 0 0 3 3h10a3 3 0 0 0 3-3V9Zm-6-4 3 3h-2a1 1 0 0 1-1-1V5Zm4 14a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h5v3a3 3 0 0 0 3 3h3v9Zm-3-3H9a1 1 0 0 0 0 2h6a1 1 0 0 0 0-2Z'/%3E%3C/svg%3E") center no-repeat;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#qs_search .pagefind-ui__result-inner {
 | 
				
			||||||
 | 
					  align-items: stretch;
 | 
				
			||||||
 | 
					  gap: 1px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#qs_search .pagefind-ui__result-link {
 | 
				
			||||||
 | 
					  position: unset;
 | 
				
			||||||
 | 
					  --pagefind-ui-text: hsl(0 0 85);
 | 
				
			||||||
 | 
					  font-weight: 600;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#qs_search .pagefind-ui__result-link:hover {
 | 
				
			||||||
 | 
					  text-decoration: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#qs_search .pagefind-ui__result-nested .pagefind-ui__result-link::before {
 | 
				
			||||||
 | 
					  content: unset;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#qs_search .pagefind-ui__result-nested::before {
 | 
				
			||||||
 | 
					  content: "";
 | 
				
			||||||
 | 
					  position: absolute;
 | 
				
			||||||
 | 
					  inset-block: 0;
 | 
				
			||||||
 | 
					  inset-inline-start: var(--search-tree-diagram-inline-start);
 | 
				
			||||||
 | 
					  width: var(--search-tree-diagram-size);
 | 
				
			||||||
 | 
					  background: hsl(var(--blue) 5 25);
 | 
				
			||||||
 | 
					  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' viewBox='0 0 16 1000' preserveAspectRatio='xMinYMin slice'%3E%3Cpath d='M8 0v1000m6-988H8'/%3E%3C/svg%3E") 0% 0% / 100% no-repeat;
 | 
				
			||||||
 | 
					  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' viewBox='0 0 16 1000' preserveAspectRatio='xMinYMin slice'%3E%3Cpath d='M8 0v1000m6-988H8'/%3E%3C/svg%3E") 0% 0% / 100% no-repeat;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#qs_search .pagefind-ui__result-nested:last-child::before {
 | 
				
			||||||
 | 
					  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' viewBox='0 0 16 16'%3E%3Cpath d='M8 0v12m6 0H8'/%3E%3C/svg%3E");
 | 
				
			||||||
 | 
					  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' viewBox='0 0 16 16'%3E%3Cpath d='M8 0v12m6 0H8'/%3E%3C/svg%3E");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#qs_search .pagefind-ui__result-link::after {
 | 
				
			||||||
 | 
					  content: "";
 | 
				
			||||||
 | 
					  position: absolute;
 | 
				
			||||||
 | 
					  inset: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#qs_search .pagefind-ui__result-excerpt {
 | 
				
			||||||
 | 
					  font-size: calc(1rem * var(--pagefind-ui-scale));
 | 
				
			||||||
 | 
					  overflow-wrap: anywhere;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#qs_search mark {
 | 
				
			||||||
 | 
					  color: hsl(var(--blue) 15 60);
 | 
				
			||||||
 | 
					  background-color: transparent;
 | 
				
			||||||
 | 
					  font-weight: 600;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* default styles */
 | 
				
			||||||
 | 
					site-search {
 | 
				
			||||||
 | 
					  --shadow-lg:
 | 
				
			||||||
 | 
					    0px 25px 7px hsla(0, 0%, 0%, 0.03), 0px 16px 6px hsla(0,
 | 
				
			||||||
 | 
					      0%,
 | 
				
			||||||
 | 
					      0%,
 | 
				
			||||||
 | 
					      0.1),
 | 
				
			||||||
 | 
					    0px 9px 5px hsla(223, 13%, 10%, 0.33), 0px 4px 4px hsla(0,
 | 
				
			||||||
 | 
					      0%,
 | 
				
			||||||
 | 
					      0%,
 | 
				
			||||||
 | 
					      0.75),
 | 
				
			||||||
 | 
					    0px 4px 2px hsla(0, 0%, 0%, 0.25);
 | 
				
			||||||
 | 
					  display: contents;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.search-label {
 | 
				
			||||||
 | 
					  /* >720px block */
 | 
				
			||||||
 | 
					  display: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.search-kbd {
 | 
				
			||||||
 | 
					  /* >720px flex */
 | 
				
			||||||
 | 
					  display: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.search-dialog {
 | 
				
			||||||
 | 
					  padding: 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  & .dialog-frame {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  & .search-cancel {
 | 
				
			||||||
 | 
					    /* >720px none */
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* --- */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					button[data-open-modal] {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  gap: 0.5rem;
 | 
				
			||||||
 | 
					  border: 0;
 | 
				
			||||||
 | 
					  background-color: transparent;
 | 
				
			||||||
 | 
					  color: hsl(0 0 65);
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					  height: 2.5rem;
 | 
				
			||||||
 | 
					  font-size: 1.10rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					button>kbd {
 | 
				
			||||||
 | 
					  border-radius: 0.25rem;
 | 
				
			||||||
 | 
					  font-size: 0.75rem;
 | 
				
			||||||
 | 
					  gap: 0.25em;
 | 
				
			||||||
 | 
					  padding-inline: 0.375rem;
 | 
				
			||||||
 | 
					  background-color: hsl(0 0 15);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					dialog {
 | 
				
			||||||
 | 
					  margin: 0;
 | 
				
			||||||
 | 
					  background-color: hsl(var(--blue) 15 12);
 | 
				
			||||||
 | 
					  border: 1px solid hsl(0 0 25);
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  max-width: 100%;
 | 
				
			||||||
 | 
					  height: 100%;
 | 
				
			||||||
 | 
					  max-height: 100%;
 | 
				
			||||||
 | 
					  box-shadow: var(--shadow-lg);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					dialog[open] {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					dialog::backdrop {
 | 
				
			||||||
 | 
					  background-color: hsla(var(--blue) 15 6 / 0.66);
 | 
				
			||||||
 | 
					  -webkit-backdrop-filter: blur(0.25rem);
 | 
				
			||||||
 | 
					  backdrop-filter: blur(0.25rem);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.dialog-frame {
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					  flex-grow: 1;
 | 
				
			||||||
 | 
					  gap: 1rem;
 | 
				
			||||||
 | 
					  padding: 1rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					button[data-close-modal] {
 | 
				
			||||||
 | 
					  position: absolute;
 | 
				
			||||||
 | 
					  z-index: 11;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  align-self: flex-end;
 | 
				
			||||||
 | 
					  height: calc(64px * var(--pagefind-ui-scale));
 | 
				
			||||||
 | 
					  padding: 0.25rem;
 | 
				
			||||||
 | 
					  border: 0;
 | 
				
			||||||
 | 
					  background: transparent;
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					  color: hsla(var(--accent-600));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#qs_search {
 | 
				
			||||||
 | 
					  --pagefind-ui-primary: hsla(var(--accent-400));
 | 
				
			||||||
 | 
					  --pagefind-ui-text: hsla(0 0 60);
 | 
				
			||||||
 | 
					  --pagefind-ui-font: Rubik;
 | 
				
			||||||
 | 
					  --pagefind-ui-background: hsl(0 0 10);
 | 
				
			||||||
 | 
					  --pagefind-ui-border: hsl(0 0 15);
 | 
				
			||||||
 | 
					  --pagefind-ui-border-width: 1px;
 | 
				
			||||||
 | 
					  --search-cancel-space: 5rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@media (min-width: 768px) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /* difault styles */
 | 
				
			||||||
 | 
					  .search-label {
 | 
				
			||||||
 | 
					    display: block;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .search-kbd {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .search-dialog {
 | 
				
			||||||
 | 
					    & .search-cancel {
 | 
				
			||||||
 | 
					      display: none;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /* --- */
 | 
				
			||||||
 | 
					  button[data-open-modal] {
 | 
				
			||||||
 | 
					    border: 1px solid hsl(0 0 25);
 | 
				
			||||||
 | 
					    border-radius: 6px;
 | 
				
			||||||
 | 
					    padding-inline-start: 0.75rem;
 | 
				
			||||||
 | 
					    padding-inline-end: 0.5rem;
 | 
				
			||||||
 | 
					    background-color: hsla(0 0 10 / 0.5);
 | 
				
			||||||
 | 
					    color: hsl(0 0 50);
 | 
				
			||||||
 | 
					    font-size: 0.875rem;
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					    max-width: 15rem;
 | 
				
			||||||
 | 
					    transition: color 0.23s, border-color 0.23s;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &:hover {
 | 
				
			||||||
 | 
					      border-color: hsl(0 0 50);
 | 
				
			||||||
 | 
					      color: hsl(0 0 85);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &> :last-child {
 | 
				
			||||||
 | 
					      margin-inline-start: auto;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  #qs_search {
 | 
				
			||||||
 | 
					    --search-cancel-space: 0px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  dialog {
 | 
				
			||||||
 | 
					    margin: 4rem auto auto;
 | 
				
			||||||
 | 
					    border-radius: 0.5rem;
 | 
				
			||||||
 | 
					    width: 90%;
 | 
				
			||||||
 | 
					    max-width: 40rem;
 | 
				
			||||||
 | 
					    height: max-content;
 | 
				
			||||||
 | 
					    min-height: 15rem;
 | 
				
			||||||
 | 
					    max-height: calc(100% - 8rem);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .dialog-frame {
 | 
				
			||||||
 | 
					    padding: 1.5rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -35,7 +35,7 @@
 | 
				
			||||||
  font-size: 1.614rem;
 | 
					  font-size: 1.614rem;
 | 
				
			||||||
  max-height: 500px;
 | 
					  max-height: 500px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  &>svg {
 | 
					  & > svg {
 | 
				
			||||||
    height: 100%;
 | 
					    height: 100%;
 | 
				
			||||||
    width: 24px;
 | 
					    width: 24px;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					@ -65,7 +65,8 @@
 | 
				
			||||||
      display: none;
 | 
					      display: none;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    transition: width 0.3s ease,
 | 
					    transition:
 | 
				
			||||||
 | 
					      width 0.3s ease,
 | 
				
			||||||
      height 0.3s ease,
 | 
					      height 0.3s ease,
 | 
				
			||||||
      background-color 0.3s ease,
 | 
					      background-color 0.3s ease,
 | 
				
			||||||
      backdrop-filter 0.3s ease,
 | 
					      backdrop-filter 0.3s ease,
 | 
				
			||||||
| 
						 | 
					@ -100,8 +101,8 @@
 | 
				
			||||||
    display: block;
 | 
					    display: block;
 | 
				
			||||||
    position: fixed;
 | 
					    position: fixed;
 | 
				
			||||||
    top: 5rem;
 | 
					    top: 5rem;
 | 
				
			||||||
    right: 10svw;
 | 
					    right: 8svw;
 | 
				
			||||||
    width: 250px;
 | 
					    width: 18rem;
 | 
				
			||||||
    max-height: 90svh;
 | 
					    max-height: 90svh;
 | 
				
			||||||
    overflow-y: scroll;
 | 
					    overflow-y: scroll;
 | 
				
			||||||
    z-index: 10;
 | 
					    z-index: 10;
 | 
				
			||||||
| 
						 | 
					@ -119,7 +120,7 @@
 | 
				
			||||||
      list-style: none;
 | 
					      list-style: none;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      &.active {
 | 
					      &.active {
 | 
				
			||||||
        &>.toc_a {
 | 
					        & > .toc_a {
 | 
				
			||||||
          color: hsl(var(--green) 72 60);
 | 
					          color: hsl(var(--green) 72 60);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,7 +2,6 @@
 | 
				
			||||||
@import "./css-config/base.css";
 | 
					@import "./css-config/base.css";
 | 
				
			||||||
@import "./css-config/animations.css";
 | 
					@import "./css-config/animations.css";
 | 
				
			||||||
@import "./css-config/code.css";
 | 
					@import "./css-config/code.css";
 | 
				
			||||||
@import "./css-config/search.css";
 | 
					 | 
				
			||||||
@import "./css-config/colors.css";
 | 
					@import "./css-config/colors.css";
 | 
				
			||||||
@import "./docs/nav/nav.css";
 | 
					@import "./docs/nav/nav.css";
 | 
				
			||||||
@import "./docs/toc/toc.css";
 | 
					@import "./docs/toc/toc.css";
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										45
									
								
								yarn.lock
									
										
									
									
									
								
							
							
						
						
									
										45
									
								
								yarn.lock
									
										
									
									
									
								
							| 
						 | 
					@ -1225,7 +1225,7 @@ __metadata:
 | 
				
			||||||
  languageName: node
 | 
					  languageName: node
 | 
				
			||||||
  linkType: hard
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"@pagefind/default-ui@npm:^1.0.3":
 | 
					"@pagefind/default-ui@npm:^1.1.1":
 | 
				
			||||||
  version: 1.1.1
 | 
					  version: 1.1.1
 | 
				
			||||||
  resolution: "@pagefind/default-ui@npm:1.1.1"
 | 
					  resolution: "@pagefind/default-ui@npm:1.1.1"
 | 
				
			||||||
  checksum: 10c0/a1069183ae31daa8b71344abd6cdd85b7183b2af4f88fdc89d8c5298ea0e653e7f8a89f95d10ee54053ab55c36dd1142b6768efd1a7c29d589432b432b7a291b
 | 
					  checksum: 10c0/a1069183ae31daa8b71344abd6cdd85b7183b2af4f88fdc89d8c5298ea0e653e7f8a89f95d10ee54053ab55c36dd1142b6768efd1a7c29d589432b432b7a291b
 | 
				
			||||||
| 
						 | 
					@ -1260,13 +1260,6 @@ __metadata:
 | 
				
			||||||
  languageName: node
 | 
					  languageName: node
 | 
				
			||||||
  linkType: hard
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"@polka/url@npm:^1.0.0-next.24":
 | 
					 | 
				
			||||||
  version: 1.0.0-next.28
 | 
					 | 
				
			||||||
  resolution: "@polka/url@npm:1.0.0-next.28"
 | 
					 | 
				
			||||||
  checksum: 10c0/acc5ea62597e4da2fb42dbee02749d07f102ae7d6d2c966bf7e423c79cd65d1621da305af567e6e7c232f3b565e242d1ec932cbb3dcc0db1508d02e9a2cafa2e
 | 
					 | 
				
			||||||
  languageName: node
 | 
					 | 
				
			||||||
  linkType: hard
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
"@rollup/pluginutils@npm:^5.1.0":
 | 
					"@rollup/pluginutils@npm:^5.1.0":
 | 
				
			||||||
  version: 5.1.0
 | 
					  version: 5.1.0
 | 
				
			||||||
  resolution: "@rollup/pluginutils@npm:5.1.0"
 | 
					  resolution: "@rollup/pluginutils@npm:5.1.0"
 | 
				
			||||||
| 
						 | 
					@ -2942,19 +2935,6 @@ __metadata:
 | 
				
			||||||
  languageName: node
 | 
					  languageName: node
 | 
				
			||||||
  linkType: hard
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"astro-pagefind@npm:^1.6.0":
 | 
					 | 
				
			||||||
  version: 1.6.0
 | 
					 | 
				
			||||||
  resolution: "astro-pagefind@npm:1.6.0"
 | 
					 | 
				
			||||||
  dependencies:
 | 
					 | 
				
			||||||
    "@pagefind/default-ui": "npm:^1.0.3"
 | 
					 | 
				
			||||||
    pagefind: "npm:^1.0.3"
 | 
					 | 
				
			||||||
    sirv: "npm:^2.0.3"
 | 
					 | 
				
			||||||
  peerDependencies:
 | 
					 | 
				
			||||||
    astro: ^2.0.4 || ^3.0.0 || ^4.0.0
 | 
					 | 
				
			||||||
  checksum: 10c0/4ee30b5fdb229351a496a9bc21995b459dfd82091efd9f6e0281695f795fa47689253fefde46695af785d2dd278ba90b3e2b261a3a2ede49f7e27ee7d5070650
 | 
					 | 
				
			||||||
  languageName: node
 | 
					 | 
				
			||||||
  linkType: hard
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
"astro@npm:^4.15.9":
 | 
					"astro@npm:^4.15.9":
 | 
				
			||||||
  version: 4.15.9
 | 
					  version: 4.15.9
 | 
				
			||||||
  resolution: "astro@npm:4.15.9"
 | 
					  resolution: "astro@npm:4.15.9"
 | 
				
			||||||
| 
						 | 
					@ -6070,7 +6050,7 @@ __metadata:
 | 
				
			||||||
  languageName: node
 | 
					  languageName: node
 | 
				
			||||||
  linkType: hard
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"pagefind@npm:^1.0.3":
 | 
					"pagefind@npm:^1.1.1":
 | 
				
			||||||
  version: 1.1.1
 | 
					  version: 1.1.1
 | 
				
			||||||
  resolution: "pagefind@npm:1.1.1"
 | 
					  resolution: "pagefind@npm:1.1.1"
 | 
				
			||||||
  dependencies:
 | 
					  dependencies:
 | 
				
			||||||
| 
						 | 
					@ -6345,13 +6325,14 @@ __metadata:
 | 
				
			||||||
    "@astrojs/ts-plugin": "npm:^1.10.2"
 | 
					    "@astrojs/ts-plugin": "npm:^1.10.2"
 | 
				
			||||||
    "@biomejs/biome": "npm:^1.8.3"
 | 
					    "@biomejs/biome": "npm:^1.8.3"
 | 
				
			||||||
    "@hbsnow/rehype-sectionize": "npm:^1.0.7"
 | 
					    "@hbsnow/rehype-sectionize": "npm:^1.0.7"
 | 
				
			||||||
 | 
					    "@pagefind/default-ui": "npm:^1.1.1"
 | 
				
			||||||
    "@types/node": "npm:^20.14.11"
 | 
					    "@types/node": "npm:^20.14.11"
 | 
				
			||||||
    astro: "npm:^4.15.9"
 | 
					    astro: "npm:^4.15.9"
 | 
				
			||||||
    astro-breadcrumbs: "npm:^2.3.1"
 | 
					    astro-breadcrumbs: "npm:^2.3.1"
 | 
				
			||||||
    astro-pagefind: "npm:^1.6.0"
 | 
					 | 
				
			||||||
    marked: "npm:^14.1.0"
 | 
					    marked: "npm:^14.1.0"
 | 
				
			||||||
    marked-alert: "npm:^2.0.2"
 | 
					    marked-alert: "npm:^2.0.2"
 | 
				
			||||||
    node: "npm:22.7.0"
 | 
					    node: "npm:22.7.0"
 | 
				
			||||||
 | 
					    pagefind: "npm:^1.1.1"
 | 
				
			||||||
    remark-github-blockquote-alert: "npm:^1.2.1"
 | 
					    remark-github-blockquote-alert: "npm:^1.2.1"
 | 
				
			||||||
    remark-parse: "npm:^11.0.0"
 | 
					    remark-parse: "npm:^11.0.0"
 | 
				
			||||||
    shiki: "npm:^1.11.0"
 | 
					    shiki: "npm:^1.11.0"
 | 
				
			||||||
| 
						 | 
					@ -6896,17 +6877,6 @@ __metadata:
 | 
				
			||||||
  languageName: node
 | 
					  languageName: node
 | 
				
			||||||
  linkType: hard
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"sirv@npm:^2.0.3":
 | 
					 | 
				
			||||||
  version: 2.0.4
 | 
					 | 
				
			||||||
  resolution: "sirv@npm:2.0.4"
 | 
					 | 
				
			||||||
  dependencies:
 | 
					 | 
				
			||||||
    "@polka/url": "npm:^1.0.0-next.24"
 | 
					 | 
				
			||||||
    mrmime: "npm:^2.0.0"
 | 
					 | 
				
			||||||
    totalist: "npm:^3.0.0"
 | 
					 | 
				
			||||||
  checksum: 10c0/68f8ee857f6a9415e9c07a1f31c7c561df8d5f1b1ba79bee3de583fa37da8718def5309f6b1c6e2c3ef77de45d74f5e49efc7959214443aa92d42e9c99180a4e
 | 
					 | 
				
			||||||
  languageName: node
 | 
					 | 
				
			||||||
  linkType: hard
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
"sisteransi@npm:^1.0.5":
 | 
					"sisteransi@npm:^1.0.5":
 | 
				
			||||||
  version: 1.0.5
 | 
					  version: 1.0.5
 | 
				
			||||||
  resolution: "sisteransi@npm:1.0.5"
 | 
					  resolution: "sisteransi@npm:1.0.5"
 | 
				
			||||||
| 
						 | 
					@ -7219,13 +7189,6 @@ __metadata:
 | 
				
			||||||
  languageName: node
 | 
					  languageName: node
 | 
				
			||||||
  linkType: hard
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"totalist@npm:^3.0.0":
 | 
					 | 
				
			||||||
  version: 3.0.1
 | 
					 | 
				
			||||||
  resolution: "totalist@npm:3.0.1"
 | 
					 | 
				
			||||||
  checksum: 10c0/4bb1fadb69c3edbef91c73ebef9d25b33bbf69afe1e37ce544d5f7d13854cda15e47132f3e0dc4cafe300ddb8578c77c50a65004d8b6e97e77934a69aa924863
 | 
					 | 
				
			||||||
  languageName: node
 | 
					 | 
				
			||||||
  linkType: hard
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
"tough-cookie@npm:^5.0.0":
 | 
					"tough-cookie@npm:^5.0.0":
 | 
				
			||||||
  version: 5.0.0
 | 
					  version: 5.0.0
 | 
				
			||||||
  resolution: "tough-cookie@npm:5.0.0"
 | 
					  resolution: "tough-cookie@npm:5.0.0"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue