662 lines
		
	
	
		
			24 KiB
		
	
	
	
		
			HTML
		
	
	
	
	
	
			
		
		
	
	
			662 lines
		
	
	
		
			24 KiB
		
	
	
	
		
			HTML
		
	
	
	
	
	
<!DOCTYPE html>
 | 
						|
<meta charset="utf-8" />
 | 
						|
<meta name="color-scheme" content="dark light" />
 | 
						|
<meta name="robots" content="noindex" />
 | 
						|
<meta name="viewport" content="width=device-width, initial-scale=1" />
 | 
						|
 | 
						|
<link rel="icon" href="favicon.ico" sizes="any">
 | 
						|
<link rel="icon" href="favicon.svg" type="image/svg+xml">
 | 
						|
<link rel="manifest" href="manifest.webmanifest">
 | 
						|
 | 
						|
<title>TKA - Start Page</title>
 | 
						|
 | 
						|
<style>
 | 
						|
  :root {
 | 
						|
    --color-background: #303030;
 | 
						|
    --color-text-subtle: #b3b3b3;
 | 
						|
    --color-text: #ff4000;
 | 
						|
    --font-family: monospace;
 | 
						|
    --font-size-1: 1rem;
 | 
						|
    --font-size-2: 3rem;
 | 
						|
    --font-size-base: clamp(16px, 1.2vw, 18px);
 | 
						|
    --font-weight-bold: 700;
 | 
						|
    --font-weight-normal: 400;
 | 
						|
    --line-height-base: 1.4;
 | 
						|
    --space-1: 0.5rem;
 | 
						|
    --space-2: 1rem;
 | 
						|
    --space-3: 4rem;
 | 
						|
    --transition-speed: 200ms;
 | 
						|
  }
 | 
						|
</style>
 | 
						|
 | 
						|
<script>
 | 
						|
  const CONFIG = {
 | 
						|
    commandPathDelimiter: '/',
 | 
						|
    commandSearchDelimiter: ' ',
 | 
						|
    defaultSearchTemplate: 'https://duckduckgo.com/?kl=fr-fr&kad=fr_FR&kp=-2&kz=-1&kc=-1&k1=-1&kaj=m&kay=i&kak=-1&kax=-1&kaq=-1&kao=-1&kau=-1&kap=-1&k7=303030&kae=d&kj=262626&k9=f5f5f5&kaa=b3b3b3&k8=b3b3b3&kx=ff4000&k21=363636&q={}',
 | 
						|
    openLinksInNewTab: true,
 | 
						|
    suggestionLimit: 4,
 | 
						|
  };
 | 
						|
 | 
						|
  const COMMANDS = new Map([
 | 
						|
    ['b', {
 | 
						|
        name: 'Blog',
 | 
						|
        url: 'https://myblog.net',
 | 
						|
        suggestions: ['myblog.net']
 | 
						|
    }],
 | 
						|
    ['g', {
 | 
						|
        name: 'GitHub',
 | 
						|
        searchTemplate: '/search?q={}',
 | 
						|
        suggestions: [
 | 
						|
            'github.com',
 | 
						|
            'g/trending',
 | 
						|
            'g/collections',
 | 
						|
            'gist.github.com',
 | 
						|
        ],
 | 
						|
        url: 'https://github.com',
 | 
						|
    }],
 | 
						|
    ['m', {
 | 
						|
        name: 'Webmail',
 | 
						|
        url: 'https://mywebmail.tld',
 | 
						|
        suggestions: ['mywebmail.tld', 'm/?admin']
 | 
						|
    }],
 | 
						|
    ['n', {
 | 
						|
        name: 'nb',
 | 
						|
        suggestions: ['localhost:6789', 'n/dev', 'n/home', 'n/sys',],
 | 
						|
        url: 'http://localhost:6789',
 | 
						|
    }],
 | 
						|
    ['n/dev', {
 | 
						|
        searchTemplate: ':6789/dev:?--query={}&--limit=30&--page=1&--columns=70',
 | 
						|
        url: 'http://localhost:6789/dev:',
 | 
						|
    }],
 | 
						|
    ['n/home', {
 | 
						|
        searchTemplate: ':6789/home:?--query={}&--limit=30&--page=1&--columns=70',
 | 
						|
        url: 'http://localhost:6789/home:',
 | 
						|
    }],
 | 
						|
    ['n/sys', {
 | 
						|
        searchTemplate: ':6789/sys:?--query={}&--limit=30&--page=1&--columns=70',
 | 
						|
        url: 'http://localhost:6789/sys:',
 | 
						|
    }],
 | 
						|
    ['N', {
 | 
						|
        name: 'Ntfy',
 | 
						|
        url: 'https://ntfy.domain.tld',
 | 
						|
        suggestions: ['ntfy.domain.tld', 'N/tests']
 | 
						|
    }],
 | 
						|
    ['t', {
 | 
						|
        name: 'Translate',
 | 
						|
        searchTemplate: '/?sl=fr&tl=en&text={}',
 | 
						|
        url: 'https://translate.google.com',
 | 
						|
        suggestions: ['translate.google.com']
 | 
						|
    }],
 | 
						|
    ['w', {
 | 
						|
        name: 'Wikipedia',
 | 
						|
        searchTemplate: '/w/index.php?search={}',
 | 
						|
        suggestions: ['fr.wikipedia.org', 'w/wiki/Special:Random'],
 | 
						|
        url: 'https://fr.wikipedia.org/wiki/Main_Page'
 | 
						|
    }],
 | 
						|
    ['y', {
 | 
						|
        name: 'YouTube',
 | 
						|
        searchTemplate: '/results?search_query={}',
 | 
						|
        suggestions: ['youtube.com', 'y/feed/subscriptions'],
 | 
						|
        url: 'https://youtube.com'
 | 
						|
    }],
 | 
						|
    ['0', {
 | 
						|
        name: 'localhost',
 | 
						|
        searchTemplate: ':{}',
 | 
						|
        suggestions: ['0 6789', '0 8000'],
 | 
						|
        url: 'http://localhost:3000'
 | 
						|
    }],
 | 
						|
    ['?', {
 | 
						|
        name: 'README',
 | 
						|
        url: 'https://github.com/xvvvyz/tilde',
 | 
						|
        suggestions: ['git.tkapias.net/tkapias/stilde']
 | 
						|
    }],
 | 
						|
  ]);
 | 
						|
