mirror of
https://codeberg.org/vlw/victorwesterlund.com.git
synced 2025-09-14 11:33:41 +02:00
dev21w36a
This commit is contained in:
parent
6d31cd8e0d
commit
ff474e1786
7 changed files with 155 additions and 58 deletions
|
@ -4,10 +4,6 @@ body {
|
||||||
transition: var(--transition) background-color;
|
transition: var(--transition) background-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.modalActive {
|
|
||||||
background-color: var(--color-contrast);
|
|
||||||
}
|
|
||||||
|
|
||||||
body main .screen {
|
body main .screen {
|
||||||
transition: var(--transition) transform;
|
transition: var(--transition) transform;
|
||||||
}
|
}
|
||||||
|
@ -22,6 +18,7 @@ body.modalActive main .screen {
|
||||||
/* -- Boilerplate -- */
|
/* -- Boilerplate -- */
|
||||||
|
|
||||||
.modal {
|
.modal {
|
||||||
|
transition: var(--transition) background-color;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
@ -29,8 +26,37 @@ body.modalActive main .screen {
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background-color: rgba(var(--comp-inverted),0);
|
||||||
|
padding: var(--padding);
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal.active {
|
.modal.active {
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
|
background-color: rgba(var(--comp-inverted),.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal .button {
|
||||||
|
background-color: rgba(var(--comp-inverted),.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -- Cards -- */
|
||||||
|
|
||||||
|
.modal.card .inner {
|
||||||
|
transition: var(--transition) transform, var(--transition) opacity;
|
||||||
|
position: relative;
|
||||||
|
background-color: var(--color-contrast);
|
||||||
|
width: calc(100vw - var(--padding));
|
||||||
|
max-width: 600px;
|
||||||
|
align-self: flex-end;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: var(--padding);
|
||||||
|
transform: translateY(1vh);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal.card.active .inner {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
|
@ -2,10 +2,12 @@
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--comp-background: 255,255,255;
|
--comp-background: 255,255,255;
|
||||||
|
--comp-inverted: 0,0,0;
|
||||||
--comp-contrast: 33,33,33;
|
--comp-contrast: 33,33,33;
|
||||||
--comp-accent: 22,183,255;
|
--comp-accent: 22,183,255;
|
||||||
|
|
||||||
--color-background: rgb(var(--comp-background));
|
--color-background: rgb(var(--comp-background));
|
||||||
|
--color-inverted: rgb(var(--comp-inverted));
|
||||||
--color-contrast: rgb(var(--comp-contrast));
|
--color-contrast: rgb(var(--comp-contrast));
|
||||||
--color-accent: rgb(var(--comp-accent));
|
--color-accent: rgb(var(--comp-accent));
|
||||||
|
|
||||||
|
@ -17,10 +19,12 @@
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--comp-background: 33,33,33;
|
--comp-background: 33,33,33;
|
||||||
|
--comp-inverted: 255,255,255;
|
||||||
--comp-contrast: 255,255,255;
|
--comp-contrast: 255,255,255;
|
||||||
--comp-accent: 255,255,255;
|
--comp-accent: 255,255,255;
|
||||||
|
|
||||||
--color-background: rgb(var(--comp-background));
|
--color-background: rgb(var(--comp-background));
|
||||||
|
--color-inverted: rgb(var(--comp-inverted));
|
||||||
--color-contrast: rgb(var(--comp-contrast));
|
--color-contrast: rgb(var(--comp-contrast));
|
||||||
--color-accent: rgb(var(--comp-accent));
|
--color-accent: rgb(var(--comp-accent));
|
||||||
}
|
}
|
||||||
|
@ -228,8 +232,7 @@ header .logo {
|
||||||
}
|
}
|
||||||
|
|
||||||
.screen.landingpage img {
|
.screen.landingpage img {
|
||||||
width: 100%;
|
width: clamp(100px,80vw,40vh);
|
||||||
max-width: 300px;
|
|
||||||
align-self: flex-end;
|
align-self: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
34
public/assets/js/modules/Components.mjs
Normal file
34
public/assets/js/modules/Components.mjs
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
// Copyright © Victor Westerlund - No libraries! 😲
|
||||||
|
|
||||||
|
// UI component constructor
|
||||||
|
class Component {
|
||||||
|
constructor(tag) {
|
||||||
|
this.element = document.createElement(tag); // Root element
|
||||||
|
}
|
||||||
|
|
||||||
|
getElement() {
|
||||||
|
return this.element;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ⬇ UI Components ⬇
|
||||||
|
|
||||||
|
export class Button extends Component {
|
||||||
|
constructor(properties) {
|
||||||
|
super("div");
|
||||||
|
this.element.classList.add("button");
|
||||||
|
|
||||||
|
this.setText(properties.text);
|
||||||
|
this.setAction(properties.action);
|
||||||
|
}
|
||||||
|
|
||||||
|
setText(text) {
|
||||||
|
const textElement = document.createElement("p");
|
||||||
|
textElement.innerText = text;
|
||||||
|
this.element.appendChild(textElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
setAction(action) {
|
||||||
|
this.element.setAttribute("data-action",action);
|
||||||
|
}
|
||||||
|
}
|
22
public/assets/js/modules/Debugging.mjs
Normal file
22
public/assets/js/modules/Debugging.mjs
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
// Copyright © Victor Westerlund - No libraries! 😲
|
||||||
|
|
||||||
|
class Debug {
|
||||||
|
constructor() {
|
||||||
|
console.log("Debug mode is enabled.\nList debug functions by running window._debug.list()");
|
||||||
|
}
|
||||||
|
|
||||||
|
list() {
|
||||||
|
const functions = [
|
||||||
|
"list",
|
||||||
|
"openContactsModal"
|
||||||
|
];
|
||||||
|
console.log("Available functions:",functions.map(f => `window._debug.${f}();`));
|
||||||
|
}
|
||||||
|
|
||||||
|
openContactsModal() {
|
||||||
|
document.getElementsByClassName("hamburger")[0].click();
|
||||||
|
document.querySelector("div[data-action='openContactCard']").click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default window._debug = new Debug();
|
|
@ -1,6 +1,7 @@
|
||||||
// Copyright © Victor Westerlund - No libraries! 😲
|
// Copyright © Victor Westerlund - No libraries! 😲
|
||||||
|
|
||||||
import { default as Interaction, destroy } from "./UI.mjs";
|
import { default as Interaction, destroy } from "./UI.mjs";
|
||||||
|
import { Button } from "./Components.mjs";
|
||||||
|
|
||||||
// Boilerplate for creating element overlays
|
// Boilerplate for creating element overlays
|
||||||
class Modal extends Interaction {
|
class Modal extends Interaction {
|
||||||
|
@ -11,26 +12,19 @@ class Modal extends Interaction {
|
||||||
this.close();
|
this.close();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
// Combine template and incoming interactions into one object
|
||||||
interactions = Object.assign(interactions,extendedInteractions);
|
interactions = Object.assign(interactions,extendedInteractions);
|
||||||
super(element,interactions);
|
super(interactions,element);
|
||||||
|
|
||||||
this.element = this.applyTemplate(element);
|
this.element = this.applyTemplate(element);
|
||||||
this.importStyleSheet();
|
this.importStyleSheet();
|
||||||
|
document.body.appendChild(this.element);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Import the companion CSS rules
|
// Import the companion CSS rules
|
||||||
importStyleSheet() {
|
importStyleSheet() {
|
||||||
let sheet = "css/modal.css";
|
let sheet = "assets/css/modal.css";
|
||||||
|
const element = document.createElement("link");
|
||||||
// Import stylesheet with CSS module script if supported
|
|
||||||
if(document.adoptedStyleSheets) {
|
|
||||||
sheet = "../../" + sheet;
|
|
||||||
const module = import(sheet, {
|
|
||||||
assert: { type: "css" }
|
|
||||||
});
|
|
||||||
module.then(style => document.adoptedStyleSheets = [style.default]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exit if the stylesheet has already been imported
|
// Exit if the stylesheet has already been imported
|
||||||
if(document.head.querySelector("link[data-async-modalcss]")) {
|
if(document.head.querySelector("link[data-async-modalcss]")) {
|
||||||
|
@ -38,8 +32,6 @@ class Modal extends Interaction {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Import the stylesheet with a link tag
|
// Import the stylesheet with a link tag
|
||||||
sheet = "assets/" + sheet;
|
|
||||||
const element = document.createElement("link");
|
|
||||||
element.setAttribute("rel","stylesheet");
|
element.setAttribute("rel","stylesheet");
|
||||||
element.setAttribute("href",sheet);
|
element.setAttribute("href",sheet);
|
||||||
element.setAttribute("data-async-modalcss","");
|
element.setAttribute("data-async-modalcss","");
|
||||||
|
@ -49,20 +41,24 @@ class Modal extends Interaction {
|
||||||
// Apply a modal template to the provided element
|
// Apply a modal template to the provided element
|
||||||
applyTemplate(element) {
|
applyTemplate(element) {
|
||||||
// The inner div will contain modal content
|
// The inner div will contain modal content
|
||||||
const inner = document.createElement("div");
|
this.inner = document.createElement("div");
|
||||||
inner.classList.add("inner");
|
this.inner.classList.add("inner");
|
||||||
element.appendChild(inner);
|
element.appendChild(this.inner);
|
||||||
element.classList.add("modal");
|
element.classList.add("modal");
|
||||||
|
|
||||||
// PointerEvents on the outer div will close the modal
|
// PointerEvents on the outer div will close the modal
|
||||||
element.addEventListener("click",() => this.close());
|
element.addEventListener("click",event => {
|
||||||
|
if(event.target == element) {
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
|
|
||||||
open() {
|
open() {
|
||||||
document.body.classList.add("modalActive");
|
document.body.classList.add("modalActive");
|
||||||
this.element.classList.add("active");
|
setTimeout(() => this.element.classList.add("active"),this.transition / 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close the modal and remove it from the DOM
|
// Close the modal and remove it from the DOM
|
||||||
|
@ -70,7 +66,7 @@ class Modal extends Interaction {
|
||||||
const activeModals = document.getElementsByClassName("modal");
|
const activeModals = document.getElementsByClassName("modal");
|
||||||
if(!activeModals || activeModals.length === 1) {
|
if(!activeModals || activeModals.length === 1) {
|
||||||
// Remove active effects if all modals have been closed
|
// Remove active effects if all modals have been closed
|
||||||
document.body.classList.remove("modalActive");
|
setTimeout(() => document.body.classList.remove("modalActive"),this.transition / 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.element.classList.remove("active");
|
this.element.classList.remove("active");
|
||||||
|
@ -91,9 +87,17 @@ export class Card extends Modal {
|
||||||
this.element.insertAdjacentHTML("beforeend",content);
|
this.element.insertAdjacentHTML("beforeend",content);
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init(slim) {
|
||||||
this.element.classList.add("card");
|
this.element.classList.add("card");
|
||||||
document.body.appendChild(this.element);
|
this.element.classList.add("center");
|
||||||
|
const closeButton = new Button({
|
||||||
|
text: "close",
|
||||||
|
action: "close"
|
||||||
|
});
|
||||||
|
const closeButtonElement = closeButton.getElement();
|
||||||
|
|
||||||
|
this.bind(closeButtonElement);
|
||||||
|
this.inner.appendChild(closeButtonElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
openPage(page) {
|
openPage(page) {
|
||||||
|
|
|
@ -2,30 +2,6 @@
|
||||||
|
|
||||||
import { default as Logging } from "./Logging.mjs";
|
import { default as Logging } from "./Logging.mjs";
|
||||||
|
|
||||||
const interactions = {
|
|
||||||
toggleMenu: () => {
|
|
||||||
const speed = 200;
|
|
||||||
const menu = document.getElementsByTagName("main")[0];
|
|
||||||
|
|
||||||
menu.style.setProperty("transition",`${speed}ms`);
|
|
||||||
menu.classList.toggle("active");
|
|
||||||
setTimeout(() => menu.style.removeProperty("transition"),speed + 1);
|
|
||||||
},
|
|
||||||
openContactCard: () => {
|
|
||||||
const module = import("./Modals.mjs");
|
|
||||||
const interactions = {
|
|
||||||
hello: () => {
|
|
||||||
console.log("Hello world");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
module.then(modals => {
|
|
||||||
const card = new modals.Card(interactions);
|
|
||||||
card.openPage("contact_card");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove an element and its subtree
|
// Remove an element and its subtree
|
||||||
export function destroy(family) {
|
export function destroy(family) {
|
||||||
while(family.firstChild) {
|
while(family.firstChild) {
|
||||||
|
@ -36,18 +12,23 @@ export function destroy(family) {
|
||||||
|
|
||||||
// General-purpose scoped event handler
|
// General-purpose scoped event handler
|
||||||
export default class Interaction extends Logging {
|
export default class Interaction extends Logging {
|
||||||
constructor(scope = document.body) {
|
constructor(interactions,scope) {
|
||||||
super();
|
super();
|
||||||
|
this.interactions = interactions;
|
||||||
this.attribute = "data-action"; // Target elements with this attribute
|
this.attribute = "data-action"; // Target elements with this attribute
|
||||||
|
|
||||||
// Bind listeners to the target attribute within the provided scope
|
// Bind listeners to the target attribute within the provided scope
|
||||||
const elements = scope.querySelectorAll(`[${this.attribute}]`);
|
const elements = scope.querySelectorAll(`[${this.attribute}]`);
|
||||||
for(const element of elements) {
|
for(const element of elements) {
|
||||||
element.addEventListener("click",event => this.pointerEvent(event));
|
this.bind(element);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the theme-color for Chrome on Android devices
|
bind(element) {
|
||||||
|
element.addEventListener("click",event => this.pointerEvent(event));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the page theme color (and the theme-color meta tag)
|
||||||
setThemeColor(color) {
|
setThemeColor(color) {
|
||||||
const meta = document.head.querySelector("meta[name='theme-color']");
|
const meta = document.head.querySelector("meta[name='theme-color']");
|
||||||
const style = getComputedStyle(document.body);
|
const style = getComputedStyle(document.body);
|
||||||
|
@ -67,6 +48,7 @@ export default class Interaction extends Logging {
|
||||||
color = color.replaceAll(" ","");
|
color = color.replaceAll(" ","");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document.body.style.setProperty("background-color",color);
|
||||||
meta.setAttribute("content",color);
|
meta.setAttribute("content",color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,12 +57,12 @@ export default class Interaction extends Logging {
|
||||||
const target = event.target.closest(`[${this.attribute}]`);
|
const target = event.target.closest(`[${this.attribute}]`);
|
||||||
const action = target?.getAttribute(this.attribute) ?? null;
|
const action = target?.getAttribute(this.attribute) ?? null;
|
||||||
|
|
||||||
if(!target || !action || !Object.keys(interactions).includes(action)) {
|
if(!target || !action || !Object.keys(this.interactions).includes(action)) {
|
||||||
// Exit if the interaction is invalid or action doesn't exist
|
// Exit if the interaction is invalid or action doesn't exist
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// Execute the function from the data-action attribute
|
// Execute the function from the data-action attribute
|
||||||
interactions[action](event);
|
this.interactions[action](event);
|
||||||
|
|
||||||
// The button has requested a theme-color change
|
// The button has requested a theme-color change
|
||||||
if(target.hasAttribute("data-theme-color")) {
|
if(target.hasAttribute("data-theme-color")) {
|
||||||
|
|
|
@ -1,8 +1,34 @@
|
||||||
// Copyright © Victor Westerlund - No libraries! 😲
|
// Copyright © Victor Westerlund - No libraries! 😲
|
||||||
import { default as Interaction } from "./modules/UI.mjs";
|
import { default as Interaction } from "./modules/UI.mjs";
|
||||||
|
import { default as Debug } from "./modules/Debugging.mjs";
|
||||||
|
|
||||||
|
// All default interactions
|
||||||
|
const interactions = {
|
||||||
|
toggleMenu: () => {
|
||||||
|
const speed = 200;
|
||||||
|
const menu = document.getElementsByTagName("main")[0];
|
||||||
|
|
||||||
|
menu.style.setProperty("transition",`${speed}ms`);
|
||||||
|
menu.classList.toggle("active");
|
||||||
|
setTimeout(() => menu.style.removeProperty("transition"),speed + 1);
|
||||||
|
},
|
||||||
|
openContactCard: () => {
|
||||||
|
const module = import("./modules/Modals.mjs");
|
||||||
|
const interactions = {
|
||||||
|
hello: () => {
|
||||||
|
console.log("Hello world");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.then(modals => {
|
||||||
|
const card = new modals.Card(interactions);
|
||||||
|
card.openPage("contact_card");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const theme = window.matchMedia("(prefers-color-scheme: dark)");
|
const theme = window.matchMedia("(prefers-color-scheme: dark)");
|
||||||
const main = new Interaction();
|
const main = new Interaction(interactions,document.body);
|
||||||
|
|
||||||
function updateTheme() {
|
function updateTheme() {
|
||||||
const media = window.matchMedia("(prefers-color-scheme: dark)");
|
const media = window.matchMedia("(prefers-color-scheme: dark)");
|
||||||
|
|
Loading…
Add table
Reference in a new issue