Initial commit
This commit is contained in:
commit
43860742b6
6
.prettierrc
Normal file
6
.prettierrc
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"htmlWhitespaceSensitivity": "ignore",
|
||||
"proseWrap": "always",
|
||||
"singleQuote": true,
|
||||
"trailingComma": "es5"
|
||||
}
|
39
README.md
Normal file
39
README.md
Normal file
|
@ -0,0 +1,39 @@
|
|||
# Tilde
|
||||
|
||||
Inspired by [r/startpages](https://www.reddit.com/r/startpages)—Tilde is
|
||||
the browser homepage for pro web surfers.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
To go to a site, type the corresponding key and press return. e.g:
|
||||
|
||||
- `g` will redirect you to [github.com](https://github.com)
|
||||
|
||||
To search a site, type a space after the site’s key followed by your
|
||||
query. e.g:
|
||||
|
||||
- `y kittens` will
|
||||
[search YouTube for kittens](https://www.youtube.com/results?search_query=kittens)
|
||||
|
||||
A DuckDuckGo search will be triggered if your input doesn’t match a key.
|
||||
e.g:
|
||||
|
||||
- `google` will [search DuckDuckGo for google](https://duckduckgo.com/?q=google)
|
||||
|
||||
To go to a specific path on a site, type the path after the site’s key.
|
||||
e.g:
|
||||
|
||||
- `r/r/startpages` will redirect you to
|
||||
[reddit.com/r/startpages](https://www.reddit.com/r/startpages)
|
||||
|
||||
To access any other site, enter the URL directly. e.g:
|
||||
|
||||
- `example.com` will redirect you to [example.com](https://example.com)
|
||||
|
||||
## Beyond
|
||||
|
||||
Tilde is meant to be customized—[make it yours!](index.html)
|
||||
|
||||
## License
|
||||
|
||||
Use and modify Tilde [as you see fit](UNLICENSE).
|
24
UNLICENSE
Normal file
24
UNLICENSE
Normal file
|
@ -0,0 +1,24 @@
|
|||
This is free and unencumbered software released into the public domain.
|
||||
|
||||
Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||
distribute this software, either in source code form or as a compiled
|
||||
binary, for any purpose, commercial or non-commercial, and by any
|
||||
means.
|
||||
|
||||
In jurisdictions that recognize copyright laws, the author or authors
|
||||
of this software dedicate any and all copyright interest in the
|
||||
software to the public domain. We make this dedication for the benefit
|
||||
of the public at large and to the detriment of our heirs and
|
||||
successors. We intend this dedication to be an overt act of
|
||||
relinquishment in perpetuity of all present and future rights to this
|
||||
software under copyright law.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
For more information, please refer to <http://unlicense.org>
|
590
index.html
Normal file
590
index.html
Normal file
|
@ -0,0 +1,590 @@
|
|||
<!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>
|
||||
:root {
|
||||
--border-radius: 1rem;
|
||||
--color-background: #111;
|
||||
--color-text-subtle: #888;
|
||||
--color-text: #eee;
|
||||
--font-family: -apple-system, Helvetica, sans-serif;
|
||||
--font-size: clamp(16px, 1.6vw, 18px);
|
||||
--font-weight-bold: 700;
|
||||
--font-weight-normal: 400;
|
||||
--space: 1rem;
|
||||
--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,
|
||||
};
|
||||
|
||||
const COMMANDS = new Map([
|
||||
['a', { name: 'Chat', url: 'https://chat.openai.com' }],
|
||||
['b', { name: 'Dribbble', url: 'https://dribbble.com/shots/recent' }],
|
||||
['c', { name: 'Calendar', url: 'https://calendar.google.com' }],
|
||||
['d', { name: 'Drive', url: 'https://drive.google.com' }],
|
||||
['f', { name: 'Figma', url: 'https://www.figma.com' }],
|
||||
['g', { name: 'GitHub', url: 'https://github.com' }],
|
||||
['k', { name: 'Keep', url: 'https://keep.google.com' }],
|
||||
['m', { name: 'Mail', url: 'https://mail.proton.me/u/0/inbox' }],
|
||||
['n', { name: 'Notion', url: 'https://www.notion.so' }],
|
||||
['p', { name: 'Pomodoro', url: 'https://pomodoro.xvvvyz.xyz' }],
|
||||
[
|
||||
'r',
|
||||
{
|
||||
name: 'Reddit',
|
||||
suggestions: [
|
||||
'r/r/webdev',
|
||||
'r/r/dataisbeautiful',
|
||||
'r/r/fujix',
|
||||
'r/r/leetcode',
|
||||
],
|
||||
url: 'https://reddit.com',
|
||||
},
|
||||
],
|
||||
['s', { name: 'Supabase', url: 'https://supabase.com/dashboard/projects' }],
|
||||
['t', { name: 'Translate', url: 'https://www.deepl.com/translator' }],
|
||||
['v', { name: 'Vercel', url: 'https://vercel.com/dashboard' }],
|
||||
[
|
||||
'y',
|
||||
{
|
||||
name: 'YouTube',
|
||||
searchTemplate: '/results?search_query={}',
|
||||
url: 'https://youtube.com/feed/subscriptions',
|
||||
},
|
||||
],
|
||||
[
|
||||
'0',
|
||||
{
|
||||
name: 'localhost',
|
||||
searchTemplate: ':{}',
|
||||
suggestions: ['0 54323', '0 54324'],
|
||||
url: 'http://localhost:3000',
|
||||
},
|
||||
],
|
||||
]);
|
||||
</script>
|
||||
|
||||
<template id="commands-template">
|
||||
<style>
|
||||
.commands {
|
||||
border-radius: var(--border-radius);
|
||||
column-gap: 0;
|
||||
columns: 1;
|
||||
list-style: none;
|
||||
margin: 0 auto;
|
||||
max-width: 10rem;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
.command {
|
||||
display: flex;
|
||||
gap: var(--space);
|
||||
outline: 0;
|
||||
padding: var(--space);
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.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 {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
@media (min-width: 500px) {
|
||||
.commands {
|
||||
columns: 2;
|
||||
max-width: 25rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 900px) {
|
||||
.commands {
|
||||
columns: 4;
|
||||
max-width: 45rem;
|
||||
}
|
||||
}
|
||||
</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: 3rem;
|
||||
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) 0 0;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.suggestion {
|
||||
color: var(--color-text);
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
padding: var(--space);
|
||||
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: calc(var(--border-radius) / 5);
|
||||
content: ' ';
|
||||
inset: calc(var(--space) / 1.5) calc(var(--space) / 3);
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
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);
|
||||
}
|
||||
|
||||
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 = '';
|
||||
}
|
||||
|
||||
#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 () => {
|
||||
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);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
main {
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
padding: calc(var(--space) * 4) var(--space);
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
}
|
||||
</style>
|
||||
|
||||
<main>
|
||||
<commands-component></commands-component>
|
||||
<search-component></search-component>
|
||||
</main>
|
Loading…
Reference in a new issue