diff --git a/assets/css/pages/about.css b/assets/css/pages/about.css
index 9bd3615..92739c8 100755
--- a/assets/css/pages/about.css
+++ b/assets/css/pages/about.css
@@ -5,7 +5,7 @@
--color-accent: rgb(var(--primer-color-accent));
}
-main {
+vv-shell {
display: flex;
flex-direction: column;
gap: var(--padding);
@@ -15,7 +15,7 @@ main {
/* ## Divider */
-main > hr {
+vv-shell > hr {
border-color: rgba(255, 255, 255, .1);
}
diff --git a/assets/css/pages/about/battlestation-retired.css b/assets/css/pages/about/battlestation-retired.css
index c0cd09c..a6a92da 100644
--- a/assets/css/pages/about/battlestation-retired.css
+++ b/assets/css/pages/about/battlestation-retired.css
@@ -5,7 +5,7 @@
--color-accent: rgb(var(--primer-color-accent));
}
-main {
+vv-shell {
display: flex;
flex-direction: column;
gap: var(--padding);
diff --git a/assets/css/pages/about/battlestation.css b/assets/css/pages/about/battlestation.css
index 84e4641..981c2cc 100644
--- a/assets/css/pages/about/battlestation.css
+++ b/assets/css/pages/about/battlestation.css
@@ -5,7 +5,7 @@
--color-accent: rgb(var(--primer-color-accent));
}
-main {
+vv-shell {
display: flex;
flex-direction: column;
gap: var(--padding);
diff --git a/assets/css/pages/contact.css b/assets/css/pages/contact.css
index 9d1e4db..68c60e4 100755
--- a/assets/css/pages/contact.css
+++ b/assets/css/pages/contact.css
@@ -5,7 +5,7 @@
--color-accent: rgb(var(--primer-color-accent));
}
-main {
+vv-shell {
display: flex;
flex-direction: column;
align-items: center;
@@ -14,7 +14,7 @@ main {
/* # Sections */
-main > svg {
+vv-shell > svg {
margin: var(--padding) 0;
}
diff --git a/assets/css/pages/error.css b/assets/css/pages/error.css
index 5dd4d2a..c3beedb 100755
--- a/assets/css/pages/error.css
+++ b/assets/css/pages/error.css
@@ -6,7 +6,7 @@ header {
backdrop-filter: unset;
}
-main {
+vv-shell {
max-width: unset;
display: grid;
justify-items: center;
diff --git a/assets/css/pages/index.css b/assets/css/pages/index.css
index f1f6e6c..eb3f071 100755
--- a/assets/css/pages/index.css
+++ b/assets/css/pages/index.css
@@ -4,18 +4,18 @@ body[vv-top-page="/"]::before {
opacity: 0;
}
-/* # Main styles */
+/* # vv-shell styles */
/* ## Picture */
-main {
+vv-shell {
display: flex;
align-items: center;
justify-content: space-between;
flex-direction: column-reverse;
}
-main img {
+vv-shell img {
margin: auto;
width: 25vh;
pointer-events: none;
@@ -171,14 +171,14 @@ splash::after {
/* # Size quries */
@media (min-width: 900px) {
- main {
+ vv-shell {
display: grid;
grid-template-columns: repeat(2, 1fr);
justify-items: center;
align-items: center;
}
- main img {
+ vv-shell img {
width: 35vh;
}
}
diff --git a/assets/css/pages/search.css b/assets/css/pages/search.css
index 86fedee..ac9f75c 100755
--- a/assets/css/pages/search.css
+++ b/assets/css/pages/search.css
@@ -21,7 +21,7 @@ section.search {
margin-bottom: calc(var(--padding) * 2);
}
-main[vv-page="/search"] > section.search {
+vv-shell[vv-page="/search"] > section.search {
display: flex;
}
diff --git a/assets/css/pages/work.css b/assets/css/pages/work.css
index c39860c..829ae93 100755
--- a/assets/css/pages/work.css
+++ b/assets/css/pages/work.css
@@ -5,7 +5,7 @@
--color-accent: rgb(var(--primer-color-accent));
}
-main {
+vv-shell {
display: flex;
flex-direction: column;
gap: var(--padding);
diff --git a/assets/css/document.css b/assets/css/shells/document.css
old mode 100755
new mode 100644
similarity index 99%
rename from assets/css/document.css
rename to assets/css/shells/document.css
index c30225d..4a0cf35
--- a/assets/css/document.css
+++ b/assets/css/shells/document.css
@@ -260,21 +260,21 @@ header.searchboxActive searchbox {
transform: rotateX(0);
}
-/* ## Main */
+/* ## vv-shell */
-main {
+vv-shell {
position: relative;
padding: calc(var(--padding) * 1.5);
width: 100%;
max-width: 1000px;
}
-main > * {
+vv-shell > * {
transition: 100ms opacity;
opacity: 1;
}
-main.loading > * {
+vv-shell.loading > * {
opacity: 0;
}
diff --git a/assets/js/pages/about.js b/assets/js/pages/about.js
index be75c4f..bc933f2 100755
--- a/assets/js/pages/about.js
+++ b/assets/js/pages/about.js
@@ -1,5 +1,3 @@
-new vv.Interactions("about");
-
const randomIntFromInterval = (min, max) => {
return Math.floor(Math.random() * (max - min + 1) + min);
}
diff --git a/assets/js/pages/about/battlestation-retired.js b/assets/js/pages/about/battlestation-retired.js
index 112c1d6..e69de29 100644
--- a/assets/js/pages/about/battlestation-retired.js
+++ b/assets/js/pages/about/battlestation-retired.js
@@ -1 +0,0 @@
-new vv.Interactions("battlestation-retired");
\ No newline at end of file
diff --git a/assets/js/pages/about/battlestation.js b/assets/js/pages/about/battlestation.js
index 4550346..d537f8d 100644
--- a/assets/js/pages/about/battlestation.js
+++ b/assets/js/pages/about/battlestation.js
@@ -1,19 +1,19 @@
-new vv.Interactions("battlestation", {
- toggleGroup: (event) => {
- // Collapse self if already active and current target
- if (event.target.classList.contains("active")) {
- return event.target.classList.remove("active");
- }
+import { Elevent } from "/assets/js/._Elevent.mjs";
- // Collapse all and open current target
- [...event.target.closest(".specs").querySelectorAll(".group")].forEach(element => element.classList.remove("active"));
- event.target.classList.add("active");
- },
- setSpecActive: (event) => {
- event.target.classList.add("active");
-
- event.target.addEventListener("mouseleave", () => event.target.classList.remove("active"));
+new Elevent("click", document.querySelectorAll(".group"), (event) => {
+ // Collapse self if already active and current target
+ if (event.target.classList.contains("active")) {
+ return event.target.classList.remove("active");
}
+
+ // Collapse all and open current target
+ [...event.target.closest(".specs").querySelectorAll(".group")].forEach(element => element.classList.remove("active"));
+ event.target.classList.add("active");
+});
+
+new Elevent("click", document.querySelectorAll(".spec"), (event) => {
+ event.target.classList.add("active");
+ event.target.addEventListener("mouseleave", () => event.target.classList.remove("active"));
});
// Bind hover listeners for components in the SVGs
diff --git a/assets/js/pages/index.js b/assets/js/pages/index.js
index 0eeb43f..277e667 100755
--- a/assets/js/pages/index.js
+++ b/assets/js/pages/index.js
@@ -1,108 +1,108 @@
-const EMAIL_CPY_ANIM_DUR_MSECONDS = 1000;
+import { Elevent } from "/assets/js/._Elevent.mjs";
-// Run email copied splash animation
-const emailCopiedAnimation = () => {
- const CONFETTI_COUNT = 40;
- const CONFETTI_SCALE_PIXELS = 300;
+// Click to copy email button
+{
+ const EMAIL_CPY_ANIM_DUR_MSECONDS = 1000;
- const randomIntFromInterval = (min, max) => {
- return Math.floor(Math.random() * (max - min + 1) + min)
+ // Run email copied splash animation
+ const emailCopiedAnimation = () => {
+ const CONFETTI_COUNT = 40;
+ const CONFETTI_SCALE_PIXELS = 300;
+
+ const randomIntFromInterval = (min, max) => {
+ return Math.floor(Math.random() * (max - min + 1) + min)
+ }
+
+ // Create new splash element
+ const splashElement = document.createElement("splash");
+ splashElement.innerText = "copied!";
+
+ // Set inline display to none to hide this element on pages where the splash element has no override styles defined.
+ splashElement.style.display = "none";
+
+ // Array of box-shadow strings as "confetti"
+ const confetti = [];
+
+ // Generate random confetti
+ for (let i = 0; i < CONFETTI_COUNT; i++) {
+ // Random confetti position
+ const x = randomIntFromInterval(CONFETTI_SCALE_PIXELS * -1, CONFETTI_SCALE_PIXELS);
+ const y = randomIntFromInterval(CONFETTI_SCALE_PIXELS * -1, CONFETTI_SCALE_PIXELS);
+
+ // Random confetti RGB color
+ const rgb = [
+ randomIntFromInterval(0, 255),
+ randomIntFromInterval(0, 255),
+ randomIntFromInterval(0, 255)
+ ];
+
+ // Interpolate random values and append to outer confetti array
+ confetti.push(`${x}px ${y}px 0 rgb(${rgb.join(",")})`);
+ }
+
+ // Set CSS variable on splash element that in turn will be used by pseudo-element
+ splashElement.style.setProperty("--confetti", confetti.join(","));
+
+ // Start animation by appending the created element to the document body
+ document.body.appendChild(splashElement);
+
+ // Run hide animation
+ setTimeout(() => {
+ splashElement.classList.add("hide");
+
+ // Selfdestruct element when hide animation finishes
+ setTimeout(() => splashElement.remove(), 400);
+ }, EMAIL_CPY_ANIM_DUR_MSECONDS + 100);
}
-
- // Create new splash element
- const splashElement = document.createElement("splash");
- splashElement.innerText = "copied!";
-
- // Set inline display to none to hide this element on pages where the splash element has no override styles defined.
- splashElement.style.display = "none";
-
- // Array of box-shadow strings as "confetti"
- const confetti = [];
-
- // Generate random confetti
- for (let i = 0; i < CONFETTI_COUNT; i++) {
- // Random confetti position
- const x = randomIntFromInterval(CONFETTI_SCALE_PIXELS * -1, CONFETTI_SCALE_PIXELS);
- const y = randomIntFromInterval(CONFETTI_SCALE_PIXELS * -1, CONFETTI_SCALE_PIXELS);
-
- // Random confetti RGB color
- const rgb = [
- randomIntFromInterval(0, 255),
- randomIntFromInterval(0, 255),
- randomIntFromInterval(0, 255)
- ];
-
- // Interpolate random values and append to outer confetti array
- confetti.push(`${x}px ${y}px 0 rgb(${rgb.join(",")})`);
- }
-
- // Set CSS variable on splash element that in turn will be used by pseudo-element
- splashElement.style.setProperty("--confetti", confetti.join(","));
-
- // Start animation by appending the created element to the document body
- document.body.appendChild(splashElement);
-
- // Run hide animation
- setTimeout(() => {
- splashElement.classList.add("hide");
-
- // Selfdestruct element when hide animation finishes
- setTimeout(() => splashElement.remove(), 400);
- }, EMAIL_CPY_ANIM_DUR_MSECONDS + 100);
-}
-
-new vv.Interactions("index", {
- // Copy email address to clipboard
- copyEmail: async () => {
+
+ new Elevent("click", document.querySelector(".email"), async () => {
try {
await navigator.clipboard.writeText("victor@vlw.se");
-
+
// Run "email copied" animation!
emailCopiedAnimation();
-
+
// NOTE: I don't know, spamming the button is kinda fun
// Prevent interactions with the copy email elements while the animation is running
/*[...document.querySelectorAll("[vv-call='copyEmail']")].forEach(element => {
//element.classList.add("lock");
-
+
setTimeout(() => element.classList.remove("lock"), EMAIL_CPY_ANIM_DUR_MSECONDS);
});*/
} catch (error) {
console.error(error.message);
}
- },
- // Open the fullscreen menu
- openMenu: () => document.querySelector("menu").classList.add("active"),
- // Close the fullscreen menu
- closeMenu: () => document.querySelector("menu").classList.remove("active")
-});
+ });
+}
// Change site accent color on hover of menu items
-if (window.matchMedia("(hover: hover)")) {
- // Update root CSS variables
- const updateColor = (rgb = null, hue = 0) => {
- if (!rgb) {
- document.documentElement.style.removeProperty("--hue-accent");
- document.documentElement.style.removeProperty("--primer-color-accent");
- document.documentElement.style.removeProperty("--color-accent");
+{
+ if (window.matchMedia("(hover: hover)")) {
+ // Update root CSS variables
+ const updateColor = (rgb = null, hue = 0) => {
+ if (!rgb) {
+ document.documentElement.style.removeProperty("--hue-accent");
+ document.documentElement.style.removeProperty("--primer-color-accent");
+ document.documentElement.style.removeProperty("--color-accent");
- return;
- }
+ return;
+ }
- document.documentElement.style.setProperty("--hue-accent", `${hue}deg`);
+ document.documentElement.style.setProperty("--hue-accent", `${hue}deg`);
- document.documentElement.style.setProperty("--primer-color-accent", `${rgb}`);
- // Compiled color variable must to be updated to receive the new RGB values
- document.documentElement.style.setProperty("--color-accent", "rgb(var(--primer-color-accent)");
- };
+ document.documentElement.style.setProperty("--primer-color-accent", `${rgb}`);
+ // Compiled color variable must to be updated to receive the new RGB values
+ document.documentElement.style.setProperty("--color-accent", "rgb(var(--primer-color-accent)");
+ };
- [...document.querySelectorAll("menu li")].forEach(element => {
- // Change site accent color to RGB and HUE rotation defined in element dataset
- element.addEventListener("mouseenter", (event) => updateColor(event.target.dataset.rgb, event.target.dataset.hue));
- // Reset initial accent color and hues
- element.addEventListener("mouseleave", () => updateColor());
- });
+ [...document.querySelectorAll("menu li")].forEach(element => {
+ // Change site accent color to RGB and HUE rotation defined in element dataset
+ element.addEventListener("mouseenter", (event) => updateColor(event.target.dataset.rgb, event.target.dataset.hue));
+ // Reset initial accent color and hues
+ element.addEventListener("mouseleave", () => updateColor());
+ });
- // Reset color on navigation
- document.querySelector(vv._env.MAIN).addEventListener(vv.Navigation.events.LOADED, () => updateColor(), { once: true });
-}
+ // Reset color on navigation
+ vv.Navigation.rootShellElement.addEventListener(vv.Navigation.EVENTS.STARTED, () => updateColor(), { once: true });
+ }
+}
\ No newline at end of file
diff --git a/assets/js/pages/search.js b/assets/js/pages/search.js
index 14f4494..e69de29 100755
--- a/assets/js/pages/search.js
+++ b/assets/js/pages/search.js
@@ -1 +0,0 @@
-new vv.Interactions("search");
\ No newline at end of file
diff --git a/assets/js/pages/work.js b/assets/js/pages/work.js
index 723281b..e69de29 100755
--- a/assets/js/pages/work.js
+++ b/assets/js/pages/work.js
@@ -1 +0,0 @@
-new vv.Interactions("work");
\ No newline at end of file
diff --git a/assets/js/document.js b/assets/js/shells/document.js
old mode 100755
new mode 100644
similarity index 62%
rename from assets/js/document.js
rename to assets/js/shells/document.js
index 829f2bb..a573ad4
--- a/assets/js/document.js
+++ b/assets/js/shells/document.js
@@ -1,6 +1,16 @@
-new vv.Interactions("document", {
- navigateHome: () => new vv.Navigation("/").navigate(),
- closeSearchbox: () => {
+import { Elevent } from "/assets/js/._Elevent.mjs";
+
+// Handle search box open/close buttons
+{
+ // Open search box
+ new Elevent("click", document.querySelector(".searchbox-open"), () => {
+ document.querySelector("header").classList.add("searchboxActive");
+ // Select searchbox inner input element
+ document.querySelector("searchbox input").focus();
+ });
+
+ // Close searchbox
+ new Elevent("click", document.querySelector(".searchbox-close"), () => {
// Disable search button interaction while animation is running
// This is required to prevent conflicts with the :hover "peak" transformation
const searchButtonElement = document.querySelector("header button.search");
@@ -11,28 +21,27 @@ new vv.Interactions("document", {
// Wait for the transform animation to finish
setTimeout(() => searchButtonElement.style.removeProperty("pointer-events"), transformDuration);
- },
- openSearchbox: () => {
- document.querySelector("header").classList.add("searchboxActive");
- // Select searchbox inner input element
- document.querySelector("searchbox input").focus();
- }
-});
-
-// Crossfade pages on navigation
-{
- const mainElement = document.querySelector(vv._env.MAIN);
-
- mainElement.addEventListener(vv.Navigation.events.LOADING, () => {
- mainElement.classList.add("loading");
});
+}
- mainElement.addEventListener(vv.Navigation.events.LOADED, () => {
- // Close searchbox on main page navigation
+// Root shell navigation event handlers
+{
+ const classNameLoading = "loading";
+
+ // Top navigation started
+ new Elevent(vv.Navigation.EVENTS.STARTED, vv.Navigation.rootShellElement, () => {
+ vv.Navigation.rootShellElement.classList.add(classNameLoading);
+
+ // Close searchbox on vv-shell page navigation
document.querySelector("header").classList.remove("searchboxActive");
// Wait 200ms for the page fade-in animation to finish
- setTimeout(() => mainElement.classList.remove("loading"), 200);
+ setTimeout(() => vv.Navigation.rootShellElement.classList.remove("loading"), 200);
+ });
+
+ // Top navigation finished
+ new Elevent(vv.Navigation.EVENTS.FINISHED, vv.Navigation.rootShellElement, () => {
+ vv.Navigation.rootShellElement.classList.remove(classNameLoading);
});
}
diff --git a/pages/error.php b/pages/error.php
deleted file mode 100755
index 623b01a..0000000
--- a/pages/error.php
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-404
-Hi, I'm
Victor Westerlund
@@ -31,7 +31,7 @@
website version: = VV::include("pages/about/version") ?>
+website version: = VV::include("public/about/version") ?>
I'd be happy to send you any component that you find here for "free". The only thing I ask in return is that you pay for shipping.
@@ -33,4 +33,4 @@Motherboard
Case
CPU
GPU
PSU
DRAM
- = VV::media("icons/chevron.svg") ?> + = VV::embed("assets/media/icons/chevron.svg") ?>DRAM - = $dram[DramModel::TECHNOLOGY->value] ?>
Storage
- = VV::media("icons/chevron.svg") ?> + = VV::embed("assets/media/icons/chevron.svg") ?>= $storage[StorageModel::DISK_FORMFACTOR->value] ?> = $storage[StorageModel::DISK_TYPE->value] ?>
The best way to get in touch is by email, or with the form on this page. I will try to reply as quickly as possible, probably within a few hours. The time is = (new DateTime("now", new DateTimeZone($_ENV["time"]["date_time_zone"])))->format("h:i a") ?> in Sweden right now.
my key is also listed on the openPGP key server for victor@vlw.se so your e-mail client can automatically retreive it if supported.
Start typing to search
Most of my free open-source software is available on GitHub and it's also mirrored on my server