diff --git a/public/assets/css/modal.css b/public/assets/css/modal.css index 77ca2d6..e113f6f 100644 --- a/public/assets/css/modal.css +++ b/public/assets/css/modal.css @@ -4,10 +4,6 @@ body { transition: var(--transition) background-color; } -body.modalActive { - background-color: var(--color-contrast); -} - body main .screen { transition: var(--transition) transform; } @@ -22,6 +18,7 @@ body.modalActive main .screen { /* -- Boilerplate -- */ .modal { + transition: var(--transition) background-color; position: fixed; top: 0; left: 0; @@ -29,8 +26,37 @@ body.modalActive main .screen { height: 100vh; z-index: 1; pointer-events: none; + box-sizing: border-box; + background-color: rgba(var(--comp-inverted),0); + padding: var(--padding); } .modal.active { 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; } \ No newline at end of file diff --git a/public/assets/css/style.css b/public/assets/css/style.css index 0d9d814..624edab 100644 --- a/public/assets/css/style.css +++ b/public/assets/css/style.css @@ -2,10 +2,12 @@ :root { --comp-background: 255,255,255; + --comp-inverted: 0,0,0; --comp-contrast: 33,33,33; --comp-accent: 22,183,255; --color-background: rgb(var(--comp-background)); + --color-inverted: rgb(var(--comp-inverted)); --color-contrast: rgb(var(--comp-contrast)); --color-accent: rgb(var(--comp-accent)); @@ -17,10 +19,12 @@ .dark { --comp-background: 33,33,33; + --comp-inverted: 255,255,255; --comp-contrast: 255,255,255; --comp-accent: 255,255,255; --color-background: rgb(var(--comp-background)); + --color-inverted: rgb(var(--comp-inverted)); --color-contrast: rgb(var(--comp-contrast)); --color-accent: rgb(var(--comp-accent)); } @@ -228,8 +232,7 @@ header .logo { } .screen.landingpage img { - width: 100%; - max-width: 300px; + width: clamp(100px,80vw,40vh); align-self: flex-end; } diff --git a/public/assets/js/modules/Components.mjs b/public/assets/js/modules/Components.mjs new file mode 100644 index 0000000..a7395a7 --- /dev/null +++ b/public/assets/js/modules/Components.mjs @@ -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); + } +} \ No newline at end of file diff --git a/public/assets/js/modules/Debugging.mjs b/public/assets/js/modules/Debugging.mjs new file mode 100644 index 0000000..65dd09b --- /dev/null +++ b/public/assets/js/modules/Debugging.mjs @@ -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(); \ No newline at end of file diff --git a/public/assets/js/modules/Modals.mjs b/public/assets/js/modules/Modals.mjs index 1034a7d..a09bcb9 100644 --- a/public/assets/js/modules/Modals.mjs +++ b/public/assets/js/modules/Modals.mjs @@ -1,6 +1,7 @@ // Copyright © Victor Westerlund - No libraries! 😲 import { default as Interaction, destroy } from "./UI.mjs"; +import { Button } from "./Components.mjs"; // Boilerplate for creating element overlays class Modal extends Interaction { @@ -11,26 +12,19 @@ class Modal extends Interaction { this.close(); } }; + // Combine template and incoming interactions into one object interactions = Object.assign(interactions,extendedInteractions); - super(element,interactions); + super(interactions,element); this.element = this.applyTemplate(element); this.importStyleSheet(); + document.body.appendChild(this.element); } // Import the companion CSS rules importStyleSheet() { - let sheet = "css/modal.css"; - - // 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; - } + let sheet = "assets/css/modal.css"; + const element = document.createElement("link"); // Exit if the stylesheet has already been imported if(document.head.querySelector("link[data-async-modalcss]")) { @@ -38,8 +32,6 @@ class Modal extends Interaction { } // Import the stylesheet with a link tag - sheet = "assets/" + sheet; - const element = document.createElement("link"); element.setAttribute("rel","stylesheet"); element.setAttribute("href",sheet); element.setAttribute("data-async-modalcss",""); @@ -49,20 +41,24 @@ class Modal extends Interaction { // Apply a modal template to the provided element applyTemplate(element) { // The inner div will contain modal content - const inner = document.createElement("div"); - inner.classList.add("inner"); - element.appendChild(inner); + this.inner = document.createElement("div"); + this.inner.classList.add("inner"); + element.appendChild(this.inner); element.classList.add("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; } open() { 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 @@ -70,7 +66,7 @@ class Modal extends Interaction { const activeModals = document.getElementsByClassName("modal"); if(!activeModals || activeModals.length === 1) { // 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"); @@ -91,9 +87,17 @@ export class Card extends Modal { this.element.insertAdjacentHTML("beforeend",content); } - init() { + init(slim) { 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) { diff --git a/public/assets/js/modules/UI.mjs b/public/assets/js/modules/UI.mjs index 63782e1..086e2d0 100644 --- a/public/assets/js/modules/UI.mjs +++ b/public/assets/js/modules/UI.mjs @@ -2,30 +2,6 @@ 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 export function destroy(family) { while(family.firstChild) { @@ -36,18 +12,23 @@ export function destroy(family) { // General-purpose scoped event handler export default class Interaction extends Logging { - constructor(scope = document.body) { + constructor(interactions,scope) { super(); + this.interactions = interactions; this.attribute = "data-action"; // Target elements with this attribute // Bind listeners to the target attribute within the provided scope const elements = scope.querySelectorAll(`[${this.attribute}]`); 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) { const meta = document.head.querySelector("meta[name='theme-color']"); const style = getComputedStyle(document.body); @@ -67,6 +48,7 @@ export default class Interaction extends Logging { color = color.replaceAll(" ",""); } + document.body.style.setProperty("background-color",color); meta.setAttribute("content",color); } @@ -75,12 +57,12 @@ export default class Interaction extends Logging { const target = event.target.closest(`[${this.attribute}]`); 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 return false; } // Execute the function from the data-action attribute - interactions[action](event); + this.interactions[action](event); // The button has requested a theme-color change if(target.hasAttribute("data-theme-color")) { diff --git a/public/assets/js/script.js b/public/assets/js/script.js index 00abbb0..ca0f090 100644 --- a/public/assets/js/script.js +++ b/public/assets/js/script.js @@ -1,8 +1,34 @@ // Copyright © Victor Westerlund - No libraries! 😲 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 main = new Interaction(); +const main = new Interaction(interactions,document.body); function updateTheme() { const media = window.matchMedia("(prefers-color-scheme: dark)");