tilde/index.html
2026-02-28 13:25:46 -08:00

562 lines
16 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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" />
<title>~</title>
<style>
@font-face {
font-family: 'SpaceGrotesk';
font-style: normal;
font-weight: normal;
src: url('./fonts/SpaceGrotesk-Regular.woff2') format('woff2');
}
@font-face {
font-family: 'SpaceGrotesk';
font-style: normal;
font-weight: bold;
src: url('./fonts/SpaceGrotesk-Bold.woff2') format('woff2');
}
:root {
--color-background: #111;
--color-text-subtle: #888;
--color-text: #eee;
--font-family: SpaceGrotesk, -apple-system, Helvetica, sans-serif;
--font-size: clamp(16px, 1.5vw, 18px);
--transition-speed: 200ms;
}
@media (prefers-color-scheme: light) {
:root {
--color-background: #e8e8e8;
--color-text-subtle: #606060;
--color-text: #111;
}
}
</style>
<script>
const CONFIG = {
commandPathDelimiter: '/',
commandSearchDelimiter: ' ',
defaultSearchTemplate: 'https://duckduckgo.com/?q={}',
openLinksInNewTab: true,
suggestionLimit: 4,
};
// prettier-ignore
const COMMANDS = new Map([
['a', { name: 'AI', searchTemplate: '/?q={}', suggestions: ['a/chat', 'a/gemini', 'a/grok', 'a/lumo'], url: 'https://claude.ai/new' }],
['a/chat', { url: 'https://chatgpt.com' }],
['a/gemini', { url: 'https://gemini.google.com' }],
['a/grok', { url: 'https://grok.com' }],
['a/lumo', { url: 'https://lumo.proton.me/u/0' }],
['c', { name: 'Cloudflare', url: 'https://dash.cloudflare.com' }],
['d', { name: 'Discord', url: 'https://discord.com/channels/@me' }],
['e', { name: 'PostHog', url: 'https://us.posthog.com' }],
['f', { name: 'Figma', url: 'https://www.figma.com' }],
['g', { name: 'GitHub', searchTemplate: '/search?q={}', suggestions: ['g/copilot', 'g/trending'], url: 'https://github.com' }],
['h', { name: 'Hetzner', url: 'https://console.hetzner.cloud/projects' }],
['i', { name: 'InstantDB', url: 'https://instantdb.com/dash' }],
['l', { name: 'LinkedIn', url: 'https://www.linkedin.com/feed/' }],
['m', { name: 'Modal', url: 'https://modal.com/apps' }],
['n', { name: 'Pinecone', url: 'https://app.pinecone.io' }],
['o', { name: 'OpenRouter', url: 'https://openrouter.ai/models' }],
['p', { name: 'Proton', suggestions: ['p/drive', 'p/pass'], 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' }],
['q', { name: 'QBO', url: 'https://qbo.intuit.com' }],
['r', { name: 'Reddit', url: 'https://www.reddit.com' }],
['s', { name: 'Supabase', url: 'https://supabase.com/dashboard/organizations' }],
['t', { name: 'TickTick', url: 'https://ticktick.com/webapp/#q/today/tasks' }],
['u', { name: 'Duolingo', url: 'https://www.duolingo.com/learn' }],
['v', { name: 'Vercel', url: 'https://vercel.com/dashboard' }],
['x', { name: '𝕏', suggestions: ['x/jellyseerr', 'x/n8n'], url: 'https://x.com/home' }],
['x/jellyseerr', { url: 'https://jellyseerr.xvyz.co' }],
['x/n8n', { url: 'https://n8n.xvyz.co/home/workflows' }],
['y', { name: 'YouTube', searchTemplate: '/results?search_query={}', url: 'https://www.youtube.com/feed/subscriptions' }],
['0', { name: 'localhost', url: 'http://localhost:3000' }],
]);
</script>
<template id="commands-template">
<style>
nav {
align-items: center;
box-sizing: border-box;
display: flex;
justify-content: center;
min-height: 100dvh;
padding: 4rem 0;
width: 100%;
}
.commands {
display: inline-grid;
grid-template-columns: 1fr 1fr;
list-style: none;
margin: 0;
padding: 0;
}
.command {
align-items: center;
color: var(--color-text);
display: flex;
height: 4em;
justify-content: center;
outline: 0;
position: relative;
text-align: center;
text-decoration: none;
width: 4em;
}
.key {
color: var(--color-text-subtle);
position: absolute;
transition: all var(--transition-speed);
}
.command:where(:focus, :hover) .key {
opacity: 0;
pointer-events: none;
transform: translateY(1.5em) scale(0.9);
}
.name {
opacity: 0;
pointer-events: none;
transform: translateY(-1.5em) scale(0.9);
transition: all var(--transition-speed);
}
.command:where(:focus, :hover) .name {
opacity: 1;
transform: translateY(0);
}
@media (min-width: 60rem) {
.commands {
grid-template-columns: repeat(11, 1fr);h
}
}
</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;
font-family: var(--font-family);
-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: 2rem;
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: 1rem 0 0;
overflow: hidden;
padding: 0;
}
.suggestion {
color: var(--color-text);
cursor: pointer;
font-size: 1rem;
padding: 1rem;
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);
border-radius: 0.1em;
content: ' ';
inset: 0.9em 0.5em;
opacity: 0;
position: absolute;
transform: translateY(0.3em) scale(0.9);
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);
}
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(template, search) {
return template.replace(/{}/g, encodeURIComponent(search));
}
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 { key, url } = COMMANDS.get(query);
return { key, query, url };
}
let splitBy = CONFIG.commandSearchDelimiter;
const [searchKey, rawSearch] = query.split(new RegExp(`${splitBy}(.*)`));
if (COMMANDS.has(searchKey)) {
const command = COMMANDS.get(searchKey);
const template = new URL(command.searchTemplate ?? '', command.url);
const search = rawSearch.trim();
const url = Search.#formatSearchUrl(decodeURI(template.href), search);
return { key: searchKey, query, search, splitBy, url };
}
splitBy = CONFIG.commandPathDelimiter;
const [pathKey, path] = query.split(new RegExp(`${splitBy}(.*)`));
if (COMMANDS.has(pathKey)) {
const command = COMMANDS.get(pathKey);
const url = `${new URL(command.url).origin}/${path}`;
return { key: pathKey, path, query, splitBy, url };
}
const url = Search.#formatSearchUrl(CONFIG.defaultSearchTemplate, query);
return { query, search: query, url };
};
#close() {
this.#input.value = '';
this.#input.blur();
this.#dialog.close();
this.#suggestions.innerHTML = '';
}
#execute(query) {
const target = CONFIG.openLinksInNewTab ? '_blank' : '_self';
window.open(Search.#parseQuery(query).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 () => {
const oq = Search.#parseQuery(this.#input.value);
if (!oq.query) {
this.#close();
return;
}
let suggestions = COMMANDS.get(oq.query)?.suggestions ?? [];
if (
oq.search?.length > 1 &&
suggestions.length < CONFIG.suggestionLimit
) {
const res = await Search.#fetchDuckDuckGoSuggestions(oq.search);
suggestions = suggestions
.concat(
oq.splitBy
? res.map((search) => `${oq.key}${oq.splitBy}${search}`)
: res
)
.slice(0, CONFIG.suggestionLimit);
}
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 template = document.getElementById('suggestion-template');
for (const [index, suggestion] of suggestions.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);
line-height: 1.4;
user-select: none;
}
body {
margin: 0;
}
</style>
<commands-component></commands-component>
<search-component></search-component>