tilde/index.html

551 lines
16 KiB
HTML
Raw Normal View History

2023-08-16 08:02:02 +00:00
<!doctype html>
2021-01-12 10:32:47 +00:00
<meta charset="utf-8" />
2022-07-24 03:07:50 +00:00
<meta name="color-scheme" content="dark light" />
2021-01-16 07:32:59 +00:00
<meta name="robots" content="noindex" />
2022-07-24 03:07:50 +00:00
<meta name="viewport" content="width=device-width, initial-scale=1" />
2021-01-16 07:32:59 +00:00
<title>~</title>
2016-02-01 02:28:14 +00:00
2023-03-30 23:26:24 +00:00
<style>
:root {
2024-01-31 03:50:09 +00:00
--border-radius: 1rem;
2023-03-30 23:26:24 +00:00
--color-background: #111;
2024-01-26 21:00:51 +00:00
--color-text-subtle: #888;
2024-09-24 21:24:09 +00:00
--color-text: #ddd;
2023-03-30 23:26:24 +00:00
--font-family: -apple-system, Helvetica, sans-serif;
2024-11-18 22:36:17 +00:00
--font-size: clamp(16px, 1.3vw, 18px);
2023-03-30 23:26:24 +00:00
--font-weight-bold: 700;
--font-weight-normal: 400;
2024-01-31 03:50:09 +00:00
--space: 1rem;
2023-03-30 23:26:24 +00:00
--transition-speed: 200ms;
}
@media (prefers-color-scheme: light) {
:root {
2024-01-26 21:00:51 +00:00
--color-background: #e8e8e8;
--color-text-subtle: #606060;
--color-text: #111;
2023-03-30 23:26:24 +00:00
}
}
</style>
<script>
2017-03-03 07:37:14 +00:00
const CONFIG = {
2022-08-05 16:32:34 +00:00
commandPathDelimiter: '/',
commandSearchDelimiter: ' ',
defaultSearchTemplate: 'https://duckduckgo.com/?q={}',
2022-08-04 14:15:29 +00:00
openLinksInNewTab: true,
2024-07-10 17:19:46 +00:00
suggestionLimit: 4,
2022-08-05 16:32:34 +00:00
};
2024-06-18 19:34:57 +00:00
// prettier-ignore
const COMMANDS = new Map([
2024-12-04 21:19:56 +00:00
['a', { name: 'Alphabet', suggestions: ['a/drive', 'a/contacts'], url: 'https://www.google.com' }],
['a/drive', { url: 'https://drive.google.com/drive/u/0/my-drive' }],
['a/contacts', { url: 'https://contacts.google.com' }],
['c', { name: 'Cloudflare', url: 'https://dash.cloudflare.com' }],
['d', { name: 'Droplet', url: 'https://cloud.digitalocean.com/projects/50ffcff7-ad65-40e3-af88-3cbc5a38cf99/resources' }],
2024-11-18 22:36:17 +00:00
['e', { name: 'Resend', url: 'https://resend.com/emails?time=all&page=1' }],
2024-01-24 06:38:48 +00:00
['f', { name: 'Figma', url: 'https://www.figma.com' }],
2024-12-04 21:19:56 +00:00
['g', { name: 'GitHub', searchTemplate: '/search?q={}', suggestions: ['g/copilot', 'g/trending'], url: 'https://github.com' }],
2024-09-24 19:46:06 +00:00
['i', { name: 'Intuit', url: 'https://selfemployed.intuit.com/home' }],
2024-12-04 21:19:56 +00:00
['o', { name: 'ChatGPT', searchTemplate: '?q={}', url: 'https://chatgpt.com' }],
['p', { name: 'Proton', suggestions: ['p/pass', 'p/drive'], url: 'https://mail.proton.me/u/0/inbox' }],
['p/drive', { url: 'https://drive.proton.me/u/0' }],
['p/pass', { url: 'https://pass.proton.me/u/0' }],
2024-11-18 22:36:17 +00:00
['r', { name: 'Reddit', searchTemplate: '/search/?q={}', url: 'https://www.reddit.com' }],
2024-12-04 21:19:56 +00:00
['s', { name: 'Supabase', url: 'https://supabase.com/dashboard/projects' }],
['t', { name: 'TickTick', url: 'https://ticktick.com/webapp/#q/today/tasks' }],
2024-11-19 20:15:53 +00:00
['v', { name: 'Vercel', suggestions: ['v0.dev'], url: 'https://vercel.com/dashboard' }],
2024-12-04 21:19:56 +00:00
['x', { name: 'xvvvyz', suggestions: ['x/jelly', 'x/torrent'], url: 'https://xvvvyz.xyz' }],
['x/jelly', { url: 'https://jelly.xvvvyz.xyz' }],
['x/torrent', { url: 'https://torrent.xvvvyz.xyz' }],
['y', { name: 'YouTube', searchTemplate: '/results?search_query={}', url: 'https://www.youtube.com/feed/subscriptions' }],
['0', { name: 'localhost', suggestions: ['0:54323', '0:54324'], url: 'http://localhost:3000' }],
['0:54323', { url: 'http://localhost:54323' }],
['0:54324', { url: 'http://localhost:54324' }],
]);
</script>
2022-07-24 03:07:50 +00:00
<template id="commands-template">
<style>
.commands {
2024-11-18 22:36:17 +00:00
border-radius: var(--border-radius);
2024-01-24 06:38:48 +00:00
column-gap: 0;
2024-04-09 06:56:40 +00:00
columns: 2;
2022-07-24 03:07:50 +00:00
list-style: none;
margin: 0 auto;
2024-09-22 17:19:35 +00:00
max-width: 23rem;
2024-01-26 21:00:51 +00:00
overflow: hidden;
2024-01-24 06:38:48 +00:00
padding: 0;
2023-01-17 20:48:59 +00:00
width: 100vw;
}
2022-07-24 03:07:50 +00:00
.command {
display: flex;
2024-01-31 03:50:09 +00:00
gap: var(--space);
2022-07-24 03:07:50 +00:00
outline: 0;
2024-01-31 03:50:09 +00:00
padding: var(--space);
2024-01-24 06:38:48 +00:00
position: relative;
2022-07-24 03:07:50 +00:00
text-decoration: none;
2016-06-01 03:14:45 +00:00
}
2024-01-24 06:38:48 +00:00
.command::after {
background: var(--color-text-subtle);
content: ' ';
inset: 1px;
opacity: 0.05;
position: absolute;
transition: opacity var(--transition-speed);
}
.command:where(:focus, :hover)::after {
opacity: 0.1;
}
2023-01-09 09:07:23 +00:00
.key {
color: var(--color-text);
display: inline-block;
text-align: center;
width: 3ch;
}
.name {
color: var(--color-text-subtle);
2023-01-09 09:07:23 +00:00
transition: color var(--transition-speed);
2022-12-16 08:03:02 +00:00
}
.command:where(:focus, :hover) .name {
2022-12-03 04:44:27 +00:00
color: var(--color-text);
}
2024-12-04 21:19:56 +00:00
@media (min-width: 55rem) {
.commands {
2024-12-04 21:19:56 +00:00
columns: 4;
max-width: 45rem;
}
}
2022-07-24 03:07:50 +00:00
</style>
<nav>
<menu class="commands"></menu>
</nav>
</template>
<template id="command-template">
<li>
2022-08-04 14:15:29 +00:00
<a class="command" rel="noopener noreferrer">
2022-07-24 03:07:50 +00:00
<span class="key"></span>
<span class="name"></span>
</a>
</li>
</template>
<script type="module">
2022-07-30 20:25:30 +00:00
class Commands extends HTMLElement {
2022-07-24 03:07:50 +00:00
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');
2022-07-30 21:03:40 +00:00
for (const [key, { name, url }] of COMMANDS.entries()) {
2022-08-05 16:32:34 +00:00
if (!name || !url) continue;
const clone = commandTemplate.content.cloneNode(true);
2022-08-04 14:15:29 +00:00
const command = clone.querySelector('.command');
command.href = url;
if (CONFIG.openLinksInNewTab) command.target = '_blank';
2022-07-24 03:07:50 +00:00
clone.querySelector('.key').innerText = key;
clone.querySelector('.name').innerText = name;
commands.append(clone);
2022-07-24 03:07:50 +00:00
}
this.shadowRoot.append(clone);
2017-03-04 04:03:53 +00:00
}
2017-03-03 07:37:14 +00:00
}
2016-06-01 03:14:45 +00:00
2022-07-30 20:25:30 +00:00
customElements.define('commands-component', Commands);
2022-07-24 03:07:50 +00:00
</script>
2017-07-11 06:04:57 +00:00
2022-07-24 03:07:50 +00:00
<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;
2022-07-28 02:39:53 +00:00
height: 100%;
2022-07-24 03:07:50 +00:00
justify-content: center;
left: 0;
padding: 0;
top: 0;
2022-07-28 02:39:53 +00:00
width: 100%;
2022-07-24 03:07:50 +00:00
}
.dialog[open] {
display: flex;
}
2022-07-28 02:39:53 +00:00
.form {
width: 100%;
}
2022-07-24 03:07:50 +00:00
.input {
color: var(--color-text);
2024-01-31 03:50:09 +00:00
font-size: 3rem;
2022-07-24 03:07:50 +00:00
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;
2024-01-31 03:50:09 +00:00
margin: var(--space) 0 0;
2022-07-24 03:07:50 +00:00
overflow: hidden;
padding: 0;
}
.suggestion {
color: var(--color-text);
2022-07-24 03:07:50 +00:00
cursor: pointer;
2024-01-31 03:50:09 +00:00
font-size: 1rem;
padding: var(--space);
2022-07-24 03:07:50 +00:00
position: relative;
transition: color var(--transition-speed);
white-space: nowrap;
z-index: 1;
2022-07-24 03:07:50 +00:00
}
2022-11-17 23:15:48 +00:00
.suggestion:where(:focus, :hover) {
2022-07-24 03:07:50 +00:00
color: var(--color-background);
}
.suggestion::before {
background-color: var(--color-text);
2024-01-31 03:50:09 +00:00
border-radius: calc(var(--border-radius) / 5);
2022-07-24 03:07:50 +00:00
content: ' ';
2024-01-31 03:50:09 +00:00
inset: calc(var(--space) / 1.5) calc(var(--space) / 3);
2022-07-24 03:07:50 +00:00
opacity: 0;
position: absolute;
transform: translateY(0.5em);
transition: all var(--transition-speed);
z-index: -1;
}
2022-11-17 23:15:48 +00:00
.suggestion:where(:focus, :hover)::before {
2022-07-24 03:07:50 +00:00
opacity: 1;
transform: translateY(0);
}
.match {
color: var(--color-text-subtle);
transition: color var(--transition-speed);
}
2022-11-17 23:15:48 +00:00
.suggestion:where(:focus, :hover) .match {
2022-07-24 03:07:50 +00:00
color: var(--color-background);
}
@media (min-width: 700px) {
.suggestions {
flex-direction: row;
}
2021-01-17 10:40:04 +00:00
}
2022-07-24 03:07:50 +00:00
</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">
2022-07-30 20:25:30 +00:00
class Search extends HTMLElement {
#dialog;
#form;
#input;
#suggestions;
2017-07-02 06:52:47 +00:00
2022-07-24 03:07:50 +00:00
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);
2022-07-24 03:07:50 +00:00
document.addEventListener('keydown', this.#onKeydown);
this.shadowRoot.append(clone);
2019-01-17 23:14:21 +00:00
}
2022-07-24 03:07:50 +00:00
static #escapeRegexCharacters(s) {
return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
}
2017-06-28 05:13:49 +00:00
2022-07-25 04:35:59 +00:00
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);
};
2022-07-25 04:35:59 +00:00
const script = document.createElement('script');
document.querySelector('head').appendChild(script);
script.src = `https://duckduckgo.com/ac/?callback=autocompleteCallback&q=${search}`;
2023-02-12 19:59:04 +00:00
script.onload = script.remove;
2022-07-25 04:35:59 +00:00
});
}
static #formatSearchUrl(template, search) {
return template.replace(/{}/g, encodeURIComponent(search));
2022-07-30 20:25:30 +00:00
}
2022-07-24 03:07:50 +00:00
static #hasProtocol(s) {
return /^[a-zA-Z]+:\/\//i.test(s);
}
2022-07-24 03:07:50 +00:00
static #isUrl(s) {
return /^((https?:\/\/)?[\w-]+(\.[\w-]+)+\.?(:\d+)?(\/\S*)?)$/i.test(s);
2017-06-28 05:13:49 +00:00
}
2022-07-21 03:34:58 +00:00
2022-07-24 03:07:50 +00:00
static #parseQuery = (raw) => {
const query = raw.trim();
2022-07-21 03:34:58 +00:00
2022-07-24 03:07:50 +00:00
if (this.#isUrl(query)) {
const url = this.#hasProtocol(query) ? query : `https://${query}`;
return { query, url };
2017-03-03 07:37:14 +00:00
}
if (COMMANDS.has(query)) {
const { key, url } = COMMANDS.get(query);
return { key, query, url };
2022-07-30 20:25:30 +00:00
}
2021-01-20 16:43:08 +00:00
2022-08-05 16:32:34 +00:00
let splitBy = CONFIG.commandSearchDelimiter;
2022-07-30 20:25:30 +00:00
const [searchKey, rawSearch] = query.split(new RegExp(`${splitBy}(.*)`));
2021-01-20 16:43:08 +00:00
if (COMMANDS.has(searchKey)) {
const command = COMMANDS.get(searchKey);
const template = new URL(command.searchTemplate ?? '', command.url);
2022-07-30 20:25:30 +00:00
const search = rawSearch.trim();
const url = Search.#formatSearchUrl(decodeURI(template.href), search);
2022-07-30 20:25:30 +00:00
return { key: searchKey, query, search, splitBy, url };
}
2021-01-20 16:43:08 +00:00
2022-08-05 16:32:34 +00:00
splitBy = CONFIG.commandPathDelimiter;
2022-07-30 20:25:30 +00:00
const [pathKey, path] = query.split(new RegExp(`${splitBy}(.*)`));
2021-01-20 16:43:08 +00:00
if (COMMANDS.has(pathKey)) {
const command = COMMANDS.get(pathKey);
const url = `${new URL(command.url).origin}/${path}`;
return { key: pathKey, path, query, splitBy, url };
2022-07-24 03:07:50 +00:00
}
2021-01-20 16:43:08 +00:00
const url = Search.#formatSearchUrl(CONFIG.defaultSearchTemplate, query);
2022-07-30 20:25:30 +00:00
return { query, search: query, url };
2022-07-21 03:34:58 +00:00
};
2017-03-03 07:37:14 +00:00
2022-07-24 03:07:50 +00:00
#close() {
this.#input.value = '';
this.#input.blur();
this.#dialog.close();
this.#suggestions.innerHTML = '';
}
2022-07-24 03:07:50 +00:00
#execute(query) {
2022-08-04 14:15:29 +00:00
const target = CONFIG.openLinksInNewTab ? '_blank' : '_self';
window.open(Search.#parseQuery(query).url, target, 'noopener noreferrer');
2022-07-24 03:07:50 +00:00
this.#close();
2022-07-21 03:34:58 +00:00
}
2022-07-24 03:07:50 +00:00
#focusNextSuggestion(previous = false) {
const active = this.shadowRoot.activeElement;
let nextIndex;
2022-07-21 03:34:58 +00:00
2022-07-28 02:39:53 +00:00
if (active.dataset.index) {
const activeIndex = Number(active.dataset.index);
2022-07-24 03:07:50 +00:00
nextIndex = previous ? activeIndex - 1 : activeIndex + 1;
2022-07-28 02:39:53 +00:00
} else {
nextIndex = previous ? this.#suggestions.childElementCount - 1 : 0;
}
2018-12-16 11:50:11 +00:00
const next = this.#suggestions.children[nextIndex];
2022-07-28 02:39:53 +00:00
if (next) next.querySelector('.suggestion').focus();
else this.#input.focus();
2022-07-24 03:07:50 +00:00
}
2017-07-19 07:04:54 +00:00
#onInput = async () => {
const oq = Search.#parseQuery(this.#input.value);
if (!oq.query) {
this.#close();
return;
}
let suggestions = COMMANDS.get(oq.query)?.suggestions ?? [];
2022-07-25 04:35:59 +00:00
if (oq.search && suggestions.length < CONFIG.suggestionLimit) {
const res = await Search.#fetchDuckDuckGoSuggestions(oq.search);
2024-02-20 06:01:07 +00:00
suggestions = suggestions.concat(
oq.splitBy
? res.map((search) => `${oq.key}${oq.splitBy}${search}`)
: res
);
2022-07-25 04:35:59 +00:00
}
const nq = Search.#parseQuery(this.#input.value);
if (nq.query !== oq.query) return;
this.#renderSuggestions(suggestions, oq.query);
2022-07-21 03:34:58 +00:00
};
2022-07-24 03:07:50 +00:00
#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;
2017-07-19 07:04:54 +00:00
}
if (e.key === 'Escape') {
this.#close();
return;
2022-02-24 21:07:22 +00:00
}
2021-01-20 16:43:08 +00:00
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();
2022-07-24 03:07:50 +00:00
return;
2021-01-20 16:43:08 +00:00
}
if (/^(ArrowUp|ctrl-p|shift-Tab)$/.test(modifierPrefixedKey)) {
e.preventDefault();
this.#focusNextSuggestion(true);
}
2022-07-21 03:34:58 +00:00
};
2022-07-28 02:39:53 +00:00
#onSubmit = () => {
this.#execute(this.#input.value);
2022-07-28 02:39:53 +00:00
};
#onSuggestionClick = (e) => {
const ref = e.target.closest('.suggestion');
if (!ref) return;
this.#execute(ref.dataset.suggestion);
};
2022-07-24 03:07:50 +00:00
#renderSuggestions(suggestions, query) {
this.#suggestions.innerHTML = '';
const sliced = suggestions.slice(0, CONFIG.suggestionLimit);
2022-07-30 21:03:40 +00:00
const template = document.getElementById('suggestion-template');
2022-07-24 03:07:50 +00:00
2022-07-28 02:39:53 +00:00
for (const [index, suggestion] of sliced.entries()) {
2022-07-24 03:07:50 +00:00
const clone = template.content.cloneNode(true);
const ref = clone.querySelector('.suggestion');
2022-07-28 02:39:53 +00:00
ref.dataset.index = index;
ref.dataset.suggestion = suggestion;
2022-07-30 20:25:30 +00:00
const escapedQuery = Search.#escapeRegexCharacters(query);
2022-07-24 03:07:50 +00:00
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;
}
2022-07-21 03:34:58 +00:00
this.#suggestions.append(clone);
2022-07-21 03:34:58 +00:00
}
2022-07-24 03:07:50 +00:00
}
}
2022-07-21 03:34:58 +00:00
2022-07-30 20:25:30 +00:00
customElements.define('search-component', Search);
2022-07-24 03:07:50 +00:00
</script>
2022-07-21 03:34:58 +00:00
2022-07-24 03:07:50 +00:00
<style>
2023-03-30 23:26:24 +00:00
html {
background-color: var(--color-background);
font-family: var(--font-family);
2024-01-31 03:50:09 +00:00
font-size: var(--font-size);
line-height: 1.4;
2023-03-30 23:26:24 +00:00
}
body {
margin: 0;
}
2022-07-24 03:07:50 +00:00
main {
align-items: center;
2022-11-17 23:15:48 +00:00
box-sizing: border-box;
2022-07-24 03:07:50 +00:00
display: flex;
justify-content: center;
2024-04-09 07:31:43 +00:00
min-height: 100dvh;
2023-03-30 23:26:24 +00:00
overflow: hidden;
2024-01-31 03:50:09 +00:00
padding: calc(var(--space) * 4) var(--space);
2023-03-30 23:26:24 +00:00
position: relative;
2022-11-17 23:15:48 +00:00
width: 100vw;
2017-03-03 07:37:14 +00:00
}
2022-07-24 03:07:50 +00:00
</style>
2017-03-03 07:37:14 +00:00
2022-07-24 03:07:50 +00:00
<main>
<commands-component></commands-component>
<search-component></search-component>
</main>