</script>
 | 
						|
 | 
						|
<template id="commands-template">
 | 
						|
  <style>
 | 
						|
    .commands {
 | 
						|
      display: grid;
 | 
						|
      grid-template-columns: repeat(auto-fill, minmax(20ch, 1fr));
 | 
						|
      gap: var(--space-1);
 | 
						|
      list-style: none;
 | 
						|
      margin: 0 auto;
 | 
						|
      min-width: 50vw;
 | 
						|
      padding: 0 0 0 var(--space-1);
 | 
						|
      justify-items: start;
 | 
						|
      align-items: center;
 | 
						|
      place-content: center;
 | 
						|
    }
 | 
						|
 | 
						|
    .command {
 | 
						|
      outline: 0;
 | 
						|
      padding: var(--space-1);
 | 
						|
      text-decoration: none;
 | 
						|
    }
 | 
						|
 | 
						|
    .key {
 | 
						|
      color: var(--color-text);
 | 
						|
      display: inline-block;
 | 
						|
      text-align: center;
 | 
						|
      width: 3ch;
 | 
						|
    }
 | 
						|
 | 
						|
    .name {
 | 
						|
      color: var(--color-text-subtle);
 | 
						|
      transition: color var(--transition-speed);
 | 
						|
    }
 | 
						|
 | 
						|
    .command:where(:focus, :hover) .name {
 | 
						|
    }
 | 
						|
 | 
						|
  </style>
 | 
						|
  <nav>
 | 
						|
    <menu class="commands"></menu>
 | 
						|
  </nav>
 | 
						|
</template>
 | 
						|
 | 
						|
<template id="command-template">
 | 
						|
  <li>
 | 
						|
    <a class="command" rel="noopener noreferrer">
 | 
						|
      <span class="key"></span>
 | 
						|
      <span class="name"></span>
 | 
						|
    </a>
 | 
						|
  </li>
 | 
						|
</template>
 | 
						|
 | 
						|
