feat: add new page search ui

This commit is contained in:
Victor Westerlund 2024-05-06 17:32:37 +02:00
parent 4de391fbdb
commit 96ada4b66c
10 changed files with 285 additions and 126 deletions

View file

@ -5,6 +5,7 @@
--padding: 20px; --padding: 20px;
--running-size: 80px; --running-size: 80px;
--header-search-size: var(--running-size);
} }
/* # Cornerstones */ /* # Cornerstones */
@ -94,15 +95,23 @@ h3 {
/* ## Buttons */ /* ## Buttons */
button { button {
border: none;
background-color: transparent;
color: inherit;
fill: inherit;
cursor: pointer;
}
/* ### Inline */
button.inline {
padding: calc(var(--padding) / 2) var(--padding); padding: calc(var(--padding) / 2) var(--padding);
color: white; color: white;
border: solid 2px white; border: solid 2px white;
border-radius: 6px; border-radius: 6px;
background-color: transparent;
cursor: pointer;
} }
button.solid { button.inline.solid {
color: black; color: black;
border-color: var(--color-accent); border-color: var(--color-accent);
background-color: var(--color-accent); background-color: var(--color-accent);
@ -112,6 +121,8 @@ a > button::after {
content: " ➜"; content: " ➜";
} }
/* ### Text links */
a[target="_blank"] > button::after, a[target="_blank"] > button::after,
:is(h1, h2, h3, p, li) > a[target="_blank"]::after { :is(h1, h2, h3, p, li) > a[target="_blank"]::after {
content: " ↑"; content: " ↑";
@ -135,13 +146,31 @@ header {
border-bottom: var(--border-style); border-bottom: var(--border-style);
display: grid; display: grid;
align-items: stretch; align-items: stretch;
justify-items: end; grid-template-columns: 1fr var(--header-search-size) var(--running-size);
grid-template-columns: 1fr var(--running-size);
grid-template-rows: var(--running-size); grid-template-rows: var(--running-size);
background-color: rgba(0, 0, 0, .8); background-color: rgba(0, 0, 0, .8);
z-index: 100; z-index: 100;
perspective: 3000px;
-webkit-backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
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 { header nav {
@ -150,27 +179,90 @@ header nav {
padding: var(--padding); 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 { header .logo path.stroke {
fill: var(--color-accent); fill: var(--color-accent);
} }
header searchbox { header header .search {
display: none; 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 */
main { main {
transition: 400ms transform;
position: relative; position: relative;
padding: calc(var(--padding) * 1.5); padding: calc(var(--padding) * 1.5);
width: 100%; width: 100%;
@ -186,26 +278,49 @@ main.loading > * {
opacity: 0; opacity: 0;
} }
/* ## Search */ /* ## Search results */
searchbox { search-results {
--icon-size: 25px; transition: 500ms opacity, 300ms transform;
position: fixed;
display: grid; top: var(--running-size);
right: 0;
width: 100%; width: 100%;
border-left: var(--border-style);
grid-template-columns: var(--icon-size) 1fr;
align-items: center;
padding: var(--padding); padding: var(--padding);
gap: var(--padding); height: calc(100svh - var(--running-size));
fill: var(--color-accent); background-color: black;
font-size: 13px; pointer-events: none;
color: rgba(255, 255, 255, .5); opacity: 0;
cursor: pointer; transform: scale(.99);
transform-origin: 100% 0;
overflow-y: scroll;
} }
searchbox > svg { search-results:not([vv-page]) {
width: var(--icon-size); display: grid;
align-items: center;
justify-items: center;
}
header.searchboxActive ~ search-results {
opacity: 1;
pointer-events: all;
transform: scale(1);
}
/* ### "Start typing" prompt */
search-results .info {
display: flex;
align-items: center;
flex-direction: column;
margin: auto;
gap: 3svh;
}
search-results .info :is(svg, img) {
width: 128px;
fill: var(--color-accent);
} }
/* # Feature queries */ /* # Feature queries */
@ -219,7 +334,7 @@ searchbox > svg {
/* # Components */ /* # Components */
button { button.inline {
transition: 200ms background-color, 200ms border-color, 200ms color; transition: 200ms background-color, 200ms border-color, 200ms color;
} }
@ -241,18 +356,31 @@ searchbox > svg {
fill: var(--color-accent); fill: var(--color-accent);
} }
searchbox { header searchbox button:hover {
transition: 200ms background-color; background-color: rgba(0, 0, 0, .08);
} }
searchbox:hover { /* ### Search */
background-color: rgba(255, 255, 255, .07);
@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 */ /* # Size queries */
@media (min-width: 700px) { @media (min-width: 700px) {
:root {
--header-search-size: 250px;
}
/* # Cornerstones */ /* # Cornerstones */
body::before { body::before {
@ -267,19 +395,49 @@ searchbox > svg {
/* ## Header */ /* ## Header */
header { header nav {
grid-template-columns: 1fr 250px var(--running-size); margin: 0 calc(var(--padding) / 2);
} }
header nav { header > button.search {
justify-self: start; grid-template-columns: var(--icon-size) 1fr;
margin: 0 calc(var(--padding) / 2); }
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 */ /* ### Menu */
/* Move the search box to the header */ /* Move the search box to the header */
header searchbox { header > button.search {
display: grid; 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; opacity: 1;
text-shadow: 0 0 10px rgba(var(--primer-color-accent), .4); text-shadow: 0 0 10px rgba(var(--primer-color-accent), .4);
} }
button.email:hover {
background-color: transparent;
}
} }
/* # Size quries */ /* # Size quries */
@ -169,8 +173,4 @@ splash::after {
main img { main img {
width: 35vh; width: 35vh;
} }
button:hover {
background-color: transparent;
}
} }

View file

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

View file

@ -1,16 +1,40 @@
new vv.Interactions("document"); new vv.Interactions("document", {
navigateHome: () => new vv.Navigation("/").navigate(),
const mainElement = document.querySelector(vv._env.MAIN); 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 // Crossfade pages on navigation
// Or maybe I shouldn't... hmmm {
mainElement.addEventListener(vv.Navigation.events.LOADING, () => { const mainElement = document.querySelector(vv._env.MAIN);
mainElement.classList.add("loading");
});
mainElement.addEventListener(vv.Navigation.events.LOADED, () => { mainElement.addEventListener(vv.Navigation.events.LOADING, () => {
[...document.querySelectorAll("dialog")].forEach(element => element.close()) mainElement.classList.add("loading");
});
// Wait 200ms for the page fade-in animation to finish mainElement.addEventListener(vv.Navigation.events.LOADED, () => {
setTimeout(() => mainElement.classList.remove("loading"), 200); // 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);
});
}
// Handle search logic
{
const searchResultsElement = document.querySelector("search-results");
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"); 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> <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> <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"> <div class="buttons">
<a href="https://keys.openpgp.org/vks/v1/by-fingerprint/DCE987311CB5D2A252F58951D0AD730E1057DFC6"><button class="solid">download ASC</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>more info</button></a> <a href="https://emailselfdefense.fsf.org/en/" target="_blank" rel="noopener noreferer"><button class="inline">more info</button></a>
</div> </div>
</section> </section>
<?= VV::media("line.svg") ?> <?= VV::media("line.svg") ?>
@ -77,7 +77,7 @@
<label title="this field is required">your message<sup>*</sup></label> <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> <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> </input-group>
<button class="solid">send</button> <button class="inline solid">send</button>
</form> </form>
</section> </section>
<?php else: ?> <?php else: ?>

View file

@ -2,7 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <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> <script>
<!--//--><![CDATA[//><!-- <!--//--><![CDATA[//><!--
@ -44,19 +44,26 @@
<nav> <nav>
<p><a href="/" vv="document" vv-call="navigate">victor westerlund</a></p> <p><a href="/" vv="document" vv-call="navigate">victor westerlund</a></p>
</nav> </nav>
<searchbox> <button class="search" vv="document" vv-call="openSearchbox">
<?= VV::media("icons/search.svg") ?> <?= 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> </searchbox>
<a href="/" vv="document" vv-call="navigate">
<div class="logo">
<?= VV::media("vw.svg") ?>
</div>
</a>
</header> </header>
<main></main> <main></main>
<search-results>
<div class="info empty">
<?= VV::media("icons/search.svg") ?>
<p>start typing to search</p>
</div>
</search-results>
<?php // Bootstrapping ?> <?php // Bootstrapping ?>
<script><?= VV::init() ?></script> <script><?= VV::init() ?></script>
<script><?= VV::js("document") ?></script> <script><?= VV::js("document") ?></script>

View file

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

View file

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