feat: add new search UI (#24)

* feat: remove old search features

* feat: add new page search ui
This commit is contained in:
Victor Westerlund 2024-05-06 17:17:29 +00:00 committed by GitHub
parent e1e3a4a68a
commit 3fd7ce6bf0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 270 additions and 253 deletions

View file

@ -5,6 +5,7 @@
--padding: 20px;
--running-size: 80px;
--header-search-size: var(--running-size);
}
/* # Cornerstones */
@ -94,15 +95,23 @@ h3 {
/* ## Buttons */
button {
border: none;
background-color: transparent;
color: inherit;
fill: inherit;
cursor: pointer;
}
/* ### Inline */
button.inline {
padding: calc(var(--padding) / 2) var(--padding);
color: white;
border: solid 2px white;
border-radius: 6px;
background-color: transparent;
cursor: pointer;
}
button.solid {
button.inline.solid {
color: black;
border-color: var(--color-accent);
background-color: var(--color-accent);
@ -112,6 +121,8 @@ a > button::after {
content: " ➜";
}
/* ### Text links */
a[target="_blank"] > button::after,
:is(h1, h2, h3, p, li) > a[target="_blank"]::after {
content: " ↑";
@ -135,13 +146,31 @@ header {
border-bottom: var(--border-style);
display: grid;
align-items: stretch;
justify-items: end;
grid-template-columns: 1fr var(--running-size);
grid-template-columns: 1fr var(--header-search-size) var(--running-size);
grid-template-rows: var(--running-size);
background-color: rgba(0, 0, 0, .8);
z-index: 100;
perspective: 3000px;
-webkit-backdrop-filter: blur(20px);
backdrop-filter: blur(20px);
overflow: hidden;
}
header > * {
--anim-3d-depth: 5px;
--anim-3d-peek: 25deg;
transition: 300ms background-color;
transform: rotateX(0deg);
backface-visibility: hidden;
box-shadow: 0 var(--anim-3d-depth) 0 0 rgba(255, 255, 255, .2);
}
/* enable 3d flip animation */
@media not (prefers-reduced-motion: reduce) {
header > * {
transition: 600ms transform, 300ms background-color;
}
}
header nav {
@ -150,27 +179,90 @@ header nav {
padding: var(--padding);
}
header .logo {
width: calc(var(--running-size) - 1px);
height: calc(var(--running-size) - 1px);
display: grid;
align-items: center;
justify-items: center;
border-left: var(--border-style);
}
header .logo path.stroke {
fill: var(--color-accent);
}
header searchbox {
header header .search {
display: none;
}
/* ### Buttons */
header button {
--icon-size: 25px;
display: grid;
width: 100%;
border-left: var(--border-style);
grid-template-columns: 1fr;
align-items: center;
justify-items: center;
padding: var(--padding);
gap: var(--padding);
fill: var(--color-accent);
font-size: 13px;
color: rgba(255, 255, 255, .5);
cursor: pointer;
}
header button:not(.logo) svg {
width: var(--icon-size);
}
header button.search p {
display: none;
}
/* ### Searchbox */
header searchbox {
position: absolute;
right: 0;
width: 100%;
height: var(--running-size);
background-color: var(--color-accent);
display: grid;
align-items: stretch;
grid-template-columns: 1fr var(--running-size);
grid-template-rows: var(--running-size);
box-shadow: none;
transform: rotateX(180deg);
}
header searchbox > * {
box-shadow: 0 calc(var(--anim-3d-depth) * -1) 0 0 rgba(var(--primer-color-accent), .8);
}
header searchbox button {
transition: 300ms background-color, 300ms border-color;
border-color: rgba(0, 0, 0, .1);
fill: black;
}
header searchbox input {
padding: 0 var(--padding);
background-color: transparent;
outline: none;
color: black;
border: none;
}
/* #### Active */
header.searchboxActive > * {
transform: rotateX(-180deg);
pointer-events: none;
}
header.searchboxActive searchbox {
transform: rotateX(0);
pointer-events: all;
}
/* ## Main */
main {
transition: 400ms transform;
position: relative;
padding: calc(var(--padding) * 1.5);
width: 100%;
@ -186,105 +278,49 @@ main.loading > * {
opacity: 0;
}
/* ## Search */
/* ## Search results */
/* ### Box */
searchbox {
--icon-size: 25px;
display: grid;
search-results {
transition: 500ms opacity, 300ms transform;
position: fixed;
top: var(--running-size);
right: 0;
width: 100%;
border-left: var(--border-style);
grid-template-columns: var(--icon-size) 1fr;
align-items: center;
padding: var(--padding);
gap: var(--padding);
fill: var(--color-accent);
font-size: 13px;
color: rgba(255, 255, 255, .5);
cursor: pointer;
}
searchbox > svg {
width: var(--icon-size);
}
/* ### Dialog */
body.search-dialog-open main {
transform: scale(.94);
}
dialog.search {
transition: 200ms height cubic-bezier(.41,0,.34,.99);
margin: auto;
width: 100%;
max-width: 1000px;
height: calc(var(--running-size) + (var(--padding) * 5));
max-height: 1000px;
border-color: transparent;
background-color: transparent;
overflow: visible;
outline: none;
}
dialog.search.active {
height: 70vh;
}
dialog.search search {
transition: 400ms transform, 200ms opacity;
width: 100%;
height: 100%;
display: grid;
grid-template-rows: var(--running-size) 1fr;
gap: calc(var(--padding) * 2);
transform: scale(1.1);
overflow: hidden;
background-color: rgba(255, 255, 255, .05);
-webkit-backdrop-filter: blur(20px);
backdrop-filter: brightness(.3) blur(20px);
border-radius: 12px;
box-shadow: 0 10px 30px 10px black;
height: calc(100svh - var(--running-size));
background-color: black;
pointer-events: none;
opacity: 0;
transform: scale(.99);
transform-origin: 100% 0;
overflow-y: scroll;
}
body.search-dialog-open dialog.search search {
transform: scale(1);
padding: calc(var(--padding) * 1.5);
search-results:not([vv-page]) {
display: grid;
align-items: center;
justify-items: center;
}
header.searchboxActive ~ search-results {
opacity: 1;
pointer-events: all;
transform: scale(1);
}
search input {
transition: 200ms background-color, 200ms box-shadow, 200ms color;
border-radius: 6px;
border: none;
outline: none;
color: black;
font-size: 16px;
padding: var(--padding) calc(var(--padding) * 1.5);
background-color: rgba(255, 255, 255, .05);
box-shadow: 0 5px 70px 10px rgba(0, 0, 0, .3);
color: white;
}
/* ### "Start typing" prompt */
search input:focus {
background-color: rgba(255, 255, 255, .9);
box-shadow: 0 10px 30px 10px black;
color: black;
}
/* ### Search results */
dialog.search search search-results {
overflow-y: auto;
}
dialog.search search search-results > svg {
search-results .info {
display: flex;
align-items: center;
flex-direction: column;
margin: auto;
width: 150px;
fill: rgba(255, 255, 255, .05);
gap: 3svh;
}
search-results .info :is(svg, img) {
width: 128px;
fill: var(--color-accent);
}
/* # Feature queries */
@ -298,7 +334,7 @@ dialog.search search search-results > svg {
/* # Components */
button {
button.inline {
transition: 200ms background-color, 200ms border-color, 200ms color;
}
@ -320,18 +356,31 @@ dialog.search search search-results > svg {
fill: var(--color-accent);
}
searchbox {
transition: 200ms background-color;
header searchbox button:hover {
background-color: rgba(0, 0, 0, .08);
}
searchbox:hover {
background-color: rgba(255, 255, 255, .07);
/* ### Search */
@media not (prefers-reduced-motion: reduce) {
header:not(.searchboxActive) button.search:hover,
header:not(.searchboxActive) button.search:hover + button.logo {
transform: rotateX(calc(var(--anim-3d-peek) * -1));
}
header:not(.searchboxActive) button.search:hover ~ searchbox {
transform: rotateX(calc(180deg - var(--anim-3d-peek)));
}
}
}
/* # Size queries */
@media (min-width: 700px) {
:root {
--header-search-size: 250px;
}
/* # Cornerstones */
body::before {
@ -346,19 +395,49 @@ dialog.search search search-results > svg {
/* ## Header */
header {
grid-template-columns: 1fr 250px var(--running-size);
header nav {
margin: 0 calc(var(--padding) / 2);
}
header nav {
justify-self: start;
margin: 0 calc(var(--padding) / 2);
header > button.search {
grid-template-columns: var(--icon-size) 1fr;
}
header > button.search p {
display: initial;
}
header.searchboxActive > nav {
transform: rotateX(0deg);
pointer-events: all;
}
/* ### Searchbox */
header searchbox {
width: calc(var(--header-search-size) + var(--running-size));
}
/* ### Menu */
/* Move the search box to the header */
header searchbox {
header > button.search {
display: grid;
justify-items: baseline;
}
@media (min-height: 600px) {
search-results {
top: calc(var(--running-size) + var(--padding));
width: 50%;
height: calc(100svh - 100px);
background-color: rgba(0, 0, 0, .8);
box-shadow:
inset 0 0 100px 200px rgba(0, 0, 0, 1),
0 0 100px 200px rgba(0, 0, 0, 1)
;
--webkit-backdrop-filter: blur(15px);
backdrop-filter: blur(15px);
}
}
}

View file

@ -154,6 +154,10 @@ splash::after {
opacity: 1;
text-shadow: 0 0 10px rgba(var(--primer-color-accent), .4);
}
button.email:hover {
background-color: transparent;
}
}
/* # Size quries */
@ -169,8 +173,4 @@ splash::after {
main img {
width: 35vh;
}
button:hover {
background-color: transparent;
}
}

View file

@ -12,7 +12,7 @@
section.search {
width: 100%;
display: flex;
display: none;
flex-direction: column;
align-items: center;
gap: var(--padding);
@ -21,6 +21,10 @@ section.search {
margin-bottom: calc(var(--padding) * 2);
}
main[vv-page="/search"] > section.search {
display: flex;
}
section.search form {
display: contents;
}
@ -31,6 +35,11 @@ section.search search {
section.search input {
width: 100%;
border: none;
color: black;
outline: none;
padding: var(--padding);
background-color: var(--color-accent);
}
section.search button[type="submit"] {
@ -42,10 +51,6 @@ section.search > svg {
width: 100%;
}
body:not([vv-page="/search"]) section.search {
display: none;
}
/* # Search results */
section.results .result {
@ -54,19 +59,6 @@ section.results .result {
gap: calc(var(--padding) / 2);
}
/* ---- */
main > svg,
dialog.search search search-results > svg {
margin: auto;
width: 150px;
fill: rgba(255, 255, 255, .05);
}
dialog.search search search-results .empty {
text-align: center;
}
/* ## Titles */
section.title a h2 {

View file

@ -1,72 +1,40 @@
new vv.Interactions("document");
const mainElement = document.querySelector(vv._env.MAIN);
new vv.Interactions("document", {
navigateHome: () => new vv.Navigation("/").navigate(),
closeSearchbox: () => document.querySelector("header").classList.remove("searchboxActive"),
openSearchbox: () => {
document.querySelector("header").classList.add("searchboxActive");
// Select searchbox inner input element
document.querySelector("searchbox input").focus();
}
});
// Crossfade pages on navigation
// Or maybe I shouldn't... hmmm
mainElement.addEventListener(vv.Navigation.events.LOADING, () => {
mainElement.classList.add("loading");
// Clean up modified transform-origin if set after search dialog animation
mainElement.style.removeProperty("transform-origin");
});
mainElement.addEventListener(vv.Navigation.events.LOADED, () => {
[...document.querySelectorAll("dialog")].forEach(element => element.close())
// Wait 200ms for the page fade-in animation to finish
setTimeout(() => mainElement.classList.remove("loading"), 200);
});
// Search dialog open/close logic
{
const CLASNAME_DIALOG_OPEN = "search-dialog-open";
// Offset in pixels from scroll position when scaling the main element
const TRANSFORM_ORIGIN_Y_PADDING = 350;
const mainElement = document.querySelector(vv._env.MAIN);
const dialog = document.querySelector("dialog.search");
// "Polyfill" for HTMLDialogELement open and close events
(new MutationObserver((mutations) => {
// There is only one search dialog elemenet
const target = mutations[0].target;
// Set or unset dialog open class on body depending on dialog visibility
target.hasAttribute("open")
? target.dispatchEvent(new Event("open"))
: target.dispatchEvent(new Event("close"));
}).observe(dialog, { attributes: true }));
dialog.addEventListener("open", () => {
// Scale main element from the current scroll position
mainElement.style.setProperty("transform-origin", `50% calc(${window.scrollY}px + ${TRANSFORM_ORIGIN_Y_PADDING}px)`);
document.body.classList.add(CLASNAME_DIALOG_OPEN);
mainElement.addEventListener(vv.Navigation.events.LOADING, () => {
mainElement.classList.add("loading");
});
dialog.addEventListener("close", () => document.body.classList.remove(CLASNAME_DIALOG_OPEN));
// Close search dialog if dialog is clicked outside inner content
dialog.addEventListener("click", (event) => event.target === dialog ? dialog.close() : null);
// Open search dialog when searchbox is clicked
document.querySelector("searchbox").addEventListener("click", () => dialog.showModal());
mainElement.addEventListener(vv.Navigation.events.LOADED, () => {
// Close searchbox on main page navigation
document.querySelector("header").classList.remove("searchboxActive");
// Wait 200ms for the page fade-in animation to finish
setTimeout(() => mainElement.classList.remove("loading"), 200);
});
}
// Search logic
// Handle search logic
{
const searchResultsElement = document.querySelector("search-results");
const search = (query) => {
new vv.Navigation(`/search?q=${query}`, {
carrySearchParams: true
}).navigate(searchResultsElement);
};
// Run search on keyup
document.querySelector("search input").addEventListener("keyup", (event) => search(event.target.value));
// Trigger expand search box animation
document.querySelector("search input").addEventListener("keydown", () => {
searchResultsElement.closest("dialog").classList.add("active");
}, { once: true });
document.querySelector("header input[type='search']").addEventListener("input", (event) => {
// Debounce user input
clearTimeout(event.target._throttle);
event.target._throttle = setTimeout(() => {
// Navigate search-results element on user input
new vv.Navigation(`/search?q=${event.target.value}`).navigate(searchResultsElement);
}, 100);
});
}

View file

@ -1,25 +1 @@
// Don't open the search dialog overlay if search page is open stand-alone
{
const searchBox = document.querySelector("body:not(.search-dialog-open) searchbox");
// Page is stand-alone
if (searchBox) {
// Shift focus to the on-page search box instead of opening search dialog on click
const shiftSearchboxFocus = () => {
// Override normal "open search dialog" behavior
document.querySelector("dialog.search").close();
// Shift focus to the on-page search input instead
}
// Bind event listener to searchbox element
document.querySelector("body:not(.search-dialog-open) searchbox").addEventListener("click", shiftSearchboxFocus, true);
// Remove event listener from searchbox element on page navigation
mainElement.addEventListener(vv.Navigation.events.LOADING, () => {
searchBox.removeEventListener("click", shiftSearchboxFocus);
});
}
}
new vv.Interactions("search");

BIN
assets/media/travolta.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

View file

@ -49,8 +49,8 @@
<h3>encrypt your message with my OpenPGP key.</h3>
<p>my key is also listed on the <a href="https://keys.openpgp.org/search?q=victor%40vlw.se" target="_blank" rel="noopener noreferer">openPGP key server</a> for victor@vlw.se so your e-mail client can automatically retreive it if supported.</p>
<div class="buttons">
<a href="https://keys.openpgp.org/vks/v1/by-fingerprint/DCE987311CB5D2A252F58951D0AD730E1057DFC6"><button class="solid">download ASC</button></a>
<a href="https://emailselfdefense.fsf.org/en/" target="_blank" rel="noopener noreferer"><button>more info</button></a>
<a href="https://keys.openpgp.org/vks/v1/by-fingerprint/DCE987311CB5D2A252F58951D0AD730E1057DFC6"><button class="inline solid">download ASC</button></a>
<a href="https://emailselfdefense.fsf.org/en/" target="_blank" rel="noopener noreferer"><button class="inline">more info</button></a>
</div>
</section>
<?= VV::media("line.svg") ?>
@ -77,7 +77,7 @@
<label title="this field is required">your message<sup>*</sup></label>
<textarea name="<?= ContactFieldsEnum::MESSAGE->value ?>" required placeholder="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed molestie dignissim mauris vel dignissim. Sed et aliquet odio, id egestas libero. Vestibulum ut dui a turpis aliquam hendrerit id et dui. Morbi eu tristique quam, sit amet dictum felis. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin ac nibh a ex accumsan ullamcorper non quis eros. Nam at suscipit lacus. Nullam placerat semper sapien, vitae aliquet nisl elementum a. Duis viverra quam eros, eu vestibulum quam egestas sit amet. Duis lobortis varius malesuada. Mauris in fringilla mi. "></textarea>
</input-group>
<button class="solid">send</button>
<button class="inline solid">send</button>
</form>
</section>
<?php else: ?>

View file

@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<script>
<!--//--><![CDATA[//><!--
@ -44,25 +44,25 @@
<nav>
<p><a href="/" vv="document" vv-call="navigate">victor westerlund</a></p>
</nav>
<searchbox>
<button class="search" vv="document" vv-call="openSearchbox">
<?= VV::media("icons/search.svg") ?>
<p>search anything...</p>
<p>search vlw.se...</p>
</button>
<button class="logo" vv="document" vv-call="navigateHome"><?= VV::media("vw.svg") ?></button>
<searchbox>
<input type="search" autocomplete="off" placeholder="search vlw.se...">
<button class="close" vv="document" vv-call="closeSearchbox"><?= VV::media("icons/close.svg") ?></button>
</searchbox>
<a href="/" vv="document" vv-call="navigate">
<div class="logo">
<?= VV::media("vw.svg") ?>
</div>
</a>
</header>
<main></main>
<dialog class="search">
<search>
<input type="text" placeholder="start typing to search..."></input>
<search-results></search-results>
</search>
</dialog>
<search-results>
<div class="info empty">
<?= VV::media("icons/search.svg") ?>
<p>start typing to search</p>
</div>
</search-results>
<?php // Bootstrapping ?>
<script><?= VV::init() ?></script>

View file

@ -18,12 +18,12 @@
<section class="search">
<form method="GET">
<search>
<input name="q" type="text" placeholder="search anything..." value="<?= $query ?>"></input>
<input name="q" type="text" placeholder="search vlw.se..." value="<?= $query ?>"></input>
</search>
<button type="submit" class="solid">Search</button>
<button type="submit" class="inline solid">Search</button>
</form>
<?= VV::media("line.svg") ?>
<button>advanced search options</button>
<button class="inline">advanced search options</button>
</section>
<?php if ($response): ?>
@ -33,7 +33,7 @@
<?php // Do things depending on the response code from API ?>
<?php switch ($response->code): default: ?>
<?php // An unknown error occured ?>
<section class="error">
<section class="info">
<p>Something went wrong</p>
</section>
<?php break; ?>
@ -69,9 +69,9 @@
<?php foreach ($result["actions"] as $action): ?>
<?php if (!$action["external"]): ?>
<a href="<?= $action["href"] ?>" vv="search" vv-call="navigate"><button class="<?= $action["class_list"] ?>"><?= $action["display_text"] ?></button></a>
<a href="<?= $action["href"] ?>" vv="search" vv-call="navigate"><button class="inline <?= $action["class_list"] ?>"><?= $action["display_text"] ?></button></a>
<?php else: ?>
<a href="<?= $action["href"] ?>" target="_blank"><button class="<?= $action["class_list"] ?>"><?= $action["display_text"] ?></button></a>
<a href="<?= $action["href"] ?>" target="_blank"><button class="inline <?= $action["class_list"] ?>"><?= $action["display_text"] ?></button></a>
<?php endif; ?>
<?php endforeach; ?>
@ -87,7 +87,8 @@
<?php // No search matches were found ?>
<?php else: ?>
<section class="empty">
<section class="info noresults">
<img src="/assets/media/travolta.gif" alt="">
<p>No results for search term "<?= $_GET["q"] ?>"</p>
</section>
<?php endif; ?>
@ -96,7 +97,7 @@
<?php // No access to the search endpoint ?>
<?php case 404: ?>
<section class="error">
<section class="info">
<p>Connection to VLW API was successful but lacking permission to search</p>
</section>
<?php break; ?>
@ -109,15 +110,16 @@
<?php // Check the error code of the current error ?>
<?php switch ($error_code): default: ?>
<section class="error">
<section class="info">
<p>Unknown request validation error</p>
</section>
<?php break; ?>
<?php // Search query string is not long enough ?>
<?php case "VALUE_MIN_ERROR": ?>
<section class="error">
<p>Type at least <?= $error_msg ?> characters to search!</p>
<section class="info">
<?= VV::media("icons/search.svg") ?>
<p>type at least <?= $error_msg ?> characters to search!</p>
</section>
<?php break; ?>

View file

@ -16,8 +16,8 @@
<?= VV::media("icons/github.svg") ?>
<p>Most of my free open-source software is available on GitHub and it's also mirrored on my server</p>
<div class="buttons">
<a href="https://github.com/victorwesterlund"><button class="solid">open GitHub</button></a>
<a href="https://git.vlw.se"><button>mirror</button></a>
<a href="https://github.com/victorwesterlund"><button class="inline solid">open GitHub</button></a>
<a href="https://git.vlw.se"><button class="inline">mirror</button></a>
</div>
</section>
@ -132,7 +132,7 @@
$link_href = $action["href"] === null ? "/work/{$item["id"]}" : $action["href"];
?>
<a href="<?= $link_href ?>" <?= $link_attr ?>><button class="<?= $action["class_list"] ?>"><?= $action["display_text"] ?></button></a>
<a href="<?= $link_href ?>" <?= $link_attr ?>><button class="inline <?= $action["class_list"] ?>"><?= $action["display_text"] ?></button></a>
<?php endforeach; ?>
</div>
<?php endif; ?>