<script type="module">
 | 
						|
  class Commands extends HTMLElement {
 | 
						|
    constructor() {
 | 
						|
      super();
 | 
						|
      this.attachShadow({ mode: 'open' });
 | 
						|
      const template = document.getElementById('commands-template');
 | 
						|
      const clone = template.content.cloneNode(true);
 | 
						|
      const commands = clone.querySelector('.commands');
 | 
						|
      const commandTemplate = document.getElementById('command-template');
 | 
						|
 | 
						|
      for (const [key, { name, url }] of COMMANDS.entries()) {
 | 
						|
        if (!name || !url) continue;
 | 
						|
        const clone = commandTemplate.content.cloneNode(true);
 | 
						|
        const command = clone.querySelector('.command');
 | 
						|
        command.href = url;
 | 
						|
        if (CONFIG.openLinksInNewTab) command.target = '_blank';
 | 
						|
        clone.querySelector('.key').innerText = key;
 | 
						|
        clone.querySelector('.name').innerText = name;
 | 
						|
        commands.append(clone);
 | 
						|
      }
 | 
						|
 | 
						|
      this.shadowRoot.append(clone);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  customElements.define('commands-component', Commands);
 | 
						|
</script>
 | 
						|
 | 
						|
<template id="search-template">
 | 
						|
  <style>
 | 
						|
    input,
 | 
						|
    button {
 | 
						|
      -moz-appearance: none;
 | 
						|
      -webkit-appearance: none;
 | 
						|
      background: transparent;
 | 
						|
      border: 0;
 | 
						|
      display: block;
 | 
						|
      outline: 0;
 | 
						|
    }
 | 
						|
 | 
						|
    .dialog {
 | 
						|
      align-items: center;
 | 
						|
      background: var(--color-background);
 | 
						|
      border: none;
 | 
						|
      display: none;
 | 
						|
      flex-direction: column;
 | 
						|
      height: 100%;
 | 
						|
      justify-content: center;
 | 
						|
      left: 0;
 | 
						|
      padding: 0;
 | 
						|
      top: 0;
 | 
						|
      width: 100%;
 | 
						|
    }
 | 
						|
 | 
						|
    .dialog[open] {
 | 
						|
      display: flex;
 | 
						|
    }
 | 
						|
 | 
						|
    .form {
 | 
						|
      width: 100%;
 | 
						|
    }
 | 
						|
 | 
						|
    .input {
 | 
						|
      color: var(--color-text);
 | 
						|
      font-size: var(--font-size-2);
 | 
						|
      font-weight: var(--font-weight-bold);
 | 
						|
      padding: 0;
 | 
						|
      text-align: center;
 | 
						|
      width: 100%;
 | 
						|
    }
 | 
						|
 | 
						|
    .suggestions {
 | 
						|
      align-items: center;
 | 
						|
      display: flex;
 | 
						|
      flex-direction: column;
 | 
						|
      flex-wrap: wrap;
 | 
						|
      justify-content: center;
 | 
						|
      list-style: none;
 | 
						|
      margin: var(--space-2) 0 0;
 | 
						|
      overflow: hidden;
 | 
						|
      padding: 0;
 | 
						|
    }
 | 
						|
 | 
						|
    .suggestion {
 | 
						|
      color: var(--color-text);
 | 
						|
      cursor: pointer;
 | 
						|
      font-size: var(--font-size-1);
 | 
						|
      padding: var(--space-2);
 | 
						|
      position: relative;
 | 
						|
      transition: color var(--transition-speed);
 | 
						|
      white-space: nowrap;
 | 
						|
      z-index: 1;
 | 
						|
    }
 | 
						|
 | 
						|
    .suggestion:where(:focus, :hover) {
 | 
						|
      color: var(--color-background);
 | 
						|
    }
 | 
						|
 | 
						|
    .suggestion::before {
 | 
						|
      background-color: var(--color-text);
 | 
						|
      bottom: var(--space-2);
 | 
						|
      content: ' ';
 | 
						|
      left: var(--space-2);
 | 
						|
      opacity: 0;
 | 
						|
      position: absolute;
 | 
						|
      right: var(--space-2);
 | 
						|
      top: var(--space-2);
 | 
						|
      transform: translateY(0.5em);
 | 
						|
      transition: all var(--transition-speed);
 | 
						|
      z-index: -1;
 | 
						|
    }
 | 
						|
 | 
						|
    .suggestion:where(:focus, :hover)::before {
 | 
						|
      opacity: 1;
 | 
						|
      transform: translateY(0);
 | 
						|
    }
 | 
						|
 | 
						|
    .match {
 | 
						|
      color: var(--color-text-subtle);
 | 
						|
      transition: color var(--transition-speed);
 | 
						|
    }
 | 
						|
 | 
						|
    .suggestion:where(:focus, :hover) .match {
 | 
						|
      color: var(--color-background);
 | 
						|
    }
 | 
						|
 | 
						|
    @media (min-width: 700px) {
 | 
						|
      .suggestions {
 | 
						|
        flex-direction: row;
 | 
						|
      }
 | 
						|
    }
 | 
						|
  </style>
 | 
						|
  <dialog class="dialog">
 | 
						|
    <form autocomplete="off" class="form" method="dialog" spellcheck="false">
 | 
						|
      <input class="input" title="search" type="text" />
 | 
						|
      <menu class="suggestions"></menu>
 | 
						|
    </form>
 | 
						|
  </dialog>
 | 
						|
</template>
 | 
						|
 | 
						|
<template id="suggestion-template">
 | 
						|
  <li>
 | 
						|
    <button class="suggestion" type="button"></button>
 | 
						|
  </li>
 | 
						|
</template>
 | 
						|
 | 
						|
<template id="match-template">
 | 
						|
  <span class="match"></span>
 | 
						|
</template>
 | 
						|
 | 
						|
<script type="module">
 | 
						|
  class Search extends HTMLElement {
 | 
						|
    #dialog;
 | 
						|
    #form;
 | 
						|
    #input;
 | 
						|
    #suggestions;
 | 
						|
 | 
						|
    constructor() {
 | 
						|
      super();
 | 
						|
      this.attachShadow({ mode: 'open' });
 | 
						|
      const template = document.getElementById('search-template');
 | 
						|
      const clone = template.content.cloneNode(true);
 | 
						|
      this.#dialog = clone.querySelector('.dialog');
 | 
						|
      this.#form = clone.querySelector('.form');
 | 
						|
      this.#input = clone.querySelector('.input');
 | 
						|
      this.#suggestions = clone.querySelector('.suggestions');
 | 
						|
      this.#form.addEventListener('submit', this.#onSubmit, false);
 | 
						|
      this.#input.addEventListener('input', this.#onInput);
 | 
						|
      this.#suggestions.addEventListener('click', this.#onSuggestionClick);
 | 
						|
      document.addEventListener('keydown', this.#onKeydown);
 | 
						|
      this.shadowRoot.append(clone);
 | 
						|
      const shadows = document.querySelector('#logo path#shadows');
 | 
						|
    }
 | 
						|
 | 
						|
    static #attachSearchPrefix(array, { key, splitBy }) {
 | 
						|
      if (!splitBy) return array;
 | 
						|
      return array.map((search) => `${key}${splitBy}${search}`);
 | 
						|
    }
 | 
						|
 | 
						|
    static #escapeRegexCharacters(s) {
 | 
						|
      return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
 | 
						|
    }
 | 
						|
 | 
						|
    static #fetchDuckDuckGoSuggestions(search) {
 | 
						|
      return new Promise((resolve) => {
 | 
						|
        window.autocompleteCallback = (res) => {
 | 
						|
          const suggestions = [];
 | 
						|
 | 
						|
          for (const item of res) {
 | 
						|
            if (item.phrase === search.toLowerCase()) continue;
 | 
						|
            suggestions.push(item.phrase);
 | 
						|
          }
 | 
						|
 | 
						|
          resolve(suggestions);
 | 
						|
        };
 | 
						|
 | 
						|
        const script = document.createElement('script');
 | 
						|
        document.querySelector('head').appendChild(script);
 | 
						|
        script.src = `https://duckduckgo.com/ac/?callback=autocompleteCallback&q=${search}`;
 | 
						|
        script.onload = script.remove;
 | 
						|
      });
 | 
						|
    }
 | 
						|
 | 
						|
    static #formatSearchUrl(url, searchPath, search) {
 | 
						|
      if (!searchPath) return url;
 | 
						|
      const [baseUrl] = Search.#splitUrl(url);
 | 
						|
      const urlQuery = encodeURIComponent(search);
 | 
						|
      searchPath = searchPath.replace(/{}/g, urlQuery);
 | 
						|
      return baseUrl + searchPath;
 | 
						|
    }
 | 
						|
 | 
						|
    static #hasProtocol(s) {
 | 
						|
      return /^[a-zA-Z]+:\/\//i.test(s);
 | 
						|
    }
 | 
						|
 | 
						|
    static #isUrl(s) {
 | 
						|
      return /^((https?:\/\/)?[\w-]+(\.[\w-]+)+\.?(:\d+)?(\/\S*)?)$/i.test(s);
 | 
						|
    }
 | 
						|
 | 
						|
    static #parseQuery = (raw) => {
 | 
						|
      const query = raw.trim();
 | 
						|
 | 
						|
      if (this.#isUrl(query)) {
 | 
						|
        const url = this.#hasProtocol(query) ? query : `https://${query}`;
 | 
						|
        return { query, url };
 | 
						|
      }
 | 
						|
 | 
						|
      if (COMMANDS.has(query)) {
 | 
						|
        const { command, key, url } = COMMANDS.get(query);
 | 
						|
        return command ? Search.#parseQuery(command) : { key, query, url };
 | 
						|
      }
 | 
						|
 | 
						|
      let splitBy = CONFIG.commandSearchDelimiter;
 | 
						|
      const [searchKey, rawSearch] = query.split(new RegExp(`${splitBy}(.*)`));
 | 
						|
 | 
						|
      if (COMMANDS.has(searchKey)) {
 | 
						|
        const { searchTemplate, url: base } = COMMANDS.get(searchKey);
 | 
						|
        const search = rawSearch.trim();
 | 
						|
        const url = Search.#formatSearchUrl(base, searchTemplate, search);
 | 
						|
        return { key: searchKey, query, search, splitBy, url };
 | 
						|
      }
 | 
						|
 | 
						|
      splitBy = CONFIG.commandPathDelimiter;
 | 
						|
      const [pathKey, path] = query.split(new RegExp(`${splitBy}(.*)`));
 | 
						|
 | 
						|
      if (COMMANDS.has(pathKey)) {
 | 
						|
        const { url: base } = COMMANDS.get(pathKey);
 | 
						|
        const [baseUrl] = Search.#splitUrl(base);
 | 
						|
        const url = `${baseUrl}/${path}`;
 | 
						|
        return { key: pathKey, path, query, splitBy, url };
 | 
						|
      }
 | 
						|
 | 
						|
      const [baseUrl, rest] = Search.#splitUrl(CONFIG.defaultSearchTemplate);
 | 
						|
      const url = Search.#formatSearchUrl(baseUrl, rest, query);
 | 
						|
      return { query, search: query, url };
 | 
						|
    };
 | 
						|
 | 
						|
    static #splitUrl(url) {
 | 
						|
      const parser = document.createElement('a');
 | 
						|
      parser.href = url;
 | 
						|
      const baseUrl = `${parser.protocol}//${parser.hostname}`;
 | 
						|
      const rest = `${parser.pathname}${parser.search}`;
 | 
						|
      return [baseUrl, rest];
 | 
						|
    }
 | 
						|
 | 
						|
    #close() {
 | 
						|
      this.#input.value = '';
 | 
						|
      this.#input.blur();
 | 
						|
      this.#dialog.close();
 | 
						|
      this.#suggestions.innerHTML = '';
 | 
						|
      shadows.style.fill = '#808080'
 | 
						|
    }
 | 
						|
 | 
						|
    #execute(query) {
 | 
						|
      const { url } = Search.#parseQuery(query);
 | 
						|
      const target = CONFIG.openLinksInNewTab ? '_blank' : '_self';
 | 
						|
      window.open(url, target, 'noopener noreferrer');
 | 
						|
      this.#close();
 | 
						|
    }
 | 
						|
 | 
						|
    #focusNextSuggestion(previous = false) {
 | 
						|
      const active = this.shadowRoot.activeElement;
 | 
						|
      let nextIndex;
 | 
						|
 | 
						|
      if (active.dataset.index) {
 | 
						|
        const activeIndex = Number(active.dataset.index);
 | 
						|
        nextIndex = previous ? activeIndex - 1 : activeIndex + 1;
 | 
						|
      } else {
 | 
						|
        nextIndex = previous ? this.#suggestions.childElementCount - 1 : 0;
 | 
						|
      }
 | 
						|
 | 
						|
      const next = this.#suggestions.children[nextIndex];
 | 
						|
      if (next) next.querySelector('.suggestion').focus();
 | 
						|
      else this.#input.focus();
 | 
						|
    }
 | 
						|
 | 
						|
    #onInput = async () => {
 | 
						|
      shadows.style.fill = '#ff4000'
 | 
						|
      const oq = Search.#parseQuery(this.#input.value);
 | 
						|
 | 
						|
      if (!oq.query) {
 | 
						|
        this.#close();
 | 
						|
        return;
 | 
						|
      }
 | 
						|
 | 
						|
      let suggestions = COMMANDS.get(oq.query)?.suggestions ?? [];
 | 
						|
 | 
						|
      if (oq.search && suggestions.length < CONFIG.suggestionLimit) {
 | 
						|
        const res = await Search.#fetchDuckDuckGoSuggestions(oq.search);
 | 
						|
        const formatted = Search.#attachSearchPrefix(res, oq);
 | 
						|
        suggestions = suggestions.concat(formatted);
 | 
						|
      }
 | 
						|
 | 
						|
      const nq = Search.#parseQuery(this.#input.value);
 | 
						|
      if (nq.query !== oq.query) return;
 | 
						|
      this.#renderSuggestions(suggestions, oq.query);
 | 
						|
    };
 | 
						|
 | 
						|
    #onKeydown = (e) => {
 | 
						|
      if (!this.#dialog.open) {
 | 
						|
        this.#dialog.show();
 | 
						|
        this.#input.focus();
 | 
						|
 | 
						|
        requestAnimationFrame(() => {
 | 
						|
          // close the search dialog before the next repaint if a character is
 | 
						|
          // not produced (e.g. if you type shift, control, alt etc.)
 | 
						|
          if (!this.#input.value) this.#close();
 | 
						|
        });
 | 
						|
 | 
						|
        return;
 | 
						|
      }
 | 
						|
 | 
						|
      if (e.key === 'Escape') {
 | 
						|
        this.#close();
 | 
						|
        return;
 | 
						|
      }
 | 
						|
 | 
						|
      const alt = e.altKey ? 'alt-' : '';
 | 
						|
      const ctrl = e.ctrlKey ? 'ctrl-' : '';
 | 
						|
      const meta = e.metaKey ? 'meta-' : '';
 | 
						|
      const shift = e.shiftKey ? 'shift-' : '';
 | 
						|
      const modifierPrefixedKey = `${alt}${ctrl}${meta}${shift}${e.key}`;
 | 
						|
 | 
						|
      if (/^(ArrowDown|Tab|ctrl-n)$/.test(modifierPrefixedKey)) {
 | 
						|
        e.preventDefault();
 | 
						|
        this.#focusNextSuggestion();
 | 
						|
        return;
 | 
						|
      }
 | 
						|
 | 
						|
      if (/^(ArrowUp|ctrl-p|shift-Tab)$/.test(modifierPrefixedKey)) {
 | 
						|
        e.preventDefault();
 | 
						|
        this.#focusNextSuggestion(true);
 | 
						|
      }
 | 
						|
    };
 | 
						|
 | 
						|
    #onSubmit = () => {
 | 
						|
      this.#execute(this.#input.value);
 | 
						|
    };
 | 
						|
 | 
						|
    #onSuggestionClick = (e) => {
 | 
						|
      const ref = e.target.closest('.suggestion');
 | 
						|
      if (!ref) return;
 | 
						|
      this.#execute(ref.dataset.suggestion);
 | 
						|
    };
 | 
						|
 | 
						|
    #renderSuggestions(suggestions, query) {
 | 
						|
      this.#suggestions.innerHTML = '';
 | 
						|
      const sliced = suggestions.slice(0, CONFIG.suggestionLimit);
 | 
						|
      const template = document.getElementById('suggestion-template');
 | 
						|
 | 
						|
      for (const [index, suggestion] of sliced.entries()) {
 | 
						|
        const clone = template.content.cloneNode(true);
 | 
						|
        const ref = clone.querySelector('.suggestion');
 | 
						|
        ref.dataset.index = index;
 | 
						|
        ref.dataset.suggestion = suggestion;
 | 
						|
        const escapedQuery = Search.#escapeRegexCharacters(query);
 | 
						|
        const matched = suggestion.match(new RegExp(escapedQuery, 'i'));
 | 
						|
 | 
						|
        if (matched) {
 | 
						|
          const template = document.getElementById('match-template');
 | 
						|
          const clone = template.content.cloneNode(true);
 | 
						|
          const matchRef = clone.querySelector('.match');
 | 
						|
          const pre = suggestion.slice(0, matched.index);
 | 
						|
          const post = suggestion.slice(matched.index + matched[0].length);
 | 
						|
          matchRef.innerText = matched[0];
 | 
						|
          matchRef.insertAdjacentHTML('beforebegin', pre);
 | 
						|
          matchRef.insertAdjacentHTML('afterend', post);
 | 
						|
          ref.append(clone);
 | 
						|
        } else {
 | 
						|
          ref.innerText = suggestion;
 | 
						|
        }
 | 
						|
 | 
						|
        this.#suggestions.append(clone);
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  customElements.define('search-component', Search);
 | 
						|
</script>
 | 
						|
 | 
						|
<style>
 | 
						|
  html {
 | 
						|
    background-color: var(--color-background);
 | 
						|
    font-family: var(--font-family);
 | 
						|
    font-size: var(--font-size-base);
 | 
						|
    line-height: var(--line-height-base);
 | 
						|
  }
 | 
						|
 | 
						|
  body {
 | 
						|
    margin: 0;
 | 
						|
    padding: 0;
 | 
						|
  }
 | 
						|
 | 
						|
  .box-container {
 | 
						|
    display: flex;
 | 
						|
    flex-direction: column;
 | 
						|
    align-items: center;
 | 
						|
    justify-content: center;
 | 
						|
    background: var(--color-background);
 | 
						|
    min-height: 70vh;
 | 
						|
    margin: 5vh 15vw 15vh 15vw;
 | 
						|
    padding: 0;
 | 
						|
  }
 | 
						|
 | 
						|
  main {
 | 
						|
    align-items: center;
 | 
						|
    box-sizing: border-box;
 | 
						|
    display: flex;
 | 
						|
    justify-content: center;
 | 
						|
    padding: 0 10vw 15vh 10vw;
 | 
						|
    position: relative;
 | 
						|
    width: 100%;
 | 
						|
  }
 | 
						|
 | 
						|
  #logo {
 | 
						|
    padding: 15vh 10vw 10vh 10vw;
 | 
						|
    width: 200px;
 | 
						|
  }
 | 
						|
  #logo path#characters {
 | 
						|
    fill-opacity: 0.5 !important;
 | 
						|
    transition: fill 2s, fill-opacity 1s;
 | 
						|
  }
 | 
						|
  #logo path#characters:hover {
 | 
						|
    fill: #ff4000 !important;
 | 
						|
    fill-opacity: 0.8 !important;
 | 
						|
    transition: fill 2s, fill-opacity 1s;
 | 
						|
  }
 | 
						|
  #logo path#shadows {
 | 
						|
    fill: #808080;
 | 
						|
    transition: fill 2s;
 | 
						|
  }
 | 
						|
