diff --git a/index.html b/index.html index 8f76372..e084db6 100644 --- a/index.html +++ b/index.html @@ -28,6 +28,7 @@ --color-text: #eee; --font-family: SpaceGrotesk, -apple-system, Helvetica, sans-serif; --font-size: clamp(16px, 1.5vw, 18px); + --color-accent: #e34; --transition-speed: 200ms; } @@ -102,29 +103,44 @@ display: flex; justify-content: center; min-height: 100dvh; - padding: 4rem 0; + position: relative; width: 100%; } .commands { - display: inline-grid; - grid-template-columns: 1fr 1fr; + aspect-ratio: 1; + height: min(55vh, 55vw); list-style: none; margin: 0; padding: 0; + position: relative; + } + + .commands li { + border-radius: 50%; + height: calc(var(--item-size) * 100%); + left: calc(var(--x) * 100%); + position: absolute; + top: calc(var(--y) * 100%); + transform: translate(-50%, -50%) scale(0.8); + transition: + opacity var(--transition-speed), + transform var(--transition-speed); + width: calc(var(--item-size) * 100%); } .command { align-items: center; + border-radius: 50%; color: var(--color-text); display: flex; - height: 4em; + height: 100%; justify-content: center; outline: 0; position: relative; text-align: center; text-decoration: none; - width: 4em; + width: 100%; } .key { @@ -151,14 +167,63 @@ transform: translateY(0); } - @media (min-width: 60rem) { - .commands { - grid-template-columns: repeat(11, 1fr); - } + .clock { + aspect-ratio: 1; + height: calc(min(55vh, 55vw) * 0.85); + left: 50%; + pointer-events: none; + position: absolute; + top: 50%; + transform: translate(-50%, -50%); + } + + .hand { + background: var(--color-text); + border-radius: 2px; + bottom: 50%; + left: 50%; + position: absolute; + transform-origin: bottom center; + } + + .hand--hour { + height: 30%; + width: 3px; + margin-left: -1.5px; + } + + .hand--minute { + height: 40%; + width: 2px; + margin-left: -1px; + } + + .hand--second { + background: var(--color-accent); + height: 45%; + width: 1px; + margin-left: -0.5px; + } + + .dot { + background: var(--color-text); + border-radius: 50%; + height: 6px; + left: 50%; + position: absolute; + top: 50%; + transform: translate(-50%, -50%); + width: 6px; } @@ -181,9 +246,23 @@ const commands = clone.querySelector('.commands'); const commandTemplate = document.getElementById('command-template'); - for (const [key, { name, url }] of COMMANDS.entries()) { - if (!name || !url) continue; + const entries = [...COMMANDS.entries()].filter( + ([, { name, url }]) => name && url + ); + + const total = entries.length; + + commands.style.setProperty( + '--item-size', + Math.sin(Math.PI / total) / 0.8 + ); + + for (const [index, [key, { name, url }]] of entries.entries()) { const clone = commandTemplate.content.cloneNode(true); + const li = clone.querySelector('li'); + const angle = (index / total) * 2 * Math.PI - Math.PI / 2; + li.style.setProperty('--x', 0.5 + 0.5 * Math.cos(angle)); + li.style.setProperty('--y', 0.5 + 0.5 * Math.sin(angle)); const command = clone.querySelector('.command'); command.href = url; command.setAttribute('aria-label', name); @@ -194,6 +273,55 @@ } this.shadowRoot.append(clone); + const items = commands.querySelectorAll('li'); + + for (const [i] of entries.entries()) { + const li = items[i]; + + li.addEventListener('mouseenter', () => { + for (const [j] of entries.entries()) { + const dist = Math.min(Math.abs(j - i), total - Math.abs(j - i)); + + if (dist === 0) { + items[j].style.transform = 'translate(-50%, -50%) scale(1)'; + } else if (dist === 1) { + items[j].style.opacity = '0.25'; + } else if (dist === 2) { + items[j].style.opacity = '0.5'; + } else if (dist === 3) { + items[j].style.opacity = '0.75'; + } + } + }); + + li.addEventListener('mouseleave', () => { + for (const item of items) { + item.style.opacity = ''; + item.style.transform = ''; + } + }); + } + + this.#startClock(); + } + + #startClock() { + const hour = this.shadowRoot.querySelector('.hand--hour'); + const minute = this.shadowRoot.querySelector('.hand--minute'); + const second = this.shadowRoot.querySelector('.hand--second'); + + const tick = () => { + const now = new Date(); + const h = now.getHours() % 12; + const m = now.getMinutes(); + const s = now.getSeconds(); + second.style.transform = `rotate(${s * 6}deg)`; + minute.style.transform = `rotate(${(m + s / 60) * 6}deg)`; + hour.style.transform = `rotate(${(h + m / 60) * 30}deg)`; + }; + + tick(); + setInterval(tick, 1000); } } @@ -222,8 +350,13 @@ height: 100%; justify-content: center; left: 0; + opacity: 0; padding: 0; top: 0; + transform: translateY(10px); + transition: + opacity var(--transition-speed), + transform var(--transition-speed); width: 100%; } @@ -231,6 +364,11 @@ display: flex; } + .dialog.visible { + opacity: 1; + transform: translateY(0); + } + .form { width: 100%; } @@ -281,11 +419,15 @@ color: var(--color-background); } + .suggestions:hover .suggestion:focus:not(:hover) { + color: var(--color-text); + } + .suggestion::before { background-color: var(--color-text); - border-radius: 0.1em; + border-radius: 1em; content: ' '; - inset: 0.9em 0.5em; + inset: 0.8em 0.4em; opacity: 0; position: absolute; transform: translateY(0.3em) scale(0.9); @@ -298,6 +440,11 @@ transform: translateY(0); } + .suggestions:hover .suggestion:focus:not(:hover)::before { + opacity: 0; + transform: translateY(0.3em) scale(0.9); + } + .match { color: var(--color-text-subtle); transition: color var(--transition-speed); @@ -307,6 +454,10 @@ color: var(--color-background); } + .suggestions:hover .suggestion:focus:not(:hover) .match { + color: var(--color-text-subtle); + } + @media (min-width: 700px) { .suggestions { flex-direction: row; @@ -435,11 +586,26 @@ return { query, search: query, url }; }; + #open() { + this.#dialog.show(); + requestAnimationFrame(() => this.#dialog.classList.add('visible')); + } + #close() { + this.#dialog.classList.remove('visible'); this.#input.value = ''; this.#input.blur(); - this.#dialog.close(); this.#suggestions.innerHTML = ''; + + this.#dialog.addEventListener( + 'transitionend', + () => { + if (!this.#dialog.classList.contains('visible')) { + this.#dialog.close(); + } + }, + { once: true } + ); } #execute(query) { @@ -503,12 +669,16 @@ suggestions = suggestions.filter((s) => s.includes(oq.query)); - const withStale = [...suggestions, ...this.#lastSuggestions].slice( - 0, - CONFIG.suggestionLimit - ); + const withStale = [ + ...suggestions, + ...this.#lastSuggestions.filter((s) => s.includes(oq.query)), + ].slice(0, CONFIG.suggestionLimit); - this.#renderSuggestions(withStale, oq.query); + if (withStale.length) { + this.#renderSuggestions(withStale, oq.query); + } else if (!(oq.search?.length > 1)) { + this.#suggestions.innerHTML = ''; + } if (oq.search?.length > 1) { const res = await Search.#fetchDuckDuckGoSuggestions(oq.search); @@ -526,32 +696,41 @@ CONFIG.suggestionLimit ); - this.#renderSuggestions(suggestions, oq.query); - } else { - this.#lastSuggestions = []; + if (suggestions.length) { + this.#renderSuggestions(suggestions, oq.query); + } else { + this.#suggestions.innerHTML = ''; + } } }; #onPaste = (e) => { - if (this.#dialog.open) return; + if (this.#dialog.classList.contains('visible')) return; const text = e.clipboardData?.getData('text'); if (!text) return; e.preventDefault(); - this.#dialog.show(); + this.#open(); this.#input.focus(); this.#input.value = text; this.#onInput(); }; #onKeydown = (e) => { - if (!this.#dialog.open) { + if (!this.#dialog.classList.contains('visible')) { + if (e.key === 'Escape') return; 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(); + if (!this.#input.value) { + this.#input.blur(); + this.#dialog.close(); + this.#suggestions.innerHTML = ''; + } else { + this.#dialog.classList.add('visible'); + } }); return; @@ -593,26 +772,26 @@ #renderSuggestions(suggestions, query) { const focused = this.shadowRoot.activeElement?.dataset?.suggestion; this.#suggestions.innerHTML = ''; - const template = document.getElementById('suggestion-template'); + const suggestionTemplate = document.getElementById('suggestion-template'); + const matchTemplate = document.getElementById('match-template'); + const pattern = new RegExp(Search.#escapeRegexCharacters(query), 'i'); for (const [index, suggestion] of suggestions.entries()) { - const clone = template.content.cloneNode(true); + const clone = suggestionTemplate.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')); + const matched = suggestion.match(pattern); if (matched) { - const template = document.getElementById('match-template'); - const clone = template.content.cloneNode(true); - const matchRef = clone.querySelector('.match'); + const matchClone = matchTemplate.content.cloneNode(true); + const matchRef = matchClone.querySelector('.match'); const pre = suggestion.slice(0, matched.index); const post = suggestion.slice(matched.index + matched[0].length); matchRef.innerText = matched[0]; matchRef.insertAdjacentText('beforebegin', pre); matchRef.insertAdjacentText('afterend', post); - ref.append(clone); + ref.append(matchClone); } else { ref.innerText = suggestion; }