</style>
 | 
						|
 | 
						|
<div class="box-container">
 | 
						|
 | 
						|
  <svg
 | 
						|
     preserveAspectRatio=true
 | 
						|
     viewBox="-1 -13.22 54.916672 30.471189"
 | 
						|
     version="1.1"
 | 
						|
     id="logo"
 | 
						|
     xmlns="http://www.w3.org/2000/svg"
 | 
						|
     xmlns:svg="http://www.w3.org/2000/svg">
 | 
						|
    <g
 | 
						|
       id="trigram"
 | 
						|
       style="shape-inside:url(#rect835);display:inline"
 | 
						|
       transform="matrix(0.57142858,0,0,0.57142858,-1.930636,-81.450047)">
 | 
						|
      <path
 | 
						|
         id="shadows"
 | 
						|
         style="shape-inside:url(#rect835);display:inline;fill:#808080;fill-opacity:0.503856;stroke:none;stroke-width:0.401402;stroke-linecap:square;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:markers stroke fill"
 | 
						|
         d="m 206.65832,477.40693 v 4.35548 h 12.42184 v 34.65225 25.84255 H 205.95563 181.954 157.95237 147.04042 v 32.72422 h 0.0348 v 57.05107 57.81182 l 0.0465,57.05687 h -0.0465 v 26.5975 h -13.11873 -0.48201 -23.51962 -13.600734 v -33.94956 h -4.355487 v 38.29924 h 17.956221 23.51962 0.48201 17.47421 v -29.43728 h 0.0406 l -0.0406,-58.56677 V 632.0325 573.47153 h -0.0406 v -26.86464 h 6.56227 24.00163 24.00163 17.47421 v -30.19223 -39.00773 z m 71.99909,0 v 4.35548 h 12.42185 v 34.65225 57.10333 h 4.34968 v -57.10333 -39.00773 z m 120.00236,0 v 4.35548 h 12.41604 v 34.65225 25.84255 h -13.12453 -10.91776 l 0.0465,31.96927 v 25.84255 H 373.95545 363.0377 v 31.26078 h 4.34968 v -26.9111 h 6.56807 17.47421 v -30.19223 l -0.0407,-27.61959 h 6.56227 17.48001 v -30.19223 -39.00773 z m 167.99982,0 v 4.35548 h 12.41603 v 33.94376 h 4.34968 v -38.29924 z m -360.00127,7.03846 v 4.35549 h 5.38338 v 27.61378 18.80409 H 205.95563 181.954 157.95237 139.99615 v 39.00773 57.80602 57.81182 l 0.0406,57.81182 v 18.80409 h -6.08026 -0.48201 -23.51962 -6.56227 v -26.9111 h -4.355485 v 31.26077 h 10.917755 23.51962 0.48201 10.43575 v -23.15376 l -0.0407,-57.81182 V 632.0325 574.22648 539.56843 h 13.60074 24.00163 24.00163 10.43575 v -23.15377 -31.96927 z m 71.99909,0 v 4.35549 h 5.38338 v 26.85883 h -0.0407 v 57.85828 h 4.34968 v -56.34838 h 0.0406 v -32.72422 z m 120.00236,0 v 4.35549 h 5.37757 v 27.61378 18.80409 h -6.08606 -17.95042 l 0.0349,39.00773 v 18.79828 h -6.08026 -17.95622 v 38.30505 h 4.34968 v -33.94956 h 13.60654 10.43575 v -23.15377 l -0.0407,-34.65805 h 13.60074 10.43574 v -23.15377 -31.96927 z m 167.99982,0 v 4.35549 h 5.37757 v 26.90529 h 4.34968 V 484.44539 Z M 19.999597,508.30765 v 38.29924 h 17.956219 24.001633 22.811133 v -4.34968 H 61.957449 37.955816 24.349276 v -33.94956 z m 7.038466,0 v 31.26078 h 10.917753 24.001633 22.811133 v -4.34968 H 61.957449 37.955816 31.387742 v -26.9111 z m 448.957717,26.9111 v 0.75495 37.54429 h 4.35549 v -33.94956 h 13.60073 24.00163 22.81114 v -4.34968 H 517.95363 493.952 Z m 114.65963,0 v 4.34968 h 12.42185 v 34.65805 57.80602 57.81182 57.81182 25.84255 h -13.12453 -0.48201 -23.51963 -13.60073 v -33.94956 h -4.35549 v 38.29924 h 17.95622 23.51963 0.48201 17.47421 V 747.65614 689.84432 632.0325 574.22648 535.21875 Z m -107.62116,7.03846 v 0.75495 30.50583 h 4.35548 v -26.9111 h 6.56227 24.00163 22.81114 v -4.34968 H 517.95363 493.952 Z m 107.62116,0 v 4.34968 h 5.38338 v 26.86464 h -0.0406 v 58.56097 57.81182 58.56677 h 0.0406 v 18.04914 h -6.08606 -0.48201 -23.51963 -6.56226 v -26.9111 h -4.34968 v 31.26077 h 10.91194 23.51963 0.48201 10.43574 v -23.90871 h -0.0407 V 689.84432 632.0325 574.98143 h 0.0407 V 542.25721 Z M 284.00014,650.83658 v 0.75495 38.25279 l 0.0407,57.81182 v 18.80409 h -6.08607 -24.00163 -6.56226 v -26.9111 h -4.34968 v 31.26077 h 10.91194 24.00163 10.43575 v -23.15376 l -0.0406,-57.81182 v -34.65225 h 13.60073 22.81694 v -4.35549 h -22.81694 z m 90.658,0 v 4.35549 h 12.42184 v 33.94376 h 4.34968 v -38.29925 z m 101.33764,0 v 0.75495 38.25279 l 0.0406,57.81182 v 18.80409 h -6.08606 -23.99583 -6.56807 v -26.9111 h -4.34968 v 31.26077 h 10.91775 23.99583 10.43574 v -23.15376 l -0.0348,-57.81182 v -34.65225 h 13.60073 24.00163 22.81114 v -4.35549 H 517.95363 493.952 Z m -184.95717,7.04428 v 0.75495 31.96346 h 0.0406 l 0.0407,56.30192 h -0.0407 v 26.5975 h -13.12454 -24.00163 -13.60073 v -33.94956 h -4.35549 v 38.29924 h 17.95622 24.00163 17.47422 v -29.43728 h 0.0406 l -0.0406,-59.32172 h -0.0407 v -26.85884 h 6.56227 22.81694 v -4.34967 h -22.81694 z m 83.61953,0 v 4.34967 h 5.37757 v 26.9053 h 4.35549 v -31.25497 z m 108.37611,0 v 0.75495 31.96346 h 0.0407 l 0.0406,56.30192 h -0.0406 v 26.5975 H 469.95037 445.95454 432.348 v -33.94956 h -4.34968 v 38.29924 h 17.95622 23.99583 17.48002 v -29.43728 h 0.0407 l -0.0407,-59.32172 h -0.0407 v -26.85884 h 6.56227 24.00163 22.81114 v -4.34967 H 517.95363 493.952 Z M 331.9976,681.73731 v 38.29924 h 16.77152 v -4.34968 h -12.42184 v -33.94956 z m 7.03846,0 v 31.26077 h 9.73306 v -4.34968 h -5.37757 v -26.91109 z m 59.62371,26.91109 v 4.34968 h 12.41604 v 34.65806 25.84255 h -13.12453 -23.99583 -13.60654 v -33.94956 h -4.34968 v 38.29924 h 17.95622 23.99583 17.48001 V 747.65614 708.6484 Z m 0,7.03847 v 4.34968 h 5.37757 v 27.61959 18.80409 h -6.08606 -23.99583 -6.56807 v -26.9111 h -4.34968 v 31.26077 h 10.91775 23.99583 10.43574 v -23.15376 -31.96927 z"
 | 
						|
         transform="matrix(0.15572681,0,0,0.15572681,1.390094,49.845283)" />
 | 
						|
      <path
 | 
						|
         id="characters"
 | 
						|
         style="shape-inside:url(#rect835);display:inline;fill:#9e9e9e;fill-opacity:1;stroke:none;stroke-width:1.88976;stroke-linecap:square;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
 | 
						|
         d="m 12.76949,457.90016 v 0.94078 49.46671 h 24.948225 23.995826 24.001633 24.001636 23.99582 24.00164 24.00163 24.94242 V 457.90016 H 181.7159 157.71427 133.71263 109.71681 85.715174 61.713541 37.717715 Z m 215.99728,0 v 0.94078 49.46671 h 24.94822 24.94242 v -50.40749 h -24.94242 z m 120.00235,0 v 0.94078 49.46671 h 24.94242 24.94823 v -50.40749 h -24.94823 z m 96.00073,0 v 0.94078 49.46671 h 24.94242 24.00163 24.00163 23.99583 24.94822 V 457.90016 H 541.71136 517.71553 493.7139 469.71227 Z M 84.768582,515.70617 v 0.9466 49.4667 h 24.948228 24.94242 v -50.4133 h -24.94242 z m 143.998188,0 v 0.9466 49.4667 h 24.94822 24.94242 v -50.4133 h -24.94242 z m 96.00072,0 v 0.9466 49.4667 h 24.94823 24.94241 v -50.4133 h -24.94241 z m 96.00072,0 v 0.9466 49.4667 h 24.94242 24.94823 v -50.4133 h -24.94823 z m 119.99656,0 v 0.9466 49.4667 h 24.94822 24.94242 v -50.4133 H 565.71299 Z M 84.768582,573.51799 v 0.9466 49.46671 h 24.948228 24.94242 v -50.41331 h -24.94242 z m 143.998188,0 v 0.9466 49.46671 h 24.94822 24.00163 23.99583 24.00163 24.94242 v -50.41331 h -24.94242 -24.00163 -23.99583 -24.00163 z m 192.00144,0 v 0.9466 49.46671 h 24.94242 24.00164 24.00163 24.00163 23.99583 24.00163 24.94242 V 573.51799 H 565.71299 541.71136 517.71553 493.7139 469.71227 445.71063 Z M 84.768582,631.32982 v 0.94078 49.46671 h 24.948228 24.94242 v -50.40749 h -24.94242 z m 143.998188,0 v 0.94078 49.46671 h 24.94822 24.94242 v -50.40749 h -24.94242 z m 96.00072,0 v 0.94078 49.46671 h 24.94823 24.94241 v -50.40749 h -24.94241 z m 96.00072,0 v 0.94078 49.46671 h 24.94242 24.94823 v -50.40749 h -24.94823 z m 119.99656,0 v 0.94078 49.46671 h 24.94822 24.94242 V 631.32982 H 565.71299 Z M 84.768582,689.13583 v 0.94659 49.46671 h 24.948228 24.94242 v -50.4133 h -24.94242 z m 143.998188,0 v 0.94659 49.46671 h 24.94822 24.94242 v -50.4133 h -24.94242 z m 120.00235,0 v 0.94659 49.46671 h 24.94242 24.94823 v -50.4133 h -24.94823 z m 71.99909,0 v 0.94659 49.46671 h 24.94242 24.94823 v -50.4133 h -24.94823 z m 119.99656,0 v 0.94659 49.46671 h 24.94822 24.94242 v -50.4133 h -24.94242 z"
 | 
						|
         transform="matrix(0.15572681,0,0,0.15572681,1.390094,49.845283)" />
 | 
						|
    </g>
 | 
						|
    <defs
 | 
						|
       id="defs2">
 | 
						|
      <rect
 | 
						|
         x="3.6285765"
 | 
						|
         y="28.230612"
 | 
						|
         width="198.25528"
 | 
						|
         height="291.22577"
 | 
						|
         id="rect835" />
 | 
						|
    </defs>
 | 
						|
  </svg>
 | 
						|
 | 
						|
  <main>
 | 
						|
    <commands-component></commands-component>
 | 
						|
    <search-component></search-component>
 | 
						|
  </main>
 | 
						|
 | 
						|
</div>
 |